Add schematic dashboard page and F1/F2 navigation

This commit is contained in:
Codex Agent
2025-11-26 22:33:55 +01:00
parent fae85404a7
commit abc1cb79e1
2 changed files with 90 additions and 4 deletions

View File

@@ -7,6 +7,11 @@
- [ ] Introduce CHF/DNB margin, clad/fuel split temps, and SCRAM matrix for subcooling loss or SG level/pressure trips. - [ ] Introduce CHF/DNB margin, clad/fuel split temps, and SCRAM matrix for subcooling loss or SG level/pressure trips.
- [ ] Flesh out condenser behavior: vacuum pump limits, cooling water temperature coupling, and dynamic back-pressure with fouling. - [ ] Flesh out condenser behavior: vacuum pump limits, cooling water temperature coupling, and dynamic back-pressure with fouling.
- [ ] Dashboard polish: compact turbine/generator rows, color critical warnings (SCRAM/heat-sink), and reduce repeated log noise. - [ ] Dashboard polish: compact turbine/generator rows, color critical warnings (SCRAM/heat-sink), and reduce repeated log noise.
- [ ] Dashboard multi-page view (F1/F2):
- F1 retains current numeric layout.
- F2 adds an ASCII schematic of the plant (core, primary pumps/pressurizer/HX, secondary pumps/drum, turbines/gens/consumer, reliefs/condenser) with inline key values (flows, pressures, steam quality/enthalpy, MW) and simple animations for flow/status.
- Add page indicator/status hint and size checks; keep updates performant (prebuilt template, minimal redraws).
- Optional: color-coded states (RUN/START/CAV/RELIEF) and blinking for alarms.
- [ ] Incremental realism plan: - [ ] Incremental realism plan:
- Add stored enthalpy for primary/secondary loops and a steam-drum mass/energy balance (sensible + latent) while keeping existing pump logic and tests passing. Target representative PWR conditions: primary 1516 MPa, 290320 °C inlet/320330 °C outlet, secondary saturation ~67 MPa with boil at ~490510 K. - Add stored enthalpy for primary/secondary loops and a steam-drum mass/energy balance (sensible + latent) while keeping existing pump logic and tests passing. Target representative PWR conditions: primary 1516 MPa, 290320 °C inlet/320330 °C outlet, secondary saturation ~67 MPa with boil at ~490510 K.
- Adjust HX/pressure handling to use stored energy (saturation clamp and pressure rise) and validate steam formation with both pumps at ~3 GW. Use realistic tube-side material assumptions (Inconel 690/SS cladding) and clamp steam quality to phase-equilibrium enthalpy. - Adjust HX/pressure handling to use stored energy (saturation clamp and pressure rise) and validate steam formation with both pumps at ~3 GW. Use realistic tube-side material assumptions (Inconel 690/SS cladding) and clamp steam quality to phase-equilibrium enthalpy.

View File

@@ -13,7 +13,7 @@ from . import constants
from .commands import ReactorCommand from .commands import ReactorCommand
from .reactor import Reactor from .reactor import Reactor
from .simulation import ReactorSimulation from .simulation import ReactorSimulation
from .state import PlantState from .state import PlantState, PumpState
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
@@ -76,6 +76,7 @@ class ReactorDashboard:
self.sim: Optional[ReactorSimulation] = None self.sim: Optional[ReactorSimulation] = None
self.quit_requested = False self.quit_requested = False
self.reset_requested = False self.reset_requested = False
self.page = 1 # 1=metrics, 2=schematic
self._last_state: Optional[PlantState] = None self._last_state: Optional[PlantState] = None
self._trend_history: deque[tuple[float, float, float]] = deque(maxlen=120) self._trend_history: deque[tuple[float, float, float]] = deque(maxlen=120)
self.log_buffer: deque[str] = deque(maxlen=8) self.log_buffer: deque[str] = deque(maxlen=8)
@@ -90,6 +91,7 @@ class ReactorDashboard:
DashboardKey("space", "SCRAM"), DashboardKey("space", "SCRAM"),
DashboardKey("r", "Reset & clear state"), DashboardKey("r", "Reset & clear state"),
DashboardKey("a", "Toggle auto rod control"), DashboardKey("a", "Toggle auto rod control"),
DashboardKey("F1/F2", "Metrics / schematic views"),
DashboardKey("+/-", "Withdraw/insert rods"), DashboardKey("+/-", "Withdraw/insert rods"),
DashboardKey("1-9 / Numpad", "Set rods to 0.1 … 0.9 (manual)"), DashboardKey("1-9 / Numpad", "Set rods to 0.1 … 0.9 (manual)"),
DashboardKey("[/]", "Adjust consumer demand /+50 MW"), DashboardKey("[/]", "Adjust consumer demand /+50 MW"),
@@ -213,6 +215,10 @@ class ReactorDashboard:
self._queue_command(ReactorCommand(generator_auto=not self.reactor.generator_auto)) 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 ch == curses.KEY_F1:
self.page = 1
elif ch == curses.KEY_F2:
self.page = 2
elif keyname and keyname.decode(errors="ignore") in ("!", "@", "#", '"'): elif keyname and keyname.decode(errors="ignore") in ("!", "@", "#", '"'):
name = keyname.decode(errors="ignore") name = keyname.decode(errors="ignore")
turbine_hotkeys = {"!": 0, "@": 1, "#": 2, '"': 1} turbine_hotkeys = {"!": 0, "@": 1, "#": 2, '"': 1}
@@ -228,7 +234,7 @@ class ReactorDashboard:
self._queue_command(ReactorCommand(rod_position=target, rod_manual=True)) self._queue_command(ReactorCommand(rod_position=target, rod_manual=True))
elif ch in _NUMPAD_ROD_KEYS: elif ch in _NUMPAD_ROD_KEYS:
self._queue_command(ReactorCommand(rod_position=_NUMPAD_ROD_KEYS[ch], rod_manual=True)) self._queue_command(ReactorCommand(rod_position=_NUMPAD_ROD_KEYS[ch], rod_manual=True))
elif curses.KEY_F1 <= ch <= curses.KEY_F9: elif curses.KEY_F3 <= ch <= curses.KEY_F9:
target = (ch - curses.KEY_F1 + 1) / 10.0 target = (ch - curses.KEY_F1 + 1) / 10.0
self._queue_command(ReactorCommand(rod_position=target, rod_manual=True)) self._queue_command(ReactorCommand(rod_position=target, rod_manual=True))
elif ch in (ord("+"), ord("=")): elif ch in (ord("+"), ord("=")):
@@ -379,7 +385,10 @@ class ReactorDashboard:
help_win = stdscr.derwin(data_height, right_width, 0, left_width + gap) help_win = stdscr.derwin(data_height, right_width, 0, left_width + gap)
status_win = stdscr.derwin(status_height, width, data_height, 0) status_win = stdscr.derwin(status_height, width, data_height, 0)
if self.page == 1:
self._draw_data_panel(data_win, state) self._draw_data_panel(data_win, state)
else:
self._draw_schematic_panel(data_win, state)
self._draw_help_panel(help_win) self._draw_help_panel(help_win)
self._draw_status_panel(status_win, state) self._draw_status_panel(status_win, state)
stdscr.refresh() stdscr.refresh()
@@ -563,7 +572,7 @@ class ReactorDashboard:
f"Time {state.time_elapsed:7.1f}s | Rods {self.reactor.control.rod_fraction:.3f} | " 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"Primary {'ON' if self.reactor.primary_pump_active else 'OFF'} | "
f"Secondary {'ON' if self.reactor.secondary_pump_active else 'OFF'} | " f"Secondary {'ON' if self.reactor.secondary_pump_active else 'OFF'} | "
f"Turbines {turbine_text}" f"Turbines {turbine_text} | Page {'Metrics' if self.page == 1 else 'Schematic'}"
) )
win.addstr(1, 1, msg, curses.color_pair(3)) win.addstr(1, 1, msg, curses.color_pair(3))
if self.reactor.health_monitor.failure_log: if self.reactor.health_monitor.failure_log:
@@ -608,6 +617,78 @@ class ReactorDashboard:
row += 1 row += 1
return row + 1 return row + 1
def _flow_arrow(self, flow: float) -> str:
if flow > 15000:
return "====>"
if flow > 5000:
return "===>"
if flow > 500:
return "->"
return "--"
def _pump_glyph(self, pump_state: PumpState | None) -> str:
if pump_state is None:
return "·"
status = getattr(pump_state, "status", "OFF")
if status == "RUN":
return ""
if status == "CAV":
return "!"
if status == "STARTING":
return ">"
if status == "STOPPING":
return "-"
return "·"
def _draw_schematic_panel(self, win: "curses._CursesWindow", state: PlantState) -> None:
win.erase()
win.box()
try:
win.addstr(0, 2, " Plant Schematic ", curses.color_pair(1) | curses.A_BOLD)
except curses.error:
pass
height, width = win.getmaxyx()
prim = state.primary_loop
sec = state.secondary_loop
p_pumps = state.primary_pumps if state.primary_pumps else []
s_pumps = state.secondary_pumps if state.secondary_pumps else []
p1 = p_pumps[0] if len(p_pumps) > 0 else None
p2 = p_pumps[1] if len(p_pumps) > 1 else None
s1 = s_pumps[0] if len(s_pumps) > 0 else None
s2 = s_pumps[1] if len(s_pumps) > 1 else None
steam_avail = self._steam_available_power(state)
enthalpy = state.turbines[0].steam_enthalpy if state.turbines else 0.0
lines = [
f"CORE {state.core.power_output_mw:5.0f}MW {state.core.fuel_temperature:5.0f}K | Rods {self.reactor.control.rod_fraction:.2f} ({'AUTO' if not self.reactor.control.manual_control else 'MAN'})",
f"Primary Flow: {self._flow_arrow(prim.mass_flow_rate)} {prim.mass_flow_rate:7.0f} kg/s | ΔT hx={state.primary_to_secondary_delta_t:4.0f}K eff={state.heat_exchanger_efficiency*100:5.1f}%",
f"Pumps P1[{self._pump_glyph(p1)}]{(p1.flow_rate if p1 else 0):6.0f}kg/s P2[{self._pump_glyph(p2)}]{(p2.flow_rate if p2 else 0):6.0f}kg/s Relief:{'OPEN' if self.reactor.primary_relief_open else 'CLOSED'}",
f"Ppri={prim.pressure:4.1f}MPa | Tin={prim.temperature_in:6.1f}K Tout={prim.temperature_out:6.1f}K",
"-" * max(10, min(width - 4, 70)),
f"Secondary Flow: {self._flow_arrow(sec.mass_flow_rate)} {sec.mass_flow_rate:7.0f} kg/s | Steam q={sec.steam_quality:4.2f} h={enthalpy:5.0f} kJ/kg avail={steam_avail:5.1f}MW",
f"Pumps S1[{self._pump_glyph(s1)}]{(s1.flow_rate if s1 else 0):6.0f}kg/s S2[{self._pump_glyph(s2)}]{(s2.flow_rate if s2 else 0):6.0f}kg/s Relief:{'OPEN' if self.reactor.secondary_relief_open else 'CLOSED'}",
f"Psec={sec.pressure:4.1f}MPa | Tin={sec.temperature_in:6.1f}K Tout={sec.temperature_out:6.1f}K",
f"Drum Level={sec.level*100:5.1f}% Energy={sec.energy_j/1e6:7.0f} MJ",
]
turbine_bits = []
for idx, t_state in enumerate(state.turbines):
turbine_bits.append(f"T{idx+1}:{t_state.electrical_output_mw:4.0f}MW")
lines.append("Turbines " + " ".join(turbine_bits) if turbine_bits else "Turbines n/a")
consumer_status = "OFF"
demand = 0.0
if self.reactor.consumer:
consumer_status = "ONLINE" if self.reactor.consumer.online else "OFF"
demand = self.reactor.consumer.demand_mw
lines.append(f"Consumer {consumer_status} demand={demand:5.0f}MW supplied={state.total_electrical_output():5.0f}MW")
for row, text in enumerate(lines, start=1):
if row >= height - 1:
break
try:
win.addstr(row, 2, text[: max(1, width - 3)])
except curses.error:
continue
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"]