Handle heat-sink loss and update turbine controls
This commit is contained in:
@@ -12,7 +12,7 @@ All source code lives under `src/reactor_sim`. Submodules map to plant systems:
|
|||||||
- A git remote for `origin` is configured; push changes to `origin/main` once work is complete so dashboards stay in sync.
|
- A git remote for `origin` is configured; push changes to `origin/main` once work is complete so dashboards stay in sync.
|
||||||
|
|
||||||
## Operations & Control Hooks
|
## Operations & Control Hooks
|
||||||
Manual commands live in `reactor_sim.commands.ReactorCommand`. Pass a `command_provider` callable to `ReactorSimulation` to adjust rods, pumps, turbine, coolant demand, or the attached `ElectricalConsumer`. Use `ReactorCommand.scram_all()` for full shutdown, `ReactorCommand(consumer_online=True, consumer_demand=600)` to hook the grid, or toggle pumps (`primary_pump_on=False`) to simulate faults. Control helpers in `control.py` expose `set_rods`, `increment_rods`, and `scram`, and you can switch `set_manual_mode(True)` to pause the automatic rod controller. For hands-on runs, launch the curses dashboard (`FISSION_DASHBOARD=1 FISSION_REALTIME=1 python run_simulation.py`) and use the on-screen shortcuts (q quit/save, space SCRAM, p/o pumps, t turbine, 1/2/3 toggle individual turbines, r reset/clear saved state, +/- rods in 0.05 steps, [/ ] consumer demand, s/d setpoint, `a` toggles auto/manual rods). Recommended startup: enable manual rods (`a`), withdraw to ~0.3 before ramping the turbine/consumer, then re-enable auto control when you want closed-loop operation.
|
Manual commands live in `reactor_sim.commands.ReactorCommand`. Pass a `command_provider` callable to `ReactorSimulation` to adjust rods, pumps, turbine, coolant demand, or the attached `ElectricalConsumer`. Use `ReactorCommand.scram_all()` for full shutdown, `ReactorCommand(consumer_online=True, consumer_demand=600)` to hook the grid, or toggle pumps (`primary_pump_on=False`) to simulate faults. Control helpers in `control.py` expose `set_rods`, `increment_rods`, and `scram`, and you can switch `set_manual_mode(True)` to pause the automatic rod controller. For hands-on runs, launch the curses dashboard (`FISSION_DASHBOARD=1 FISSION_REALTIME=1 python run_simulation.py`) and use the on-screen shortcuts (q quit/save, space SCRAM, p/o pumps, t turbine, 1/2 toggle individual turbines, y/u turbine maintenance, m/n pump maintenance, k core maintenance, c consumer, r reset/clear saved state, +/- rods in 0.05 steps, [/ ] consumer demand, s/d setpoint, `a` toggles auto/manual rods). Recommended startup: enable manual rods (`a`), withdraw to ~0.3 before ramping the turbine/consumer, then re-enable auto control when you want closed-loop operation.
|
||||||
The plant now boots cold (ambient core temperature, idle pumps); scripts must sequence startup: enable pumps, gradually withdraw rods, connect the consumer after turbine spin-up, and use `ControlSystem.set_power_setpoint` to chase desired output. Set `FISSION_REALTIME=1` to run continuously with real-time pacing; optionally set `FISSION_SIM_DURATION=infinite` for indefinite runs and send SIGINT/Ctrl+C to stop. Use `FISSION_SIM_DURATION=600` (default) for bounded offline batches.
|
The plant now boots cold (ambient core temperature, idle pumps); scripts must sequence startup: enable pumps, gradually withdraw rods, connect the consumer after turbine spin-up, and use `ControlSystem.set_power_setpoint` to chase desired output. Set `FISSION_REALTIME=1` to run continuously with real-time pacing; optionally set `FISSION_SIM_DURATION=infinite` for indefinite runs and send SIGINT/Ctrl+C to stop. Use `FISSION_SIM_DURATION=600` (default) for bounded offline batches.
|
||||||
|
|
||||||
## Coding Style & Naming Conventions
|
## Coding Style & Naming Conventions
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ class ReactorDashboard:
|
|||||||
DashboardKey("p", "Toggle primary pump"),
|
DashboardKey("p", "Toggle primary pump"),
|
||||||
DashboardKey("o", "Toggle secondary pump"),
|
DashboardKey("o", "Toggle secondary pump"),
|
||||||
DashboardKey("t", "Toggle turbine"),
|
DashboardKey("t", "Toggle turbine"),
|
||||||
DashboardKey("1/2/3", "Toggle turbine units 1-3"),
|
DashboardKey("1/2", "Toggle turbine units 1-2"),
|
||||||
DashboardKey("y/u/i", "Maintain turbine 1/2/3"),
|
DashboardKey("y/u", "Maintain turbine 1/2"),
|
||||||
DashboardKey("c", "Toggle consumer"),
|
DashboardKey("c", "Toggle consumer"),
|
||||||
DashboardKey("r", "Reset & clear state"),
|
DashboardKey("r", "Reset & clear state"),
|
||||||
DashboardKey("m", "Maintain primary pump"),
|
DashboardKey("m", "Maintain primary pump"),
|
||||||
@@ -162,8 +162,6 @@ class ReactorDashboard:
|
|||||||
self._queue_command(ReactorCommand.maintain("turbine_1"))
|
self._queue_command(ReactorCommand.maintain("turbine_1"))
|
||||||
elif ch in (ord("u"), ord("U")):
|
elif ch in (ord("u"), ord("U")):
|
||||||
self._queue_command(ReactorCommand.maintain("turbine_2"))
|
self._queue_command(ReactorCommand.maintain("turbine_2"))
|
||||||
elif ch in (ord("i"), ord("I")):
|
|
||||||
self._queue_command(ReactorCommand.maintain("turbine_3"))
|
|
||||||
|
|
||||||
def _queue_command(self, command: ReactorCommand) -> None:
|
def _queue_command(self, command: ReactorCommand) -> None:
|
||||||
if self.pending_command is None:
|
if self.pending_command is None:
|
||||||
@@ -302,12 +300,12 @@ class ReactorDashboard:
|
|||||||
y,
|
y,
|
||||||
"Turbine / Grid",
|
"Turbine / Grid",
|
||||||
[
|
[
|
||||||
("Turbines", " ".join(self._turbine_status_lines())),
|
("Turbines", " ".join(self._turbine_status_lines())),
|
||||||
("Electrical", f"{state.total_electrical_output():7.1f} MW"),
|
("Electrical", f"{state.total_electrical_output():7.1f} MW"),
|
||||||
("Load", f"{self._total_load_supplied(state):7.1f}/{self._total_load_demand(state):7.1f} MW"),
|
("Load", f"{self._total_load_supplied(state):7.1f}/{self._total_load_demand(state):7.1f} MW"),
|
||||||
("Consumer", f"{consumer_status}"),
|
("Consumer", f"{consumer_status}"),
|
||||||
("Demand", f"{consumer_demand:7.1f} MW"),
|
("Demand", f"{consumer_demand:7.1f} MW"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
self._draw_health_bar(win, y + 1)
|
self._draw_health_bar(win, y + 1)
|
||||||
|
|
||||||
@@ -323,8 +321,8 @@ class ReactorDashboard:
|
|||||||
tips = [
|
tips = [
|
||||||
"Start pumps before withdrawing rods.",
|
"Start pumps before withdrawing rods.",
|
||||||
"Bring turbine and consumer online after thermal stabilization.",
|
"Bring turbine and consumer online after thermal stabilization.",
|
||||||
"Toggle turbine units (1/2/3) for staggered maintenance.",
|
"Toggle turbine units (1/2) for staggered maintenance.",
|
||||||
"Use m/n/k/y/u/i to request maintenance (stop equipment first).",
|
"Use m/n/k/y/u to request maintenance (stop equipment first).",
|
||||||
"Press 'r' to reset/clear state if you want a cold start.",
|
"Press 'r' to reset/clear state if you want a cold start.",
|
||||||
"Watch component health to avoid automatic trips.",
|
"Watch component health to avoid automatic trips.",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ class HealthMonitor:
|
|||||||
"primary_pump": ComponentHealth("primary_pump"),
|
"primary_pump": ComponentHealth("primary_pump"),
|
||||||
"secondary_pump": ComponentHealth("secondary_pump"),
|
"secondary_pump": ComponentHealth("secondary_pump"),
|
||||||
}
|
}
|
||||||
for idx in range(3):
|
for idx in range(2):
|
||||||
name = f"turbine_{idx + 1}"
|
name = f"turbine_{idx + 1}"
|
||||||
self.components[name] = ComponentHealth(name)
|
self.components[name] = ComponentHealth(name)
|
||||||
self.failure_log: list[str] = []
|
self.failure_log: list[str] = []
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ class Reactor:
|
|||||||
secondary_pump=Pump(nominal_flow=16_000.0, efficiency=0.85),
|
secondary_pump=Pump(nominal_flow=16_000.0, efficiency=0.85),
|
||||||
thermal=ThermalSolver(),
|
thermal=ThermalSolver(),
|
||||||
steam_generator=SteamGenerator(),
|
steam_generator=SteamGenerator(),
|
||||||
turbines=[Turbine() for _ in range(3)],
|
turbines=[Turbine() for _ in range(2)],
|
||||||
atomic_model=atomic_model,
|
atomic_model=atomic_model,
|
||||||
consumer=ElectricalConsumer(name="Grid", demand_mw=800.0, online=False),
|
consumer=ElectricalConsumer(name="Grid", demand_mw=800.0, online=False),
|
||||||
health_monitor=HealthMonitor(),
|
health_monitor=HealthMonitor(),
|
||||||
@@ -150,11 +150,17 @@ class Reactor:
|
|||||||
state.secondary_loop.pressure = 0.5
|
state.secondary_loop.pressure = 0.5
|
||||||
|
|
||||||
self.thermal.step_core(state.core, state.primary_loop, total_power, dt)
|
self.thermal.step_core(state.core, state.primary_loop, total_power, dt)
|
||||||
transferred = heat_transfer(state.primary_loop, state.secondary_loop, total_power)
|
if not self.secondary_pump_active or state.secondary_loop.mass_flow_rate <= 1.0:
|
||||||
|
transferred = 0.0
|
||||||
|
else:
|
||||||
|
transferred = heat_transfer(state.primary_loop, state.secondary_loop, total_power)
|
||||||
self.thermal.step_secondary(state.secondary_loop, transferred)
|
self.thermal.step_secondary(state.secondary_loop, transferred)
|
||||||
|
|
||||||
self._step_turbine_bank(state, transferred)
|
self._step_turbine_bank(state, transferred)
|
||||||
|
|
||||||
|
if (not self.secondary_pump_active or state.secondary_loop.mass_flow_rate <= 1.0) and total_power > 50.0:
|
||||||
|
self._handle_heat_sink_loss(state)
|
||||||
|
|
||||||
failures = self.health_monitor.evaluate(
|
failures = self.health_monitor.evaluate(
|
||||||
state,
|
state,
|
||||||
self.primary_pump_active,
|
self.primary_pump_active,
|
||||||
@@ -388,6 +394,16 @@ class Reactor:
|
|||||||
)
|
)
|
||||||
return plant
|
return plant
|
||||||
|
|
||||||
|
def _handle_heat_sink_loss(self, state: PlantState) -> None:
|
||||||
|
if not self.shutdown:
|
||||||
|
LOGGER.critical("Loss of secondary heat sink detected. Initiating SCRAM.")
|
||||||
|
self.shutdown = True
|
||||||
|
self.control.scram()
|
||||||
|
self._set_turbine_state(False)
|
||||||
|
# Clear turbine output and demands to reflect lost steam.
|
||||||
|
for turbine_state in state.turbines:
|
||||||
|
self._reset_turbine_state(turbine_state)
|
||||||
|
|
||||||
def _check_poison_alerts(self, state: PlantState) -> None:
|
def _check_poison_alerts(self, state: PlantState) -> None:
|
||||||
inventory = state.core.fission_product_inventory or {}
|
inventory = state.core.fission_product_inventory or {}
|
||||||
for symbol, threshold in constants.KEY_POISON_THRESHOLDS.items():
|
for symbol, threshold in constants.KEY_POISON_THRESHOLDS.items():
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
def heat_transfer(primary: CoolantLoopState, secondary: CoolantLoopState, core_power_mw: float) -> float:
|
def heat_transfer(primary: CoolantLoopState, secondary: CoolantLoopState, core_power_mw: float) -> float:
|
||||||
"""Return MW transferred to the secondary loop."""
|
"""Return MW transferred to the secondary loop."""
|
||||||
|
if secondary.mass_flow_rate <= 0.0:
|
||||||
|
return 0.0
|
||||||
delta_t = max(0.0, primary.temperature_out - secondary.temperature_in)
|
delta_t = max(0.0, primary.temperature_out - secondary.temperature_in)
|
||||||
conductance = 0.15 # steam generator effectiveness
|
conductance = 0.15 # steam generator effectiveness
|
||||||
efficiency = 1.0 - math.exp(-conductance * delta_t)
|
efficiency = 1.0 - math.exp(-conductance * delta_t)
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ def test_health_monitor_flags_core_failure():
|
|||||||
reactor = Reactor.default()
|
reactor = Reactor.default()
|
||||||
state = reactor.initial_state()
|
state = reactor.initial_state()
|
||||||
state.core.fuel_temperature = constants.MAX_CORE_TEMPERATURE
|
state.core.fuel_temperature = constants.MAX_CORE_TEMPERATURE
|
||||||
failures = reactor.health_monitor.evaluate(state, True, True, [True, True, True], dt=200.0)
|
failures = reactor.health_monitor.evaluate(state, True, True, [True, True], dt=200.0)
|
||||||
assert "core" in failures
|
assert "core" in failures
|
||||||
reactor._handle_failure("core")
|
reactor._handle_failure("core")
|
||||||
assert reactor.shutdown is True
|
assert reactor.shutdown is True
|
||||||
@@ -52,3 +52,16 @@ def test_maintenance_recovers_component_health():
|
|||||||
assert restored is True
|
assert restored is True
|
||||||
assert pump.integrity == pytest.approx(0.8)
|
assert pump.integrity == pytest.approx(0.8)
|
||||||
assert pump.failed is False
|
assert pump.failed is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_secondary_pump_loss_triggers_scram_and_no_steam():
|
||||||
|
reactor = Reactor.default()
|
||||||
|
state = reactor.initial_state()
|
||||||
|
# Make sure some power is present to trigger heat-sink logic.
|
||||||
|
state.core.power_output_mw = 500.0
|
||||||
|
reactor.secondary_pump_active = False
|
||||||
|
reactor.control.manual_control = True
|
||||||
|
reactor.control.rod_fraction = 0.1
|
||||||
|
reactor.step(state, dt=1.0)
|
||||||
|
assert reactor.shutdown is True
|
||||||
|
assert all(t.electrical_output_mw == 0.0 for t in state.turbines)
|
||||||
|
|||||||
Reference in New Issue
Block a user