Compare commits
8 Commits
b03e80da9f
...
fb1276f39f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb1276f39f | ||
|
|
c30c838fcc | ||
|
|
faac1dc7b0 | ||
|
|
bcd1eec84f | ||
|
|
79f98faeeb | ||
|
|
5d8b617c9e | ||
|
|
2856d83600 | ||
|
|
f0f2128ae6 |
@@ -24,6 +24,7 @@ class ReactorCommand:
|
||||
primary_pumps: dict[int, bool] | None = None
|
||||
secondary_pumps: dict[int, bool] | None = None
|
||||
generator_units: dict[int, bool] | None = None
|
||||
generator_auto: bool | None = None
|
||||
maintenance_components: tuple[str, ...] = tuple()
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -47,29 +47,45 @@ class ReactorDashboard:
|
||||
self._log_handler: Optional[logging.Handler] = None
|
||||
self._previous_handlers: list[logging.Handler] = []
|
||||
self._logger = logging.getLogger("reactor_sim")
|
||||
self.keys = [
|
||||
self.help_sections: list[tuple[str, list[DashboardKey]]] = [
|
||||
(
|
||||
"Reactor / Safety",
|
||||
[
|
||||
DashboardKey("q", "Quit & save"),
|
||||
DashboardKey("space", "SCRAM"),
|
||||
DashboardKey("g", "Toggle primary pump 1"),
|
||||
DashboardKey("h", "Toggle primary pump 2"),
|
||||
DashboardKey("j", "Toggle secondary pump 1"),
|
||||
DashboardKey("k", "Toggle secondary pump 2"),
|
||||
DashboardKey("b", "Toggle generator 1"),
|
||||
DashboardKey("v", "Toggle generator 2"),
|
||||
DashboardKey("t", "Toggle turbine"),
|
||||
DashboardKey("r", "Reset & clear state"),
|
||||
DashboardKey("a", "Toggle auto rod control"),
|
||||
DashboardKey("+/-", "Withdraw/insert rods"),
|
||||
DashboardKey("[/]", "Adjust consumer demand −/+50 MW"),
|
||||
DashboardKey("s/d", "Setpoint −/+250 MW"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"Pumps",
|
||||
[
|
||||
DashboardKey("g/h", "Toggle primary pump 1/2"),
|
||||
DashboardKey("j/k", "Toggle secondary pump 1/2"),
|
||||
DashboardKey("m/n", "Maintain primary pumps 1/2"),
|
||||
DashboardKey(",/.", "Maintain secondary pumps 1/2"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"Generators",
|
||||
[
|
||||
DashboardKey("b/v", "Toggle generator 1/2"),
|
||||
DashboardKey("x", "Toggle generator auto"),
|
||||
DashboardKey("B/V", "Maintain generator 1/2"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"Turbines / Grid",
|
||||
[
|
||||
DashboardKey("t", "Toggle turbine bank"),
|
||||
DashboardKey("1/2/3", "Toggle turbine units 1-3"),
|
||||
DashboardKey("y/u/i", "Maintain turbine 1/2/3"),
|
||||
DashboardKey("c", "Toggle consumer"),
|
||||
DashboardKey("r", "Reset & clear state"),
|
||||
DashboardKey("m/n", "Maintain primary pumps 1/2"),
|
||||
DashboardKey(",/.", "Maintain secondary pumps 1/2"),
|
||||
DashboardKey("B/V", "Maintain generator 1/2"),
|
||||
DashboardKey("k", "Maintain core (requires shutdown)"),
|
||||
DashboardKey("+/-", "Withdraw/insert rods"),
|
||||
DashboardKey("[/]", "Adjust consumer demand −/+50 MW"),
|
||||
DashboardKey("s", "Setpoint −250 MW"),
|
||||
DashboardKey("d", "Setpoint +250 MW"),
|
||||
DashboardKey("a", "Toggle auto rod control"),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
def run(self) -> None:
|
||||
@@ -146,6 +162,8 @@ class ReactorDashboard:
|
||||
self._toggle_generator_unit(0)
|
||||
elif ch in (ord("v"), ord("V")):
|
||||
self._toggle_generator_unit(1)
|
||||
elif ch in (ord("x"), ord("X")):
|
||||
self._queue_command(ReactorCommand(generator_auto=not self.reactor.generator_auto))
|
||||
elif ch in (ord("t"), ord("T")):
|
||||
self._queue_command(ReactorCommand(turbine_on=not self.reactor.turbine_active))
|
||||
elif ord("1") <= ch <= ord("9"):
|
||||
@@ -259,6 +277,7 @@ class ReactorDashboard:
|
||||
self.reactor = Reactor.default()
|
||||
self.start_state = None
|
||||
self.pending_command = None
|
||||
self._last_state = None
|
||||
self.reset_requested = False
|
||||
self.log_buffer.clear()
|
||||
|
||||
@@ -307,10 +326,23 @@ class ReactorDashboard:
|
||||
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,
|
||||
height, width = win.getmaxyx()
|
||||
inner_height = height - 2
|
||||
inner_width = width - 2
|
||||
left_width = max(28, inner_width // 2)
|
||||
right_width = inner_width - left_width
|
||||
left_win = win.derwin(inner_height, left_width, 1, 1)
|
||||
right_win = win.derwin(inner_height, right_width, 1, 1 + left_width)
|
||||
for row in range(1, height - 1):
|
||||
win.addch(row, 1 + left_width, curses.ACS_VLINE)
|
||||
|
||||
left_win.erase()
|
||||
right_win.erase()
|
||||
|
||||
left_y = 0
|
||||
left_y = self._draw_section(
|
||||
left_win,
|
||||
left_y,
|
||||
"Core",
|
||||
[
|
||||
("Fuel Temp", f"{state.core.fuel_temperature:8.1f} K"),
|
||||
@@ -322,10 +354,10 @@ class ReactorDashboard:
|
||||
("Reactivity", f"{state.core.reactivity_margin:+.4f}"),
|
||||
],
|
||||
)
|
||||
y = self._draw_section(win, y, "Key Poisons / Emitters", self._poison_lines(state))
|
||||
y = self._draw_section(
|
||||
win,
|
||||
y,
|
||||
left_y = self._draw_section(left_win, left_y, "Key Poisons / Emitters", self._poison_lines(state))
|
||||
left_y = self._draw_section(
|
||||
left_win,
|
||||
left_y,
|
||||
"Primary Loop",
|
||||
[
|
||||
("Pump1", self._pump_status(state.primary_pumps, 0)),
|
||||
@@ -336,9 +368,9 @@ class ReactorDashboard:
|
||||
("Pressure", f"{state.primary_loop.pressure:5.2f} MPa"),
|
||||
],
|
||||
)
|
||||
y = self._draw_section(
|
||||
win,
|
||||
y,
|
||||
self._draw_section(
|
||||
left_win,
|
||||
left_y,
|
||||
"Secondary Loop",
|
||||
[
|
||||
("Pump1", self._pump_status(state.secondary_pumps, 0)),
|
||||
@@ -349,20 +381,16 @@ class ReactorDashboard:
|
||||
("Steam Quality", f"{state.secondary_loop.steam_quality:5.2f}"),
|
||||
],
|
||||
)
|
||||
y = self._draw_section(
|
||||
win,
|
||||
y,
|
||||
"Generators",
|
||||
self._generator_lines(state),
|
||||
)
|
||||
|
||||
right_y = 0
|
||||
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,
|
||||
right_y = self._draw_section(
|
||||
right_win,
|
||||
right_y,
|
||||
"Turbine / Grid",
|
||||
[
|
||||
("Turbines", " ".join(self._turbine_status_lines())),
|
||||
@@ -381,18 +409,23 @@ class ReactorDashboard:
|
||||
("Demand", f"{consumer_demand:7.1f} MW"),
|
||||
],
|
||||
)
|
||||
y = self._draw_section(win, y, "Maintenance", self._maintenance_lines())
|
||||
y = self._draw_health_bars(win, y)
|
||||
right_y = self._draw_section(right_win, right_y, "Generators", self._generator_lines(state))
|
||||
right_y = self._draw_section(right_win, right_y, "Maintenance", self._maintenance_lines())
|
||||
self._draw_health_bars(right_win, right_y)
|
||||
|
||||
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}")
|
||||
for title, entries in self.help_sections:
|
||||
win.addstr(y, 2, title, curses.color_pair(1) | curses.A_BOLD)
|
||||
y += 1
|
||||
win.addstr(y + 1, 2, "Tips:", curses.color_pair(2) | curses.A_BOLD)
|
||||
for entry in entries:
|
||||
win.addstr(y, 4, f"{entry.key:<8} {entry.description}")
|
||||
y += 1
|
||||
y += 1
|
||||
win.addstr(y, 2, "Tips:", curses.color_pair(2) | curses.A_BOLD)
|
||||
tips = [
|
||||
"Start pumps before withdrawing rods.",
|
||||
"Bring turbine and consumer online after thermal stabilization.",
|
||||
@@ -458,9 +491,15 @@ class ReactorDashboard:
|
||||
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)
|
||||
]
|
||||
lines: list[str] = []
|
||||
for idx, active in enumerate(self.reactor.turbine_unit_active):
|
||||
label = f"{idx + 1}:"
|
||||
status = "ON" if active else "OFF"
|
||||
if idx < len(getattr(self._last_state, "turbines", [])):
|
||||
t_state = self._last_state.turbines[idx]
|
||||
status = getattr(t_state, "status", status)
|
||||
lines.append(f"{label}{status}")
|
||||
return lines
|
||||
|
||||
def _total_load_supplied(self, state: PlantState) -> float:
|
||||
return sum(t.load_supplied_mw for t in state.turbines)
|
||||
@@ -504,6 +543,8 @@ class ReactorDashboard:
|
||||
if not state.generators:
|
||||
return [("Status", "n/a")]
|
||||
lines: list[tuple[str, str]] = []
|
||||
control = "AUTO" if self.reactor.generator_auto else "MANUAL"
|
||||
lines.append(("Control", control))
|
||||
for idx, gen in enumerate(state.generators):
|
||||
status = "RUN" if gen.running else "START" if gen.starting else "OFF"
|
||||
spool = f" spool {gen.spool_remaining:4.1f}s" if gen.starting else ""
|
||||
@@ -518,17 +559,18 @@ class ReactorDashboard:
|
||||
return height - 2
|
||||
win.addstr(start_y, 2, "Component Health", curses.A_BOLD | curses.color_pair(1))
|
||||
bar_width = max(8, min(inner_width - 18, 40))
|
||||
label_width = 16
|
||||
row = start_y + 1
|
||||
for name, comp in self.reactor.health_monitor.components.items():
|
||||
if row >= height - 1:
|
||||
break
|
||||
label = f"{name:<12}"
|
||||
label = f"{name:<{label_width}}"
|
||||
target = 0.0 if comp.failed else comp.integrity
|
||||
filled = int(bar_width * max(0.0, min(1.0, target)))
|
||||
bar = "#" * filled + "-" * (bar_width - filled)
|
||||
color = 3 if comp.integrity > 0.5 else 2 if comp.integrity > 0.2 else 4
|
||||
win.addstr(row, 4, f"{label}:")
|
||||
bar_start = 4 + len(label) + 1
|
||||
bar_start = 4 + label_width + 1
|
||||
win.addstr(row, bar_start, bar[:bar_width], curses.color_pair(color))
|
||||
percent_text = "FAILED" if comp.failed else f"{comp.integrity*100:5.1f}%"
|
||||
percent_x = min(width - len(percent_text) - 2, bar_start + bar_width + 2)
|
||||
@@ -540,7 +582,8 @@ class ReactorDashboard:
|
||||
if index >= len(pumps):
|
||||
return "n/a"
|
||||
state = pumps[index]
|
||||
return f"{'ON ' if state.active else 'OFF'} {state.flow_rate:6.0f} kg/s"
|
||||
status = getattr(state, "status", "ON" if state.active else "OFF")
|
||||
return f"{status:<8} {state.flow_rate:6.0f} kg/s"
|
||||
|
||||
def _current_demand(self) -> float:
|
||||
if self.reactor.consumer:
|
||||
|
||||
@@ -17,6 +17,7 @@ class GeneratorState:
|
||||
spool_remaining: float
|
||||
power_output_mw: float
|
||||
battery_charge: float
|
||||
status: str = "OFF"
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -32,6 +33,7 @@ class DieselGenerator:
|
||||
return
|
||||
state.starting = True
|
||||
state.spool_remaining = self.spool_time
|
||||
state.status = "STARTING"
|
||||
LOGGER.info("Generator starting (spool %.0fs)", self.spool_time)
|
||||
|
||||
def stop(self, state: GeneratorState) -> None:
|
||||
@@ -41,6 +43,7 @@ class DieselGenerator:
|
||||
state.starting = False
|
||||
state.spool_remaining = 0.0
|
||||
state.power_output_mw = 0.0
|
||||
state.status = "OFF"
|
||||
LOGGER.info("Generator stopped")
|
||||
|
||||
def step(self, state: GeneratorState, load_demand_mw: float, dt: float) -> float:
|
||||
@@ -51,12 +54,15 @@ class DieselGenerator:
|
||||
if state.spool_remaining <= 0.0:
|
||||
state.starting = False
|
||||
state.running = True
|
||||
state.status = "RUN"
|
||||
LOGGER.info("Generator online at %.1f MW", self.rated_output_mw)
|
||||
elif state.running:
|
||||
available = self.rated_output_mw
|
||||
state.power_output_mw = min(available, load_demand_mw)
|
||||
state.status = "RUN" if state.power_output_mw > 0 else "IDLE"
|
||||
else:
|
||||
state.power_output_mw = 0.0
|
||||
state.status = "OFF"
|
||||
|
||||
if state.running:
|
||||
state.battery_charge = min(1.0, state.battery_charge + 0.02 * dt)
|
||||
|
||||
@@ -44,6 +44,7 @@ class Reactor:
|
||||
turbine_unit_active: list[bool] = field(default_factory=lambda: [True, True, True])
|
||||
shutdown: bool = False
|
||||
meltdown: bool = False
|
||||
generator_auto: bool = True
|
||||
poison_alerts: set[str] = field(default_factory=set)
|
||||
maintenance_active: set[str] = field(default_factory=set)
|
||||
|
||||
@@ -96,6 +97,8 @@ class Reactor:
|
||||
self.meltdown = False
|
||||
self.primary_pump_active = False
|
||||
self.secondary_pump_active = False
|
||||
self.primary_pump_units = [False] * len(self.primary_pump_units)
|
||||
self.secondary_pump_units = [False] * len(self.secondary_pump_units)
|
||||
self.turbine_unit_active = [False] * len(self.turbines)
|
||||
self.turbine_active = any(self.turbine_unit_active)
|
||||
if self.consumer:
|
||||
@@ -115,10 +118,18 @@ class Reactor:
|
||||
mass_flow_rate=0.0,
|
||||
steam_quality=0.0,
|
||||
)
|
||||
primary_pumps = [PumpState(active=self.primary_pump_active, flow_rate=0.0, pressure=0.5) for _ in range(2)]
|
||||
secondary_pumps = [PumpState(active=self.secondary_pump_active, flow_rate=0.0, pressure=0.5) for _ in range(2)]
|
||||
primary_pumps = [
|
||||
PumpState(active=self.primary_pump_active and self.primary_pump_units[idx], flow_rate=0.0, pressure=0.5)
|
||||
for idx in range(2)
|
||||
]
|
||||
secondary_pumps = [
|
||||
PumpState(active=self.secondary_pump_active and self.secondary_pump_units[idx], flow_rate=0.0, pressure=0.5)
|
||||
for idx in range(2)
|
||||
]
|
||||
generator_states = [
|
||||
GeneratorState(running=False, starting=False, spool_remaining=0.0, power_output_mw=0.0, battery_charge=1.0)
|
||||
GeneratorState(
|
||||
running=False, starting=False, spool_remaining=0.0, power_output_mw=0.0, battery_charge=1.0, status="OFF"
|
||||
)
|
||||
for _ in self.generators
|
||||
]
|
||||
turbine_states = [
|
||||
@@ -218,6 +229,14 @@ class Reactor:
|
||||
pump_state.pressure, desired_pressure, dt, self.primary_pump.spool_time
|
||||
)
|
||||
pump_state.active = (unit_enabled and power_ratio > 0.05) or pump_state.flow_rate > 1.0
|
||||
if unit_enabled and pump_state.flow_rate < max(1.0, desired_flow * 0.8):
|
||||
pump_state.status = "STARTING"
|
||||
elif not unit_enabled and pump_state.flow_rate > 1.0:
|
||||
pump_state.status = "STOPPING"
|
||||
elif pump_state.active:
|
||||
pump_state.status = "RUN"
|
||||
else:
|
||||
pump_state.status = "OFF"
|
||||
total_flow += pump_state.flow_rate
|
||||
loop_pressure = max(loop_pressure, pump_state.pressure)
|
||||
state.primary_loop.mass_flow_rate = total_flow
|
||||
@@ -239,6 +258,7 @@ class Reactor:
|
||||
pump_state.pressure = self._ramp_value(
|
||||
pump_state.pressure, state.primary_loop.pressure, dt, self.primary_pump.spool_time
|
||||
)
|
||||
pump_state.status = "STOPPING" if pump_state.flow_rate > 1.0 else "OFF"
|
||||
if self.secondary_pump_active:
|
||||
total_flow = 0.0
|
||||
target_pressure = 12.0 * 0.75 + 2.0
|
||||
@@ -257,6 +277,14 @@ class Reactor:
|
||||
pump_state.pressure, desired_pressure, dt, self.secondary_pump.spool_time
|
||||
)
|
||||
pump_state.active = unit_enabled or pump_state.flow_rate > 1.0
|
||||
if unit_enabled and pump_state.flow_rate < max(1.0, desired_flow * 0.8):
|
||||
pump_state.status = "STARTING"
|
||||
elif not unit_enabled and pump_state.flow_rate > 1.0:
|
||||
pump_state.status = "STOPPING"
|
||||
elif pump_state.active:
|
||||
pump_state.status = "RUN"
|
||||
else:
|
||||
pump_state.status = "OFF"
|
||||
total_flow += pump_state.flow_rate
|
||||
loop_pressure = max(loop_pressure, pump_state.pressure)
|
||||
state.secondary_loop.mass_flow_rate = total_flow
|
||||
@@ -278,6 +306,7 @@ class Reactor:
|
||||
pump_state.pressure = self._ramp_value(
|
||||
pump_state.pressure, state.secondary_loop.pressure, dt, self.secondary_pump.spool_time
|
||||
)
|
||||
pump_state.status = "STOPPING" if pump_state.flow_rate > 1.0 else "OFF"
|
||||
|
||||
self.thermal.step_core(state.core, state.primary_loop, total_power, dt)
|
||||
if not self.secondary_pump_active or state.secondary_loop.mass_flow_rate <= 1.0:
|
||||
@@ -334,8 +363,15 @@ class Reactor:
|
||||
turbine_state = state.turbines[idx]
|
||||
if idx in active_indices:
|
||||
turbine.step(state.secondary_loop, turbine_state, steam_power_mw=power_per_unit, dt=dt)
|
||||
if power_per_unit <= 0.0 and turbine_state.electrical_output_mw < 0.1:
|
||||
turbine_state.status = "OFF"
|
||||
elif turbine_state.electrical_output_mw < max(0.5, power_per_unit * 0.5):
|
||||
turbine_state.status = "STARTING"
|
||||
else:
|
||||
turbine_state.status = "RUN"
|
||||
else:
|
||||
self._spin_down_turbine(turbine_state, dt, turbine.spool_time)
|
||||
turbine_state.status = "STOPPING" if turbine_state.electrical_output_mw > 0 else "OFF"
|
||||
self._dispatch_consumer_load(state, active_indices)
|
||||
|
||||
def _reset_turbine_state(self, turbine_state: TurbineState) -> None:
|
||||
@@ -343,6 +379,7 @@ class Reactor:
|
||||
turbine_state.electrical_output_mw = 0.0
|
||||
turbine_state.load_demand_mw = 0.0
|
||||
turbine_state.load_supplied_mw = 0.0
|
||||
turbine_state.status = "OFF"
|
||||
|
||||
@staticmethod
|
||||
def _ramp_value(current: float, target: float, dt: float, time_constant: float) -> float:
|
||||
@@ -405,6 +442,8 @@ class Reactor:
|
||||
self.shutdown = True
|
||||
overrides["rod_fraction"] = self.control.scram()
|
||||
self._set_turbine_state(False)
|
||||
if command.generator_auto is not None:
|
||||
self.generator_auto = command.generator_auto
|
||||
if command.power_setpoint is not None:
|
||||
self.control.set_power_setpoint(command.power_setpoint)
|
||||
if command.rod_manual is not None:
|
||||
@@ -446,8 +485,6 @@ class Reactor:
|
||||
LOGGER.info("Primary pump %s", "enabled" if active else "stopped")
|
||||
if not active:
|
||||
self.primary_pump_units = [False] * len(self.primary_pump_units)
|
||||
elif active and not any(self.primary_pump_units):
|
||||
self.primary_pump_units = [True] * len(self.primary_pump_units)
|
||||
|
||||
def _set_secondary_pump(self, active: bool) -> None:
|
||||
if self.secondary_pump_active != active:
|
||||
@@ -455,8 +492,6 @@ class Reactor:
|
||||
LOGGER.info("Secondary pump %s", "enabled" if active else "stopped")
|
||||
if not active:
|
||||
self.secondary_pump_units = [False] * len(self.secondary_pump_units)
|
||||
elif active and not any(self.secondary_pump_units):
|
||||
self.secondary_pump_units = [True] * len(self.secondary_pump_units)
|
||||
|
||||
def _toggle_primary_pump_unit(self, index: int, active: bool) -> None:
|
||||
if index < 0 or index >= len(self.primary_pump_units):
|
||||
@@ -512,6 +547,7 @@ class Reactor:
|
||||
GeneratorState(running=False, starting=False, spool_remaining=0.0, power_output_mw=0.0, battery_charge=1.0)
|
||||
)
|
||||
deficit = max(0.0, aux_demand - turbine_electric)
|
||||
if self.generator_auto:
|
||||
if deficit > 0.0:
|
||||
for idx, gen_state in enumerate(state.generators):
|
||||
if not (gen_state.running or gen_state.starting):
|
||||
@@ -526,8 +562,10 @@ class Reactor:
|
||||
|
||||
total_power = 0.0
|
||||
remaining = max(0.0, aux_demand - turbine_electric)
|
||||
active_indices = [idx for idx, g in enumerate(state.generators) if g.running or g.starting]
|
||||
share = remaining / len(active_indices) if active_indices and remaining > 0 else 0.0
|
||||
for idx, gen_state in enumerate(state.generators):
|
||||
load = remaining if remaining > 0 else 0.0
|
||||
load = share if idx in active_indices else 0.0
|
||||
delivered = self.generators[idx].step(gen_state, load, dt)
|
||||
total_power += delivered
|
||||
remaining = max(0.0, remaining - delivered)
|
||||
@@ -640,6 +678,7 @@ class Reactor:
|
||||
"turbine_units": self.turbine_unit_active,
|
||||
"shutdown": self.shutdown,
|
||||
"meltdown": self.meltdown,
|
||||
"generator_auto": self.generator_auto,
|
||||
"maintenance_active": list(self.maintenance_active),
|
||||
"generators": [
|
||||
{
|
||||
@@ -648,6 +687,7 @@ class Reactor:
|
||||
"spool_remaining": g.spool_remaining,
|
||||
"power_output_mw": g.power_output_mw,
|
||||
"battery_charge": g.battery_charge,
|
||||
"status": g.status,
|
||||
}
|
||||
for g in state.generators
|
||||
],
|
||||
@@ -671,6 +711,7 @@ class Reactor:
|
||||
self.turbine_active = metadata.get("turbine_active", any(self.turbine_unit_active))
|
||||
self.shutdown = metadata.get("shutdown", self.shutdown)
|
||||
self.meltdown = metadata.get("meltdown", self.meltdown)
|
||||
self.generator_auto = metadata.get("generator_auto", self.generator_auto)
|
||||
maint = metadata.get("maintenance_active")
|
||||
if maint is not None:
|
||||
self.maintenance_active = set(maint)
|
||||
@@ -691,7 +732,12 @@ class Reactor:
|
||||
# Back-fill pump state lists for compatibility.
|
||||
if not plant.primary_pumps or len(plant.primary_pumps) < 2:
|
||||
plant.primary_pumps = [
|
||||
PumpState(active=self.primary_pump_active, flow_rate=plant.primary_loop.mass_flow_rate / 2, pressure=plant.primary_loop.pressure)
|
||||
PumpState(
|
||||
active=self.primary_pump_active,
|
||||
flow_rate=plant.primary_loop.mass_flow_rate / 2,
|
||||
pressure=plant.primary_loop.pressure,
|
||||
status="OFF",
|
||||
)
|
||||
for _ in range(2)
|
||||
]
|
||||
if not plant.secondary_pumps or len(plant.secondary_pumps) < 2:
|
||||
@@ -700,6 +746,7 @@ class Reactor:
|
||||
active=self.secondary_pump_active,
|
||||
flow_rate=plant.secondary_loop.mass_flow_rate / 2,
|
||||
pressure=plant.secondary_loop.pressure,
|
||||
status="OFF",
|
||||
)
|
||||
for _ in range(2)
|
||||
]
|
||||
@@ -726,6 +773,7 @@ class Reactor:
|
||||
spool_remaining=0.0,
|
||||
power_output_mw=0.0,
|
||||
battery_charge=1.0,
|
||||
status="OFF",
|
||||
)
|
||||
)
|
||||
for idx, gen_state in enumerate(plant.generators):
|
||||
@@ -736,6 +784,7 @@ class Reactor:
|
||||
gen_state.spool_remaining = cfg.get("spool_remaining", gen_state.spool_remaining)
|
||||
gen_state.power_output_mw = cfg.get("power_output_mw", gen_state.power_output_mw)
|
||||
gen_state.battery_charge = cfg.get("battery_charge", gen_state.battery_charge)
|
||||
gen_state.status = cfg.get("status", gen_state.status)
|
||||
return plant
|
||||
|
||||
def _handle_heat_sink_loss(self, state: PlantState) -> None:
|
||||
|
||||
@@ -54,6 +54,7 @@ class TurbineState:
|
||||
condenser_temperature: float
|
||||
load_demand_mw: float = 0.0
|
||||
load_supplied_mw: float = 0.0
|
||||
status: str = "OFF"
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -61,6 +62,7 @@ class PumpState:
|
||||
active: bool
|
||||
flow_rate: float
|
||||
pressure: float
|
||||
status: str = "OFF"
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -162,6 +162,8 @@ def test_full_rod_withdrawal_reaches_gigawatt_power():
|
||||
reactor.control.rod_fraction = 0.0
|
||||
reactor.primary_pump_active = True
|
||||
reactor.secondary_pump_active = True
|
||||
reactor.primary_pump_units = [True, True]
|
||||
reactor.secondary_pump_units = [True, True]
|
||||
|
||||
early_power = 0.0
|
||||
for step in range(60):
|
||||
@@ -180,6 +182,8 @@ def test_partially_inserted_rods_hold_near_three_gw():
|
||||
reactor.control.rod_fraction = 0.4
|
||||
reactor.primary_pump_active = True
|
||||
reactor.secondary_pump_active = True
|
||||
reactor.primary_pump_units = [True, True]
|
||||
reactor.secondary_pump_units = [True, True]
|
||||
|
||||
for _ in range(120):
|
||||
reactor.step(state, dt=1.0)
|
||||
@@ -206,6 +210,30 @@ def test_generator_spools_and_powers_pumps():
|
||||
assert state.primary_loop.mass_flow_rate > 0.0
|
||||
|
||||
|
||||
def test_generator_manual_mode_allows_single_unit_and_stop():
|
||||
reactor = Reactor.default()
|
||||
state = reactor.initial_state()
|
||||
reactor.shutdown = False
|
||||
reactor.control.manual_control = True
|
||||
reactor.control.rod_fraction = 0.95
|
||||
reactor.generator_auto = False
|
||||
reactor.primary_pump_units = [True, False]
|
||||
reactor.secondary_pump_units = [False, False]
|
||||
|
||||
reactor.step(state, dt=1.0, command=ReactorCommand(generator_units={1: True}, primary_pumps={1: True}))
|
||||
assert state.generators[0].starting or state.generators[0].running
|
||||
|
||||
for _ in range(15):
|
||||
reactor.step(state, dt=1.0)
|
||||
|
||||
assert state.generators[0].running is True
|
||||
reactor.step(state, dt=1.0, command=ReactorCommand(generator_units={1: False}))
|
||||
for _ in range(5):
|
||||
reactor.step(state, dt=1.0)
|
||||
assert state.generators[0].running is False
|
||||
assert state.generators[1].running is False
|
||||
|
||||
|
||||
def test_meltdown_triggers_shutdown():
|
||||
reactor = Reactor.default()
|
||||
state = reactor.initial_state()
|
||||
|
||||
Reference in New Issue
Block a user