chore: refine dashboard layout and timestep
This commit is contained in:
@@ -53,6 +53,12 @@ class ReactorDashboard:
|
||||
|
||||
def _main(self, stdscr: "curses._CursesWindow") -> None: # type: ignore[name-defined]
|
||||
curses.curs_set(0)
|
||||
curses.start_color()
|
||||
curses.use_default_colors()
|
||||
curses.init_pair(1, curses.COLOR_CYAN, -1)
|
||||
curses.init_pair(2, curses.COLOR_YELLOW, -1)
|
||||
curses.init_pair(3, curses.COLOR_GREEN, -1)
|
||||
curses.init_pair(4, curses.COLOR_RED, -1)
|
||||
stdscr.nodelay(True)
|
||||
self.sim = ReactorSimulation(
|
||||
self.reactor,
|
||||
@@ -65,7 +71,7 @@ class ReactorDashboard:
|
||||
try:
|
||||
for state in self.sim.run():
|
||||
self._draw(stdscr, state)
|
||||
self._handle_input(stdscr, state)
|
||||
self._handle_input(stdscr)
|
||||
if self.quit_requested:
|
||||
self.sim.stop()
|
||||
break
|
||||
@@ -73,7 +79,7 @@ class ReactorDashboard:
|
||||
if self.save_path and self.sim and self.sim.last_state:
|
||||
self.reactor.save_state(self.save_path, self.sim.last_state)
|
||||
|
||||
def _handle_input(self, stdscr: "curses._CursesWindow", state: PlantState) -> None:
|
||||
def _handle_input(self, stdscr: "curses._CursesWindow") -> None:
|
||||
while True:
|
||||
ch = stdscr.getch()
|
||||
if ch == -1:
|
||||
@@ -110,76 +116,168 @@ class ReactorDashboard:
|
||||
def _queue_command(self, command: ReactorCommand) -> None:
|
||||
if self.pending_command is None:
|
||||
self.pending_command = command
|
||||
else:
|
||||
for field in command.__dataclass_fields__: # type: ignore[attr-defined]
|
||||
value = getattr(command, field)
|
||||
if value is None or value is False:
|
||||
continue
|
||||
if field == "rod_step" and getattr(self.pending_command, field) is not None:
|
||||
setattr(
|
||||
self.pending_command,
|
||||
field,
|
||||
getattr(self.pending_command, field) + value,
|
||||
)
|
||||
else:
|
||||
setattr(self.pending_command, field, value)
|
||||
return
|
||||
for field in command.__dataclass_fields__: # type: ignore[attr-defined]
|
||||
value = getattr(command, field)
|
||||
if value is None or value is False:
|
||||
continue
|
||||
if field == "rod_step" and getattr(self.pending_command, field) is not None:
|
||||
setattr(
|
||||
self.pending_command,
|
||||
field,
|
||||
getattr(self.pending_command, field) + value,
|
||||
)
|
||||
else:
|
||||
setattr(self.pending_command, field, value)
|
||||
|
||||
def _next_command(self, sim_time: float, _: PlantState) -> Optional[ReactorCommand]:
|
||||
def _next_command(self, _: float, __: PlantState) -> Optional[ReactorCommand]:
|
||||
cmd = self.pending_command
|
||||
self.pending_command = None
|
||||
return cmd
|
||||
|
||||
def _draw(self, stdscr: "curses._CursesWindow", state: PlantState) -> None:
|
||||
stdscr.erase()
|
||||
stdscr.addstr(0, 0, "Realtime Reactor Dashboard".ljust(60))
|
||||
stdscr.addstr(1, 0, f"Time: {state.time_elapsed:8.1f}s Mode: realtime")
|
||||
stdscr.addstr(
|
||||
3,
|
||||
0,
|
||||
f"Core Temp: {state.core.fuel_temperature:8.1f} K Power: {state.core.power_output_mw:8.1f} MW "
|
||||
f"Flux: {state.core.neutron_flux:10.2e}",
|
||||
)
|
||||
stdscr.addstr(
|
||||
4,
|
||||
0,
|
||||
f"Rod fraction: {self.reactor.control.rod_fraction:.3f} Setpoint: {self.reactor.control.setpoint_mw:.0f} MW",
|
||||
)
|
||||
stdscr.addstr(
|
||||
6,
|
||||
0,
|
||||
f"Primary Loop: {'ON ' if self.reactor.primary_pump_active else 'OFF'} "
|
||||
f"Flow {state.primary_loop.mass_flow_rate:7.0f} kg/s Outlet {state.primary_loop.temperature_out:6.1f} K",
|
||||
)
|
||||
stdscr.addstr(
|
||||
7,
|
||||
0,
|
||||
f"Secondary Loop: {'ON ' if self.reactor.secondary_pump_active else 'OFF'} "
|
||||
f"Flow {state.secondary_loop.mass_flow_rate:7.0f} kg/s Pressure {state.secondary_loop.pressure:4.1f} MPa",
|
||||
)
|
||||
stdscr.addstr(
|
||||
9,
|
||||
0,
|
||||
f"Turbine: {'ON ' if self.reactor.turbine_active else 'OFF'} Electrical {state.turbine.electrical_output_mw:7.1f} MW "
|
||||
f"Load {state.turbine.load_supplied_mw:7.1f}/{state.turbine.load_demand_mw:7.1f} MW",
|
||||
)
|
||||
if self.reactor.consumer:
|
||||
height, width = stdscr.getmaxyx()
|
||||
if height < 24 or width < 90:
|
||||
stdscr.addstr(
|
||||
10,
|
||||
0,
|
||||
f"Consumer: {'ONLINE ' if self.reactor.consumer.online else 'OFFLINE'} "
|
||||
f"Demand {self.reactor.consumer.demand_mw:7.1f} MW",
|
||||
0,
|
||||
"Terminal window too small. Resize to at least 90x24.".ljust(width),
|
||||
curses.color_pair(4),
|
||||
)
|
||||
health = self.reactor.health_monitor.components
|
||||
stdscr.addstr(
|
||||
12,
|
||||
0,
|
||||
"Health: " + " ".join(f"{name}:{comp.integrity*100:5.1f}%" for name, comp in health.items()),
|
||||
)
|
||||
stdscr.addstr(14, 0, "Controls:")
|
||||
for idx, key in enumerate(self.keys, start=15):
|
||||
stdscr.addstr(idx, 0, f"{key.key.ljust(8)} - {key.description}")
|
||||
stdscr.refresh()
|
||||
return
|
||||
|
||||
data_height = height - 6
|
||||
left_width = min(max(width - 32, 60), width - 20)
|
||||
right_width = width - left_width
|
||||
|
||||
data_win = stdscr.derwin(data_height, left_width, 0, 0)
|
||||
help_win = stdscr.derwin(data_height, right_width, 0, left_width)
|
||||
status_win = stdscr.derwin(6, width, data_height, 0)
|
||||
|
||||
self._draw_data_panel(data_win, state)
|
||||
self._draw_help_panel(help_win)
|
||||
self._draw_status_panel(status_win, state)
|
||||
stdscr.refresh()
|
||||
|
||||
def _draw_data_panel(self, win: "curses._CursesWindow", state: PlantState) -> None:
|
||||
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,
|
||||
"Core",
|
||||
[
|
||||
f"Tfuel {state.core.fuel_temperature:8.1f} K Power {state.core.power_output_mw:8.1f} MW",
|
||||
f"Neutron Flux {state.core.neutron_flux:10.2e} Rods {self.reactor.control.rod_fraction:.3f}",
|
||||
f"Setpoint {self.reactor.control.setpoint_mw:7.0f} MW Reactivity {state.core.reactivity_margin:+.4f}",
|
||||
],
|
||||
)
|
||||
y = self._draw_section(
|
||||
win,
|
||||
y,
|
||||
"Primary Loop",
|
||||
[
|
||||
f"Pump {'ON ' if self.reactor.primary_pump_active else 'OFF'} "
|
||||
f"Flow {state.primary_loop.mass_flow_rate:7.0f} kg/s",
|
||||
f"T_in {state.primary_loop.temperature_in:7.1f} K "
|
||||
f"T_out {state.primary_loop.temperature_out:7.1f} K",
|
||||
f"Pressure {state.primary_loop.pressure:5.2f} MPa",
|
||||
],
|
||||
)
|
||||
y = self._draw_section(
|
||||
win,
|
||||
y,
|
||||
"Secondary Loop",
|
||||
[
|
||||
f"Pump {'ON ' if self.reactor.secondary_pump_active else 'OFF'} "
|
||||
f"Flow {state.secondary_loop.mass_flow_rate:7.0f} kg/s",
|
||||
f"T_in {state.secondary_loop.temperature_in:7.1f} K "
|
||||
f"Pressure {state.secondary_loop.pressure:5.2f} MPa",
|
||||
f"Steam quality {state.secondary_loop.steam_quality:5.2f}",
|
||||
],
|
||||
)
|
||||
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,
|
||||
"Turbine / Grid",
|
||||
[
|
||||
f"Turbine {'ON ' if self.reactor.turbine_active else 'OFF'} "
|
||||
f"Electrical {state.turbine.electrical_output_mw:7.1f} MW",
|
||||
f"Load {state.turbine.load_supplied_mw:7.1f}/{state.turbine.load_demand_mw:7.1f} MW",
|
||||
f"Consumer {consumer_status} demand {consumer_demand:7.1f} MW",
|
||||
],
|
||||
)
|
||||
self._draw_health_bar(win, y + 1)
|
||||
|
||||
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}")
|
||||
y += 1
|
||||
win.addstr(y + 1, 2, "Tips:", curses.color_pair(2) | curses.A_BOLD)
|
||||
tips = [
|
||||
"Start pumps before withdrawing rods.",
|
||||
"Bring turbine and consumer online after thermal stabilization.",
|
||||
"Watch component health to avoid automatic trips.",
|
||||
]
|
||||
for idx, tip in enumerate(tips, start=y + 2):
|
||||
win.addstr(idx, 4, f"- {tip}")
|
||||
|
||||
def _draw_status_panel(self, win: "curses._CursesWindow", state: PlantState) -> None:
|
||||
win.erase()
|
||||
win.hline(0, 0, curses.ACS_HLINE, win.getmaxyx()[1])
|
||||
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'}"
|
||||
)
|
||||
win.addstr(1, 1, msg, curses.color_pair(3))
|
||||
if self.reactor.health_monitor.failure_log:
|
||||
win.addstr(
|
||||
3,
|
||||
1,
|
||||
f"Failures: {', '.join(self.reactor.health_monitor.failure_log)}",
|
||||
curses.color_pair(4) | curses.A_BOLD,
|
||||
)
|
||||
win.addstr(4, 1, "Press 'q' to exit and persist the current snapshot.", curses.color_pair(2))
|
||||
|
||||
def _draw_section(
|
||||
self,
|
||||
win: "curses._CursesWindow",
|
||||
start_y: int,
|
||||
title: str,
|
||||
lines: list[str],
|
||||
) -> int:
|
||||
width = win.getmaxyx()[1] - 4
|
||||
win.addstr(start_y, 2, title, curses.A_BOLD | curses.color_pair(1))
|
||||
for idx, line in enumerate(lines, start=start_y + 1):
|
||||
win.addstr(idx, 4, line[:width])
|
||||
return start_y + len(lines) + 2
|
||||
|
||||
def _draw_health_bar(self, win: "curses._CursesWindow", start_y: int) -> None:
|
||||
win.addstr(start_y, 2, "Component Health", curses.A_BOLD | curses.color_pair(1))
|
||||
bar_width = win.getmaxyx()[1] - 10
|
||||
for idx, (name, comp) in enumerate(self.reactor.health_monitor.components.items(), start=1):
|
||||
filled = int(bar_width * comp.integrity)
|
||||
bar = "█" * filled + "-" * (bar_width - filled)
|
||||
color = 3 if comp.integrity > 0.5 else 2 if comp.integrity > 0.2 else 4
|
||||
win.addstr(start_y + idx, 4, f"{name:<12}", curses.A_BOLD)
|
||||
win.addstr(start_y + idx, 16, bar, curses.color_pair(color))
|
||||
win.addstr(start_y + idx, 18 + bar_width, f"{comp.integrity*100:5.1f}%", curses.color_pair(color))
|
||||
|
||||
def _current_demand(self) -> float:
|
||||
if self.reactor.consumer:
|
||||
return self.reactor.consumer.demand_mw
|
||||
|
||||
Reference in New Issue
Block a user