diff --git a/FEATURES.md b/FEATURES.md index 95a94de..c74c632 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -5,6 +5,6 @@ - **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 when flow is low. - **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, load dispatch to consumer, steam quality gating for output, generator states with batteries/spool. +- **Steam cycle**: three turbines with spool dynamics, throttle mapping, condenser back-pressure penalty, load dispatch to consumer, steam quality gating for output, generator states with batteries/spool. - **Protections & failures**: health monitor degrading components under stress, automatic SCRAM on core or heat-sink loss, relief valves per loop, 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/constants.py b/src/reactor_sim/constants.py index 7ce78b4..354b693 100644 --- a/src/reactor_sim/constants.py +++ b/src/reactor_sim/constants.py @@ -24,6 +24,14 @@ PUMP_SPOOL_TIME = 5.0 # seconds to reach commanded flow PRIMARY_PUMP_SHUTOFF_HEAD_MPA = 8.0 # approximate shutoff head for primary pumps SECONDARY_PUMP_SHUTOFF_HEAD_MPA = 7.0 TURBINE_SPOOL_TIME = 12.0 # seconds to reach steady output + +# Turbine/condenser parameters +TURBINE_THROTTLE_MIN = 0.1 +TURBINE_THROTTLE_MAX = 1.0 +TURBINE_THROTTLE_EFFICIENCY_DROP = 0.15 # efficiency loss when at minimum throttle +CONDENSER_BASE_PRESSURE_MPA = 0.01 +CONDENSER_MAX_PRESSURE_MPA = 0.3 +CONDENSER_BACKPRESSURE_PENALTY = 0.35 # fractional power loss at max back-pressure GENERATOR_SPOOL_TIME = 10.0 # seconds to reach full output # Auxiliary power assumptions PUMP_POWER_MW = 12.0 # MW draw per pump unit diff --git a/src/reactor_sim/dashboard.py b/src/reactor_sim/dashboard.py index 2451d38..f00ee5b 100644 --- a/src/reactor_sim/dashboard.py +++ b/src/reactor_sim/dashboard.py @@ -435,6 +435,7 @@ class ReactorDashboard: "Unit3 Elec", f"{state.turbines[2].electrical_output_mw:7.1f} MW" if len(state.turbines) > 2 else "n/a", ), + ("Throttle", f"{self.reactor.turbines[0].throttle:5.2f}" if self.reactor.turbines else "n/a"), ("Electrical", f"{state.total_electrical_output():7.1f} MW"), ("Load", f"{self._total_load_supplied(state):7.1f}/{self._total_load_demand(state):7.1f} MW"), ("Consumer", f"{consumer_status}"), diff --git a/src/reactor_sim/reactor.py b/src/reactor_sim/reactor.py index 5b0bd8b..06ec045 100644 --- a/src/reactor_sim/reactor.py +++ b/src/reactor_sim/reactor.py @@ -461,6 +461,10 @@ class Reactor: break turbine_state = state.turbines[idx] 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 turbine.step(state.secondary_loop, turbine_state, steam_power_mw=power_per_unit, dt=dt) if power_per_unit <= 0.0 and turbine_state.electrical_output_mw < 0.1: turbine_state.status = "OFF" diff --git a/src/reactor_sim/turbine.py b/src/reactor_sim/turbine.py index 7837ec4..1844cd7 100644 --- a/src/reactor_sim/turbine.py +++ b/src/reactor_sim/turbine.py @@ -27,6 +27,7 @@ class Turbine: mechanical_efficiency: float = constants.STEAM_TURBINE_EFFICIENCY rated_output_mw: float = 400.0 # cap per unit electrical output spool_time: float = constants.TURBINE_SPOOL_TIME + throttle: float = 1.0 # 0-1 valve position def step( self, @@ -46,11 +47,15 @@ class Turbine: state.condenser_temperature = max(305.0, loop.temperature_in - 20.0) return + throttle = min(constants.TURBINE_THROTTLE_MAX, max(constants.TURBINE_THROTTLE_MIN, self.throttle)) + throttle_eff = 1.0 - constants.TURBINE_THROTTLE_EFFICIENCY_DROP * (constants.TURBINE_THROTTLE_MAX - throttle) + enthalpy = 2_700.0 + loop.steam_quality * 600.0 - mass_flow = effective_mass_flow * 0.6 + mass_flow = effective_mass_flow * 0.6 * throttle computed_power = (enthalpy * mass_flow / 1_000.0) / 1_000.0 available_power = steam_power_mw if steam_power_mw > 0 else computed_power - shaft_power_mw = available_power * self.mechanical_efficiency + backpressure_loss = 1.0 - _backpressure_penalty(loop) + shaft_power_mw = available_power * self.mechanical_efficiency * throttle_eff * backpressure_loss electrical = shaft_power_mw * self.generator_efficiency if electrical > self.rated_output_mw: electrical = self.rated_output_mw @@ -71,5 +76,15 @@ class Turbine: def _ramp(current: float, target: float, dt: float, time_constant: float) -> float: if time_constant <= 0.0: return target - alpha = min(1.0, max(0.0, dt / time_constant)) + alpha = min(1.0, max(0.0, dt / max(1e-6, time_constant))) return current + (target - current) * alpha + + +def _backpressure_penalty(loop: CoolantLoopState) -> float: + base = constants.CONDENSER_BASE_PRESSURE_MPA + max_p = constants.CONDENSER_MAX_PRESSURE_MPA + pressure = max(base, min(max_p, loop.pressure)) + if pressure <= base: + return 0.0 + frac = (pressure - base) / max(1e-6, max_p - base) + return min(constants.CONDENSER_BACKPRESSURE_PENALTY, frac * constants.CONDENSER_BACKPRESSURE_PENALTY) diff --git a/tests/test_turbine.py b/tests/test_turbine.py index 6680345..0a9da47 100644 --- a/tests/test_turbine.py +++ b/tests/test_turbine.py @@ -6,10 +6,11 @@ from reactor_sim.turbine import Turbine def test_turbine_spools_toward_target_output(): turbine = Turbine() + turbine.throttle = 1.0 loop = CoolantLoopState( temperature_in=600.0, temperature_out=650.0, - pressure=6.0, + pressure=0.02, mass_flow_rate=20_000.0, steam_quality=0.9, ) @@ -20,14 +21,15 @@ def test_turbine_spools_toward_target_output(): condenser_temperature=300.0, ) target_electric = min( - turbine.rated_output_mw, 300.0 * turbine.mechanical_efficiency * turbine.generator_efficiency + turbine.rated_output_mw, + 300.0 * turbine.mechanical_efficiency * turbine.generator_efficiency, ) - dt = 5.0 + dt = 1.0 turbine.step(loop, state, steam_power_mw=300.0, dt=dt) assert 0.0 < state.electrical_output_mw < target_electric - for _ in range(5): + for _ in range(60): turbine.step(loop, state, steam_power_mw=300.0, dt=dt) assert state.electrical_output_mw == pytest.approx(target_electric, rel=0.05)