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.
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.
10import sys, os, io, time, json
11from pathlib
import Path
15import matplotlib.pyplot
as plt
17MODULE_ROOT = Path(__file__).resolve().parent.parent.parent
18REPO_ROOT = MODULE_ROOT.parent
19for p
in [str(REPO_ROOT), str(MODULE_ROOT)]:
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]:
36from radcluster_code.py_utils.simulation
import RadClusterSimulation
37from radcluster_code.py_utils.defect_production
import FISSION
as _FISSION_DICT
43 return 2.0 * np.sqrt(np.maximum(n, 1) * Omega / (np.pi * b_111)) * 1e9
46I_SIM, V_SIM = 3000, 3000
48 't_span': (1e-6, 1e5),
56 'concentration_threshold': 1e-18,
'window_pad': 10,
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,
64 'shape_function':
'linear',
69 """Run one simulation and return full results + rate info."""
70 params = dict(BASE_PARAMS)
71 params.update(overrides)
73 i_mob = int(params.get(
'i_mobile', 10))
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']:
79 _FISSION_DICT[key] = params[key]
81 saved = sys.stdout, sys.stderr
82 sys.stdout = sys.stderr = io.StringIO()
84 sim = RadClusterSimulation(
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,
91 for key, val
in params.items():
93 for d
in [inp.production_fission, inp.production_fusion,
94 inp.diffusion, inp.reactions,
95 inp.energetics, inp.dissociation]:
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()
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,
116 sys.stdout, sys.stderr = saved
117 _FISSION_DICT.update(fission_backup)
123 rr = sim.reaction_rates
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']
132 ci1_vs_t = np.maximum(y_all[0, :], 0.0)
133 cv1_vs_t = np.maximum(y_all[I_SIM, :], 0.0)
134 flux_i = omega_i * ci1_vs_t * s2m
135 flux_v = omega_v * cv1_vs_t * s2m
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
144 ipeak = np.argmax(c_i_01[1:]) + 1
145 sia_peak_n = ipeak + 1
147 sia_peak_c = c_i_01[ipeak]
148 sia_mean_d =
n_to_d_nm(results[
'mean_n_i'][idx_01])
151 vpeak = np.argmax(c_v_01[1:]) + 1
152 vac_peak_n = vpeak + 1
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
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,
195 'eta': 0.30,
'f_cl_i': 0.30,
'f_cl_v': 0.25,
196 'Z_i': 1.05,
'Z_ii': 1.2,
201 'eta': 0.30,
'f_cl_i': 0.30,
'f_cl_v': 0.25,
202 'Z_i': 1.10,
'Z_ii': 1.2,
205 'eta': 0.30,
'f_cl_i': 0.30,
'f_cl_v': 0.25,
206 'Z_i': 1.15,
'Z_ii': 1.2,
209 'eta': 0.30,
'f_cl_i': 0.30,
'f_cl_v': 0.25,
210 'Z_i': 1.20,
'Z_ii': 1.2,
215 'eta': 0.30,
'f_cl_i': 0.50,
'f_cl_v': 0.25,
216 'Z_i': 1.05,
'Z_ii': 1.2,
219 'eta': 0.30,
'f_cl_i': 0.30,
'f_cl_v': 0.40,
220 'Z_i': 1.05,
'Z_ii': 1.2,
225 'eta': 0.30,
'f_cl_i': 0.50,
'f_cl_v': 0.25,
226 'Z_i': 1.15,
'Z_ii': 1.2,
229 'eta': 0.30,
'f_cl_i': 0.30,
'f_cl_v': 0.40,
230 'Z_i': 1.15,
'Z_ii': 1.2,
238if __name__ ==
'__main__':
239 out_dir = Path(__file__).parent /
'bias_study_output'
240 out_dir.mkdir(exist_ok=
True)
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)')
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():
257 except Exception
as e:
258 print(f
' FAILED: {e}')
259 import traceback; traceback.print_exc()
261 elapsed = time.time() - t1
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}')
269 total = time.time() - t0
270 print(f
'\nTotal time: {total:.0f}s ({total/60:.1f} min)')
275 fig1, axes1 = plt.subplots(2, 1, figsize=(10, 8), sharex=
True)
277 for res
in all_results:
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'])
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)
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)
295 fig1.savefig(str(out_dir /
'monomer_fluxes.png'), dpi=150)
296 print(f
'\nSaved: {out_dir / "monomer_fluxes.png"}')
301 fig2, ax2 = plt.subplots(figsize=(10, 5))
302 for res
in all_results:
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]))
315 fig2.savefig(str(out_dir /
'flux_ratio.png'), dpi=150)
316 print(f
'Saved: {out_dir / "flux_ratio.png"}')
321 fig3, (ax3a, ax3b) = plt.subplots(1, 2, figsize=(14, 6))
322 ns = np.arange(1, I_SIM + 1)
325 for res
in all_results:
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)
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)')
335 ax3a.legend(fontsize=7)
336 ax3a.grid(
True, alpha=0.3)
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)')
342 ax3b.legend(fontsize=7)
343 ax3b.grid(
True, alpha=0.3)
346 fig3.savefig(str(out_dir /
'sia_distributions.png'), dpi=150)
347 print(f
'Saved: {out_dir / "sia_distributions.png"}')
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}')
357 for r
in all_results:
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}')
run_case(name, overrides)