RadCluster
Graph-based cluster dynamics for irradiated materials
Loading...
Searching...
No Matches
check_loop_conversion_conservation.py
Go to the documentation of this file.
1"""
2check_loop_conversion_conservation.py — Phase-7e end-to-end conservation test.
3
4Runs the C++ solver with ½⟨111⟩→<100> conversion ON and verifies that the
5Frenkel-pair conservation diagnostics hold once the SIA100 inventory and the
6<100> shrink annihilation are folded into the accounting:
7
8 * dFP (swelling identity) → <= 1e-6 in the accumulated regime,
9 * dFP_sia (per-species SIA) → < 1e-3 in the accumulated regime,
10 * total SIA inventory S_I = Σ n (c_n^{111} + c_n^{100}) is finite and
11 below the cumulative cascade production η·G·t.
12
13Conservation is checked at t >= 1e-3 s: the very first log-time point (t≈1e-6)
14carries a benign startup transient (production ≈ floor), so the per-step `max`
15is not a meaningful gate — the diagnostics decay to ~1e-6 as dose accumulates.
16
17Requires a built solver.exe. Run:
18 python codes/Python_Testing/check_loop_conversion_conservation.py
19"""
20import io
21import sys
22from pathlib import Path
23
24import numpy as np
25
26ROOT = Path(__file__).resolve().parents[2]
27sys.path.insert(0, str(ROOT))
28
29from py_utils.simulation import RadClusterSimulation
30
31
32def _run(loop_conversion):
33 _s = sys.stdout, sys.stderr
34 sys.stdout = sys.stderr = io.StringIO()
35 try:
37 I=200, V=200, solver_mode="full_system",
38 physics_option="full_CD_fission", C_floor=1e-25,
39 he_kinetics="quasi_steady_state", i_mobile=5, v_mobile=2)
40 # Drive conversion hard so the ⟨100⟩ block is genuinely populated and the
41 # conservation accounting is exercised: high T + low unary barrier.
42 if loop_conversion:
43 sim.input_data.reactions["T"] = 700.0 + 273.15
44 sim.input_data.reactions["E_a0_conv"] = 0.8
45 sim.input_data._calculate_derived()
46 sim.rebuild_rates()
47 cfg = {"t_span": (1e-6, 1e0), "n_points": 8, "log_time": True,
48 "rtol": 1e-7, "atol": 1e-28,
49 "solver_method": {"linsol": "dense"}}
50 if loop_conversion:
51 cfg["loop_conversion"] = 1
52 r = sim.run_adaptive(solver_config=cfg, save_output=False)
53 finally:
54 sys.stdout, sys.stderr = _s
55 return r
56
57
58def main():
59 fails = 0
60
61 def check(name, cond, *info):
62 nonlocal fails
63 if not cond:
64 fails += 1
65 print(f" {'PASS' if cond else 'FAIL'} {name} {info if info else ''}")
66
67 r = _run(loop_conversion=1)
68 t = np.asarray(r["t"])
69 dfp = np.asarray(r["delta_FP"])
70 dfps = np.asarray(r["delta_FP_sia"])
71 late = t >= 1e-3 # accumulated regime (skip startup)
72
73 check("conversion ON produced <100>",
74 "y_sia100" in r and float(np.max(r["y_sia100"])) > 0.0)
75 check("dFP < 1e-3 in accumulated regime (t>=1e-3)",
76 np.all(dfp[late] < 1e-3), float(np.max(dfp[late])))
77 check("dFP_sia < 1e-3 in accumulated regime",
78 np.all(dfps[late] < 1e-3), float(np.max(dfps[late])))
79 check("dFP converges to <= 1e-6 by final time",
80 dfp[-1] < 1e-6, float(dfp[-1]))
81 check("dFP decreases after the startup transient",
82 dfp[-1] < dfp[1], (float(dfp[1]), float(dfp[-1])))
83
84 # total SIA inventory finite and below cumulative production η·G·t
85 y = r["y"]; y100 = r["y_sia100"]; sz = np.arange(1, 201)
86 S_I = float(np.dot(sz, np.maximum(y[:200, -1], 0.0))
87 + np.dot(sz, np.maximum(y100[:, -1], 0.0)))
88 eta, G = 0.30, 1e-6
89 prod = eta * G * t[-1]
90 check("total SIA inventory finite & < cumulative production",
91 np.isfinite(S_I) and 0.0 < S_I < prod, (S_I, prod))
92
93 print(f"\n{'ALL PASS' if not fails else str(fails) + ' FAILED'}")
94 print(f" [info] final dFP={dfp[-1]:.2e} dFP_sia={dfps[-1]:.2e} "
95 f"S_I={S_I:.3e} (cascade prod {prod:.3e})")
96 return 1 if fails else 0
97
98
99if __name__ == "__main__":
100 sys.exit(main())