3mobility_sweep.py — diagnostic sweep of SIA / vacancy mobility cutoffs.
7With the current input (n_max_i=50, m_max_v=5, L_hat=50), mobile SIA
8clusters of size 4–50 glide in 1D and are rapidly drained at fixed sinks
9(dislocations, GBs, precipitates) and at voids — even when the 3D
10coalescence diffusivity D_SIA_eff[n>=2] is zero. Vacancy clusters lose
11mobility at m=6 and become permanent sinks for the residual flux. The
12asymmetry should disappear (and may invert) when i_mobile is reduced
13toward 1, when v_mobile is raised, or when L_hat is shortened.
17 A baseline i_mobile=50, v_mobile=5, L_hat=50
18 B di-SIA only i_mobile=2, v_mobile=5, L_hat=50
19 C monomer-only SIA i_mobile=1, v_mobile=5, L_hat=50
20 D symmetric mobility i_mobile=50, v_mobile=20, L_hat=50
21 E mono-defect (reference) i_mobile=1, v_mobile=1, L_hat=50
22 F short MFP (1D suppressed) i_mobile=50, v_mobile=5, L_hat=5
26 output/mobility_sweep_<timestamp>/
27 summary.csv — per-case scalars at 0.1 dpa
28 mean_sizes_vs_dose.png — mean_n_i, mean_n_v(t) for all cases
29 swelling_vs_dose.png — swelling(t) for all cases
30 conservation_vs_dose.png — delta_FP vs dose for all cases
31 size_dist_at_0.1dpa.png — c_n, c_m at 0.1 dpa, all cases
34import sys, os, io, csv, time
35from pathlib
import Path
36from datetime
import datetime
40import matplotlib.pyplot
as plt
42MODULE_ROOT = Path(__file__).resolve().parent.parent.parent
43REPO_ROOT = MODULE_ROOT.parent
44for p
in [str(REPO_ROOT), str(MODULE_ROOT)]:
49import radcluster_code.py_utils.defect_production
as _dp_mod
50import radcluster_code.py_utils.binding_energies
as _be_mod
51import radcluster_code.py_utils.bin_moment_rates
as _bmr
52import radcluster_code.py_utils.input_data
as _inp_mod
53import radcluster_code.py_utils.reaction_rates
as _rr_mod
54import radcluster_code.py_utils.rate_equations
as _re_mod
55import radcluster_code.py_utils.cpp_bridge
as _cb_mod
56import radcluster_code.py_utils.post_process
as _pp_mod
57import radcluster_code.py_utils.simulation
as _sim_mod
58for _m
in [_dp_mod, _be_mod, _bmr, _inp_mod, _rr_mod, _re_mod,
59 _cb_mod, _pp_mod, _sim_mod]:
61from radcluster_code.py_utils.simulation
import RadClusterSimulation
68I_SIM, V_SIM = 1000, 1000
76 't_span': (1e-6, 1e4),
89 (
'A_baseline', {
'i_mobile': 50,
'v_mobile': 5,
'L_hat': 50.0}),
90 (
'B_di_SIA', {
'i_mobile': 2,
'v_mobile': 5,
'L_hat': 50.0}),
91 (
'C_mono_SIA', {
'i_mobile': 1,
'v_mobile': 5,
'L_hat': 50.0}),
92 (
'D_symmetric', {
'i_mobile': 50,
'v_mobile': 20,
'L_hat': 50.0}),
93 (
'E_mono_defect', {
'i_mobile': 1,
'v_mobile': 1,
'L_hat': 50.0}),
94 (
'F_short_MFP', {
'i_mobile': 50,
'v_mobile': 5,
'L_hat': 5.0}),
98def run_case(name, overrides, quiet=True, heartbeat_every=1.5):
99 """Run one case via active_window and return time-resolved results.
101 Per-row progress is forwarded to the *real* terminal even while the
102 inner solver's stdout is captured. A line is printed at most once
103 every `heartbeat_every` seconds (wall-clock) plus always at the
104 final point, to avoid flooding while still confirming progress.
106 i_mob = int(overrides[
'i_mobile'])
107 v_mob = int(overrides[
'v_mobile'])
108 L_hat = float(overrides[
'L_hat'])
110 real_stdout = sys.stdout
111 real_stderr = sys.stderr
112 state = {
'last_print': 0.0,
'rows': 0,
'G_dpa':
None,
't0': time.time()}
118 if (now - state[
'last_print']) < heartbeat_every:
120 state[
'last_print'] = now
121 t = row.get(
't', 0.0)
122 G = state[
'G_dpa']
or 1e-6
124 ci1 = row.get(
'c_i1', 0.0)
125 cv1 = row.get(
'c_v1', 0.0)
126 elapsed = now - state[
't0']
128 f
" [{name}] step {state['rows']:>4d} "
129 f
"t={t:.2e}s dose={dose:.3e} dpa "
130 f
"c_i1={ci1:.2e} c_v1={cv1:.2e} "
131 f
"({elapsed:5.1f}s elapsed)\n"
135 saved_stdout, saved_stderr = sys.stdout, sys.stderr
137 sys.stdout = sys.stderr = io.StringIO()
139 sim = RadClusterSimulation(
141 solver_mode=
'full_system',
142 physics_option=
'full_CD_fission',
144 he_kinetics=
'quasi_steady_state',
149 inp.diffusion[
'L_hat'] = L_hat
150 inp.diffusion[
'i_mobile'] = i_mob
151 inp.diffusion[
'v_mobile'] = v_mob
152 inp.reactions[
'i_mobile'] = i_mob
153 inp.reactions[
'v_mobile'] = v_mob
154 inp.reactions[
'i_discrete'] = i_mob
155 inp.reactions[
'v_discrete'] = max(v_mob, 5)
156 inp.reactions[
'I_bin'] = 0
157 inp.reactions[
'V_bin'] = 0
158 inp._calculate_derived()
160 state[
'G_dpa'] = sim.input_data.derived[
'G']
162 results = sim.run_adaptive(
163 solver_config=SOLVER_CONFIG, save_output=
False,
164 progress_callback=_hb, boundary_threshold=0.1,
165 max_doublings=1, points_per_segment=10,
169 sys.stdout, sys.stderr = saved_stdout, saved_stderr
174 dose = np.asarray(results[
'dose'])
175 j01 = int(np.argmin(np.abs(dose - DOSE_SNAPSHOT)))
178 y_all = np.asarray(results[
'y'])
179 s2m = 1.0 / sim.input_data.derived[
'Omega']
180 I_run = sim.input_data.I
181 V_run = sim.input_data.V
182 c_n_01 = np.maximum(y_all[0:I_run, j01], 0.0) * s2m
183 c_v_01 = np.maximum(y_all[I_run:I_run + V_run, j01], 0.0) * s2m
187 'overrides': overrides,
188 't': np.asarray(results[
't']),
190 'mean_n_i': np.asarray(results[
'mean_n_i']),
191 'mean_n_v': np.asarray(results[
'mean_n_v']),
192 'swelling': np.asarray(results[
'swelling']),
193 'N_loops': np.asarray(results[
'N_loops']),
194 'N_voids': np.asarray(results[
'N_voids']),
195 'delta_FP': np.asarray(results[
'delta_FP']),
205 path = out_dir /
'summary.csv'
206 tag = f
"{DOSE_SNAPSHOT:g}"
207 with open(path,
'w', newline=
'')
as f:
209 w.writerow([
'case',
'i_mobile',
'v_mobile',
'L_hat',
210 'dose_dpa@end', f
'mean_n_i@{tag}', f
'mean_n_v@{tag}',
211 f
'swelling@{tag}[%]', f
'N_loops@{tag}[m^-3]',
212 f
'N_voids@{tag}[m^-3]', f
'delta_FP@{tag}'])
220 ov[
'i_mobile'], ov[
'v_mobile'], ov[
'L_hat'],
221 f
"{c['dose'][-1]:.4g}",
222 f
"{c['mean_n_i'][j]:.3f}",
223 f
"{c['mean_n_v'][j]:.3f}",
224 f
"{c['swelling'][j] * 100:.4f}",
225 f
"{c['N_loops'][j]:.3e}",
226 f
"{c['N_voids'][j]:.3e}",
227 f
"{c['delta_FP'][j]:.3e}",
233 fig, axes = plt.subplots(1, 2, figsize=(11, 4.2), sharex=
True)
237 axes[0].plot(c[
'dose'], c[
'mean_n_i'], label=c[
'name'])
238 axes[1].plot(c[
'dose'], c[
'mean_n_v'], label=c[
'name'])
239 for ax, ttl
in zip(axes, [
'mean_n_i (SIA)',
'mean_n_v (vacancy)']):
240 ax.set_xscale(
'log'); ax.set_yscale(
'log')
241 ax.set_xlabel(
'dose (dpa)'); ax.set_ylabel(ttl)
242 ax.grid(
True, which=
'both', alpha=0.3); ax.legend(fontsize=8)
243 fig.suptitle(
'Mean cluster size vs dose — mobility-cutoff sweep')
245 fig.savefig(out_dir /
'mean_sizes_vs_dose.png', dpi=150)
248 fig, ax = plt.subplots(figsize=(6.5, 4.2))
252 ax.plot(c[
'dose'], np.asarray(c[
'swelling']) * 100, label=c[
'name'])
253 ax.set_xscale(
'log'); ax.set_yscale(
'log')
254 ax.set_xlabel(
'dose (dpa)'); ax.set_ylabel(
'swelling (%)')
255 ax.grid(
True, which=
'both', alpha=0.3); ax.legend(fontsize=8)
256 ax.set_title(
'Swelling vs dose')
258 fig.savefig(out_dir /
'swelling_vs_dose.png', dpi=150)
261 fig, ax = plt.subplots(figsize=(6.5, 4.2))
265 ax.plot(c[
'dose'], c[
'delta_FP'], label=c[
'name'])
266 ax.set_xscale(
'log'); ax.set_yscale(
'log')
267 ax.set_xlabel(
'dose (dpa)'); ax.set_ylabel(
'delta_FP')
268 ax.axhline(1e-3, ls=
'--', c=
'k', alpha=0.5, label=
'1e-3 (target)')
269 ax.grid(
True, which=
'both', alpha=0.3); ax.legend(fontsize=8)
270 ax.set_title(
'Frenkel-pair conservation vs dose')
272 fig.savefig(out_dir /
'conservation_vs_dose.png', dpi=150)
277 fig, axes = plt.subplots(1, 2, figsize=(12, 4.5), sharey=
True)
281 ns = np.arange(1, c[
'I_run'] + 1)
282 ms = np.arange(1, c[
'V_run'] + 1)
283 axes[0].plot(ns, c[
'c_n_01'], label=c[
'name'], lw=1.0)
284 axes[1].plot(ms, c[
'c_v_01'], label=c[
'name'], lw=1.0)
285 for ax, ttl
in zip(axes, [f
'SIA c_n at {DOSE_SNAPSHOT} dpa',
286 f
'Vacancy c_m at {DOSE_SNAPSHOT} dpa']):
287 ax.set_xscale(
'log'); ax.set_yscale(
'log')
288 ax.set_xlabel(
'cluster size')
289 ax.set_ylabel(
'concentration (m$^{-3}$)')
290 ax.grid(
True, which=
'both', alpha=0.3); ax.legend(fontsize=8)
292 ax.set_ylim(bottom=1e10)
294 fig.savefig(out_dir / f
'size_dist_at_{DOSE_SNAPSHOT:g}dpa.png', dpi=150)
299 timestamp = datetime.now().strftime(
'%Y%m%d_%H%M%S')
300 out_dir = MODULE_ROOT /
'output' / f
'mobility_sweep_{timestamp}'
301 out_dir.mkdir(parents=
True, exist_ok=
True)
302 print(f
"Output -> {out_dir}", flush=
True)
305 sweep_t0 = time.time()
306 for k, (name, ov)
in enumerate(CASES, start=1):
307 clock = datetime.now().strftime(
'%H:%M:%S')
308 print(f
"\n=== [{k}/{len(CASES)}] {name} "
309 f
"(i_mobile={ov['i_mobile']}, v_mobile={ov['v_mobile']}, "
310 f
"L_hat={ov['L_hat']}) start {clock} ===", flush=
True)
311 case_t0 = time.time()
314 except Exception
as e:
315 print(f
" FAILED: {type(e).__name__}: {e}", flush=
True)
317 case_dt = time.time() - case_t0
319 print(f
" (no result; {case_dt:.1f}s)", flush=
True)
322 print(f
" -> mean_n_i = {r['mean_n_i'][j]:.2f} "
323 f
"mean_n_v = {r['mean_n_v'][j]:.2f} "
324 f
"swelling = {r['swelling'][j]*100:.4f}% "
325 f
"delta_FP = {r['delta_FP'][j]:.2e} "
326 f
"({case_dt:.1f}s)", flush=
True)
334 total_dt = time.time() - sweep_t0
336 print(f
"\nSweep finished in {total_dt:.1f}s. Wrote {csv_path}", flush=
True)
339 print(f
"Plots written to {out_dir}", flush=
True)
342if __name__ ==
'__main__':
write_summary_csv(cases, out_dir)
run_case(name, overrides, quiet=True, heartbeat_every=1.5)
plot_time_series(cases, out_dir)
plot_size_distributions(cases, out_dir)