OpenMC/tests/unit_tests/test_source_mesh.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

361 lines
13 KiB
Python
Raw Permalink Normal View History

from itertools import product
from pathlib import Path
import pytest
import numpy as np
import openmc
import openmc.lib
from tests import cdtemp
###################
# MeshSpatial Tests
###################
TETS_PER_VOXEL = 12
2022-12-27 12:21:01 -06:00
# This test uses a geometry file with cells that match a regular mesh. Each cell
# in the geometry corresponds to 12 tetrahedra in the unstructured mesh file.
@pytest.fixture
def model():
openmc.reset_auto_ids()
### Materials ###
materials = openmc.Materials()
water_mat = openmc.Material(name="water")
water_mat.add_nuclide("H1", 2.0)
water_mat.add_nuclide("O16", 1.0)
water_mat.set_density("atom/b-cm", 0.07416)
materials.append(water_mat)
### Geometry ###
# This test uses a geometry file that resembles a regular mesh.
# 12 tets are used to match each voxel in the geometry.
2022-12-27 11:45:11 -06:00
# create a regular mesh that matches the superimposed mesh
regular_mesh = openmc.RegularMesh(mesh_id=10)
regular_mesh.lower_left = (-10, -10, -10)
regular_mesh.dimension = (10, 10, 10)
regular_mesh.width = (2, 2, 2)
root_cell, _ = regular_mesh.build_cells(bc=['vacuum']*6)
2022-12-27 11:45:11 -06:00
geometry = openmc.Geometry(root=[root_cell])
### Settings ###
settings = openmc.Settings()
settings.run_mode = 'fixed source'
settings.particles = 100
settings.batches = 2
return openmc.Model(geometry=geometry,
materials=materials,
settings=settings)
2022-12-27 11:45:11 -06:00
### Setup test cases ###
param_values = (['libmesh', 'moab'], # mesh libraries
['uniform', 'manual']) # Element weighting schemes
test_cases = []
for i, (lib, schemes) in enumerate(product(*param_values)):
test_cases.append({'library' : lib,
'source_strengths' : schemes})
def ids(params):
2022-12-27 11:45:11 -06:00
"""Test naming function for clarity"""
return f"{params['library']}-{params['source_strengths']}"
@pytest.mark.parametrize("test_cases", test_cases, ids=ids)
def test_unstructured_mesh_sampling(model, request, test_cases):
# skip the test if the library is not enabled
if test_cases['library'] == 'moab' and not openmc.lib._dagmc_enabled():
pytest.skip("DAGMC (and MOAB) mesh not enabled in this build.")
if test_cases['library'] == 'libmesh' and not openmc.lib._libmesh_enabled():
pytest.skip("LibMesh is not enabled in this build.")
# setup mesh source ###
mesh_filename = Path(request.fspath).parent / "test_mesh_tets.e"
uscd_mesh = openmc.UnstructuredMesh(mesh_filename, test_cases['library'])
2022-12-27 11:45:11 -06:00
# subtract one to account for root cell produced by RegularMesh.build_cells
n_cells = len(model.geometry.get_all_cells()) - 1
# set source weights according to test case
if test_cases['source_strengths'] == 'uniform':
vol_norm = True
strengths = None
elif test_cases['source_strengths'] == 'manual':
vol_norm = False
# assign random weights
strengths = np.random.rand(n_cells*TETS_PER_VOXEL)
# create the spatial distribution based on the mesh
space = openmc.stats.MeshSpatial(uscd_mesh, strengths, vol_norm)
energy = openmc.stats.Discrete(x=[15.e+06], p=[1.0])
source = openmc.IndependentSource(space=space, energy=energy)
model.settings.source = source
with cdtemp([mesh_filename]):
model.export_to_xml()
n_measurements = 100
n_samples = 1000
cell_counts = np.zeros((n_cells, n_measurements))
# This model contains 1000 geometry cells. Each cell is a hex
# corresponding to 12 of the tets. This test runs 1000 samples. This
# results in the following average for each cell
openmc.lib.init([])
# perform many sets of samples and track counts for each cell
for m in range(n_measurements):
sites = openmc.lib.sample_external_source(n_samples)
cells = [openmc.lib.find_cell(s.r) for s in sites]
for c in cells:
2022-12-27 11:45:11 -06:00
# subtract one from index to account for root cell
cell_counts[c[0]._index - 1, m] += 1
# make sure particle transport is successful
openmc.lib.run()
openmc.lib.finalize()
# normalize cell counts to get sampling frequency per particle
cell_counts /= n_samples
# get the mean and std. dev. of the cell counts
mean = cell_counts.mean(axis=1)
std_dev = cell_counts.std(axis=1)
if test_cases['source_strengths'] == 'uniform':
exp_vals = np.ones(n_cells) / n_cells
else:
# sum up the source strengths for each tet, these are the expected true mean
# of the sampling frequency for that cell
exp_vals = strengths.reshape(-1, 12).sum(axis=1) / sum(strengths)
diff = np.abs(mean - exp_vals)
2023-01-13 08:45:48 -06:00
assert((diff < 2*std_dev).sum() / diff.size >= 0.95)
assert((diff < 6*std_dev).sum() / diff.size >= 0.997)
2022-12-27 12:21:01 -06:00
def test_strengths_size_failure(request, model):
# setup mesh source ###
mesh_filename = Path(request.fspath).parent / "test_mesh_tets.e"
uscd_mesh = openmc.UnstructuredMesh(mesh_filename, 'libmesh')
# intentionally incorrectly sized to trigger an error
n_cells = len(model.geometry.get_all_cells())
strengths = np.random.rand(n_cells*TETS_PER_VOXEL)
# create the spatial distribution based on the mesh
space = openmc.stats.MeshSpatial(uscd_mesh, strengths)
energy = openmc.stats.Discrete(x=[15.e+06], p=[1.0])
source = openmc.IndependentSource(space=space, energy=energy)
model.settings.source = source
# skip the test if unstructured mesh is not available
if not openmc.lib._libmesh_enabled():
if openmc.lib._dagmc_enabled():
source.space.mesh.library = 'moab'
else:
pytest.skip("Unstructured mesh support unavailable.")
# make sure that an incorrrectly sized strengths array causes a failure
source.space.strengths = source.space.strengths[:-1]
mesh_filename = Path(request.fspath).parent / source.space.mesh.filename
with pytest.raises(RuntimeError, match=r'strengths array'), cdtemp([mesh_filename]):
model.export_to_xml()
openmc.run()
def test_roundtrip(run_in_tmpdir, model, request):
if not openmc.lib._libmesh_enabled() and not openmc.lib._dagmc_enabled():
pytest.skip("Unstructured mesh is not enabled in this build.")
mesh_filename = Path(request.fspath).parent / 'test_mesh_tets.e'
ucd_mesh = openmc.UnstructuredMesh(mesh_filename, library='libmesh')
if not openmc.lib._libmesh_enabled():
ucd_mesh.library = 'moab'
n_cells = len(model.geometry.get_all_cells())
space_out = openmc.MeshSpatial(ucd_mesh)
space_out.strengths = np.random.rand(n_cells*TETS_PER_VOXEL)
model.settings.source = openmc.IndependentSource(space=space_out)
# write out the model
model.export_to_xml()
model_in = openmc.Model.from_xml()
space_in = model_in.settings.source[0].space
np.testing.assert_equal(space_out.strengths, space_in.strengths)
assert space_in.mesh.id == space_out.mesh.id
assert space_in.volume_normalized == space_out.volume_normalized
###################
# MeshSource tests
###################
@pytest.mark.parametrize('mesh_type', ('rectangular', 'cylindrical'))
def test_mesh_source_independent(run_in_tmpdir, mesh_type):
"""
A void model containing a single box
"""
min, max = -10, 10
box = openmc.model.RectangularParallelepiped(
min, max, min, max, min, max, boundary_type='vacuum')
geometry = openmc.Geometry([openmc.Cell(region=-box)])
settings = openmc.Settings()
settings.particles = 100
settings.batches = 10
settings.run_mode = 'fixed source'
model = openmc.Model(geometry=geometry, settings=settings)
# define a 2 x 2 x 2 mesh
if mesh_type == 'rectangular':
mesh = openmc.RegularMesh.from_domain(model.geometry, (2, 2, 2))
elif mesh_type == 'cylindrical':
mesh = openmc.CylindricalMesh.from_domain(model.geometry, (1, 4, 2))
energy = openmc.stats.Discrete([1.e6], [1.0])
# create sources with only one non-zero strength for the source in the mesh
# voxel occupying the lowest octant. Direct source particles straight out of
# the problem from there. This demonstrates that
# 1) particles are only being sourced within the intented mesh voxel based
# on source strength
# 2) particles are respecting the angle distributions assigned to each voxel
sources = np.empty(mesh.dimension, dtype=openmc.SourceBase)
centroids = mesh.centroids
x, y, z = np.swapaxes(mesh.centroids, -1, 0)
for i, j, k in mesh.indices:
# mesh.indices is currently one-indexed, adjust for Python arrays
ijk = (i-1, j-1, k-1)
# get the centroid of the ijk mesh element and use it to set the
# direction of the source directly out of the problem
centroid = centroids[ijk]
vec = np.sign(centroid, dtype=float)
vec /= np.linalg.norm(vec)
angle = openmc.stats.Monodirectional(vec)
sources[ijk] = openmc.IndependentSource(energy=energy, angle=angle, strength=0.0)
# create and apply the mesh source
mesh_source = openmc.MeshSource(mesh, sources)
model.settings.source = mesh_source
# tally the flux on the mesh
mesh_filter = openmc.MeshFilter(mesh)
tally = openmc.Tally()
tally.filters = [mesh_filter]
tally.scores = ['flux']
model.tallies = openmc.Tallies([tally])
# for each element, set a single-non zero source with particles
# traveling out of the mesh (and geometry) w/o crossing any other
# mesh elements
for i, j, k in mesh.indices:
ijk = (i-1, j-1, k-1)
# zero-out all source strengths and set the strength
# on the element of interest
mesh_source.strength = 0.0
mesh_source.sources[ijk].strength = 1.0
sp_file = model.run()
with openmc.StatePoint(sp_file) as sp:
tally_out = sp.get_tally(id=tally.id)
mean = tally_out.get_reshaped_data(expand_dims=True)
# remove nuclides and scores axes
mean = mean[..., 0, 0]
# the mesh elment with a non-zero source strength should have a value
assert mean[ijk] != 0
# all other values should be zero
mean[ijk] = 0
assert np.all(mean == 0), f'Failed on index {ijk} with centroid {mesh.centroids[ijk]}'
# test roundtrip
xml_model = openmc.Model.from_model_xml()
xml_source = xml_model.settings.source[0]
assert isinstance(xml_source, openmc.MeshSource)
assert xml_source.strength == 1.0
assert isinstance(xml_source.mesh, type(mesh_source.mesh))
assert xml_source.mesh.dimension == mesh_source.mesh.dimension
assert xml_source.mesh.id == mesh_source.mesh.id
assert len(xml_source.sources) == len(mesh_source.sources)
# check strength adjustment methods
assert mesh_source.strength == 1.0
mesh_source.strength = 100.0
assert mesh_source.strength == 100.0
mesh_source.normalize_source_strengths()
assert mesh_source.strength == 1.0
def test_mesh_source_file(run_in_tmpdir):
# Creating a source file with a single particle
source_particle = openmc.SourceParticle(time=10.0)
openmc.write_source_file([source_particle], 'source.h5')
file_source = openmc.FileSource('source.h5')
model = openmc.Model()
rect_prism = openmc.model.RectangularParallelepiped(
-5.0, 5.0, -5.0, 5.0, -5.0, 5.0, boundary_type='vacuum')
mat = openmc.Material()
mat.add_nuclide('H1', 1.0)
model.geometry = openmc.Geometry([openmc.Cell(fill=mat, region=-rect_prism)])
model.settings.particles = 1000
model.settings.batches = 10
model.settings.run_mode = 'fixed source'
mesh = openmc.RegularMesh()
mesh.lower_left = (-1, -2, -3)
mesh.upper_right = (2, 3, 4)
mesh.dimension = (1, 1, 1)
mesh_source_arr = np.asarray([file_source]).reshape(mesh.dimension)
source = openmc.MeshSource(mesh, mesh_source_arr)
model.settings.source = source
model.export_to_model_xml()
openmc.lib.init()
openmc.lib.simulation_init()
sites = openmc.lib.sample_external_source(10)
openmc.lib.simulation_finalize()
openmc.lib.finalize()
# The mesh bounds do not contain the point of the lone source site in the
# file source, so it should not appear in the set of source sites produced
# from the mesh source. Additionally, the source should be located within
# the mesh
bbox = mesh.bounding_box
for site in sites:
assert site.r != (0, 0, 0)
assert site.E == source_particle.E
assert site.u == source_particle.u
assert site.time == source_particle.time
assert site.r in bbox