OpenMC/tests/testing_harness.py
John Tramm f01852411d
Some checks are pending
Tests and Coverage / filter-changes (push) Waiting to run
Tests and Coverage / Python 3.13 (omp=n, mpi=n, dagmc=, libmesh=, event= (push) Blocked by required conditions
Tests and Coverage / Python 3.14 (omp=n, mpi=n, dagmc=, libmesh=, event= (push) Blocked by required conditions
Tests and Coverage / Python 3.14t (omp=n, mpi=n, dagmc=, libmesh=, event= (push) Blocked by required conditions
Tests and Coverage / Python 3.12 (omp=n, mpi=n, dagmc=n, libmesh=n, event=n (push) Blocked by required conditions
Tests and Coverage / Python 3.12 (omp=y, mpi=n, dagmc=n, libmesh=n, event=n (push) Blocked by required conditions
Tests and Coverage / Python 3.12 (omp=n, mpi=y, dagmc=n, libmesh=n, event=n (push) Blocked by required conditions
Tests and Coverage / Python 3.12 (omp=y, mpi=y, dagmc=n, libmesh=n, event=n (push) Blocked by required conditions
Tests and Coverage / Python 3.12 (omp=y, mpi=n, dagmc=, libmesh=y, event= (push) Blocked by required conditions
Tests and Coverage / Python 3.12 (omp=y, mpi=n, dagmc=, libmesh=, event=y (push) Blocked by required conditions
Tests and Coverage / Python 3.12 (omp=y, mpi=y, dagmc=y, libmesh=, event= (push) Blocked by required conditions
Tests and Coverage / Python 3.12 (omp=y, mpi=y, dagmc=, libmesh=y, event= (push) Blocked by required conditions
Tests and Coverage / coverage (push) Blocked by required conditions
Tests and Coverage / Check CI status (push) Blocked by required conditions
dockerhub-publish-develop / main (push) Waiting to run
dockerhub-publish-develop-dagmc-libmesh / main (push) Waiting to run
dockerhub-publish-develop-dagmc / main (push) Waiting to run
dockerhub-publish-develop-libmesh / main (push) Waiting to run
Random Ray Forward Flux Save in Adjoint Mode (#3962)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 22:35:27 +02:00

661 lines
23 KiB
Python

from difflib import unified_diff
import filecmp
import glob
import h5py
import hashlib
import os
import shutil
import numpy as np
import openmc
from openmc.examples import pwr_core
from colorama import Fore, init
from tests.regression_tests import config
init()
def colorize(diff):
"""Produce colored diff for test results"""
for line in diff:
if line.startswith('+'):
yield Fore.RED + line + Fore.RESET
elif line.startswith('-'):
yield Fore.GREEN + line + Fore.RESET
elif line.startswith('^'):
yield Fore.BLUE + line + Fore.RESET
else:
yield line
class TestHarness:
"""General class for running OpenMC regression tests."""
def __init__(self, statepoint_name):
self._sp_name = statepoint_name
def main(self):
"""Accept commandline arguments and either run or update tests."""
if config['update']:
self.update_results()
else:
self.execute_test()
def execute_test(self):
"""Run OpenMC with the appropriate arguments and check the outputs."""
try:
self._run_openmc()
self._test_output_created()
results = self._get_results()
self._write_results(results)
self._compare_results()
finally:
self._cleanup()
def update_results(self):
"""Update the results_true using the current version of OpenMC."""
try:
self._run_openmc()
self._test_output_created()
results = self._get_results()
self._write_results(results)
self._overwrite_results()
finally:
self._cleanup()
def _run_openmc(self):
if config['mpi']:
mpi_args = [config['mpiexec'], '-n', config['mpi_np']]
openmc.run(openmc_exec=config['exe'], mpi_args=mpi_args,
event_based=config['event'])
else:
openmc.run(openmc_exec=config['exe'], event_based=config['event'])
def _test_output_created(self):
"""Make sure statepoint.* and tallies.out have been created."""
statepoint = glob.glob(self._sp_name)
assert len(statepoint) == 1, 'Either multiple or no statepoint files' \
' exist.'
assert statepoint[0].endswith('h5'), \
'Statepoint file is not a HDF5 file.'
if os.path.exists('tallies.xml'):
assert os.path.exists('tallies.out'), \
'Tally output file does not exist.'
def _get_results(self, hash_output=False):
"""Digest info in the statepoint and return as a string."""
# Read the statepoint file.
statepoint = glob.glob(self._sp_name)[0]
with openmc.StatePoint(statepoint) as sp:
outstr = ''
if sp.run_mode == 'eigenvalue':
# Write out k-combined.
outstr += 'k-combined:\n'
form = '{0:12.6E} {1:12.6E}\n'
outstr += form.format(sp.keff.n, sp.keff.s)
# Write out tally data.
for i, tally_ind in enumerate(sp.tallies):
tally = sp.tallies[tally_ind]
results = np.zeros((tally.sum.size * 2, ))
results[0::2] = tally.sum.ravel()
results[1::2] = tally.sum_sq.ravel()
results = ['{0:12.6E}'.format(x) for x in results]
outstr += 'tally {}:\n'.format(i + 1)
outstr += '\n'.join(results) + '\n'
# Hash the results if necessary.
if hash_output:
sha512 = hashlib.sha512()
sha512.update(outstr.encode('utf-8'))
outstr = sha512.hexdigest()
return outstr
@property
def statepoint_name(self):
return self._sp_name
def _write_results(self, results_string):
"""Write the results to an ASCII file."""
with open('results_test.dat', 'w') as fh:
fh.write(results_string)
def _overwrite_results(self):
"""Overwrite the results_true with the results_test."""
shutil.copyfile('results_test.dat', 'results_true.dat')
def _compare_results(self):
"""Make sure the current results agree with the reference."""
compare = filecmp.cmp('results_test.dat', 'results_true.dat')
if not compare:
expected = open('results_true.dat').readlines()
actual = open('results_test.dat').readlines()
diff = unified_diff(expected, actual, 'results_true.dat',
'results_test.dat')
print('Result differences:')
print(''.join(colorize(diff)))
os.rename('results_test.dat', 'results_error.dat')
assert compare, 'Results do not agree'
def _cleanup(self):
"""Delete statepoints, tally, and test files."""
output = glob.glob('statepoint.*.h5')
output += ['tallies.out', 'tallies.forward.out']
output += ['results_test.dat', 'summary.h5']
output += glob.glob('volume_*.h5')
for f in output:
if os.path.exists(f):
os.remove(f)
class HashedTestHarness(TestHarness):
"""Specialized TestHarness that hashes the results."""
def _get_results(self):
"""Digest info in the statepoint and return as a string."""
return super()._get_results(True)
class CMFDTestHarness(TestHarness):
"""Specialized TestHarness for running OpenMC CMFD tests."""
def __init__(self, statepoint_name, cmfd_run):
super().__init__(statepoint_name)
self._create_cmfd_result_str(cmfd_run)
def _create_cmfd_result_str(self, cmfd_run):
"""Create CMFD result string from variables of CMFDRun instance"""
outstr = 'cmfd indices\n'
outstr += '\n'.join(['{:.6E}'.format(x) for x in cmfd_run.indices])
outstr += '\nk cmfd\n'
outstr += '\n'.join(['{:.6E}'.format(x) for x in cmfd_run.k_cmfd])
outstr += '\ncmfd entropy\n'
outstr += '\n'.join(['{:.6E}'.format(x) for x in cmfd_run.entropy])
outstr += '\ncmfd balance\n'
outstr += '\n'.join(['{:.5E}'.format(x) for x in cmfd_run.balance])
outstr += '\ncmfd dominance ratio\n'
outstr += '\n'.join(['{:.3E}'.format(x) for x in cmfd_run.dom])
outstr += '\ncmfd openmc source comparison\n'
outstr += '\n'.join(['{:.6E}'.format(x) for x in cmfd_run.src_cmp])
outstr += '\ncmfd source\n'
cmfdsrc = np.reshape(cmfd_run.cmfd_src, np.prod(cmfd_run.indices),
order='F')
outstr += '\n'.join(['{:.6E}'.format(x) for x in cmfdsrc])
outstr += '\n'
self._cmfdrun_results = outstr
def execute_test(self):
"""Don't call _run_openmc as OpenMC will be called through C API for
CMFD tests, and write CMFD results that were passsed as argument
"""
try:
self._test_output_created()
results = self._get_results()
results += self._cmfdrun_results
self._write_results(results)
self._compare_results()
finally:
self._cleanup()
def update_results(self):
"""Don't call _run_openmc as OpenMC will be called through C API for
CMFD tests, and write CMFD results that were passsed as argument
"""
try:
self._test_output_created()
results = self._get_results()
results += self._cmfdrun_results
self._write_results(results)
self._overwrite_results()
finally:
self._cleanup()
def _cleanup(self):
"""Delete output files for numpy matrices and flux vectors."""
super()._cleanup()
output = ['loss.npz', 'loss.dat', 'prod.npz', 'prod.dat',
'fluxvec.npy', 'fluxvec.dat']
for f in output:
if os.path.exists(f):
os.remove(f)
class ParticleRestartTestHarness(TestHarness):
"""Specialized TestHarness for running OpenMC particle restart tests."""
def _run_openmc(self):
# Set arguments
args = {'openmc_exec': config['exe']}
if config['mpi']:
args['mpi_args'] = [config['mpiexec'], '-n', config['mpi_np']]
# Initial run
openmc.run(**args)
# Run particle restart
args.update({'restart_file': self._sp_name})
openmc.run(**args)
def _test_output_created(self):
"""Make sure the restart file has been created."""
particle = glob.glob(self._sp_name)
assert len(particle) == 1, 'Either multiple or no particle restart ' \
'files exist.'
assert particle[0].endswith('h5'), \
'Particle restart file is not a HDF5 file.'
def _get_results(self):
"""Digest info in the statepoint and return as a string."""
# Read the particle restart file.
particle = glob.glob(self._sp_name)[0]
p = openmc.Particle(particle)
# Write out the properties.
outstr = ''
outstr += 'current batch:\n'
outstr += "{0:12.6E}\n".format(p.current_batch)
outstr += 'current generation:\n'
outstr += "{0:12.6E}\n".format(p.current_generation)
outstr += 'particle id:\n'
outstr += "{0:12.6E}\n".format(p.id)
outstr += 'run mode:\n'
outstr += "{0}\n".format(p.run_mode)
outstr += 'particle weight:\n'
outstr += "{0:12.6E}\n".format(p.weight)
outstr += 'particle energy:\n'
outstr += "{0:12.6E}\n".format(p.energy)
outstr += 'particle xyz:\n'
outstr += "{0:12.6E} {1:12.6E} {2:12.6E}\n".format(p.xyz[0], p.xyz[1],
p.xyz[2])
outstr += 'particle uvw:\n'
outstr += "{0:12.6E} {1:12.6E} {2:12.6E}\n".format(p.uvw[0], p.uvw[1],
p.uvw[2])
return outstr
def _cleanup(self):
"""Delete particle restart files."""
super()._cleanup()
output = glob.glob('particle*.h5')
for f in output:
os.remove(f)
class PyAPITestHarness(TestHarness):
def __init__(self, statepoint_name, model=None, inputs_true=None):
super().__init__(statepoint_name)
if model is None:
self._model = pwr_core()
else:
self._model = model
self._model.plots = []
self.inputs_true = "inputs_true.dat" if not inputs_true else inputs_true
def main(self):
"""Accept commandline arguments and either run or update tests."""
if config['build_inputs']:
self._build_inputs()
elif config['update']:
self.update_results()
else:
self.execute_test()
def execute_test(self, change_dir=False):
"""Build input XMLs, run OpenMC, and verify correct results."""
base_dir = os.getcwd() if change_dir else None
try:
if change_dir:
os.chdir(self.workdir)
self._build_inputs()
inputs = self._get_inputs()
self._write_inputs(inputs)
self._compare_inputs()
self._run_openmc()
self._test_output_created()
results = self._get_results()
self._write_results(results)
self._compare_results()
finally:
self._cleanup()
if base_dir:
os.chdir(base_dir)
def update_results(self, change_dir=False):
"""Update results_true.dat and inputs_true.dat"""
base_dir = os.getcwd() if change_dir else None
try:
if change_dir:
os.chdir(self.workdir)
self._build_inputs()
inputs = self._get_inputs()
self._write_inputs(inputs)
self._overwrite_inputs()
self._run_openmc()
self._test_output_created()
results = self._get_results()
self._write_results(results)
self._overwrite_results()
finally:
self._cleanup()
if base_dir:
os.chdir(base_dir)
def _build_inputs(self):
"""Write input XML files."""
self._model.export_to_model_xml()
def _get_inputs(self):
"""Return a hash digest of the input XML files."""
xmls = ['model.xml', 'plots.xml']
return ''.join([open(fname).read() for fname in xmls
if os.path.exists(fname)])
def _write_inputs(self, input_digest):
"""Write the digest of the input XMLs to an ASCII file."""
with open('inputs_test.dat', 'w') as fh:
fh.write(input_digest)
def _overwrite_inputs(self):
"""Overwrite inputs_true.dat with inputs_test.dat"""
shutil.copyfile('inputs_test.dat', self.inputs_true)
def _compare_inputs(self):
"""Make sure the current inputs agree with the _true standard."""
compare = filecmp.cmp('inputs_test.dat', self.inputs_true)
if not compare:
expected = open(self.inputs_true, 'r').readlines()
actual = open('inputs_test.dat', 'r').readlines()
diff = unified_diff(expected, actual, self.inputs_true,
'inputs_test.dat')
print('Input differences:')
print(''.join(colorize(diff)))
os.rename('inputs_test.dat', 'inputs_error.dat')
assert compare, 'Input files are broken.'
def _cleanup(self):
"""Delete XMLs, statepoints, tally, and test files."""
super()._cleanup()
output = ['materials.xml', 'geometry.xml', 'settings.xml',
'tallies.xml', 'plots.xml', 'inputs_test.dat', 'model.xml',
'collision_track.h5', 'collision_track.mcpl']
for f in output:
if os.path.exists(f):
os.remove(f)
class HashedPyAPITestHarness(PyAPITestHarness):
def _get_results(self):
"""Digest info in the statepoint and return as a string."""
return super()._get_results(True)
class TolerantPyAPITestHarness(PyAPITestHarness):
"""Specialized harness for running tests that involve significant levels
of floating point non-associativity when using shared memory parallelism
due to single precision usage (e.g., as in the random ray solver).
"""
def _are_files_equal(self, actual_path, expected_path, tolerance):
def isfloat(value):
try:
float(value)
return True
except ValueError:
return False
def tokenize(line):
return line.strip().split()
def compare_tokens(token1, token2):
if isfloat(token1) and isfloat(token2):
float1, float2 = float(token1), float(token2)
return abs(float1 - float2) <= tolerance * max(abs(float1), abs(float2))
else:
return token1 == token2
expected = open(expected_path).readlines()
actual = open(actual_path).readlines()
if len(expected) != len(actual):
return False
for line1, line2 in zip(expected, actual):
tokens1 = tokenize(line1)
tokens2 = tokenize(line2)
if len(tokens1) != len(tokens2):
return False
for token1, token2 in zip(tokens1, tokens2):
if not compare_tokens(token1, token2):
return False
return True
def _compare_results(self):
"""Make sure the current results agree with the reference."""
compare = self._are_files_equal(
'results_test.dat', 'results_true.dat', 1e-6)
if not compare:
expected = open('results_true.dat').readlines()
actual = open('results_test.dat').readlines()
diff = unified_diff(expected, actual, 'results_true.dat',
'results_test.dat')
print('Result differences:')
print(''.join(colorize(diff)))
os.rename('results_test.dat', 'results_error.dat')
assert compare, 'Results do not agree'
class WeightWindowPyAPITestHarness(PyAPITestHarness):
def _get_results(self):
"""Digest info in the weight window file and return as a string."""
ww = openmc.WeightWindowsList.from_hdf5()[0]
# Access the weight window bounds
lower_bound = ww.lower_ww_bounds
upper_bound = ww.upper_ww_bounds
# Flatten both arrays
flattened_lower_bound = lower_bound.flatten()
flattened_upper_bound = upper_bound.flatten()
# Convert each element to a string in scientific notation with 2 decimal places
formatted_lower_bound = [f'{x:.2e}' for x in flattened_lower_bound]
formatted_upper_bound = [f'{x:.2e}' for x in flattened_upper_bound]
# Concatenate the formatted arrays
concatenated_strings = ["Lower Bounds"] + formatted_lower_bound + \
["Upper Bounds"] + formatted_upper_bound
# Join the concatenated strings into a single string with newline characters
final_string = '\n'.join(concatenated_strings)
# Prepend the mesh text description and return final string
return str(ww.mesh) + final_string
def _cleanup(self):
super()._cleanup()
f = 'weight_windows.h5'
if os.path.exists(f):
os.remove(f)
class PlotTestHarness(TestHarness):
"""Specialized TestHarness for running OpenMC plotting tests."""
def __init__(self, plot_names, voxel_convert_checks=[]):
super().__init__(None)
self._plot_names = plot_names
self._voxel_convert_checks = voxel_convert_checks
def _run_openmc(self):
openmc.plot_geometry(openmc_exec=config['exe'])
# Check that voxel h5 can be converted to vtk
for voxel_h5_filename in self._voxel_convert_checks:
openmc.voxel_to_vtk(voxel_h5_filename)
def _test_output_created(self):
"""Make sure *.png has been created."""
for fname in self._plot_names:
assert os.path.exists(fname), 'Plot output file does not exist.'
def _cleanup(self):
super()._cleanup()
for fname in self._plot_names:
if os.path.exists(fname):
os.remove(fname)
def _get_results(self):
"""Return a string hash of the plot files."""
outstr = bytes()
for fname in self._plot_names:
if fname.endswith('.png'):
# Add PNG output to results
with open(fname, 'rb') as fh:
outstr += fh.read()
elif fname.endswith('.h5'):
# Add voxel data to results
with h5py.File(fname, 'r') as fh:
outstr += fh.attrs['filetype']
outstr += fh.attrs['num_voxels'].tobytes()
outstr += fh.attrs['lower_left'].tobytes()
outstr += fh.attrs['voxel_width'].tobytes()
outstr += fh['data'][()].tobytes()
# Hash the information and return.
sha512 = hashlib.sha512()
sha512.update(outstr)
outstr = sha512.hexdigest()
return outstr
class CollisionTrackTestHarness(PyAPITestHarness):
def __init__(self, statepoint_name, model=None, inputs_true=None, workdir=None):
super().__init__(statepoint_name, model, inputs_true)
self.workdir = workdir
def _test_output_created(self):
"""Make sure collision_track.h5 has also been created."""
if self._model.settings.collision_track:
assert os.path.exists(
"collision_track.h5"
), "collision_track file has not been created."
def _compare_results(self):
"""Compare collision_track.h5 files."""
if self._model.settings.collision_track:
collision_track_true = self._return_collision_track_data(
"collision_track_true.h5")
collision_track_test = self._return_collision_track_data(
"collision_track.h5")
assert collision_track_true.shape == collision_track_test.shape
np.testing.assert_allclose(
collision_track_true, collision_track_test, rtol=1e-07)
def main(self):
"""Accept commandline arguments and either run or update tests."""
if config["build_inputs"]:
self.build_inputs()
elif config["update"]:
self.update_results(change_dir=True)
else:
self.execute_test(change_dir=True)
def build_inputs(self):
"""Build inputs."""
base_dir = os.getcwd()
try:
os.chdir(self.workdir)
self._build_inputs()
finally:
os.chdir(base_dir)
def _overwrite_results(self):
"""Also add the 'collision_track.h5' file during overwriting."""
if os.path.exists("collision_track.h5"):
shutil.copyfile("collision_track.h5", "collision_track_true.h5")
def _write_results(self, results_string):
# The result file for this test are written by the OpenMC executable itself
pass
@staticmethod
def _return_collision_track_data(filepath):
"""
Read a collision_track file and return a sorted array composed of
flattened collision records.
Parameters
----------
filepath : str
Path to the collision_track file
Returns
-------
data : np.array
Sorted array composed of flattened collision-track records.
"""
source = openmc.read_collision_track_file(filepath)
columns = [
source['r']['x'],
source['r']['y'],
source['r']['z'],
source['u']['x'],
source['u']['y'],
source['u']['z'],
source['E'],
source['dE'],
source['time'],
source['wgt'],
source['event_mt'],
source['delayed_group'],
source['cell_id'],
source['nuclide_id'],
source['material_id'],
source['universe_id'],
source['n_collision'],
source['particle'],
source['parent_id'],
source['progeny_id'],
]
data = np.column_stack(columns)
# Sort by the complete record, prioritizing stable integer identifiers
# before floating-point fields. This removes dependence on the order in
# which threads append otherwise reproducible collision records.
sort_columns = [
source['parent_id'],
source['progeny_id'],
source['n_collision'],
source['particle'],
source['cell_id'],
source['material_id'],
source['universe_id'],
source['nuclide_id'],
source['event_mt'],
source['delayed_group'],
source['r']['x'],
source['r']['y'],
source['r']['z'],
source['u']['x'],
source['u']['y'],
source['u']['z'],
source['E'],
source['dE'],
source['time'],
source['wgt'],
]
sorted_idx = np.lexsort(tuple(reversed(sort_columns)))
return data[sorted_idx]