feat: add reactor control persistence and tests
This commit is contained in:
110
src/reactor_sim/failures.py
Normal file
110
src/reactor_sim/failures.py
Normal 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)
|
||||
|
||||
Reference in New Issue
Block a user