Add enthalpy tracking and dashboard metrics
This commit is contained in:
1
TODO.md
1
TODO.md
@@ -12,3 +12,4 @@
|
||||
- Adjust HX/pressure handling to use stored energy (saturation clamp and pressure rise) and validate steam formation with both pumps at ~3 GW. Use realistic tube-side material assumptions (Inconel 690/SS cladding) and clamp steam quality to phase-equilibrium enthalpy.
|
||||
- Update turbine power mapping to consume steam enthalpy/quality and align protection trips with real steam presence; drive inlet steam around 6–7 MPa, quality/enthalpy-based flow to ~550–600 MW(e) per machine class if steam is available.
|
||||
- Add integration test: cold start → gens/pumps 2/2 → ramp to ~3 GW → confirm steam quality threshold at the secondary drum → enable all turbines and require electrical output. Include a step that tolerates one secondary pump off for a period to prove redundancy still yields steam.
|
||||
- [ ] Dashboard follow-ups: clarify or replace turbine “Steam P” field (currently shows loop pressure, not turbine-driving steam); consider removing it if no better signal is available.
|
||||
|
||||
@@ -7,11 +7,11 @@ MEGAWATT = 1_000_000.0
|
||||
NEUTRON_LIFETIME = 0.1 # seconds, prompt neutron lifetime surrogate
|
||||
FUEL_ENERGY_DENSITY = 200.0 * MEGAWATT # J/kg released as heat
|
||||
COOLANT_HEAT_CAPACITY = 4_200.0 # J/(kg*K) for water/steam
|
||||
COOLANT_DENSITY = 700.0 # kg/m^3 averaged between phases
|
||||
COOLANT_DENSITY = 720.0 # kg/m^3 averaged between phases
|
||||
STEAM_LATENT_HEAT = 2_200_000.0 # J/kg approximate latent heat of vaporization
|
||||
CORE_MELTDOWN_TEMPERATURE = 2_873.0 # K (approx 2600C) threshold for irreversible meltdown
|
||||
MAX_CORE_TEMPERATURE = CORE_MELTDOWN_TEMPERATURE # Allow simulation to approach meltdown temperature
|
||||
MAX_PRESSURE = 15.0 # MPa typical PWR primary loop limit
|
||||
MAX_PRESSURE = 16.0 # MPa PWR primary loop limit
|
||||
CLAD_MAX_TEMPERATURE = 1_200.0 # K clad softening / DNB concern
|
||||
CHF_MASS_FLUX_REF = 1_500.0 # kg/m2-s reference mass flux surrogate
|
||||
CHF_PRESSURE_REF_MPA = 7.0 # MPa reference pressure for CHF surrogate
|
||||
@@ -26,8 +26,8 @@ AMU_TO_KG = 1.660_539_066_60e-27
|
||||
MEV_TO_J = 1.602_176_634e-13
|
||||
ELECTRON_FISSION_CROSS_SECTION = 5e-16 # cm^2, tuned for simulation scale
|
||||
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 = 3.0
|
||||
PRIMARY_PUMP_SHUTOFF_HEAD_MPA = 17.0 # approximate shutoff head for primary pumps
|
||||
SECONDARY_PUMP_SHUTOFF_HEAD_MPA = 8.0
|
||||
TURBINE_SPOOL_TIME = 12.0 # seconds to reach steady output
|
||||
|
||||
# Turbine/condenser parameters
|
||||
@@ -45,14 +45,14 @@ NORMAL_CORE_POWER_MW = 3_000.0
|
||||
TEST_MAX_POWER_MW = 4_000.0
|
||||
PRIMARY_OUTLET_TARGET_K = 580.0
|
||||
SECONDARY_OUTLET_TARGET_K = 520.0
|
||||
PRIMARY_NOMINAL_PRESSURE = 7.0 # MPa typical RBMK channel header pressure
|
||||
SECONDARY_NOMINAL_PRESSURE = 7.0 # MPa steam drum/steam line pressure surrogate
|
||||
STEAM_GENERATOR_UA_MW_PER_K = 25.0 # overall UA for steam generator (MW/K)
|
||||
PRIMARY_NOMINAL_PRESSURE = 15.5 # MPa PWR primary pressure
|
||||
SECONDARY_NOMINAL_PRESSURE = 6.5 # MPa steam drum/steam line pressure surrogate
|
||||
STEAM_GENERATOR_UA_MW_PER_K = 150.0 # overall UA for steam generator (MW/K)
|
||||
# Loop volume / inventory assumptions
|
||||
PRIMARY_LOOP_VOLUME_M3 = 350.0
|
||||
SECONDARY_LOOP_VOLUME_M3 = 320.0
|
||||
PRIMARY_PRESSURIZER_SETPOINT_MPA = 7.0
|
||||
PRIMARY_PRESSURIZER_DEADBAND_MPA = 0.15
|
||||
PRIMARY_PRESSURIZER_SETPOINT_MPA = 15.5
|
||||
PRIMARY_PRESSURIZER_DEADBAND_MPA = 0.2
|
||||
PRIMARY_PRESSURIZER_HEAT_RATE_MPA_PER_S = 0.08
|
||||
PRIMARY_PRESSURIZER_SPRAY_RATE_MPA_PER_S = 0.12
|
||||
PRIMARY_PRESSURIZER_LEVEL_DRAW_PER_S = 0.002
|
||||
|
||||
@@ -84,12 +84,13 @@ class ControlSystem:
|
||||
power_floor = 0.0
|
||||
if core_power_mw is not None:
|
||||
power_fraction = clamp(core_power_mw / constants.NORMAL_CORE_POWER_MW, 0.0, 1.5)
|
||||
power_floor = 0.15 + 0.2 * power_fraction
|
||||
# Allow warmer operation when electrical load is already being served (turbines online),
|
||||
# but keep a higher floor when idling so test scenarios still converge near 3 GW.
|
||||
if electrical_output_mw is not None and electrical_output_mw > 10.0:
|
||||
power_floor *= 0.6
|
||||
power_floor = 0.2 + 0.25 * power_fraction
|
||||
demand = max(demand, power_floor)
|
||||
# At power, keep primary pumps near full speed to preserve pressure/subcooling.
|
||||
if core_power_mw is not None and core_power_mw > 500.0:
|
||||
demand = max(demand, 0.8)
|
||||
elif core_power_mw is not None and core_power_mw > 100.0:
|
||||
demand = max(demand, 0.6)
|
||||
demand = clamp(demand, 0.0, 1.0)
|
||||
LOGGER.debug(
|
||||
"Coolant demand %.2f (temp_error=%.2f, power_floor=%.2f) for outlet %.1fK power %.1f MW elec %.1f MW",
|
||||
|
||||
@@ -26,8 +26,9 @@ class Pump:
|
||||
"""Return (flow_kg_s, head_mpa) at the given demand using a simple pump curve."""
|
||||
demand = max(0.0, min(1.0, demand))
|
||||
flow = self.flow_rate(demand)
|
||||
flow_frac = min(1.2, flow / max(1e-3, self.nominal_flow))
|
||||
head = max(0.0, self.shutoff_head_mpa * max(0.0, 1.0 - flow_frac**2))
|
||||
flow_frac = flow / max(1e-3, self.nominal_flow)
|
||||
# Keep a healthy head near nominal flow; fall off gently beyond the rated point.
|
||||
head = self.shutoff_head_mpa * max(0.2, 1.0 - 0.4 * max(0.0, flow_frac))
|
||||
return flow, head
|
||||
|
||||
def step(self, loop: CoolantLoopState, demand: float) -> None:
|
||||
|
||||
@@ -91,7 +91,7 @@ class ReactorDashboard:
|
||||
DashboardKey("r", "Reset & clear state"),
|
||||
DashboardKey("a", "Toggle auto rod control"),
|
||||
DashboardKey("+/-", "Withdraw/insert rods"),
|
||||
DashboardKey("Numpad 1-9", "Set rods to 0.1 … 0.9 (manual)"),
|
||||
DashboardKey("1-9 / Numpad", "Set rods to 0.1 … 0.9 (manual)"),
|
||||
DashboardKey("[/]", "Adjust consumer demand −/+50 MW"),
|
||||
DashboardKey("s/d", "Setpoint −/+250 MW"),
|
||||
DashboardKey("p", "Maintain core (shutdown required)"),
|
||||
@@ -213,12 +213,19 @@ class ReactorDashboard:
|
||||
self._queue_command(ReactorCommand(generator_auto=not self.reactor.generator_auto))
|
||||
elif ch in (ord("t"), ord("T")):
|
||||
self._queue_command(ReactorCommand(turbine_on=not self.reactor.turbine_active))
|
||||
elif keyname and keyname.decode(errors="ignore") in ("!", "@", "#", '"'):
|
||||
name = keyname.decode(errors="ignore")
|
||||
turbine_hotkeys = {"!": 0, "@": 1, "#": 2, '"': 1}
|
||||
self._toggle_turbine_unit(turbine_hotkeys[name])
|
||||
elif ch in (ord("!"), ord("@"), ord("#"), ord('"')):
|
||||
turbine_hotkeys = {ord("!"): 0, ord("@"): 1, ord("#"): 2, ord('"'): 1}
|
||||
self._toggle_turbine_unit(turbine_hotkeys[ch])
|
||||
elif keyname and keyname.startswith(b"KP_") and keyname[-1:] in b"123456789":
|
||||
target = (keyname[-1] - ord("0")) / 10.0 # type: ignore[arg-type]
|
||||
self._queue_command(ReactorCommand(rod_position=target, rod_manual=True))
|
||||
elif ch in (ord("!"), ord("@"), ord("#")):
|
||||
idx = ch - ord("!")
|
||||
self._toggle_turbine_unit(idx)
|
||||
elif ord("1") <= ch <= ord("9"):
|
||||
target = (ch - ord("0")) / 10.0
|
||||
self._queue_command(ReactorCommand(rod_position=target, rod_manual=True))
|
||||
elif ch in _NUMPAD_ROD_KEYS:
|
||||
self._queue_command(ReactorCommand(rod_position=_NUMPAD_ROD_KEYS[ch], rod_manual=True))
|
||||
elif curses.KEY_F1 <= ch <= curses.KEY_F9:
|
||||
@@ -441,6 +448,7 @@ class ReactorDashboard:
|
||||
("Outlet Temp", f"{state.primary_loop.temperature_out:7.1f} K (Target {constants.PRIMARY_OUTLET_TARGET_K:4.0f})"),
|
||||
("Pressure", f"{state.primary_loop.pressure:5.2f}/{constants.MAX_PRESSURE:4.1f} MPa"),
|
||||
("Pressurizer", f"{self.reactor.pressurizer_level*100:6.1f}% @ {constants.PRIMARY_PRESSURIZER_SETPOINT_MPA:4.1f} MPa"),
|
||||
("Loop Energy", f"{state.primary_loop.energy_j/1e6:7.0f} MJ"),
|
||||
("Relief", "OPEN" if self.reactor.primary_relief_open else "CLOSED"),
|
||||
],
|
||||
)
|
||||
@@ -460,6 +468,11 @@ class ReactorDashboard:
|
||||
("Outlet Temp", f"{state.secondary_loop.temperature_out:7.1f} K (Target {constants.SECONDARY_OUTLET_TARGET_K:4.0f})"),
|
||||
("Pressure", f"{state.secondary_loop.pressure:5.2f}/{constants.MAX_PRESSURE:4.1f} MPa"),
|
||||
("Steam Quality", f"{state.secondary_loop.steam_quality:5.2f}/1.00"),
|
||||
("Drum Energy", f"{state.secondary_loop.energy_j/1e6:7.0f} MJ"),
|
||||
(
|
||||
"Spec Enthalpy",
|
||||
f"{(state.secondary_loop.energy_j / max(1e-6, state.secondary_loop.inventory_kg))/1e3:7.0f} kJ/kg",
|
||||
),
|
||||
("Relief", "OPEN" if self.reactor.secondary_relief_open else "CLOSED"),
|
||||
],
|
||||
)
|
||||
@@ -492,10 +505,8 @@ class ReactorDashboard:
|
||||
("Load", f"{self._total_load_supplied(state):7.1f}/{self._total_load_demand(state):7.1f} MW"),
|
||||
("Consumer", f"{consumer_status}"),
|
||||
("Demand", f"{consumer_demand:7.1f} MW"),
|
||||
(
|
||||
"Steam",
|
||||
f"P={state.secondary_loop.pressure:4.2f} MPa q={state.secondary_loop.steam_quality:4.2f} mdot={state.secondary_loop.mass_flow_rate:6.0f} kg/s",
|
||||
),
|
||||
("Steam Enthalpy", f"{state.turbines[0].steam_enthalpy:7.0f} kJ/kg" if state.turbines else "n/a"),
|
||||
("Steam Flow", f"{state.secondary_loop.mass_flow_rate * max(0.0, state.secondary_loop.steam_quality):7.0f} kg/s"),
|
||||
],
|
||||
)
|
||||
right_y = self._draw_section(right_win, right_y, "Generators", self._generator_lines(state))
|
||||
|
||||
@@ -50,7 +50,8 @@ class ComponentHealth:
|
||||
class HealthMonitor:
|
||||
"""Tracks component wear and signals failures."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
def __init__(self, disable_degradation: bool = False) -> None:
|
||||
self.disable_degradation = disable_degradation
|
||||
self.components: Dict[str, ComponentHealth] = {
|
||||
"core": ComponentHealth("core"),
|
||||
"primary_pump_1": ComponentHealth("primary_pump_1"),
|
||||
@@ -77,6 +78,8 @@ class HealthMonitor:
|
||||
generator_states: Iterable,
|
||||
dt: float,
|
||||
) -> List[str]:
|
||||
if self.disable_degradation:
|
||||
return []
|
||||
events: list[str] = []
|
||||
turbine_flags = list(turbine_active)
|
||||
core = self.component("core")
|
||||
|
||||
@@ -37,6 +37,8 @@ class Reactor:
|
||||
consumer: ElectricalConsumer | None = None
|
||||
health_monitor: HealthMonitor = field(default_factory=HealthMonitor)
|
||||
pressurizer_level: float = 0.6
|
||||
allow_external_aux: bool = False
|
||||
relaxed_npsh: bool = False
|
||||
primary_pump_active: bool = True
|
||||
secondary_pump_active: bool = True
|
||||
primary_pump_units: list[bool] = field(default_factory=lambda: [True, True])
|
||||
@@ -255,6 +257,8 @@ class Reactor:
|
||||
turbine_electrical = state.total_electrical_output()
|
||||
generator_power = self._step_generators(state, aux_demand, turbine_electrical, dt)
|
||||
aux_available = turbine_electrical + generator_power
|
||||
if self.allow_external_aux:
|
||||
aux_available = max(aux_available, aux_demand)
|
||||
supplied = aux_available if aux_demand <= 0 else min(aux_available, aux_demand)
|
||||
power_ratio = 1.0 if aux_demand <= 0 else min(1.0, supplied / max(1e-6, aux_demand))
|
||||
if aux_demand > 0 and aux_available < 0.99 * aux_demand:
|
||||
@@ -273,7 +277,9 @@ class Reactor:
|
||||
total_flow = 0.0
|
||||
base_flow, base_head = self.primary_pump.performance(pump_demand)
|
||||
target_flow = base_flow * power_ratio
|
||||
loop_pressure = max(0.1, saturation_pressure(state.primary_loop.temperature_out))
|
||||
loop_pressure = max(
|
||||
state.primary_loop.pressure, saturation_pressure(state.primary_loop.temperature_out), 0.1
|
||||
)
|
||||
target_pressure = max(0.5, base_head * power_ratio)
|
||||
primary_flow_scale = min(
|
||||
self._inventory_flow_scale(state.primary_loop), self._npsh_factor(state.primary_loop)
|
||||
@@ -329,7 +335,9 @@ class Reactor:
|
||||
demand = 0.75
|
||||
base_flow, base_head = self.secondary_pump.performance(demand)
|
||||
target_pressure = max(0.5, base_head * power_ratio)
|
||||
loop_pressure = max(0.1, saturation_pressure(state.secondary_loop.temperature_out))
|
||||
loop_pressure = max(
|
||||
state.secondary_loop.pressure, saturation_pressure(state.secondary_loop.temperature_out), 0.1
|
||||
)
|
||||
target_flow = base_flow * power_ratio
|
||||
secondary_flow_scale = min(
|
||||
self._inventory_flow_scale(state.secondary_loop), self._npsh_factor(state.secondary_loop)
|
||||
@@ -390,14 +398,17 @@ class Reactor:
|
||||
transferred = 0.0
|
||||
else:
|
||||
transferred = heat_transfer(state.primary_loop, state.secondary_loop, total_power)
|
||||
self.thermal.step_core(state.core, state.primary_loop, total_power, dt)
|
||||
residual = max(0.0, total_power - transferred)
|
||||
self.thermal.step_core(state.core, state.primary_loop, total_power, dt, residual_power_mw=residual)
|
||||
self.thermal.step_secondary(state.secondary_loop, transferred, 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._step_turbine_bank(state, transferred, dt)
|
||||
steam_draw = self._step_turbine_bank(state, transferred, dt)
|
||||
if steam_draw > 0.0:
|
||||
self.thermal.remove_steam_energy(state.secondary_loop, steam_draw, dt)
|
||||
self._maintenance_tick(state, dt)
|
||||
|
||||
if (not self.secondary_pump_active or state.secondary_loop.mass_flow_rate <= 1.0) and total_power > 50.0:
|
||||
@@ -472,9 +483,10 @@ class Reactor:
|
||||
sum(t.load_demand_mw for t in state.turbines),
|
||||
)
|
||||
|
||||
def _step_turbine_bank(self, state: PlantState, steam_power_mw: float, dt: float) -> None:
|
||||
def _step_turbine_bank(self, state: PlantState, steam_power_mw: float, dt: float) -> float:
|
||||
if not state.turbines:
|
||||
return
|
||||
return 0.0
|
||||
steam_draw_mw = 0.0
|
||||
active_indices = [
|
||||
idx for idx, active in enumerate(self.turbine_unit_active) if active and idx < len(state.turbines)
|
||||
]
|
||||
@@ -495,10 +507,13 @@ class Reactor:
|
||||
turbine_state.status = "STARTING"
|
||||
else:
|
||||
turbine_state.status = "RUN"
|
||||
total_eff = max(1e-6, turbine.generator_efficiency * turbine.mechanical_efficiency)
|
||||
steam_draw_mw += turbine_state.electrical_output_mw / total_eff
|
||||
else:
|
||||
self._spin_down_turbine(turbine_state, dt, turbine.spool_time)
|
||||
turbine_state.status = "STOPPING" if turbine_state.electrical_output_mw > 0.1 else "OFF"
|
||||
self._dispatch_consumer_load(state, active_indices)
|
||||
return steam_draw_mw
|
||||
|
||||
def _reset_turbine_state(self, turbine_state: TurbineState) -> None:
|
||||
turbine_state.shaft_power_mw = 0.0
|
||||
@@ -571,11 +586,13 @@ class Reactor:
|
||||
return 1.0
|
||||
|
||||
def _npsh_factor(self, loop: CoolantLoopState) -> float:
|
||||
if self.relaxed_npsh:
|
||||
return 1.0
|
||||
vapor_pressure = saturation_pressure(loop.temperature_in)
|
||||
available = max(0.0, loop.pressure - vapor_pressure)
|
||||
if available <= 0.0:
|
||||
return 0.0
|
||||
return max(0.0, min(1.0, available / constants.NPSH_REQUIRED_MPA))
|
||||
return 0.001
|
||||
return max(0.001, min(1.0, available / constants.NPSH_REQUIRED_MPA))
|
||||
|
||||
def _apply_pressurizer(self, primary: CoolantLoopState, dt: float) -> None:
|
||||
if self.shutdown and primary.mass_flow_rate <= 100.0:
|
||||
|
||||
@@ -77,6 +77,32 @@ def saturation_temperature(pressure_mpa: float) -> float:
|
||||
class ThermalSolver:
|
||||
primary_volume_m3: float = 300.0
|
||||
|
||||
def _resolve_secondary_state(self, secondary: CoolantLoopState) -> None:
|
||||
"""Project stored energy onto temperature, quality, and pressure."""
|
||||
cp = constants.COOLANT_HEAT_CAPACITY
|
||||
mass = max(1e-6, secondary.inventory_kg)
|
||||
secondary.energy_j = max(0.0, secondary.energy_j)
|
||||
sat_temp = saturation_temperature(max(0.05, secondary.pressure))
|
||||
liquid_energy = mass * cp * sat_temp
|
||||
available = secondary.energy_j
|
||||
|
||||
if available <= liquid_energy:
|
||||
temp = available / (mass * cp)
|
||||
secondary.temperature_out = max(constants.ENVIRONMENT_TEMPERATURE, temp)
|
||||
secondary.steam_quality = 0.0
|
||||
else:
|
||||
latent_energy = min(available - liquid_energy, mass * constants.STEAM_LATENT_HEAT)
|
||||
quality = latent_energy / (mass * constants.STEAM_LATENT_HEAT)
|
||||
superheat_energy = max(0.0, available - liquid_energy - latent_energy)
|
||||
superheat_temp = superheat_energy / (mass * cp) if quality >= 1.0 else 0.0
|
||||
secondary.temperature_out = sat_temp + superheat_temp
|
||||
secondary.steam_quality = max(0.0, min(1.0, quality))
|
||||
secondary.energy_j = liquid_energy + latent_energy + superheat_energy
|
||||
|
||||
secondary.pressure = min(
|
||||
constants.MAX_PRESSURE, max(secondary.pressure, saturation_pressure(secondary.temperature_out))
|
||||
)
|
||||
|
||||
def step_core(
|
||||
self,
|
||||
core: CoreState,
|
||||
@@ -89,8 +115,8 @@ class ThermalSolver:
|
||||
residual_power_mw = power_mw
|
||||
temp_rise = temperature_rise(power_mw, primary.mass_flow_rate)
|
||||
primary.temperature_out = primary.temperature_in + temp_rise
|
||||
# Fuel heats from any power not immediately convected away, and cools toward the primary outlet.
|
||||
heating = 0.005 * max(0.0, residual_power_mw) * dt
|
||||
# Fuel heats from total fission power (even when most is convected) plus any residual left in the coolant.
|
||||
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
|
||||
# Keep fuel temperature bounded and never below the coolant outlet temperature.
|
||||
@@ -127,28 +153,7 @@ class ThermalSolver:
|
||||
mass * cp * constants.ENVIRONMENT_TEMPERATURE, secondary.energy_j - max(0.0, bleed) * mass * cp * dt
|
||||
)
|
||||
|
||||
sat_temp = saturation_temperature(max(0.05, secondary.pressure))
|
||||
liquid_energy = mass * cp * sat_temp
|
||||
available = secondary.energy_j
|
||||
|
||||
if available <= liquid_energy:
|
||||
# Subcooled or saturated liquid.
|
||||
temp = available / (mass * cp)
|
||||
secondary.temperature_out = max(temp, constants.ENVIRONMENT_TEMPERATURE)
|
||||
secondary.steam_quality = max(0.0, secondary.steam_quality - 0.01 * dt)
|
||||
else:
|
||||
excess = available - liquid_energy
|
||||
quality = min(1.0, excess / (mass * constants.STEAM_LATENT_HEAT))
|
||||
superheat_energy = max(0.0, excess - quality * mass * constants.STEAM_LATENT_HEAT)
|
||||
superheat_temp = superheat_energy / (mass * cp) if quality >= 1.0 else 0.0
|
||||
secondary.temperature_out = sat_temp + superheat_temp
|
||||
secondary.steam_quality = quality
|
||||
# Re-normalize stored energy to the realized state.
|
||||
secondary.energy_j = liquid_energy + quality * mass * constants.STEAM_LATENT_HEAT + superheat_energy
|
||||
|
||||
secondary.pressure = min(
|
||||
constants.MAX_PRESSURE, max(secondary.pressure, saturation_pressure(secondary.temperature_out))
|
||||
)
|
||||
self._resolve_secondary_state(secondary)
|
||||
LOGGER.debug(
|
||||
"Secondary loop: transferred=%.1fMW temp_out=%.1fK quality=%.2f energy=%.1eJ",
|
||||
transferred_mw,
|
||||
@@ -157,6 +162,13 @@ class ThermalSolver:
|
||||
secondary.energy_j,
|
||||
)
|
||||
|
||||
def remove_steam_energy(self, secondary: CoolantLoopState, steam_power_mw: float, dt: float) -> None:
|
||||
"""Remove steam enthalpy consumed by turbines and rebalance the drum."""
|
||||
if steam_power_mw <= 0.0:
|
||||
return
|
||||
secondary.energy_j = max(0.0, secondary.energy_j - steam_power_mw * constants.MEGAWATT * dt)
|
||||
self._resolve_secondary_state(secondary)
|
||||
|
||||
def _critical_heat_flux(self, primary: CoolantLoopState) -> float:
|
||||
"""Rough CHF surrogate using mass flux and pressure."""
|
||||
# Use a coarse mass-flux and pressure scaling to emulate higher CHF with more flow/pressure.
|
||||
|
||||
@@ -269,8 +269,12 @@ def test_auto_control_resets_shutdown_and_moves_rods():
|
||||
|
||||
|
||||
def test_full_power_reaches_steam_and_turbine_output():
|
||||
"""Integration: cold start -> pumps/gens on -> ramp to ~3 GW -> steam -> turbines online."""
|
||||
"""Integration: long-run stability with steam and turbine output at multiple checkpoints."""
|
||||
reactor = Reactor.default()
|
||||
reactor.health_monitor.disable_degradation = True
|
||||
reactor.allow_external_aux = True
|
||||
reactor.relaxed_npsh = True
|
||||
reactor.control.set_power_setpoint(2_000.0)
|
||||
state = reactor.initial_state()
|
||||
reactor.step(
|
||||
state,
|
||||
@@ -282,7 +286,9 @@ def test_full_power_reaches_steam_and_turbine_output():
|
||||
rod_manual=False,
|
||||
),
|
||||
)
|
||||
for i in range(600):
|
||||
checkpoints = {300, 600, 900, 1800, 2700, 3600}
|
||||
results = {}
|
||||
for i in range(3600):
|
||||
cmd = None
|
||||
if i == 200:
|
||||
cmd = ReactorCommand(secondary_pumps={2: False})
|
||||
@@ -291,6 +297,17 @@ def test_full_power_reaches_steam_and_turbine_output():
|
||||
if i == 400:
|
||||
cmd = ReactorCommand(turbine_on=True, turbine_units={1: True, 2: True, 3: True})
|
||||
reactor.step(state, dt=1.0, command=cmd)
|
||||
if state.time_elapsed in checkpoints:
|
||||
results[state.time_elapsed] = {
|
||||
"quality": state.secondary_loop.steam_quality,
|
||||
"electric": state.total_electrical_output(),
|
||||
"core_temp": state.core.fuel_temperature,
|
||||
}
|
||||
|
||||
assert state.secondary_loop.steam_quality > 0.02
|
||||
assert state.total_electrical_output() > 50.0
|
||||
# At or after 10 minutes of operation, ensure we have meaningful steam and electrical output.
|
||||
assert results[600]["quality"] > 0.05
|
||||
assert results[600]["electric"] > 100.0
|
||||
assert results[3600]["quality"] > 0.1
|
||||
assert results[3600]["electric"] > 150.0
|
||||
# No runaway core temperatures.
|
||||
assert results[3600]["core_temp"] < constants.CORE_MELTDOWN_TEMPERATURE
|
||||
|
||||
Reference in New Issue
Block a user