Add generator power model and meltdown handling
This commit is contained in:
@@ -13,6 +13,7 @@ from .consumer import ElectricalConsumer
|
||||
from .control import ControlSystem
|
||||
from .failures import HealthMonitor
|
||||
from .fuel import FuelAssembly, decay_heat_fraction
|
||||
from .generator import DieselGenerator, GeneratorState
|
||||
from .neutronics import NeutronDynamics
|
||||
from .state import CoolantLoopState, CoreState, PlantState, PumpState, TurbineState
|
||||
from .thermal import ThermalSolver, heat_transfer
|
||||
@@ -31,6 +32,7 @@ class Reactor:
|
||||
thermal: ThermalSolver
|
||||
steam_generator: SteamGenerator
|
||||
turbines: list[Turbine]
|
||||
generators: list[DieselGenerator]
|
||||
atomic_model: AtomicPhysics
|
||||
consumer: ElectricalConsumer | None = None
|
||||
health_monitor: HealthMonitor = field(default_factory=HealthMonitor)
|
||||
@@ -41,6 +43,7 @@ class Reactor:
|
||||
turbine_active: bool = True
|
||||
turbine_unit_active: list[bool] = field(default_factory=lambda: [True, True, True])
|
||||
shutdown: bool = False
|
||||
meltdown: bool = False
|
||||
poison_alerts: set[str] = field(default_factory=set)
|
||||
maintenance_active: set[str] = field(default_factory=set)
|
||||
|
||||
@@ -50,6 +53,8 @@ class Reactor:
|
||||
if not self.turbine_unit_active or len(self.turbine_unit_active) != len(self.turbines):
|
||||
self.turbine_unit_active = [True] * len(self.turbines)
|
||||
self.turbine_active = any(self.turbine_unit_active)
|
||||
if not self.generators:
|
||||
self.generators = [DieselGenerator() for _ in range(2)]
|
||||
if not self.primary_pump_units or len(self.primary_pump_units) != 2:
|
||||
self.primary_pump_units = [True, True]
|
||||
if not self.secondary_pump_units or len(self.secondary_pump_units) != 2:
|
||||
@@ -67,6 +72,7 @@ class Reactor:
|
||||
thermal=ThermalSolver(),
|
||||
steam_generator=SteamGenerator(),
|
||||
turbines=[Turbine() for _ in range(3)],
|
||||
generators=[DieselGenerator() for _ in range(2)],
|
||||
atomic_model=atomic_model,
|
||||
consumer=ElectricalConsumer(name="Grid", demand_mw=800.0, online=False),
|
||||
health_monitor=HealthMonitor(),
|
||||
@@ -87,6 +93,7 @@ class Reactor:
|
||||
self.control.manual_control = True
|
||||
self.control.rod_fraction = 0.95
|
||||
self.shutdown = True
|
||||
self.meltdown = False
|
||||
self.primary_pump_active = False
|
||||
self.secondary_pump_active = False
|
||||
self.turbine_unit_active = [False] * len(self.turbines)
|
||||
@@ -110,6 +117,10 @@ class Reactor:
|
||||
)
|
||||
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)]
|
||||
generator_states = [
|
||||
GeneratorState(running=False, starting=False, spool_remaining=0.0, power_output_mw=0.0, battery_charge=1.0)
|
||||
for _ in self.generators
|
||||
]
|
||||
turbine_states = [
|
||||
TurbineState(
|
||||
steam_enthalpy=2_000.0,
|
||||
@@ -128,6 +139,7 @@ class Reactor:
|
||||
turbines=turbine_states,
|
||||
primary_pumps=primary_pumps,
|
||||
secondary_pumps=secondary_pumps,
|
||||
generators=generator_states,
|
||||
)
|
||||
|
||||
def step(self, state: PlantState, dt: float, command: ReactorCommand | None = None) -> None:
|
||||
@@ -136,6 +148,9 @@ class Reactor:
|
||||
else:
|
||||
rod_fraction = self.control.update_rods(state.core, dt)
|
||||
|
||||
if state.core.fuel_temperature >= constants.CORE_MELTDOWN_TEMPERATURE and not self.meltdown:
|
||||
self._trigger_meltdown(state)
|
||||
|
||||
overrides = {}
|
||||
if command:
|
||||
overrides = self._apply_command(command, state)
|
||||
@@ -165,13 +180,35 @@ class Reactor:
|
||||
self._check_poison_alerts(state)
|
||||
|
||||
pump_demand = overrides.get("coolant_demand", self.control.coolant_demand(state.primary_loop))
|
||||
self.primary_pump_active = self.primary_pump_active and any(self.primary_pump_units)
|
||||
self.secondary_pump_active = self.secondary_pump_active and any(self.secondary_pump_units)
|
||||
primary_units_active = [
|
||||
self.primary_pump_active and idx < len(self.primary_pump_units) and self.primary_pump_units[idx]
|
||||
for idx in range(2)
|
||||
]
|
||||
secondary_units_active = [
|
||||
self.secondary_pump_active and idx < len(self.secondary_pump_units) and self.secondary_pump_units[idx]
|
||||
for idx in range(2)
|
||||
]
|
||||
aux_demand = constants.BASE_AUX_LOAD_MW + constants.PUMP_POWER_MW * (
|
||||
sum(primary_units_active) + sum(secondary_units_active)
|
||||
)
|
||||
turbine_electrical = state.total_electrical_output()
|
||||
generator_power = self._step_generators(state, aux_demand, turbine_electrical, dt)
|
||||
aux_available = turbine_electrical + generator_power
|
||||
power_ratio = 1.0 if aux_demand <= 0 else min(1.0, aux_available / aux_demand)
|
||||
if aux_demand > 0 and aux_available < 0.5 * aux_demand:
|
||||
LOGGER.warning("Aux power deficit: available %.1f/%.1f MW", aux_available, aux_demand)
|
||||
|
||||
if self.primary_pump_active:
|
||||
total_flow = 0.0
|
||||
target_pressure = 12.0 * pump_demand + 2.0
|
||||
target_pressure = (12.0 * pump_demand + 2.0) * power_ratio
|
||||
loop_pressure = 0.5
|
||||
target_flow = self.primary_pump.flow_rate(pump_demand)
|
||||
target_flow = self.primary_pump.flow_rate(pump_demand) * power_ratio
|
||||
for idx, pump_state in enumerate(state.primary_pumps):
|
||||
unit_enabled = idx < len(self.primary_pump_units) and self.primary_pump_units[idx]
|
||||
unit_enabled = (
|
||||
self.primary_pump_active and idx < len(self.primary_pump_units) and self.primary_pump_units[idx]
|
||||
)
|
||||
desired_flow = target_flow if unit_enabled else 0.0
|
||||
desired_pressure = target_pressure if unit_enabled else 0.5
|
||||
pump_state.flow_rate = self._ramp_value(
|
||||
@@ -180,7 +217,7 @@ class Reactor:
|
||||
pump_state.pressure = self._ramp_value(
|
||||
pump_state.pressure, desired_pressure, dt, self.primary_pump.spool_time
|
||||
)
|
||||
pump_state.active = unit_enabled or pump_state.flow_rate > 1.0
|
||||
pump_state.active = (unit_enabled and power_ratio > 0.05) or pump_state.flow_rate > 1.0
|
||||
total_flow += pump_state.flow_rate
|
||||
loop_pressure = max(loop_pressure, pump_state.pressure)
|
||||
state.primary_loop.mass_flow_rate = total_flow
|
||||
@@ -208,7 +245,9 @@ class Reactor:
|
||||
loop_pressure = 0.5
|
||||
target_flow = self.secondary_pump.flow_rate(0.75)
|
||||
for idx, pump_state in enumerate(state.secondary_pumps):
|
||||
unit_enabled = idx < len(self.secondary_pump_units) and self.secondary_pump_units[idx]
|
||||
unit_enabled = (
|
||||
self.secondary_pump_active and idx < len(self.secondary_pump_units) and self.secondary_pump_units[idx]
|
||||
)
|
||||
desired_flow = target_flow if unit_enabled else 0.0
|
||||
desired_pressure = target_pressure if unit_enabled else 0.5
|
||||
pump_state.flow_rate = self._ramp_value(
|
||||
@@ -255,9 +294,10 @@ class Reactor:
|
||||
|
||||
failures = self.health_monitor.evaluate(
|
||||
state,
|
||||
self.primary_pump_active,
|
||||
self.secondary_pump_active,
|
||||
primary_units_active,
|
||||
secondary_units_active,
|
||||
self.turbine_unit_active,
|
||||
state.generators,
|
||||
dt,
|
||||
)
|
||||
for failure in failures:
|
||||
@@ -346,10 +386,15 @@ class Reactor:
|
||||
LOGGER.critical("Core failure detected. Initiating SCRAM.")
|
||||
self.shutdown = True
|
||||
self.control.scram()
|
||||
elif component == "primary_pump":
|
||||
self._set_primary_pump(False)
|
||||
elif component == "secondary_pump":
|
||||
self._set_secondary_pump(False)
|
||||
elif component.startswith("primary_pump"):
|
||||
idx = self._component_index(component)
|
||||
self._toggle_primary_pump_unit(idx, False)
|
||||
elif component.startswith("secondary_pump"):
|
||||
idx = self._component_index(component)
|
||||
self._toggle_secondary_pump_unit(idx, False)
|
||||
elif component.startswith("generator"):
|
||||
idx = self._component_index(component)
|
||||
LOGGER.warning("Generator %d failed", idx + 1)
|
||||
elif component.startswith("turbine"):
|
||||
idx = self._component_index(component)
|
||||
self._set_turbine_state(False, index=idx)
|
||||
@@ -370,16 +415,15 @@ class Reactor:
|
||||
self.shutdown = self.shutdown or command.rod_position >= 0.95
|
||||
elif command.rod_step is not None:
|
||||
overrides["rod_fraction"] = self.control.increment_rods(command.rod_step)
|
||||
if command.primary_pump_on is not None:
|
||||
self._set_primary_pump(command.primary_pump_on)
|
||||
if command.secondary_pump_on is not None:
|
||||
self._set_secondary_pump(command.secondary_pump_on)
|
||||
if command.primary_pumps:
|
||||
for idx, flag in command.primary_pumps.items():
|
||||
self._toggle_primary_pump_unit(idx - 1, flag)
|
||||
if command.secondary_pumps:
|
||||
for idx, flag in command.secondary_pumps.items():
|
||||
self._toggle_secondary_pump_unit(idx - 1, flag)
|
||||
if command.generator_units:
|
||||
for idx, flag in command.generator_units.items():
|
||||
self._toggle_generator(idx - 1, flag, state)
|
||||
if command.turbine_on is not None:
|
||||
self._set_turbine_state(command.turbine_on)
|
||||
if command.turbine_units:
|
||||
@@ -418,10 +462,9 @@ class Reactor:
|
||||
if index < 0 or index >= len(self.primary_pump_units):
|
||||
LOGGER.warning("Ignoring primary pump index %s", index)
|
||||
return
|
||||
if self.primary_pump_units[index] == active:
|
||||
return
|
||||
self.primary_pump_units[index] = active
|
||||
LOGGER.info("Primary pump %d %s", index + 1, "enabled" if active else "stopped")
|
||||
if self.primary_pump_units[index] != active:
|
||||
self.primary_pump_units[index] = active
|
||||
LOGGER.info("Primary pump %d %s", index + 1, "enabled" if active else "stopped")
|
||||
if active:
|
||||
self._set_primary_pump(True)
|
||||
elif not any(self.primary_pump_units):
|
||||
@@ -431,15 +474,65 @@ class Reactor:
|
||||
if index < 0 or index >= len(self.secondary_pump_units):
|
||||
LOGGER.warning("Ignoring secondary pump index %s", index)
|
||||
return
|
||||
if self.secondary_pump_units[index] == active:
|
||||
return
|
||||
self.secondary_pump_units[index] = active
|
||||
LOGGER.info("Secondary pump %d %s", index + 1, "enabled" if active else "stopped")
|
||||
if self.secondary_pump_units[index] != active:
|
||||
self.secondary_pump_units[index] = active
|
||||
LOGGER.info("Secondary pump %d %s", index + 1, "enabled" if active else "stopped")
|
||||
if active:
|
||||
self._set_secondary_pump(True)
|
||||
elif not any(self.secondary_pump_units):
|
||||
self._set_secondary_pump(False)
|
||||
|
||||
def _toggle_generator(self, index: int, active: bool, state: PlantState) -> None:
|
||||
if index < 0 or index >= len(self.generators) or index >= len(state.generators):
|
||||
LOGGER.warning("Ignoring generator index %s", index)
|
||||
return
|
||||
gen_state = state.generators[index]
|
||||
if active:
|
||||
self.generators[index].start(gen_state)
|
||||
else:
|
||||
self.generators[index].stop(gen_state)
|
||||
|
||||
def _trigger_meltdown(self, state: PlantState) -> None:
|
||||
LOGGER.critical("Core meltdown in progress (%.1f K)", state.core.fuel_temperature)
|
||||
self.meltdown = True
|
||||
self.shutdown = True
|
||||
self.control.scram()
|
||||
try:
|
||||
self.health_monitor.component("core").fail()
|
||||
except KeyError:
|
||||
pass
|
||||
self._set_turbine_state(False)
|
||||
|
||||
def _step_generators(self, state: PlantState, aux_demand: float, turbine_electric: float, dt: float) -> float:
|
||||
# Ensure we have generator state objects aligned with hardware.
|
||||
if not state.generators or len(state.generators) < len(self.generators):
|
||||
missing = len(self.generators) - len(state.generators)
|
||||
for _ in range(missing):
|
||||
state.generators.append(
|
||||
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 deficit > 0.0:
|
||||
for idx, gen_state in enumerate(state.generators):
|
||||
if not (gen_state.running or gen_state.starting):
|
||||
self.generators[idx].start(gen_state)
|
||||
deficit -= self.generators[idx].rated_output_mw
|
||||
if deficit <= 0:
|
||||
break
|
||||
elif turbine_electric > aux_demand:
|
||||
for idx, gen_state in enumerate(state.generators):
|
||||
if gen_state.running and not gen_state.starting:
|
||||
self.generators[idx].stop(gen_state)
|
||||
|
||||
total_power = 0.0
|
||||
remaining = max(0.0, aux_demand - turbine_electric)
|
||||
for idx, gen_state in enumerate(state.generators):
|
||||
load = remaining if remaining > 0 else 0.0
|
||||
delivered = self.generators[idx].step(gen_state, load, dt)
|
||||
total_power += delivered
|
||||
remaining = max(0.0, remaining - delivered)
|
||||
return total_power
|
||||
|
||||
def _set_turbine_state(self, active: bool, index: int | None = None) -> None:
|
||||
if index is None:
|
||||
for idx in range(len(self.turbine_unit_active)):
|
||||
@@ -456,9 +549,11 @@ class Reactor:
|
||||
def _component_index(self, name: str) -> int:
|
||||
if name == "turbine":
|
||||
return 0
|
||||
parts = name.split("_")
|
||||
try:
|
||||
return int(name.split("_")[1]) - 1
|
||||
except (IndexError, ValueError):
|
||||
for token in reversed(parts):
|
||||
return int(token) - 1
|
||||
except (ValueError, TypeError):
|
||||
return -1
|
||||
|
||||
def _perform_maintenance(self, component: str) -> None:
|
||||
@@ -495,12 +590,27 @@ class Reactor:
|
||||
if component == "core" and not self.shutdown:
|
||||
LOGGER.warning("Cannot maintain core while reactor is running")
|
||||
return False
|
||||
if component == "primary_pump" and self.primary_pump_active:
|
||||
LOGGER.warning("Stop primary pump before maintenance")
|
||||
return False
|
||||
if component == "secondary_pump" and self.secondary_pump_active:
|
||||
LOGGER.warning("Stop secondary pump before maintenance")
|
||||
return False
|
||||
if component.startswith("primary_pump_"):
|
||||
idx = self._component_index(component)
|
||||
if idx < 0 or idx >= len(self.primary_pump_units):
|
||||
LOGGER.warning("Unknown primary pump maintenance target %s", component)
|
||||
return False
|
||||
if self.primary_pump_units[idx]:
|
||||
LOGGER.warning("Stop primary pump %d before maintenance", idx + 1)
|
||||
return False
|
||||
if component.startswith("secondary_pump_"):
|
||||
idx = self._component_index(component)
|
||||
if idx < 0 or idx >= len(self.secondary_pump_units):
|
||||
LOGGER.warning("Unknown secondary pump maintenance target %s", component)
|
||||
return False
|
||||
if self.secondary_pump_units[idx]:
|
||||
LOGGER.warning("Stop secondary pump %d before maintenance", idx + 1)
|
||||
return False
|
||||
if component.startswith("generator_"):
|
||||
idx = self._component_index(component)
|
||||
if idx < 0 or idx >= len(self.generators):
|
||||
LOGGER.warning("Unknown generator maintenance target %s", component)
|
||||
return False
|
||||
if component.startswith("turbine"):
|
||||
idx = self._component_index(component)
|
||||
if idx < 0 or idx >= len(self.turbine_unit_active):
|
||||
@@ -529,7 +639,18 @@ class Reactor:
|
||||
"turbine_active": self.turbine_active,
|
||||
"turbine_units": self.turbine_unit_active,
|
||||
"shutdown": self.shutdown,
|
||||
"meltdown": self.meltdown,
|
||||
"maintenance_active": list(self.maintenance_active),
|
||||
"generators": [
|
||||
{
|
||||
"running": g.running,
|
||||
"starting": g.starting,
|
||||
"spool_remaining": g.spool_remaining,
|
||||
"power_output_mw": g.power_output_mw,
|
||||
"battery_charge": g.battery_charge,
|
||||
}
|
||||
for g in state.generators
|
||||
],
|
||||
"consumer": {
|
||||
"online": self.consumer.online if self.consumer else False,
|
||||
"demand_mw": self.consumer.demand_mw if self.consumer else 0.0,
|
||||
@@ -549,6 +670,7 @@ class Reactor:
|
||||
self.turbine_unit_active = list(unit_states)
|
||||
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)
|
||||
maint = metadata.get("maintenance_active")
|
||||
if maint is not None:
|
||||
self.maintenance_active = set(maint)
|
||||
@@ -594,6 +716,26 @@ class Reactor:
|
||||
load_supplied_mw=0.0,
|
||||
)
|
||||
)
|
||||
gen_meta = metadata.get("generators", [])
|
||||
if not plant.generators or len(plant.generators) < len(self.generators):
|
||||
while len(plant.generators) < len(self.generators):
|
||||
plant.generators.append(
|
||||
GeneratorState(
|
||||
running=False,
|
||||
starting=False,
|
||||
spool_remaining=0.0,
|
||||
power_output_mw=0.0,
|
||||
battery_charge=1.0,
|
||||
)
|
||||
)
|
||||
for idx, gen_state in enumerate(plant.generators):
|
||||
if idx < len(gen_meta):
|
||||
cfg = gen_meta[idx]
|
||||
gen_state.running = cfg.get("running", gen_state.running)
|
||||
gen_state.starting = cfg.get("starting", gen_state.starting)
|
||||
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)
|
||||
return plant
|
||||
|
||||
def _handle_heat_sink_loss(self, state: PlantState) -> None:
|
||||
|
||||
Reference in New Issue
Block a user