Add toggleable maintenance state and status panel
This commit is contained in:
@@ -304,13 +304,14 @@ 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"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
y = self._draw_section(win, y, "Maintenance", self._maintenance_lines())
|
||||||
self._draw_health_bar(win, y + 1)
|
self._draw_health_bar(win, y + 1)
|
||||||
|
|
||||||
def _draw_help_panel(self, win: "curses._CursesWindow") -> None:
|
def _draw_help_panel(self, win: "curses._CursesWindow") -> None:
|
||||||
@@ -415,6 +416,11 @@ class ReactorDashboard:
|
|||||||
lines.append(("Alphas", f"{particles.get('alpha', 0.0):9.2e}"))
|
lines.append(("Alphas", f"{particles.get('alpha', 0.0):9.2e}"))
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
|
def _maintenance_lines(self) -> list[tuple[str, str]]:
|
||||||
|
if not self.reactor.maintenance_active:
|
||||||
|
return [("Active", "None")]
|
||||||
|
return [(comp, "IN PROGRESS") for comp in sorted(self.reactor.maintenance_active)]
|
||||||
|
|
||||||
def _draw_health_bar(self, win: "curses._CursesWindow", start_y: int) -> None:
|
def _draw_health_bar(self, win: "curses._CursesWindow", start_y: int) -> None:
|
||||||
height, width = win.getmaxyx()
|
height, width = win.getmaxyx()
|
||||||
if start_y >= height - 2:
|
if start_y >= height - 2:
|
||||||
|
|||||||
@@ -40,6 +40,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
|
||||||
poison_alerts: set[str] = field(default_factory=set)
|
poison_alerts: set[str] = field(default_factory=set)
|
||||||
|
maintenance_active: set[str] = field(default_factory=set)
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if not self.turbines:
|
if not self.turbines:
|
||||||
@@ -168,6 +169,7 @@ class Reactor:
|
|||||||
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)
|
||||||
|
self._maintenance_tick(state, dt)
|
||||||
|
|
||||||
if (not self.secondary_pump_active or state.secondary_loop.mass_flow_rate <= 1.0) and total_power > 50.0:
|
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)
|
self._handle_heat_sink_loss(state)
|
||||||
@@ -289,7 +291,7 @@ class Reactor:
|
|||||||
if command.coolant_demand is not None:
|
if command.coolant_demand is not None:
|
||||||
overrides["coolant_demand"] = max(0.0, min(1.0, command.coolant_demand))
|
overrides["coolant_demand"] = max(0.0, min(1.0, command.coolant_demand))
|
||||||
for component in command.maintenance_components:
|
for component in command.maintenance_components:
|
||||||
self._perform_maintenance(component)
|
self._toggle_maintenance(component)
|
||||||
return overrides
|
return overrides
|
||||||
|
|
||||||
def _set_primary_pump(self, active: bool) -> None:
|
def _set_primary_pump(self, active: bool) -> None:
|
||||||
@@ -324,24 +326,54 @@ class Reactor:
|
|||||||
return -1
|
return -1
|
||||||
|
|
||||||
def _perform_maintenance(self, component: str) -> None:
|
def _perform_maintenance(self, component: str) -> None:
|
||||||
|
if not self._can_maintain(component):
|
||||||
|
return
|
||||||
|
self.health_monitor.maintain(component)
|
||||||
|
|
||||||
|
def _maintenance_tick(self, state: PlantState, dt: float) -> None:
|
||||||
|
if not self.maintenance_active:
|
||||||
|
return
|
||||||
|
completed: list[str] = []
|
||||||
|
for component in list(self.maintenance_active):
|
||||||
|
if not self._can_maintain(component):
|
||||||
|
continue
|
||||||
|
restored = self.health_monitor.maintain(component, amount=0.02 * dt)
|
||||||
|
comp_state = self.health_monitor.component(component)
|
||||||
|
if comp_state.integrity >= 0.999:
|
||||||
|
completed.append(component)
|
||||||
|
for comp in completed:
|
||||||
|
self.maintenance_active.discard(comp)
|
||||||
|
LOGGER.info("Maintenance completed for %s", comp)
|
||||||
|
|
||||||
|
def _toggle_maintenance(self, component: str) -> None:
|
||||||
|
if component in self.maintenance_active:
|
||||||
|
self.maintenance_active.remove(component)
|
||||||
|
LOGGER.info("Maintenance stopped for %s", component)
|
||||||
|
return
|
||||||
|
if not self._can_maintain(component):
|
||||||
|
return
|
||||||
|
self.maintenance_active.add(component)
|
||||||
|
LOGGER.info("Maintenance started for %s", component)
|
||||||
|
|
||||||
|
def _can_maintain(self, component: str) -> bool:
|
||||||
if component == "core" and not self.shutdown:
|
if component == "core" and not self.shutdown:
|
||||||
LOGGER.warning("Cannot maintain core while reactor is running")
|
LOGGER.warning("Cannot maintain core while reactor is running")
|
||||||
return
|
return False
|
||||||
if component == "primary_pump" and self.primary_pump_active:
|
if component == "primary_pump" and self.primary_pump_active:
|
||||||
LOGGER.warning("Stop primary pump before maintenance")
|
LOGGER.warning("Stop primary pump before maintenance")
|
||||||
return
|
return False
|
||||||
if component == "secondary_pump" and self.secondary_pump_active:
|
if component == "secondary_pump" and self.secondary_pump_active:
|
||||||
LOGGER.warning("Stop secondary pump before maintenance")
|
LOGGER.warning("Stop secondary pump before maintenance")
|
||||||
return
|
return False
|
||||||
if component.startswith("turbine"):
|
if component.startswith("turbine"):
|
||||||
idx = self._component_index(component)
|
idx = self._component_index(component)
|
||||||
if idx < 0 or idx >= len(self.turbine_unit_active):
|
if idx < 0 or idx >= len(self.turbine_unit_active):
|
||||||
LOGGER.warning("Unknown turbine maintenance target %s", component)
|
LOGGER.warning("Unknown turbine maintenance target %s", component)
|
||||||
return
|
return False
|
||||||
if self.turbine_unit_active[idx]:
|
if self.turbine_unit_active[idx]:
|
||||||
LOGGER.warning("Stop turbine %d before maintenance", idx + 1)
|
LOGGER.warning("Stop turbine %d before maintenance", idx + 1)
|
||||||
return
|
return False
|
||||||
self.health_monitor.maintain(component)
|
return True
|
||||||
|
|
||||||
def attach_consumer(self, consumer: ElectricalConsumer) -> None:
|
def attach_consumer(self, consumer: ElectricalConsumer) -> None:
|
||||||
self.consumer = consumer
|
self.consumer = consumer
|
||||||
@@ -359,6 +391,7 @@ class Reactor:
|
|||||||
"turbine_active": self.turbine_active,
|
"turbine_active": self.turbine_active,
|
||||||
"turbine_units": self.turbine_unit_active,
|
"turbine_units": self.turbine_unit_active,
|
||||||
"shutdown": self.shutdown,
|
"shutdown": self.shutdown,
|
||||||
|
"maintenance_active": list(self.maintenance_active),
|
||||||
"consumer": {
|
"consumer": {
|
||||||
"online": self.consumer.online if self.consumer else False,
|
"online": self.consumer.online if self.consumer else False,
|
||||||
"demand_mw": self.consumer.demand_mw if self.consumer else 0.0,
|
"demand_mw": self.consumer.demand_mw if self.consumer else 0.0,
|
||||||
@@ -376,6 +409,9 @@ class Reactor:
|
|||||||
self.turbine_unit_active = list(unit_states)
|
self.turbine_unit_active = list(unit_states)
|
||||||
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)
|
||||||
|
maint = metadata.get("maintenance_active")
|
||||||
|
if maint is not None:
|
||||||
|
self.maintenance_active = set(maint)
|
||||||
consumer_cfg = metadata.get("consumer")
|
consumer_cfg = metadata.get("consumer")
|
||||||
if consumer_cfg:
|
if consumer_cfg:
|
||||||
if not self.consumer:
|
if not self.consumer:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from pathlib import Path
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from reactor_sim import constants
|
from reactor_sim import constants
|
||||||
|
from reactor_sim.commands import ReactorCommand
|
||||||
from reactor_sim.failures import HealthMonitor
|
from reactor_sim.failures import HealthMonitor
|
||||||
from reactor_sim.reactor import Reactor
|
from reactor_sim.reactor import Reactor
|
||||||
from reactor_sim.simulation import ReactorSimulation
|
from reactor_sim.simulation import ReactorSimulation
|
||||||
@@ -79,3 +80,20 @@ def test_cold_shutdown_stays_subcritical():
|
|||||||
reactor.step(state, dt=1.0)
|
reactor.step(state, dt=1.0)
|
||||||
assert state.core.power_output_mw <= initial_power + 0.5
|
assert state.core.power_output_mw <= initial_power + 0.5
|
||||||
assert reactor.shutdown is True
|
assert reactor.shutdown is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_toggle_maintenance_progresses_until_restored():
|
||||||
|
reactor = Reactor.default()
|
||||||
|
reactor.primary_pump_active = False
|
||||||
|
pump = reactor.health_monitor.component("primary_pump")
|
||||||
|
pump.integrity = 0.2
|
||||||
|
|
||||||
|
def provider(t: float, _state):
|
||||||
|
if t == 0:
|
||||||
|
return ReactorCommand.maintain("primary_pump")
|
||||||
|
return None
|
||||||
|
|
||||||
|
sim = ReactorSimulation(reactor, timestep=1.0, duration=50.0, command_provider=provider)
|
||||||
|
sim.log()
|
||||||
|
assert pump.integrity >= 0.99
|
||||||
|
assert "primary_pump" not in reactor.maintenance_active
|
||||||
|
|||||||
Reference in New Issue
Block a user