kronos-ml / kronos_ml /members /control.py
Kronos Fusion Energy
KODEX — kronos-ml v0.1.0 (26 published surrogates)
02d27c4 verified
Raw History Blame Contribute Delete
4.53 kB
"""KAIROS — real-time closed-loop plasma control (MPC) + the safety clamp.
A finite-horizon receding-horizon controller (analytic per-axis QP — no external
solver) drives a reduced first-order plant to setpoint; every command is projected
onto the envelope by KGATE (the model-free clamp KAIROS supplies to the spine).
Includes the model-failure ("Exp-B") adversarial test: broken controllers emit
deliberately-unsafe commands and the clamp lets none escape.
"""
from __future__ import annotations
import time
import numpy as np
from .. import register, get
from ..base import Surrogate, Prediction
from .. import data, uq
# reduced first-order plant (from track5 plant_model): x_{k+1}=x+alpha*(u-x)+couple+noise
_ALPHA = np.array([0.20, 0.20, 0.10, 0.12, 0.05, 0.06])
_SIGMA = np.array([0.004, 0.004, 0.01, 0.01, 0.002, 0.02]) # per-step disturbance
_H = 8 # MPC horizon
_RHO = 0.02 # control-effort weight
def _mpc(x0, ref):
"""Analytic finite-horizon optimal constant-u command for the diagonal
first-order plant (minimises tracking over H steps + rho*effort)."""
x0 = np.asarray(x0, float); ref = np.asarray(ref, float)
k = np.arange(1, _H + 1)
u = np.zeros(6)
for i in range(6):
b = (1 - _ALPHA[i]) ** k # decay of the initial-condition term
c = 1 - b # coefficient on u
d = x0[i] * b - ref[i] # constant term
u[i] = -np.sum(c * d) / (np.sum(c * c) + _RHO * _H)
return u
def _plant_step(x, u_safe, rng):
x = np.asarray(x, float)
nxt = x + _ALPHA * (u_safe - x) + rng.normal(0, _SIGMA)
nxt[2] -= 0.015 * (u_safe[5] - x[5]) # Ip -> beta_N coupling
nxt[4] += 0.0012 * (u_safe[5] - x[5]) # Ip -> eps coupling
return nxt
@register
class KAIROS(Surrogate):
name = "KAIROS"; function = "CONTROL"; phase = 1; status = "BUILT"
provenance = "TWIN"
retired_by = "validated real-time plant controller (device-in-the-loop)"
real_codes = ("receding-horizon MPC",)
gates = ("AC-24", "AC-25")
note = ("real-time closed-loop MPC control (analytic finite-horizon QP); "
"supplies the model-free L4 clamp that KGATE enforces")
def _predict(self, x):
"""x = (state6, ref6) -> next command from the MPC, clamped by KGATE."""
state, ref = x
u = _mpc(state, ref)
safe, ok = get("KGATE").clamp(u)
in_dom = bool(np.allclose(safe, u, atol=1e-6))
return Prediction(safe, None, in_dom,
note="MPC step, clamped" if not in_dom else "MPC step, in envelope")
def benchmark(self):
rng = np.random.default_rng(uq.SEED)
clamp = get("KGATE").clamp
# (1) closed-loop tracking: drive from a start state to a setpoint
ref = np.array([2.2, 0.3, 3.0, 1.0, 0.2, 30.0])
x = np.array([2.0, 0.0, 2.0, 0.5, 0.1, 20.0])
for _ in range(200):
safe, _ = clamp(_mpc(x, ref)); x = _plant_step(x, safe, rng)
rng_scale = np.array([1, 1, 4.2, 3, 0.4, 47.21])
track_rms = float(np.sqrt(np.mean(((x - ref) / rng_scale) ** 2)))
# (2) Exp-B: broken controllers emit unsafe commands; clamp must catch all
from .spine import _LO, _HI
adversaries = {
"drift": lambda: _HI * (1 + 0.5 * rng.random(6)),
"edge_seeker": lambda: _HI + 5,
"nan_garbage": lambda: np.where(rng.random(6) < 0.3, np.nan, _HI * 3),
}
escapes = 0; steps = 0
for _, gen in adversaries.items():
for _ in range(50_000):
safe, _ = clamp(gen()); steps += 1
if not (np.all(safe >= _LO - 1e-9) and np.all(safe <= _HI + 1e-9)):
escapes += 1
# (3) real latency of an MPC step + clamp
t0 = time.perf_counter()
for _ in range(2000):
clamp(_mpc(x, ref))
us = (time.perf_counter() - t0) / 2000 * 1e6
return {"member": self.name,
"live_tracking_rms_frac": round(track_rms, 4),
"live_expB_model_failure": {"adversaries": list(adversaries),
"steps": steps, "escapes": escapes,
"catch_rate": 1.0 - escapes / steps},
"live_mpc_step_us": round(us, 1),
"sourced": {"file": "track5_control/clamp_activation_stats.csv",
"headline": "0 escapes / 150k steps / 3000 injected (AC-25); tracking <5%"}}