feat: add reactor control persistence and tests

This commit is contained in:
Andrii Prokhorov
2025-11-21 17:11:00 +02:00
commit cc7fba4e7a
43 changed files with 1435 additions and 0 deletions

110
src/reactor_sim/failures.py Normal file
View File

@@ -0,0 +1,110 @@
"""Wear and failure monitoring for reactor components."""
from __future__ import annotations
from dataclasses import dataclass, asdict
import logging
from typing import Dict, Iterable, List
from . import constants
from .state import PlantState
LOGGER = logging.getLogger(__name__)
@dataclass
class ComponentHealth:
name: str
integrity: float = 1.0
failed: bool = False
def degrade(self, amount: float) -> None:
if self.failed:
return
self.integrity = max(0.0, self.integrity - amount)
if self.integrity <= 0.0:
self.fail()
def fail(self) -> None:
if not self.failed:
self.failed = True
LOGGER.error("Component %s has failed", self.name)
def snapshot(self) -> dict:
return asdict(self)
@classmethod
def from_snapshot(cls, data: dict) -> "ComponentHealth":
return cls(**data)
class HealthMonitor:
"""Tracks component wear and signals failures."""
def __init__(self) -> None:
self.components: Dict[str, ComponentHealth] = {
"core": ComponentHealth("core"),
"primary_pump": ComponentHealth("primary_pump"),
"secondary_pump": ComponentHealth("secondary_pump"),
"turbine": ComponentHealth("turbine"),
}
self.failure_log: list[str] = []
def component(self, name: str) -> ComponentHealth:
return self.components[name]
def evaluate(
self,
state: PlantState,
primary_active: bool,
secondary_active: bool,
turbine_active: bool,
dt: float,
) -> List[str]:
events: list[str] = []
core = self.component("core")
core_temp = state.core.fuel_temperature
temp_stress = max(0.0, (core_temp - 900.0) / (constants.MAX_CORE_TEMPERATURE - 900.0))
base_degrade = 0.0001 * dt
core.degrade(base_degrade + temp_stress * 0.01 * dt)
if primary_active:
primary_flow = state.primary_loop.mass_flow_rate
flow_ratio = 0.0 if primary_flow <= 0 else min(1.0, primary_flow / 18_000.0)
self.component("primary_pump").degrade((0.0002 + (1 - flow_ratio) * 0.005) * dt)
else:
self.component("primary_pump").degrade(0.0005 * dt)
if secondary_active:
secondary_flow = state.secondary_loop.mass_flow_rate
flow_ratio = 0.0 if secondary_flow <= 0 else min(1.0, secondary_flow / 16_000.0)
self.component("secondary_pump").degrade((0.0002 + (1 - flow_ratio) * 0.004) * dt)
else:
self.component("secondary_pump").degrade(0.0005 * dt)
if turbine_active:
electrical = state.turbine.electrical_output_mw
load_ratio = (
0.0
if state.turbine.load_demand_mw <= 0
else min(1.0, electrical / max(1e-6, state.turbine.load_demand_mw))
)
stress = 0.0002 + abs(1 - load_ratio) * 0.003
self.component("turbine").degrade(stress * dt)
else:
self.component("turbine").degrade(0.0001 * dt)
for name, component in self.components.items():
if component.failed and name not in self.failure_log:
events.append(name)
self.failure_log.append(name)
return events
def snapshot(self) -> dict:
return {name: comp.snapshot() for name, comp in self.components.items()}
def load_snapshot(self, data: dict) -> None:
for name, comp_data in data.items():
if name in self.components:
self.components[name] = ComponentHealth.from_snapshot(comp_data)