RadCluster
Graph-based cluster dynamics for irradiated materials
Loading...
Searching...
No Matches
benchmark_woodbury.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""
3benchmark_woodbury.py — Benchmark Woodbury vs Jacobi preconditioner.
4
5Uses a SHORT time span (t=1e-6 to 0.1) to isolate the preconditioner
6effect from the physics stiffness at later times. All 4 cases complete
7in under a minute each.
8"""
9
10import sys, os, io, time, importlib, subprocess, json
11from pathlib import Path
12
13MODULE_ROOT = Path(__file__).resolve().parent.parent.parent
14REPO_ROOT = MODULE_ROOT.parent
15for p in [str(REPO_ROOT), str(MODULE_ROOT)]:
16 if p not in sys.path:
17 sys.path.insert(0, p)
18
19# ── Rebuild C++ solver ───────────────────────────────────────────────────────
20build_dir = MODULE_ROOT / 'build'
21build_dir.mkdir(exist_ok=True)
22cmake_src = MODULE_ROOT / 'cpp_utils'
23print("Building C++ solver...", flush=True)
24for cmd in [
25 ['cmake', '-S', str(cmake_src), '-B', str(build_dir), '-DCMAKE_BUILD_TYPE=Release'],
26 ['cmake', '--build', str(build_dir), '--config', 'Release'],
27]:
28 res = subprocess.run(cmd, capture_output=True, text=True)
29 if res.returncode != 0:
30 print(f"Build failed: {res.stderr[-1000:]}", flush=True)
31 sys.exit(1)
32print("Build OK\n", flush=True)
33
34# ── Import ───────────────────────────────────────────────────────────────────
35import radcluster_code.py_utils.defect_production as _dp
36import radcluster_code.py_utils.binding_energies as _be
37import radcluster_code.py_utils.bin_moment_rates as _bmr
38import radcluster_code.py_utils.input_data as _inp
39import radcluster_code.py_utils.reaction_rates as _rr
40import radcluster_code.py_utils.rate_equations as _re
41import radcluster_code.py_utils.cpp_bridge as _cb
42import radcluster_code.py_utils.post_process as _pp
43import radcluster_code.py_utils.simulation as _sim
44for _m in [_dp, _be, _bmr, _inp, _rr, _re, _cb, _pp, _sim]:
45 importlib.reload(_m)
46from radcluster_code.py_utils.simulation import RadClusterSimulation
47
48# ── Parameters ───────────────────────────────────────────────────────────────
49I = 10_000
50V = 10_000
51
52BASE_OVERRIDES = {
53 'eta': 0.3, 'f_cl_i': 0.0, 'f_cl_v': 0.5,
54 'E_m_1D': 0.4, 'L_hat': 71.8,
55 'c_C': 1.94e-4, 'E_b_C_SIA': 0.65,
56 'rho_d': 1e16, 'Z_i': 1.05, 'Z_ii': 5,
57 'i_discrete': I, 'v_discrete': V,
58 'I_bin': 0, 'V_bin': 0,
59 'shape_function': 'linear',
60}
61
62
63def make_config(prec_type, t_end):
64 return {
65 't_span': (1e-6, t_end),
66 'n_points': 100,
67 'log_time': True,
68 'rtol': 1e-6,
69 'atol': 1e-25,
70 'solver_method': {
71 'linsol': 'gmres',
72 'window_width': 50,
73 'concentration_threshold': 1e-20, 'window_pad': 100,
74 'prec_type': prec_type,
75 }
76 }
77
78
79def create_sim(i_mob, v_mob):
80 saved = sys.stdout, sys.stderr
81 sys.stdout = sys.stderr = io.StringIO()
82 try:
83 sim = RadClusterSimulation(
84 I=I, V=V, solver_mode='active_window',
85 physics_option='full_CD_fission',
86 C_floor=1e-25, he_kinetics='quasi_steady_state',
87 i_mobile=i_mob, v_mobile=v_mob,
88 )
89 ov = dict(BASE_OVERRIDES)
90 ov['i_mobile'] = i_mob
91 inp = sim.input_data
92 for key, val in ov.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'] = int(ov['i_mobile'])
103 inp.reactions['i_mobile'] = int(ov['i_mobile'])
104 inp._calculate_derived()
105 sim.rebuild_rates()
106 finally:
107 sys.stdout, sys.stderr = saved
108 return sim
109
110
111def run_case(label, i_mob, v_mob, prec_type, t_end):
112 prec_name = 'Woodbury' if prec_type == 1 else 'Jacobi'
113 print(f"\n{'='*60}", flush=True)
114 print(f" {label}", flush=True)
115 print(f" i_mobile={i_mob}, v_mobile={v_mob}, prec={prec_name}, t_end={t_end:.0e}", flush=True)
116 print(f"{'='*60}", flush=True)
117
118 sim = create_sim(i_mob, v_mob)
119 cfg = make_config(prec_type, t_end)
120
121 t0 = time.perf_counter()
122 results = sim.run(solver_config=cfg, save_output=False)
123 wall = time.perf_counter() - t0
124
125 info = {'label': label, 'i_mobile': i_mob, 'v_mobile': v_mob,
126 'prec_type': prec_type, 'wall_time': wall, 't_end': t_end}
127
128 if results is not None:
129 info['n_pts'] = len(results['t'])
130 info['swelling'] = results['swelling'][-1] * 100
131 info['delta_FP'] = results['delta_FP'][-1]
132 print(f" Wall time: {wall:.2f} s", flush=True)
133 print(f" Swelling: {info['swelling']:.6f} %", flush=True)
134 print(f" delta_FP: {info['delta_FP']:.2e}", flush=True)
135 else:
136 print(f" FAILED ({wall:.1f} s)", flush=True)
137 return info
138
139
140# ══════════════════════════════════════════════════════════════════════════════
141# Run benchmarks at two time horizons
142# ══════════════════════════════════════════════════════════════════════════════
143
144all_results = []
145
146for t_end in [1e-1, 1e0]:
147 tag = f"t={t_end:.0e}"
148 all_results.append(run_case(f"Mono+Woodbury ({tag})", 1, 1, 1, t_end))
149 all_results.append(run_case(f"Mono+Jacobi ({tag})", 1, 1, 0, t_end))
150 all_results.append(run_case(f"Coal+Woodbury ({tag})", 10, 5, 1, t_end))
151 all_results.append(run_case(f"Coal+Jacobi ({tag})", 10, 5, 0, t_end))
152
153# ══════════════════════════════════════════════════════════════════════════════
154# Summary
155# ══════════════════════════════════════════════════════════════════════════════
156
157print(f"\n\n{'='*60}", flush=True)
158print(f" BENCHMARK SUMMARY (I={I}, V={V})", flush=True)
159print(f"{'='*60}", flush=True)
160print(f" {'Case':<35s} {'Wall(s)':>8s} {'Swell%':>10s} {'dFP':>10s}", flush=True)
161print(f" {'-'*33} {'-'*8} {'-'*10} {'-'*10}", flush=True)
162for r in all_results:
163 sw = f"{r.get('swelling',0):.5f}" if 'swelling' in r else 'FAIL'
164 dfp = f"{r.get('delta_FP',0):.1e}" if 'delta_FP' in r else 'FAIL'
165 print(f" {r['label']:<35s} {r['wall_time']:>8.2f} {sw:>10s} {dfp:>10s}", flush=True)
166
167# Speedup analysis
168print(f"\n Speedup (Jacobi time / Woodbury time):", flush=True)
169for t_end in [1e-1, 1e0]:
170 tag = f"t={t_end:.0e}"
171 for mob_label, im, vm in [("Monomer", 1, 1), ("Coalescence", 10, 5)]:
172 wood = [r for r in all_results if r['i_mobile']==im and r['prec_type']==1 and r['t_end']==t_end]
173 jac = [r for r in all_results if r['i_mobile']==im and r['prec_type']==0 and r['t_end']==t_end]
174 if wood and jac and wood[0]['wall_time'] > 0:
175 ratio = jac[0]['wall_time'] / wood[0]['wall_time']
176 print(f" {mob_label} ({tag}): {ratio:.2f}x", flush=True)
177
178print(f"\n Coalescence overhead (Coal time / Mono time):", flush=True)
179for t_end in [1e-1, 1e0]:
180 tag = f"t={t_end:.0e}"
181 for pt, pn in [(1, "Woodbury"), (0, "Jacobi")]:
182 mono = [r for r in all_results if r['i_mobile']==1 and r['prec_type']==pt and r['t_end']==t_end]
183 coal = [r for r in all_results if r['i_mobile']==10 and r['prec_type']==pt and r['t_end']==t_end]
184 if mono and coal and mono[0]['wall_time'] > 0:
185 ratio = coal[0]['wall_time'] / mono[0]['wall_time']
186 print(f" {pn} ({tag}): {ratio:.2f}x", flush=True)
187
188print(flush=True)
run_case(label, i_mob, v_mob, prec_type, t_end)
make_config(prec_type, t_end)