RadCluster
Graph-based cluster dynamics for irradiated materials
Loading...
Searching...
No Matches
growth_diagnostic.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""
3Growth rate diagnostic — examines WHY the SIA peak doesn't move.
4
5At ~0.1 dpa, extracts the SIA distribution and computes:
6 - Net growth rate v(n) = K_grow(n)*c_1 - K_shrink(n)*c_v1 [size increments per second]
7 - Production contribution at each n
8 - Sink loss at each n
9 - Monomer SIA and vacancy concentrations
10
11This tells us whether loops are growing (v > 0) or shrinking (v < 0) at each size.
12"""
13
14import sys, os, io
15from pathlib import Path
16import numpy as np
17
18MODULE_ROOT = Path(__file__).resolve().parent.parent.parent
19REPO_ROOT = MODULE_ROOT.parent
20for p in [str(REPO_ROOT), str(MODULE_ROOT)]:
21 if p not in sys.path:
22 sys.path.insert(0, p)
23
24import importlib
25import radcluster_code.py_utils.defect_production as _dp_mod
26import radcluster_code.py_utils.binding_energies as _be_mod
27import radcluster_code.py_utils.bin_moment_rates as _bmr
28import radcluster_code.py_utils.input_data as _inp_mod
29import radcluster_code.py_utils.reaction_rates as _rr_mod
30import radcluster_code.py_utils.rate_equations as _re_mod
31import radcluster_code.py_utils.cpp_bridge as _cb_mod
32import radcluster_code.py_utils.post_process as _pp_mod
33import radcluster_code.py_utils.simulation as _sim_mod
34for _m in [_dp_mod, _be_mod, _bmr, _inp_mod, _rr_mod, _re_mod,
35 _cb_mod, _pp_mod, _sim_mod]:
36 importlib.reload(_m)
37from radcluster_code.py_utils.simulation import RadClusterSimulation
38
39Omega = 1.18e-29
40b_111 = 2.482e-10
41
42def n_to_d_nm(n):
43 return 2.0 * np.sqrt(n * Omega / (np.pi * b_111)) * 1e9
44
45# ── Run baseline simulation to 0.1 dpa ─────────────────────────────────────
46SOLVER_CONFIG = {
47 't_span': (1e-6, 1e5),
48 'n_points': 100,
49 'log_time': True,
50 'rtol': 1e-5,
51 'atol': 1e-20,
52 'solver_method': {
53 'linsol': 'gmres',
54 'window_width': 50,
55 'concentration_threshold': 1e-18, 'window_pad': 10,
56 }
57}
58
59I_SIM, V_SIM = 3000, 3000
60i_mobile = 10
61
62OVERRIDES = {
63 'eta': 0.3, 'f_cl_i': 0.3, 'f_cl_v': 0.25,
64 'E_m_1D': 0.4, 'i_mobile': i_mobile, 'L_hat': 71.8,
65 'c_C': 1.94e-4, 'E_b_C_SIA': 0.65,
66 'rho_d': 1e13, 'Z_i': 1.05, 'Z_ii': 1.2,
67 'shape_function': 'linear',
68}
69
70print('Running baseline simulation to 0.1 dpa...')
71saved = sys.stdout, sys.stderr
72sys.stdout = sys.stderr = io.StringIO()
73try:
74 sim = RadClusterSimulation(
75 I=I_SIM, V=V_SIM,
76 solver_mode='active_window', physics_option='full_CD_fission',
77 C_floor=1e-25, he_kinetics='quasi_steady_state',
78 i_mobile=i_mobile, v_mobile=5,
79 )
80 inp = sim.input_data
81 for key, val in OVERRIDES.items():
82 placed = False
83 for d in [inp.production_fission, inp.production_fusion,
84 inp.diffusion, inp.reactions,
85 inp.energetics, inp.dissociation]:
86 if key in d:
87 d[key] = val
88 placed = True
89 if not placed:
90 inp.reactions[key] = val
91 inp.diffusion['i_mobile'] = i_mobile
92 inp.reactions['i_mobile'] = i_mobile
93 inp.reactions['i_discrete'] = i_mobile
94 inp.reactions['v_discrete'] = 5
95 inp.reactions['I_bin'] = 0
96 inp.reactions['V_bin'] = 0
97 inp._calculate_derived()
98 sim.rebuild_rates()
99 results = sim.run_adaptive(
100 solver_config=SOLVER_CONFIG, save_output=False,
101 progress_callback=None, boundary_threshold=0.1,
102 max_doublings=0, points_per_segment=10,
103 )
104finally:
105 sys.stdout, sys.stderr = saved
106
107if results is None:
108 print('Simulation failed!')
109 sys.exit(1)
110
111# ── Extract state at 0.1 dpa ───────────────────────────────────────────────
112d = inp.derived
113G = d['G']
114dose = results['dose']
115idx_01 = np.argmin(np.abs(dose - 0.1))
116actual_dose = dose[idx_01]
117t_01 = results['t'][idx_01]
118
119y = results['y'][:, idx_01]
120c_i = np.maximum(y[0:I_SIM], 0.0) # SIA concentrations [at.frac]
121c_v = np.maximum(y[I_SIM:2*I_SIM], 0.0) # VAC concentrations [at.frac]
122
123ci1 = c_i[0] # SIA monomer
124cv1 = c_v[0] # vacancy monomer
125
126s2m = 1.0 / d['Omega']
127print(f'\n{"="*70}')
128print(f' GROWTH RATE DIAGNOSTIC at dose = {actual_dose:.4f} dpa (t = {t_01:.2e} s)')
129print(f'{"="*70}')
130print(f' C_i1 (SIA monomer) = {ci1:.4e} at.frac = {ci1*s2m:.4e} m^-3')
131print(f' C_v1 (vac monomer) = {cv1:.4e} at.frac = {cv1*s2m:.4e} m^-3')
132print(f' C_i1/C_v1 ratio = {ci1/max(cv1, 1e-300):.3f}')
133
134rr = sim.reaction_rates
135re = sim.rate_equations
136
137# ── Compute growth rates for each size n ───────────────────────────────────
138# At each size n, the net flux is:
139# Growth: K_grow(n) * c_1 * c_n (SIA monomer absorption makes n → n+1)
140# Shrink: K_shrink(n) * c_v1 * c_n (vacancy monomer makes n → n-1)
141# Net growth velocity: v(n) = K_grow(n) * c_1 - K_shrink(n) * c_v1
142# This has units of [s^-1] — rate of concentration being transported
143
144print(f'\n{"n":>5} {"d(nm)":>7} {"c_n(m-3)":>11} {"K_grow":>10} {"K_shrink":>10} '
145 f'{"v_net":>11} {"G_emit":>10} {"k2_sink":>10} {"dc/dt ratio":>11}')
146print('-' * 100)
147
148for n in [2, 3, 4, 5, 8, 10, 11, 12, 15, 17, 20, 30, 50, 100, 200, 500]:
149 if n > I_SIM:
150 continue
151 cn = c_i[n-1]
152 K_g = rr.K_SIA_grow[n-1] # growth rate constant (monomer absorption)
153 K_s = rr.K_SIA_shrink[n-1] # shrinkage rate constant (vacancy absorption)
154 G_em = rr.G_SIA[n-1] # thermal emission rate
155 k2 = rr.k2_SIA[n-1] # fixed sink rate
156
157 v_net = K_g * ci1 - K_s * cv1 # net growth velocity [s^-1]
158 # dc_n/dt contributions (just the growth/shrink part)
159 growth_flux = K_g * ci1 * cn # rate of c_n being removed by growth
160 shrink_flux = K_s * cv1 * cn # rate of c_n being removed by shrinkage
161 ratio = growth_flux / max(shrink_flux, 1e-300) if shrink_flux > 0 else float('inf')
162
163 print(f'{n:5d} {n_to_d_nm(n):7.2f} {cn*s2m:11.3e} {K_g:10.3e} {K_s:10.3e} '
164 f'{v_net:11.3e} {G_em:10.3e} {k2:10.3e} {ratio:11.3f}')
165
166# ── Overall flux balance for SIA monomers ──────────────────────────────────
167print(f'\n{"="*70}')
168print(f' SIA MONOMER BALANCE (n=1)')
169print(f'{"="*70}')
170
171# Production: P_1 = eta * G * (1 - f_cl_i)
172Pr_1 = re.Pr_SIA[0]
173print(f' Production P_1: {Pr_1:.4e} at.frac/s')
174
175# Sink loss to fixed sinks
176sink_loss = rr.k2_SIA[0] * ci1
177print(f' Fixed sink loss (k2*c1): {sink_loss:.4e} at.frac/s')
178
179# Loss to cluster growth (SIA monomers absorbed by all loops)
180growth_loss = ci1 * np.dot(rr.K_SIA_grow, c_i)
181print(f' Loop growth loss: {growth_loss:.4e} at.frac/s')
182
183# Recombination with vacancies
184recom_loss = rr.K_iv * ci1 * cv1
185print(f' V-I recombination: {recom_loss:.4e} at.frac/s')
186
187# Cavity absorption
188K_3D = rr.K_3D_cav_pref
189m13 = np.arange(1, V_SIM+1)**(1./3.)
190cav_loss = ci1 * K_3D * np.dot(m13, c_v)
191print(f' Cavity absorption: {cav_loss:.4e} at.frac/s')
192
193total_loss = sink_loss + growth_loss + recom_loss + cav_loss
194print(f' ────────────────────')
195print(f' Total loss: {total_loss:.4e} at.frac/s')
196print(f' Production/Loss ratio: {Pr_1/max(total_loss,1e-300):.4f}')
197print(f' Fixed sinks fraction: {sink_loss/max(total_loss,1e-300)*100:.1f}%')
198print(f' Loop growth fraction: {growth_loss/max(total_loss,1e-300)*100:.1f}%')
199print(f' Recombination fraction: {recom_loss/max(total_loss,1e-300)*100:.1f}%')
200print(f' Cavity fraction: {cav_loss/max(total_loss,1e-300)*100:.1f}%')
201
202# ── Growth front velocity ──────────────────────────────────────────────────
203print(f'\n{"="*70}')
204print(f' GROWTH FRONT ANALYSIS')
205print(f'{"="*70}')
206v_growth = np.array([rr.K_SIA_grow[n-1]*ci1 - rr.K_SIA_shrink[n-1]*cv1 for n in range(1, I_SIM+1)])
207print(f' v_net(n=10) = {v_growth[9]:.4e} s^-1 (growth velocity at mobility cutoff)')
208print(f' v_net(n=50) = {v_growth[49]:.4e} s^-1')
209print(f' v_net(n=100) = {v_growth[99]:.4e} s^-1')
210print(f' v_net(n=500) = {v_growth[499]:.4e} s^-1')
211print(f' At t={t_01:.0e}s, if uniform growth at v_net(100):')
212print(f' distance = v_net * t ≈ {v_growth[99]*t_01:.1f} size increments')
213print(f' → This means the growth front reaches n ≈ {v_growth[99]*t_01:.0f} by 0.1 dpa')
214print(f' → Diameter = {n_to_d_nm(max(1,v_growth[99]*t_01)):.2f} nm')
215
216# ── Distribution shape ─────────────────────────────────────────────────────
217print(f'\n{"="*70}')
218print(f' SIA SIZE DISTRIBUTION (top 20 sizes by concentration)')
219print(f'{"="*70}')
220sorted_idx = np.argsort(c_i)[::-1]
221for rank in range(20):
222 n = sorted_idx[rank] + 1
223 cn = c_i[sorted_idx[rank]]
224 print(f' #{rank+1:2d} n={n:5d} d={n_to_d_nm(n):6.2f} nm c_n={cn*s2m:.3e} m^-3')