From c34f49319f53740f4cfa366b67ec0ed1a5006fdf Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Fri, 28 Nov 2025 18:50:38 +0100 Subject: [PATCH] Polish curses dashboard warnings and turbine/generator layout --- src/reactor_sim/dashboard.py | 63 ++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/src/reactor_sim/dashboard.py b/src/reactor_sim/dashboard.py index 65e41af..49a4187 100644 --- a/src/reactor_sim/dashboard.py +++ b/src/reactor_sim/dashboard.py @@ -486,33 +486,29 @@ class ReactorDashboard: [ ("Turbines", " ".join(self._turbine_status_lines())), ("Rated Elec", f"{len(self.reactor.turbines)*self.reactor.turbines[0].rated_output_mw:7.1f} MW"), - ("Steam Avail", f"{self._steam_available_power(state):7.1f} MW"), - ("Unit1 Elec", f"{state.turbines[0].electrical_output_mw:7.1f} MW" if state.turbines else "n/a"), ( - "Unit2 Elec", - f"{state.turbines[1].electrical_output_mw:7.1f} MW" if len(state.turbines) > 1 else "n/a", + "Steam", + f"h={state.turbines[0].steam_enthalpy:5.0f} kJ/kg avail {self._steam_available_power(state):6.1f} MW " + f"flow {state.secondary_loop.mass_flow_rate * max(0.0, state.secondary_loop.steam_quality):6.0f} kg/s" + if state.turbines + else "n/a", ), ( - "Unit3 Elec", - f"{state.turbines[2].electrical_output_mw:7.1f} MW" if len(state.turbines) > 2 else "n/a", + "Units Elec", + " ".join([f"{t.electrical_output_mw:6.1f}MW" for t in state.turbines]) if state.turbines else "n/a", ), ("Throttle", f"{self.reactor.turbines[0].throttle:5.2f}" if self.reactor.turbines else "n/a"), ( "Condenser", ( f"P={state.turbines[0].condenser_pressure:4.2f}/{constants.CONDENSER_MAX_PRESSURE_MPA:4.2f} MPa " - f"T={state.turbines[0].condenser_temperature:6.1f}/{constants.CONDENSER_COOLING_WATER_TEMP_K:6.1f}K " - f"Foul={state.turbines[0].fouling_penalty*100:4.1f}%" + f"T={state.turbines[0].condenser_temperature:6.1f}K Foul={state.turbines[0].fouling_penalty*100:4.1f}%" ) if state.turbines else "n/a", ), - ("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"), - ("Steam Enthalpy", f"{state.turbines[0].steam_enthalpy:7.0f} kJ/kg" if state.turbines else "n/a"), - ("Steam Flow", f"{state.secondary_loop.mass_flow_rate * max(0.0, state.secondary_loop.steam_quality):7.0f} kg/s"), + ("Electrical", f"{state.total_electrical_output():7.1f} MW | Load {self._total_load_supplied(state):6.1f}/{self._total_load_demand(state):6.1f} MW"), + ("Consumer", f"{consumer_status} demand {consumer_demand:6.1f} MW"), ], ) right_y = self._draw_section(right_win, right_y, "Generators", self._generator_lines(state)) @@ -594,7 +590,7 @@ class ReactorDashboard: win: "curses._CursesWindow", start_y: int, title: str, - lines: list[tuple[str, str] | str], + lines: list[tuple[str, str] | tuple[str, str, int] | str], ) -> int: height, width = win.getmaxyx() inner_width = width - 4 @@ -605,12 +601,16 @@ class ReactorDashboard: for line in lines: if row >= height - 1: break + attr = 0 if isinstance(line, tuple): - label, value = line + if len(line) == 3: + label, value, attr = line + else: + label, value = line text = f"{label:<18}: {value}" else: text = line - win.addstr(row, 4, text[:inner_width]) + win.addstr(row, 4, text[:inner_width], attr) row += 1 return row + 1 @@ -730,37 +730,44 @@ class ReactorDashboard: ] def _protection_lines(self, state: PlantState) -> list[tuple[str, str]]: - lines: list[tuple[str, str]] = [] - lines.append(("SCRAM", "ACTIVE" if self.reactor.shutdown else "CLEAR")) + lines: list[tuple[str, str] | tuple[str, str, int]] = [] + lines.append(("SCRAM", "ACTIVE" if self.reactor.shutdown else "CLEAR", curses.color_pair(4) if self.reactor.shutdown else 0)) if self.reactor.meltdown: - lines.append(("Meltdown", "IN PROGRESS")) + lines.append(("Meltdown", "IN PROGRESS", curses.color_pair(4) | curses.A_BOLD)) sec_flow_low = state.secondary_loop.mass_flow_rate <= 1.0 or not self.reactor.secondary_pump_active heat_sink_risk = sec_flow_low and state.core.power_output_mw > 50.0 if heat_sink_risk: heat_text = "TRIPPED low secondary flow >50 MW" + heat_attr = curses.color_pair(4) | curses.A_BOLD elif sec_flow_low: heat_text = "ARMED (secondary off/low flow)" + heat_attr = curses.color_pair(2) | curses.A_BOLD else: heat_text = "OK" - lines.append(("Heat sink", heat_text)) + heat_attr = curses.color_pair(3) + lines.append(("Heat sink", heat_text, heat_attr)) draws = getattr(state, "aux_draws", {}) or {} demand = draws.get("total_demand", 0.0) supplied = draws.get("supplied", 0.0) if demand > 0.1 and supplied + 1e-6 < demand: aux_text = f"DEFICIT {supplied:5.1f}/{demand:5.1f} MW" + aux_attr = curses.color_pair(2) | curses.A_BOLD elif demand > 0.1: aux_text = f"OK {supplied:5.1f}/{demand:5.1f} MW" + aux_attr = curses.color_pair(3) else: aux_text = "Idle" - lines.append(("Aux power", aux_text)) + aux_attr = 0 + lines.append(("Aux power", aux_text, aux_attr)) reliefs = [] if self.reactor.primary_relief_open: reliefs.append("Primary") if self.reactor.secondary_relief_open: reliefs.append("Secondary") - lines.append(("Relief valves", ", ".join(reliefs) if reliefs else "Closed")) + relief_attr = curses.color_pair(2) | curses.A_BOLD if reliefs else 0 + lines.append(("Relief valves", ", ".join(reliefs) if reliefs else "Closed", relief_attr)) lines.append( ( "SCRAM trips", @@ -874,8 +881,14 @@ class _DashboardLogHandler(logging.Handler): msg = self.format(record) if msg == self._last_msg: self._repeat_count += 1 - if self._repeat_count > 3: - return + if self.buffer and self.buffer[-1].startswith(self._last_msg): + try: + self.buffer[-1] = f"{self._last_msg} (x{self._repeat_count + 1})" + except Exception: + self.buffer.append(f"{msg} (x{self._repeat_count + 1})") + else: + self.buffer.append(f"{msg} (x{self._repeat_count + 1})") + return else: self._last_msg = msg self._repeat_count = 0