RadCluster
Graph-based cluster dynamics for irradiated materials
Loading...
Searching...
No Matches
test_two_layer_im5vm2.py
Go to the documentation of this file.
1"""
2Stage-4 validation — the two-layer RadCluster_2_1 on the im5vm2 case.
3
4This is the capstone regression test for the graph-based two-layer
5refactor. It reproduces the RadCluster_1_0 reference case
6
7 full_system / full_CD_fission, I = V = 1000, i_mobile = 5,
8 v_mobile = 2, T = 673 K, G = 1e-6 dpa/s, t in [1e-6, 1e-2] s
9
10and checks two things:
11
12 (A) STRUCTURE — that a RadClusterSimulation now carries the live
13 two-layer description: an abstract-core ReactionAdmissibilityGraph
14 instantiated by the EUROFER-97 material layer (Layer 1 + Layer 2).
15
16 (B) PHYSICS — that running through the restructured (core/ + materials/)
17 C++ solver still reproduces the locked post-Stage-2 baseline, i.e.
18 the refactor is behaviour-preserving.
19
20Reference values are the post-Stage-2 baseline (the G_He single-source
21fix is included by design); agreement to 1e-5 relative — comfortably
22above the ~5e-7 solver-tolerance noise — counts as a pass.
23
24Run: python codes/Python_Testing/test_two_layer_im5vm2.py
25Exit code 0 = PASS, 1 = FAIL.
26"""
27import sys
28from pathlib import Path
29
30ROOT = Path(r"d:/GitHub/RadCluster/radcluster_code")
31sys.path.insert(0, str(ROOT))
32
33from py_utils.core import ReactionAdmissibilityGraph, StateLayout, EdgeClass
34from py_utils.simulation import RadClusterSimulation
35
36# ── locked post-Stage-2 baseline (im5vm2 @ T=673) ────────────────────────────
37BASELINE = {
38 "C_SIA_tot": 1.49422818e20,
39 "C_VAC_tot": 2.37571338e20,
40 "C_He_tot": 1.58830031e13,
41 "mean_n_i": 7.11591710,
42 "mean_n_v": 3.09365893,
43 "swelling": 2.80334179e-9,
44}
45RTOL = 1e-5
46
47failures = []
48
49
50def check(name, ok, detail=""):
51 mark = "PASS" if ok else "FAIL"
52 print(f" [{mark}] {name}{(' -- ' + detail) if detail else ''}")
53 if not ok:
54 failures.append(name)
55
56
57print("=" * 72)
58print("Stage-4 validation: two-layer RadCluster_2_1 on the im5vm2 case")
59print("=" * 72)
60
61# ── build the simulation (constructs the two-layer RAG description) ───────────
63 I=1000, V=1000, solver_mode="full_system",
64 physics_option="full_CD_fission",
65 C_floor=1e-25, he_kinetics="quasi_steady_state",
66 i_mobile=5, v_mobile=2,
67)
68sim.input_data.reactions["T"] = 673.0
69sim.input_data._calculate_derived()
70sim.rebuild_rates()
71
72# ── (A) STRUCTURE checks ─────────────────────────────────────────────────────
73print("\n(A) Two-layer structure")
74rag = getattr(sim, "material_rag", None)
75layout = getattr(sim, "state_layout", None)
76
77check("material RAG present", isinstance(rag, ReactionAdmissibilityGraph))
78check("state layout present", isinstance(layout, StateLayout))
79
80if isinstance(rag, ReactionAdmissibilityGraph):
81 rag_ok = True
82 try:
83 rag.validate()
84 except Exception as exc: # noqa: BLE001
85 rag_ok = False
86 check("RAG validates", False, repr(exc))
87 if rag_ok:
88 check("RAG validates", True)
89 check("2 populations (1 SIA + 1 vacancy bulk)",
90 len(rag.populations) == 2,
91 f"got {len(rag.populations)}")
92 active = {ec for ec in EdgeClass if rag.edges_of_class(ec)}
93 expected = {EdgeClass.GROWTH, EdgeClass.SHRINKAGE, EdgeClass.DISSOCIATION,
94 EdgeClass.RECOMBINATION, EdgeClass.ANNIHILATION,
95 EdgeClass.COALESCENCE, EdgeClass.SOURCE, EdgeClass.SINK}
96 check("all P1-P6 edge classes present", expected <= active,
97 f"missing {expected - active}")
98 check("P7/P8 edges declared (growth>=3 or solute_trapping)",
99 len(rag.edges_of_class(EdgeClass.GROWTH)) >= 3 or
100 bool(rag.edges_of_class(EdgeClass.SOLUTE_TRAPPING)),
101 f"{len(rag.edges)} edges total")
102 print(f" {rag.summary()}")
103
104# ── (B) PHYSICS check — run and compare to the locked baseline ───────────────
105print("\n(B) Behaviour-preserving run")
106cfg = sim._default_solver_config()
107cfg["t_span"] = (1e-6, 0.01)
108cfg["n_points"] = 200
109cfg["log_time"] = True
110cfg["rtol"] = 1e-6
111cfg["atol"] = 1e-20
112cfg["solver_method"]["linsol"] = "gmres"
113
114results = sim.run_adaptive(solver_config=cfg, save_output=False,
115 max_doublings=0, points_per_segment=200)
116
117if results is None:
118 check("solver run completed", False, "run returned None")
119else:
120 check("solver run completed", True)
121
122 def last(key):
123 v = results.get(key)
124 try:
125 return float(v[-1])
126 except (TypeError, IndexError):
127 return float(v)
128
129 print(f" {'quantity':12s} {'baseline':>16s} {'this run':>16s} {'rel.diff':>11s}")
130 for key, ref in BASELINE.items():
131 val = last(key)
132 rel = abs(val - ref) / abs(ref)
133 print(f" {key:12s} {ref:16.6e} {val:16.6e} {rel:11.2e}")
134 check(f"{key} within {RTOL:g}", rel < RTOL, f"rel.diff {rel:.2e}")
135
136 # conservation diagnostics: finite and small (delta_He no longer NaN)
137 dfp, dhe = last("delta_FP"), last("delta_He")
138 import math
139 check("delta_FP finite and < 1e-3",
140 math.isfinite(dfp) and abs(dfp) < 1e-3, f"delta_FP={dfp:.2e}")
141 check("delta_He finite and < 1e-3 (NaN bug fixed)",
142 math.isfinite(dhe) and abs(dhe) < 1e-3, f"delta_He={dhe:.2e}")
143
144# ── verdict ──────────────────────────────────────────────────────────────────
145print("\n" + "=" * 72)
146if failures:
147 print(f"RESULT: FAIL ({len(failures)} check(s) failed: {failures})")
148 sys.exit(1)
149print("RESULT: PASS -- two-layer RadCluster_2_1 reproduces the im5vm2 "
150 "baseline\n and carries the abstract-core + EUROFER-97 RAG "
151 "description.")
152sys.exit(0)
check(name, ok, detail="")