Add pressurizer and coolant inventory controls

This commit is contained in:
Codex Agent
2025-11-23 19:43:57 +01:00
parent 86baa43865
commit ba4505701a
6 changed files with 146 additions and 3 deletions

View File

@@ -36,6 +36,7 @@ class Reactor:
atomic_model: AtomicPhysics
consumer: ElectricalConsumer | None = None
health_monitor: HealthMonitor = field(default_factory=HealthMonitor)
pressurizer_level: float = 0.6
primary_pump_active: bool = True
secondary_pump_active: bool = True
primary_pump_units: list[bool] = field(default_factory=lambda: [True, True])
@@ -85,6 +86,9 @@ class Reactor:
def initial_state(self) -> PlantState:
ambient = constants.ENVIRONMENT_TEMPERATURE
primary_nominal_mass = constants.PRIMARY_LOOP_VOLUME_M3 * constants.COOLANT_DENSITY
secondary_nominal_mass = constants.SECONDARY_LOOP_VOLUME_M3 * constants.COOLANT_DENSITY
self.pressurizer_level = 0.6
core = CoreState(
fuel_temperature=ambient,
neutron_flux=1e5,
@@ -119,6 +123,8 @@ class Reactor:
pressure=0.5,
mass_flow_rate=0.0,
steam_quality=0.0,
inventory_kg=primary_nominal_mass * constants.PRIMARY_INVENTORY_TARGET,
level=constants.PRIMARY_INVENTORY_TARGET,
)
secondary = CoolantLoopState(
temperature_in=ambient,
@@ -126,6 +132,8 @@ class Reactor:
pressure=0.5,
mass_flow_rate=0.0,
steam_quality=0.0,
inventory_kg=secondary_nominal_mass * constants.SECONDARY_INVENTORY_TARGET,
level=constants.SECONDARY_INVENTORY_TARGET,
)
primary_pumps = [
PumpState(active=self.primary_pump_active and self.primary_pump_units[idx], flow_rate=0.0, pressure=0.5)
@@ -202,6 +210,13 @@ class Reactor:
state.core.add_emitted_particles(particles)
self._check_poison_alerts(state)
self._update_loop_inventory(
state.primary_loop, constants.PRIMARY_LOOP_VOLUME_M3, constants.PRIMARY_INVENTORY_TARGET, dt
)
self._update_loop_inventory(
state.secondary_loop, constants.SECONDARY_LOOP_VOLUME_M3, constants.SECONDARY_INVENTORY_TARGET, dt
)
pump_demand = overrides.get(
"coolant_demand",
self.control.coolant_demand(
@@ -256,12 +271,15 @@ class Reactor:
target_flow = base_flow * power_ratio
loop_pressure = max(0.1, saturation_pressure(state.primary_loop.temperature_out))
target_pressure = max(0.5, base_head * power_ratio)
primary_flow_scale = min(
self._inventory_flow_scale(state.primary_loop), self._npsh_factor(state.primary_loop)
)
for idx, pump_state in enumerate(state.primary_pumps):
unit_enabled = (
self.primary_pump_active and idx < len(self.primary_pump_units) and self.primary_pump_units[idx]
)
powered = power_ratio > 0.1
desired_flow = target_flow if unit_enabled else 0.0
desired_flow = target_flow * primary_flow_scale if unit_enabled else 0.0
desired_pressure = target_pressure if unit_enabled else 0.5
if not powered:
desired_flow = 0.0
@@ -275,6 +293,8 @@ class Reactor:
pump_state.active = unit_enabled and powered and pump_state.flow_rate > 1.0
if not powered or not unit_enabled:
pump_state.status = "STOPPING" if pump_state.flow_rate > 1.0 else "OFF"
elif primary_flow_scale < 0.99:
pump_state.status = "CAV"
elif pump_state.flow_rate < max(1.0, desired_flow * 0.8):
pump_state.status = "STARTING"
else:
@@ -289,8 +309,10 @@ class Reactor:
state.primary_loop.mass_flow_rate = self._ramp_value(
state.primary_loop.mass_flow_rate, 0.0, dt, self.primary_pump.spool_time
)
pressurizer_floor = constants.PRIMARY_PRESSURIZER_SETPOINT_MPA - 0.5 if self.pressurizer_level > 0.05 else 0.5
target_pressure = max(pressurizer_floor, saturation_pressure(state.primary_loop.temperature_out))
state.primary_loop.pressure = self._ramp_value(
state.primary_loop.pressure, max(0.1, saturation_pressure(state.primary_loop.temperature_out)), dt, self.primary_pump.spool_time
state.primary_loop.pressure, target_pressure, dt, self.primary_pump.spool_time
)
for pump_state in state.primary_pumps:
pump_state.active = False
@@ -305,12 +327,15 @@ class Reactor:
target_pressure = max(0.5, base_head * power_ratio)
loop_pressure = max(0.1, saturation_pressure(state.secondary_loop.temperature_out))
target_flow = base_flow * power_ratio
secondary_flow_scale = min(
self._inventory_flow_scale(state.secondary_loop), self._npsh_factor(state.secondary_loop)
)
for idx, pump_state in enumerate(state.secondary_pumps):
unit_enabled = (
self.secondary_pump_active and idx < len(self.secondary_pump_units) and self.secondary_pump_units[idx]
)
powered = power_ratio > 0.1
desired_flow = target_flow if unit_enabled else 0.0
desired_flow = target_flow * secondary_flow_scale if unit_enabled else 0.0
desired_pressure = target_pressure if unit_enabled else 0.5
if not powered:
desired_flow = 0.0
@@ -324,6 +349,8 @@ class Reactor:
pump_state.active = unit_enabled and powered and pump_state.flow_rate > 1.0
if not powered or not unit_enabled:
pump_state.status = "STOPPING" if pump_state.flow_rate > 1.0 else "OFF"
elif secondary_flow_scale < 0.99:
pump_state.status = "CAV"
elif pump_state.flow_rate < max(1.0, desired_flow * 0.8):
pump_state.status = "STARTING"
else:
@@ -349,6 +376,7 @@ class Reactor:
pump_state.pressure = state.secondary_loop.pressure
pump_state.status = "STOPPING" if pump_state.flow_rate > 0.1 else "OFF"
self._apply_pressurizer(state.primary_loop, dt)
if self.primary_relief_open:
state.primary_loop.pressure = max(0.1, saturation_pressure(state.primary_loop.temperature_out))
if self.secondary_relief_open:
@@ -360,6 +388,10 @@ class Reactor:
transferred = heat_transfer(state.primary_loop, state.secondary_loop, total_power)
self.thermal.step_core(state.core, state.primary_loop, total_power, dt)
self.thermal.step_secondary(state.secondary_loop, transferred)
self._apply_secondary_boiloff(state, dt)
self._update_loop_inventory(
state.secondary_loop, constants.SECONDARY_LOOP_VOLUME_M3, constants.SECONDARY_INVENTORY_TARGET, dt
)
self._step_turbine_bank(state, transferred, dt)
self._maintenance_tick(state, dt)
@@ -487,6 +519,59 @@ class Reactor:
turbine_state.load_demand_mw = demand_per_unit
turbine_state.load_supplied_mw = share if demand_per_unit <= 0 else min(share, demand_per_unit)
def _nominal_inventory(self, volume_m3: float) -> float:
return volume_m3 * constants.COOLANT_DENSITY
def _update_loop_inventory(
self, loop: CoolantLoopState, volume_m3: float, target_level: float, dt: float
) -> None:
nominal_mass = self._nominal_inventory(volume_m3)
if nominal_mass <= 0.0:
loop.level = 0.0
return
if loop.inventory_kg <= 0.0:
loop.inventory_kg = nominal_mass * target_level
current_level = loop.inventory_kg / nominal_mass
correction = (target_level - current_level) * constants.LOOP_INVENTORY_CORRECTION_RATE
loop.inventory_kg = max(0.0, loop.inventory_kg + correction * nominal_mass * dt)
loop.level = min(1.2, max(0.0, loop.inventory_kg / nominal_mass))
def _inventory_flow_scale(self, loop: CoolantLoopState) -> float:
if loop.level <= constants.LOW_LEVEL_FLOW_FLOOR:
return 0.0
if loop.level <= 0.25:
return max(0.0, (loop.level - constants.LOW_LEVEL_FLOW_FLOOR) / (0.25 - constants.LOW_LEVEL_FLOW_FLOOR))
return 1.0
def _npsh_factor(self, loop: CoolantLoopState) -> float:
vapor_pressure = saturation_pressure(loop.temperature_in)
available = max(0.0, loop.pressure - vapor_pressure)
if available <= 0.0:
return 0.0
return max(0.0, min(1.0, available / constants.NPSH_REQUIRED_MPA))
def _apply_pressurizer(self, primary: CoolantLoopState, dt: float) -> None:
target = constants.PRIMARY_PRESSURIZER_SETPOINT_MPA
band = constants.PRIMARY_PRESSURIZER_DEADBAND_MPA
heat_rate = constants.PRIMARY_PRESSURIZER_HEAT_RATE_MPA_PER_S
spray_rate = constants.PRIMARY_PRESSURIZER_SPRAY_RATE_MPA_PER_S
if primary.pressure < target - band and self.pressurizer_level > 0.05:
primary.pressure = min(target, primary.pressure + heat_rate * dt)
self.pressurizer_level = max(0.0, self.pressurizer_level - constants.PRIMARY_PRESSURIZER_LEVEL_DRAW_PER_S * dt)
elif primary.pressure > target + band:
primary.pressure = max(target - band, primary.pressure - spray_rate * dt)
self.pressurizer_level = min(1.0, self.pressurizer_level + constants.PRIMARY_PRESSURIZER_LEVEL_FILL_PER_S * dt)
primary.pressure = min(constants.MAX_PRESSURE, max(saturation_pressure(primary.temperature_out), primary.pressure))
def _apply_secondary_boiloff(self, state: PlantState, dt: float) -> None:
loop = state.secondary_loop
if loop.mass_flow_rate <= 0.0 or loop.steam_quality <= 0.0:
return
steam_mass = loop.mass_flow_rate * loop.steam_quality * constants.SECONDARY_STEAM_LOSS_FRACTION * dt
loop.inventory_kg = max(0.0, loop.inventory_kg - steam_mass)
nominal = self._nominal_inventory(constants.SECONDARY_LOOP_VOLUME_M3)
loop.level = min(1.2, max(0.0, loop.inventory_kg / nominal)) if nominal > 0 else 0.0
def _handle_failure(self, component: str) -> None:
if component == "core":
LOGGER.critical("Core failure detected. Initiating SCRAM.")
@@ -765,6 +850,7 @@ class Reactor:
"generator_auto": self.generator_auto,
"primary_relief_open": self.primary_relief_open,
"secondary_relief_open": self.secondary_relief_open,
"pressurizer_level": self.pressurizer_level,
"maintenance_active": list(self.maintenance_active),
"generators": [
{
@@ -800,6 +886,7 @@ class Reactor:
self.generator_auto = metadata.get("generator_auto", self.generator_auto)
self.primary_relief_open = metadata.get("primary_relief_open", self.primary_relief_open)
self.secondary_relief_open = metadata.get("secondary_relief_open", self.secondary_relief_open)
self.pressurizer_level = metadata.get("pressurizer_level", self.pressurizer_level)
maint = metadata.get("maintenance_active")
if maint is not None:
self.maintenance_active = set(maint)