RadCluster
Graph-based cluster dynamics for irradiated materials
Loading...
Searching...
No Matches
bias_study.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""
3Bias study — examines how dislocation bias (Z_i) and production bias (f_cl_i, f_cl_v)
4affect SIA and vacancy cluster size peaks, and plots the fundamental monomer fluxes.
5
6With the loop geometry correction (A_loop * n^{1/2} for n ≥ 4), the Z_i_loop bias
7creates differential SIA vs vacancy absorption that drives net loop growth.
8"""
9
10import sys, os, io, time, json
11from pathlib import Path
12import numpy as np
13import matplotlib
14matplotlib.use('Agg')
15import matplotlib.pyplot as plt
16
17MODULE_ROOT = Path(__file__).resolve().parent.parent.parent
18REPO_ROOT = MODULE_ROOT.parent
19for p in [str(REPO_ROOT), str(MODULE_ROOT)]:
20 if p not in sys.path:
21 sys.path.insert(0, p)
22
23import importlib
24import radcluster_code.py_utils.defect_production as _dp_mod
25import radcluster_code.py_utils.binding_energies as _be_mod
26import radcluster_code.py_utils.bin_moment_rates as _bmr
27import radcluster_code.py_utils.input_data as _inp_mod
28import radcluster_code.py_utils.reaction_rates as _rr_mod
29import radcluster_code.py_utils.rate_equations as _re_mod
30import radcluster_code.py_utils.cpp_bridge as _cb_mod
31import radcluster_code.py_utils.post_process as _pp_mod
32import radcluster_code.py_utils.simulation as _sim_mod
33for _m in [_dp_mod, _be_mod, _bmr, _inp_mod, _rr_mod, _re_mod,
34 _cb_mod, _pp_mod, _sim_mod]:
35 importlib.reload(_m)
36from radcluster_code.py_utils.simulation import RadClusterSimulation
37from radcluster_code.py_utils.defect_production import FISSION as _FISSION_DICT
38
39# ── Constants ───────────────────────────────────────────────────────────────
40Omega = 1.18e-29
41b_111 = 2.482e-10
42def n_to_d_nm(n):
43 return 2.0 * np.sqrt(np.maximum(n, 1) * Omega / (np.pi * b_111)) * 1e9
44
45# ── Solver config ───────────────────────────────────────────────────────────
46I_SIM, V_SIM = 3000, 3000
47SOLVER_CONFIG = {
48 't_span': (1e-6, 1e5), # → 0.1 dpa at G=1e-6
49 'n_points': 150,
50 'log_time': True,
51 'rtol': 1e-5,
52 'atol': 1e-20,
53 'solver_method': {
54 'linsol': 'gmres',
55 'window_width': 50,
56 'concentration_threshold': 1e-18, 'window_pad': 10,
57 }
58}
59
60BASE_PARAMS = {
61 'E_m_1D': 0.4, 'i_mobile': 10, 'L_hat': 71.8,
62 'c_C': 1.94e-4, 'E_b_C_SIA': 0.65,
63 'rho_d': 1e13,
64 'shape_function': 'linear',
65}
66
67
68def run_case(name, overrides):
69 """Run one simulation and return full results + rate info."""
70 params = dict(BASE_PARAMS)
71 params.update(overrides)
72
73 i_mob = int(params.get('i_mobile', 10))
74
75 # Patch FISSION dict for production parameters
76 fission_backup = dict(_FISSION_DICT)
77 for key in ['eta', 'f_cl_i', 'f_cl_v', 's_i', 's_v', 'i_cascade', 'v_cascade']:
78 if key in params:
79 _FISSION_DICT[key] = params[key]
80
81 saved = sys.stdout, sys.stderr
82 sys.stdout = sys.stderr = io.StringIO()
83 try:
84 sim = RadClusterSimulation(
85 I=I_SIM, V=V_SIM,
86 solver_mode='active_window', physics_option='full_CD_fission',
87 C_floor=1e-25, he_kinetics='quasi_steady_state',
88 i_mobile=i_mob, v_mobile=5,
89 )
90 inp = sim.input_data
91 for key, val in params.items():
92 placed = False
93 for d in [inp.production_fission, inp.production_fusion,
94 inp.diffusion, inp.reactions,
95 inp.energetics, inp.dissociation]:
96 if key in d:
97 d[key] = val
98 placed = True
99 if not placed:
100 inp.reactions[key] = val
101 inp.diffusion['i_mobile'] = i_mob
102 inp.reactions['i_mobile'] = i_mob
103 inp.reactions['i_discrete'] = i_mob
104 inp.reactions['v_discrete'] = 5
105 inp.reactions['I_bin'] = 0
106 inp.reactions['V_bin'] = 0
107 inp._calculate_derived()
108 sim.rebuild_rates()
109
110 results = sim.run_adaptive(
111 solver_config=SOLVER_CONFIG, save_output=False,
112 progress_callback=None, boundary_threshold=0.1,
113 max_doublings=0, points_per_segment=10,
114 )
115 finally:
116 sys.stdout, sys.stderr = saved
117 _FISSION_DICT.update(fission_backup)
118
119 if results is None:
120 return None
121
122 d = inp.derived
123 rr = sim.reaction_rates
124
125 # Extract time-resolved monomer concentrations and fluxes
126 y_all = results['y']
127 n_t = len(results['t'])
128 omega_i = d['omega_i_eff']
129 omega_v = d['omega_v_eff']
130 s2m = 1.0 / d['Omega']
131
132 ci1_vs_t = np.maximum(y_all[0, :], 0.0) # at.frac
133 cv1_vs_t = np.maximum(y_all[I_SIM, :], 0.0) # at.frac
134 flux_i = omega_i * ci1_vs_t * s2m # SIA monomer flux [m^-3 s^-1]
135 flux_v = omega_v * cv1_vs_t * s2m # VAC monomer flux [m^-3 s^-1]
136
137 # SIA peak at 0.1 dpa
138 dose = results['dose']
139 idx_01 = np.argmin(np.abs(dose - 0.1))
140 c_i_01 = np.maximum(y_all[0:I_SIM, idx_01], 0.0) * s2m
141 c_v_01 = np.maximum(y_all[I_SIM:2*I_SIM, idx_01], 0.0) * s2m
142
143 # SIA peak (exclude n=1)
144 ipeak = np.argmax(c_i_01[1:]) + 1
145 sia_peak_n = ipeak + 1
146 sia_peak_d = n_to_d_nm(sia_peak_n)
147 sia_peak_c = c_i_01[ipeak]
148 sia_mean_d = n_to_d_nm(results['mean_n_i'][idx_01])
149
150 # VAC peak (exclude m=1)
151 vpeak = np.argmax(c_v_01[1:]) + 1
152 vac_peak_n = vpeak + 1
153 vac_peak_d = n_to_d_nm(vac_peak_n)
154
155 # Growth rate diagnostic
156 K_g = rr.K_SIA_grow
157 K_s = rr.K_SIA_shrink
158 ci1_01 = ci1_vs_t[idx_01]
159 cv1_01 = cv1_vs_t[idx_01]
160 v_net_100 = K_g[99] * ci1_01 - K_s[99] * cv1_01 if I_SIM > 100 else 0
161 ratio = (K_g[99] * ci1_01) / max(K_s[99] * cv1_01, 1e-300) if I_SIM > 100 else 0
162
163 return {
164 'name': name,
165 'params': params,
166 'dose': dose,
167 't': results['t'],
168 'flux_i': flux_i,
169 'flux_v': flux_v,
170 'ci1_m3': ci1_vs_t * s2m,
171 'cv1_m3': cv1_vs_t * s2m,
172 'sia_peak_n': sia_peak_n,
173 'sia_peak_d': sia_peak_d,
174 'sia_peak_c': sia_peak_c,
175 'sia_mean_d': sia_mean_d,
176 'vac_peak_n': vac_peak_n,
177 'vac_peak_d': vac_peak_d,
178 'N_loops': results['N_loops'][idx_01],
179 'N_voids': results['N_voids'][idx_01],
180 'swelling': results['swelling'][idx_01] * 100,
181 'v_net_100': v_net_100,
182 'growth_ratio': ratio,
183 'c_i_01': c_i_01,
184 'c_v_01': c_v_01,
185 'omega_i': omega_i,
186 'omega_v': omega_v,
187 }
188
189
190# ── Case definitions ────────────────────────────────────────────────────────
191# Explore: (1) dislocation bias Z_i, (2) production bias f_cl_i/f_cl_v
192CASES = {
193 # ── Baseline ─────────────────────────────────────────────────────────
194 'baseline': {
195 'eta': 0.30, 'f_cl_i': 0.30, 'f_cl_v': 0.25,
196 'Z_i': 1.05, 'Z_ii': 1.2,
197 },
198
199 # ── Dislocation bias scan ────────────────────────────────────────────
200 'Zi_1.10': {
201 'eta': 0.30, 'f_cl_i': 0.30, 'f_cl_v': 0.25,
202 'Z_i': 1.10, 'Z_ii': 1.2,
203 },
204 'Zi_1.15': {
205 'eta': 0.30, 'f_cl_i': 0.30, 'f_cl_v': 0.25,
206 'Z_i': 1.15, 'Z_ii': 1.2,
207 },
208 'Zi_1.20': {
209 'eta': 0.30, 'f_cl_i': 0.30, 'f_cl_v': 0.25,
210 'Z_i': 1.20, 'Z_ii': 1.2,
211 },
212
213 # ── Production bias scan (increase f_cl_i → more SIA in clusters → more free VAC)
214 'fcli_0.50': {
215 'eta': 0.30, 'f_cl_i': 0.50, 'f_cl_v': 0.25,
216 'Z_i': 1.05, 'Z_ii': 1.2,
217 },
218 'fclv_0.40': {
219 'eta': 0.30, 'f_cl_i': 0.30, 'f_cl_v': 0.40,
220 'Z_i': 1.05, 'Z_ii': 1.2,
221 },
222
223 # ── Combined: strong dislocation bias + production bias ─────────────
224 'Zi1.15_fcli0.50': {
225 'eta': 0.30, 'f_cl_i': 0.50, 'f_cl_v': 0.25,
226 'Z_i': 1.15, 'Z_ii': 1.2,
227 },
228 'Zi1.15_fclv0.40': {
229 'eta': 0.30, 'f_cl_i': 0.30, 'f_cl_v': 0.40,
230 'Z_i': 1.15, 'Z_ii': 1.2,
231 },
232}
233
234
235# ═══════════════════════════════════════════════════════════════════════════
236# MAIN
237# ═══════════════════════════════════════════════════════════════════════════
238if __name__ == '__main__':
239 out_dir = Path(__file__).parent / 'bias_study_output'
240 out_dir.mkdir(exist_ok=True)
241
242 print('=' * 70)
243 print(' BIAS STUDY — Loop Geometry Corrected')
244 print(' (A_loop * n^{1/2} * Z_i for SIA; A_loop * n^{1/2} for vacancy)')
245 print('=' * 70)
246
247 all_results = []
248 t0 = time.time()
249
250 for i, (name, overrides) in enumerate(CASES.items()):
251 print(f'\n[{i+1}/{len(CASES)}] {name}', flush=True)
252 for k, v in overrides.items():
253 print(f' {k} = {v}')
254 t1 = time.time()
255 try:
256 res = run_case(name, overrides)
257 except Exception as e:
258 print(f' FAILED: {e}')
259 import traceback; traceback.print_exc()
260 res = None
261 elapsed = time.time() - t1
262
263 if res:
264 all_results.append(res)
265 print(f' Done ({elapsed:.0f}s) | SIA peak: n={res["sia_peak_n"]}, '
266 f'd={res["sia_peak_d"]:.2f} nm | VAC peak: n={res["vac_peak_n"]} | '
267 f'v_net(100)={res["v_net_100"]:.3e} | ratio={res["growth_ratio"]:.3f}')
268
269 total = time.time() - t0
270 print(f'\nTotal time: {total:.0f}s ({total/60:.1f} min)')
271
272 # ═══════════════════════════════════════════════════════════════════════
273 # PLOT 1: Fundamental monomer fluxes ω_i·c_i1 and ω_v·c_v1 vs dose
274 # ═══════════════════════════════════════════════════════════════════════
275 fig1, axes1 = plt.subplots(2, 1, figsize=(10, 8), sharex=True)
276
277 for res in all_results:
278 dose = res['dose']
279 mask = dose > 1e-8
280 axes1[0].loglog(dose[mask], res['flux_i'][mask], label=res['name'])
281 axes1[1].loglog(dose[mask], res['flux_v'][mask], label=res['name'])
282
283 axes1[0].set_ylabel(r'$\omega_i^{eff} \cdot C_{i1}$ [m$^{-3}$ s$^{-1}$]')
284 axes1[0].set_title('SIA monomer flux (drives loop growth)')
285 axes1[0].legend(fontsize=7, ncol=2)
286 axes1[0].grid(True, alpha=0.3)
287
288 axes1[1].set_ylabel(r'$\omega_v^{eff} \cdot C_{v1}$ [m$^{-3}$ s$^{-1}$]')
289 axes1[1].set_xlabel('Dose [dpa]')
290 axes1[1].set_title('Vacancy monomer flux (drives loop shrinkage)')
291 axes1[1].legend(fontsize=7, ncol=2)
292 axes1[1].grid(True, alpha=0.3)
293
294 fig1.tight_layout()
295 fig1.savefig(str(out_dir / 'monomer_fluxes.png'), dpi=150)
296 print(f'\nSaved: {out_dir / "monomer_fluxes.png"}')
297
298 # ═══════════════════════════════════════════════════════════════════════
299 # PLOT 2: Flux ratio ω_i·c_i1 / ω_v·c_v1 vs dose
300 # ═══════════════════════════════════════════════════════════════════════
301 fig2, ax2 = plt.subplots(figsize=(10, 5))
302 for res in all_results:
303 dose = res['dose']
304 mask = dose > 1e-8
305 ratio = res['flux_i'][mask] / np.maximum(res['flux_v'][mask], 1e-30)
306 ax2.semilogx(dose[mask], ratio, label=res['name'])
307 ax2.axhline(y=1.0, color='red', linestyle='--', linewidth=1.5, label='ratio = 1 (growth/shrink balance)')
308 ax2.set_xlabel('Dose [dpa]')
309 ax2.set_ylabel(r'$\omega_i C_{i1}\;/\;\omega_v C_{v1}$')
310 ax2.set_title('Monomer flux ratio — above 1 means net SIA loop growth')
311 ax2.legend(fontsize=7, ncol=2)
312 ax2.grid(True, alpha=0.3)
313 ax2.set_ylim(0, max(2.0, ax2.get_ylim()[1]))
314 fig2.tight_layout()
315 fig2.savefig(str(out_dir / 'flux_ratio.png'), dpi=150)
316 print(f'Saved: {out_dir / "flux_ratio.png"}')
317
318 # ═══════════════════════════════════════════════════════════════════════
319 # PLOT 3: SIA size distributions at 0.1 dpa
320 # ═══════════════════════════════════════════════════════════════════════
321 fig3, (ax3a, ax3b) = plt.subplots(1, 2, figsize=(14, 6))
322 ns = np.arange(1, I_SIM + 1)
323 d_nm = n_to_d_nm(ns)
324
325 for res in all_results:
326 c_i = res['c_i_01']
327 mask = c_i > 1e5
328 ax3a.semilogy(d_nm[mask], c_i[mask], label=res['name'], linewidth=1.2)
329 ax3b.plot(d_nm[mask], c_i[mask], label=res['name'], linewidth=1.2)
330
331 ax3a.set_xlabel('SIA loop diameter [nm]')
332 ax3a.set_ylabel('Concentration [m$^{-3}$]')
333 ax3a.set_title('SIA size distribution at 0.1 dpa (log scale)')
334 ax3a.set_xlim(0, 15)
335 ax3a.legend(fontsize=7)
336 ax3a.grid(True, alpha=0.3)
337
338 ax3b.set_xlabel('SIA loop diameter [nm]')
339 ax3b.set_ylabel('Concentration [m$^{-3}$]')
340 ax3b.set_title('SIA size distribution at 0.1 dpa (linear scale)')
341 ax3b.set_xlim(0, 10)
342 ax3b.legend(fontsize=7)
343 ax3b.grid(True, alpha=0.3)
344
345 fig3.tight_layout()
346 fig3.savefig(str(out_dir / 'sia_distributions.png'), dpi=150)
347 print(f'Saved: {out_dir / "sia_distributions.png"}')
348
349 # ═══════════════════════════════════════════════════════════════════════
350 # Summary table
351 # ═══════════════════════════════════════════════════════════════════════
352 print('\n' + '=' * 100)
353 print(f'{"Case":<22} {"Z_i":>5} {"f_cl_i":>7} {"f_cl_v":>7} '
354 f'{"SIA pk d":>9} {"SIA pk n":>9} {"VAC pk n":>9} '
355 f'{"v_net(100)":>11} {"ratio":>7} {"N_loops":>10} {"Swell%":>8}')
356 print('-' * 100)
357 for r in all_results:
358 p = r['params']
359 print(f'{r["name"]:<22} {p["Z_i"]:>5.2f} {p["f_cl_i"]:>7.2f} {p["f_cl_v"]:>7.2f} '
360 f'{r["sia_peak_d"]:>9.2f} {r["sia_peak_n"]:>9d} {r["vac_peak_n"]:>9d} '
361 f'{r["v_net_100"]:>11.3e} {r["growth_ratio"]:>7.3f} '
362 f'{r["N_loops"]:>10.2e} {r["swelling"]:>8.4f}')
363 print('=' * 100)
364
365 plt.close('all')
366 print('\nDone.')
run_case(name, overrides)
Definition bias_study.py:68