diff --git a/CONTEXT_NOTES.md b/CONTEXT_NOTES.md index e0341a7..3fdaad2 100644 --- a/CONTEXT_NOTES.md +++ b/CONTEXT_NOTES.md @@ -1,6 +1,7 @@ # Session Context Notes - 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. - 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. @@ -16,6 +17,6 @@ - Relief valves: l (primary) and ; (secondary) vent with mass/enthalpy loss, ramp pressure toward ~1 MPa over several seconds, cap pump targets while open; status displayed per loop. - Generator behavior: starting/running only produce power when load is present; auto off by default; manual toggles b/v; auto stops with no load; base aux drops to 0 when idle/cold. - Pressure tying: loop pressure floors to saturation(temp) when pumps off; pump targets aim for ~7 MPa nominal RBMK-like setpoints. -- Turbines: require meaningful steam flow/quality; otherwise zero output. Steam supply pressure in turbine panel reads 0 when no steam. +- Turbines/governor: require meaningful steam flow/quality; otherwise zero output. Steam supply pressure in turbine panel reads 0 when no steam. Throttle now biases toward load demand with a governor term, and overspeed/overload >105% rated electrical trips a unit and spools it down. - Rod control now supports three banks with weighted worth; xenon/iodine tracked with decay and burn-out; new UA·ΔT_lm steam-generator model and pump head/flow curves. - Dashboard shows heat-exchanger ΔT/efficiency and protections; pumps and HX changes documented in FEATURES.md / TODO.md. diff --git a/FEATURES.md b/FEATURES.md index 768098e..3dc92a6 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -3,8 +3,8 @@ - **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. - **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. +- **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. - **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. +- **Steam cycle**: three turbines with spool dynamics, throttle mapping and a simple governor with overspeed/overload trip, 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 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/TODO.md b/TODO.md index 14222f8..02f8b3b 100644 --- a/TODO.md +++ b/TODO.md @@ -11,7 +11,7 @@ - [x] Core thermal realism: add a simple radial fuel model (pellet/rim/clad temps) with burnup-driven conductivity drop and gap conductance; upgrade CHF/DNB correlation (e.g., Groeneveld/Chen) parameterized by pressure, mass flux, and quality, then calibrate margins. - [ ] 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. - [ ] 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. -- [ ] 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. +- [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. - [ ] 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/reactor.py b/src/reactor_sim/reactor.py index 6bef6d7..8c39b19 100644 --- a/src/reactor_sim/reactor.py +++ b/src/reactor_sim/reactor.py @@ -61,6 +61,9 @@ class Reactor: self.turbine_active = any(self.turbine_unit_active) if not self.generators: self.generators = [DieselGenerator() for _ in range(2)] + # Balance-of-plant controls + self.feedwater_valve = 0.5 + self._last_steam_out_kg_s = 0.0 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: @@ -218,9 +221,6 @@ class Reactor: self._update_loop_inventory( state.primary_loop, constants.PRIMARY_LOOP_VOLUME_M3, constants.PRIMARY_INVENTORY_TARGET, dt ) - self._update_loop_inventory( - state.secondary_loop, constants.SECONDARY_LOOP_VOLUME_M3, constants.SECONDARY_INVENTORY_TARGET, dt - ) pump_demand = overrides.get( "coolant_demand", @@ -424,9 +424,7 @@ class Reactor: if not self.control.manual_control and not self.shutdown: self.control.safety_backoff(state.core.subcooling_margin, state.core.dnb_margin, dt) self._apply_secondary_boiloff(state, dt) - self._update_loop_inventory( - state.secondary_loop, constants.SECONDARY_LOOP_VOLUME_M3, constants.SECONDARY_INVENTORY_TARGET, dt - ) + self._update_secondary_level(state, dt) steam_draw = self._step_turbine_bank(state, transferred, dt) if steam_draw > 0.0: @@ -533,9 +531,17 @@ class Reactor: if idx in active_indices: # Simple throttle map: reduce throttle when electrical demand is low, open as demand rises. demand = turbine_state.load_demand_mw - throttle = 0.4 if demand <= 0 else min(1.0, 0.4 + demand / max(1e-6, turbine.rated_output_mw)) - turbine.throttle = throttle + desired = 0.4 if demand <= 0 else min(1.0, 0.4 + demand / max(1e-6, turbine.rated_output_mw)) + # Governor: nudge throttle toward desired based on electrical error. + error = (demand - turbine_state.electrical_output_mw) / max(1.0, turbine.rated_output_mw) + turbine.throttle = max(0.3, min(1.0, turbine.throttle + (desired - turbine.throttle) * 0.5 + 0.2 * error * dt)) turbine.step(state.secondary_loop, turbine_state, steam_power_mw=power_per_unit, dt=dt) + if turbine_state.electrical_output_mw > 1.05 * turbine.rated_output_mw: + LOGGER.critical("Turbine %d overspeed/overload, tripping unit", idx + 1) + self._spin_down_turbine(turbine_state, dt, turbine.spool_time) + turbine_state.status = "TRIPPED" + self.turbine_unit_active[idx] = False + continue if power_per_unit <= 0.0 and turbine_state.electrical_output_mw < 0.1: turbine_state.status = "OFF" elif turbine_state.electrical_output_mw < max(0.1 * turbine.rated_output_mw, 1.0): @@ -613,6 +619,39 @@ class Reactor: loop.inventory_kg = max(0.0, loop.inventory_kg + correction * nominal_mass * dt) loop.level = min(1.2, max(0.0, loop.inventory_kg / nominal_mass)) + def _update_secondary_level(self, state: PlantState, dt: float) -> None: + """Steam drum level controller with shrink/swell and feedwater valve.""" + loop = state.secondary_loop + nominal_mass = self._nominal_inventory(constants.SECONDARY_LOOP_VOLUME_M3) + if nominal_mass <= 0.0: + loop.level = 0.0 + return + if loop.inventory_kg <= 0.0: + loop.inventory_kg = nominal_mass * constants.SECONDARY_INVENTORY_TARGET + current_level = loop.inventory_kg / nominal_mass + steam_out = loop.mass_flow_rate * max(0.0, loop.steam_quality) + # Shrink/swell: apparent level drops when steam draw surges. + swell = -0.02 * (steam_out - self._last_steam_out_kg_s) / max(1.0, nominal_mass) + sensed_level = current_level + swell + # PI-ish valve adjustment toward target level. + error = constants.SECONDARY_INVENTORY_TARGET - sensed_level + valve_delta = 0.3 * error * dt + self.feedwater_valve = max(0.0, min(1.0, self.feedwater_valve + valve_delta)) + # Feedwater adds mass and energy at inlet temperature. + steam_factor = min(1.0, max(0.1, steam_out / max(1.0, nominal_mass * 0.1))) + feed_rate = ( + self.feedwater_valve + * nominal_mass + * 0.002 + * steam_factor + ) # up to ~0.2% of nominal mass per second, scaled by steam draw + added_mass = feed_rate * dt + loop.inventory_kg = max(0.0, loop.inventory_kg + added_mass) + cp = constants.COOLANT_HEAT_CAPACITY + loop.energy_j += added_mass * cp * loop.temperature_in + loop.level = min(1.2, max(0.0, loop.inventory_kg / nominal_mass)) + self._last_steam_out_kg_s = steam_out + def _inventory_flow_scale(self, loop: CoolantLoopState) -> float: if loop.level <= constants.LOW_LEVEL_FLOW_FLOOR: return 0.0