Add DNB/subcooling margins and keypad rod control
This commit is contained in:
@@ -12,6 +12,9 @@ STEAM_LATENT_HEAT = 2_200_000.0 # J/kg approximate latent heat of vaporization
|
|||||||
CORE_MELTDOWN_TEMPERATURE = 2_873.0 # K (approx 2600C) threshold for irreversible meltdown
|
CORE_MELTDOWN_TEMPERATURE = 2_873.0 # K (approx 2600C) threshold for irreversible meltdown
|
||||||
MAX_CORE_TEMPERATURE = CORE_MELTDOWN_TEMPERATURE # Allow simulation to approach meltdown temperature
|
MAX_CORE_TEMPERATURE = CORE_MELTDOWN_TEMPERATURE # Allow simulation to approach meltdown temperature
|
||||||
MAX_PRESSURE = 15.0 # MPa typical PWR primary loop limit
|
MAX_PRESSURE = 15.0 # MPa typical PWR primary loop limit
|
||||||
|
CLAD_MAX_TEMPERATURE = 1_200.0 # K clad softening / DNB concern
|
||||||
|
CHF_MASS_FLUX_REF = 1_500.0 # kg/m2-s reference mass flux surrogate
|
||||||
|
CHF_PRESSURE_REF_MPA = 7.0 # MPa reference pressure for CHF surrogate
|
||||||
CONTROL_ROD_SPEED = 0.03 # fraction insertion per second
|
CONTROL_ROD_SPEED = 0.03 # fraction insertion per second
|
||||||
CONTROL_ROD_WORTH = 0.042 # delta rho contribution when fully withdrawn
|
CONTROL_ROD_WORTH = 0.042 # delta rho contribution when fully withdrawn
|
||||||
CONTROL_ROD_BANK_WEIGHTS = (0.4, 0.35, 0.25)
|
CONTROL_ROD_BANK_WEIGHTS = (0.4, 0.35, 0.25)
|
||||||
|
|||||||
@@ -541,7 +541,7 @@ class ReactorDashboard:
|
|||||||
"Toggle turbine units (1/2/3) for staggered maintenance.",
|
"Toggle turbine units (1/2/3) for staggered maintenance.",
|
||||||
"Use m/n/,/. for pump maintenance; B/V for generators.",
|
"Use m/n/,/. for pump maintenance; B/V for generators.",
|
||||||
"Press 'r' to reset/clear state if you want a cold start.",
|
"Press 'r' to reset/clear state if you want a cold start.",
|
||||||
"Watch component health to avoid automatic trips.",
|
"Watch component health, DNB margin, and subcooling to avoid automatic trips.",
|
||||||
]
|
]
|
||||||
for idx, tip in enumerate(tips, start=y + 2):
|
for idx, tip in enumerate(tips, start=y + 2):
|
||||||
if not _add_safe(idx, 4, f"- {tip}"):
|
if not _add_safe(idx, 4, f"- {tip}"):
|
||||||
|
|||||||
@@ -402,6 +402,12 @@ class Reactor:
|
|||||||
|
|
||||||
if (not self.secondary_pump_active or state.secondary_loop.mass_flow_rate <= 1.0) and total_power > 50.0:
|
if (not self.secondary_pump_active or state.secondary_loop.mass_flow_rate <= 1.0) and total_power > 50.0:
|
||||||
self._handle_heat_sink_loss(state)
|
self._handle_heat_sink_loss(state)
|
||||||
|
if state.core.dnb_margin is not None and state.core.dnb_margin < 0.3:
|
||||||
|
LOGGER.critical("DNB margin low: %.2f, initiating SCRAM", state.core.dnb_margin)
|
||||||
|
self.shutdown = True
|
||||||
|
self.control.scram()
|
||||||
|
if state.core.subcooling_margin is not None and state.core.subcooling_margin < 5.0:
|
||||||
|
LOGGER.warning("Subcooling margin low: %.1fK", state.core.subcooling_margin)
|
||||||
|
|
||||||
failures = self.health_monitor.evaluate(
|
failures = self.health_monitor.evaluate(
|
||||||
state,
|
state,
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ class CoreState:
|
|||||||
power_output_mw: float # MW thermal
|
power_output_mw: float # MW thermal
|
||||||
burnup: float # fraction of fuel consumed
|
burnup: float # fraction of fuel consumed
|
||||||
clad_temperature: float | None = None # Kelvin
|
clad_temperature: float | None = None # Kelvin
|
||||||
|
dnb_margin: float | None = None # ratio to critical heat flux surrogate
|
||||||
|
subcooling_margin: float | None = None # K until boiling
|
||||||
xenon_inventory: float = 0.0
|
xenon_inventory: float = 0.0
|
||||||
iodine_inventory: float = 0.0
|
iodine_inventory: float = 0.0
|
||||||
delayed_precursors: list[float] = field(default_factory=list)
|
delayed_precursors: list[float] = field(default_factory=list)
|
||||||
@@ -30,6 +32,10 @@ class CoreState:
|
|||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if self.clad_temperature is None:
|
if self.clad_temperature is None:
|
||||||
self.clad_temperature = self.fuel_temperature
|
self.clad_temperature = self.fuel_temperature
|
||||||
|
if self.dnb_margin is None:
|
||||||
|
self.dnb_margin = 1.0
|
||||||
|
if self.subcooling_margin is None:
|
||||||
|
self.subcooling_margin = 0.0
|
||||||
|
|
||||||
def update_burnup(self, dt: float) -> None:
|
def update_burnup(self, dt: float) -> None:
|
||||||
produced_energy_mwh = self.power_output_mw * (dt / 3600.0)
|
produced_energy_mwh = self.power_output_mw * (dt / 3600.0)
|
||||||
|
|||||||
@@ -95,6 +95,11 @@ class ThermalSolver:
|
|||||||
core.fuel_temperature += heating - cooling
|
core.fuel_temperature += heating - cooling
|
||||||
# Keep fuel temperature bounded and never below the coolant outlet temperature.
|
# Keep fuel temperature bounded and never below the coolant outlet temperature.
|
||||||
core.fuel_temperature = min(max(primary.temperature_out, core.fuel_temperature), constants.MAX_CORE_TEMPERATURE)
|
core.fuel_temperature = min(max(primary.temperature_out, core.fuel_temperature), constants.MAX_CORE_TEMPERATURE)
|
||||||
|
core.clad_temperature = max(primary.temperature_out, core.clad_temperature or primary.temperature_out)
|
||||||
|
core.subcooling_margin = max(0.0, saturation_temperature(primary.pressure) - primary.temperature_out)
|
||||||
|
chf = self._critical_heat_flux(primary)
|
||||||
|
heat_flux = (power_mw * constants.MEGAWATT) / max(1.0, self._core_surface_area())
|
||||||
|
core.dnb_margin = max(0.0, chf / max(1e-6, heat_flux))
|
||||||
avg_temp = 0.5 * (primary.temperature_in + primary.temperature_out)
|
avg_temp = 0.5 * (primary.temperature_in + primary.temperature_out)
|
||||||
primary.energy_j = max(
|
primary.energy_j = max(
|
||||||
0.0, primary.inventory_kg * constants.COOLANT_HEAT_CAPACITY * avg_temp
|
0.0, primary.inventory_kg * constants.COOLANT_HEAT_CAPACITY * avg_temp
|
||||||
@@ -151,3 +156,17 @@ class ThermalSolver:
|
|||||||
secondary.steam_quality,
|
secondary.steam_quality,
|
||||||
secondary.energy_j,
|
secondary.energy_j,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _critical_heat_flux(self, primary: CoolantLoopState) -> float:
|
||||||
|
"""Rough CHF surrogate using mass flux and pressure."""
|
||||||
|
# Use a coarse mass-flux and pressure scaling to emulate higher CHF with more flow/pressure.
|
||||||
|
mass_flux = max(1.0, primary.mass_flow_rate / 50.0) # kg/m2-s surrogate
|
||||||
|
flux_factor = max(0.5, min(3.0, (mass_flux / 200.0) ** 0.5))
|
||||||
|
pressure_factor = 0.5 + 0.5 * min(1.5, primary.pressure / max(0.1, constants.MAX_PRESSURE))
|
||||||
|
base_chf = 1.0e7 # W/m2 surrogate
|
||||||
|
return base_chf * flux_factor * pressure_factor
|
||||||
|
|
||||||
|
def _core_surface_area(self) -> float:
|
||||||
|
# Simple surrogate: area scaling with volume^(2/3)
|
||||||
|
volume = self.primary_volume_m3
|
||||||
|
return max(1.0, (volume ** (2.0 / 3.0)) * 10.0)
|
||||||
|
|||||||
Reference in New Issue
Block a user