2sweep_loop100_and_network.py — exercise the two newest RadCluster_2_1 features
5 (1) ½⟨111⟩ → ⟨100⟩ loop conversion — content-weighted f₁₁₁(T) crossover.
6 Sweeps T ∈ {450, 500, 550} °C with the design-note-calibrated crossover
7 T_star_conv_C = 340 °C, so the Dudarev unary driving force turns on and
8 the large loops convert as T rises (⟨100⟩ becomes dominant at high T —
9 the experimental high-T regime).
11 (2) Loop → network-dislocation loss — dynamic ρ_net(dose).
12 Enables LOOP_NETWORK_LOSS and drives ρ_net through run_adaptive's
13 operator-split so it rises ~1 order of magnitude and saturates near the
14 experimental EUROFER network density (~5×10¹⁴ m⁻²).
16Run (from module root, needs a built solver.exe):
17 python codes/Python_Testing/sweep_loop100_and_network.py [--quick] [--T 500]
22from pathlib
import Path
26ROOT = Path(__file__).resolve().parents[2]
27sys.path.insert(0,
str(ROOT))
29 sys.stdout.reconfigure(encoding=
"utf-8")
73def run_one(T_C, t_end, n_points, I=1000, V=1000, i_mobile=20, v_mobile=5,
74 rtol=1e-5, network_loss=True, conversion=True):
75 """One temperature: conversion ON + (optionally) network loss, via
76 run_adaptive so ρ_net advances between segments. Returns the results dict.
78 Uses the efficient full_system + bin_moment + Woodbury path (the original
79 16-min config) so loops reach n≈200 — the size the network-loss channel
80 needs to activate (the capture switch needs χ·d_loop ≳ network spacing)."""
81 _s = sys.stdout, sys.stderr
82 sys.stdout = sys.stderr = io.StringIO()
85 I=I, V=V, solver_mode=
"full_system",
86 equations=
"bin_moment", cascade=
"fission", C_floor=1e-25,
87 he_kinetics=
"quasi_steady_state",
88 i_mobile=i_mobile, v_mobile=v_mobile)
89 re = sim.input_data.reactions
90 re[
"T"] =
float(T_C) + 273.15
92 re[
"T_star_conv_C"] = T_STAR_CONV
93 re[
"E_a0_conv"] = E_A0_CONV
94 re[
"gamma_a_conv"] = GAMMA_A_CONV
97 re[
"i_discrete"] = 50; re[
"v_discrete"] = 20
98 re[
"I_bin"] = 20; re[
"V_bin"] = 20
99 re[
"shape_function"] =
"linear"
101 re[
"LOOP_NETWORK_LOSS"] = 1
102 re[
"loop_net_chi"] = LOOP_NET_CHI
103 re[
"loop_net_w_c"] = LOOP_NET_WC
104 re[
"loop_net_K_rec"] = LOOP_NET_KREC
105 re[
"loop_net_n_inc"] = LOOP_NET_NINC
106 re[
"loop_net_rho_max"] = RHO_MAX
107 sim.input_data._calculate_derived()
109 cfg = {
"t_span": (1e-6, t_end),
"n_points": n_points,
"log_time":
True,
110 "rtol": rtol,
"atol": 1e-20,
"timeout_s": 2400,
111 "solver_method": {
"linsol":
"gmres",
"preconditioner":
"Woodbury",
112 "window_width": 10,
"window_pad": 20,
113 "concentration_threshold": 1e-22},
114 "loop_conversion": int(conversion)}
115 r = sim.run_adaptive(solver_config=cfg, save_output=
False,
116 points_per_segment=max(4, n_points // 10),
117 max_doublings=2, boundary_threshold=0.05)
119 sys.stdout, sys.stderr = _s
123def run_conv(T_C, t_end=3e6, n_points=60, I=200, i_mobile=10, v_mobile=3,
124 rtol=3e-3, timeout_s=4500, progress_path=None):
125 """f_111(T) CROSSOVER point: loop conversion ON, network loss OFF, on a
126 reduced active_window/discrete grid so the (stiff) conversion runs complete.
127 f_100 is a growth-vs-conversion competition gated by the dF cutoff (T_star):
128 large loops convert fast at high T (gamma_a tuned) -> f_100->1, but stay
129 partial at low T where the dF cutoff caps the convertible size.
131 If ``progress_path`` is given, per-SEGMENT f_100 / loop concentrations are
132 appended live (via a wrapper on sim._merge_results) so an in-flight run's
133 progress is visible without waiting for the point to finish."""
134 _s = sys.stdout, sys.stderr
135 sys.stdout = sys.stderr = io.StringIO()
138 I=I, V=I, solver_mode=
"active_window",
139 physics_option=
"full_CD_fission", C_floor=1e-25,
140 he_kinetics=
"quasi_steady_state",
141 i_mobile=i_mobile, v_mobile=v_mobile)
142 re = sim.input_data.reactions
143 re[
"T"] =
float(T_C) + 273.15
144 re[
"T_star_conv_C"] = T_STAR_CONV
145 re[
"E_a0_conv"] = E_A0_CONV
146 re[
"gamma_a_conv"] = GAMMA_A_CONV
153 if DH2_CONV
is not None:
154 re[
"dH2_conv"] = DH2_CONV
155 if PHI_MAX_JUNC
is not None:
156 re[
"phi_max_junc"] = PHI_MAX_JUNC
157 sim.input_data._calculate_derived(); sim.rebuild_rates()
161 if progress_path
is not None:
162 _orig_merge = sim._merge_results
163 def _logged_merge(acc, res, _o=_orig_merge, _T=T_C, _p=progress_path):
167 return float(np.asarray(a)[-1])
if a
is not None and len(a)
else d
168 with open(_p,
"a", encoding=
"utf-8")
as fh:
169 fh.write(f
" T={_T:3.0f}C dose={last('dose'):6.3f}dpa "
170 f
"f_100={1-last('f_111_loop',1.0):.4f} "
171 f
"N_111={last('N_loops_111'):.3e} "
172 f
"N_100={last('N_loops_100'):.3e} "
173 f
"mean_n111={last('mean_n_111'):.1f} "
174 f
"mean_n100={last('mean_n_100'):.1f}\n")
178 sim._merge_results = _logged_merge
180 cfg = {
"t_span": (1e-6, t_end),
"n_points": n_points,
"log_time":
True,
181 "rtol": rtol,
"atol": 1e-20,
"timeout_s": timeout_s,
182 "solver_method": {
"linsol":
"dense"},
183 "loop_conversion": 1}
184 r = sim.run_adaptive(solver_config=cfg, save_output=
False,
185 points_per_segment=5,
186 max_doublings=2, boundary_threshold=0.05)
188 sys.stdout, sys.stderr = _s
193 f111 =
float(np.asarray(r[
"f_111_loop"])[-1])
194 rho0 =
float(np.asarray(r[
"rho_net"])[0])
195 rho1 =
float(np.asarray(r[
"rho_net"])[-1])
196 dose =
float(np.asarray(r[
"dose"])[-1])
197 print(f
" T={T_C:3.0f}C dose={dose:5.2f}dpa | "
198 f
"f_111={f111:.4f} f_100={1-f111:.4f} | "
199 f
"rho_net {rho0:.2e} -> {rho1:.2e} (x{rho1/rho0:.1f}) "
200 f
"mean_n_100={float(np.asarray(r['mean_n_100'])[-1]):.1f}")
201 return dict(T_C=T_C, dose=dose, f111=f111, rho0=rho0, rho1=rho1)
204if __name__ ==
"__main__":
205 ap = argparse.ArgumentParser()
206 ap.add_argument(
"--quick", action=
"store_true",
207 help=
"fast probe: low dose, smaller domain")
208 ap.add_argument(
"--T", type=float, default=
None,
209 help=
"single temperature (°C) instead of the 3-point sweep")
210 ap.add_argument(
"--no-conv", action=
"store_true",
211 help=
"disable loop conversion (isolate/verify the ρ_net channel)")
212 ap.add_argument(
"--crossover", action=
"store_true",
213 help=
"f_111(T) crossover sweep: conversion ON, network OFF, "
214 "reduced grid (350/400/450/500/550 C)")
215 ap.add_argument(
"--progress", type=str, default=
None,
216 help=
"path to a live per-segment progress log file")
217 ap.add_argument(
"--gamma-a", type=float, default=
None,
218 help=
"override GAMMA_A_CONV (unary barrier size-slope)")
219 ap.add_argument(
"--e-a0", type=float, default=
None,
220 help=
"override E_A0_CONV (unary barrier offset)")
221 ap.add_argument(
"--t-end", type=float, default=
None,
222 help=
"override crossover t_end [s] (dose = t_end*1e-6 dpa)")
223 ap.add_argument(
"--t-star", type=float, default=
None,
224 help=
"override T_STAR_CONV (conversion crossover T, C)")
225 ap.add_argument(
"--timeout", type=float, default=
None,
226 help=
"override per-point solver timeout [s]")
227 ap.add_argument(
"--dh2", type=float, default=
None,
228 help=
"override dH2_conv [eV] — the Marian two-step gate. "
229 "Lower => P_success up => junction+absorption engage.")
230 ap.add_argument(
"--phi-max", type=float, default=
None,
231 help=
"override phi_max_junc (junction peak yield)")
232 args = ap.parse_args()
233 if args.gamma_a
is not None:
234 GAMMA_A_CONV = args.gamma_a
235 if args.e_a0
is not None:
236 E_A0_CONV = args.e_a0
237 if args.t_star
is not None:
238 T_STAR_CONV = args.t_star
239 if args.dh2
is not None:
241 if args.phi_max
is not None:
242 PHI_MAX_JUNC = args.phi_max
245 temps = ([args.T]
if args.T
is not None
246 else [350.0, 400.0, 450.0, 500.0, 550.0])
247 print(f
"f_111(T) crossover sweep: conversion ON, T_star={T_STAR_CONV}C, "
248 f
"E_a0={E_A0_CONV}eV, gamma_a={GAMMA_A_CONV}, network OFF, T={temps} C",
254 with open(args.progress,
"a", encoding=
"utf-8")
as fh:
255 fh.write(f
"\n=== T={T_C:.0f}C starting ===\n")
256 t0 = time.perf_counter()
259 _kw[
"t_end"] = args.t_end
261 _kw[
"timeout_s"] = args.timeout
262 r =
run_conv(T_C, progress_path=args.progress, **_kw)
263 dt = time.perf_counter() - t0
265 print(f
" T={T_C:3.0f}C FAILED", flush=
True);
continue
266 f111 =
float(np.asarray(r[
"f_111_loop"])[-1])
267 dose =
float(np.asarray(r[
"dose"])[-1])
268 print(f
" T={T_C:3.0f}C dose={dose:5.2f}dpa | f_100={1-f111:.4f} | "
269 f
"mean_n_100={float(np.asarray(r['mean_n_100'])[-1]):.1f} "
270 f
"mean_n_111={float(np.asarray(r['mean_n_111'])[-1]):.1f} (wall {dt:.0f}s)",
272 rows.append((T_C, 1 - f111))
273 print(
"\nf_111(T) crossover T[C] f_100")
274 for T_C, f100
in rows:
275 print(f
" {T_C:5.0f} {f100:.4f}")
280 t_end, n_points, I = 3e6, 60, 1000
282 t_end, n_points, I = 1e7, 80, 1000
284 temps = [args.T]
if args.T
is not None else [450.0, 500.0, 550.0]
285 print(f
"Sweep T={temps} °C (t_end={t_end:.0e}s ~ {t_end*1e-6:.0f} dpa, "
286 f
"I={I} bin_moment/full_system, T_star=340C, LOOP_NETWORK_LOSS on)")
290 t0 = time.perf_counter()
291 r =
run_one(T_C, t_end, n_points, I=I, V=I, conversion=
not args.no_conv)
292 dt = time.perf_counter() - t0
294 print(f
" T={T_C:3.0f}C FAILED (no results)")
298 print(f
" (wall {dt:.0f}s)")
301 print(
"\nSummary T[C] f_100 rho_net_final")
303 print(f
" {x['T_C']:5.0f} {1-x['f111']:.4f} {x['rho1']:.2e}")
run_one(T_C, t_end, n_points, I=1000, V=1000, i_mobile=20, v_mobile=5, rtol=1e-5, network_loss=True, conversion=True)
run_conv(T_C, t_end=3e6, n_points=60, I=200, i_mobile=10, v_mobile=3, rtol=3e-3, timeout_s=4500, progress_path=None)