File size: 11,945 Bytes
02d27c4
 
 
 
 
 
 
 
 
f34dc51
 
 
 
 
02d27c4
 
 
f34dc51
 
 
 
 
 
 
 
02d27c4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f34dc51
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
02d27c4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
"""Fleet extensions (Phase 2, partial data) and roadmap codes (Phase 3).

Phase-2 codes have on-disk data (traces / physics engine) but no trained ML
surrogate yet β€” status PARTIAL, predict() raises with a clear pointer.
Phase-3 codes are ROADMAP placeholders.  KECON is the one exception: a real,
generic, firewalled techno-economics calculator (no Kronos numbers, ever).
"""
from __future__ import annotations

import os
from pathlib import Path

import numpy as np

from .. import register
from ..base import Surrogate, Prediction

# real on-disk engineering data (reduced fidelity; full sims are the upgrade path)
_RESEARCH = Path(os.environ.get(
    "KODEX_RESEARCH_ROOT",
    "/Users/pford/Desktop/Kronos Fusion Energy/01 - RESEARCH & DATA/01 - Research, Data & Codes"))
_CD_SCAN = _RESEARCH / "KRONOS_PAPERS_2026-07-31" / "breeder" / "d1_cd_search.csv"
_DIV_THERMAL = _RESEARCH / "BREEDER - Phase 2" / "H9 - exhaust divertor engineering" / "h9_target_thermal.csv"
_SEEDING = _RESEARCH / "BREEDER - Phase 2" / "H9 - exhaust divertor engineering" / "h9_seeding.csv"


def _roadmap(cls_name, brand, func, phase, status, note, retired_by, gates=(), source=""):
    def _pred(self, x):
        raise NotImplementedError(
            f"{brand} is {status} β€” {note}. "
            f"{'Data/engine on disk: ' + source if source else 'No ML data on disk yet.'}")
    def _bench(self):
        return {"member": brand, "status": status,
                "sourced": source or "n/a", "note": note}
    return register(type(cls_name, (Surrogate,), {
        "name": brand, "function": func, "phase": phase, "status": status,
        "note": note, "retired_by": retired_by, "gates": tuple(gates),
        "provenance": "SIM" if status == "PARTIAL" else "n/a",
        "_predict": _pred, "benchmark": _bench,
    }))


# KBURN/KISO/KPATH -> members/plant.py; KBREED/KFLUX -> members/neutronics.py;
# KFORGE/KPILOT -> members/capstone.py; KDRIVE/KLAW/KGEN/KFUEL/KSEEK/KBENCH -> members/advanced.py.
# KHEAT/KEDGE/KRAD below are real reduced-fidelity surrogates on on-disk engineering scans
# (full RF/NBI ray-tracing and SOLPS-ITER/EIRENE edge sims are the fidelity upgrades).


@register
class KHEAT(Surrogate):
    name = "KHEAT"; function = "heating&CD"; phase = 2; status = "BUILT"
    provenance = "SIM"
    retired_by = "full RF/NBI ray-tracing (GENRAY/TORAY/NUBEAM)"
    real_codes = ("current-drive design scan",)
    gates = ("H10",)
    note = ("heating & current-drive actuator-response surrogate β€” fast RandomForest over a "
            "3888-point current-drive design scan; predicts driven current I_cd from the RF/NBI "
            "drive parameters + plasma state. Reduced CD model; ray-tracing = fidelity upgrade")
    _FEATS = ["gamma_cd", "P_cd", "ne", "R0", "B0", "Ti0", "beta_N"]

    def _fit(self):
        if getattr(self, "_m", None) is not None:
            return self._m
        import pandas as pd
        from sklearn.ensemble import RandomForestRegressor
        from sklearn.metrics import r2_score
        from .. import uq
        d = pd.read_csv(_CD_SCAN)[self._FEATS + ["I_cd"]].dropna()
        X = d[self._FEATS].to_numpy(float); y = d["I_cd"].to_numpy(float)
        rng = np.random.default_rng(uq.SEED); p = rng.permutation(len(y)); X, y = X[p], y[p]
        ntr = int(0.7 * len(y))
        m = RandomForestRegressor(n_estimators=200, random_state=0).fit(X[:ntr], y[:ntr])
        self._r2 = float(r2_score(y[ntr:], m.predict(X[ntr:]))); self._n = len(y)
        self._m = m
        return m

    def _predict(self, x):
        m = self._fit()
        return Prediction(float(m.predict(np.atleast_2d(np.asarray(x, float)))[0]), None, True,
                          note="driven current I_cd (MA) from [gamma_cd,P_cd,ne,R0,B0,Ti0,beta_N]")

    def benchmark(self):
        self._fit()
        return {"member": self.name,
                "live_cd_surrogate": {
                    "source": "d1_cd_search.csv (3888-point current-drive design scan)",
                    "features": self._FEATS, "target": "I_cd (driven current, MA)",
                    "r2_vs_scan": round(self._r2, 3), "n_samples": self._n,
                    "verdict": (f"Fast surrogate of the current-drive scan: predicts driven current from "
                                f"RF/NBI drive + plasma params, R2={self._r2:.3f} ({self._n} configs). Reduced "
                                f"CD model β€” GENRAY/TORAY/NUBEAM ray-tracing is the fidelity upgrade.")},
                "caveat": "engineering CD scan (not full ray-tracing); feeds KAIROS heating control"}


@register
class KEDGE(Surrogate):
    name = "KEDGE"; function = "edge-transport"; phase = 3; status = "BUILT"
    provenance = "SIM"
    retired_by = "SOLPS-ITER / EIRENE edge campaign"
    real_codes = ("divertor target-thermal scan",)
    gates = ("H9",)
    note = ("divertor / edge heat-flux -> target-thermal surrogate β€” maps divertor heat flux to "
            "target surface temperature + material limits (W / CuCrZr) from the H9 exhaust scan. "
            "Reduced 0-D thermal model; SOLPS-ITER/EIRENE = fidelity upgrade")

    def _fit(self):
        if getattr(self, "_coef", None) is not None:
            return
        import pandas as pd
        from sklearn.metrics import r2_score
        df = pd.read_csv(_DIV_THERMAL).dropna(subset=["q_MWm2", "T_w_surf_C"])
        q = df["q_MWm2"].to_numpy(float); T = df["T_w_surf_C"].to_numpy(float)
        self._coef = np.polyfit(q, T, 2)
        self._r2 = float(r2_score(T, np.polyval(self._coef, q))); self._n = len(q)
        bad = df[df["ok_CuCrZr"] == False] if "ok_CuCrZr" in df else df.iloc[0:0]
        self._q_limit = float(bad["q_MWm2"].min()) if len(bad) else float(q.max())

    def _predict(self, x):
        self._fit()
        q = float(np.ravel(np.asarray(x, float))[0])
        return Prediction(float(np.polyval(self._coef, q)), None, q <= self._q_limit,
                          note=f"target surface temp (C) at q={q} MW/m^2; in_domain = under CuCrZr limit")

    def benchmark(self):
        self._fit()
        return {"member": self.name,
                "live_divertor_thermal": {
                    "source": "h9_target_thermal.csv (H9 exhaust/divertor engineering scan)",
                    "map": "divertor heat flux q [MW/m^2] -> target surface temperature [C]",
                    "r2_fit": round(self._r2, 3), "n_samples": self._n,
                    "max_safe_q_MWm2_CuCrZr": self._q_limit,
                    "verdict": (f"Divertor target-thermal surrogate: q->T_surf fit R2={self._r2:.3f} over "
                                f"{self._n} points; CuCrZr material limit at q~{self._q_limit} MW/m2. Reduced "
                                f"0-D thermal model β€” SOLPS-ITER/EIRENE edge campaign is the fidelity upgrade.")},
                "caveat": "0-D target-thermal scan, not a full 2-D edge-transport solve"}


@register
class KRAD(Surrogate):
    name = "KRAD"; function = "radiation-control"; phase = 3; status = "BUILT"
    provenance = "SIM"
    retired_by = "impurity transport (SOLPS + impurity) + radiation control"
    real_codes = ("impurity-seeding radiation scan",)
    gates = ("H9",)
    note = ("impurity-seeding radiation-control evaluator β€” maps a seeded impurity + radiated-power "
            "fraction to core Zeff penalty and P_rad from the H9 seeding scan. Small scan; full "
            "impurity-transport (SOLPS + impurity) = fidelity upgrade")

    def _table(self):
        import pandas as pd
        return pd.read_csv(_SEEDING)

    def _predict(self, x):
        df = self._table()
        imp = x.get("impurity", "N") if isinstance(x, dict) else "N"
        row = df[df["impurity"] == imp]
        r = (row.iloc[0] if len(row) else df.iloc[0])
        return Prediction({"impurity": str(r["impurity"]), "P_rad_MW": float(r["P_rad_MW"]),
                           "dZeff_core": [float(r["dZeff_core_lo"]), float(r["dZeff_core_hi"])],
                           "c_div_frac_pct": float(r["c_div_frac_pct"])},
                          None, True, note="radiated power + core-Zeff penalty for the seeded impurity")

    def benchmark(self):
        df = self._table()
        best = df.loc[df["dZeff_core_hi"].idxmin()]
        return {"member": self.name,
                "live_impurity_seeding": {
                    "source": "h9_seeding.csv (H9 impurity-seeding scan)",
                    "n_impurities": int(df["impurity"].nunique()),
                    "impurities": sorted(df["impurity"].unique().tolist()),
                    "radiated_fraction": float(df["f_rad"].iloc[0]),
                    "least_core_dilution_impurity": str(best["impurity"]),
                    "verdict": (f"Impurity-seeding radiation evaluator over {int(df['impurity'].nunique())} "
                                f"impurities at f_rad={df['f_rad'].iloc[0]}: '{best['impurity']}' gives the least "
                                f"core Zeff penalty (dZeff_hi={best['dZeff_core_hi']}). Small scan β€” full "
                                f"impurity-transport (SOLPS) is the fidelity upgrade.")},
                "caveat": "small seeding scan (few impurities); reduced 0-D radiation model"}


@register
class KECON(Surrogate):
    """Generic, open fusion techno-economics β€” FINANCIAL FIREWALL.

    A community LCOE / cost-of-electricity calculator that runs ONLY on the
    caller's inputs.  It ships with ZERO Kronos cost/price/per-unit/valuation/
    funding data, and refuses to run without user-supplied inputs.  Kronos's own
    economics stay confidential and never enter this code."""
    name = "KECON"; function = "techno-economics"; phase = 3; status = "BUILT"
    provenance = "n/a"
    retired_by = "detailed engineering cost model (never public)"
    real_codes = ("open LCOE methodology",)
    note = ("GENERIC open techno-economics (LCOE) on the USER's inputs β€” "
            "FINANCIAL FIREWALL: no Kronos numbers, ever")

    #: hard firewall β€” no Kronos-specific defaults may ever be added here
    KRONOS_NUMBERS = None

    def _predict(self, x):
        """x = dict with the USER's own inputs:
            capex, opex_per_yr, annual_generation_MWh, discount_rate, lifetime_yr
        Returns generic LCOE ($/MWh).  Refuses to run without user inputs."""
        if not isinstance(x, dict) or not x:
            raise ValueError(
                "KECON requires the caller's own inputs (capex, opex_per_yr, "
                "annual_generation_MWh, discount_rate, lifetime_yr). "
                "It carries NO Kronos financial data.")
        capex = float(x["capex"]); opex = float(x["opex_per_yr"])
        gen = float(x["annual_generation_MWh"]); r = float(x["discount_rate"])
        n = int(x["lifetime_yr"])
        crf = r * (1 + r) ** n / ((1 + r) ** n - 1) if r > 0 else 1.0 / n
        cap_component = capex * crf / gen
        opex_component = opex / gen
        lcoe = cap_component + opex_component
        return Prediction({"lcoe_per_MWh": round(lcoe, 2),
                           "capital_component_per_MWh": round(cap_component, 2),
                           "opex_component_per_MWh": round(opex_component, 2),
                           "capital_recovery_factor": round(crf, 4)},
                          None, True,
                          note="generic LCOE $/MWh + breakdown from USER inputs (no Kronos data)")

    def benchmark(self):
        # a worked generic example (illustrative inputs β€” NOT Kronos numbers)
        demo = self.predict({"capex": 5e9, "opex_per_yr": 1e8,
                             "annual_generation_MWh": 3e6, "discount_rate": 0.07, "lifetime_yr": 30})
        return {"member": self.name,
                "live_worked_example": demo.y,
                "firewall": "generic community calculator; NO Kronos cost/price/valuation/funding, ever",
                "note": "founder-approved public, generic-only; inputs above are illustrative placeholders"}