From def8ded81634c95159597b297e644c3cd11b54a7 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Fri, 28 Nov 2025 18:44:16 +0100 Subject: [PATCH] Add SCRAM matrix (DNB/subcool/SG limits) and clad split --- FEATURES.md | 2 +- src/reactor_sim/dashboard.py | 6 ++++++ src/reactor_sim/reactor.py | 17 +++++++++++++++-- src/reactor_sim/thermal.py | 10 ++++++++-- 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/FEATURES.md b/FEATURES.md index 404d97a..3050196 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -6,5 +6,5 @@ - **Heat transfer**: steam-generator UA·ΔT_lm model with a pinch cap to keep the primary outlet hotter than the secondary, coolant heating uses total fission power with fuel heating decoupled from exchanger draw, and the secondary thermal solver includes passive cool-down plus steam-drum mass/energy balance with latent heat. - **Pressurizer & inventory**: primary pressurizer trims toward 7 MPa with level tracking, loop inventories/levels steer flow availability, secondary steam boil-off draws down level with auto makeup, and pumps reduce flow/status to `CAV` when NPSH is insufficient. - **Steam cycle**: three turbines with spool dynamics, throttle mapping, condenser vacuum/back-pressure with fouling and cooling sink temperature, load dispatch to consumer, steam quality gating for output, generator states with batteries/spool, and steam enthalpy-driven availability readout on the dashboard. -- **Protections & failures**: health monitor degrading components under stress, automatic SCRAM on core or heat-sink loss, relief valves per loop with venting/mass loss and pump pressure caps, maintenance actions to restore integrity. +- **Protections & failures**: health monitor degrading components under stress, automatic SCRAM on core or heat-sink loss plus DNB/subcool and secondary level/pressure trips, relief valves per loop with venting/mass loss and pump pressure caps, maintenance actions to restore integrity. - **Persistence & ops**: snapshots auto-save/load to `artifacts/last_state.json`; dashboard with live metrics, protections/warnings, heat-exchanger telemetry, component health, and control shortcuts. diff --git a/src/reactor_sim/dashboard.py b/src/reactor_sim/dashboard.py index 5dff294..65e41af 100644 --- a/src/reactor_sim/dashboard.py +++ b/src/reactor_sim/dashboard.py @@ -761,6 +761,12 @@ class ReactorDashboard: if self.reactor.secondary_relief_open: reliefs.append("Secondary") lines.append(("Relief valves", ", ".join(reliefs) if reliefs else "Closed")) + lines.append( + ( + "SCRAM trips", + "DNB<0.5 | Subcool<2K | SG lvl<5/>98% | SG P>15.2MPa", + ) + ) return lines def _steam_available_power(self, state: PlantState) -> float: diff --git a/src/reactor_sim/reactor.py b/src/reactor_sim/reactor.py index d61c36e..6bef6d7 100644 --- a/src/reactor_sim/reactor.py +++ b/src/reactor_sim/reactor.py @@ -435,12 +435,25 @@ class Reactor: 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) - if state.core.dnb_margin is not None and state.core.dnb_margin < 0.3: + # SCRAM matrix: DNB, subcooling, steam generator level/pressure + if state.core.dnb_margin is not None and state.core.dnb_margin < 0.5: 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: + if state.core.subcooling_margin is not None and state.core.subcooling_margin < 2.0: + LOGGER.critical("Subcooling margin lost: %.1fK, initiating SCRAM", state.core.subcooling_margin) + self.shutdown = True + self.control.scram() + elif state.core.subcooling_margin is not None and state.core.subcooling_margin < 5.0: LOGGER.warning("Subcooling margin low: %.1fK", state.core.subcooling_margin) + if state.secondary_loop.level < 0.05 or state.secondary_loop.level > 0.98: + LOGGER.critical("Secondary level out of bounds (%.1f%%), initiating SCRAM", state.secondary_loop.level * 100) + self.shutdown = True + self.control.scram() + if state.secondary_loop.pressure > 0.95 * constants.MAX_PRESSURE: + LOGGER.critical("Secondary pressure high (%.2f MPa), initiating SCRAM", state.secondary_loop.pressure) + self.shutdown = True + self.control.scram() failures = self.health_monitor.evaluate( state, diff --git a/src/reactor_sim/thermal.py b/src/reactor_sim/thermal.py index 53de428..3fd5a91 100644 --- a/src/reactor_sim/thermal.py +++ b/src/reactor_sim/thermal.py @@ -119,9 +119,15 @@ class ThermalSolver: heating = (0.002 * max(0.0, power_mw) + 0.01 * max(0.0, residual_power_mw)) * dt cooling = 0.025 * max(0.0, core.fuel_temperature - primary.temperature_out) * dt core.fuel_temperature += heating - cooling + # Simple clad/fuel split: clad lags fuel and is cooled by coolant. + clad = core.clad_temperature or primary.temperature_out + conduction = 0.02 * max(0.0, core.fuel_temperature - clad) * dt + clad_cooling = 0.05 * max(0.0, clad - primary.temperature_out) * dt + clad = max(primary.temperature_out, clad + conduction - clad_cooling) + core.fuel_temperature = max(primary.temperature_out, core.fuel_temperature - conduction) # 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.clad_temperature = max(primary.temperature_out, core.clad_temperature or primary.temperature_out) + core.fuel_temperature = min(core.fuel_temperature, constants.MAX_CORE_TEMPERATURE) + core.clad_temperature = min(clad, constants.MAX_CORE_TEMPERATURE) 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())