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]
|
def _main(self, stdscr: "curses._CursesWindow") -> None: # type: ignore[name-defined]
|
||||||
curses.curs_set(0)
|
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)
|
stdscr.nodelay(True)
|
||||||
self.sim = ReactorSimulation(
|
self.sim = ReactorSimulation(
|
||||||
self.reactor,
|
self.reactor,
|
||||||
@@ -65,7 +71,7 @@ class ReactorDashboard:
|
|||||||
try:
|
try:
|
||||||
for state in self.sim.run():
|
for state in self.sim.run():
|
||||||
self._draw(stdscr, state)
|
self._draw(stdscr, state)
|
||||||
self._handle_input(stdscr, state)
|
self._handle_input(stdscr)
|
||||||
if self.quit_requested:
|
if self.quit_requested:
|
||||||
self.sim.stop()
|
self.sim.stop()
|
||||||
break
|
break
|
||||||
@@ -73,7 +79,7 @@ class ReactorDashboard:
|
|||||||
if self.save_path and self.sim and self.sim.last_state:
|
if self.save_path and self.sim and self.sim.last_state:
|
||||||
self.reactor.save_state(self.save_path, 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:
|
while True:
|
||||||
ch = stdscr.getch()
|
ch = stdscr.getch()
|
||||||
if ch == -1:
|
if ch == -1:
|
||||||
@@ -110,76 +116,168 @@ class ReactorDashboard:
|
|||||||
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:
|
||||||
self.pending_command = command
|
self.pending_command = command
|
||||||
else:
|
return
|
||||||
for field in command.__dataclass_fields__: # type: ignore[attr-defined]
|
for field in command.__dataclass_fields__: # type: ignore[attr-defined]
|
||||||
value = getattr(command, field)
|
value = getattr(command, field)
|
||||||
if value is None or value is False:
|
if value is None or value is False:
|
||||||
continue
|
continue
|
||||||
if field == "rod_step" and getattr(self.pending_command, field) is not None:
|
if field == "rod_step" and getattr(self.pending_command, field) is not None:
|
||||||
setattr(
|
setattr(
|
||||||
self.pending_command,
|
self.pending_command,
|
||||||
field,
|
field,
|
||||||
getattr(self.pending_command, field) + value,
|
getattr(self.pending_command, field) + value,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
setattr(self.pending_command, field, value)
|
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
|
cmd = self.pending_command
|
||||||
self.pending_command = None
|
self.pending_command = None
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
def _draw(self, stdscr: "curses._CursesWindow", state: PlantState) -> None:
|
def _draw(self, stdscr: "curses._CursesWindow", state: PlantState) -> None:
|
||||||
stdscr.erase()
|
stdscr.erase()
|
||||||
stdscr.addstr(0, 0, "Realtime Reactor Dashboard".ljust(60))
|
height, width = stdscr.getmaxyx()
|
||||||
stdscr.addstr(1, 0, f"Time: {state.time_elapsed:8.1f}s Mode: realtime")
|
if height < 24 or width < 90:
|
||||||
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:
|
|
||||||
stdscr.addstr(
|
stdscr.addstr(
|
||||||
10,
|
|
||||||
0,
|
0,
|
||||||
f"Consumer: {'ONLINE ' if self.reactor.consumer.online else 'OFFLINE'} "
|
0,
|
||||||
f"Demand {self.reactor.consumer.demand_mw:7.1f} MW",
|
"Terminal window too small. Resize to at least 90x24.".ljust(width),
|
||||||
|
curses.color_pair(4),
|
||||||
)
|
)
|
||||||
health = self.reactor.health_monitor.components
|
stdscr.refresh()
|
||||||
stdscr.addstr(
|
return
|
||||||
12,
|
|
||||||
0,
|
data_height = height - 6
|
||||||
"Health: " + " ".join(f"{name}:{comp.integrity*100:5.1f}%" for name, comp in health.items()),
|
left_width = min(max(width - 32, 60), width - 20)
|
||||||
)
|
right_width = width - left_width
|
||||||
stdscr.addstr(14, 0, "Controls:")
|
|
||||||
for idx, key in enumerate(self.keys, start=15):
|
data_win = stdscr.derwin(data_height, left_width, 0, 0)
|
||||||
stdscr.addstr(idx, 0, f"{key.key.ljust(8)} - {key.description}")
|
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()
|
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:
|
def _current_demand(self) -> float:
|
||||||
if self.reactor.consumer:
|
if self.reactor.consumer:
|
||||||
return self.reactor.consumer.demand_mw
|
return self.reactor.consumer.demand_mw
|
||||||
|
|||||||
@@ -72,10 +72,16 @@ def main() -> None:
|
|||||||
else:
|
else:
|
||||||
duration = None if realtime else 600.0
|
duration = None if realtime else 600.0
|
||||||
reactor = Reactor.default()
|
reactor = Reactor.default()
|
||||||
sim = ReactorSimulation(reactor, timestep=5.0, duration=duration, realtime=realtime)
|
dashboard_mode = os.getenv("FISSION_DASHBOARD", "0") == "1"
|
||||||
|
timestep_env = os.getenv("FISSION_TIMESTEP")
|
||||||
|
if timestep_env:
|
||||||
|
timestep = float(timestep_env)
|
||||||
|
else:
|
||||||
|
timestep = 1.0 if dashboard_mode or realtime else 5.0
|
||||||
|
|
||||||
|
sim = ReactorSimulation(reactor, timestep=timestep, duration=duration, realtime=realtime)
|
||||||
load_path = os.getenv("FISSION_LOAD_STATE")
|
load_path = os.getenv("FISSION_LOAD_STATE")
|
||||||
save_path = os.getenv("FISSION_SAVE_STATE")
|
save_path = os.getenv("FISSION_SAVE_STATE")
|
||||||
dashboard_mode = os.getenv("FISSION_DASHBOARD", "0") == "1"
|
|
||||||
if load_path:
|
if load_path:
|
||||||
sim.start_state = reactor.load_state(load_path)
|
sim.start_state = reactor.load_state(load_path)
|
||||||
if dashboard_mode:
|
if dashboard_mode:
|
||||||
|
|||||||
Reference in New Issue
Block a user