Improve reactor controls, reactions, and maintenance UI
This commit is contained in:
@@ -23,6 +23,7 @@ Pytest is the preferred framework. Place tests under `tests/` mirroring the `rea
|
|||||||
|
|
||||||
## Commit & Pull Request Guidelines
|
## Commit & Pull Request Guidelines
|
||||||
Write commits in imperative mood (`Add turbine moisture separator model`). Squash small WIP commits before review. Pull requests must describe the scenario, attach log excerpts (`snapshots.json` diff) or plots for novel behavior, and link to any tracking issues. Include validation notes: commands run, expected trends (e.g., outlet temperature increase), and outstanding risks.
|
Write commits in imperative mood (`Add turbine moisture separator model`). Squash small WIP commits before review. Pull requests must describe the scenario, attach log excerpts (`snapshots.json` diff) or plots for novel behavior, and link to any tracking issues. Include validation notes: commands run, expected trends (e.g., outlet temperature increase), and outstanding risks.
|
||||||
|
- Agents should automatically create a commit immediately after code/config/text changes are finished; use a clear imperative subject and avoid piling multiple logical changes into one commit unless the task explicitly says otherwise.
|
||||||
|
|
||||||
## Safety & Configuration
|
## Safety & Configuration
|
||||||
Sim parameters live in constructors; never hard-code environment-specific paths. When adding new physics, guard unstable calculations with clamps and highlight operating limits in comments. Sensitive experiments (e.g., beyond design basis) should default to disabled flags so scripted studies remain safe by default.
|
Sim parameters live in constructors; never hard-code environment-specific paths. When adding new physics, guard unstable calculations with clamps and highlight operating limits in comments. Sensitive experiments (e.g., beyond design basis) should default to disabled flags so scripted studies remain safe by default.
|
||||||
|
|||||||
104
artifacts/last_state.json
Normal file
104
artifacts/last_state.json
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
{
|
||||||
|
"control": {
|
||||||
|
"setpoint_mw": 3000.0,
|
||||||
|
"rod_fraction": 0.3872181667930225
|
||||||
|
},
|
||||||
|
"plant": {
|
||||||
|
"core": {
|
||||||
|
"fuel_temperature": 633.2342060602825,
|
||||||
|
"neutron_flux": 31335131.43776027,
|
||||||
|
"reactivity_margin": 0.006241667151538859,
|
||||||
|
"power_output_mw": 2976.900110634399,
|
||||||
|
"burnup": 0.004945521266135161
|
||||||
|
},
|
||||||
|
"primary_loop": {
|
||||||
|
"temperature_in": 295.0,
|
||||||
|
"temperature_out": 338.7522062115579,
|
||||||
|
"pressure": 14.0,
|
||||||
|
"mass_flow_rate": 16200.0,
|
||||||
|
"steam_quality": 0.0
|
||||||
|
},
|
||||||
|
"secondary_loop": {
|
||||||
|
"temperature_in": 295.0,
|
||||||
|
"temperature_out": 360.05377003828596,
|
||||||
|
"pressure": 6.650537700382859,
|
||||||
|
"mass_flow_rate": 10880.0,
|
||||||
|
"steam_quality": 0.6505377003828593
|
||||||
|
},
|
||||||
|
"turbines": [
|
||||||
|
{
|
||||||
|
"steam_enthalpy": 3090.3226202297155,
|
||||||
|
"shaft_power_mw": 336.9056685758782,
|
||||||
|
"electrical_output_mw": 323.4294418328431,
|
||||||
|
"condenser_temperature": 305.0,
|
||||||
|
"load_demand_mw": 0.0,
|
||||||
|
"load_supplied_mw": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"steam_enthalpy": 3090.3226202297155,
|
||||||
|
"shaft_power_mw": 336.9056685758782,
|
||||||
|
"electrical_output_mw": 323.4294418328431,
|
||||||
|
"condenser_temperature": 305.0,
|
||||||
|
"load_demand_mw": 0.0,
|
||||||
|
"load_supplied_mw": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"steam_enthalpy": 3090.3226202297155,
|
||||||
|
"shaft_power_mw": 336.9056685758782,
|
||||||
|
"electrical_output_mw": 323.4294418328431,
|
||||||
|
"condenser_temperature": 305.0,
|
||||||
|
"load_demand_mw": 0.0,
|
||||||
|
"load_supplied_mw": 0.0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time_elapsed": 600.0
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"primary_pump_active": true,
|
||||||
|
"secondary_pump_active": true,
|
||||||
|
"turbine_active": true,
|
||||||
|
"turbine_units": [
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
"shutdown": false,
|
||||||
|
"consumer": {
|
||||||
|
"online": false,
|
||||||
|
"demand_mw": 800.0,
|
||||||
|
"name": "Grid"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"health": {
|
||||||
|
"core": {
|
||||||
|
"name": "core",
|
||||||
|
"integrity": 0.9400000000000066,
|
||||||
|
"failed": false
|
||||||
|
},
|
||||||
|
"primary_pump": {
|
||||||
|
"name": "primary_pump",
|
||||||
|
"integrity": 0.5800000000000063,
|
||||||
|
"failed": false
|
||||||
|
},
|
||||||
|
"secondary_pump": {
|
||||||
|
"name": "secondary_pump",
|
||||||
|
"integrity": 0.1120000000000021,
|
||||||
|
"failed": false
|
||||||
|
},
|
||||||
|
"turbine_1": {
|
||||||
|
"name": "turbine_1",
|
||||||
|
"integrity": 0.610000000000003,
|
||||||
|
"failed": false
|
||||||
|
},
|
||||||
|
"turbine_2": {
|
||||||
|
"name": "turbine_2",
|
||||||
|
"integrity": 0.610000000000003,
|
||||||
|
"failed": false
|
||||||
|
},
|
||||||
|
"turbine_3": {
|
||||||
|
"name": "turbine_3",
|
||||||
|
"integrity": 0.610000000000003,
|
||||||
|
"failed": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -164,6 +164,14 @@ class FissionEvent:
|
|||||||
energy_mev: float
|
energy_mev: float
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ReactionEvent:
|
||||||
|
parent: Atom
|
||||||
|
products: tuple[Atom, ...]
|
||||||
|
emitted_particles: tuple[str, ...]
|
||||||
|
energy_mev: float
|
||||||
|
|
||||||
|
|
||||||
def make_atom(protons: int, neutrons: int) -> Atom:
|
def make_atom(protons: int, neutrons: int) -> Atom:
|
||||||
return Atom(symbol=element_symbol(protons), protons=protons, neutrons=neutrons)
|
return Atom(symbol=element_symbol(protons), protons=protons, neutrons=neutrons)
|
||||||
|
|
||||||
@@ -235,6 +243,64 @@ class AtomicPhysics:
|
|||||||
)
|
)
|
||||||
return event
|
return event
|
||||||
|
|
||||||
|
def alpha_decay(self, atom: Atom) -> ReactionEvent:
|
||||||
|
if atom.protons <= 2 or atom.neutrons <= 2:
|
||||||
|
return ReactionEvent(parent=atom, products=(atom,), emitted_particles=(), energy_mev=0.0)
|
||||||
|
daughter = make_atom(atom.protons - 2, atom.neutrons - 2)
|
||||||
|
alpha = make_atom(2, 2)
|
||||||
|
energy = 5.0 # Typical alpha decay energy
|
||||||
|
return ReactionEvent(parent=atom, products=(daughter, alpha), emitted_particles=("alpha",), energy_mev=energy)
|
||||||
|
|
||||||
|
def beta_minus_decay(self, atom: Atom) -> ReactionEvent:
|
||||||
|
if atom.neutrons <= 0:
|
||||||
|
return ReactionEvent(parent=atom, products=(atom,), emitted_particles=(), energy_mev=0.0)
|
||||||
|
daughter = make_atom(atom.protons + 1, atom.neutrons - 1)
|
||||||
|
energy = 1.0
|
||||||
|
return ReactionEvent(
|
||||||
|
parent=atom,
|
||||||
|
products=(daughter,),
|
||||||
|
emitted_particles=("beta-", "anti-nu_e"),
|
||||||
|
energy_mev=energy,
|
||||||
|
)
|
||||||
|
|
||||||
|
def beta_plus_decay(self, atom: Atom) -> ReactionEvent:
|
||||||
|
if atom.protons <= 1:
|
||||||
|
return ReactionEvent(parent=atom, products=(atom,), emitted_particles=(), energy_mev=0.0)
|
||||||
|
daughter = make_atom(atom.protons - 1, atom.neutrons + 1)
|
||||||
|
energy = 1.0
|
||||||
|
return ReactionEvent(
|
||||||
|
parent=atom,
|
||||||
|
products=(daughter,),
|
||||||
|
emitted_particles=("beta+", "nu_e"),
|
||||||
|
energy_mev=energy,
|
||||||
|
)
|
||||||
|
|
||||||
|
def electron_capture(self, atom: Atom) -> ReactionEvent:
|
||||||
|
if atom.protons <= 1:
|
||||||
|
return ReactionEvent(parent=atom, products=(atom,), emitted_particles=(), energy_mev=0.0)
|
||||||
|
daughter = make_atom(atom.protons - 1, atom.neutrons + 1)
|
||||||
|
energy = 0.5
|
||||||
|
return ReactionEvent(
|
||||||
|
parent=atom,
|
||||||
|
products=(daughter,),
|
||||||
|
emitted_particles=("nu_e", "gamma"),
|
||||||
|
energy_mev=energy,
|
||||||
|
)
|
||||||
|
|
||||||
|
def gamma_emission(self, atom: Atom, energy_mev: float = 0.2) -> ReactionEvent:
|
||||||
|
return ReactionEvent(parent=atom, products=(atom,), emitted_particles=("gamma",), energy_mev=energy_mev)
|
||||||
|
|
||||||
|
def spontaneous_fission(self, atom: Atom) -> ReactionEvent:
|
||||||
|
# Reuse the generic splitter to keep mass/charge balanced.
|
||||||
|
split = self._generic_split(atom)
|
||||||
|
neutrons = ("n",) * max(0, split.emitted_neutrons)
|
||||||
|
return ReactionEvent(
|
||||||
|
parent=atom,
|
||||||
|
products=split.products,
|
||||||
|
emitted_particles=neutrons,
|
||||||
|
energy_mev=split.energy_mev,
|
||||||
|
)
|
||||||
|
|
||||||
def _generic_split(self, atom: Atom) -> FissionEvent:
|
def _generic_split(self, atom: Atom) -> FissionEvent:
|
||||||
heavy_mass = max(1, math.floor(atom.mass_number * 0.55))
|
heavy_mass = max(1, math.floor(atom.mass_number * 0.55))
|
||||||
light_mass = atom.mass_number - heavy_mass
|
light_mass = atom.mass_number - heavy_mass
|
||||||
|
|||||||
@@ -10,10 +10,16 @@ COOLANT_HEAT_CAPACITY = 4_200.0 # J/(kg*K) for water/steam
|
|||||||
COOLANT_DENSITY = 700.0 # kg/m^3 averaged between phases
|
COOLANT_DENSITY = 700.0 # kg/m^3 averaged between phases
|
||||||
MAX_CORE_TEMPERATURE = 1_800.0 # K
|
MAX_CORE_TEMPERATURE = 1_800.0 # K
|
||||||
MAX_PRESSURE = 15.0 # MPa typical PWR primary loop limit
|
MAX_PRESSURE = 15.0 # MPa typical PWR primary loop limit
|
||||||
CONTROL_ROD_SPEED = 0.05 # fraction insertion per second
|
CONTROL_ROD_SPEED = 0.03 # fraction insertion per second
|
||||||
STEAM_TURBINE_EFFICIENCY = 0.34
|
STEAM_TURBINE_EFFICIENCY = 0.34
|
||||||
GENERATOR_EFFICIENCY = 0.96
|
GENERATOR_EFFICIENCY = 0.96
|
||||||
ENVIRONMENT_TEMPERATURE = 295.0 # K
|
ENVIRONMENT_TEMPERATURE = 295.0 # K
|
||||||
AMU_TO_KG = 1.660_539_066_60e-27
|
AMU_TO_KG = 1.660_539_066_60e-27
|
||||||
MEV_TO_J = 1.602_176_634e-13
|
MEV_TO_J = 1.602_176_634e-13
|
||||||
ELECTRON_FISSION_CROSS_SECTION = 5e-16 # cm^2, tuned for simulation scale
|
ELECTRON_FISSION_CROSS_SECTION = 5e-16 # cm^2, tuned for simulation scale
|
||||||
|
# Threshold inventories (event counts) for flagging common poisons in diagnostics.
|
||||||
|
KEY_POISON_THRESHOLDS = {
|
||||||
|
"Xe": 1e20, # xenon
|
||||||
|
"I": 1e20, # iodine precursor to xenon
|
||||||
|
"Sm": 5e19, # samarium
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ class ControlSystem:
|
|||||||
if self.manual_control:
|
if self.manual_control:
|
||||||
return self.rod_fraction
|
return self.rod_fraction
|
||||||
error = (state.power_output_mw - self.setpoint_mw) / self.setpoint_mw
|
error = (state.power_output_mw - self.setpoint_mw) / self.setpoint_mw
|
||||||
adjustment = -error * 0.3
|
# When power is low (negative error) withdraw rods; when high, insert them.
|
||||||
|
adjustment = error * 0.2
|
||||||
adjustment = clamp(adjustment, -constants.CONTROL_ROD_SPEED * dt, constants.CONTROL_ROD_SPEED * dt)
|
adjustment = clamp(adjustment, -constants.CONTROL_ROD_SPEED * dt, constants.CONTROL_ROD_SPEED * dt)
|
||||||
previous = self.rod_fraction
|
previous = self.rod_fraction
|
||||||
self.rod_fraction = clamp(self.rod_fraction + adjustment, 0.0, 0.95)
|
self.rod_fraction = clamp(self.rod_fraction + adjustment, 0.0, 0.95)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from dataclasses import dataclass
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
from . import constants
|
||||||
from .commands import ReactorCommand
|
from .commands import ReactorCommand
|
||||||
from .reactor import Reactor
|
from .reactor import Reactor
|
||||||
from .simulation import ReactorSimulation
|
from .simulation import ReactorSimulation
|
||||||
@@ -50,8 +51,12 @@ class ReactorDashboard:
|
|||||||
DashboardKey("o", "Toggle secondary pump"),
|
DashboardKey("o", "Toggle secondary pump"),
|
||||||
DashboardKey("t", "Toggle turbine"),
|
DashboardKey("t", "Toggle turbine"),
|
||||||
DashboardKey("1/2/3", "Toggle turbine units 1-3"),
|
DashboardKey("1/2/3", "Toggle turbine units 1-3"),
|
||||||
|
DashboardKey("y/u/i", "Maintain turbine 1/2/3"),
|
||||||
DashboardKey("c", "Toggle consumer"),
|
DashboardKey("c", "Toggle consumer"),
|
||||||
DashboardKey("r", "Reset & clear state"),
|
DashboardKey("r", "Reset & clear state"),
|
||||||
|
DashboardKey("m", "Maintain primary pump"),
|
||||||
|
DashboardKey("n", "Maintain secondary pump"),
|
||||||
|
DashboardKey("k", "Maintain core (requires shutdown)"),
|
||||||
DashboardKey("+/-", "Withdraw/insert rods"),
|
DashboardKey("+/-", "Withdraw/insert rods"),
|
||||||
DashboardKey("[/]", "Adjust consumer demand −/+50 MW"),
|
DashboardKey("[/]", "Adjust consumer demand −/+50 MW"),
|
||||||
DashboardKey("s", "Setpoint −250 MW"),
|
DashboardKey("s", "Setpoint −250 MW"),
|
||||||
@@ -142,6 +147,18 @@ class ReactorDashboard:
|
|||||||
self._queue_command(ReactorCommand(power_setpoint=self.reactor.control.setpoint_mw + 250.0))
|
self._queue_command(ReactorCommand(power_setpoint=self.reactor.control.setpoint_mw + 250.0))
|
||||||
elif ch in (ord("a"), ord("A")):
|
elif ch in (ord("a"), ord("A")):
|
||||||
self._queue_command(ReactorCommand(rod_manual=not self.reactor.control.manual_control))
|
self._queue_command(ReactorCommand(rod_manual=not self.reactor.control.manual_control))
|
||||||
|
elif ch in (ord("m"), ord("M")):
|
||||||
|
self._queue_command(ReactorCommand.maintain("primary_pump"))
|
||||||
|
elif ch in (ord("n"), ord("N")):
|
||||||
|
self._queue_command(ReactorCommand.maintain("secondary_pump"))
|
||||||
|
elif ch in (ord("k"), ord("K")):
|
||||||
|
self._queue_command(ReactorCommand.maintain("core"))
|
||||||
|
elif ch in (ord("y"), ord("Y")):
|
||||||
|
self._queue_command(ReactorCommand.maintain("turbine_1"))
|
||||||
|
elif ch in (ord("u"), ord("U")):
|
||||||
|
self._queue_command(ReactorCommand.maintain("turbine_2"))
|
||||||
|
elif ch in (ord("i"), ord("I")):
|
||||||
|
self._queue_command(ReactorCommand.maintain("turbine_3"))
|
||||||
|
|
||||||
def _queue_command(self, command: ReactorCommand) -> None:
|
def _queue_command(self, command: ReactorCommand) -> None:
|
||||||
if self.pending_command is None:
|
if self.pending_command is None:
|
||||||
@@ -240,10 +257,12 @@ class ReactorDashboard:
|
|||||||
("Core Power", f"{state.core.power_output_mw:8.1f} MW"),
|
("Core Power", f"{state.core.power_output_mw:8.1f} MW"),
|
||||||
("Neutron Flux", f"{state.core.neutron_flux:10.2e}"),
|
("Neutron Flux", f"{state.core.neutron_flux:10.2e}"),
|
||||||
("Rods", f"{self.reactor.control.rod_fraction:.3f}"),
|
("Rods", f"{self.reactor.control.rod_fraction:.3f}"),
|
||||||
|
("Rod Mode", "AUTO" if not self.reactor.control.manual_control else "MANUAL"),
|
||||||
("Setpoint", f"{self.reactor.control.setpoint_mw:7.0f} MW"),
|
("Setpoint", f"{self.reactor.control.setpoint_mw:7.0f} MW"),
|
||||||
("Reactivity", f"{state.core.reactivity_margin:+.4f}"),
|
("Reactivity", f"{state.core.reactivity_margin:+.4f}"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
y = self._draw_section(win, y, "Key Poisons / Emitters", self._poison_lines(state))
|
||||||
y = self._draw_section(
|
y = self._draw_section(
|
||||||
win,
|
win,
|
||||||
y,
|
y,
|
||||||
@@ -300,6 +319,7 @@ class ReactorDashboard:
|
|||||||
"Start pumps before withdrawing rods.",
|
"Start pumps before withdrawing rods.",
|
||||||
"Bring turbine and consumer online after thermal stabilization.",
|
"Bring turbine and consumer online after thermal stabilization.",
|
||||||
"Toggle turbine units (1/2/3) for staggered maintenance.",
|
"Toggle turbine units (1/2/3) for staggered maintenance.",
|
||||||
|
"Use m/n/k/y/u/i to request maintenance (stop equipment first).",
|
||||||
"Press 'r' to reset/clear state if you want a cold start.",
|
"Press 'r' to reset/clear state if you want a cold start.",
|
||||||
"Watch component health to avoid automatic trips.",
|
"Watch component health to avoid automatic trips.",
|
||||||
]
|
]
|
||||||
@@ -370,6 +390,24 @@ class ReactorDashboard:
|
|||||||
def _total_load_demand(self, state: PlantState) -> float:
|
def _total_load_demand(self, state: PlantState) -> float:
|
||||||
return sum(t.load_demand_mw for t in state.turbines)
|
return sum(t.load_demand_mw for t in state.turbines)
|
||||||
|
|
||||||
|
def _poison_lines(self, state: PlantState) -> list[tuple[str, str]]:
|
||||||
|
inventory = state.core.fission_product_inventory or {}
|
||||||
|
particles = state.core.emitted_particles or {}
|
||||||
|
lines: list[tuple[str, str]] = []
|
||||||
|
def fmt(symbol: str, label: str) -> tuple[str, str]:
|
||||||
|
qty = inventory.get(symbol, 0.0)
|
||||||
|
threshold = constants.KEY_POISON_THRESHOLDS.get(symbol)
|
||||||
|
flag = " !" if threshold is not None and qty >= threshold else ""
|
||||||
|
return (f"{label}{flag}", f"{qty:9.2e}")
|
||||||
|
|
||||||
|
lines.append(fmt("Xe", "Xe (xenon)"))
|
||||||
|
lines.append(fmt("Sm", "Sm (samarium)"))
|
||||||
|
lines.append(fmt("I", "I (iodine)"))
|
||||||
|
lines.append(("Neutrons (src)", f"{particles.get('n', 0.0):9.2e}"))
|
||||||
|
lines.append(("Gammas", f"{particles.get('gamma', 0.0):9.2e}"))
|
||||||
|
lines.append(("Alphas", f"{particles.get('alpha', 0.0):9.2e}"))
|
||||||
|
return lines
|
||||||
|
|
||||||
def _draw_health_bar(self, win: "curses._CursesWindow", start_y: int) -> None:
|
def _draw_health_bar(self, win: "curses._CursesWindow", start_y: int) -> None:
|
||||||
height, width = win.getmaxyx()
|
height, width = win.getmaxyx()
|
||||||
if start_y >= height - 2:
|
if start_y >= height - 2:
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class FuelAssembly:
|
|||||||
mass_kg: float
|
mass_kg: float
|
||||||
fissile_atom: Atom = field(default_factory=lambda: make_atom(92, 143))
|
fissile_atom: Atom = field(default_factory=lambda: make_atom(92, 143))
|
||||||
atomic_physics: AtomicPhysics = field(default_factory=AtomicPhysics)
|
atomic_physics: AtomicPhysics = field(default_factory=AtomicPhysics)
|
||||||
|
decay_activity_base: float = 1e16 # nominal decay events per second at burnup 0
|
||||||
|
|
||||||
def available_energy_j(self, state: CoreState) -> float:
|
def available_energy_j(self, state: CoreState) -> float:
|
||||||
fraction_remaining = max(0.0, 1.0 - state.burnup)
|
fraction_remaining = max(0.0, 1.0 - state.burnup)
|
||||||
@@ -57,3 +58,52 @@ class FuelAssembly:
|
|||||||
power_mw,
|
power_mw,
|
||||||
)
|
)
|
||||||
return max(0.0, power_mw), event_rate, event
|
return max(0.0, power_mw), event_rate, event
|
||||||
|
|
||||||
|
def decay_reaction_effects(
|
||||||
|
self, state: CoreState
|
||||||
|
) -> tuple[float, float, dict[str, float], dict[str, float]]:
|
||||||
|
"""Return (power MW, neutron source rate, product rates, particle rates)."""
|
||||||
|
# Scale activity with burnup (fission products) and a small flux contribution.
|
||||||
|
activity = self.decay_activity_base * (0.05 + state.burnup) * (1.0 + 0.00001 * state.neutron_flux)
|
||||||
|
activity = max(0.0, min(activity, 1e20))
|
||||||
|
|
||||||
|
reactions = [
|
||||||
|
self.atomic_physics.alpha_decay(self.fissile_atom),
|
||||||
|
self.atomic_physics.beta_minus_decay(self.fissile_atom),
|
||||||
|
self.atomic_physics.beta_plus_decay(self.fissile_atom),
|
||||||
|
self.atomic_physics.electron_capture(self.fissile_atom),
|
||||||
|
self.atomic_physics.gamma_emission(self.fissile_atom),
|
||||||
|
self.atomic_physics.spontaneous_fission(self.fissile_atom),
|
||||||
|
]
|
||||||
|
weights = [1.0, 1.2, 0.5, 0.4, 1.6, 0.05]
|
||||||
|
total_weight = sum(weights)
|
||||||
|
|
||||||
|
power_watts = 0.0
|
||||||
|
neutron_source = 0.0
|
||||||
|
product_rates: dict[str, float] = {}
|
||||||
|
particle_rates: dict[str, float] = {}
|
||||||
|
|
||||||
|
for event, weight in zip(reactions, weights):
|
||||||
|
rate = activity * (weight / total_weight)
|
||||||
|
power_watts += rate * event.energy_mev * constants.MEV_TO_J
|
||||||
|
for atom in event.products:
|
||||||
|
product_rates[atom.symbol] = product_rates.get(atom.symbol, 0.0) + rate
|
||||||
|
for particle in event.emitted_particles:
|
||||||
|
particle_rates[particle] = particle_rates.get(particle, 0.0) + rate
|
||||||
|
if particle == "n":
|
||||||
|
neutron_source += rate
|
||||||
|
|
||||||
|
power_mw = power_watts / constants.MEGAWATT
|
||||||
|
capped_power = min(power_mw, 0.1 * max(state.power_output_mw, 1.0))
|
||||||
|
LOGGER.debug(
|
||||||
|
"Decay reactions contribute %.2f MW (raw %.2f MW) with neutron source %.2e 1/s",
|
||||||
|
capped_power,
|
||||||
|
power_mw,
|
||||||
|
neutron_source,
|
||||||
|
)
|
||||||
|
return max(0.0, capped_power), neutron_source, product_rates, particle_rates
|
||||||
|
|
||||||
|
def decay_reaction_power(self, state: CoreState) -> float:
|
||||||
|
"""Compatibility shim: return only power contribution."""
|
||||||
|
power_mw, _, _, _ = self.decay_reaction_effects(state)
|
||||||
|
return power_mw
|
||||||
|
|||||||
@@ -15,18 +15,19 @@ LOGGER = logging.getLogger(__name__)
|
|||||||
def temperature_feedback(temp: float) -> float:
|
def temperature_feedback(temp: float) -> float:
|
||||||
"""Negative coefficient: higher temperature lowers reactivity."""
|
"""Negative coefficient: higher temperature lowers reactivity."""
|
||||||
reference = 900.0
|
reference = 900.0
|
||||||
coefficient = -2.5e-5
|
coefficient = -1.5e-5
|
||||||
return coefficient * (temp - reference)
|
return coefficient * (temp - reference)
|
||||||
|
|
||||||
|
|
||||||
def xenon_poisoning(flux: float) -> float:
|
def xenon_poisoning(flux: float) -> float:
|
||||||
return min(0.015, 2e-9 * flux)
|
return min(0.01, 5e-10 * flux)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class NeutronDynamics:
|
class NeutronDynamics:
|
||||||
beta_effective: float = 0.0065
|
beta_effective: float = 0.0065
|
||||||
delayed_neutron_fraction: float = 0.0008
|
delayed_neutron_fraction: float = 0.0008
|
||||||
|
external_source_coupling: float = 1e-6
|
||||||
|
|
||||||
def reactivity(self, state: CoreState, control_fraction: float) -> float:
|
def reactivity(self, state: CoreState, control_fraction: float) -> float:
|
||||||
rho = (
|
rho = (
|
||||||
@@ -37,14 +38,15 @@ class NeutronDynamics:
|
|||||||
)
|
)
|
||||||
return rho
|
return rho
|
||||||
|
|
||||||
def flux_derivative(self, state: CoreState, rho: float) -> float:
|
def flux_derivative(self, state: CoreState, rho: float, external_source_rate: float = 0.0) -> float:
|
||||||
generation_time = constants.NEUTRON_LIFETIME
|
generation_time = constants.NEUTRON_LIFETIME
|
||||||
beta = self.beta_effective
|
beta = self.beta_effective
|
||||||
return ((rho - beta) / generation_time) * state.neutron_flux + 1e5
|
source_term = self.external_source_coupling * external_source_rate
|
||||||
|
return ((rho - beta) / generation_time) * state.neutron_flux + 1e5 + source_term
|
||||||
|
|
||||||
def step(self, state: CoreState, control_fraction: float, dt: float) -> None:
|
def step(self, state: CoreState, control_fraction: float, dt: float, external_source_rate: float = 0.0) -> None:
|
||||||
rho = self.reactivity(state, control_fraction)
|
rho = self.reactivity(state, control_fraction)
|
||||||
d_flux = self.flux_derivative(state, rho)
|
d_flux = self.flux_derivative(state, rho, external_source_rate)
|
||||||
state.neutron_flux = max(0.0, state.neutron_flux + d_flux * dt)
|
state.neutron_flux = max(0.0, state.neutron_flux + d_flux * dt)
|
||||||
state.reactivity_margin = rho
|
state.reactivity_margin = rho
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class Reactor:
|
|||||||
turbine_active: bool = True
|
turbine_active: bool = True
|
||||||
turbine_unit_active: list[bool] = field(default_factory=lambda: [True, True, True])
|
turbine_unit_active: list[bool] = field(default_factory=lambda: [True, True, True])
|
||||||
shutdown: bool = False
|
shutdown: bool = False
|
||||||
|
poison_alerts: set[str] = field(default_factory=set)
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if not self.turbines:
|
if not self.turbines:
|
||||||
@@ -72,6 +73,8 @@ class Reactor:
|
|||||||
reactivity_margin=-0.02,
|
reactivity_margin=-0.02,
|
||||||
power_output_mw=0.1,
|
power_output_mw=0.1,
|
||||||
burnup=0.0,
|
burnup=0.0,
|
||||||
|
fission_product_inventory={},
|
||||||
|
emitted_particles={},
|
||||||
)
|
)
|
||||||
primary = CoolantLoopState(
|
primary = CoolantLoopState(
|
||||||
temperature_in=ambient,
|
temperature_in=ambient,
|
||||||
@@ -111,15 +114,28 @@ class Reactor:
|
|||||||
overrides = self._apply_command(command, state)
|
overrides = self._apply_command(command, state)
|
||||||
rod_fraction = overrides.get("rod_fraction", rod_fraction)
|
rod_fraction = overrides.get("rod_fraction", rod_fraction)
|
||||||
|
|
||||||
self.neutronics.step(state.core, rod_fraction, dt)
|
decay_power, decay_neutron_source, decay_products, decay_particles = self.fuel.decay_reaction_effects(
|
||||||
|
state.core
|
||||||
|
)
|
||||||
|
self.neutronics.step(state.core, rod_fraction, dt, external_source_rate=decay_neutron_source)
|
||||||
|
|
||||||
prompt_power, fission_rate, fission_event = self.fuel.prompt_energy_rate(
|
prompt_power, fission_rate, fission_event = self.fuel.prompt_energy_rate(
|
||||||
state.core.neutron_flux, rod_fraction
|
state.core.neutron_flux, rod_fraction
|
||||||
)
|
)
|
||||||
decay_power = decay_heat_fraction(state.core.burnup) * state.core.power_output_mw
|
decay_heat = decay_heat_fraction(state.core.burnup) * state.core.power_output_mw
|
||||||
total_power = prompt_power + decay_power
|
total_power = prompt_power + decay_heat + decay_power
|
||||||
state.core.power_output_mw = total_power
|
state.core.power_output_mw = total_power
|
||||||
state.core.update_burnup(dt)
|
state.core.update_burnup(dt)
|
||||||
|
# Track fission products and emitted particles for diagnostics.
|
||||||
|
products: dict[str, float] = {}
|
||||||
|
for atom in fission_event.products:
|
||||||
|
products[atom.symbol] = products.get(atom.symbol, 0.0) + fission_rate * dt
|
||||||
|
for k, v in decay_products.items():
|
||||||
|
products[k] = products.get(k, 0.0) + v * dt
|
||||||
|
particles: dict[str, float] = {k: v * dt for k, v in decay_particles.items()}
|
||||||
|
state.core.add_products(products)
|
||||||
|
state.core.add_emitted_particles(particles)
|
||||||
|
self._check_poison_alerts(state)
|
||||||
|
|
||||||
pump_demand = overrides.get("coolant_demand", self.control.coolant_demand(state.primary_loop))
|
pump_demand = overrides.get("coolant_demand", self.control.coolant_demand(state.primary_loop))
|
||||||
if self.primary_pump_active:
|
if self.primary_pump_active:
|
||||||
@@ -371,3 +387,11 @@ class Reactor:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
return plant
|
return plant
|
||||||
|
|
||||||
|
def _check_poison_alerts(self, state: PlantState) -> None:
|
||||||
|
inventory = state.core.fission_product_inventory or {}
|
||||||
|
for symbol, threshold in constants.KEY_POISON_THRESHOLDS.items():
|
||||||
|
amount = inventory.get(symbol, 0.0)
|
||||||
|
if amount >= threshold and symbol not in self.poison_alerts:
|
||||||
|
self.poison_alerts.add(symbol)
|
||||||
|
LOGGER.warning("Poison level high: %s inventory %.2e exceeds %.2e", symbol, amount, threshold)
|
||||||
|
|||||||
@@ -27,6 +27,29 @@ def _default_state_path() -> Path:
|
|||||||
return Path(custom) if custom else Path("artifacts/last_state.json")
|
return Path(custom) if custom else Path("artifacts/last_state.json")
|
||||||
|
|
||||||
|
|
||||||
|
def _poison_log_path() -> Path:
|
||||||
|
custom = os.getenv("FISSION_POISON_LOG")
|
||||||
|
return Path(custom) if custom else Path("artifacts/poisons.json")
|
||||||
|
|
||||||
|
|
||||||
|
def _write_poison_log(path: Path, state: PlantState | None, snapshots: list[dict[str, float]] | None = None) -> None:
|
||||||
|
if state is None and snapshots:
|
||||||
|
latest = snapshots[-1]
|
||||||
|
inventory = latest.get("products", {})
|
||||||
|
particles = latest.get("particles", {})
|
||||||
|
time_elapsed = latest.get("time_elapsed", 0.0)
|
||||||
|
elif state is not None:
|
||||||
|
inventory = state.core.fission_product_inventory
|
||||||
|
particles = state.core.emitted_particles
|
||||||
|
time_elapsed = state.time_elapsed
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
payload = {"time_elapsed": time_elapsed, "products": inventory, "particles": particles}
|
||||||
|
path.write_text(json.dumps(payload, indent=2))
|
||||||
|
LOGGER.info("Wrote poison snapshot to %s", path)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ReactorSimulation:
|
class ReactorSimulation:
|
||||||
reactor: Reactor
|
reactor: Reactor
|
||||||
@@ -113,6 +136,7 @@ def main() -> None:
|
|||||||
snapshots = sim.log()
|
snapshots = sim.log()
|
||||||
LOGGER.info("Captured %d snapshots", len(snapshots))
|
LOGGER.info("Captured %d snapshots", len(snapshots))
|
||||||
print(json.dumps(snapshots[-5:], indent=2))
|
print(json.dumps(snapshots[-5:], indent=2))
|
||||||
|
_write_poison_log(_poison_log_path(), sim.last_state, snapshots)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
sim.stop()
|
sim.stop()
|
||||||
LOGGER.warning("Simulation interrupted by user")
|
LOGGER.warning("Simulation interrupted by user")
|
||||||
|
|||||||
@@ -16,11 +16,21 @@ class CoreState:
|
|||||||
reactivity_margin: float # delta rho
|
reactivity_margin: float # delta rho
|
||||||
power_output_mw: float # MW thermal
|
power_output_mw: float # MW thermal
|
||||||
burnup: float # fraction of fuel consumed
|
burnup: float # fraction of fuel consumed
|
||||||
|
fission_product_inventory: dict[str, float] = field(default_factory=dict)
|
||||||
|
emitted_particles: dict[str, float] = field(default_factory=dict)
|
||||||
|
|
||||||
def update_burnup(self, dt: float) -> None:
|
def update_burnup(self, dt: float) -> None:
|
||||||
produced_energy_mwh = self.power_output_mw * (dt / 3600.0)
|
produced_energy_mwh = self.power_output_mw * (dt / 3600.0)
|
||||||
self.burnup = clamp(self.burnup + produced_energy_mwh * 1e-5, 0.0, 0.99)
|
self.burnup = clamp(self.burnup + produced_energy_mwh * 1e-5, 0.0, 0.99)
|
||||||
|
|
||||||
|
def add_products(self, products: dict[str, float]) -> None:
|
||||||
|
for element, amount in products.items():
|
||||||
|
self.fission_product_inventory[element] = self.fission_product_inventory.get(element, 0.0) + amount
|
||||||
|
|
||||||
|
def add_emitted_particles(self, particles: dict[str, float]) -> None:
|
||||||
|
for name, amount in particles.items():
|
||||||
|
self.emitted_particles[name] = self.emitted_particles.get(name, 0.0) + amount
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class CoolantLoopState:
|
class CoolantLoopState:
|
||||||
@@ -61,6 +71,8 @@ class PlantState:
|
|||||||
"primary_outlet_temp": self.primary_loop.temperature_out,
|
"primary_outlet_temp": self.primary_loop.temperature_out,
|
||||||
"secondary_pressure": self.secondary_loop.pressure,
|
"secondary_pressure": self.secondary_loop.pressure,
|
||||||
"turbine_electric": self.total_electrical_output(),
|
"turbine_electric": self.total_electrical_output(),
|
||||||
|
"products": self.core.fission_product_inventory,
|
||||||
|
"particles": self.core.emitted_particles,
|
||||||
}
|
}
|
||||||
|
|
||||||
def total_electrical_output(self) -> float:
|
def total_electrical_output(self) -> float:
|
||||||
@@ -71,6 +83,9 @@ class PlantState:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: dict) -> "PlantState":
|
def from_dict(cls, data: dict) -> "PlantState":
|
||||||
|
core_blob = dict(data["core"])
|
||||||
|
inventory = core_blob.pop("fission_product_inventory", {})
|
||||||
|
particles = core_blob.pop("emitted_particles", {})
|
||||||
turbines_blob = data.get("turbines")
|
turbines_blob = data.get("turbines")
|
||||||
if turbines_blob is None:
|
if turbines_blob is None:
|
||||||
# Compatibility with previous single-turbine snapshots.
|
# Compatibility with previous single-turbine snapshots.
|
||||||
@@ -78,7 +93,7 @@ class PlantState:
|
|||||||
turbines_blob = [old_turbine] if old_turbine else []
|
turbines_blob = [old_turbine] if old_turbine else []
|
||||||
turbines = [TurbineState(**t) for t in turbines_blob]
|
turbines = [TurbineState(**t) for t in turbines_blob]
|
||||||
return cls(
|
return cls(
|
||||||
core=CoreState(**data["core"]),
|
core=CoreState(**core_blob, fission_product_inventory=inventory, emitted_particles=particles),
|
||||||
primary_loop=CoolantLoopState(**data["primary_loop"]),
|
primary_loop=CoolantLoopState(**data["primary_loop"]),
|
||||||
secondary_loop=CoolantLoopState(**data["secondary_loop"]),
|
secondary_loop=CoolantLoopState(**data["secondary_loop"]),
|
||||||
turbines=turbines,
|
turbines=turbines,
|
||||||
|
|||||||
@@ -36,8 +36,12 @@ class ThermalSolver:
|
|||||||
def step_core(self, core: CoreState, primary: CoolantLoopState, power_mw: float, dt: float) -> None:
|
def step_core(self, core: CoreState, primary: CoolantLoopState, power_mw: float, dt: float) -> None:
|
||||||
temp_rise = temperature_rise(power_mw, primary.mass_flow_rate)
|
temp_rise = temperature_rise(power_mw, primary.mass_flow_rate)
|
||||||
primary.temperature_out = primary.temperature_in + temp_rise
|
primary.temperature_out = primary.temperature_in + temp_rise
|
||||||
core.fuel_temperature += 0.1 * (power_mw - temp_rise) * dt
|
# Fuel heats from any power not immediately convected away, and cools toward the primary outlet.
|
||||||
core.fuel_temperature = min(core.fuel_temperature, constants.MAX_CORE_TEMPERATURE)
|
heating = 0.005 * max(0.0, power_mw - temp_rise) * dt
|
||||||
|
cooling = 0.05 * 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.
|
||||||
|
core.fuel_temperature = min(max(primary.temperature_out, core.fuel_temperature), constants.MAX_CORE_TEMPERATURE)
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
"Primary loop: flow=%.0f kg/s temp_out=%.1fK core_temp=%.1fK",
|
"Primary loop: flow=%.0f kg/s temp_out=%.1fK core_temp=%.1fK",
|
||||||
primary.mass_flow_rate,
|
primary.mass_flow_rate,
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class SteamGenerator:
|
|||||||
class Turbine:
|
class Turbine:
|
||||||
generator_efficiency: float = constants.GENERATOR_EFFICIENCY
|
generator_efficiency: float = constants.GENERATOR_EFFICIENCY
|
||||||
mechanical_efficiency: float = constants.STEAM_TURBINE_EFFICIENCY
|
mechanical_efficiency: float = constants.STEAM_TURBINE_EFFICIENCY
|
||||||
|
rated_output_mw: float = 400.0 # cap per unit electrical output
|
||||||
|
|
||||||
def step(
|
def step(
|
||||||
self,
|
self,
|
||||||
@@ -37,6 +38,9 @@ class Turbine:
|
|||||||
available_power = max(steam_power_mw, (enthalpy * mass_flow / 1_000.0) / 1_000.0)
|
available_power = max(steam_power_mw, (enthalpy * mass_flow / 1_000.0) / 1_000.0)
|
||||||
shaft_power_mw = available_power * self.mechanical_efficiency
|
shaft_power_mw = available_power * self.mechanical_efficiency
|
||||||
electrical = shaft_power_mw * self.generator_efficiency
|
electrical = shaft_power_mw * self.generator_efficiency
|
||||||
|
if electrical > self.rated_output_mw:
|
||||||
|
electrical = self.rated_output_mw
|
||||||
|
shaft_power_mw = electrical / max(1e-6, self.generator_efficiency)
|
||||||
condenser_temp = max(305.0, loop.temperature_in - 20.0)
|
condenser_temp = max(305.0, loop.temperature_in - 20.0)
|
||||||
state.steam_enthalpy = enthalpy
|
state.steam_enthalpy = enthalpy
|
||||||
state.shaft_power_mw = shaft_power_mw
|
state.shaft_power_mw = shaft_power_mw
|
||||||
|
|||||||
54
tests/test_atomic.py
Normal file
54
tests/test_atomic.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
from reactor_sim.atomic import AtomicPhysics, make_atom
|
||||||
|
|
||||||
|
|
||||||
|
def test_alpha_decay_reduces_mass_and_charge():
|
||||||
|
physics = AtomicPhysics(seed=1)
|
||||||
|
parent = make_atom(92, 143)
|
||||||
|
event = physics.alpha_decay(parent)
|
||||||
|
daughter, alpha = event.products
|
||||||
|
assert daughter.protons == 90
|
||||||
|
assert daughter.neutrons == 141
|
||||||
|
assert alpha.protons == 2 and alpha.neutrons == 2
|
||||||
|
assert "alpha" in event.emitted_particles
|
||||||
|
assert event.energy_mev > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_beta_minus_conserves_mass_number():
|
||||||
|
physics = AtomicPhysics(seed=2)
|
||||||
|
parent = make_atom(26, 30)
|
||||||
|
event = physics.beta_minus_decay(parent)
|
||||||
|
(daughter,) = event.products
|
||||||
|
assert daughter.mass_number == parent.mass_number
|
||||||
|
assert daughter.protons == parent.protons + 1
|
||||||
|
assert "beta-" in event.emitted_particles
|
||||||
|
|
||||||
|
|
||||||
|
def test_beta_plus_and_electron_capture_lower_proton_count():
|
||||||
|
physics = AtomicPhysics(seed=3)
|
||||||
|
parent = make_atom(15, 16)
|
||||||
|
beta_plus = physics.beta_plus_decay(parent)
|
||||||
|
capture = physics.electron_capture(parent)
|
||||||
|
assert beta_plus.products[0].protons == parent.protons - 1
|
||||||
|
assert capture.products[0].protons == parent.protons - 1
|
||||||
|
assert "beta+" in beta_plus.emitted_particles
|
||||||
|
assert "gamma" in capture.emitted_particles
|
||||||
|
|
||||||
|
|
||||||
|
def test_gamma_emission_keeps_nuclide_same():
|
||||||
|
physics = AtomicPhysics(seed=4)
|
||||||
|
parent = make_atom(8, 8)
|
||||||
|
event = physics.gamma_emission(parent, energy_mev=0.3)
|
||||||
|
(product,) = event.products
|
||||||
|
assert product == parent
|
||||||
|
assert "gamma" in event.emitted_particles
|
||||||
|
assert event.energy_mev == 0.3
|
||||||
|
|
||||||
|
|
||||||
|
def test_spontaneous_fission_creates_two_fragments():
|
||||||
|
physics = AtomicPhysics(seed=5)
|
||||||
|
parent = make_atom(92, 143)
|
||||||
|
event = physics.spontaneous_fission(parent)
|
||||||
|
assert len(event.products) == 2
|
||||||
|
total_mass = sum(atom.mass_number for atom in event.products)
|
||||||
|
assert total_mass <= parent.mass_number
|
||||||
|
assert event.energy_mev > 0
|
||||||
40
tests/test_fuel.py
Normal file
40
tests/test_fuel.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
from reactor_sim.atomic import AtomicPhysics
|
||||||
|
from reactor_sim.fuel import FuelAssembly
|
||||||
|
from reactor_sim.state import CoreState
|
||||||
|
|
||||||
|
|
||||||
|
def _core_state(
|
||||||
|
temp: float = 600.0,
|
||||||
|
flux: float = 1e6,
|
||||||
|
burnup: float = 0.05,
|
||||||
|
power: float = 100.0,
|
||||||
|
) -> CoreState:
|
||||||
|
return CoreState(
|
||||||
|
fuel_temperature=temp,
|
||||||
|
neutron_flux=flux,
|
||||||
|
reactivity_margin=0.0,
|
||||||
|
power_output_mw=power,
|
||||||
|
burnup=burnup,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_decay_reaction_power_positive_and_capped():
|
||||||
|
physics = AtomicPhysics(seed=7)
|
||||||
|
fuel = FuelAssembly(enrichment=0.045, mass_kg=80_000.0, atomic_physics=physics)
|
||||||
|
state = _core_state()
|
||||||
|
power = fuel.decay_reaction_power(state)
|
||||||
|
assert power > 0
|
||||||
|
# Should stay under 10% of current power_output_mw.
|
||||||
|
assert power <= state.power_output_mw * 0.1 + 1e-6
|
||||||
|
|
||||||
|
|
||||||
|
def test_decay_reaction_power_scales_with_burnup_and_flux():
|
||||||
|
physics = AtomicPhysics(seed=9)
|
||||||
|
fuel = FuelAssembly(enrichment=0.045, mass_kg=80_000.0, atomic_physics=physics)
|
||||||
|
low = _core_state(burnup=0.01, flux=1e5, power=50.0)
|
||||||
|
high = _core_state(burnup=0.5, flux=5e6, power=200.0)
|
||||||
|
low_power = fuel.decay_reaction_power(low)
|
||||||
|
high_power = fuel.decay_reaction_power(high)
|
||||||
|
assert high_power > low_power
|
||||||
|
# Still capped to a reasonable fraction of the prevailing power.
|
||||||
|
assert high_power <= high.power_output_mw * 0.1 + 1e-6
|
||||||
Reference in New Issue
Block a user