Add delayed kinetics and steam-drum balance
This commit is contained in:
@@ -8,6 +8,7 @@ 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
|
||||
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
|
||||
|
||||
@@ -26,7 +26,7 @@ def xenon_poisoning(flux: float) -> float:
|
||||
@dataclass
|
||||
class NeutronDynamics:
|
||||
beta_effective: float = 0.0065
|
||||
delayed_neutron_fraction: float = 0.0008
|
||||
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
|
||||
@@ -55,12 +55,18 @@ class NeutronDynamics:
|
||||
return rho
|
||||
|
||||
def flux_derivative(
|
||||
self, state: CoreState, rho: float, external_source_rate: float = 0.0, baseline_source: float = 1e5
|
||||
self,
|
||||
state: CoreState,
|
||||
rho: float,
|
||||
delayed_source: float,
|
||||
external_source_rate: float = 0.0,
|
||||
baseline_source: float = 1e5,
|
||||
) -> float:
|
||||
generation_time = constants.NEUTRON_LIFETIME
|
||||
beta = self.beta_effective
|
||||
source_term = self.external_source_coupling * external_source_rate
|
||||
return ((rho - beta) / generation_time) * state.neutron_flux + baseline_source + source_term
|
||||
prompt = ((rho - beta) / generation_time) * state.neutron_flux
|
||||
return prompt + delayed_source + baseline_source + source_term
|
||||
|
||||
def step(
|
||||
self,
|
||||
@@ -77,9 +83,22 @@ class NeutronDynamics:
|
||||
rho = min(rho, -0.04)
|
||||
baseline = 0.0 if shutdown else 1e5
|
||||
source = 0.0 if shutdown else external_source_rate
|
||||
d_flux = self.flux_derivative(state, rho, source, baseline_source=baseline)
|
||||
rod_positions = rod_banks if rod_banks else [control_fraction] * len(constants.CONTROL_ROD_BANK_WEIGHTS)
|
||||
self._ensure_precursors(state, len(rod_positions))
|
||||
bank_factors = self._bank_factors(rod_positions)
|
||||
bank_betas = self._bank_betas(len(bank_factors))
|
||||
delayed_source = self._delayed_source(state, bank_factors)
|
||||
|
||||
d_flux = self.flux_derivative(
|
||||
state,
|
||||
rho,
|
||||
delayed_source,
|
||||
external_source_rate=source,
|
||||
baseline_source=baseline,
|
||||
)
|
||||
state.neutron_flux = max(0.0, state.neutron_flux + d_flux * dt)
|
||||
state.reactivity_margin = rho
|
||||
self._update_precursors(state, bank_factors, bank_betas, dt)
|
||||
LOGGER.debug(
|
||||
"Neutronics: rho=%.5f, flux=%.2e n/cm2/s, d_flux=%.2e",
|
||||
rho,
|
||||
@@ -102,3 +121,38 @@ class NeutronDynamics:
|
||||
|
||||
def _xenon_penalty(self, state: CoreState) -> float:
|
||||
return min(0.05, state.xenon_inventory * self.xenon_reactivity_coeff)
|
||||
|
||||
def _bank_betas(self, bank_count: int) -> list[float]:
|
||||
weights = list(constants.CONTROL_ROD_BANK_WEIGHTS)
|
||||
if bank_count != len(weights):
|
||||
weights = [1.0 for _ in range(bank_count)]
|
||||
total = sum(weights) if weights else 1.0
|
||||
return [self.beta_effective * (w / total) for w in weights]
|
||||
|
||||
def _bank_factors(self, positions: list[float]) -> list[float]:
|
||||
factors: list[float] = []
|
||||
for pos in positions:
|
||||
insertion = clamp(pos, 0.0, 0.95)
|
||||
factors.append(max(0.0, 1.0 - insertion / 0.95))
|
||||
return factors
|
||||
|
||||
def _ensure_precursors(self, state: CoreState, bank_count: int) -> None:
|
||||
if not state.delayed_precursors or len(state.delayed_precursors) != bank_count:
|
||||
state.delayed_precursors = [0.0 for _ in range(bank_count)]
|
||||
|
||||
def _delayed_source(self, state: CoreState, bank_factors: list[float]) -> float:
|
||||
decay = self.delayed_decay_const
|
||||
return sum(decay * precursor * factor for precursor, factor in zip(state.delayed_precursors, bank_factors))
|
||||
|
||||
def _update_precursors(
|
||||
self, state: CoreState, bank_factors: list[float], bank_betas: list[float], dt: float
|
||||
) -> None:
|
||||
generation_time = constants.NEUTRON_LIFETIME
|
||||
decay = self.delayed_decay_const
|
||||
new_pools: list[float] = []
|
||||
for precursor, factor, beta in zip(state.delayed_precursors, bank_factors, bank_betas):
|
||||
production = (beta / generation_time) * state.neutron_flux * factor
|
||||
loss = decay * precursor
|
||||
updated = max(0.0, precursor + (production - loss) * dt)
|
||||
new_pools.append(updated)
|
||||
state.delayed_precursors = new_pools
|
||||
|
||||
@@ -95,6 +95,7 @@ class Reactor:
|
||||
reactivity_margin=-0.02,
|
||||
power_output_mw=0.1,
|
||||
burnup=0.0,
|
||||
delayed_precursors=[0.0 for _ in constants.CONTROL_ROD_BANK_WEIGHTS],
|
||||
fission_product_inventory={},
|
||||
emitted_particles={},
|
||||
)
|
||||
@@ -387,7 +388,7 @@ class Reactor:
|
||||
else:
|
||||
transferred = heat_transfer(state.primary_loop, state.secondary_loop, total_power)
|
||||
self.thermal.step_core(state.core, state.primary_loop, total_power, dt)
|
||||
self.thermal.step_secondary(state.secondary_loop, transferred)
|
||||
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
|
||||
|
||||
@@ -20,6 +20,7 @@ class CoreState:
|
||||
burnup: float # fraction of fuel consumed
|
||||
xenon_inventory: float = 0.0
|
||||
iodine_inventory: float = 0.0
|
||||
delayed_precursors: list[float] = field(default_factory=list)
|
||||
fission_product_inventory: dict[str, float] = field(default_factory=dict)
|
||||
emitted_particles: dict[str, float] = field(default_factory=dict)
|
||||
|
||||
|
||||
@@ -60,6 +60,19 @@ def saturation_pressure(temp_k: float) -> float:
|
||||
return min(constants.MAX_PRESSURE, max(0.01, psat_mpa))
|
||||
|
||||
|
||||
def saturation_temperature(pressure_mpa: float) -> float:
|
||||
"""Approximate saturation temperature (K) for water at the given pressure."""
|
||||
target = max(0.01, min(constants.MAX_PRESSURE, pressure_mpa))
|
||||
low, high = 273.15, 900.0
|
||||
for _ in range(40):
|
||||
mid = 0.5 * (low + high)
|
||||
if saturation_pressure(mid) < target:
|
||||
low = mid
|
||||
else:
|
||||
high = mid
|
||||
return high
|
||||
|
||||
|
||||
@dataclass
|
||||
class ThermalSolver:
|
||||
primary_volume_m3: float = 300.0
|
||||
@@ -89,10 +102,37 @@ class ThermalSolver:
|
||||
core.fuel_temperature,
|
||||
)
|
||||
|
||||
def step_secondary(self, secondary: CoolantLoopState, transferred_mw: float) -> None:
|
||||
delta_t = temperature_rise(transferred_mw, secondary.mass_flow_rate)
|
||||
secondary.temperature_out = secondary.temperature_in + delta_t
|
||||
secondary.steam_quality = min(1.0, max(0.0, delta_t / 100.0))
|
||||
def step_secondary(self, secondary: CoolantLoopState, transferred_mw: float, dt: float = 1.0) -> None:
|
||||
"""Update secondary loop using a simple steam-drum mass/energy balance."""
|
||||
if transferred_mw <= 0.0 or secondary.mass_flow_rate <= 0.0:
|
||||
secondary.steam_quality = max(0.0, secondary.steam_quality - 0.02 * dt)
|
||||
secondary.temperature_out = max(
|
||||
constants.ENVIRONMENT_TEMPERATURE, secondary.temperature_out - 0.5 * dt
|
||||
)
|
||||
secondary.pressure = max(
|
||||
0.1, min(constants.MAX_PRESSURE, saturation_pressure(secondary.temperature_out))
|
||||
)
|
||||
return
|
||||
|
||||
temp_in = secondary.temperature_in
|
||||
mass_flow = secondary.mass_flow_rate
|
||||
cp = constants.COOLANT_HEAT_CAPACITY
|
||||
sat_temp = saturation_temperature(max(0.05, secondary.pressure))
|
||||
energy_j = max(0.0, transferred_mw) * constants.MEGAWATT * dt
|
||||
|
||||
# Energy needed to heat incoming feed to saturation.
|
||||
sensible_j = max(0.0, sat_temp - temp_in) * mass_flow * cp * dt
|
||||
if energy_j <= sensible_j:
|
||||
delta_t = temperature_rise(transferred_mw, mass_flow)
|
||||
secondary.temperature_out = temp_in + delta_t
|
||||
secondary.steam_quality = 0.0
|
||||
else:
|
||||
energy_left = energy_j - sensible_j
|
||||
steam_mass = energy_left / constants.STEAM_LATENT_HEAT
|
||||
produced_fraction = steam_mass / max(1e-6, mass_flow * dt)
|
||||
secondary.temperature_out = sat_temp
|
||||
secondary.steam_quality = min(1.0, max(0.0, produced_fraction))
|
||||
|
||||
secondary.pressure = min(
|
||||
constants.MAX_PRESSURE, max(secondary.pressure, saturation_pressure(secondary.temperature_out))
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user