20import sys, os, csv, time, argparse
63 """Simple LHS: stratified random in [0,1]^d."""
64 result = np.zeros((n_samples, n_dims))
65 for j
in range(n_dims):
66 perm = rng.permutation(n_samples)
67 for i
in range(n_samples):
68 result[perm[i], j] = (i + rng.random()) / n_samples
73 """Map u in [0,1] to physical value according to spec."""
74 name, sheet, lo, hi, scale = spec
76 return 10 ** (np.log10(lo) + u * (np.log10(hi) - np.log10(lo)))
78 return int(round(lo + u * (hi - lo)))
80 return lo + u * (hi - lo)
133 Compute scalar cost measuring deviation from experimental targets at
134 TARGET_DOSE dpa. Uses TEM-visible clusters only (n,m >= N_MIN_TEM).
135 Returns (cost, metrics_dict) or (inf, None) on failure.
138 return float(
'inf'),
None
140 dose = results[
'dose']
141 if dose[-1] < TARGET_DOSE * 0.9:
142 return float(
'inf'),
None
144 idx = np.argmin(np.abs(dose - TARGET_DOSE))
145 inv_Omega = 1.0 / OMEGA
148 re_obj = sim.rate_equations
151 yj = np.maximum(results[
'y'][:, idx], 0.0)
153 is_bin = hasattr(re_obj,
'bins')
155 from radcluster_code.py_utils.bin_moment_rates
import \
156 distribution_from_moments_hat
157 i_d = getattr(re_obj,
'i_discrete', 0)
158 v_d = getattr(re_obj,
'v_discrete', 0)
159 K_bins = re_obj.I_bin
if hasattr(re_obj,
'I_bin')
else len(re_obj.bins)
164 mu0 = mom[0::2][:K_bins]
165 mu1 = mom[1::2][:K_bins]
166 c_binned = distribution_from_moments_hat(mu0, mu1, re_obj.bins, N)
167 c_n[i_d:] = c_binned[i_d:]
168 i_VAC = getattr(re_obj,
'i_VAC', i_d + 2 * K_bins)
169 K_v = getattr(re_obj,
'V_bin', getattr(re_obj,
'K_v', 0))
171 c_v[:v_d] = yj[i_VAC:i_VAC + v_d]
173 vac_start = i_VAC + v_d
174 vmu0 = yj[vac_start::2][:K_v]
175 vmu1 = yj[vac_start + 1::2][:K_v]
176 c_v_binned = distribution_from_moments_hat(
177 vmu0, vmu1, re_obj.vac_bins, M)
178 c_v[v_d:] = c_v_binned[v_d:]
180 c_v = yj[i_VAC:i_VAC + M]
182 i_SIA = getattr(re_obj,
'i_SIA', 0)
183 i_VAC = getattr(re_obj,
'i_VAC', N)
184 c_n = yj[i_SIA:i_VAC]
185 c_v = yj[i_VAC:i_VAC + M]
188 ns = np.arange(1, N + 1, dtype=float)
189 ms = np.arange(1, M + 1, dtype=float)
190 tem_i = ns >= N_MIN_TEM
191 tem_v = ms >= N_MIN_TEM
194 N_loops_vis = np.sum(c_n[tem_i]) * inv_Omega
195 content_vis = np.dot(ns[tem_i], c_n[tem_i])
196 count_vis = np.sum(c_n[tem_i])
197 mean_n_i_vis = content_vis / max(count_vis, 1e-300)
201 N_voids_vis = np.sum(c_v[tem_v]) * inv_Omega
202 vcontent_vis = np.dot(ms[tem_v], c_v[tem_v])
203 vcount_vis = np.sum(c_v[tem_v])
204 mean_n_v_vis = vcontent_vis / max(vcount_vis, 1e-300)
208 if N_loops_vis < 1e10
or N_voids_vis < 1e10:
209 return float(
'inf'),
None
210 if d_i_nm < 0.01
or d_v_nm < 0.01:
211 return float(
'inf'),
None
215 (np.log10(N_loops_vis) - np.log10(TARGET_N_LOOPS)) ** 2
216 + ((d_i_nm - TARGET_D_I_NM) / TARGET_D_I_NM) ** 2
217 + (np.log10(N_voids_vis) - np.log10(TARGET_N_VOIDS)) ** 2
218 + ((d_v_nm - TARGET_D_V_NM) / TARGET_D_V_NM) ** 2
222 'dose_dpa': dose[idx],
223 'N_loops': N_loops_vis,
224 'N_voids': N_voids_vis,
225 'mean_n_i': mean_n_i_vis,
226 'mean_n_v': mean_n_v_vis,
229 'delta_FP': results[
'delta_FP'][idx],
230 'delta_He': results[
'delta_He'][idx],
241 Run one simulation with the given parameter dict.
242 Returns (cost, metrics, wall_time) or (inf, None, wall_time).
247 sim = RadClusterSimulation(
249 solver_mode=
'full_system',
250 physics_option=
'bin_moment_CD_fission',
251 he_kinetics=
'quasi_steady_state',
256 for pname, sheet, *_
in PARAM_SPEC:
258 if sheet ==
'production_fission':
259 inp.production_fission[pname] = val
260 elif sheet ==
'diffusion':
261 inp.diffusion[pname] = val
262 elif sheet ==
'reactions':
263 inp.reactions[pname] = val
266 if 'i_mobile' in params:
267 inp.reactions[
'i_mobile'] = int(params[
'i_mobile'])
268 inp.diffusion[
'i_mobile'] = int(params[
'i_mobile'])
271 inp._calculate_derived()
275 G = float(inp.reactions.get(
'G', 1e-6))
276 t_end = TARGET_DOSE / G
279 't_span': (1e-3, t_end),
287 solver_config=solver_config,
294 except Exception
as exc:
295 print(f
" [run {run_id}] EXCEPTION: {exc}")
296 cost, metrics = float(
'inf'),
None
298 wall = time.time() - t0
299 return cost, metrics, wall
341 parser = argparse.ArgumentParser(description=
'Calibrate RadCluster_2_1')
342 parser.add_argument(
'--phase1', type=int, default=20,
343 help=
'Number of Phase 1 (exploration) samples')
344 parser.add_argument(
'--phase2', type=int, default=10,
345 help=
'Number of Phase 2 (refinement) samples')
346 parser.add_argument(
'--timeout', type=int, default=180,
347 help=
'Max seconds per run')
348 parser.add_argument(
'--seed', type=int, default=42,
350 args = parser.parse_args()
352 rng = np.random.default_rng(args.seed)
353 log_path = BASE_DIR /
'output' /
'calibration_log.csv'
354 os.makedirs(log_path.parent, exist_ok=
True)
357 if log_path.exists():
365 n_dims = len(PARAM_SPEC)
369 print(f
" Phase 1: Latin Hypercube Sampling — {n1} runs")
370 print(f
" Timeout: {args.timeout}s per run")
371 print(f
" Log: {log_path}")
376 param_str =
' '.join(f
'{k}={params[k]:.4g}' for k
in PARAM_NAMES)
377 print(f
"[{i+1}/{n1}] {param_str}")
379 cost, metrics, wall =
run_single(params, timeout_s=args.timeout,
381 all_results.append((cost, params, metrics))
382 row =
build_row(run_counter,
'phase1', params, cost, metrics, wall)
385 status = f
"cost={cost:.4f}" if cost < 1e10
else "FAILED"
388 summary = (f
" N_loops={metrics['N_loops']:.2e}"
389 f
" d_i={metrics['d_i_nm']:.1f}nm"
390 f
" N_voids={metrics['N_voids']:.2e}"
391 f
" d_v={metrics['d_v_nm']:.1f}nm")
392 print(f
" -> {status} ({wall:.0f}s){summary}\n")
399 valid = [(c, p, m)
for c, p, m
in all_results
if c < 1e10]
400 valid.sort(key=
lambda x: x[0])
403 print(
"\nNo valid Phase 1 runs — skipping Phase 2.")
405 n_best = min(3, len(valid))
407 print(f
" Phase 2: Refinement — {n2} runs around top {n_best}")
412 for rank
in range(n_best):
413 _, best_p, _ = valid[rank]
414 n_per = max(1, n2 // n_best + (1
if rank < n2 % n_best
else 0))
415 for _
in range(n_per):
417 for spec
in PARAM_SPEC:
419 base_val = best_p[pname]
422 delta = 0.20 * (rng.random() * 2 - 1)
423 u_new = np.clip(u_base + delta, 0.0, 1.0)
425 refine_samples.append(perturbed)
427 for i, params
in enumerate(refine_samples):
428 param_str =
' '.join(f
'{k}={params[k]:.4g}'
429 for k
in PARAM_NAMES)
430 print(f
"[R{i+1}/{len(refine_samples)}] {param_str}")
433 params, timeout_s=args.timeout, run_id=run_counter)
434 all_results.append((cost, params, metrics))
435 row =
build_row(run_counter,
'phase2', params, cost,
439 status = f
"cost={cost:.4f}" if cost < 1e10
else "FAILED"
442 summary = (f
" N_loops={metrics['N_loops']:.2e}"
443 f
" d_i={metrics['d_i_nm']:.1f}nm"
444 f
" N_voids={metrics['N_voids']:.2e}"
445 f
" d_v={metrics['d_v_nm']:.1f}nm")
446 print(f
" -> {status} ({wall:.0f}s){summary}\n")
450 valid = [(c, p, m)
for c, p, m
in all_results
if c < 1e10]
451 valid.sort(key=
lambda x: x[0])
454 print(f
" CALIBRATION COMPLETE — {run_counter} runs, {len(valid)} valid")
458 print(
"\n No valid runs found. Try relaxing bounds or increasing timeout.")
461 print(f
"\n Experimental targets at {TARGET_DOSE} dpa:")
462 print(f
" SIA loop density: {TARGET_N_LOOPS:.1e} m^-3")
463 print(f
" SIA loop diameter: {TARGET_D_I_NM} nm")
464 print(f
" Void density: {TARGET_N_VOIDS:.1e} m^-3")
465 print(f
" Void diameter: {TARGET_D_V_NM} nm")
467 print(f
"\n Top {min(5, len(valid))} results:\n")
468 print(f
" {'Rank':>4} {'Cost':>8} {'N_loops':>10} {'d_i(nm)':>8}"
469 f
" {'N_voids':>10} {'d_v(nm)':>8}")
472 for rank, (cost, params, metrics)
in enumerate(valid[:5]):
473 print(f
" {rank+1:>4} {cost:8.4f}"
474 f
" {metrics['N_loops']:10.2e} {metrics['d_i_nm']:8.1f}"
475 f
" {metrics['N_voids']:10.2e} {metrics['d_v_nm']:8.1f}")
478 best_cost, best_params, best_metrics = valid[0]
479 print(f
"\n Best parameter set (cost={best_cost:.4f}):\n")
480 print(f
" {'Parameter':>12} {'Value':>12} {'Sheet'}")
482 for spec
in PARAM_SPEC:
483 pname, sheet = spec[0], spec[1]
484 val = best_params[pname]
485 if isinstance(val, int):
486 print(f
" {pname:>12} {val:>12} {sheet}")
488 print(f
" {pname:>12} {val:>12.4g} {sheet}")
490 print(f
"\n Full log: {log_path}\n")