Add HX diagnostics and pump curves; retune coolant demand

This commit is contained in:
Codex Agent
2025-11-23 11:21:39 +01:00
parent 01fec1df42
commit 9807806663
7 changed files with 61 additions and 22 deletions

View File

@@ -20,6 +20,8 @@ AMU_TO_KG = 1.660_539_066_60e-27
MEV_TO_J = 1.602_176_634e-13
ELECTRON_FISSION_CROSS_SECTION = 5e-16 # cm^2, tuned for simulation scale
PUMP_SPOOL_TIME = 5.0 # seconds to reach commanded flow
PRIMARY_PUMP_SHUTOFF_HEAD_MPA = 8.0 # approximate shutoff head for primary pumps
SECONDARY_PUMP_SHUTOFF_HEAD_MPA = 7.0
TURBINE_SPOOL_TIME = 12.0 # seconds to reach steady output
GENERATOR_SPOOL_TIME = 10.0 # seconds to reach full output
# Auxiliary power assumptions
@@ -31,6 +33,7 @@ PRIMARY_OUTLET_TARGET_K = 580.0
SECONDARY_OUTLET_TARGET_K = 520.0
PRIMARY_NOMINAL_PRESSURE = 7.0 # MPa typical RBMK channel header pressure
SECONDARY_NOMINAL_PRESSURE = 7.0 # MPa steam drum/steam line pressure surrogate
STEAM_GENERATOR_UA_MW_PER_K = 30.0 # overall UA for steam generator (MW/K)
# Threshold inventories (event counts) for flagging common poisons in diagnostics.
KEY_POISON_THRESHOLDS = {
"Xe": 1e20, # xenon

View File

@@ -59,25 +59,35 @@ class ControlSystem:
self.manual_control = manual
LOGGER.info("Rod control %s", "manual" if manual else "automatic")
def coolant_demand(self, primary: CoolantLoopState, core_power_mw: float | None = None) -> float:
def coolant_demand(
self,
primary: CoolantLoopState,
core_power_mw: float | None = None,
electrical_output_mw: float | None = None,
) -> float:
desired_temp = constants.PRIMARY_OUTLET_TARGET_K
# Increase demand when outlet is hotter than desired, reduce when cooler.
temp_error = (primary.temperature_out - desired_temp) / 100.0
demand = 0.8 + temp_error
# Keep pumps spinning proportionally to reactor power so we do not idle them at full load.
# Keep a light power-proportional floor so both pumps stay spinning without flooding the loop.
power_floor = 0.0
if core_power_mw is not None:
power_fraction = clamp(core_power_mw / constants.NORMAL_CORE_POWER_MW, 0.0, 1.5)
power_floor = 0.1 + 0.7 * power_fraction
power_floor = 0.15 + 0.2 * power_fraction
# Allow warmer operation when electrical load is already being served (turbines online),
# but keep a higher floor when idling so test scenarios still converge near 3 GW.
if electrical_output_mw is not None and electrical_output_mw > 10.0:
power_floor *= 0.6
demand = max(demand, power_floor)
demand = clamp(demand, 0.0, 1.0)
LOGGER.debug(
"Coolant demand %.2f (temp_error=%.2f, power_floor=%.2f) for outlet %.1fK power %.1f MW",
"Coolant demand %.2f (temp_error=%.2f, power_floor=%.2f) for outlet %.1fK power %.1f MW elec %.1f MW",
demand,
temp_error,
power_floor,
primary.temperature_out,
core_power_mw or 0.0,
electrical_output_mw or 0.0,
)
return demand

View File

@@ -15,12 +15,21 @@ LOGGER = logging.getLogger(__name__)
class Pump:
nominal_flow: float
efficiency: float = 0.9
shutoff_head_mpa: float = 6.0
spool_time: float = constants.PUMP_SPOOL_TIME
def flow_rate(self, demand: float) -> float:
demand = max(0.0, min(1.0, demand))
return self.nominal_flow * (0.2 + 0.8 * demand) * self.efficiency
def performance(self, demand: float) -> tuple[float, float]:
"""Return (flow_kg_s, head_mpa) at the given demand using a simple pump curve."""
demand = max(0.0, min(1.0, demand))
flow = self.flow_rate(demand)
flow_frac = min(1.2, flow / max(1e-3, self.nominal_flow))
head = max(0.0, self.shutoff_head_mpa * max(0.0, 1.0 - flow_frac**2))
return flow, head
def step(self, loop: CoolantLoopState, demand: float) -> None:
loop.mass_flow_rate = self.flow_rate(demand)
loop.pressure = 12.0 * demand + 2.0

View File

@@ -28,7 +28,7 @@ class NeutronDynamics:
beta_effective: float = 0.0065
delayed_neutron_fraction: float = 0.0008
external_source_coupling: float = 1e-6
shutdown_bias: float = -0.014
shutdown_bias: float = -0.012
def reactivity(self, state: CoreState, control_fraction: float) -> float:
rho = (

View File

@@ -70,8 +70,10 @@ class Reactor:
fuel=FuelAssembly(enrichment=0.045, mass_kg=80_000.0, atomic_physics=atomic_model),
neutronics=NeutronDynamics(),
control=ControlSystem(),
primary_pump=Pump(nominal_flow=18_000.0),
secondary_pump=Pump(nominal_flow=16_000.0, efficiency=0.85),
primary_pump=Pump(nominal_flow=18_000.0, shutoff_head_mpa=constants.PRIMARY_PUMP_SHUTOFF_HEAD_MPA),
secondary_pump=Pump(
nominal_flow=16_000.0, efficiency=0.85, shutoff_head_mpa=constants.SECONDARY_PUMP_SHUTOFF_HEAD_MPA
),
thermal=ThermalSolver(),
steam_generator=SteamGenerator(),
turbines=[Turbine() for _ in range(3)],
@@ -198,7 +200,12 @@ class Reactor:
self._check_poison_alerts(state)
pump_demand = overrides.get(
"coolant_demand", self.control.coolant_demand(state.primary_loop, state.core.power_output_mw)
"coolant_demand",
self.control.coolant_demand(
state.primary_loop,
state.core.power_output_mw,
state.total_electrical_output(),
),
)
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)
@@ -242,11 +249,10 @@ class Reactor:
if self.primary_pump_active:
total_flow = 0.0
target_pressure = (
0.5 + (constants.PRIMARY_NOMINAL_PRESSURE - 0.5) * pump_demand
) * power_ratio
base_flow, base_head = self.primary_pump.performance(pump_demand)
target_flow = base_flow * power_ratio
loop_pressure = max(0.1, saturation_pressure(state.primary_loop.temperature_out))
target_flow = self.primary_pump.flow_rate(pump_demand) * power_ratio
target_pressure = max(0.5, base_head * power_ratio)
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]
@@ -292,11 +298,10 @@ class Reactor:
pump_state.status = "STOPPING" if pump_state.flow_rate > 0.1 else "OFF"
if self.secondary_pump_active:
total_flow = 0.0
target_pressure = (
0.5 + (constants.SECONDARY_NOMINAL_PRESSURE - 0.5) * 0.75
) * power_ratio
base_flow, base_head = self.secondary_pump.performance(0.75)
target_pressure = max(0.5, base_head * power_ratio)
loop_pressure = max(0.1, saturation_pressure(state.secondary_loop.temperature_out))
target_flow = self.secondary_pump.flow_rate(0.75) * power_ratio
target_flow = base_flow * power_ratio
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]

View File

@@ -17,12 +17,17 @@ def heat_transfer(primary: CoolantLoopState, secondary: CoolantLoopState, core_p
"""Return MW transferred to the secondary loop."""
if secondary.mass_flow_rate <= 0.0:
return 0.0
delta_t = max(0.0, primary.temperature_out - secondary.temperature_in)
# Require a modest temperature difference to push full power across the steam generator.
conductance = 0.05
efficiency = 1.0 - math.exp(-conductance * delta_t)
transferred = min(core_power_mw, core_power_mw * efficiency)
LOGGER.debug("Heat transfer %.2f MW with ΔT=%.1fK", transferred, delta_t)
delta_t1 = max(1e-3, primary.temperature_out - secondary.temperature_in)
delta_t2 = max(1e-3, primary.temperature_in - secondary.temperature_out)
if delta_t1 <= 0.0 or delta_t2 <= 0.0:
return 0.0
if abs(delta_t1 - delta_t2) < 1e-6:
lmtd = delta_t1
else:
lmtd = (delta_t1 - delta_t2) / math.log(delta_t1 / delta_t2)
ua = constants.STEAM_GENERATOR_UA_MW_PER_K
transferred = max(0.0, min(core_power_mw, ua * lmtd))
LOGGER.debug("Heat transfer %.2f MW with LMTD=%.1fK (ΔT1=%.1f ΔT2=%.1f)", transferred, lmtd, delta_t1, delta_t2)
return transferred