Compare commits

..

8 Commits

Author SHA1 Message Date
Codex Agent
fb1276f39f Clarify generator control and turbine status 2025-11-22 22:57:31 +01:00
Codex Agent
c30c838fcc Show pumps as OFF once fully stopped 2025-11-22 22:51:05 +01:00
Codex Agent
faac1dc7b0 Keep pump units off until explicitly enabled 2025-11-22 22:48:04 +01:00
Codex Agent
bcd1eec84f Balance generator load sharing 2025-11-22 22:44:00 +01:00
Codex Agent
79f98faeeb Group dashboard controls and show turbine control mode 2025-11-22 20:53:03 +01:00
Codex Agent
5d8b617c9e Improve generator control and status displays 2025-11-22 20:35:13 +01:00
Codex Agent
2856d83600 Align dashboard health bars 2025-11-22 20:26:31 +01:00
Codex Agent
f0f2128ae6 Split dashboard data into two columns 2025-11-22 20:24:33 +01:00
6 changed files with 203 additions and 74 deletions

View File

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

View File

@@ -47,29 +47,45 @@ class ReactorDashboard:
self._log_handler: Optional[logging.Handler] = None
self._previous_handlers: list[logging.Handler] = []
self._logger = logging.getLogger("reactor_sim")
self.keys = [
DashboardKey("q", "Quit & save"),
DashboardKey("space", "SCRAM"),
DashboardKey("g", "Toggle primary pump 1"),
DashboardKey("h", "Toggle primary pump 2"),
DashboardKey("j", "Toggle secondary pump 1"),
DashboardKey("k", "Toggle secondary pump 2"),
DashboardKey("b", "Toggle generator 1"),
DashboardKey("v", "Toggle generator 2"),
DashboardKey("t", "Toggle turbine"),
DashboardKey("1/2/3", "Toggle turbine units 1-3"),
DashboardKey("y/u/i", "Maintain turbine 1/2/3"),
DashboardKey("c", "Toggle consumer"),
DashboardKey("r", "Reset & clear state"),
DashboardKey("m/n", "Maintain primary pumps 1/2"),
DashboardKey(",/.", "Maintain secondary pumps 1/2"),
DashboardKey("B/V", "Maintain generator 1/2"),
DashboardKey("k", "Maintain core (requires shutdown)"),
DashboardKey("+/-", "Withdraw/insert rods"),
DashboardKey("[/]", "Adjust consumer demand /+50 MW"),
DashboardKey("s", "Setpoint 250 MW"),
DashboardKey("d", "Setpoint +250 MW"),
DashboardKey("a", "Toggle auto rod control"),
self.help_sections: list[tuple[str, list[DashboardKey]]] = [
(
"Reactor / Safety",
[
DashboardKey("q", "Quit & save"),
DashboardKey("space", "SCRAM"),
DashboardKey("r", "Reset & clear state"),
DashboardKey("a", "Toggle auto rod control"),
DashboardKey("+/-", "Withdraw/insert rods"),
DashboardKey("[/]", "Adjust consumer demand /+50 MW"),
DashboardKey("s/d", "Setpoint /+250 MW"),
],
),
(
"Pumps",
[
DashboardKey("g/h", "Toggle primary pump 1/2"),
DashboardKey("j/k", "Toggle secondary pump 1/2"),
DashboardKey("m/n", "Maintain primary pumps 1/2"),
DashboardKey(",/.", "Maintain secondary pumps 1/2"),
],
),
(
"Generators",
[
DashboardKey("b/v", "Toggle generator 1/2"),
DashboardKey("x", "Toggle generator auto"),
DashboardKey("B/V", "Maintain generator 1/2"),
],
),
(
"Turbines / Grid",
[
DashboardKey("t", "Toggle turbine bank"),
DashboardKey("1/2/3", "Toggle turbine units 1-3"),
DashboardKey("y/u/i", "Maintain turbine 1/2/3"),
DashboardKey("c", "Toggle consumer"),
],
),
]
def run(self) -> None:
@@ -146,6 +162,8 @@ class ReactorDashboard:
self._toggle_generator_unit(0)
elif ch in (ord("v"), ord("V")):
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")):
self._queue_command(ReactorCommand(turbine_on=not self.reactor.turbine_active))
elif ord("1") <= ch <= ord("9"):
@@ -259,6 +277,7 @@ class ReactorDashboard:
self.reactor = Reactor.default()
self.start_state = None
self.pending_command = None
self._last_state = None
self.reset_requested = False
self.log_buffer.clear()
@@ -307,10 +326,23 @@ class ReactorDashboard:
win.erase()
win.box()
win.addstr(0, 2, " Plant Overview ", curses.color_pair(1) | curses.A_BOLD)
y = 2
y = self._draw_section(
win,
y,
height, width = win.getmaxyx()
inner_height = height - 2
inner_width = width - 2
left_width = max(28, inner_width // 2)
right_width = inner_width - left_width
left_win = win.derwin(inner_height, left_width, 1, 1)
right_win = win.derwin(inner_height, right_width, 1, 1 + left_width)
for row in range(1, height - 1):
win.addch(row, 1 + left_width, curses.ACS_VLINE)
left_win.erase()
right_win.erase()
left_y = 0
left_y = self._draw_section(
left_win,
left_y,
"Core",
[
("Fuel Temp", f"{state.core.fuel_temperature:8.1f} K"),
@@ -322,10 +354,10 @@ class ReactorDashboard:
("Reactivity", f"{state.core.reactivity_margin:+.4f}"),
],
)
y = self._draw_section(win, y, "Key Poisons / Emitters", self._poison_lines(state))
y = self._draw_section(
win,
y,
left_y = self._draw_section(left_win, left_y, "Key Poisons / Emitters", self._poison_lines(state))
left_y = self._draw_section(
left_win,
left_y,
"Primary Loop",
[
("Pump1", self._pump_status(state.primary_pumps, 0)),
@@ -336,9 +368,9 @@ class ReactorDashboard:
("Pressure", f"{state.primary_loop.pressure:5.2f} MPa"),
],
)
y = self._draw_section(
win,
y,
self._draw_section(
left_win,
left_y,
"Secondary Loop",
[
("Pump1", self._pump_status(state.secondary_pumps, 0)),
@@ -349,20 +381,16 @@ class ReactorDashboard:
("Steam Quality", f"{state.secondary_loop.steam_quality:5.2f}"),
],
)
y = self._draw_section(
win,
y,
"Generators",
self._generator_lines(state),
)
right_y = 0
consumer_status = "n/a"
consumer_demand = 0.0
if self.reactor.consumer:
consumer_status = "ONLINE" if self.reactor.consumer.online else "OFF"
consumer_demand = self.reactor.consumer.demand_mw
y = self._draw_section(
win,
y,
right_y = self._draw_section(
right_win,
right_y,
"Turbine / Grid",
[
("Turbines", " ".join(self._turbine_status_lines())),
@@ -381,18 +409,23 @@ class ReactorDashboard:
("Demand", f"{consumer_demand:7.1f} MW"),
],
)
y = self._draw_section(win, y, "Maintenance", self._maintenance_lines())
y = self._draw_health_bars(win, y)
right_y = self._draw_section(right_win, right_y, "Generators", self._generator_lines(state))
right_y = self._draw_section(right_win, right_y, "Maintenance", self._maintenance_lines())
self._draw_health_bars(right_win, right_y)
def _draw_help_panel(self, win: "curses._CursesWindow") -> None:
win.erase()
win.box()
win.addstr(0, 2, " Controls ", curses.color_pair(1) | curses.A_BOLD)
y = 2
for entry in self.keys:
win.addstr(y, 2, f"{entry.key:<8} {entry.description}")
for title, entries in self.help_sections:
win.addstr(y, 2, title, curses.color_pair(1) | curses.A_BOLD)
y += 1
win.addstr(y + 1, 2, "Tips:", curses.color_pair(2) | curses.A_BOLD)
for entry in entries:
win.addstr(y, 4, f"{entry.key:<8} {entry.description}")
y += 1
y += 1
win.addstr(y, 2, "Tips:", curses.color_pair(2) | curses.A_BOLD)
tips = [
"Start pumps before withdrawing rods.",
"Bring turbine and consumer online after thermal stabilization.",
@@ -458,9 +491,15 @@ class ReactorDashboard:
def _turbine_status_lines(self) -> list[str]:
if not self.reactor.turbine_unit_active:
return ["n/a"]
return [
f"{idx + 1}:{'ON' if active else 'OFF'}" for idx, active in enumerate(self.reactor.turbine_unit_active)
]
lines: list[str] = []
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:
return sum(t.load_supplied_mw for t in state.turbines)
@@ -504,6 +543,8 @@ class ReactorDashboard:
if not state.generators:
return [("Status", "n/a")]
lines: list[tuple[str, str]] = []
control = "AUTO" if self.reactor.generator_auto else "MANUAL"
lines.append(("Control", control))
for idx, gen in enumerate(state.generators):
status = "RUN" if gen.running else "START" if gen.starting else "OFF"
spool = f" spool {gen.spool_remaining:4.1f}s" if gen.starting else ""
@@ -518,17 +559,18 @@ class ReactorDashboard:
return height - 2
win.addstr(start_y, 2, "Component Health", curses.A_BOLD | curses.color_pair(1))
bar_width = max(8, min(inner_width - 18, 40))
label_width = 16
row = start_y + 1
for name, comp in self.reactor.health_monitor.components.items():
if row >= height - 1:
break
label = f"{name:<12}"
label = f"{name:<{label_width}}"
target = 0.0 if comp.failed else comp.integrity
filled = int(bar_width * max(0.0, min(1.0, target)))
bar = "#" * filled + "-" * (bar_width - filled)
color = 3 if comp.integrity > 0.5 else 2 if comp.integrity > 0.2 else 4
win.addstr(row, 4, f"{label}:")
bar_start = 4 + len(label) + 1
bar_start = 4 + label_width + 1
win.addstr(row, bar_start, bar[:bar_width], curses.color_pair(color))
percent_text = "FAILED" if comp.failed else f"{comp.integrity*100:5.1f}%"
percent_x = min(width - len(percent_text) - 2, bar_start + bar_width + 2)
@@ -540,7 +582,8 @@ class ReactorDashboard:
if index >= len(pumps):
return "n/a"
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:
if self.reactor.consumer:

View File

@@ -17,6 +17,7 @@ class GeneratorState:
spool_remaining: float
power_output_mw: float
battery_charge: float
status: str = "OFF"
@dataclass
@@ -32,6 +33,7 @@ class DieselGenerator:
return
state.starting = True
state.spool_remaining = self.spool_time
state.status = "STARTING"
LOGGER.info("Generator starting (spool %.0fs)", self.spool_time)
def stop(self, state: GeneratorState) -> None:
@@ -41,6 +43,7 @@ class DieselGenerator:
state.starting = False
state.spool_remaining = 0.0
state.power_output_mw = 0.0
state.status = "OFF"
LOGGER.info("Generator stopped")
def step(self, state: GeneratorState, load_demand_mw: float, dt: float) -> float:
@@ -51,12 +54,15 @@ class DieselGenerator:
if state.spool_remaining <= 0.0:
state.starting = False
state.running = True
state.status = "RUN"
LOGGER.info("Generator online at %.1f MW", self.rated_output_mw)
elif state.running:
available = self.rated_output_mw
state.power_output_mw = min(available, load_demand_mw)
state.status = "RUN" if state.power_output_mw > 0 else "IDLE"
else:
state.power_output_mw = 0.0
state.status = "OFF"
if state.running:
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])
shutdown: bool = False
meltdown: bool = False
generator_auto: bool = True
poison_alerts: set[str] = field(default_factory=set)
maintenance_active: set[str] = field(default_factory=set)
@@ -96,6 +97,8 @@ class Reactor:
self.meltdown = False
self.primary_pump_active = False
self.secondary_pump_active = False
self.primary_pump_units = [False] * len(self.primary_pump_units)
self.secondary_pump_units = [False] * len(self.secondary_pump_units)
self.turbine_unit_active = [False] * len(self.turbines)
self.turbine_active = any(self.turbine_unit_active)
if self.consumer:
@@ -115,10 +118,18 @@ class Reactor:
mass_flow_rate=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)]
secondary_pumps = [PumpState(active=self.secondary_pump_active, flow_rate=0.0, pressure=0.5) for _ in range(2)]
primary_pumps = [
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 = [
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
]
turbine_states = [
@@ -218,6 +229,14 @@ class Reactor:
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
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
loop_pressure = max(loop_pressure, pump_state.pressure)
state.primary_loop.mass_flow_rate = total_flow
@@ -239,6 +258,7 @@ class Reactor:
pump_state.pressure = self._ramp_value(
pump_state.pressure, state.primary_loop.pressure, dt, self.primary_pump.spool_time
)
pump_state.status = "STOPPING" if pump_state.flow_rate > 1.0 else "OFF"
if self.secondary_pump_active:
total_flow = 0.0
target_pressure = 12.0 * 0.75 + 2.0
@@ -257,6 +277,14 @@ class Reactor:
pump_state.pressure, desired_pressure, dt, self.secondary_pump.spool_time
)
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
loop_pressure = max(loop_pressure, pump_state.pressure)
state.secondary_loop.mass_flow_rate = total_flow
@@ -278,6 +306,7 @@ class Reactor:
pump_state.pressure = self._ramp_value(
pump_state.pressure, state.secondary_loop.pressure, dt, self.secondary_pump.spool_time
)
pump_state.status = "STOPPING" if pump_state.flow_rate > 1.0 else "OFF"
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:
@@ -334,8 +363,15 @@ class Reactor:
turbine_state = state.turbines[idx]
if idx in active_indices:
turbine.step(state.secondary_loop, turbine_state, steam_power_mw=power_per_unit, dt=dt)
if power_per_unit <= 0.0 and turbine_state.electrical_output_mw < 0.1:
turbine_state.status = "OFF"
elif turbine_state.electrical_output_mw < max(0.5, power_per_unit * 0.5):
turbine_state.status = "STARTING"
else:
turbine_state.status = "RUN"
else:
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)
def _reset_turbine_state(self, turbine_state: TurbineState) -> None:
@@ -343,6 +379,7 @@ class Reactor:
turbine_state.electrical_output_mw = 0.0
turbine_state.load_demand_mw = 0.0
turbine_state.load_supplied_mw = 0.0
turbine_state.status = "OFF"
@staticmethod
def _ramp_value(current: float, target: float, dt: float, time_constant: float) -> float:
@@ -405,6 +442,8 @@ class Reactor:
self.shutdown = True
overrides["rod_fraction"] = self.control.scram()
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:
self.control.set_power_setpoint(command.power_setpoint)
if command.rod_manual is not None:
@@ -446,8 +485,6 @@ class Reactor:
LOGGER.info("Primary pump %s", "enabled" if active else "stopped")
if not active:
self.primary_pump_units = [False] * len(self.primary_pump_units)
elif active and not any(self.primary_pump_units):
self.primary_pump_units = [True] * len(self.primary_pump_units)
def _set_secondary_pump(self, active: bool) -> None:
if self.secondary_pump_active != active:
@@ -455,8 +492,6 @@ class Reactor:
LOGGER.info("Secondary pump %s", "enabled" if active else "stopped")
if not active:
self.secondary_pump_units = [False] * len(self.secondary_pump_units)
elif active and not any(self.secondary_pump_units):
self.secondary_pump_units = [True] * len(self.secondary_pump_units)
def _toggle_primary_pump_unit(self, index: int, active: bool) -> None:
if index < 0 or index >= len(self.primary_pump_units):
@@ -512,22 +547,25 @@ class Reactor:
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)
if deficit > 0.0:
for idx, gen_state in enumerate(state.generators):
if not (gen_state.running or gen_state.starting):
self.generators[idx].start(gen_state)
deficit -= self.generators[idx].rated_output_mw
if deficit <= 0:
break
elif turbine_electric > aux_demand:
for idx, gen_state in enumerate(state.generators):
if gen_state.running and not gen_state.starting:
self.generators[idx].stop(gen_state)
if self.generator_auto:
if deficit > 0.0:
for idx, gen_state in enumerate(state.generators):
if not (gen_state.running or gen_state.starting):
self.generators[idx].start(gen_state)
deficit -= self.generators[idx].rated_output_mw
if deficit <= 0:
break
elif turbine_electric > aux_demand:
for idx, gen_state in enumerate(state.generators):
if gen_state.running and not gen_state.starting:
self.generators[idx].stop(gen_state)
total_power = 0.0
remaining = max(0.0, aux_demand - turbine_electric)
active_indices = [idx for idx, g in enumerate(state.generators) if g.running or g.starting]
share = remaining / len(active_indices) if active_indices and remaining > 0 else 0.0
for idx, gen_state in enumerate(state.generators):
load = remaining if remaining > 0 else 0.0
load = share if idx in active_indices else 0.0
delivered = self.generators[idx].step(gen_state, load, dt)
total_power += delivered
remaining = max(0.0, remaining - delivered)
@@ -640,6 +678,7 @@ class Reactor:
"turbine_units": self.turbine_unit_active,
"shutdown": self.shutdown,
"meltdown": self.meltdown,
"generator_auto": self.generator_auto,
"maintenance_active": list(self.maintenance_active),
"generators": [
{
@@ -648,6 +687,7 @@ class Reactor:
"spool_remaining": g.spool_remaining,
"power_output_mw": g.power_output_mw,
"battery_charge": g.battery_charge,
"status": g.status,
}
for g in state.generators
],
@@ -671,6 +711,7 @@ class Reactor:
self.turbine_active = metadata.get("turbine_active", any(self.turbine_unit_active))
self.shutdown = metadata.get("shutdown", self.shutdown)
self.meltdown = metadata.get("meltdown", self.meltdown)
self.generator_auto = metadata.get("generator_auto", self.generator_auto)
maint = metadata.get("maintenance_active")
if maint is not None:
self.maintenance_active = set(maint)
@@ -691,7 +732,12 @@ class Reactor:
# Back-fill pump state lists for compatibility.
if not plant.primary_pumps or len(plant.primary_pumps) < 2:
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)
]
if not plant.secondary_pumps or len(plant.secondary_pumps) < 2:
@@ -700,6 +746,7 @@ class Reactor:
active=self.secondary_pump_active,
flow_rate=plant.secondary_loop.mass_flow_rate / 2,
pressure=plant.secondary_loop.pressure,
status="OFF",
)
for _ in range(2)
]
@@ -726,6 +773,7 @@ class Reactor:
spool_remaining=0.0,
power_output_mw=0.0,
battery_charge=1.0,
status="OFF",
)
)
for idx, gen_state in enumerate(plant.generators):
@@ -736,6 +784,7 @@ class Reactor:
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.battery_charge = cfg.get("battery_charge", gen_state.battery_charge)
gen_state.status = cfg.get("status", gen_state.status)
return plant
def _handle_heat_sink_loss(self, state: PlantState) -> None:

View File

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

View File

@@ -162,6 +162,8 @@ def test_full_rod_withdrawal_reaches_gigawatt_power():
reactor.control.rod_fraction = 0.0
reactor.primary_pump_active = True
reactor.secondary_pump_active = True
reactor.primary_pump_units = [True, True]
reactor.secondary_pump_units = [True, True]
early_power = 0.0
for step in range(60):
@@ -180,6 +182,8 @@ def test_partially_inserted_rods_hold_near_three_gw():
reactor.control.rod_fraction = 0.4
reactor.primary_pump_active = True
reactor.secondary_pump_active = True
reactor.primary_pump_units = [True, True]
reactor.secondary_pump_units = [True, True]
for _ in range(120):
reactor.step(state, dt=1.0)
@@ -206,6 +210,30 @@ def test_generator_spools_and_powers_pumps():
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():
reactor = Reactor.default()
state = reactor.initial_state()