#143 #103 #101 #102 work in progress auto control, added coordinator controls, save/auto load configuration, auto enable/disable on reactor PLC for auto control (untested)

This commit is contained in:
Mikayla Fischler
2023-01-26 18:26:26 -05:00
parent e808ee2be0
commit e9562a140c
17 changed files with 750 additions and 161 deletions

View File

@@ -11,6 +11,7 @@ local PROTOCOLS = comms.PROTOCOLS
local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES
local SCADA_CRDN_TYPES = comms.SCADA_CRDN_TYPES
local UNIT_COMMANDS = comms.UNIT_COMMANDS
local FAC_COMMANDS = comms.FAC_COMMANDS
local SV_Q_CMDS = svqtypes.SV_Q_CMDS
local SV_Q_DATA = svqtypes.SV_Q_DATA
@@ -133,6 +134,7 @@ function coordinator.new_session(id, in_queue, out_queue, facility)
-- send facility status
local function _send_fac_status()
local status = {
facility.get_control_status(),
facility.get_rtu_statuses()
}
@@ -146,9 +148,7 @@ function coordinator.new_session(id, in_queue, out_queue, facility)
for i = 1, #self.units do
local unit = self.units[i] ---@type reactor_unit
local auto_ctl = {
unit.get_control_inf().lim_br10 / 10
}
local auto_ctl = {}
status[unit.get_id()] = {
unit.get_reactor_status(),
@@ -208,6 +208,37 @@ function coordinator.new_session(id, in_queue, out_queue, facility)
if pkt.type == SCADA_CRDN_TYPES.FAC_BUILDS then
-- acknowledgement to coordinator receiving builds
self.acks.fac_builds = true
elseif pkt.type == SCADA_CRDN_TYPES.FAC_CMD then
if pkt.length >= 1 then
local cmd = pkt.data[1]
if cmd == FAC_COMMANDS.SCRAM_ALL then
facility.scram_all()
_send(SCADA_CRDN_TYPES.FAC_CMD, { cmd, true })
elseif cmd == FAC_COMMANDS.STOP then
facility.auto_stop()
_send(SCADA_CRDN_TYPES.FAC_CMD, { cmd, true })
elseif cmd == FAC_COMMANDS.START then
if pkt.length == 6 then
---@type coord_auto_config
local config = {
mode = pkt.data[2],
burn_target = pkt.data[3],
charge_target = pkt.data[4],
gen_target = pkt.data[5],
limits = pkt.data[6]
}
_send(SCADA_CRDN_TYPES.FAC_CMD, { cmd, table.unpack(facility.auto_start(config)) })
else
log.debug(log_header .. "CRDN auto start (with configuration) packet length mismatch")
end
else
log.debug(log_header .. "CRDN facility command unknown")
end
else
log.debug(log_header .. "CRDN facility command packet length mismatch")
end
elseif pkt.type == SCADA_CRDN_TYPES.UNIT_BUILDS then
-- acknowledgement to coordinator receiving builds
self.acks.unit_builds = true
@@ -234,13 +265,13 @@ function coordinator.new_session(id, in_queue, out_queue, facility)
if pkt.length == 3 then
self.out_q.push_data(SV_Q_DATA.SET_BURN, data)
else
log.debug(log_header .. "CRDN command unit burn rate missing option")
log.debug(log_header .. "CRDN unit command burn rate missing option")
end
elseif cmd == UNIT_COMMANDS.SET_WASTE then
if pkt.length == 3 then
unit.set_waste(pkt.data[3])
else
log.debug(log_header .. "CRDN command unit set waste missing option")
log.debug(log_header .. "CRDN unit command set waste missing option")
end
elseif cmd == UNIT_COMMANDS.ACK_ALL_ALARMS then
unit.ack_all()
@@ -249,36 +280,29 @@ function coordinator.new_session(id, in_queue, out_queue, facility)
if pkt.length == 3 then
unit.ack_alarm(pkt.data[3])
else
log.debug(log_header .. "CRDN command unit ack alarm missing alarm id")
log.debug(log_header .. "CRDN unit command ack alarm missing alarm id")
end
elseif cmd == UNIT_COMMANDS.RESET_ALARM then
if pkt.length == 3 then
unit.reset_alarm(pkt.data[3])
else
log.debug(log_header .. "CRDN command unit reset alarm missing alarm id")
log.debug(log_header .. "CRDN unit command reset alarm missing alarm id")
end
elseif cmd == UNIT_COMMANDS.SET_GROUP then
if pkt.length == 3 then
facility.set_group(unit.get_id(), pkt.data[3])
_send(SCADA_CRDN_TYPES.UNIT_CMD, { cmd, uid, pkt.data[3] })
else
log.debug(log_header .. "CRDN command unit set group missing group id")
end
elseif cmd == UNIT_COMMANDS.SET_LIMIT then
if pkt.length == 3 then
unit.set_burn_limit(pkt.data[3])
_send(SCADA_CRDN_TYPES.UNIT_CMD, { cmd, uid, pkt.data[3] })
else
log.debug(log_header .. "CRDN command unit set limit missing group id")
log.debug(log_header .. "CRDN unit command set group missing group id")
end
else
log.debug(log_header .. "CRDN command unknown")
log.debug(log_header .. "CRDN unit command unknown")
end
else
log.debug(log_header .. "CRDN command unit invalid")
log.debug(log_header .. "CRDN unit command invalid")
end
else
log.debug(log_header .. "CRDN command unit packet length mismatch")
log.debug(log_header .. "CRDN unit command packet length mismatch")
end
else
log.debug(log_header .. "handler received unexpected SCADA_CRDN packet type " .. pkt.type)
@@ -331,6 +355,8 @@ function coordinator.new_session(id, in_queue, out_queue, facility)
self.retry_times.builds_packet = util.time() + RETRY_PERIOD
_send_fac_builds()
_send_unit_builds()
else
log.warning(log_header .. "unsupported command received in in_queue (this is a bug)")
end
elseif message.qtype == mqueue.TYPE.DATA then
-- instruction with body
@@ -339,6 +365,8 @@ function coordinator.new_session(id, in_queue, out_queue, facility)
if cmd.key == CRD_S_DATA.CMD_ACK then
local ack = cmd.val ---@type coord_ack
_send(SCADA_CRDN_TYPES.UNIT_CMD, { ack.cmd, ack.unit, ack.ack })
else
log.warning(log_header .. "unsupported data command received in in_queue (this is a bug)")
end
end
end

View File

@@ -1,12 +1,12 @@
local log = require("scada-common.log")
local rsio = require("scada-common.rsio")
local types = require("scada-common.types")
local util = require("scada-common.util")
local rsctl = require("supervisor.session.rsctl")
local unit = require("supervisor.session.unit")
local HEATING_WATER = 20000
local HEATING_SODIUM = 200000
local PROCESS = types.PROCESS
-- 7.14 kJ per blade for 1 mB of fissile fuel<br/>
-- 2856 FE per blade per 1 mB, 285.6 FE per blade per 0.1 mB (minimum)
@@ -15,15 +15,6 @@ local POWER_PER_BLADE = util.joules_to_fe(7140)
local MAX_CHARGE = 0.99
local RE_ENABLE_CHARGE = 0.95
---@alias PROCESS integer
local PROCESS = {
INACTIVE = 1,
SIMPLE = 2,
CHARGE = 3,
GEN_RATE = 4,
BURN_RATE = 5
}
local AUTO_SCRAM = {
NONE = 0,
MATRIX_DC = 1,
@@ -31,7 +22,7 @@ local AUTO_SCRAM = {
}
local charge_Kp = 1.0
local charge_Ki = 0.0
local charge_Ki = 0.00001
local charge_Kd = 0.0
local rate_Kp = 1.0
@@ -41,8 +32,6 @@ local rate_Kd = 0.0
---@class facility_management
local facility = {}
facility.PROCESS_MODES = PROCESS
-- create a new facility management object
---@param num_reactors integer number of reactor units
---@param cooling_conf table cooling configurations of reactor units
@@ -54,9 +43,11 @@ function facility.new(num_reactors, cooling_conf)
-- process control
mode = PROCESS.INACTIVE,
last_mode = PROCESS.INACTIVE,
burn_target = 0.0, -- burn rate target for aggregate burn mode
mode_set = PROCESS.SIMPLE,
max_burn_combined = 0.0, -- maximum burn rate to clamp at
burn_target = 0.1, -- burn rate target for aggregate burn mode
charge_target = 0, -- FE charge target
charge_rate = 0, -- FE/t charge rate target
gen_rate_target = 0, -- FE/t charge rate target
group_map = { 0, 0, 0, 0 }, -- units -> group IDs
prio_defs = { {}, {}, {}, {} }, -- priority definitions (each level is a table of units)
ascram = false,
@@ -67,6 +58,7 @@ function facility.new(num_reactors, cooling_conf)
initial_ramp = true,
waiting_on_ramp = false,
accumulator = 0.0,
saturated = false,
last_error = 0.0,
last_time = 0.0,
-- statistics
@@ -214,6 +206,8 @@ function facility.new(num_reactors, cooling_conf)
if state_changed then
if self.last_mode == PROCESS.INACTIVE then
local blade_count = 0
self.max_burn_combined = 0.0
for i = 1, #self.prio_defs do
table.sort(self.prio_defs[i],
---@param a reactor_unit
@@ -224,6 +218,7 @@ function facility.new(num_reactors, cooling_conf)
for _, u in pairs(self.prio_defs[i]) do
blade_count = blade_count + u.get_db().blade_count
u.a_engage()
self.max_burn_combined = self.max_burn_combined + (u.get_control_inf().lim_br10 / 10.0)
end
end
@@ -247,6 +242,18 @@ function facility.new(num_reactors, cooling_conf)
if state_changed then
self.time_start = now
end
elseif self.mode == PROCESS.BURN_RATE then
-- a total aggregate burn rate
if state_changed then
-- nothing special to do
elseif self.waiting_on_ramp and _all_units_ramped() then
self.waiting_on_ramp = false
self.time_start = now
end
if not self.waiting_on_ramp then
_allocate_burn_rate(self.burn_target, self.initial_ramp)
end
elseif self.mode == PROCESS.CHARGE then
-- target a level of charge
local error = (self.charge_target - avg_charge) / self.charge_conversion
@@ -261,23 +268,32 @@ function facility.new(num_reactors, cooling_conf)
end
if not self.waiting_on_ramp then
self.accumulator = self.accumulator + (avg_charge / self.charge_conversion)
if not self.saturated then
self.accumulator = self.accumulator + ((avg_charge / self.charge_conversion) * (now - self.last_time))
end
local runtime = now - self.time_start
local integral = self.accumulator / runtime
local derivative = (error - self.last_error) / (now - self.last_time)
local integral = self.accumulator
-- local derivative = (error - self.last_error) / (now - self.last_time)
local P = (charge_Kp * error)
local I = (charge_Ki * integral)
local D = (charge_Kd * derivative)
local D = 0 -- (charge_Kd * derivative)
local setpoint = P + I + D
-- round setpoint -> setpoint rounded (sp_r)
local sp_r = util.round(setpoint * 10.0) / 10.0
log.debug(util.sprintf("PROC_CHRG[%f] { CHRG[%f] ERR[%f] INT[%f] => SP[%f] SP_R[%f] <= P[%f] I[%f] D[%d] }",
runtime, avg_charge, error, integral, setpoint, sp_r, P, I, D))
-- clamp at range -> setpoint clamped (sp_c)
local sp_c = math.max(0, math.min(sp_r, self.max_burn_combined))
_allocate_burn_rate(sp_r, self.initial_ramp)
self.saturated = sp_r ~= sp_c
log.debug(util.sprintf("PROC_CHRG[%f] { CHRG[%f] ERR[%f] INT[%f] => SP[%f] SP_C[%f] <= P[%f] I[%f] D[%d] }",
runtime, avg_charge, error, integral, setpoint, sp_c, P, I, D))
_allocate_burn_rate(sp_c, self.initial_ramp)
if self.initial_ramp then
self.waiting_on_ramp = true
@@ -285,7 +301,7 @@ function facility.new(num_reactors, cooling_conf)
end
elseif self.mode == PROCESS.GEN_RATE then
-- target a rate of generation
local error = (self.charge_rate - avg_inflow) / self.charge_conversion
local error = (self.gen_rate_target - avg_inflow) / self.charge_conversion
local setpoint = 0.0
if state_changed then
@@ -303,36 +319,32 @@ function facility.new(num_reactors, cooling_conf)
end
if not self.waiting_on_ramp then
self.accumulator = self.accumulator + (avg_inflow / self.charge_conversion)
if not self.saturated then
self.accumulator = self.accumulator + ((avg_inflow / self.charge_conversion) * (now - self.last_time))
end
local runtime = util.time_s() - self.time_start
local integral = self.accumulator / runtime
local derivative = (error - self.last_error) / (now - self.last_time)
local integral = self.accumulator
-- local derivative = (error - self.last_error) / (now - self.last_time)
local P = (rate_Kp * error)
local I = (rate_Ki * integral)
local D = (rate_Kd * derivative)
local D = 0 -- (rate_Kd * derivative)
setpoint = P + I + D
-- round setpoint -> setpoint rounded (sp_r)
local sp_r = util.round(setpoint * 10.0) / 10.0
log.debug(util.sprintf("PROC_RATE[%f] { RATE[%f] ERR[%f] INT[%f] => SP[%f] SP_R[%f] <= P[%f] I[%f] D[%f] }",
runtime, avg_inflow, error, integral, setpoint, sp_r, P, I, D))
-- clamp at range -> setpoint clamped (sp_c)
local sp_c = math.max(0, math.min(sp_r, self.max_burn_combined))
_allocate_burn_rate(sp_r, false)
end
elseif self.mode == PROCESS.BURN_RATE then
-- a total aggregate burn rate
if state_changed then
-- nothing special to do
elseif self.waiting_on_ramp and _all_units_ramped() then
self.waiting_on_ramp = false
self.time_start = now
end
self.saturated = sp_r ~= sp_c
if not self.waiting_on_ramp then
_allocate_burn_rate(self.burn_target, self.initial_ramp)
log.debug(util.sprintf("PROC_RATE[%f] { RATE[%f] ERR[%f] INT[%f] => SP[%f] SP_C[%f] <= P[%f] I[%f] D[%f] }",
runtime, avg_inflow, error, integral, setpoint, sp_c, P, I, D))
_allocate_burn_rate(sp_c, false)
end
end
@@ -387,6 +399,83 @@ function facility.new(num_reactors, cooling_conf)
end
end
-- COMMANDS --
-- SCRAM all reactor units
function public.scram_all()
for i = 1, #self.units do
local u = self.units[i] ---@type reactor_unit
u.scram()
end
end
-- stop auto control
function public.auto_stop()
self.mode = PROCESS.INACTIVE
end
-- set automatic control configuration and start the process
---@param config coord_auto_config configuration
---@return table response ready state (successfully started) and current configuration (after updating)
function public.auto_start(config)
local ready = false
-- load up current limits
local limits = {}
for i = 1, num_reactors do
local u = self.units[i] ---@type reactor_unit
limits[i] = u.get_control_inf().lim_br10 * 10
end
-- only allow changes if not running
if self.mode == PROCESS.INACTIVE then
if (type(config.mode) == "number") and (config.mode > PROCESS.INACTIVE) and (config.mode <= PROCESS.SIMPLE) then
self.mode_set = config.mode
end
if (type(config.burn_target) == "number") and config.burn_target >= 0.1 then
self.burn_target = config.burn_target
log.debug("SET BURN TARGET " .. config.burn_target)
end
if (type(config.charge_target) == "number") and config.charge_target >= 0 then
self.charge_target = config.charge_target
log.debug("SET CHARGE TARGET " .. config.charge_target)
end
if (type(config.gen_target) == "number") and config.gen_target >= 0 then
self.gen_rate_target = config.gen_target
log.debug("SET RATE TARGET " .. config.gen_target)
end
if (type(config.limits) == "table") and (#config.limits == num_reactors) then
for i = 1, num_reactors do
local limit = config.limits[i]
if (type(limit) == "number") and (limit >= 0.1) then
limits[i] = limit
self.units[i].set_burn_limit(limit)
log.debug("SET UNIT " .. i .. " LIMIT " .. limit)
end
end
end
ready = self.mode_set > 0
if (self.mode_set == PROCESS.CHARGE) and (self.charge_target <= 0) then
ready = false
elseif (self.mode_set == PROCESS.GEN_RATE) and (self.gen_rate_target <= 0) then
ready = false
elseif (self.mode_set == PROCESS.BURN_RATE) and (self.burn_target <= 0.1) then
ready = false
end
if ready then self.mode = self.mode_set end
end
return { ready, self.mode_set, self.burn_target, self.charge_target, self.gen_rate_target, limits }
end
-- SETTINGS --
-- set the automatic control group of a unit
@@ -424,10 +513,27 @@ function facility.new(num_reactors, cooling_conf)
return build
end
-- get automatic process control status
function public.get_control_status()
return {
self.mode,
self.waiting_on_ramp,
self.ascram,
self.ascram_reason
}
end
-- get RTU statuses
function public.get_rtu_statuses()
local status = {}
-- power averages from induction matricies
status.power = {
self.avg_charge,
self.avg_inflow,
self.avg_outflow
}
-- status of induction matricies (including tanks)
status.induction = {}
for i = 1, #self.induction do

View File

@@ -10,6 +10,7 @@ local plc = {}
local PROTOCOLS = comms.PROTOCOLS
local RPLC_TYPES = comms.RPLC_TYPES
local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES
local PLC_AUTO_ACK = comms.PLC_AUTO_ACK
local UNIT_COMMANDS = comms.UNIT_COMMANDS
@@ -19,8 +20,9 @@ local print_ts = util.print_ts
local println_ts = util.println_ts
-- retry time constants in ms
local INITIAL_WAIT = 1500
local RETRY_PERIOD = 1000
local INITIAL_WAIT = 1500
local INITIAL_AUTO_WAIT = 1000
local RETRY_PERIOD = 1000
local PLC_S_CMDS = {
SCRAM = 1,
@@ -440,6 +442,21 @@ function plc.new_session(id, for_reactor, in_queue, out_queue)
cmd = UNIT_COMMANDS.RESET_RPS,
ack = ack
})
elseif pkt.type == RPLC_TYPES.AUTO_BURN_RATE then
if pkt.length == 1 then
local ack = pkt.data[1]
self.acks.burn_rate = ack ~= PLC_AUTO_ACK.FAIL
if ack == PLC_AUTO_ACK.FAIL then
elseif ack == PLC_AUTO_ACK.DIRECT_SET_OK then
elseif ack == PLC_AUTO_ACK.RAMP_SET_OK then
elseif ack == PLC_AUTO_ACK.ZERO_DIS_OK then
elseif ack == PLC_AUTO_ACK.ZERO_DIS_WAIT then
end
else
log.debug(log_header .. "RPLC automatic burn rate ack packet length mismatch")
end
else
log.debug(log_header .. "handler received unsupported RPLC packet type " .. pkt.type)
end
@@ -517,6 +534,11 @@ function plc.new_session(id, for_reactor, in_queue, out_queue)
---@param engage boolean true to engage the lockout
function public.auto_lock(engage)
self.auto_lock = engage
-- stop retrying a burn rate command
if engage then
self.acks.burn_rate = true
end
end
-- set the burn rate on behalf of automatic control
@@ -583,6 +605,8 @@ function plc.new_session(id, for_reactor, in_queue, out_queue)
self.acks.rps_reset = false
self.retry_times.rps_reset_req = util.time() + INITIAL_WAIT
_send(RPLC_TYPES.RPS_RESET, {})
else
log.warning(log_header .. "unsupported command received in in_queue (this is a bug)")
end
elseif message.qtype == mqueue.TYPE.DATA then
-- instruction with body
@@ -613,13 +637,20 @@ function plc.new_session(id, for_reactor, in_queue, out_queue)
end
elseif cmd.key == PLC_S_DATA.AUTO_BURN_RATE then
-- set automatic burn rate
cmd.val = math.floor(cmd.val * 10) / 10 -- round to 10ths place
if cmd.val > 0 and cmd.val <= self.sDB.mek_struct.max_burn then
self.commanded_burn_rate = cmd.val
self.acks.burn_rate = false
self.retry_times.burn_rate_req = util.time() + INITIAL_WAIT
_send(RPLC_TYPES.MEK_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate })
if self.auto_lock then
cmd.val = math.floor(cmd.val * 10) / 10 -- round to 10ths place
if cmd.val > 0 and cmd.val <= self.sDB.mek_struct.max_burn then
self.commanded_burn_rate = cmd.val
-- this is only for manual control, only retry auto ramps
self.acks.burn_rate = not self.ramping_rate
self.retry_times.burn_rate_req = util.time() + INITIAL_AUTO_WAIT
_send(RPLC_TYPES.AUTO_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate })
end
end
else
log.warning(log_header .. "unsupported data command received in in_queue (this is a bug)")
end
end
end
@@ -685,7 +716,12 @@ function plc.new_session(id, for_reactor, in_queue, out_queue)
if not self.acks.burn_rate then
if rtimes.burn_rate_req - util.time() <= 0 then
_send(RPLC_TYPES.MEK_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate })
if self.auto_lock then
_send(RPLC_TYPES.AUTO_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate })
else
_send(RPLC_TYPES.MEK_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate })
end
rtimes.burn_rate_req = util.time() + RETRY_PERIOD
end
end

View File

@@ -450,6 +450,13 @@ function unit.new(for_reactor, num_boilers, num_turbines)
-- OPERATIONS --
-- queue a command to SCRAM the reactor
function public.scram()
if self.plc_s ~= nil then
self.plc_s.in_queue.push_command(PLC_S_CMDS.SCRAM)
end
end
-- acknowledge all alarms (if possible)
function public.ack_all()
for i = 1, #self.db.alarm_states do

View File

@@ -14,7 +14,7 @@ local svsessions = require("supervisor.session.svsessions")
local config = require("supervisor.config")
local supervisor = require("supervisor.supervisor")
local SUPERVISOR_VERSION = "beta-v0.9.6"
local SUPERVISOR_VERSION = "beta-v0.9.7"
local print = util.print
local println = util.println