From 5469f142a74d903233452612b9ffc86f69e94f33 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Fri, 28 Nov 2025 20:28:46 +0100 Subject: [PATCH] Add boron reactivity trim and power measurement smoothing --- CONTEXT_NOTES.md | 1 + FEATURES.md | 2 +- TODO.md | 1 + src/reactor_sim/constants.py | 2 ++ src/reactor_sim/control.py | 18 +++++++++++++++++- src/reactor_sim/neutronics.py | 3 ++- src/reactor_sim/reactor.py | 20 ++++++++++++++++++++ 7 files changed, 44 insertions(+), 3 deletions(-) diff --git a/CONTEXT_NOTES.md b/CONTEXT_NOTES.md index 195631a..61e7c7f 100644 --- a/CONTEXT_NOTES.md +++ b/CONTEXT_NOTES.md @@ -3,6 +3,7 @@ - Reactor model: two-loop but tuned to RBMK-like pressures (nominal ~7 MPa for both loops). Loop pressure clamps to saturation baseline when pumps are off; pumps ramp flow/pressure over spool time when stopping or starting. - Feedwater & level: secondary steam drum uses shrink/swell-aware level sensing to drive a feedwater valve; makeup flow scales with steam draw toward the target level instead of instant inventory clamps. - Chemistry/fouling: plant tracks dissolved O2/boron/sodium; impurities plus high temp/steam draw increase HX fouling (reduces UA) and add condenser fouling/back-pressure. Oxygen degasses with steam; impurity ingress accelerates when venting. +- Reactivity bias: boron ppm now biases shutdown reactivity; a very slow trim nudges boron toward the power setpoint after ~300s of operation. - Turbines: produce zero output unless steam quality is present and effective steam flow is >10 kg/s. Turbine panel shows steam availability (enthalpy × quality × mass flow) and steam enthalpy instead of loop pressure; condenser pressure/temperature/fouling shown with nominal bounds. - Generators: two diesel units, rated 50 MW, spool time 10s. Auto mode default `False`; manual toggles b/v. Auto stops when no load. Relief valves toggles l (primary) / ; (secondary) and displayed per loop. - Pumps: per-unit controls g/h (primary), j/k (secondary). Flow/pressure ramp down over spool when pumps stop. Pump status thresholds use >0.1 kg/s to show STOPPING. diff --git a/FEATURES.md b/FEATURES.md index fcf54d1..62b1fa0 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -1,7 +1,7 @@ ## C.O.R.E. feature set - **Core physics**: point-kinetics with per-bank delayed neutron precursors, temperature feedback, fuel burnup penalty, xenon/iodine buildup with decay and burn-out, and rod-bank worth curves. -- **Rod control**: three rod banks with weighted worth; auto controller chases 3 GW setpoint with safety backoff; manual mode with staged bank motion and SCRAM; state persists across runs. +- **Rod control**: three rod banks with weighted worth; auto controller chases 3 GW setpoint with safety backoff and filtered power feedback; manual mode with staged bank motion and SCRAM; state persists across runs. Soluble boron bias contributes slow negative reactivity and trims toward the setpoint. - **Coolant & hydraulics**: primary/secondary pumps with head/flow curves, power draw scaling, wear tracking; pressure floors tied to saturation; auxiliary power model with generator auto-start. - **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 and a shrink/swell-aware feedwater valve controller; dissolved oxygen/sodium drive HX fouling that reduces effective UA. - **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. diff --git a/TODO.md b/TODO.md index 5c4a72b..52ad6ff 100644 --- a/TODO.md +++ b/TODO.md @@ -12,6 +12,7 @@ - [ ] Transient protection ladder: add dP/dt and dT/dt trips for SG overfill/depressurization and rod-run-in alarms; implement graded warn/arm/trip stages surfaced on the dashboard. - [x] Chemistry & fouling: track dissolved oxygen/boron and corrosion/fouling that degrade HX efficiency and condenser vacuum; let feedwater temperature/chemistry affect steam purity/back-pressure. - [x] Balance-of-plant dynamics: steam-drum level controller with shrink/swell, feedwater valve model, turbine throttle governor/overspeed trip, and improved load-follow tied to grid demand ramps. +- [ ] Neutronics/feedback smoothing: add detector/measurement lag, fuel→clad→coolant transport delays, shared boron trim for fine regulation, and retune rod gains/rate limits to reduce power hunting while keeping safety margins intact. - [ ] Component wear & maintenance: make wear depend on duty cycle and off-nominal conditions (cavitation, high ΔP, high temp); add preventive maintenance scheduling and show next-due in the dashboard. - [ ] Scenarios & tooling: presets for cold start, load-follow, and fault injection (pump fail, relief stuck) with seedable randomness; snapshot diff tooling to compare saved states. - [ ] Incremental realism plan: diff --git a/src/reactor_sim/constants.py b/src/reactor_sim/constants.py index 8c06237..db97f4c 100644 --- a/src/reactor_sim/constants.py +++ b/src/reactor_sim/constants.py @@ -77,6 +77,8 @@ CHEM_SODIUM_DEFAULT_PPM = 5.0 HX_FOULING_RATE = 1e-5 # fouling increment per second scaled by impurities/temp 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 # 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 5e6aba1..efd1ed1 100644 --- a/src/reactor_sim/control.py +++ b/src/reactor_sim/control.py @@ -24,6 +24,7 @@ class ControlSystem: manual_control: bool = False rod_banks: list[float] = field(default_factory=lambda: [0.5, 0.5, 0.5]) rod_target: float = 0.5 + _filtered_power_mw: float = 0.0 def update_rods(self, state: CoreState, dt: float) -> float: if not self.rod_banks or len(self.rod_banks) != len(constants.CONTROL_ROD_BANK_WEIGHTS): @@ -35,7 +36,22 @@ class ControlSystem: self.rod_target = clamp(self.rod_fraction, 0.0, 0.95) self._advance_banks(self.rod_target, dt) return self.rod_fraction - error = (state.power_output_mw - self.setpoint_mw) / self.setpoint_mw + raw_power = state.power_output_mw + # Begin filtering once we're in the vicinity of the setpoint to avoid chasing noise. + 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 + self._filtered_power_mw += alpha * (raw_power - self._filtered_power_mw) + measured_power = self._filtered_power_mw + else: + measured_power = raw_power + + error = (measured_power - self.setpoint_mw) / self.setpoint_mw + # Deadband near setpoint to prevent dithering; only apply when we're close to target. + if measured_power > 0.9 * self.setpoint_mw and abs(error) < 0.01: + self._advance_banks(self.rod_target, dt) + return self.rod_fraction # When power is low (negative error) withdraw rods; when high, insert them. adjustment = error * 0.35 adjustment = clamp(adjustment, -constants.CONTROL_ROD_SPEED * dt, constants.CONTROL_ROD_SPEED * dt) diff --git a/src/reactor_sim/neutronics.py b/src/reactor_sim/neutronics.py index e69f443..9cc875c 100644 --- a/src/reactor_sim/neutronics.py +++ b/src/reactor_sim/neutronics.py @@ -25,10 +25,11 @@ def xenon_poisoning(flux: float) -> float: @dataclass class NeutronDynamics: + base_shutdown_bias: float = -0.014 + shutdown_bias: float = -0.014 beta_effective: float = 0.0065 delayed_decay_const: float = 0.08 # 1/s effective precursor decay external_source_coupling: float = 1e-6 - shutdown_bias: float = -0.014 iodine_yield: float = 1e-6 # inventory units per MW*s iodine_decay_const: float = 1.0 / 66000.0 # ~18h xenon_decay_const: float = 1.0 / 33000.0 # ~9h diff --git a/src/reactor_sim/reactor.py b/src/reactor_sim/reactor.py index c8371bc..fcaf12b 100644 --- a/src/reactor_sim/reactor.py +++ b/src/reactor_sim/reactor.py @@ -64,6 +64,8 @@ class Reactor: # Balance-of-plant controls self.feedwater_valve = 0.5 self._last_steam_out_kg_s = 0.0 + # Slow chemistry/boron trim control + self._boron_trim_active = True if not self.primary_pump_units or len(self.primary_pump_units) != 2: self.primary_pump_units = [True, True] if not self.secondary_pump_units or len(self.secondary_pump_units) != 2: @@ -197,6 +199,9 @@ class Reactor: state.core ) self.neutronics.update_poisons(state.core, dt) + # Apply soluble boron reactivity bias (slow trim). + boron_delta = state.boron_ppm - constants.CHEM_BORON_DEFAULT_PPM + self.neutronics.shutdown_bias = self.neutronics.base_shutdown_bias - boron_delta * constants.BORON_WORTH_PER_PPM self.neutronics.step(state.core, rod_fraction, dt, external_source_rate=decay_neutron_source, rod_banks=self.control.rod_banks) prompt_power, fission_rate, fission_event = self.fuel.prompt_energy_rate( @@ -431,6 +436,7 @@ class Reactor: self._apply_secondary_boiloff(state, dt) self._update_secondary_level(state, dt) self._update_chemistry(state, dt) + self._apply_boron_trim(state, dt) steam_draw = self._step_turbine_bank(state, transferred, dt) if steam_draw > 0.0: @@ -658,6 +664,20 @@ class Reactor: loop.level = min(1.2, max(0.0, loop.inventory_kg / nominal_mass)) self._last_steam_out_kg_s = steam_out + def _apply_boron_trim(self, state: PlantState, dt: float) -> None: + """Slow soluble boron trim to hold power near setpoint; acts only near target.""" + if not self._boron_trim_active or self.control.manual_control or self.shutdown: + return + if state.time_elapsed < 300.0: + return + if self.control.setpoint_mw <= 0.0: + return + error = (state.core.power_output_mw - self.control.setpoint_mw) / self.control.setpoint_mw + if abs(error) < 0.02: + return + delta = constants.BORON_TRIM_RATE_PPM_PER_S * error * dt + state.boron_ppm = min(constants.CHEM_MAX_PPM, max(0.0, state.boron_ppm + delta)) + def _update_chemistry(self, state: PlantState, dt: float) -> None: """Track dissolved species and fouling impacts on HX and condenser.""" env = constants.ENVIRONMENT_TEMPERATURE