134 """Construct an RadClusterSimulation with the reference parameters."""
135 if physics_option
is None:
136 physics_option = PHYSICS_OPTION
137 saved = sys.stdout, sys.stderr
138 sys.stdout = sys.stderr = io.StringIO()
140 sim = RadClusterSimulation(
142 solver_mode=solver_mode,
143 physics_option=physics_option,
145 he_kinetics=HE_KINETICS,
150 sys.stdout, sys.stderr = saved
152 saved2 = sys.stdout, sys.stderr
153 sys.stdout = sys.stderr = io.StringIO()
156 for key, val
in PARAM_OVERRIDES.items():
158 for d
in [inp.production_fission, inp.production_fusion,
159 inp.diffusion, inp.reactions,
160 inp.energetics, inp.dissociation]:
165 inp.reactions[key] = val
166 if 'i_mobile' in PARAM_OVERRIDES:
167 inp.diffusion[
'i_mobile'] = int(PARAM_OVERRIDES[
'i_mobile'])
168 inp.reactions[
'i_mobile'] = int(PARAM_OVERRIDES[
'i_mobile'])
169 inp._calculate_derived()
172 sys.stdout, sys.stderr = saved2
330 res_dict : {'mode0': results, 'mode4': results} (any key may be absent)
331 wall_dict: {'mode0': float, 'mode4': float}
332 out_path : Path to save the figure
334 modes = [k
for k
in [
'mode0',
'mode4']
if k
in res_dict
and res_dict[k]
is not None]
336 fig = plt.figure(figsize=(18, 22))
337 gs = gridspec.GridSpec(4, 3, figure=fig, hspace=0.42, wspace=0.35)
340 ax = fig.add_subplot(gs[0, 0])
343 ax.loglog(r[
'dose'], r[
'swelling']*100,
344 color=COLORS[m], ls=LS[m], lw=1.8, label=LABELS[m])
345 ax.set_xlabel(
'Dose (dpa)'); ax.set_ylabel(
'Swelling (%)')
346 ax.set_title(
'Void swelling'); ax.legend(fontsize=7)
349 ax = fig.add_subplot(gs[0, 1])
352 ax.loglog(r[
'dose'], r[
'N_loops'], color=COLORS[m], ls=LS[m], lw=1.8)
353 ax.loglog(r[
'dose'], r[
'N_voids'], color=COLORS[m], ls=LS[m], lw=1.0, alpha=0.5)
355 from matplotlib.lines
import Line2D
356 ax.add_line(Line2D([],[], color=
'k', lw=1.8, label=
'Loops (solid)'))
357 ax.add_line(Line2D([],[], color=
'k', lw=1.0, alpha=0.5, label=
'Voids (faint)'))
358 ax.set_xlabel(
'Dose (dpa)'); ax.set_ylabel(
'Number density (m⁻³)')
359 ax.set_title(
'N_loops & N_voids'); ax.legend(fontsize=7)
362 ax = fig.add_subplot(gs[0, 2])
365 ax.loglog(r[
'dose'], r[
'mean_n_i'], color=COLORS[m], ls=LS[m], lw=1.8)
366 ax.loglog(r[
'dose'], r[
'mean_n_v'], color=COLORS[m], ls=LS[m], lw=1.0, alpha=0.5)
367 ax.set_xlabel(
'Dose (dpa)'); ax.set_ylabel(
'Mean cluster size (defects)')
368 ax.set_title(
'Mean sizes (loops solid, voids faint)')
371 ax = fig.add_subplot(gs[1, 0])
374 ax.loglog(r[
'dose'], r[
'C_i1'], color=COLORS[m], ls=LS[m], lw=1.8)
375 ax.loglog(r[
'dose'], r[
'C_v1'], color=COLORS[m], ls=LS[m], lw=1.0, alpha=0.5)
376 ax.set_xlabel(
'Dose (dpa)'); ax.set_ylabel(
'Concentration (m⁻³)')
377 ax.set_title(
'Monomers C_i1 (solid) & C_v1 (faint)')
380 ax = fig.add_subplot(gs[1, 1])
383 S_I = r[
'C_SIA_tot'] * r[
'Omega']
385 ax.loglog(r[
'dose'], S_I, color=COLORS[m], ls=LS[m], lw=1.8, label=LABELS[m])
386 ax.loglog(r[
'dose'], S, color=COLORS[m], ls=LS[m], lw=1.0, alpha=0.5)
387 ax.set_xlabel(
'Dose (dpa)'); ax.set_ylabel(
'Defect content (atom fraction)')
388 ax.set_title(
'S_I SIA content (solid) & S VAC content (faint)')
389 ax.legend(fontsize=7)
392 ax = fig.add_subplot(gs[1, 2])
395 ax.loglog(r[
'dose'], np.maximum(r[
'delta_FP'], 1e-16),
396 color=COLORS[m], ls=LS[m], lw=1.8, label=
r'$\delta_{FP}$ '+LABELS[m])
397 ax.axhline(1e-6, color=
'gray', ls=
'--', lw=0.8, label=
'1e-6 threshold')
398 ax.set_xlabel(
'Dose (dpa)'); ax.set_ylabel(
r'$\delta_{FP}$ (relative)')
399 ax.set_title(
'Frenkel-pair conservation'); ax.legend(fontsize=6)
402 ref = res_dict.get(
'mode0')
403 if ref
is not None and 'mode4' in res_dict
and res_dict[
'mode4']
is not None:
404 quantities = [
'swelling',
'N_loops',
'N_voids',
'mean_n_i',
'mean_n_v',
'delta_FP']
405 q_labels = [
'Swelling',
'N_loops',
'N_voids',
r'$\bar{n}_i$',
r'$\bar{n}_v$',
r'$\delta_{FP}$']
406 q_colors = [
'C0',
'C1',
'C2',
'C3',
'C4',
'C5']
408 ax = fig.add_subplot(gs[2, 0])
409 wr = res_dict[
'mode4']
412 for qn, ql, qc
in zip(quantities, q_labels, q_colors):
414 ref_q = np.interp(t_win, t_ref, ref[qn])
417 ax.loglog(t_win, np.maximum(err, 1e-16),
418 color=qc, lw=1.4, label=ql)
421 ax.axhline(1e-3, color=
'gray', ls=
'--', lw=0.8, label=
'0.1% error')
422 ax.set_xlabel(
'Dose (dpa)')
423 ax.set_ylabel(
'|ε| relative error')
424 ax.set_title(f
'Relative error: {LABELS["mode4"]}\nvs Mode 0 reference')
425 ax.legend(fontsize=6, ncol=2)
428 ax = fig.add_subplot(gs[3, 0])
429 labels_bar = [LABELS[m]
for m
in modes
if m
in wall_dict]
430 times_bar = [wall_dict[m]
for m
in modes
if m
in wall_dict]
431 colors_bar = [COLORS[m]
for m
in modes
if m
in wall_dict]
432 bars = ax.bar(range(len(labels_bar)), times_bar, color=colors_bar, width=0.5)
433 ax.set_xticks(range(len(labels_bar)))
434 ax.set_xticklabels([l.split(
'—')[0].strip()
for l
in labels_bar],
435 rotation=15, ha=
'right', fontsize=8)
436 ax.set_ylabel(
'Wall-clock time (s)')
437 ax.set_title(
'Solver timing comparison')
438 for bar, t
in zip(bars, times_bar):
439 ax.text(bar.get_x() + bar.get_width()/2, bar.get_height()*1.02,
440 f
'{t:.0f}s', ha=
'center', va=
'bottom', fontsize=8)
442 ax.set_ylim(0, max(times_bar)*1.25)
445 ax = fig.add_subplot(gs[3, 1])
446 if 'mode0' in wall_dict
and wall_dict[
'mode0'] > 0
and 'mode4' in wall_dict:
447 speedup = wall_dict[
'mode0'] / wall_dict[
'mode4']
448 ax.bar([0], [speedup], color=COLORS[
'mode4'], width=0.4)
450 ax.set_xticklabels([LABELS[
'mode4'].split(
'—')[0].strip()],
451 rotation=15, ha=
'right', fontsize=8)
452 ax.set_ylabel(
'Speedup vs Mode 0')
453 ax.set_title(
'Speedup factor')
454 ax.axhline(1.0, color=
'gray', ls=
'--', lw=0.8)
455 ax.text(0, speedup*1.02, f
'{speedup:.1f}×',
456 ha=
'center', va=
'bottom', fontsize=9)
458 ax.text(0.5, 0.5,
'Mode 0 timing\nnot available',
459 ha=
'center', va=
'center', transform=ax.transAxes, fontsize=9)
460 ax.set_title(
'Speedup factor')
463 ax = fig.add_subplot(gs[3, 2])
468 f
' i_mobile = v_mobile = {i_mobile}',
469 f
' G = 1×10⁻⁶ dpa/s',
471 f
' t_span = (1e-6, 1e4) s',
472 f
' rtol={BASE_SOLVER_CFG["rtol"]:.0e} atol={BASE_SOLVER_CFG["atol"]:.0e}',
474 'Window parameters (Mode IV)',
475 f
' width = {WINDOW_METHOD["window_width"]} (shared SIA/VAC)',
476 f
' SIA pad={WINDOW_METHOD["window_pad"]} '
477 f
'VAC pad={WINDOW_METHOD["window_pad_v"]}',
478 f
' conc_threshold = {WINDOW_METHOD["concentration_threshold"]:.0e} (shared)',
479 f
' linsol = gmres maxl=50 (hardwired)',
480 f
' OMP threads = auto from N_eq (override via OMP_NUM_THREADS)',
482 'From I=V=1000 reference:',
483 ' SIA 99.9% content: n≤24',
484 ' VAC front at t=1e4s: m≈1000',
486 ax.text(0.05, 0.97,
'\n'.join(lines), transform=ax.transAxes,
487 fontsize=7.5, va=
'top', family=
'monospace',
488 bbox=dict(boxstyle=
'round', facecolor=
'lightyellow', alpha=0.8))
491 'Sliding-window solver comparison\n'
492 f
'Mode 0 (full_system) vs Mode IV (active_window)\n'
493 f
'I = V = {I:,} physics: {PHYSICS_OPTION} fission cascade',
496 fig.savefig(out_path, dpi=150, bbox_inches=
'tight')
497 print(f
'\nSaved comparison figure → {out_path}')
506 modes = [k
for k
in [
'mode0',
'mode4']
if k
in res_dict
and res_dict[k]
is not None]
507 fig, axes = plt.subplots(1, 2, figsize=(13, 5))
510 y = res_dict[m].get(
'y')
515 n_arr = np.arange(1, I+1)
516 m_arr = np.arange(1, V+1)
520 axes[0].loglog(n_arr[mask_i], ci[mask_i],
521 color=COLORS[m], ls=LS[m], lw=1.6, label=LABELS[m])
524 axes[1].loglog(m_arr[mask_v], cv[mask_v],
525 color=COLORS[m], ls=LS[m], lw=1.6)
527 axes[0].set_xlabel(
'SIA cluster size n')
528 axes[0].set_ylabel(
'Concentration (atom fraction)')
529 axes[0].set_title(
'SIA size distribution at t=10⁴ s')
530 axes[0].legend(fontsize=8)
532 axes[1].set_xlabel(
'Vacancy cluster size m')
533 axes[1].set_ylabel(
'Concentration (atom fraction)')
534 axes[1].set_title(
'VAC size distribution at t=10⁴ s')
537 f
'Cluster size distributions at final time — I=V={I:,}',
540 fig.savefig(out_path, dpi=150, bbox_inches=
'tight')
541 print(f
'Saved distribution figure → {out_path}')
655 ap = argparse.ArgumentParser(description=__doc__,
656 formatter_class=argparse.RawDescriptionHelpFormatter)
657 ap.add_argument(
'--ref-dir', help=
'Path to existing mode-0 output directory')
658 ap.add_argument(
'--load-mode4', help=
'Load Mode IV from this existing output directory')
659 ap.add_argument(
'--skip-mode0', action=
'store_true',
660 help=
'Skip running mode 0 (use existing reference only)')
661 ap.add_argument(
'--skip-mode4', action=
'store_true',
662 help=
'Skip running Mode IV (load existing if available)')
663 ap.add_argument(
'--wall-mode0', type=float, default=0.0,
664 help=
'Known wall-clock time for mode 0 (s), for speedup plots')
665 ap.add_argument(
'--wall-mode4', type=float, default=0.0,
666 help=
'Known wall-clock time for Mode IV (s)')
667 ap.add_argument(
'--physics', default=
'full_CD_fission',
668 choices=[
'full_CD_fission',
'full_CD_fusion',
669 'bin_moment_CD_fission',
'bin_moment_CD_fusion'],
670 help=
'Physics option for all modes (default: full_CD_fission)')
671 ap.add_argument(
'--out-dir', default=str(MODULE_ROOT /
'output' /
'window_comparison'),
672 help=
'Directory for comparison plots and report')
673 args = ap.parse_args()
676 global PHYSICS_OPTION
677 PHYSICS_OPTION = args.physics
679 out_dir = Path(args.out_dir)
680 out_dir.mkdir(parents=
True, exist_ok=
True)
686 if not args.skip_mode0:
687 r0, w0, _ =
run_mode(
'Mode 0 — full_system',
'full_system', MODE0_CFG)
688 res_dict[
'mode0'] = r0
689 wall_dict[
'mode0'] = w0
691 print(
'\nSearching for existing mode-0 reference run …')
694 res_dict[
'mode0'] = r0
695 wall_dict[
'mode0'] = args.wall_mode0
696 print(f
' Loaded from {src}')
698 print(
' No I=V=10000 mode-0 run found. Relative-error plots will be skipped.')
701 if not args.skip_mode4:
702 r4, w4, _ =
run_mode(
'Mode IV — active_window',
'active_window', MODE4_CFG)
703 res_dict[
'mode4'] = r4
704 wall_dict[
'mode4'] = w4
706 print(
'\nSearching for existing Mode IV run …')
709 res_dict[
'mode4'] = r4
710 wall_dict[
'mode4'] = args.wall_mode4
711 print(f
' Loaded from {src4}')
713 print(
' No Mode IV run found.')
718 write_report(res_dict, wall_dict, out_dir /
'comparison_report.txt')
720 print(
'\nAll done. Output in:', out_dir)