RadCluster
Graph-based cluster dynamics for irradiated materials
Loading...
Searching...
No Matches
zii_study.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""
3Z_ii sweep — the SIA-SIA coalescence bias factor.
4
5Z_ii multiplies the full K_ii_coal rate (SIA absorption by SIA loops)
6but does NOT affect vacancy absorption. This makes it the primary knob
7for shifting the growth/shrink balance.
8
9With corrected loop geometry (A_loop * n^{1/2}):
10 growth_ratio = Z_ii * Z_i_loop * Di * c_i1 / (Dv * c_v1)
11
12Baseline ratio ≈ 0.742 at Z_ii=1.2. Need ratio > 1.0 for net growth.
13Estimated crossover: Z_ii ≈ 1.6-1.7.
14"""
15
16import sys, os, io, time
17from pathlib import Path
18import numpy as np
19import matplotlib
20matplotlib.use('Agg')
21import matplotlib.pyplot as plt
22
23MODULE_ROOT = Path(__file__).resolve().parent.parent.parent
24REPO_ROOT = MODULE_ROOT.parent
25for p in [str(REPO_ROOT), str(MODULE_ROOT)]:
26 if p not in sys.path:
27 sys.path.insert(0, p)
28
29import importlib
30import radcluster_code.py_utils.defect_production as _dp_mod
31import radcluster_code.py_utils.binding_energies as _be_mod
32import radcluster_code.py_utils.bin_moment_rates as _bmr
33import radcluster_code.py_utils.input_data as _inp_mod
34import radcluster_code.py_utils.reaction_rates as _rr_mod
35import radcluster_code.py_utils.rate_equations as _re_mod
36import radcluster_code.py_utils.cpp_bridge as _cb_mod
37import radcluster_code.py_utils.post_process as _pp_mod
38import radcluster_code.py_utils.simulation as _sim_mod
39for _m in [_dp_mod, _be_mod, _bmr, _inp_mod, _rr_mod, _re_mod,
40 _cb_mod, _pp_mod, _sim_mod]:
41 importlib.reload(_m)
42from radcluster_code.py_utils.simulation import RadClusterSimulation
43from radcluster_code.py_utils.defect_production import FISSION as _FISSION_DICT
44
45Omega = 1.18e-29
46b_111 = 2.482e-10
47def n_to_d_nm(n):
48 return 2.0 * np.sqrt(np.maximum(n, 1) * Omega / (np.pi * b_111)) * 1e9
49
50I_SIM, V_SIM = 3000, 3000
51SOLVER_CONFIG = {
52 't_span': (1e-6, 1e5),
53 'n_points': 150,
54 'log_time': True,
55 'rtol': 1e-5,
56 'atol': 1e-20,
57 'solver_method': {
58 'linsol': 'gmres',
59 'window_width': 50,
60 'concentration_threshold': 1e-18, 'window_pad': 10,
61 }
62}
63
64BASE = {
65 'eta': 0.30, 'f_cl_i': 0.30, 'f_cl_v': 0.25,
66 'E_m_1D': 0.4, 'i_mobile': 10, 'L_hat': 71.8,
67 'c_C': 1.94e-4, 'E_b_C_SIA': 0.65,
68 'rho_d': 1e13, 'Z_i': 1.05,
69 'shape_function': 'linear',
70}
71
72
73def run_one(name, Z_ii_val):
74 params = dict(BASE)
75 params['Z_ii'] = Z_ii_val
76
77 fission_bak = dict(_FISSION_DICT)
78 for key in ['eta', 'f_cl_i', 'f_cl_v']:
79 if key in params:
80 _FISSION_DICT[key] = params[key]
81
82 saved = sys.stdout, sys.stderr
83 sys.stdout = sys.stderr = io.StringIO()
84 try:
85 sim = RadClusterSimulation(
86 I=I_SIM, V=V_SIM,
87 solver_mode='active_window', physics_option='full_CD_fission',
88 C_floor=1e-25, he_kinetics='quasi_steady_state',
89 i_mobile=10, v_mobile=5,
90 )
91 inp = sim.input_data
92 for key, val in params.items():
93 placed = False
94 for d in [inp.production_fission, inp.production_fusion,
95 inp.diffusion, inp.reactions,
96 inp.energetics, inp.dissociation]:
97 if key in d:
98 d[key] = val
99 placed = True
100 if not placed:
101 inp.reactions[key] = val
102 inp.diffusion['i_mobile'] = 10
103 inp.reactions['i_mobile'] = 10
104 inp.reactions['i_discrete'] = 10
105 inp.reactions['v_discrete'] = 5
106 inp.reactions['I_bin'] = 0
107 inp.reactions['V_bin'] = 0
108 inp._calculate_derived()
109 sim.rebuild_rates()
110
111 results = sim.run_adaptive(
112 solver_config=SOLVER_CONFIG, save_output=False,
113 progress_callback=None, boundary_threshold=0.1,
114 max_doublings=0, points_per_segment=10,
115 )
116 finally:
117 sys.stdout, sys.stderr = saved
118 _FISSION_DICT.update(fission_bak)
119
120 if results is None:
121 return None
122
123 d = inp.derived
124 rr = sim.reaction_rates
125 s2m = 1.0 / d['Omega']
126 y = results['y']
127 dose = results['dose']
128 idx = np.argmin(np.abs(dose - 0.1))
129
130 ci1 = np.maximum(y[0, idx], 0.0)
131 cv1 = np.maximum(y[I_SIM, idx], 0.0)
132 c_i = np.maximum(y[0:I_SIM, idx], 0.0) * s2m
133 c_v = np.maximum(y[I_SIM:2*I_SIM, idx], 0.0) * s2m
134
135 # SIA peak (n >= 2)
136 ipk = np.argmax(c_i[1:]) + 1
137 sia_pk_n = ipk + 1
138 sia_pk_d = n_to_d_nm(sia_pk_n)
139
140 # Mean SIA diameter
141 ns = np.arange(1, I_SIM + 1, dtype=float)
142 mask2 = c_i[1:] > 1e5
143 if mask2.any():
144 mean_d = np.sum(n_to_d_nm(ns[1:])[mask2] * c_i[1:][mask2]) / np.sum(c_i[1:][mask2])
145 else:
146 mean_d = 0.0
147
148 # VAC peak (m >= 2)
149 vpk = np.argmax(c_v[1:]) + 1
150 vac_pk_n = vpk + 1
151
152 # Growth ratio at n=100
153 K_g = rr.K_SIA_grow[99]
154 K_s = rr.K_SIA_shrink[99]
155 v_net = K_g * ci1 - K_s * cv1
156 ratio = (K_g * ci1) / max(K_s * cv1, 1e-300)
157
158 # Flux arrays vs dose
159 omega_i = d['omega_i_eff']
160 omega_v = d['omega_v_eff']
161 flux_i = omega_i * np.maximum(y[0, :], 0.0) * s2m
162 flux_v = omega_v * np.maximum(y[I_SIM, :], 0.0) * s2m
163
164 return {
165 'name': name, 'Z_ii': Z_ii_val,
166 'dose': dose, 'flux_i': flux_i, 'flux_v': flux_v,
167 'sia_pk_n': sia_pk_n, 'sia_pk_d': sia_pk_d,
168 'sia_mean_d': mean_d,
169 'vac_pk_n': vac_pk_n,
170 'v_net': v_net, 'ratio': ratio,
171 'N_loops': results['N_loops'][idx],
172 'swelling': results['swelling'][idx] * 100,
173 'c_i': c_i, 'c_v': c_v,
174 'ci1_m3': ci1 * s2m, 'cv1_m3': cv1 * s2m,
175 }
176
177
178# ═══════════════════════════════════════════════════════════════════════════
179if __name__ == '__main__':
180 out_dir = Path(__file__).parent / 'zii_study_output'
181 out_dir.mkdir(exist_ok=True)
182
183 Z_ii_values = [1.0, 1.2, 1.5, 1.7, 2.0, 2.5, 3.0]
184
185 print('=' * 70)
186 print(' Z_ii SWEEP — SIA-SIA Coalescence Bias')
187 print(' Loop geometry corrected (A_loop * n^{1/2})')
188 print('=' * 70)
189
190 results = []
191 t0 = time.time()
192 for i, zii in enumerate(Z_ii_values):
193 name = f'Zii_{zii:.1f}'
194 print(f'\n[{i+1}/{len(Z_ii_values)}] Z_ii = {zii}', flush=True)
195 t1 = time.time()
196 try:
197 r = run_one(name, zii)
198 except Exception as e:
199 print(f' FAILED: {e}')
200 import traceback; traceback.print_exc()
201 r = None
202 dt = time.time() - t1
203 if r:
204 results.append(r)
205 marker = ' *** GROWING ***' if r['ratio'] > 1.0 else ''
206 print(f' Done ({dt:.0f}s) | peak: n={r["sia_pk_n"]}, d={r["sia_pk_d"]:.2f} nm | '
207 f'mean_d={r["sia_mean_d"]:.2f} nm | ratio={r["ratio"]:.3f}{marker}')
208
209 total = time.time() - t0
210 print(f'\nTotal: {total:.0f}s ({total/60:.1f} min)')
211
212 # ── Plot 1: Flux ratio vs dose for all Z_ii ─────────────────────────
213 fig1, ax1 = plt.subplots(figsize=(10, 5))
214 for r in results:
215 dose = r['dose']
216 mask = dose > 1e-8
217 rat = r['flux_i'][mask] / np.maximum(r['flux_v'][mask], 1e-30)
218 ax1.semilogx(dose[mask], rat, label=f'Z_ii={r["Z_ii"]:.1f}', linewidth=1.5)
219 ax1.axhline(y=1.0, color='red', ls='--', lw=2, label='ratio=1 (balance)')
220 ax1.set_xlabel('Dose [dpa]')
221 ax1.set_ylabel(r'$\omega_i C_{i1}\;/\;\omega_v C_{v1}$')
222 ax1.set_title(r'Monomer flux ratio vs $Z_{ii}$ — above 1 = net loop growth')
223 ax1.legend(fontsize=9)
224 ax1.grid(True, alpha=0.3)
225 ax1.set_ylim(0, max(3.0, ax1.get_ylim()[1]))
226 fig1.tight_layout()
227 fig1.savefig(str(out_dir / 'flux_ratio_vs_Zii.png'), dpi=150)
228 print(f'\nSaved: {out_dir / "flux_ratio_vs_Zii.png"}')
229
230 # ── Plot 2: SIA size distributions at 0.1 dpa ───────────────────────
231 fig2, (ax2a, ax2b) = plt.subplots(1, 2, figsize=(14, 6))
232 ns = np.arange(1, I_SIM + 1)
233 d_nm = n_to_d_nm(ns)
234 for r in results:
235 c_i = r['c_i']
236 m = c_i > 1e5
237 ax2a.semilogy(d_nm[m], c_i[m], label=f'Z_ii={r["Z_ii"]:.1f}', lw=1.3)
238 ax2b.plot(d_nm[m], c_i[m], label=f'Z_ii={r["Z_ii"]:.1f}', lw=1.3)
239 for ax in [ax2a, ax2b]:
240 ax.set_xlabel('SIA loop diameter [nm]')
241 ax.set_ylabel('Concentration [m$^{-3}$]')
242 ax.legend(fontsize=8)
243 ax.grid(True, alpha=0.3)
244 ax2a.set_title('SIA distribution at 0.1 dpa (log)')
245 ax2a.set_xlim(0, 15)
246 ax2b.set_title('SIA distribution at 0.1 dpa (linear)')
247 ax2b.set_xlim(0, 10)
248 fig2.tight_layout()
249 fig2.savefig(str(out_dir / 'sia_dist_vs_Zii.png'), dpi=150)
250 print(f'Saved: {out_dir / "sia_dist_vs_Zii.png"}')
251
252 # ── Plot 3: Growth ratio and SIA peak vs Z_ii ────────────────────────
253 fig3, (ax3a, ax3b) = plt.subplots(1, 2, figsize=(12, 5))
254 zvals = [r['Z_ii'] for r in results]
255 ratios = [r['ratio'] for r in results]
256 peaks = [r['sia_pk_d'] for r in results]
257 means = [r['sia_mean_d'] for r in results]
258
259 ax3a.plot(zvals, ratios, 'bo-', markersize=8, linewidth=2)
260 ax3a.axhline(y=1.0, color='red', ls='--', lw=2, label='ratio=1 (balance)')
261 ax3a.set_xlabel(r'$Z_{ii}$')
262 ax3a.set_ylabel('Growth/Shrink ratio at n=100')
263 ax3a.set_title('Growth ratio vs Z_ii')
264 ax3a.legend()
265 ax3a.grid(True, alpha=0.3)
266
267 ax3b.plot(zvals, peaks, 'rs-', markersize=8, linewidth=2, label='Peak diameter')
268 ax3b.plot(zvals, means, 'g^-', markersize=8, linewidth=2, label='Mean diameter')
269 ax3b.axhspan(2, 5, alpha=0.15, color='green', label='Target: 2-5 nm')
270 ax3b.set_xlabel(r'$Z_{ii}$')
271 ax3b.set_ylabel('Diameter [nm]')
272 ax3b.set_title('SIA loop size vs Z_ii at 0.1 dpa')
273 ax3b.legend()
274 ax3b.grid(True, alpha=0.3)
275
276 fig3.tight_layout()
277 fig3.savefig(str(out_dir / 'ratio_and_peak_vs_Zii.png'), dpi=150)
278 print(f'Saved: {out_dir / "ratio_and_peak_vs_Zii.png"}')
279
280 # ── Summary table ────────────────────────────────────────────────────
281 print('\n' + '=' * 95)
282 print(f'{"Z_ii":>6} {"SIA pk n":>9} {"SIA pk d":>9} {"Mean d":>8} '
283 f'{"VAC pk n":>9} {"ratio":>7} {"v_net(100)":>11} '
284 f'{"N_loops":>10} {"Swell%":>8} {"Status":>10}')
285 print('-' * 95)
286 for r in results:
287 status = 'GROWING' if r['ratio'] > 1.0 else 'shrinking'
288 print(f'{r["Z_ii"]:>6.1f} {r["sia_pk_n"]:>9d} {r["sia_pk_d"]:>9.2f} '
289 f'{r["sia_mean_d"]:>8.2f} {r["vac_pk_n"]:>9d} {r["ratio"]:>7.3f} '
290 f'{r["v_net"]:>11.3e} {r["N_loops"]:>10.2e} {r["swelling"]:>8.4f} '
291 f'{status:>10}')
292 print('=' * 95)
293
294 plt.close('all')
295 print('\nDone.')
n_to_d_nm(n)
Definition zii_study.py:47
run_one(name, Z_ii_val)
Definition zii_study.py:73