Add HX diagnostics and pump curves; retune coolant demand
This commit is contained in:
7
TODO.md
Normal file
7
TODO.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
## Future realism upgrades
|
||||||
|
|
||||||
|
- Implement steam generator UA·ΔT_lm heat exchange (done), pump head/flow curves (done); validate against target temps under nominal load.
|
||||||
|
- Next up: rod banks with worth curves and delayed groups, plus xenon/samarium buildup tied to power history.
|
||||||
|
- Add pressurizer behavior, primary/secondary inventory and level effects, and pump NPSH/cavitation checks.
|
||||||
|
- Model feedwater/steam-drum mass-energy balance, turbine throttle/efficiency maps, and condenser back-pressure.
|
||||||
|
- Introduce CHF/DNB margin, clad/fuel split temps, and SCRAM matrix for subcooling loss or SG level/pressure trips.
|
||||||
@@ -20,6 +20,8 @@ AMU_TO_KG = 1.660_539_066_60e-27
|
|||||||
MEV_TO_J = 1.602_176_634e-13
|
MEV_TO_J = 1.602_176_634e-13
|
||||||
ELECTRON_FISSION_CROSS_SECTION = 5e-16 # cm^2, tuned for simulation scale
|
ELECTRON_FISSION_CROSS_SECTION = 5e-16 # cm^2, tuned for simulation scale
|
||||||
PUMP_SPOOL_TIME = 5.0 # seconds to reach commanded flow
|
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
|
TURBINE_SPOOL_TIME = 12.0 # seconds to reach steady output
|
||||||
GENERATOR_SPOOL_TIME = 10.0 # seconds to reach full output
|
GENERATOR_SPOOL_TIME = 10.0 # seconds to reach full output
|
||||||
# Auxiliary power assumptions
|
# Auxiliary power assumptions
|
||||||
@@ -31,6 +33,7 @@ PRIMARY_OUTLET_TARGET_K = 580.0
|
|||||||
SECONDARY_OUTLET_TARGET_K = 520.0
|
SECONDARY_OUTLET_TARGET_K = 520.0
|
||||||
PRIMARY_NOMINAL_PRESSURE = 7.0 # MPa typical RBMK channel header pressure
|
PRIMARY_NOMINAL_PRESSURE = 7.0 # MPa typical RBMK channel header pressure
|
||||||
SECONDARY_NOMINAL_PRESSURE = 7.0 # MPa steam drum/steam line pressure surrogate
|
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.
|
# Threshold inventories (event counts) for flagging common poisons in diagnostics.
|
||||||
KEY_POISON_THRESHOLDS = {
|
KEY_POISON_THRESHOLDS = {
|
||||||
"Xe": 1e20, # xenon
|
"Xe": 1e20, # xenon
|
||||||
|
|||||||
@@ -59,25 +59,35 @@ class ControlSystem:
|
|||||||
self.manual_control = manual
|
self.manual_control = manual
|
||||||
LOGGER.info("Rod control %s", "manual" if manual else "automatic")
|
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
|
desired_temp = constants.PRIMARY_OUTLET_TARGET_K
|
||||||
# Increase demand when outlet is hotter than desired, reduce when cooler.
|
# Increase demand when outlet is hotter than desired, reduce when cooler.
|
||||||
temp_error = (primary.temperature_out - desired_temp) / 100.0
|
temp_error = (primary.temperature_out - desired_temp) / 100.0
|
||||||
demand = 0.8 + temp_error
|
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
|
power_floor = 0.0
|
||||||
if core_power_mw is not None:
|
if core_power_mw is not None:
|
||||||
power_fraction = clamp(core_power_mw / constants.NORMAL_CORE_POWER_MW, 0.0, 1.5)
|
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 = max(demand, power_floor)
|
||||||
demand = clamp(demand, 0.0, 1.0)
|
demand = clamp(demand, 0.0, 1.0)
|
||||||
LOGGER.debug(
|
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,
|
demand,
|
||||||
temp_error,
|
temp_error,
|
||||||
power_floor,
|
power_floor,
|
||||||
primary.temperature_out,
|
primary.temperature_out,
|
||||||
core_power_mw or 0.0,
|
core_power_mw or 0.0,
|
||||||
|
electrical_output_mw or 0.0,
|
||||||
)
|
)
|
||||||
return demand
|
return demand
|
||||||
|
|
||||||
|
|||||||
@@ -15,12 +15,21 @@ LOGGER = logging.getLogger(__name__)
|
|||||||
class Pump:
|
class Pump:
|
||||||
nominal_flow: float
|
nominal_flow: float
|
||||||
efficiency: float = 0.9
|
efficiency: float = 0.9
|
||||||
|
shutoff_head_mpa: float = 6.0
|
||||||
spool_time: float = constants.PUMP_SPOOL_TIME
|
spool_time: float = constants.PUMP_SPOOL_TIME
|
||||||
|
|
||||||
def flow_rate(self, demand: float) -> float:
|
def flow_rate(self, demand: float) -> float:
|
||||||
demand = max(0.0, min(1.0, demand))
|
demand = max(0.0, min(1.0, demand))
|
||||||
return self.nominal_flow * (0.2 + 0.8 * demand) * self.efficiency
|
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:
|
def step(self, loop: CoolantLoopState, demand: float) -> None:
|
||||||
loop.mass_flow_rate = self.flow_rate(demand)
|
loop.mass_flow_rate = self.flow_rate(demand)
|
||||||
loop.pressure = 12.0 * demand + 2.0
|
loop.pressure = 12.0 * demand + 2.0
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class NeutronDynamics:
|
|||||||
beta_effective: float = 0.0065
|
beta_effective: float = 0.0065
|
||||||
delayed_neutron_fraction: float = 0.0008
|
delayed_neutron_fraction: float = 0.0008
|
||||||
external_source_coupling: float = 1e-6
|
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:
|
def reactivity(self, state: CoreState, control_fraction: float) -> float:
|
||||||
rho = (
|
rho = (
|
||||||
|
|||||||
@@ -70,8 +70,10 @@ class Reactor:
|
|||||||
fuel=FuelAssembly(enrichment=0.045, mass_kg=80_000.0, atomic_physics=atomic_model),
|
fuel=FuelAssembly(enrichment=0.045, mass_kg=80_000.0, atomic_physics=atomic_model),
|
||||||
neutronics=NeutronDynamics(),
|
neutronics=NeutronDynamics(),
|
||||||
control=ControlSystem(),
|
control=ControlSystem(),
|
||||||
primary_pump=Pump(nominal_flow=18_000.0),
|
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),
|
secondary_pump=Pump(
|
||||||
|
nominal_flow=16_000.0, efficiency=0.85, shutoff_head_mpa=constants.SECONDARY_PUMP_SHUTOFF_HEAD_MPA
|
||||||
|
),
|
||||||
thermal=ThermalSolver(),
|
thermal=ThermalSolver(),
|
||||||
steam_generator=SteamGenerator(),
|
steam_generator=SteamGenerator(),
|
||||||
turbines=[Turbine() for _ in range(3)],
|
turbines=[Turbine() for _ in range(3)],
|
||||||
@@ -198,7 +200,12 @@ class Reactor:
|
|||||||
self._check_poison_alerts(state)
|
self._check_poison_alerts(state)
|
||||||
|
|
||||||
pump_demand = overrides.get(
|
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.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)
|
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:
|
if self.primary_pump_active:
|
||||||
total_flow = 0.0
|
total_flow = 0.0
|
||||||
target_pressure = (
|
base_flow, base_head = self.primary_pump.performance(pump_demand)
|
||||||
0.5 + (constants.PRIMARY_NOMINAL_PRESSURE - 0.5) * pump_demand
|
target_flow = base_flow * power_ratio
|
||||||
) * power_ratio
|
|
||||||
loop_pressure = max(0.1, saturation_pressure(state.primary_loop.temperature_out))
|
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):
|
for idx, pump_state in enumerate(state.primary_pumps):
|
||||||
unit_enabled = (
|
unit_enabled = (
|
||||||
self.primary_pump_active and idx < len(self.primary_pump_units) and self.primary_pump_units[idx]
|
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"
|
pump_state.status = "STOPPING" if pump_state.flow_rate > 0.1 else "OFF"
|
||||||
if self.secondary_pump_active:
|
if self.secondary_pump_active:
|
||||||
total_flow = 0.0
|
total_flow = 0.0
|
||||||
target_pressure = (
|
base_flow, base_head = self.secondary_pump.performance(0.75)
|
||||||
0.5 + (constants.SECONDARY_NOMINAL_PRESSURE - 0.5) * 0.75
|
target_pressure = max(0.5, base_head * power_ratio)
|
||||||
) * power_ratio
|
|
||||||
loop_pressure = max(0.1, saturation_pressure(state.secondary_loop.temperature_out))
|
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):
|
for idx, pump_state in enumerate(state.secondary_pumps):
|
||||||
unit_enabled = (
|
unit_enabled = (
|
||||||
self.secondary_pump_active and idx < len(self.secondary_pump_units) and self.secondary_pump_units[idx]
|
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."""
|
"""Return MW transferred to the secondary loop."""
|
||||||
if secondary.mass_flow_rate <= 0.0:
|
if secondary.mass_flow_rate <= 0.0:
|
||||||
return 0.0
|
return 0.0
|
||||||
delta_t = max(0.0, primary.temperature_out - secondary.temperature_in)
|
delta_t1 = max(1e-3, primary.temperature_out - secondary.temperature_in)
|
||||||
# Require a modest temperature difference to push full power across the steam generator.
|
delta_t2 = max(1e-3, primary.temperature_in - secondary.temperature_out)
|
||||||
conductance = 0.05
|
if delta_t1 <= 0.0 or delta_t2 <= 0.0:
|
||||||
efficiency = 1.0 - math.exp(-conductance * delta_t)
|
return 0.0
|
||||||
transferred = min(core_power_mw, core_power_mw * efficiency)
|
if abs(delta_t1 - delta_t2) < 1e-6:
|
||||||
LOGGER.debug("Heat transfer %.2f MW with ΔT=%.1fK", transferred, delta_t)
|
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
|
return transferred
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user