diff --git a/src/reactor_sim/constants.py b/src/reactor_sim/constants.py index db97f4c..b9376e5 100644 --- a/src/reactor_sim/constants.py +++ b/src/reactor_sim/constants.py @@ -79,6 +79,10 @@ HX_FOULING_HEAL_RATE = 5e-6 # cleaning/settling when cool/low steam HX_FOULING_MAX_PENALTY = 0.25 # fractional UA loss cap BORON_WORTH_PER_PPM = 8e-6 # delta rho per ppm relative to baseline boron BORON_TRIM_RATE_PPM_PER_S = 0.02 # slow boron trim toward setpoint when near target +# Mild thermal/measurement lags +FUEL_TO_CLAD_TIME_CONSTANT = 0.3 # seconds (mild lag) +CLAD_TO_COOLANT_TIME_CONSTANT = 0.2 # seconds (mild lag) +POWER_MEASUREMENT_TIME_CONSTANT = 6.0 # seconds # Threshold inventories (event counts) for flagging common poisons in diagnostics. KEY_POISON_THRESHOLDS = { "Xe": 1e20, # xenon diff --git a/src/reactor_sim/control.py b/src/reactor_sim/control.py index efd1ed1..50ebc78 100644 --- a/src/reactor_sim/control.py +++ b/src/reactor_sim/control.py @@ -41,7 +41,8 @@ class ControlSystem: if self._filtered_power_mw <= 0.0: self._filtered_power_mw = raw_power if raw_power > 0.7 * self.setpoint_mw: - alpha = clamp(dt / 6.0, 0.0, 1.0) # ~6s time constant + tau = constants.POWER_MEASUREMENT_TIME_CONSTANT + alpha = clamp(dt / max(1e-6, tau), 0.0, 1.0) self._filtered_power_mw += alpha * (raw_power - self._filtered_power_mw) measured_power = self._filtered_power_mw else: diff --git a/src/reactor_sim/dashboard.py b/src/reactor_sim/dashboard.py index d8f6f63..4246f01 100644 --- a/src/reactor_sim/dashboard.py +++ b/src/reactor_sim/dashboard.py @@ -784,6 +784,15 @@ class ReactorDashboard: reliefs.append("Secondary") relief_attr = curses.color_pair(2) | curses.A_BOLD if reliefs else 0 lines.append(("Relief valves", ", ".join(reliefs) if reliefs else "Closed", relief_attr)) + lines.append(("DNB margin", f"{state.core.dnb_margin:5.2f}" if state.core.dnb_margin is not None else "n/a")) + lines.append(("Subcooling", f"{state.core.subcooling_margin:5.1f} K" if state.core.subcooling_margin is not None else "n/a")) + lines.append( + ( + "SG level", + f"{state.secondary_loop.level*100:5.1f}%", + ) + ) + lines.append(("SG pressure", f"{state.secondary_loop.pressure:5.2f}/{constants.MAX_PRESSURE:4.1f} MPa")) lines.append( ( "SCRAM trips", diff --git a/src/reactor_sim/thermal.py b/src/reactor_sim/thermal.py index 80f263e..0f07f79 100644 --- a/src/reactor_sim/thermal.py +++ b/src/reactor_sim/thermal.py @@ -114,8 +114,16 @@ class ThermalSolver: dt: float, residual_power_mw: float | None = None, ) -> None: + def _lag(prev: float, new: float, tau: float) -> float: + if tau <= 0.0: + return new + alpha = min(1.0, max(0.0, dt / max(1e-6, tau))) + return prev + alpha * (new - prev) + if residual_power_mw is None: residual_power_mw = power_mw + prev_fuel = core.fuel_temperature + prev_clad = core.clad_temperature or primary.temperature_out temp_rise = temperature_rise(power_mw, primary.mass_flow_rate) primary.temperature_out = primary.temperature_in + temp_rise # Fuel heats from total fission power (even when most is convected) plus any residual left in the coolant. @@ -136,6 +144,9 @@ class ThermalSolver: # Keep temperatures bounded and never below coolant outlet. core.fuel_temperature = min(core.fuel_temperature, constants.MAX_CORE_TEMPERATURE) core.clad_temperature = min(clad, constants.MAX_CORE_TEMPERATURE) + # Apply mild lags so heat moves from fuel to clad to coolant over a short time constant. + core.fuel_temperature = _lag(prev_fuel, core.fuel_temperature, constants.FUEL_TO_CLAD_TIME_CONSTANT) + core.clad_temperature = _lag(prev_clad, core.clad_temperature or prev_clad, constants.CLAD_TO_COOLANT_TIME_CONSTANT) 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())