diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 68750e0..06ff298 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -1,4 +1,5 @@ local comms = require("scada-common.comms") +local const = require("scada-common.constants") local log = require("scada-common.log") local ppm = require("scada-common.ppm") local types = require("scada-common.types") @@ -15,6 +16,8 @@ local RPLC_TYPE = comms.RPLC_TYPE local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE local AUTO_ACK = comms.PLC_AUTO_ACK +local RPS_LIMITS = const.RPS_LIMITS + local print = util.print local println = util.println local print_ts = util.print_ts @@ -25,16 +28,6 @@ local println_ts = util.println_ts local PCALL_SCRAM_MSG = "pcall: Scram requires the reactor to be active." local PCALL_START_MSG = "pcall: Reactor is already active." ---#region RPS SAFETY CONSTANTS - -local MAX_DAMAGE_PERCENT = 90 -local MAX_DAMAGE_TEMPERATURE = 1200 -local MIN_COOLANT_FILL = 0.10 -local MAX_WASTE_FILL = 0.8 -local MAX_HEATED_COLLANT_FILL = 0.95 - ---#endregion END RPS SAFETY CONSTANTS - -- RPS: Reactor Protection System
-- identifies dangerous states and SCRAMs reactor if warranted
-- autonomous from main SCADA supervisor/coordinator control @@ -118,7 +111,7 @@ function plc.rps_init(reactor, is_formed) -- lost the peripheral or terminated, handled later _set_fault() elseif not self.state[state_keys.dmg_crit] then - self.state[state_keys.dmg_crit] = damage_percent >= MAX_DAMAGE_PERCENT + self.state[state_keys.dmg_crit] = damage_percent >= RPS_LIMITS.MAX_DAMAGE_PERCENT end end @@ -130,7 +123,7 @@ function plc.rps_init(reactor, is_formed) -- lost the peripheral or terminated, handled later _set_fault() elseif not self.state[state_keys.high_temp] then - self.state[state_keys.high_temp] = temp >= MAX_DAMAGE_TEMPERATURE + self.state[state_keys.high_temp] = temp >= RPS_LIMITS.MAX_DAMAGE_TEMPERATURE end end @@ -141,7 +134,7 @@ function plc.rps_init(reactor, is_formed) -- lost the peripheral or terminated, handled later _set_fault() elseif not self.state[state_keys.no_coolant] then - self.state[state_keys.no_coolant] = coolant_filled < MIN_COOLANT_FILL + self.state[state_keys.no_coolant] = coolant_filled < RPS_LIMITS.MIN_COOLANT_FILL end end @@ -152,7 +145,7 @@ function plc.rps_init(reactor, is_formed) -- lost the peripheral or terminated, handled later _set_fault() elseif not self.state[state_keys.ex_waste] then - self.state[state_keys.ex_waste] = w_filled > MAX_WASTE_FILL + self.state[state_keys.ex_waste] = w_filled > RPS_LIMITS.MAX_WASTE_FILL end end @@ -163,7 +156,7 @@ function plc.rps_init(reactor, is_formed) -- lost the peripheral or terminated, handled later _set_fault() elseif not self.state[state_keys.ex_hcoolant] then - self.state[state_keys.ex_hcoolant] = hc_filled > MAX_HEATED_COLLANT_FILL + self.state[state_keys.ex_hcoolant] = hc_filled > RPS_LIMITS.MAX_HEATED_COLLANT_FILL end end @@ -174,7 +167,7 @@ function plc.rps_init(reactor, is_formed) -- lost the peripheral or terminated, handled later _set_fault() elseif not self.state[state_keys.no_fuel] then - self.state[state_keys.no_fuel] = fuel == 0 + self.state[state_keys.no_fuel] = fuel <= RPS_LIMITS.NO_FUEL_FILL end end diff --git a/scada-common/constants.lua b/scada-common/constants.lua new file mode 100644 index 0000000..c10722d --- /dev/null +++ b/scada-common/constants.lua @@ -0,0 +1,71 @@ +-- +-- System and Safety Constants +-- + +-- Notes on Radiation +-- - background radiation 0.0000001 Sv/h (99.99 nSv/h) +-- - "green tint" radiation 0.00001 Sv/h (10 uSv/h) +-- - damaging radiation 0.00006 Sv/h (60 uSv/h) + +local constants = {} + +--#region Reactor Protection System (on the PLC) Limits + +local rps = {} + +rps.MAX_DAMAGE_PERCENT = 90 -- damage >= 90% +rps.MAX_DAMAGE_TEMPERATURE = 1200 -- temp >= 1200K +rps.MIN_COOLANT_FILL = 0.10 -- fill < 10% +rps.MAX_WASTE_FILL = 0.8 -- fill > 80% +rps.MAX_HEATED_COLLANT_FILL = 0.95 -- fill > 95% +rps.NO_FUEL_FILL = 0.0 -- fill <= 0% + +constants.RPS_LIMITS = rps + +--#endregion + +--#region Annunciator Limits + +local annunc = {} + +annunc.RCSFlowLow = -2.0 -- flow < -2.0 mB/s +annunc.CoolantLevelLow = 0.4 -- fill < 40% +annunc.ReactorTempHigh = 1000 -- temp > 1000K +annunc.ReactorHighDeltaT = 50 -- rate > 50K/s +annunc.FuelLevelLow = 0.05 -- fill <= 5% +annunc.WasteLevelHigh = 0.85 -- fill >= 85% +annunc.SteamFeedMismatch = 10 -- ±10mB difference between total coolant flow and total steam input rate +annunc.RadiationWarning = 0.00001 -- 10 uSv/h + +constants.ANNUNCIATOR_LIMITS = annunc + +--#endregion + +--#region Supervisor Alarm Limits + +local alarms = {} + +-- unit alarms + +alarms.HIGH_TEMP = 1150 -- temp >= 1150K +alarms.HIGH_WASTE = 0.5 -- fill > 50% +alarms.HIGH_RADIATION = 0.00005 -- 50 uSv/h, not yet damaging but this isn't good + +-- facility alarms + +alarms.CHARGE_HIGH = 1.0 -- once at or above 100% charge +alarms.CHARGE_RE_ENABLE = 0.95 -- once below 95% charge +alarms.FAC_HIGH_RAD = 0.00001 -- 10 uSv/h + +constants.ALARM_LIMITS = alarms + +--#endregion + +--#region Supervisor Constants + +-- milliseconds until turbine flow is assumed to be stable enough to enable coolant checks +constants.FLOW_STABILITY_DELAY_MS = 15000 + +--#endregion + +return constants diff --git a/supervisor/facility.lua b/supervisor/facility.lua index 21da1a1..106c4d7 100644 --- a/supervisor/facility.lua +++ b/supervisor/facility.lua @@ -1,3 +1,4 @@ +local const = require("scada-common.constants") local log = require("scada-common.log") local rsio = require("scada-common.rsio") local types = require("scada-common.types") @@ -16,15 +17,9 @@ local IO = rsio.IO -- 2856 FE per blade per 1 mB, 285.6 FE per blade per 0.1 mB (minimum) local POWER_PER_BLADE = util.joules_to_fe(7140) -local FLOW_STABILITY_DELAY_S = unit.FLOW_STABILITY_DELAY_MS / 1000 +local FLOW_STABILITY_DELAY_S = const.FLOW_STABILITY_DELAY_MS / 1000 --- background radiation 0.0000001 Sv/h (99.99 nSv/h) --- "green tint" radiation 0.00001 Sv/h (10 uSv/h) --- damaging radiation 0.00006 Sv/h (60 uSv/h) -local RADIATION_ALARM_LEVEL = 0.00001 - -local HIGH_CHARGE = 1.0 -local RE_ENABLE_CHARGE = 0.95 +local ALARM_LIMS = const.ALARM_LIMITS local AUTO_SCRAM = { NONE = 0, @@ -563,10 +558,10 @@ function facility.new(num_reactors, cooling_conf) -- check matrix fill too high local was_fill = astatus.matrix_fill - astatus.matrix_fill = (db.tanks.energy_fill >= HIGH_CHARGE) or (astatus.matrix_fill and db.tanks.energy_fill > RE_ENABLE_CHARGE) + astatus.matrix_fill = (db.tanks.energy_fill >= ALARM_LIMS.CHARGE_HIGH) or (astatus.matrix_fill and db.tanks.energy_fill > ALARM_LIMS.CHARGE_RE_ENABLE) if was_fill and not astatus.matrix_fill then - log.info("FAC: charge state of induction matrix entered acceptable range <= " .. (RE_ENABLE_CHARGE * 100) .. "%") + log.info("FAC: charge state of induction matrix entered acceptable range <= " .. (ALARM_LIMS.CHARGE_RE_ENABLE * 100) .. "%") end -- check for critical unit alarms @@ -585,7 +580,7 @@ function facility.new(num_reactors, cooling_conf) local envd = self.envd[1] ---@type unit_session local e_db = envd.get_db() ---@type envd_session_db - astatus.radiation = e_db.radiation_raw > RADIATION_ALARM_LEVEL + astatus.radiation = e_db.radiation_raw > ALARM_LIMS.FAC_HIGH_RAD else -- don't clear, if it is true then we lost it with high radiation, so just keep alarming -- operator can restart the system or hit the stop/reset button diff --git a/supervisor/unit.lua b/supervisor/unit.lua index b79036e..c3f9482 100644 --- a/supervisor/unit.lua +++ b/supervisor/unit.lua @@ -12,8 +12,6 @@ local rsctl = require("supervisor.session.rsctl") local unit = {} local WASTE_MODE = types.WASTE_MODE -local DUMPING_MODE = types.DUMPING_MODE - local ALARM = types.ALARM local PRIO = types.ALARM_PRIORITY local ALARM_STATE = types.ALARM_STATE @@ -23,8 +21,6 @@ local PLC_S_CMDS = plc.PLC_S_CMDS local IO = rsio.IO -local FLOW_STABILITY_DELAY_MS = 15000 - local DT_KEYS = { ReactorBurnR = "RBR", ReactorTemp = "RTP", @@ -50,8 +46,6 @@ local AISTATE = { RING_BACK_TRIPPING = 6 } -unit.FLOW_STABILITY_DELAY_MS = FLOW_STABILITY_DELAY_MS - ---@class alarm_def ---@field state ALARM_INT_STATE internal alarm state ---@field trip_time integer time (ms) when first tripped @@ -73,7 +67,6 @@ function unit.new(reactor_id, num_boilers, num_turbines) num_boilers = num_boilers, num_turbines = num_turbines, types = { DT_KEYS = DT_KEYS, AISTATE = AISTATE }, - defs = { FLOW_STABILITY_DELAY_MS = FLOW_STABILITY_DELAY_MS }, -- rtus redstone = {}, boilers = {}, diff --git a/supervisor/unitlogic.lua b/supervisor/unitlogic.lua index 1e701a8..3a55c82 100644 --- a/supervisor/unitlogic.lua +++ b/supervisor/unitlogic.lua @@ -1,3 +1,4 @@ +local const = require("scada-common.constants") local log = require("scada-common.log") local rsio = require("scada-common.rsio") local types = require("scada-common.types") @@ -5,11 +6,10 @@ local util = require("scada-common.util") local plc = require("supervisor.session.plc") -local TRI_FAIL = types.TRI_FAIL +local TRI_FAIL = types.TRI_FAIL local DUMPING_MODE = types.DUMPING_MODE - -local PRIO = types.ALARM_PRIORITY -local ALARM_STATE = types.ALARM_STATE +local PRIO = types.ALARM_PRIORITY +local ALARM_STATE = types.ALARM_STATE local IO = rsio.IO @@ -24,11 +24,10 @@ local AISTATE_NAMES = { "RING_BACK_TRIPPING" } --- background radiation 0.0000001 Sv/h (99.99 nSv/h) --- "green tint" radiation 0.00001 Sv/h (10 uSv/h) --- damaging radiation 0.00006 Sv/h (60 uSv/h) -local RADIATION_ALERT_LEVEL = 0.00001 -- 10 uSv/h -local RADIATION_ALARM_LEVEL = 0.00005 -- 50 uSv/h, not yet damaging but this isn't good +local FLOW_STABILITY_DELAY_MS = const.FLOW_STABILITY_DELAY_MS + +local ANNUNC_LIMS = const.ANNUNCIATOR_LIMITS +local ALARM_LIMS = const.ALARM_LIMITS ---@class unit_logic_extension local logic = {} @@ -111,12 +110,12 @@ function logic.update_annunciator(self) self.db.annunciator.ManualReactorSCRAM = plc_db.rps_trip_cause == types.RPS_TRIP_CAUSE.MANUAL self.db.annunciator.AutoReactorSCRAM = plc_db.rps_trip_cause == types.RPS_TRIP_CAUSE.AUTOMATIC self.db.annunciator.RCPTrip = plc_db.rps_tripped and (plc_db.rps_status.ex_hcool or plc_db.rps_status.no_cool) - self.db.annunciator.RCSFlowLow = _get_dt(DT_KEYS.ReactorCCool) < -2.0 - self.db.annunciator.CoolantLevelLow = plc_db.mek_status.ccool_fill < 0.4 - self.db.annunciator.ReactorTempHigh = plc_db.mek_status.temp > 1000 - self.db.annunciator.ReactorHighDeltaT = _get_dt(DT_KEYS.ReactorTemp) > 100 - self.db.annunciator.FuelInputRateLow = _get_dt(DT_KEYS.ReactorFuel) < -1.0 or plc_db.mek_status.fuel_fill <= 0.01 - self.db.annunciator.WasteLineOcclusion = _get_dt(DT_KEYS.ReactorWaste) > 1.0 or plc_db.mek_status.waste_fill >= 0.85 + self.db.annunciator.RCSFlowLow = _get_dt(DT_KEYS.ReactorCCool) < ANNUNC_LIMS.RCSFlowLow + self.db.annunciator.CoolantLevelLow = plc_db.mek_status.ccool_fill < ANNUNC_LIMS.CoolantLevelLow + self.db.annunciator.ReactorTempHigh = plc_db.mek_status.temp > ANNUNC_LIMS.ReactorTempHigh + self.db.annunciator.ReactorHighDeltaT = _get_dt(DT_KEYS.ReactorTemp) > ANNUNC_LIMS.ReactorHighDeltaT + self.db.annunciator.FuelInputRateLow = _get_dt(DT_KEYS.ReactorFuel) < -1.0 or plc_db.mek_status.fuel_fill <= ANNUNC_LIMS.FuelLevelLow + self.db.annunciator.WasteLineOcclusion = _get_dt(DT_KEYS.ReactorWaste) > 1.0 or plc_db.mek_status.waste_fill >= ANNUNC_LIMS.WasteLevelHigh -- this warning applies when no coolant is buffered (which we can't easily determine without running) --[[ @@ -150,7 +149,7 @@ function logic.update_annunciator(self) for i = 1, #self.envd do local envd = self.envd[i] ---@type unit_session self.db.annunciator.RadiationMonitor = util.trinary(envd.is_faulted(), 2, 3) - self.db.annunciator.RadiationWarning = envd.get_db().radiation_raw > RADIATION_ALERT_LEVEL + self.db.annunciator.RadiationWarning = envd.get_db().radiation_raw > ANNUNC_LIMS.RadiationWarning break end @@ -299,7 +298,7 @@ function logic.update_annunciator(self) self.db.annunciator.BoilRateMismatch = math.abs(total_boil_rate - total_input_rate) > (0.04 * total_boil_rate) -- check for steam feed mismatch and max return rate - local sfmismatch = math.abs(total_flow_rate - total_input_rate) > 10 + local sfmismatch = math.abs(total_flow_rate - total_input_rate) > ANNUNC_LIMS.SteamFeedMismatch sfmismatch = sfmismatch or boiler_steam_dt_sum > 2.0 or boiler_water_dt_sum < -2.0 self.db.annunciator.SteamFeedMismatch = sfmismatch self.db.annunciator.MaxWaterReturnFeed = max_water_return_rate == total_flow_rate and total_flow_rate ~= 0 @@ -449,7 +448,7 @@ function logic.update_alarms(self) -- Containment Radiation local rad_alarm = false for i = 1, #self.envd do - rad_alarm = self.envd[i].get_db().radiation_raw > RADIATION_ALARM_LEVEL + rad_alarm = self.envd[i].get_db().radiation_raw > ALARM_LIMS.HIGH_RADIATION break end _update_alarm_state(self, rad_alarm, self.alarms.ContainmentRadiation) @@ -469,14 +468,14 @@ function logic.update_alarms(self) _update_alarm_state(self, (plc_cache.temp >= 1200) or rps_high_temp, self.alarms.ReactorOverTemp) -- High Temperature - _update_alarm_state(self, plc_cache.temp > 1150, self.alarms.ReactorHighTemp) + _update_alarm_state(self, plc_cache.temp >= ALARM_LIMS.HIGH_TEMP, self.alarms.ReactorHighTemp) -- Waste Leak - _update_alarm_state(self, plc_cache.waste >= 0.99, self.alarms.ReactorWasteLeak) + _update_alarm_state(self, plc_cache.waste >= 1.0, self.alarms.ReactorWasteLeak) -- High Waste local rps_high_waste = plc_cache.rps_status.ex_waste and not self.last_rps_trips.ex_waste - _update_alarm_state(self, (plc_cache.waste > 0.50) or rps_high_waste, self.alarms.ReactorHighWaste) + _update_alarm_state(self, (plc_cache.waste > ALARM_LIMS.HIGH_WASTE) or rps_high_waste, self.alarms.ReactorHighWaste) -- RPS Transient (excludes timeouts and manual trips) local rps_alarm = false @@ -501,7 +500,7 @@ function logic.update_alarms(self) -- annunciator indicators for these states may not indicate a real issue when: -- > flow is ramping up right after reactor start -- > flow is ramping down after reactor shutdown - if ((util.time_ms() - self.last_rate_change_ms) > self.defs.FLOW_STABILITY_DELAY_MS) and plc_cache.active then + if ((util.time_ms() - self.last_rate_change_ms) > FLOW_STABILITY_DELAY_MS) and plc_cache.active then rcs_trans = rcs_trans or annunc.BoilRateMismatch or annunc.CoolantFeedMismatch or annunc.SteamFeedMismatch end @@ -621,7 +620,7 @@ function logic.update_status_text(self) self.status_text[2] = "insufficient fuel input rate" elseif self.db.annunciator.WasteLineOcclusion then self.status_text[2] = "insufficient waste output rate" - elseif (util.time_ms() - self.last_rate_change_ms) <= self.defs.FLOW_STABILITY_DELAY_MS then + elseif (util.time_ms() - self.last_rate_change_ms) <= FLOW_STABILITY_DELAY_MS then self.status_text[2] = "awaiting flow stability" else self.status_text[2] = "system nominal"