Add rod banks, xenon kinetics, and document feature set
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
@@ -22,30 +22,41 @@ class ControlSystem:
|
||||
setpoint_mw: float = 3_000.0
|
||||
rod_fraction: float = 0.5
|
||||
manual_control: bool = False
|
||||
rod_banks: list[float] = field(default_factory=lambda: [0.5, 0.5, 0.5])
|
||||
rod_target: float = 0.5
|
||||
|
||||
def update_rods(self, state: CoreState, dt: float) -> float:
|
||||
if not self.rod_banks or len(self.rod_banks) != len(constants.CONTROL_ROD_BANK_WEIGHTS):
|
||||
self.rod_banks = [self.rod_fraction] * len(constants.CONTROL_ROD_BANK_WEIGHTS)
|
||||
# Keep manual tweaks in sync with the target.
|
||||
self.rod_target = clamp(self.rod_target, 0.0, 0.95)
|
||||
if self.manual_control:
|
||||
if abs(self.rod_fraction - self.effective_insertion()) > 1e-6:
|
||||
self.rod_target = clamp(self.rod_fraction, 0.0, 0.95)
|
||||
self._advance_banks(self.rod_target, dt)
|
||||
return self.rod_fraction
|
||||
error = (state.power_output_mw - self.setpoint_mw) / self.setpoint_mw
|
||||
# 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)
|
||||
previous = self.rod_fraction
|
||||
self.rod_fraction = clamp(self.rod_fraction + adjustment, 0.0, 0.95)
|
||||
LOGGER.debug("Control rods %.3f -> %.3f (error=%.3f)", previous, self.rod_fraction, error)
|
||||
self.rod_target = clamp(self.rod_target + adjustment, 0.0, 0.95)
|
||||
self._advance_banks(self.rod_target, dt)
|
||||
LOGGER.debug("Control rod target=%.3f (error=%.3f)", self.rod_target, error)
|
||||
return self.rod_fraction
|
||||
|
||||
def set_rods(self, fraction: float) -> float:
|
||||
previous = self.rod_fraction
|
||||
self.rod_fraction = clamp(fraction, 0.0, 0.95)
|
||||
LOGGER.info("Manual rod set %.3f -> %.3f", previous, self.rod_fraction)
|
||||
return self.rod_fraction
|
||||
self.rod_target = clamp(fraction, 0.0, 0.95)
|
||||
self._advance_banks(self.rod_target, 0.0)
|
||||
LOGGER.info("Manual rod target set to %.3f", self.rod_target)
|
||||
return self.rod_target
|
||||
|
||||
def increment_rods(self, delta: float) -> float:
|
||||
return self.set_rods(self.rod_fraction + delta)
|
||||
|
||||
def scram(self) -> float:
|
||||
self.rod_fraction = 0.95
|
||||
self.rod_target = 0.95
|
||||
self.rod_banks = [0.95 for _ in self.rod_banks]
|
||||
self._sync_fraction()
|
||||
LOGGER.warning("SCRAM: rods fully inserted")
|
||||
return self.rod_fraction
|
||||
|
||||
@@ -91,6 +102,32 @@ class ControlSystem:
|
||||
)
|
||||
return demand
|
||||
|
||||
def effective_insertion(self) -> float:
|
||||
if not self.rod_banks:
|
||||
return self.rod_fraction
|
||||
weights = constants.CONTROL_ROD_BANK_WEIGHTS
|
||||
total = sum(weights)
|
||||
effective = sum(w * b for w, b in zip(weights, self.rod_banks)) / total
|
||||
return clamp(effective, 0.0, 0.95)
|
||||
|
||||
def _advance_banks(self, target: float, dt: float) -> None:
|
||||
speed = constants.CONTROL_ROD_SPEED * dt
|
||||
new_banks: list[float] = []
|
||||
for idx, pos in enumerate(self.rod_banks):
|
||||
direction = 1 if target > pos else -1
|
||||
step = direction * speed
|
||||
updated = clamp(pos + step, 0.0, 0.95)
|
||||
# Avoid overshoot
|
||||
if (direction > 0 and updated > target) or (direction < 0 and updated < target):
|
||||
updated = target
|
||||
new_banks.append(updated)
|
||||
self.rod_banks = new_banks
|
||||
self._sync_fraction()
|
||||
|
||||
def _sync_fraction(self) -> None:
|
||||
self.rod_fraction = self.effective_insertion()
|
||||
|
||||
|
||||
def save_state(
|
||||
self,
|
||||
filepath: str,
|
||||
@@ -103,6 +140,8 @@ class ControlSystem:
|
||||
"setpoint_mw": self.setpoint_mw,
|
||||
"rod_fraction": self.rod_fraction,
|
||||
"manual_control": self.manual_control,
|
||||
"rod_banks": self.rod_banks,
|
||||
"rod_target": self.rod_target,
|
||||
},
|
||||
"plant": plant_state.to_dict(),
|
||||
"metadata": metadata or {},
|
||||
@@ -121,6 +160,9 @@ class ControlSystem:
|
||||
self.setpoint_mw = control.get("setpoint_mw", self.setpoint_mw)
|
||||
self.rod_fraction = control.get("rod_fraction", self.rod_fraction)
|
||||
self.manual_control = control.get("manual_control", self.manual_control)
|
||||
self.rod_banks = control.get("rod_banks", self.rod_banks) or self.rod_banks
|
||||
self.rod_target = control.get("rod_target", self.rod_fraction)
|
||||
self._sync_fraction()
|
||||
plant = PlantState.from_dict(data["plant"])
|
||||
LOGGER.info("Loaded plant state from %s", path)
|
||||
return plant, data.get("metadata", {}), data.get("health")
|
||||
|
||||
Reference in New Issue
Block a user