Implement a new MeshMaterialFilter (#3406)

Co-authored-by: Jonathan Shimwell <drshimwell@gmail.com>
Co-authored-by: Patrick Shriwise <pshriwise@gmail.com>
This commit is contained in:
Paul Romano 2025-06-06 16:30:50 -05:00 committed by GitHub
parent 67aa0def9d
commit 7fda3eb846
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 614 additions and 16 deletions

View file

@ -414,6 +414,7 @@ list(APPEND libopenmc_SOURCES
src/tallies/filter_materialfrom.cpp
src/tallies/filter_mesh.cpp
src/tallies/filter_meshborn.cpp
src/tallies/filter_meshmaterial.cpp
src/tallies/filter_meshsurface.cpp
src/tallies/filter_mu.cpp
src/tallies/filter_musurface.cpp

View file

@ -129,6 +129,7 @@ Constructing Tallies
openmc.SurfaceFilter
openmc.MeshFilter
openmc.MeshBornFilter
openmc.MeshMaterialFilter
openmc.MeshSurfaceFilter
openmc.EnergyFilter
openmc.EnergyoutFilter

View file

@ -33,6 +33,7 @@ enum class FilterType {
MATERIALFROM,
MESH,
MESHBORN,
MESH_MATERIAL,
MESH_SURFACE,
MU,
MUSURFACE,

View file

@ -9,9 +9,9 @@
namespace openmc {
//==============================================================================
//! Indexes the location of particle events to a regular mesh. For tracklength
//! tallies, it will produce multiple valid bins and the bin weight will
//! correspond to the fraction of the track length that lies in that bin.
//! Indexes the location of particle events to a mesh. For tracklength tallies,
//! it will produce multiple valid bins and the bin weight will correspond to
//! the fraction of the track length that lies in that bin.
//==============================================================================
class MeshFilter : public Filter {

View file

@ -0,0 +1,114 @@
#ifndef OPENMC_TALLIES_FILTER_MESHMATERIAL_H
#define OPENMC_TALLIES_FILTER_MESHMATERIAL_H
#include <cstdint>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include "openmc/position.h"
#include "openmc/random_ray/source_region.h"
#include "openmc/span.h"
#include "openmc/tallies/filter.h"
#include "openmc/vector.h"
namespace openmc {
//==============================================================================
//! Helper structs that define a combination of a mesh element index and a
//! material index and a functor for hashing to place in an unordered_map
//==============================================================================
struct ElementMat {
//! Check for equality
bool operator==(const ElementMat& other) const
{
return index_element == other.index_element && index_mat == other.index_mat;
}
int32_t index_element;
int32_t index_mat;
};
struct ElementMatHash {
std::size_t operator()(const ElementMat& k) const
{
size_t seed = 0;
hash_combine(seed, k.index_element);
hash_combine(seed, k.index_mat);
return seed;
}
};
//==============================================================================
//! Indexes the location of particle events to combinations of mesh element
//! index and material. For tracklength tallies, it will produce multiple valid
//! bins and the bin weight will correspond to the fraction of the track length
//! that lies in that bin.
//==============================================================================
class MeshMaterialFilter : public Filter {
public:
//----------------------------------------------------------------------------
// Constructors, destructors
~MeshMaterialFilter() = default;
//----------------------------------------------------------------------------
// Methods
std::string type_str() const override { return "meshmaterial"; }
FilterType type() const override { return FilterType::MESH_MATERIAL; }
void from_xml(pugi::xml_node node) override;
void get_all_bins(const Particle& p, TallyEstimator estimator,
FilterMatch& match) const override;
void to_statepoint(hid_t filter_group) const override;
std::string text_label(int bin) const override;
//----------------------------------------------------------------------------
// Accessors
int32_t mesh() const { return mesh_; }
void set_mesh(int32_t mesh);
//! Set the bins based on a flat vector of alternating element index and
//! material IDs
void set_bins(span<int32_t> bins);
//! Set the bins based on a vector of (element, material index) pairs
void set_bins(vector<ElementMat>&& bins);
virtual void set_translation(const Position& translation);
virtual void set_translation(const double translation[3]);
virtual const Position& translation() const { return translation_; }
virtual bool translated() const { return translated_; }
private:
//----------------------------------------------------------------------------
// Data members
int32_t mesh_; //!< Index of the mesh
bool translated_ {false}; //!< Whether or not the filter is translated
Position translation_ {0.0, 0.0, 0.0}; //!< Filter translation
//! The indices of the mesh element-material combinations binned by this
//! filter.
vector<ElementMat> bins_;
//! The set of materials used in this filter
std::unordered_set<int32_t> materials_;
//! A map from mesh element-material indices to filter bin indices.
std::unordered_map<ElementMat, int32_t, ElementMatHash> map_;
};
} // namespace openmc
#endif // OPENMC_TALLIES_FILTER_MESHMATERIAL_H

View file

@ -56,8 +56,8 @@ def get_microxs_and_flux(
----------
model : openmc.Model
OpenMC model object. Must contain geometry, materials, and settings.
domains : list of openmc.Material or openmc.Cell or openmc.Universe, or openmc.MeshBase
Domains in which to tally reaction rates.
domains : list of openmc.Material or openmc.Cell or openmc.Universe, or openmc.MeshBase, or openmc.Filter
Domains in which to tally reaction rates, or a spatial tally filter.
nuclides : list of str
Nuclides to get cross sections for. If not specified, all burnable
nuclides from the depletion chain file are used.
@ -103,7 +103,10 @@ def get_microxs_and_flux(
energy_filter = openmc.EnergyFilter.from_group_structure(energies)
else:
energy_filter = openmc.EnergyFilter(energies)
if isinstance(domains, openmc.MeshBase):
if isinstance(domains, openmc.Filter):
domain_filter = domains
elif isinstance(domains, openmc.MeshBase):
domain_filter = openmc.MeshFilter(domains)
elif isinstance(domains[0], openmc.Material):
domain_filter = openmc.MaterialFilter(domains)

View file

@ -25,7 +25,8 @@ _FILTER_TYPES = (
'energyout', 'mu', 'musurface', 'polar', 'azimuthal', 'distribcell', 'delayedgroup',
'energyfunction', 'cellfrom', 'materialfrom', 'legendre', 'spatiallegendre',
'sphericalharmonics', 'zernike', 'zernikeradial', 'particle', 'cellinstance',
'collision', 'time', 'parentnuclide', 'weight'
'collision', 'time', 'parentnuclide', 'weight', 'meshborn', 'meshsurface',
'meshmaterial',
)
_CURRENT_NAMES = (
@ -900,8 +901,6 @@ class MeshFilter(Filter):
@property
def shape(self):
if isinstance(self, MeshSurfaceFilter):
return (self.num_bins,)
return self.mesh.dimension
@property
@ -992,8 +991,11 @@ class MeshFilter(Filter):
XML element containing filter data
"""
element = super().to_xml_element()
element[0].text = str(self.mesh.id)
element = ET.Element('filter')
element.set('id', str(self.id))
element.set('type', self.short_name.lower())
subelement = ET.SubElement(element, 'bins')
subelement.text = str(self.mesh.id)
if self.translation is not None:
element.set('translation', ' '.join(map(str, self.translation)))
return element
@ -1039,6 +1041,181 @@ class MeshBornFilter(MeshFilter):
"""
class MeshMaterialFilter(MeshFilter):
"""Filter events by combinations of mesh elements and materials.
.. versionadded:: 0.15.3
Parameters
----------
mesh : openmc.MeshBase
The mesh object that events will be tallied onto
bins : iterable of 2-tuples or numpy.ndarray
Combinations of (mesh element, material) to tally, given as 2-tuples.
The first value in the tuple represents the index of the mesh element,
and the second value indicates the material (either a
:class:`openmc.Material` instance of the ID).
filter_id : int
Unique identifier for the filter
"""
def __init__(self, mesh: openmc.MeshBase, bins, filter_id=None):
self.mesh = mesh
self.bins = bins
self.id = filter_id
self._translation = None
@classmethod
def from_volumes(cls, mesh: openmc.MeshBase, volumes: openmc.MeshMaterialVolumes):
"""Construct a MeshMaterialFilter from a MeshMaterialVolumes object.
Parameters
----------
mesh : openmc.MeshBase
The mesh object that events will be tallied onto
volumes : openmc.MeshMaterialVolumes
The mesh material volumes to use for the filter
Returns
-------
MeshMaterialFilter
A new MeshMaterialFilter instance
"""
bins = list(zip(*np.where(volumes._materials > -1)))
return cls(mesh, bins)
def __hash__(self):
data = (type(self).__name__, self.mesh.id, tuple(self.bins.ravel()))
return hash(data)
def __repr__(self):
string = type(self).__name__ + '\n'
string += '{: <16}=\t{}\n'.format('\tID', self.id)
string += '{: <16}=\t{}\n'.format('\tMesh ID', self.mesh.id)
string += '{: <16}=\n{}\n'.format('\tBins', self.bins)
string += '{: <16}=\t{}\n'.format('\tTranslation', self.translation)
return string
@property
def shape(self):
return (self.num_bins,)
@property
def mesh(self):
return self._mesh
@mesh.setter
def mesh(self, mesh):
cv.check_type('filter mesh', mesh, openmc.MeshBase)
self._mesh = mesh
@Filter.bins.setter
def bins(self, bins):
pairs = np.empty((len(bins), 2), dtype=int)
for i, (elem, mat) in enumerate(bins):
cv.check_type('element', elem, Integral)
cv.check_type('material', mat, (Integral, openmc.Material))
pairs[i, 0] = elem
pairs[i, 1] = mat if isinstance(mat, Integral) else mat.id
self._bins = pairs
def to_xml_element(self):
"""Return XML element representing the filter.
Returns
-------
element : lxml.etree._Element
XML element containing filter data
"""
element = ET.Element('filter')
element.set('id', str(self.id))
element.set('type', self.short_name.lower())
element.set('mesh', str(self.mesh.id))
if self.translation is not None:
element.set('translation', ' '.join(map(str, self.translation)))
subelement = ET.SubElement(element, 'bins')
subelement.text = ' '.join(str(i) for i in self.bins.ravel())
return element
@classmethod
def from_xml_element(cls, elem: ET.Element, **kwargs) -> MeshMaterialFilter:
filter_id = int(elem.get('id'))
mesh_id = int(elem.get('mesh'))
mesh_obj = kwargs['meshes'][mesh_id]
bins = [int(x) for x in get_text(elem, 'bins').split()]
bins = list(zip(bins[::2], bins[1::2]))
out = cls(mesh_obj, bins, filter_id=filter_id)
translation = elem.get('translation')
if translation:
out.translation = [float(x) for x in translation.split()]
return out
@classmethod
def from_hdf5(cls, group, **kwargs):
if group['type'][()].decode() != cls.short_name.lower():
raise ValueError("Expected HDF5 data for filter type '"
+ cls.short_name.lower() + "' but got '"
+ group['type'][()].decode() + " instead")
if 'meshes' not in kwargs:
raise ValueError(cls.__name__ + " requires a 'meshes' keyword "
"argument.")
mesh_id = group['mesh'][()]
mesh_obj = kwargs['meshes'][mesh_id]
bins = group['bins'][()]
filter_id = int(group.name.split('/')[-1].lstrip('filter '))
out = cls(mesh_obj, bins, filter_id=filter_id)
translation = group.get('translation')
if translation:
out.translation = translation[()]
return out
def get_pandas_dataframe(self, data_size, stride, **kwargs):
"""Builds a Pandas DataFrame for the Filter's bins.
This method constructs a Pandas DataFrame object for the filter with
columns annotated by filter bin information. This is a helper method for
:meth:`Tally.get_pandas_dataframe`.
Parameters
----------
data_size : int
The total number of bins in the tally corresponding to this filter
stride : int
Stride in memory for the filter
Returns
-------
pandas.DataFrame
A Pandas DataFrame with a multi-index column for the cell instance.
The number of rows in the DataFrame is the same as the total number
of bins in the corresponding tally, with the filter bin appropriately
tiled to map to the corresponding tally bins.
See also
--------
Tally.get_pandas_dataframe(), CrossFilter.get_pandas_dataframe()
"""
# Repeat and tile bins as necessary to account for other filters.
bins = np.repeat(self.bins, stride, axis=0)
tile_factor = data_size // len(bins)
bins = np.tile(bins, (tile_factor, 1))
columns = pd.MultiIndex.from_product([[self.short_name.lower()],
['element', 'material']])
return pd.DataFrame(bins, columns=columns)
class MeshSurfaceFilter(MeshFilter):
"""Filter events by surface crossings on a mesh.
@ -1065,6 +1242,9 @@ class MeshSurfaceFilter(MeshFilter):
The number of filter bins
"""
@property
def shape(self):
return (self.num_bins,)
@MeshFilter.mesh.setter
def mesh(self, mesh):

View file

@ -21,10 +21,10 @@ __all__ = [
'CellInstanceFilter', 'CollisionFilter', 'DistribcellFilter', 'DelayedGroupFilter',
'EnergyFilter', 'EnergyoutFilter', 'EnergyFunctionFilter', 'LegendreFilter',
'MaterialFilter', 'MaterialFromFilter', 'MeshFilter', 'MeshBornFilter',
'MeshSurfaceFilter', 'MuFilter', 'MuSurfaceFilter', 'ParentNuclideFilter',
'ParticleFilter', 'PolarFilter', 'SphericalHarmonicsFilter',
'SpatialLegendreFilter', 'SurfaceFilter', 'UniverseFilter', 'ZernikeFilter',
'ZernikeRadialFilter', 'filters'
'MeshMaterialFilter', 'MeshSurfaceFilter', 'MuFilter', 'MuSurfaceFilter',
'ParentNuclideFilter', 'ParticleFilter', 'PolarFilter', 'SphericalHarmonicsFilter',
'SpatialLegendreFilter', 'SurfaceFilter', 'TimeFilter', 'UniverseFilter',
'WeightFilter', 'ZernikeFilter', 'ZernikeRadialFilter', 'filters'
]
# Tally functions
@ -480,6 +480,10 @@ class MeshBornFilter(Filter):
_dll.openmc_meshborn_filter_set_translation(self._index, (c_double*3)(*translation))
class MeshMaterialFilter(Filter):
filter_type = 'meshmaterial'
class MeshSurfaceFilter(Filter):
"""MeshSurface filter stored internally.
@ -606,6 +610,10 @@ class SurfaceFilter(Filter):
filter_type = 'surface'
class TimeFilter(Filter):
filter_type = 'time'
class UniverseFilter(Filter):
filter_type = 'universe'
@ -643,6 +651,7 @@ _FILTER_TYPE_MAP = {
'cellborn': CellbornFilter,
'cellfrom': CellfromFilter,
'cellinstance': CellInstanceFilter,
'collision': CollisionFilter,
'delayedgroup': DelayedGroupFilter,
'distribcell': DistribcellFilter,
'energy': EnergyFilter,
@ -653,6 +662,7 @@ _FILTER_TYPE_MAP = {
'materialfrom': MaterialFromFilter,
'mesh': MeshFilter,
'meshborn': MeshBornFilter,
'meshmaterial': MeshMaterialFilter,
'meshsurface': MeshSurfaceFilter,
'mu': MuFilter,
'musurface': MuSurfaceFilter,
@ -662,7 +672,9 @@ _FILTER_TYPE_MAP = {
'sphericalharmonics': SphericalHarmonicsFilter,
'spatiallegendre': SpatialLegendreFilter,
'surface': SurfaceFilter,
'time': TimeFilter,
'universe': UniverseFilter,
'weight': WeightFilter,
'zernike': ZernikeFilter,
'zernikeradial': ZernikeRadialFilter
}

View file

@ -1575,7 +1575,7 @@ class Tally(IDManagerMixin):
for i, f in enumerate(self.filters):
if expand_dims:
# Mesh filter indices are backwards so we need to flip them
if isinstance(f, openmc.MeshFilter):
if type(f) in {openmc.MeshFilter, openmc.MeshBornFilter}:
fshape = f.shape[::-1]
new_shape += fshape
idx0, idx1 = i, i + len(fshape) - 1

View file

@ -25,6 +25,7 @@
#include "openmc/tallies/filter_materialfrom.h"
#include "openmc/tallies/filter_mesh.h"
#include "openmc/tallies/filter_meshborn.h"
#include "openmc/tallies/filter_meshmaterial.h"
#include "openmc/tallies/filter_meshsurface.h"
#include "openmc/tallies/filter_mu.h"
#include "openmc/tallies/filter_musurface.h"
@ -133,6 +134,8 @@ Filter* Filter::create(const std::string& type, int32_t id)
return Filter::create<MeshFilter>(id);
} else if (type == "meshborn") {
return Filter::create<MeshBornFilter>(id);
} else if (type == "meshmaterial") {
return Filter::create<MeshMaterialFilter>(id);
} else if (type == "meshsurface") {
return Filter::create<MeshSurfaceFilter>(id);
} else if (type == "mu") {

View file

@ -0,0 +1,185 @@
#include "openmc/tallies/filter_meshmaterial.h"
#include <utility> // for move
#include <fmt/core.h>
#include "openmc/capi.h"
#include "openmc/constants.h"
#include "openmc/container_util.h"
#include "openmc/error.h"
#include "openmc/material.h"
#include "openmc/mesh.h"
#include "openmc/xml_interface.h"
namespace openmc {
void MeshMaterialFilter::from_xml(pugi::xml_node node)
{
// Get mesh ID
auto mesh = get_node_array<int32_t>(node, "mesh");
if (mesh.size() != 1) {
fatal_error(
"Only one mesh can be specified per " + type_str() + " mesh filter.");
}
auto id = mesh[0];
auto search = model::mesh_map.find(id);
if (search == model::mesh_map.end()) {
fatal_error(
fmt::format("Could not find mesh {} specified on tally filter.", id));
}
set_mesh(search->second);
// Get pairs of (element index, material) and set the bins
auto bins = get_node_array<int32_t>(node, "bins");
this->set_bins(bins);
if (check_for_node(node, "translation")) {
set_translation(get_node_array<double>(node, "translation"));
}
}
void MeshMaterialFilter::set_bins(span<int32_t> bins)
{
if (bins.size() % 2 != 0) {
fatal_error(
fmt::format("Size of mesh material bins is not even: {}", bins.size()));
}
// Create a vector of ElementMat pairs from the flat vector of bins
vector<ElementMat> element_mats;
for (int64_t i = 0; i < bins.size() / 2; ++i) {
int32_t element = bins[2 * i];
int32_t mat_id = bins[2 * i + 1];
auto search = model::material_map.find(mat_id);
if (search == model::material_map.end()) {
fatal_error(fmt::format(
"Could not find material {} specified on tally filter.", mat_id));
}
int32_t mat_index = search->second;
element_mats.push_back({element, mat_index});
}
this->set_bins(std::move(element_mats));
}
void MeshMaterialFilter::set_bins(vector<ElementMat>&& bins)
{
// Swap internal bins_ with the provided vector to avoid copying
bins_.swap(bins);
// Clear and update the mapping and vector of materials
materials_.clear();
map_.clear();
for (std::size_t i = 0; i < bins_.size(); ++i) {
const auto& x = bins_[i];
assert(x.index_mat >= 0);
assert(x.index_mat < model::materials.size());
materials_.insert(x.index_mat);
map_[x] = i;
}
n_bins_ = bins_.size();
}
void MeshMaterialFilter::set_mesh(int32_t mesh)
{
// perform any additional perparation for mesh tallies here
mesh_ = mesh;
model::meshes[mesh_]->prepare_for_point_location();
}
void MeshMaterialFilter::set_translation(const Position& translation)
{
translated_ = true;
translation_ = translation;
}
void MeshMaterialFilter::set_translation(const double translation[3])
{
this->set_translation({translation[0], translation[1], translation[2]});
}
void MeshMaterialFilter::get_all_bins(
const Particle& p, TallyEstimator estimator, FilterMatch& match) const
{
// If current material is not in any bins, don't bother checking
if (!contains(materials_, p.material())) {
return;
}
Position last_r = p.r_last();
Position r = p.r();
Position u = p.u();
// apply translation if present
if (translated_) {
last_r -= translation();
r -= translation();
}
if (estimator != TallyEstimator::TRACKLENGTH) {
int32_t index_element = model::meshes[mesh_]->get_bin(r);
if (index_element >= 0) {
auto search = map_.find({index_element, p.material()});
if (search != map_.end()) {
match.bins_.push_back(search->second);
match.weights_.push_back(1.0);
}
}
} else {
// First determine which elements the particle crosses (may or may not
// actually match bins so we have to adjust bins_/weight_ after)
int32_t n_start = match.bins_.size();
model::meshes[mesh_]->bins_crossed(
last_r, r, u, match.bins_, match.weights_);
int32_t n_end = match.bins_.size();
// Go through bins and weights and check which ones are actually a match
// based on the (element, material) pair. For matches, overwrite the bin.
int i = 0;
for (int j = n_start; j < n_end; ++j) {
int32_t index_element = match.bins_[j];
double weight = match.weights_[j];
auto search = map_.find({index_element, p.material()});
if (search != map_.end()) {
match.bins_[n_start + i] = search->second;
match.weights_[n_start + i] = weight;
++i;
}
}
// Resize the vectors to remove the unmatched bins
match.bins_.resize(n_start + i);
}
}
void MeshMaterialFilter::to_statepoint(hid_t filter_group) const
{
Filter::to_statepoint(filter_group);
write_dataset(filter_group, "mesh", model::meshes[mesh_]->id_);
size_t n = bins_.size();
xt::xtensor<size_t, 2> data({n, 2});
for (int64_t i = 0; i < n; ++i) {
const auto& x = bins_[i];
data(i, 0) = x.index_element;
data(i, 1) = model::materials[x.index_mat]->id_;
}
write_dataset(filter_group, "bins", data);
if (translated_) {
write_dataset(filter_group, "translation", translation_);
}
}
std::string MeshMaterialFilter::text_label(int bin) const
{
auto& x = bins_[bin];
auto& mesh = *model::meshes.at(mesh_);
return fmt::format("Mesh {}, {}, Material {}", mesh.id(),
mesh.bin_label(x.index_element), model::materials[x.index_mat]->id_);
}
} // namespace openmc

View file

@ -0,0 +1,65 @@
import numpy as np
import openmc
from pytest import approx
def test_filter_mesh_material(run_in_tmpdir):
# Create four identical materials
openmc.reset_auto_ids()
materials = []
for i in range(4):
mat = openmc.Material()
mat.add_nuclide('Fe56', 1.0)
materials.append(mat)
# Create a slab model with four cells
z_values = [-10., -5., 0., 5., 10.]
planes = [openmc.ZPlane(z) for z in z_values]
planes[0].boundary_type = 'vacuum'
planes[-1].boundary_type = 'vacuum'
regions = [+left & -right for left, right in zip(planes[:-1], planes[1:])]
cells = [openmc.Cell(fill=m, region=r) for r, m in zip(regions, materials)]
model = openmc.Model()
model.geometry = openmc.Geometry(cells)
model.settings.particles = 1_000
model.settings.batches = 5
model.settings.run_mode = 'fixed source'
# Create a mesh that does not align with all planar surfaces
mesh = openmc.RegularMesh()
mesh.lower_left = (-1., -1., -10.)
mesh.upper_right = (1., 1., 10.)
mesh.dimension = (1, 1, 5)
# Determine material volumes in each mesh element and use result to create a
# MeshMaterialFilter with corresponding bins
vols = mesh.material_volumes(model)
mmf = openmc.MeshMaterialFilter.from_volumes(mesh, vols)
expected_bins = [(0, 1), (1, 1), (1, 2), (2, 2), (2, 3), (3, 3), (3, 4), (4, 4)]
np.testing.assert_equal(mmf.bins, expected_bins)
# Create two tallies, one with a mesh filter and one with mesh-material
mesh_tally = openmc.Tally()
mesh_tally.filters = [openmc.MeshFilter(mesh)]
mesh_tally.scores = ['flux']
mesh_material_tally = openmc.Tally()
mesh_material_tally.filters = [mmf]
mesh_material_tally.scores = ['flux']
model.tallies = [mesh_tally, mesh_material_tally]
# Run model to get results on the two tallies
model.run(apply_tally_results=True)
# The sum of the flux in each mesh-material combination within a single mesh
# element should be equal to the flux in that mesh element
mesh_mean = mesh_tally.mean.ravel()
meshmat_mean = mesh_material_tally.mean.ravel()
assert mesh_mean[0] == approx(meshmat_mean[0])
assert mesh_mean[1] == approx(meshmat_mean[1] + meshmat_mean[2])
assert mesh_mean[2] == approx(meshmat_mean[3] + meshmat_mean[4])
assert mesh_mean[3] == approx(meshmat_mean[5] + meshmat_mean[6])
assert mesh_mean[4] == approx(meshmat_mean[7])
assert mesh_tally.mean.sum() == approx(mesh_material_tally.mean.sum())
# Make sure get_pandas_dataframe method works
mesh_material_tally.get_pandas_dataframe()

View file

@ -333,3 +333,36 @@ def test_weight():
new_f = openmc.Filter.from_xml_element(elem)
assert new_f.id == f.id
assert np.allclose(new_f.bins, f.bins)
def test_mesh_material():
mat1 = openmc.Material()
mat2 = openmc.Material()
mesh = openmc.RegularMesh()
mesh.lower_left = (-1., -1., -1.)
mesh.upper_right = (1., 1., 1.)
mesh.dimension = (2, 4, 1)
bins = [(0, mat1), (0, mat2), (6, mat1), (7, mat2)]
f = openmc.MeshMaterialFilter(mesh, bins)
expected_bins = [(0, mat1.id), (0, mat2.id), (6, mat1.id), (7, mat2.id)]
assert np.allclose(f.bins, expected_bins)
assert f.mesh == mesh
assert f.shape == (4,)
# to_xml_element()
elem = f.to_xml_element()
assert elem.tag == 'filter'
assert elem.attrib['type'] == 'meshmaterial'
# from_xml_element()
new_f = openmc.Filter.from_xml_element(elem, meshes={mesh.id: mesh})
assert isinstance(new_f, openmc.MeshMaterialFilter)
assert new_f.id == f.id
assert new_f.mesh == f.mesh
assert np.allclose(new_f.bins, expected_bins)
# Test hash and str
hash(f)
str(f)