Improve generator control and status displays

This commit is contained in:
Codex Agent
2025-11-22 20:35:13 +01:00
parent 2856d83600
commit 5d8b617c9e
6 changed files with 110 additions and 19 deletions

View File

@@ -24,6 +24,7 @@ class ReactorCommand:
primary_pumps: dict[int, bool] | None = None primary_pumps: dict[int, bool] | None = None
secondary_pumps: dict[int, bool] | None = None secondary_pumps: dict[int, bool] | None = None
generator_units: dict[int, bool] | None = None generator_units: dict[int, bool] | None = None
generator_auto: bool | None = None
maintenance_components: tuple[str, ...] = tuple() maintenance_components: tuple[str, ...] = tuple()
@classmethod @classmethod

View File

@@ -56,6 +56,7 @@ class ReactorDashboard:
DashboardKey("k", "Toggle secondary pump 2"), DashboardKey("k", "Toggle secondary pump 2"),
DashboardKey("b", "Toggle generator 1"), DashboardKey("b", "Toggle generator 1"),
DashboardKey("v", "Toggle generator 2"), DashboardKey("v", "Toggle generator 2"),
DashboardKey("x", "Toggle generator auto"),
DashboardKey("t", "Toggle turbine"), DashboardKey("t", "Toggle turbine"),
DashboardKey("1/2/3", "Toggle turbine units 1-3"), DashboardKey("1/2/3", "Toggle turbine units 1-3"),
DashboardKey("y/u/i", "Maintain turbine 1/2/3"), DashboardKey("y/u/i", "Maintain turbine 1/2/3"),
@@ -146,6 +147,8 @@ class ReactorDashboard:
self._toggle_generator_unit(0) self._toggle_generator_unit(0)
elif ch in (ord("v"), ord("V")): elif ch in (ord("v"), ord("V")):
self._toggle_generator_unit(1) self._toggle_generator_unit(1)
elif ch in (ord("x"), ord("X")):
self._queue_command(ReactorCommand(generator_auto=not self.reactor.generator_auto))
elif ch in (ord("t"), ord("T")): elif ch in (ord("t"), ord("T")):
self._queue_command(ReactorCommand(turbine_on=not self.reactor.turbine_active)) self._queue_command(ReactorCommand(turbine_on=not self.reactor.turbine_active))
elif ord("1") <= ch <= ord("9"): elif ord("1") <= ch <= ord("9"):
@@ -259,6 +262,7 @@ class ReactorDashboard:
self.reactor = Reactor.default() self.reactor = Reactor.default()
self.start_state = None self.start_state = None
self.pending_command = None self.pending_command = None
self._last_state = None
self.reset_requested = False self.reset_requested = False
self.log_buffer.clear() self.log_buffer.clear()
@@ -468,9 +472,15 @@ class ReactorDashboard:
def _turbine_status_lines(self) -> list[str]: def _turbine_status_lines(self) -> list[str]:
if not self.reactor.turbine_unit_active: if not self.reactor.turbine_unit_active:
return ["n/a"] return ["n/a"]
return [ lines: list[str] = []
f"{idx + 1}:{'ON' if active else 'OFF'}" for idx, active in enumerate(self.reactor.turbine_unit_active) for idx, active in enumerate(self.reactor.turbine_unit_active):
] label = f"{idx + 1}:"
status = "ON" if active else "OFF"
if idx < len(getattr(self._last_state, "turbines", [])):
t_state = self._last_state.turbines[idx]
status = getattr(t_state, "status", status)
lines.append(f"{label}{status}")
return lines
def _total_load_supplied(self, state: PlantState) -> float: def _total_load_supplied(self, state: PlantState) -> float:
return sum(t.load_supplied_mw for t in state.turbines) return sum(t.load_supplied_mw for t in state.turbines)
@@ -551,7 +561,8 @@ class ReactorDashboard:
if index >= len(pumps): if index >= len(pumps):
return "n/a" return "n/a"
state = pumps[index] state = pumps[index]
return f"{'ON ' if state.active else 'OFF'} {state.flow_rate:6.0f} kg/s" status = getattr(state, "status", "ON" if state.active else "OFF")
return f"{status:<8} {state.flow_rate:6.0f} kg/s"
def _current_demand(self) -> float: def _current_demand(self) -> float:
if self.reactor.consumer: if self.reactor.consumer:

View File

@@ -17,6 +17,7 @@ class GeneratorState:
spool_remaining: float spool_remaining: float
power_output_mw: float power_output_mw: float
battery_charge: float battery_charge: float
status: str = "OFF"
@dataclass @dataclass
@@ -32,6 +33,7 @@ class DieselGenerator:
return return
state.starting = True state.starting = True
state.spool_remaining = self.spool_time state.spool_remaining = self.spool_time
state.status = "STARTING"
LOGGER.info("Generator starting (spool %.0fs)", self.spool_time) LOGGER.info("Generator starting (spool %.0fs)", self.spool_time)
def stop(self, state: GeneratorState) -> None: def stop(self, state: GeneratorState) -> None:
@@ -41,6 +43,7 @@ class DieselGenerator:
state.starting = False state.starting = False
state.spool_remaining = 0.0 state.spool_remaining = 0.0
state.power_output_mw = 0.0 state.power_output_mw = 0.0
state.status = "OFF"
LOGGER.info("Generator stopped") LOGGER.info("Generator stopped")
def step(self, state: GeneratorState, load_demand_mw: float, dt: float) -> float: def step(self, state: GeneratorState, load_demand_mw: float, dt: float) -> float:
@@ -51,12 +54,15 @@ class DieselGenerator:
if state.spool_remaining <= 0.0: if state.spool_remaining <= 0.0:
state.starting = False state.starting = False
state.running = True state.running = True
state.status = "RUN"
LOGGER.info("Generator online at %.1f MW", self.rated_output_mw) LOGGER.info("Generator online at %.1f MW", self.rated_output_mw)
elif state.running: elif state.running:
available = self.rated_output_mw available = self.rated_output_mw
state.power_output_mw = min(available, load_demand_mw) state.power_output_mw = min(available, load_demand_mw)
state.status = "RUN" if state.power_output_mw > 0 else "IDLE"
else: else:
state.power_output_mw = 0.0 state.power_output_mw = 0.0
state.status = "OFF"
if state.running: if state.running:
state.battery_charge = min(1.0, state.battery_charge + 0.02 * dt) state.battery_charge = min(1.0, state.battery_charge + 0.02 * dt)

View File

@@ -44,6 +44,7 @@ class Reactor:
turbine_unit_active: list[bool] = field(default_factory=lambda: [True, True, True]) turbine_unit_active: list[bool] = field(default_factory=lambda: [True, True, True])
shutdown: bool = False shutdown: bool = False
meltdown: bool = False meltdown: bool = False
generator_auto: bool = True
poison_alerts: set[str] = field(default_factory=set) poison_alerts: set[str] = field(default_factory=set)
maintenance_active: set[str] = field(default_factory=set) maintenance_active: set[str] = field(default_factory=set)
@@ -115,10 +116,18 @@ class Reactor:
mass_flow_rate=0.0, mass_flow_rate=0.0,
steam_quality=0.0, steam_quality=0.0,
) )
primary_pumps = [PumpState(active=self.primary_pump_active, flow_rate=0.0, pressure=0.5) for _ in range(2)] primary_pumps = [
secondary_pumps = [PumpState(active=self.secondary_pump_active, flow_rate=0.0, pressure=0.5) for _ in range(2)] PumpState(active=self.primary_pump_active and self.primary_pump_units[idx], flow_rate=0.0, pressure=0.5)
for idx in range(2)
]
secondary_pumps = [
PumpState(active=self.secondary_pump_active and self.secondary_pump_units[idx], flow_rate=0.0, pressure=0.5)
for idx in range(2)
]
generator_states = [ generator_states = [
GeneratorState(running=False, starting=False, spool_remaining=0.0, power_output_mw=0.0, battery_charge=1.0) GeneratorState(
running=False, starting=False, spool_remaining=0.0, power_output_mw=0.0, battery_charge=1.0, status="OFF"
)
for _ in self.generators for _ in self.generators
] ]
turbine_states = [ turbine_states = [
@@ -218,6 +227,14 @@ class Reactor:
pump_state.pressure, desired_pressure, dt, self.primary_pump.spool_time pump_state.pressure, desired_pressure, dt, self.primary_pump.spool_time
) )
pump_state.active = (unit_enabled and power_ratio > 0.05) or pump_state.flow_rate > 1.0 pump_state.active = (unit_enabled and power_ratio > 0.05) or pump_state.flow_rate > 1.0
if unit_enabled and pump_state.flow_rate < max(1.0, desired_flow * 0.8):
pump_state.status = "STARTING"
elif not unit_enabled and pump_state.flow_rate > 1.0:
pump_state.status = "STOPPING"
elif pump_state.active:
pump_state.status = "RUN"
else:
pump_state.status = "OFF"
total_flow += pump_state.flow_rate total_flow += pump_state.flow_rate
loop_pressure = max(loop_pressure, pump_state.pressure) loop_pressure = max(loop_pressure, pump_state.pressure)
state.primary_loop.mass_flow_rate = total_flow state.primary_loop.mass_flow_rate = total_flow
@@ -239,6 +256,7 @@ class Reactor:
pump_state.pressure = self._ramp_value( pump_state.pressure = self._ramp_value(
pump_state.pressure, state.primary_loop.pressure, dt, self.primary_pump.spool_time pump_state.pressure, state.primary_loop.pressure, dt, self.primary_pump.spool_time
) )
pump_state.status = "STOPPING" if pump_state.flow_rate > 0 else "OFF"
if self.secondary_pump_active: if self.secondary_pump_active:
total_flow = 0.0 total_flow = 0.0
target_pressure = 12.0 * 0.75 + 2.0 target_pressure = 12.0 * 0.75 + 2.0
@@ -257,6 +275,14 @@ class Reactor:
pump_state.pressure, desired_pressure, dt, self.secondary_pump.spool_time pump_state.pressure, desired_pressure, dt, self.secondary_pump.spool_time
) )
pump_state.active = unit_enabled or pump_state.flow_rate > 1.0 pump_state.active = unit_enabled or pump_state.flow_rate > 1.0
if unit_enabled and pump_state.flow_rate < max(1.0, desired_flow * 0.8):
pump_state.status = "STARTING"
elif not unit_enabled and pump_state.flow_rate > 1.0:
pump_state.status = "STOPPING"
elif pump_state.active:
pump_state.status = "RUN"
else:
pump_state.status = "OFF"
total_flow += pump_state.flow_rate total_flow += pump_state.flow_rate
loop_pressure = max(loop_pressure, pump_state.pressure) loop_pressure = max(loop_pressure, pump_state.pressure)
state.secondary_loop.mass_flow_rate = total_flow state.secondary_loop.mass_flow_rate = total_flow
@@ -278,6 +304,7 @@ class Reactor:
pump_state.pressure = self._ramp_value( pump_state.pressure = self._ramp_value(
pump_state.pressure, state.secondary_loop.pressure, dt, self.secondary_pump.spool_time pump_state.pressure, state.secondary_loop.pressure, dt, self.secondary_pump.spool_time
) )
pump_state.status = "STOPPING" if pump_state.flow_rate > 0 else "OFF"
self.thermal.step_core(state.core, state.primary_loop, total_power, dt) self.thermal.step_core(state.core, state.primary_loop, total_power, dt)
if not self.secondary_pump_active or state.secondary_loop.mass_flow_rate <= 1.0: if not self.secondary_pump_active or state.secondary_loop.mass_flow_rate <= 1.0:
@@ -334,8 +361,13 @@ class Reactor:
turbine_state = state.turbines[idx] turbine_state = state.turbines[idx]
if idx in active_indices: if idx in active_indices:
turbine.step(state.secondary_loop, turbine_state, steam_power_mw=power_per_unit, dt=dt) turbine.step(state.secondary_loop, turbine_state, steam_power_mw=power_per_unit, dt=dt)
if turbine_state.electrical_output_mw < max(1.0, power_per_unit * 0.7):
turbine_state.status = "STARTING"
else:
turbine_state.status = "RUN"
else: else:
self._spin_down_turbine(turbine_state, dt, turbine.spool_time) self._spin_down_turbine(turbine_state, dt, turbine.spool_time)
turbine_state.status = "STOPPING" if turbine_state.electrical_output_mw > 0 else "OFF"
self._dispatch_consumer_load(state, active_indices) self._dispatch_consumer_load(state, active_indices)
def _reset_turbine_state(self, turbine_state: TurbineState) -> None: def _reset_turbine_state(self, turbine_state: TurbineState) -> None:
@@ -343,6 +375,7 @@ class Reactor:
turbine_state.electrical_output_mw = 0.0 turbine_state.electrical_output_mw = 0.0
turbine_state.load_demand_mw = 0.0 turbine_state.load_demand_mw = 0.0
turbine_state.load_supplied_mw = 0.0 turbine_state.load_supplied_mw = 0.0
turbine_state.status = "OFF"
@staticmethod @staticmethod
def _ramp_value(current: float, target: float, dt: float, time_constant: float) -> float: def _ramp_value(current: float, target: float, dt: float, time_constant: float) -> float:
@@ -405,6 +438,8 @@ class Reactor:
self.shutdown = True self.shutdown = True
overrides["rod_fraction"] = self.control.scram() overrides["rod_fraction"] = self.control.scram()
self._set_turbine_state(False) self._set_turbine_state(False)
if command.generator_auto is not None:
self.generator_auto = command.generator_auto
if command.power_setpoint is not None: if command.power_setpoint is not None:
self.control.set_power_setpoint(command.power_setpoint) self.control.set_power_setpoint(command.power_setpoint)
if command.rod_manual is not None: if command.rod_manual is not None:
@@ -512,17 +547,18 @@ class Reactor:
GeneratorState(running=False, starting=False, spool_remaining=0.0, power_output_mw=0.0, battery_charge=1.0) GeneratorState(running=False, starting=False, spool_remaining=0.0, power_output_mw=0.0, battery_charge=1.0)
) )
deficit = max(0.0, aux_demand - turbine_electric) deficit = max(0.0, aux_demand - turbine_electric)
if deficit > 0.0: if self.generator_auto:
for idx, gen_state in enumerate(state.generators): if deficit > 0.0:
if not (gen_state.running or gen_state.starting): for idx, gen_state in enumerate(state.generators):
self.generators[idx].start(gen_state) if not (gen_state.running or gen_state.starting):
deficit -= self.generators[idx].rated_output_mw self.generators[idx].start(gen_state)
if deficit <= 0: deficit -= self.generators[idx].rated_output_mw
break if deficit <= 0:
elif turbine_electric > aux_demand: break
for idx, gen_state in enumerate(state.generators): elif turbine_electric > aux_demand:
if gen_state.running and not gen_state.starting: for idx, gen_state in enumerate(state.generators):
self.generators[idx].stop(gen_state) if gen_state.running and not gen_state.starting:
self.generators[idx].stop(gen_state)
total_power = 0.0 total_power = 0.0
remaining = max(0.0, aux_demand - turbine_electric) remaining = max(0.0, aux_demand - turbine_electric)
@@ -640,6 +676,7 @@ class Reactor:
"turbine_units": self.turbine_unit_active, "turbine_units": self.turbine_unit_active,
"shutdown": self.shutdown, "shutdown": self.shutdown,
"meltdown": self.meltdown, "meltdown": self.meltdown,
"generator_auto": self.generator_auto,
"maintenance_active": list(self.maintenance_active), "maintenance_active": list(self.maintenance_active),
"generators": [ "generators": [
{ {
@@ -648,6 +685,7 @@ class Reactor:
"spool_remaining": g.spool_remaining, "spool_remaining": g.spool_remaining,
"power_output_mw": g.power_output_mw, "power_output_mw": g.power_output_mw,
"battery_charge": g.battery_charge, "battery_charge": g.battery_charge,
"status": g.status,
} }
for g in state.generators for g in state.generators
], ],
@@ -671,6 +709,7 @@ class Reactor:
self.turbine_active = metadata.get("turbine_active", any(self.turbine_unit_active)) self.turbine_active = metadata.get("turbine_active", any(self.turbine_unit_active))
self.shutdown = metadata.get("shutdown", self.shutdown) self.shutdown = metadata.get("shutdown", self.shutdown)
self.meltdown = metadata.get("meltdown", self.meltdown) self.meltdown = metadata.get("meltdown", self.meltdown)
self.generator_auto = metadata.get("generator_auto", self.generator_auto)
maint = metadata.get("maintenance_active") maint = metadata.get("maintenance_active")
if maint is not None: if maint is not None:
self.maintenance_active = set(maint) self.maintenance_active = set(maint)
@@ -691,7 +730,12 @@ class Reactor:
# Back-fill pump state lists for compatibility. # Back-fill pump state lists for compatibility.
if not plant.primary_pumps or len(plant.primary_pumps) < 2: if not plant.primary_pumps or len(plant.primary_pumps) < 2:
plant.primary_pumps = [ plant.primary_pumps = [
PumpState(active=self.primary_pump_active, flow_rate=plant.primary_loop.mass_flow_rate / 2, pressure=plant.primary_loop.pressure) PumpState(
active=self.primary_pump_active,
flow_rate=plant.primary_loop.mass_flow_rate / 2,
pressure=plant.primary_loop.pressure,
status="OFF",
)
for _ in range(2) for _ in range(2)
] ]
if not plant.secondary_pumps or len(plant.secondary_pumps) < 2: if not plant.secondary_pumps or len(plant.secondary_pumps) < 2:
@@ -700,6 +744,7 @@ class Reactor:
active=self.secondary_pump_active, active=self.secondary_pump_active,
flow_rate=plant.secondary_loop.mass_flow_rate / 2, flow_rate=plant.secondary_loop.mass_flow_rate / 2,
pressure=plant.secondary_loop.pressure, pressure=plant.secondary_loop.pressure,
status="OFF",
) )
for _ in range(2) for _ in range(2)
] ]
@@ -726,6 +771,7 @@ class Reactor:
spool_remaining=0.0, spool_remaining=0.0,
power_output_mw=0.0, power_output_mw=0.0,
battery_charge=1.0, battery_charge=1.0,
status="OFF",
) )
) )
for idx, gen_state in enumerate(plant.generators): for idx, gen_state in enumerate(plant.generators):
@@ -736,6 +782,7 @@ class Reactor:
gen_state.spool_remaining = cfg.get("spool_remaining", gen_state.spool_remaining) gen_state.spool_remaining = cfg.get("spool_remaining", gen_state.spool_remaining)
gen_state.power_output_mw = cfg.get("power_output_mw", gen_state.power_output_mw) gen_state.power_output_mw = cfg.get("power_output_mw", gen_state.power_output_mw)
gen_state.battery_charge = cfg.get("battery_charge", gen_state.battery_charge) gen_state.battery_charge = cfg.get("battery_charge", gen_state.battery_charge)
gen_state.status = cfg.get("status", gen_state.status)
return plant return plant
def _handle_heat_sink_loss(self, state: PlantState) -> None: def _handle_heat_sink_loss(self, state: PlantState) -> None:

View File

@@ -54,6 +54,7 @@ class TurbineState:
condenser_temperature: float condenser_temperature: float
load_demand_mw: float = 0.0 load_demand_mw: float = 0.0
load_supplied_mw: float = 0.0 load_supplied_mw: float = 0.0
status: str = "OFF"
@dataclass @dataclass
@@ -61,6 +62,7 @@ class PumpState:
active: bool active: bool
flow_rate: float flow_rate: float
pressure: float pressure: float
status: str = "OFF"
@dataclass @dataclass

View File

@@ -206,6 +206,30 @@ def test_generator_spools_and_powers_pumps():
assert state.primary_loop.mass_flow_rate > 0.0 assert state.primary_loop.mass_flow_rate > 0.0
def test_generator_manual_mode_allows_single_unit_and_stop():
reactor = Reactor.default()
state = reactor.initial_state()
reactor.shutdown = False
reactor.control.manual_control = True
reactor.control.rod_fraction = 0.95
reactor.generator_auto = False
reactor.primary_pump_units = [True, False]
reactor.secondary_pump_units = [False, False]
reactor.step(state, dt=1.0, command=ReactorCommand(generator_units={1: True}, primary_pumps={1: True}))
assert state.generators[0].starting or state.generators[0].running
for _ in range(15):
reactor.step(state, dt=1.0)
assert state.generators[0].running is True
reactor.step(state, dt=1.0, command=ReactorCommand(generator_units={1: False}))
for _ in range(5):
reactor.step(state, dt=1.0)
assert state.generators[0].running is False
assert state.generators[1].running is False
def test_meltdown_triggers_shutdown(): def test_meltdown_triggers_shutdown():
reactor = Reactor.default() reactor = Reactor.default()
state = reactor.initial_state() state = reactor.initial_state()