31 if actual.shape != reference.shape
or actual.shape != (120, 7):
32 raise ValueError(f
'Expected a complete 120-step history, got {actual.shape}')
33 if not np.isfinite(actual).all():
34 raise ValueError(
'Nonfinite output')
35 if not np.allclose(actual[:, :3], reference[:, :3], rtol=1.e-12, atol=1.e-12):
36 raise ValueError(
'Time/dose sampling changed')
37 error = np.abs(actual[:, 3:] - reference[:, 3:])
38 limits = ATOL + RTOL * np.abs(reference[:, 3:])
39 if np.any(error > limits):
40 step, component = np.unravel_index(np.argmax(error / limits), error.shape)
41 raise ValueError(f
'Strain/swelling regression at step {step}, component {component}: '
42 f
'{actual[step, component+3]} versus {reference[step, component+3]}')
43 if not np.allclose(actual[:, 3], actual[:, 4], rtol=1.e-9, atol=1.e-12):
44 raise ValueError(
'In-plane strain symmetry lost')
45 return dict(max_absolute_error=error.max(axis=0).tolist(), rtol=RTOL, atol=ATOL)
49 values = np.atleast_2d(np.loadtxt(report /
'F/F_0.txt'))
50 labels = (report /
'F/F_labels.txt').read_text().splitlines()
51 if not np.array_equal(values[:, 0], np.arange(120)):
52 raise ValueError(
'Missing, repeated, or unexpected run IDs')
53 if not np.isfinite(values).all():
54 raise ValueError(
'Nonfinite simulation output')
55 material = report /
'inputFiles/Zr4_Fitted.txt'
56 t = values[:, labels.index(
'time [b/cs]')]
57 conversion =
scalar(material,
'b_SI') / np.sqrt(
scalar(material,
'mu0_SI') /
scalar(material,
'rho_SI'))
58 dose = t * conversion *
scalar(material,
'doseRate_dpaPerSec')
59 strain = np.column_stack([values[:, labels.index(f
'betaP_{ij}')]
for ij
in [
'11',
'22',
'33']])
60 for ij
in [
'12',
'13',
'23',
'21',
'31',
'32']:
61 if np.max(np.abs(values[:, labels.index(f
'betaP_{ij}')])) > 1.e-10:
62 raise ValueError(
'Unexpected shear plastic distortion')
63 result = np.column_stack([t, dose, dose + .5, strain, strain.mean(axis=1)])
64 np.savetxt(report /
'growth-swelling-data.csv', result, delimiter=
',',
65 header=
'time_b_over_cs,dose_dpa,plotted_dose_dpa,beta11,beta22,beta33,trace_beta_over_3', comments=
'')
70 parser = argparse.ArgumentParser(description=__doc__)
71 parser.add_argument(
'--executable', required=
True, type=Path)
72 parser.add_argument(
'--reports', type=Path, default=HERE /
'reports')
73 args = parser.parse_args()
74 if importlib.util.find_spec(
"matplotlib")
is None:
75 parser.error(
"Matplotlib is required to generate the test plots.")
76 stamp = datetime.datetime.now(datetime.timezone.utc).strftime(
'%Y%m%dT%H%M%S.%fZ')
77 report = args.reports.resolve() / stamp
78 report.mkdir(parents=
True)
79 print(f
'Report: {report}', flush=
True)
80 shutil.copytree(HERE /
'inputFiles', report /
'inputFiles')
81 shutil.copy2(ROOT /
'Library/Meshes/unitCube_15K.msh', report /
'inputFiles/unitCube_15K.msh')
82 for name
in [
'experimental-data.csv',
'reference.csv',
'stored-tutorial-result.pdf']:
83 shutil.copy2(HERE / name, report / name)
84 for name
in [
'F',
'config',
'.cache']:
85 (report / name).mkdir()
87 'physics':
'ClusterDynamics',
'useFEM':
True,
'temperature_K': 553,
88 'steps': 120,
'dt_b_over_cs': 3.4e18,
'output_frequency': 1,
89 'mesh':
'unitCube_15K.msh',
'boxScaling': [3093, 3093, 3093],
90 'active_periodic_faces': [],
'discrete_microstructure': [],
91 'experimental_series': {
'EXPIa':
'Iodide purity, a',
'EXPIc':
'Iodide purity, c',
92 'EXPZa':
'Zone-refined purity, a',
'EXPZc':
'Zone-refined purity, c'},
93 'experimental_strain_unit':
'1e-4',
'plot_dose_offset_dpa': .5,
94 'plot_convention':
'Original script: beta components in percent; swelling trace(beta)/3 unscaled.',
95 'reference':
'Successful WeakProblem tutorial run from 2026-10-19, not a fit to experiment.',
96 'input_sha256': {p.name: hashlib.sha256(p.read_bytes()).hexdigest()
97 for p
in sorted((report /
'inputFiles').iterdir())},
99 (report /
'settings.json').write_text(json.dumps(settings, indent=2) +
'\n')
100 env = dict(os.environ, OMP_NUM_THREADS=
'12', MPLBACKEND=
'Agg',
101 MPLCONFIGDIR=str(report /
'.cache'), XDG_CACHE_HOME=str(report /
'.cache'))
102 started = time.monotonic()
103 result = {
'passed':
False,
'report': str(report)}
105 with (report /
'simulation.log').open(
'w')
as log:
106 subprocess.run([str(args.executable.resolve()), str(report)], cwd=report,
107 env=env, stdout=log, stderr=subprocess.STDOUT, check=
True, timeout=1650)
110 with (report /
'plot.log').open(
'w')
as log:
111 subprocess.run([sys.executable, str(HERE /
'plotGrowthSwellingDensity.py'), str(report)],
112 env=env, stdout=log, stderr=subprocess.STDOUT, check=
True, timeout=60)
113 result.update(
compare(actual, np.loadtxt(report /
'reference.csv', delimiter=
',', skiprows=1)))
114 result.update(passed=
True, outputs=len(actual), final_values=actual[-1].tolist())
115 except Exception
as error:
116 result[
'error'] = str(error)
117 print(f
'FAIL: {error}\nInspect {report}', file=sys.stderr)
119 result[
'elapsed_seconds'] = time.monotonic() - started
120 (report /
'result.json').write_text(json.dumps(result, indent=2) +
'\n')
121 print((
'PASS' if result[
'passed']
else 'FAIL') + f
': spatialCDtest; report={report}', flush=
True)
122 return 0
if result[
'passed']
else 1