Add HX diagnostics and pump curves; retune coolant demand
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user