Improve persistence and reactor dynamics
This commit is contained in:
@@ -6,6 +6,7 @@ import curses
|
||||
import logging
|
||||
from collections import deque
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from .commands import ReactorCommand
|
||||
@@ -37,6 +38,7 @@ class ReactorDashboard:
|
||||
self.pending_command: Optional[ReactorCommand] = None
|
||||
self.sim: Optional[ReactorSimulation] = None
|
||||
self.quit_requested = False
|
||||
self.reset_requested = False
|
||||
self.log_buffer: deque[str] = deque(maxlen=4)
|
||||
self._log_handler: Optional[logging.Handler] = None
|
||||
self._previous_handlers: list[logging.Handler] = []
|
||||
@@ -47,7 +49,9 @@ class ReactorDashboard:
|
||||
DashboardKey("p", "Toggle primary pump"),
|
||||
DashboardKey("o", "Toggle secondary pump"),
|
||||
DashboardKey("t", "Toggle turbine"),
|
||||
DashboardKey("1/2/3", "Toggle turbine units 1-3"),
|
||||
DashboardKey("c", "Toggle consumer"),
|
||||
DashboardKey("r", "Reset & clear state"),
|
||||
DashboardKey("+/-", "Withdraw/insert rods"),
|
||||
DashboardKey("[/]", "Adjust consumer demand −/+50 MW"),
|
||||
DashboardKey("s", "Setpoint −250 MW"),
|
||||
@@ -68,24 +72,34 @@ class ReactorDashboard:
|
||||
curses.init_pair(4, curses.COLOR_RED, -1)
|
||||
stdscr.nodelay(True)
|
||||
self._install_log_capture()
|
||||
self.sim = ReactorSimulation(
|
||||
self.reactor,
|
||||
timestep=self.timestep,
|
||||
duration=None,
|
||||
realtime=True,
|
||||
command_provider=self._next_command,
|
||||
)
|
||||
self.sim.start_state = self.start_state
|
||||
try:
|
||||
for state in self.sim.run():
|
||||
self._draw(stdscr, state)
|
||||
self._handle_input(stdscr)
|
||||
while True:
|
||||
self.sim = ReactorSimulation(
|
||||
self.reactor,
|
||||
timestep=self.timestep,
|
||||
duration=None,
|
||||
realtime=True,
|
||||
command_provider=self._next_command,
|
||||
)
|
||||
self.sim.start_state = self.start_state
|
||||
try:
|
||||
for state in self.sim.run():
|
||||
self._draw(stdscr, state)
|
||||
self._handle_input(stdscr)
|
||||
if self.quit_requested or self.reset_requested:
|
||||
self.sim.stop()
|
||||
break
|
||||
finally:
|
||||
if self.save_path and self.sim and self.sim.last_state and not self.reset_requested:
|
||||
self.reactor.save_state(self.save_path, self.sim.last_state)
|
||||
if self.quit_requested:
|
||||
self.sim.stop()
|
||||
break
|
||||
if self.reset_requested:
|
||||
self._clear_saved_state()
|
||||
self._reset_to_greenfield()
|
||||
continue
|
||||
break
|
||||
finally:
|
||||
if self.save_path and self.sim and self.sim.last_state:
|
||||
self.reactor.save_state(self.save_path, self.sim.last_state)
|
||||
self._restore_logging()
|
||||
|
||||
def _handle_input(self, stdscr: "curses._CursesWindow") -> None:
|
||||
@@ -104,6 +118,9 @@ class ReactorDashboard:
|
||||
self._queue_command(ReactorCommand(secondary_pump_on=not self.reactor.secondary_pump_active))
|
||||
elif ch in (ord("t"), ord("T")):
|
||||
self._queue_command(ReactorCommand(turbine_on=not self.reactor.turbine_active))
|
||||
elif ord("1") <= ch <= ord("9"):
|
||||
idx = ch - ord("1")
|
||||
self._toggle_turbine_unit(idx)
|
||||
elif ch in (ord("+"), ord("=")):
|
||||
self._queue_command(ReactorCommand(rod_position=self._clamped_rod(-0.05)))
|
||||
elif ch == ord("-"):
|
||||
@@ -117,6 +134,8 @@ class ReactorDashboard:
|
||||
elif ch in (ord("c"), ord("C")):
|
||||
online = not (self.reactor.consumer.online if self.reactor.consumer else False)
|
||||
self._queue_command(ReactorCommand(consumer_online=online))
|
||||
elif ch in (ord("r"), ord("R")):
|
||||
self._request_reset()
|
||||
elif ch in (ord("s"), ord("S")):
|
||||
self._queue_command(ReactorCommand(power_setpoint=self.reactor.control.setpoint_mw - 250.0))
|
||||
elif ch in (ord("d"), ord("D")):
|
||||
@@ -143,6 +162,35 @@ class ReactorDashboard:
|
||||
if self.pending_command.rod_position is not None and self.pending_command.rod_manual is None:
|
||||
self.pending_command.rod_manual = True
|
||||
|
||||
def _toggle_turbine_unit(self, index: int) -> None:
|
||||
if index < 0 or index >= len(self.reactor.turbine_unit_active):
|
||||
return
|
||||
current = self.reactor.turbine_unit_active[index]
|
||||
self._queue_command(ReactorCommand(turbine_units={index + 1: not current}))
|
||||
|
||||
def _request_reset(self) -> None:
|
||||
self.reset_requested = True
|
||||
if self.sim:
|
||||
self.sim.stop()
|
||||
|
||||
def _clear_saved_state(self) -> None:
|
||||
if not self.save_path:
|
||||
return
|
||||
path = Path(self.save_path)
|
||||
try:
|
||||
path.unlink()
|
||||
LOGGER.info("Cleared saved state at %s", path)
|
||||
except FileNotFoundError:
|
||||
LOGGER.info("No saved state to clear at %s", path)
|
||||
|
||||
def _reset_to_greenfield(self) -> None:
|
||||
LOGGER.info("Resetting reactor to initial state")
|
||||
self.reactor = Reactor.default()
|
||||
self.start_state = None
|
||||
self.pending_command = None
|
||||
self.reset_requested = False
|
||||
self.log_buffer.clear()
|
||||
|
||||
|
||||
def _next_command(self, _: float, __: PlantState) -> Optional[ReactorCommand]:
|
||||
cmd = self.pending_command
|
||||
@@ -230,9 +278,9 @@ class ReactorDashboard:
|
||||
y,
|
||||
"Turbine / Grid",
|
||||
[
|
||||
("Turbine", "ON" if self.reactor.turbine_active else "OFF"),
|
||||
("Electrical", f"{state.turbine.electrical_output_mw:7.1f} MW"),
|
||||
("Load", f"{state.turbine.load_supplied_mw:7.1f}/{state.turbine.load_demand_mw:7.1f} MW"),
|
||||
("Turbines", " ".join(self._turbine_status_lines())),
|
||||
("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"),
|
||||
("Consumer", f"{consumer_status}"),
|
||||
("Demand", f"{consumer_demand:7.1f} MW"),
|
||||
],
|
||||
@@ -251,6 +299,8 @@ class ReactorDashboard:
|
||||
tips = [
|
||||
"Start pumps before withdrawing rods.",
|
||||
"Bring turbine and consumer online after thermal stabilization.",
|
||||
"Toggle turbine units (1/2/3) for staggered maintenance.",
|
||||
"Press 'r' to reset/clear state if you want a cold start.",
|
||||
"Watch component health to avoid automatic trips.",
|
||||
]
|
||||
for idx, tip in enumerate(tips, start=y + 2):
|
||||
@@ -259,11 +309,12 @@ class ReactorDashboard:
|
||||
def _draw_status_panel(self, win: "curses._CursesWindow", state: PlantState) -> None:
|
||||
win.erase()
|
||||
win.hline(0, 0, curses.ACS_HLINE, win.getmaxyx()[1])
|
||||
turbine_text = " ".join(self._turbine_status_lines())
|
||||
msg = (
|
||||
f"Time {state.time_elapsed:7.1f}s | Rods {self.reactor.control.rod_fraction:.3f} | "
|
||||
f"Primary {'ON' if self.reactor.primary_pump_active else 'OFF'} | "
|
||||
f"Secondary {'ON' if self.reactor.secondary_pump_active else 'OFF'} | "
|
||||
f"Turbine {'ON' if self.reactor.turbine_active else 'OFF'}"
|
||||
f"Turbines {turbine_text}"
|
||||
)
|
||||
win.addstr(1, 1, msg, curses.color_pair(3))
|
||||
if self.reactor.health_monitor.failure_log:
|
||||
@@ -306,6 +357,19 @@ class ReactorDashboard:
|
||||
row += 1
|
||||
return row + 1
|
||||
|
||||
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)
|
||||
]
|
||||
|
||||
def _total_load_supplied(self, state: PlantState) -> float:
|
||||
return sum(t.load_supplied_mw for t in state.turbines)
|
||||
|
||||
def _total_load_demand(self, state: PlantState) -> float:
|
||||
return sum(t.load_demand_mw for t in state.turbines)
|
||||
|
||||
def _draw_health_bar(self, win: "curses._CursesWindow", start_y: int) -> None:
|
||||
height, width = win.getmaxyx()
|
||||
if start_y >= height - 2:
|
||||
|
||||
Reference in New Issue
Block a user