RadCluster
Graph-based cluster dynamics for irradiated materials
Loading...
Searching...
No Matches
check_loop_conversion_integration.py
Go to the documentation of this file.
1"""
2check_loop_conversion_integration.py — Phase-4 integration test.
3
4Two parts:
5
6 1. Structural — the full EUROFER-97 RAG builds with the ½⟨111⟩/⟨100⟩ split:
7 3 populations, the three conversion edges present (unary INTER_POPULATION,
8 junction + absorption COALESCENCE), and a SIA100 state-layout block.
9
10 2. Conservation — all SIA-content-conserving edges (GROWTH, self-coalescence,
11 junction, unary transformation, ⟨100⟩ absorption) run *together* through the
12 GraphWalker on a minimal two-SIA-population RAG and conserve the total SIA
13 content Σ_n n·c_n^(111) + Σ_m m·c_m^(100). This catches edge-interaction
14 bugs that the per-edge Phase-1 tests cannot.
15
16(The full EUROFER walker is not exercised directly: its P8 SOLUTE_TRAPPING edge
17is owned by the He mean-loading reduction, not the discrete reference walker.)
18
19Run: python codes/Python_Testing/check_loop_conversion_integration.py
20"""
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.core.cluster_identifier import Polarity, Population
30from py_utils.core.rag import ReactionAdmissibilityGraph, Edge
31from py_utils.core.edge_classes import EdgeClass
32from py_utils.core.state_layout import StateLayout
33from py_utils.core.graph_walker import GraphWalker
34from py_utils.input_data import InputData
35from py_utils.reaction_rates import ReactionRates
36from py_utils.materials import build_eurofer_rag
37
38ATOL = 1e-12
39_fails = 0
40
41
42def check(name, cond, *info):
43 global _fails
44 tag = "PASS" if cond else "FAIL"
45 if not cond:
46 _fails += 1
47 print(f" {tag} {name} {info if info else ''}")
48
49
50# ── Part 1: structural ────────────────────────────────────────────────────────
52 inp = InputData(I=1000, V=1000, physics_option="full_CD_fission")
53 inp.diffusion["i_mobile"] = 5
54 inp.derived["i_mobile"] = 5
55 inp.diffusion["v_mobile"] = 2
56 inp.derived["v_mobile"] = 2
57 inp.reactions["C_floor"] = 1e-25
58 inp.reactions["he_kinetics"] = "quasi_steady_state"
59 rr = ReactionRates(inp)
60 rag, layout = build_eurofer_rag(inp, rr, equations="discrete", cascade="fission")
61
62 pop_names = {(p.polarity.label, p.name) for p in rag.populations}
63 check("3 populations incl. bulk-111 / bulk-100",
64 ("sia", "bulk-111") in pop_names and ("sia", "bulk-100") in pop_names
65 and len(rag.populations) == 3, sorted(pop_names))
66
67 labels = {e.label: e for e in rag.edges}
68 check("unary INTER_POPULATION edge present",
69 labels.get("loop_111to100_unary") is not None and
70 labels["loop_111to100_unary"].edge_class is EdgeClass.INTER_POPULATION)
71 junc = labels.get("SIA111_junction")
72 check("junction COALESCENCE -> bulk-100",
73 junc is not None and junc.edge_class is EdgeClass.COALESCENCE and
74 junc.product_population is not None and
75 junc.product_population.name == "bulk-100")
76 absb = labels.get("SIA100_absorb")
77 check("absorption COALESCENCE on bulk-100",
78 absb is not None and absb.edge_class is EdgeClass.COALESCENCE and
79 absb.population.name == "bulk-100" and
80 absb.partner_population.name == "bulk-111")
81 check("SIA100 state block present", layout.has("SIA100"),
82 layout.block("SIA100").length if layout.has("SIA100") else None)
83 rag.validate()
84 check("rag.validate() passes", True)
85
86
87# ── Part 2: composed-conservation on a minimal RAG ────────────────────────────
89 N = 8
90 rag = ReactionAdmissibilityGraph("mini")
91 p111 = rag.add_population(Population("bulk-111", Polarity.SIA, n_min=1, mobile_max=N))
92 p100 = rag.add_population(Population("bulk-100", Polarity.SIA, n_min=1, mobile_max=0))
93 rag.set_monomer_population(p111)
94
95 # Synthetic kernels (values irrelevant to conservation, only structure is).
96 Kg = np.full(N, 0.05) # GROWTH (absorb I_1)
97 Kfull = np.full((N, N), 0.02) # ½⟨111⟩ collision kernel
98 phi = np.zeros((N, N)); phi[2:, 2:] = 0.4 # junction branch (comparable, n≥3)
99 Gamma = np.zeros(N); Gamma[2:5] = 0.03 # unary, small loops only
100 Kabs = np.full((N, N), 0.015) # ⟨100⟩ absorbs ½⟨111⟩
101 rag.register_kernel("Kg", Kg)
102 rag.register_kernel("K_self", (1.0 - phi) * Kfull)
103 rag.register_kernel("K_junc", phi * Kfull)
104 rag.register_kernel("Gamma", Gamma)
105 rag.register_kernel("Kabs", Kabs)
106
107 rag.add_edge(Edge(EdgeClass.GROWTH, "grow111", p111, kernel="Kg"))
108 rag.add_edge(Edge(EdgeClass.COALESCENCE, "self111", p111, kernel="K_self"))
109 rag.add_edge(Edge(EdgeClass.COALESCENCE, "junction", p111,
110 kernel="K_junc", product_population=p100))
111 rag.add_edge(Edge(EdgeClass.INTER_POPULATION, "unary", p111,
112 kernel="Gamma", product_population=p100))
113 rag.add_edge(Edge(EdgeClass.COALESCENCE, "absorb", p100,
114 kernel="Kabs", partner_population=p111))
115
116 layout = StateLayout()
117 layout.add_discrete("SIA111", N, population=p111)
118 layout.add_discrete("SIA100", N, population=p100)
119 layout.freeze()
120 gw = GraphWalker(rag, layout, boundary="reflection")
121
122 # Representative non-negative state on both ladders.
123 y = np.zeros(2 * N)
124 y[layout.slice("SIA111")] = np.array([1.0, 0.6, 0.4, 0.25, 0.15, 0.08, 0.03, 0.0])
125 y[layout.slice("SIA100")] = np.array([0.0, 0.0, 0.2, 0.15, 0.1, 0.05, 0.0, 0.0])
126 dydt = gw.assemble(0.0, y)
127
128 sizes = np.arange(1, N + 1)
129 d111 = dydt[layout.slice("SIA111")]
130 d100 = dydt[layout.slice("SIA100")]
131 content_rate = float(np.dot(sizes, d111) + np.dot(sizes, d100))
132 check("all SIA-conserving edges compose -> total content flat",
133 abs(content_rate) < ATOL, content_rate)
134 check("dydt finite", np.all(np.isfinite(dydt)))
135 # The conversion edges must move content 111 -> 100 (100 strictly gains).
136 check("bulk-100 net content increases",
137 float(np.dot(sizes, d100)) > ATOL, float(np.dot(sizes, d100)))
138
139
140def main():
141 print("--- Part 1: structural ---")
143 print("--- Part 2: composed conservation ---")
145 print(f"\n{'ALL PASS' if not _fails else str(_fails) + ' FAILED'}")
146 return 1 if _fails else 0
147
148
149if __name__ == "__main__":
150 sys.exit(main())