DAGMC Cell Override Updates (#3888)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Paul Romano <paul.k.romano@gmail.com>
This commit is contained in:
Patrick Shriwise 2026-05-20 16:43:21 -05:00 committed by GitHub
parent 66497e76b1
commit 7d09a12606
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 689 additions and 264 deletions

View file

@ -406,24 +406,55 @@ Each ``<dagmc_universe>`` element can have the following attributes or sub-eleme
*Default*: None
:material_overrides:
This element contains information on material overrides to be applied to the
DAGMC universe. It has the following attributes and sub-elements:
:cell:
Material override information for a single cell. It contains the following
Zero or more ``<cell>`` sub-elements may appear to override properties of
individual DAGMC volumes. Each ``<cell>`` element supports the following
attributes and sub-elements:
:id:
The cell ID in the DAGMC geometry for which the material override will
apply.
The integer cell ID in the DAGMC geometry to override. Required.
:materials:
A list of material IDs that will apply to instances of the cell. If the
list contains only one ID, it will replace the original material
assignment of all instances of the DAGMC cell. If the list contains more
than one material, each material ID of the list will be assigned to the
various instances of the DAGMC cell.
:name:
An optional string label for the cell.
*Default*: None
:material:
The material ID to assign to this cell. Use ``void`` for vacuum. Multiple
space-separated IDs may be given to specify a distribmat (distributed
material) assignment. Required.
:temperature:
Temperature(s) in [K] to assign to the cell. Must be ≥ 0. Multiple
space-separated values may be given.
*Default*: None
:density:
Density in [g/cm³] to assign to the cell. Must be > 0. Requires a non-void
material fill. Multiple space-separated values may be given.
*Default*: None
:volume:
Volume of the cell in [cm³].
.. note:: DAGMC can compute cell volumes exactly from the triangulated
mesh surfaces. Specifying a manual volume risks inconsistency
with that capability.
*Default*: None
The following standard ``<cell>`` attributes are **not** supported inside
``<dagmc_universe>`` and will raise an error if present: ``region``,
``fill``, ``universe``, ``translation``, ``rotation``.
.. deprecated::
The ``<material_overrides>`` sub-element (containing ``<cell_override>``
children with ``<material_ids>``) is deprecated. A deprecation warning is
emitted and the overrides are converted to the ``<cell>`` format at parse
time. It is an error to specify both ``<material_overrides>`` and
``<cell>`` sub-elements on the same ``<dagmc_universe>``.
*Default*: None

View file

@ -145,6 +145,30 @@ private:
bool simple_; //!< Does the region contain only intersections?
};
//==============================================================================
// XML parsing helpers for <cell> nodes
//==============================================================================
//! Parse material IDs from a <cell> XML node.
//! \param node XML node containing a "material" attribute or child element
//! \param cell_id Cell ID used in error messages
//! \return Vector of material IDs (MATERIAL_VOID for "void")
vector<int32_t> parse_cell_material_xml(pugi::xml_node node, int32_t cell_id);
//! Parse temperatures in [K] from a <cell> XML node.
//! Validates that all values are non-negative and the list is non-empty.
//! \param node XML node containing a "temperature" attribute or child element
//! \param cell_id Cell ID used in error messages
//! \return Vector of temperatures in [K]
vector<double> parse_cell_temperature_xml(pugi::xml_node node, int32_t cell_id);
//! Parse densities in [g/cm³] from a <cell> XML node.
//! Validates that all values are positive and the list is non-empty.
//! \param node XML node containing a "density" attribute or child element
//! \param cell_id Cell ID used in error messages
//! \return Vector of densities in [g/cm³]
vector<double> parse_cell_density_xml(pugi::xml_node node, int32_t cell_id);
//==============================================================================
class Cell {

View file

@ -94,6 +94,10 @@ private:
class DAGUniverse : public Universe {
public:
using MaterialOverrides = std::unordered_map<int32_t, vector<int32_t>>;
using TemperatureOverrides = std::unordered_map<int32_t, vector<double>>;
using DensityOverrides = std::unordered_map<int32_t, vector<double>>;
explicit DAGUniverse(pugi::xml_node node);
//! Create a new DAGMC universe
@ -112,6 +116,9 @@ public:
//! Initialize the DAGMC accel. data structures, indices, material
//! assignments, etc.
void initialize();
void initialize(const MaterialOverrides& material_overrides,
const TemperatureOverrides& temperature_overrides,
const DensityOverrides& density_overrides = {});
//! Reads UWUW materials and returns an ID map
void read_uwuw_materials();
@ -146,7 +153,8 @@ public:
//! Assign a material overriding normal assignement to a cell
//! \param[in] c The OpenMC cell to which the material is assigned
void override_assign_material(std::unique_ptr<DAGCell>& c) const;
void override_assign_material(std::unique_ptr<DAGCell>& c,
const MaterialOverrides& material_overrides) const;
//! Return the index into the model cells vector for a given DAGMC volume
//! handle in the universe
@ -187,7 +195,9 @@ private:
void set_id(); //!< Deduce the universe id from model::universes
void init_dagmc(); //!< Create and initialise DAGMC pointer
void init_metadata(); //!< Create and initialise dagmcMetaData pointer
void init_geometry(); //!< Create cells and surfaces from DAGMC entities
void init_geometry(const MaterialOverrides& material_overrides,
const TemperatureOverrides& temperature_overrides,
const DensityOverrides& density_overrides);
std::string
filename_; //!< Name of the DAGMC file used to create this universe
@ -201,11 +211,6 @@ private:
//!< generate new material IDs for the universe
bool has_graveyard_; //!< Indicates if the DAGMC geometry has a "graveyard"
//!< volume
std::unordered_map<int32_t, vector<int32_t>>
material_overrides_; //!< Map of material overrides
//!< keys correspond to the DAGMCCell id
//!< values are a list of material ids used
//!< for the override
};
//==============================================================================

View file

@ -36,12 +36,6 @@ class DAGMCUniverse(openmc.UniverseBase):
auto_mat_ids : bool
Set IDs automatically on initialization (True) or report overlaps in ID
space between OpenMC and UWUW materials (False)
material_overrides : dict, optional
A dictionary of material overrides. The keys are material name strings
and the values are Iterables of openmc.Material objects. If a material
name is found in the DAGMC file, the material will be replaced with the
openmc.Material object in the value.
Attributes
----------
id : int
@ -78,15 +72,6 @@ class DAGMCUniverse(openmc.UniverseBase):
The number of surfaces in the model.
.. versionadded:: 0.13.2
material_overrides : dict
A dictionary of material overrides. Keys are cell IDs; values are
iterables of :class:`openmc.Material` objects. The material assignment
of each DAGMC cell ID key will be replaced with the
:class:`~openmc.Material` object in the value. If the value contains
multiple :class:`~openmc.Material` objects, each Material in the list
will be assigned to the corresponding instance of the cell.
.. versionadded:: 0.15.1
"""
def __init__(self,
@ -94,16 +79,12 @@ class DAGMCUniverse(openmc.UniverseBase):
universe_id=None,
name='',
auto_geom_ids=False,
auto_mat_ids=False,
material_overrides=None):
auto_mat_ids=False):
super().__init__(universe_id, name)
# Initialize class attributes
self.filename = filename
self.auto_geom_ids = auto_geom_ids
self.auto_mat_ids = auto_mat_ids
self._material_overrides = {}
if material_overrides is not None:
self.material_overrides = material_overrides
def __repr__(self):
string = super().__repr__()
@ -130,47 +111,17 @@ class DAGMCUniverse(openmc.UniverseBase):
@property
def material_overrides(self):
return self._material_overrides
raise AttributeError(
"DAGMCUniverse.material_overrides has been removed. Use "
"DAGMCCell objects added via add_cell() to manage per-cell "
"material assignments.")
@material_overrides.setter
def material_overrides(self, val):
cv.check_type('material overrides', val, Mapping)
for key, value in val.items():
self.add_material_override(key, value)
def replace_material_assignment(self, material_name: str, material: openmc.Material):
"""Replace a material assignment within the DAGMC universe.
Replace the material assignment of all cells filled with a material in
the DAGMC universe. The universe must be synchronized in an initialized
Model (see :meth:`~openmc.DAGMCUniverse.sync_dagmc_cells`) before
calling this method.
.. versionadded:: 0.15.1
Parameters
----------
material_name : str
Material name to replace
material : openmc.Material
Material to replace the material_name with
"""
if material_name not in self.material_names:
raise ValueError(
f"No material with name '{material_name}' found in the DAGMC universe")
if not self.cells:
raise RuntimeError("This DAGMC universe has not been synchronized "
"on an initialized Model.")
for cell in self.cells.values():
if cell.fill is None:
continue
if isinstance(cell.fill, openmc.Iterable):
cell.fill = list(map(lambda x: material if x.name == material_name else x, cell.fill))
else:
cell.fill = material if cell.fill.name == material_name else cell.fill
raise AttributeError(
"DAGMCUniverse.material_overrides has been removed. Use "
"DAGMCCell objects added via add_cell() to manage per-cell "
"material assignments.")
def add_material_override(self, key, overrides=None):
"""Add a material override to the universe.
@ -201,7 +152,10 @@ class DAGMCUniverse(openmc.UniverseBase):
if key not in self.cells:
raise ValueError(f"Cell ID '{key}' not found in DAGMC universe")
self._material_overrides[key] = overrides
if len(overrides) == 1:
self.cells[key].fill = overrides[0]
else:
self.cells[key].fill = list(overrides)
@property
def auto_geom_ids(self):
@ -290,12 +244,6 @@ class DAGMCUniverse(openmc.UniverseBase):
memo.add(self)
# Ensure that the material overrides are up-to-date
for cell in self.cells.values():
if cell.fill is None:
continue
self.add_material_override(cell, cell.fill)
# Set xml element values
dagmc_element = ET.Element('dagmc_universe')
dagmc_element.set('id', str(self.id))
@ -307,17 +255,10 @@ class DAGMCUniverse(openmc.UniverseBase):
if self.auto_mat_ids:
dagmc_element.set('auto_mat_ids', 'true')
dagmc_element.set('filename', str(self.filename))
if self._material_overrides:
mat_element = ET.Element('material_overrides')
for key in self._material_overrides:
cell_overrides = ET.Element('cell_override')
cell_overrides.set("id", str(key))
material_element = ET.Element('material_ids')
material_element.text = ' '.join(
str(t.id) for t in self._material_overrides[key])
cell_overrides.append(material_element)
mat_element.append(cell_overrides)
dagmc_element.append(mat_element)
if self.cells:
for cell in self.cells.values():
cell_element = cell.create_xml_subelement(xml_element, memo)
dagmc_element.append(cell_element)
xml_element.append(dagmc_element)
def bounding_region(
@ -471,21 +412,56 @@ class DAGMCUniverse(openmc.UniverseBase):
out.auto_geom_ids = bool(get_text(elem, "auto_geom_ids"))
out.auto_mat_ids = bool(get_text(elem, "auto_mat_ids"))
el_mat_override = elem.find('material_overrides')
if el_mat_override is not None:
if mats is None:
raise ValueError("Material overrides found in DAGMC universe "
"but no materials were provided to populate "
"the mapping.")
out._material_overrides = {}
for elem in el_mat_override.findall('cell_override'):
cell_id = int(get_text(elem, 'id'))
mat_ids = get_elem_list(elem, "material_ids", str) or []
mat_objs = [mats[mat_id] for mat_id in mat_ids]
out._material_overrides[cell_id] = mat_objs
has_overrides = elem.find('material_overrides') is not None
has_cells = elem.find('cell') is not None
if has_overrides and has_cells:
raise ValueError(
"DAGMCUniverse cannot specify both <material_overrides> and "
"<cell> sub-elements. Use <cell> elements only.")
if has_overrides:
warnings.warn(
"DAGMCUniverse <material_overrides> is deprecated and will be "
"removed in a future version. Use nested <cell> elements "
"instead.", DeprecationWarning, stacklevel=2)
out._parse_legacy_material_overrides(elem, mats)
elif has_cells:
out._parse_cell_overrides(elem, mats)
return out
def _parse_legacy_material_overrides(self, elem, mats):
"""Parse the deprecated <material_overrides> XML format and populate
the universe with equivalent DAGMCCell objects."""
if mats is None:
raise ValueError(
"DAGMC material overrides found but no materials were "
"provided to populate the mapping.")
mo_elem = elem.find('material_overrides')
for co_elem in mo_elem.findall('cell_override'):
cell_id = int(get_text(co_elem, 'id'))
mat_ids = co_elem.find('material_ids').text.split()
fill_objs = [mats[mid] for mid in mat_ids]
fill = fill_objs[0] if len(fill_objs) == 1 else fill_objs
if cell_id in self.cells:
raise ValueError(
f"Duplicate DAGMC cell override specified for cell {cell_id}.")
self.add_cell(DAGMCCell(cell_id=cell_id, fill=fill))
def _parse_cell_overrides(self, elem, mats):
if mats is None:
raise ValueError("DAGMC cell overrides found in DAGMC universe but "
"no materials were provided to populate the "
"mapping.")
for cell_elem in elem.findall('cell'):
cell_id = int(get_text(cell_elem, 'id'))
if cell_id in self.cells:
raise ValueError(
f"Duplicate DAGMC cell override specified for cell {cell_id}.")
DAGMCCell.from_xml_element(cell_elem, mats, self)
def _partial_deepcopy(self):
"""Clone all of the openmc.DAGMCUniverse object's attributes except for
its cells, as they are copied within the clone function. This should
@ -565,7 +541,13 @@ class DAGMCUniverse(openmc.UniverseBase):
fill = [mats_per_id[mat.id] for mat in dag_cell.fill if mat]
else:
fill = mats_per_id[dag_cell.fill.id] if dag_cell.fill else None
self.add_cell(openmc.DAGMCCell(cell_id=dag_cell_id, fill=fill))
name = dag_cell.name
if dag_cell_id in self._cells:
self._cells[dag_cell_id].name = name
self._cells[dag_cell_id].fill = fill
else:
self.add_cell(
openmc.DAGMCCell(cell_id=dag_cell_id, name=name, fill=fill))
@add_plot_params
def plot(self, *args, **kwargs):
@ -594,6 +576,14 @@ class DAGMCCell(openmc.Cell):
DAG_parent_universe : int
The parent universe of the cell.
Notes
-----
DAGMC geometries are composed of triangulated surfaces, which means cell
volumes can in principle be computed exactly (e.g. via mesh-based
integration). Manually specifying :attr:`volume` overrides any such
calculation and may introduce inconsistencies if the value does not
accurately reflect the true geometric volume.
"""
def __init__(self, cell_id=None, name='', fill=None):
super().__init__(cell_id, name, fill, None)
@ -625,8 +615,62 @@ class DAGMCCell(openmc.Cell):
raise TypeError("plot is not available for DAGMC cells.")
def create_xml_subelement(self, xml_element, memo=None):
raise TypeError("create_xml_subelement is not available for DAGMC cells.")
if self.fill_type not in ('void', 'material', 'distribmat'):
raise TypeError("DAGMC cell overrides currently only support "
"material fills.")
if self.temperature is not None and self.fill_type not in (
'material', 'distribmat'
):
raise TypeError("DAGMC cell temperature overrides require a "
"material fill.")
if self.density is not None and self.fill_type not in ('material', 'distribmat'):
raise TypeError("DAGMC cell density overrides require a "
"material fill.")
if any(getattr(self, attr) is not None for attr in ('translation', 'rotation')):
raise TypeError("DAGMC cell overrides do not support translation "
"or rotation.")
return super().create_xml_subelement(xml_element, memo)
@classmethod
def from_xml_element(cls, elem, surfaces, materials, get_universe):
raise TypeError("from_xml_element is not available for DAGMC cells.")
def from_xml_element(cls, elem, mats, universe):
"""Generate a DAGMCCell from an XML <cell> override element.
Parameters
----------
elem : lxml.etree._Element
`<cell>` element containing a DAGMC cell property override
mats : dict
Dictionary mapping material ID strings to :class:`openmc.Material`
instances
universe : DAGMCUniverse
Universe to add the parsed cell to.
Returns
-------
DAGMCCell
DAGMCCell instance
"""
if not isinstance(universe, DAGMCUniverse):
raise TypeError(
f"universe must be a DAGMCUniverse instance, "
f"got {type(universe).__name__}.")
cell_id = int(get_text(elem, 'id'))
# Validate attributes that are unsupported for DAGMC cell overrides
for tag in ('region', 'fill', 'universe'):
if get_text(elem, tag) is not None:
raise ValueError(
f"DAGMC cell {cell_id} override cannot specify '{tag}'.")
for tag in ('translation', 'rotation'):
if get_text(elem, tag) is not None:
raise ValueError(
f"DAGMC cell {cell_id} override does not support "
f"'{tag}'.")
if get_elem_list(elem, 'material', str) is None:
raise ValueError(
f"DAGMC cell {cell_id} must specify a material override.")
return super().from_xml_element(
elem, surfaces={}, materials=mats,
get_universe=lambda _: universe)

View file

@ -467,6 +467,8 @@ class Model:
This method iterates over all DAGMC universes in the geometry and
synchronizes their cells with the current material assignments. Requires
that the model has been initialized via :meth:`Model.init_lib`.
Synchronized DAGMC cells can then be edited and exported as nested
`<cell>` overrides inside each `<dagmc_universe>` element.
.. versionadded:: 0.15.1

View file

@ -340,6 +340,63 @@ void Cell::to_hdf5(hid_t cell_group) const
close_group(group);
}
//==============================================================================
// XML parsing helpers for <cell> nodes
//==============================================================================
vector<int32_t> parse_cell_material_xml(pugi::xml_node node, int32_t cell_id)
{
vector<std::string> mats {
get_node_array<std::string>(node, "material", true)};
if (mats.empty()) {
fatal_error(fmt::format(
"An empty material element was specified for cell {}", cell_id));
}
vector<int32_t> material;
material.reserve(mats.size());
for (const auto& mat : mats) {
if (mat == "void") {
material.push_back(MATERIAL_VOID);
} else {
material.push_back(std::stoi(mat));
}
}
return material;
}
vector<double> parse_cell_temperature_xml(pugi::xml_node node, int32_t cell_id)
{
auto temperatures = get_node_array<double>(node, "temperature");
if (temperatures.empty()) {
fatal_error(fmt::format(
"An empty temperature element was specified for cell {}", cell_id));
}
for (auto T : temperatures) {
if (T < 0) {
fatal_error(fmt::format(
"Cell {} was specified with a negative temperature", cell_id));
}
}
return temperatures;
}
vector<double> parse_cell_density_xml(pugi::xml_node node, int32_t cell_id)
{
auto densities = get_node_array<double>(node, "density");
if (densities.empty()) {
fatal_error(fmt::format(
"An empty density element was specified for cell {}", cell_id));
}
for (auto rho : densities) {
if (rho <= 0) {
fatal_error(fmt::format(
"Cell {} was specified with a density less than or equal to zero",
cell_id));
}
}
return densities;
}
//==============================================================================
// CSGCell implementation
//==============================================================================
@ -390,26 +447,12 @@ CSGCell::CSGCell(pugi::xml_node cell_node)
// universe), more than one material (distribmats), and some materials may
// be "void".
if (material_present) {
vector<std::string> mats {
get_node_array<std::string>(cell_node, "material", true)};
if (mats.size() > 0) {
material_.reserve(mats.size());
for (std::string mat : mats) {
if (mat.compare("void") == 0) {
material_.push_back(MATERIAL_VOID);
} else {
material_.push_back(std::stoi(mat));
}
}
} else {
fatal_error(fmt::format(
"An empty material element was specified for cell {}", id_));
}
material_ = parse_cell_material_xml(cell_node, id_);
}
// Read the temperature element which may be distributed like materials.
if (check_for_node(cell_node, "temperature")) {
sqrtkT_ = get_node_array<double>(cell_node, "temperature");
sqrtkT_ = parse_cell_temperature_xml(cell_node, id_);
sqrtkT_.shrink_to_fit();
// Make sure this is a material-filled cell.
@ -420,14 +463,6 @@ CSGCell::CSGCell(pugi::xml_node cell_node)
id_));
}
// Make sure all temperatures are non-negative.
for (auto T : sqrtkT_) {
if (T < 0) {
fatal_error(fmt::format(
"Cell {} was specified with a negative temperature", id_));
}
}
// Convert to sqrt(k*T).
for (auto& T : sqrtkT_) {
T = std::sqrt(K_BOLTZMANN * T);
@ -440,7 +475,7 @@ CSGCell::CSGCell(pugi::xml_node cell_node)
// Note: calculating the actual density multiplier is deferred until materials
// are finalized. density_mult_ contains the true density in the meantime.
if (check_for_node(cell_node, "density")) {
density_mult_ = get_node_array<double>(cell_node, "density");
density_mult_ = parse_cell_density_xml(cell_node, id_);
density_mult_.shrink_to_fit();
// Make sure this is a material-filled cell.
@ -461,15 +496,6 @@ CSGCell::CSGCell(pugi::xml_node cell_node)
id_));
}
}
// Make sure all densities are non-negative and greater than zero.
for (auto rho : density_mult_) {
if (rho <= 0) {
fatal_error(fmt::format(
"Cell {} was specified with a density less than or equal to zero",
id_));
}
}
}
// Read the region specification.

View file

@ -50,6 +50,10 @@ namespace openmc {
DAGUniverse::DAGUniverse(pugi::xml_node node)
{
MaterialOverrides material_overrides;
TemperatureOverrides temperature_overrides;
DensityOverrides density_overrides;
if (check_for_node(node, "id")) {
id_ = std::stoi(get_node_value(node, "id"));
} else {
@ -76,24 +80,77 @@ DAGUniverse::DAGUniverse(pugi::xml_node node)
adjust_material_ids_ = get_node_value_bool(node, "auto_mat_ids");
}
// get material assignment overloading
if (check_for_node(node, "material_overrides")) {
auto mat_node = node.child("material_overrides");
// loop over all subelements (each subelement corresponds to a material)
for (pugi::xml_node cell_node : mat_node.children("cell_override")) {
// Store assignment reference name
int32_t ref_assignment = std::stoi(get_node_value(cell_node, "id"));
// Get material assignment overrides from nested DAGMC cell elements.
if (node.child("cell")) {
for (pugi::xml_node cell_node : node.children("cell")) {
if (!check_for_node(cell_node, "id")) {
fatal_error(
"Must specify id for each DAGMC cell override in <dagmc_universe>.");
}
// Get mat name for each assignement instances
vector<int32_t> instance_mats =
get_node_array<int32_t>(cell_node, "material_ids");
int32_t cell_id = std::stoi(get_node_value(cell_node, "id"));
// Store mat name for each instances
material_overrides_.emplace(ref_assignment, instance_mats);
if (check_for_node(cell_node, "region")) {
fatal_error(fmt::format(
"DAGMC cell {} override cannot specify a region.", cell_id));
}
if (check_for_node(cell_node, "fill")) {
fatal_error(fmt::format(
"DAGMC cell {} override currently only supports material fills.",
cell_id));
}
if (check_for_node(cell_node, "universe")) {
fatal_error(fmt::format(
"DAGMC cell {} override cannot specify a universe.", cell_id));
}
if (check_for_node(cell_node, "translation") ||
check_for_node(cell_node, "rotation")) {
fatal_error(fmt::format(
"DAGMC cell {} override does not support translation or rotation.",
cell_id));
}
if (!check_for_node(cell_node, "material")) {
fatal_error(fmt::format(
"DAGMC cell {} override must specify material.", cell_id));
}
auto inserted = material_overrides.emplace(
cell_id, parse_cell_material_xml(cell_node, cell_id));
if (!inserted.second) {
fatal_error(fmt::format(
"Duplicate DAGMC cell override specified for cell {}", cell_id));
}
if (check_for_node(cell_node, "temperature")) {
temperature_overrides.emplace(
cell_id, parse_cell_temperature_xml(cell_node, cell_id));
}
if (check_for_node(cell_node, "density")) {
density_overrides.emplace(
cell_id, parse_cell_density_xml(cell_node, cell_id));
}
}
} else if (check_for_node(node, "material_overrides")) {
if (node.child("cell")) {
fatal_error("DAGMCUniverse cannot specify both <material_overrides> and "
"<cell> sub-elements. Use <cell> elements only.");
}
warning("DAGMCUniverse <material_overrides> is deprecated. Use nested "
"<cell> elements under <dagmc_universe> instead.");
for (pugi::xml_node co :
node.child("material_overrides").children("cell_override")) {
int32_t cell_id = std::stoi(get_node_value(co, "id"));
std::istringstream iss(co.child("material_ids").text().get());
vector<int32_t> mats;
for (std::string s; iss >> s;) {
mats.push_back(s == "void" ? MATERIAL_VOID : std::stoi(s));
}
material_overrides.emplace(cell_id, mats);
}
}
initialize();
initialize(material_overrides, temperature_overrides, density_overrides);
}
DAGUniverse::DAGUniverse(
@ -110,9 +167,12 @@ DAGUniverse::DAGUniverse(std::shared_ptr<moab::DagMC> dagmc_ptr,
: dagmc_instance_(dagmc_ptr), filename_(filename),
adjust_geometry_ids_(auto_geom_ids), adjust_material_ids_(auto_mat_ids)
{
MaterialOverrides material_overrides;
TemperatureOverrides temperature_overrides;
DensityOverrides density_overrides;
set_id();
init_metadata();
init_geometry();
init_geometry(material_overrides, temperature_overrides, density_overrides);
}
void DAGUniverse::set_id()
@ -130,6 +190,15 @@ void DAGUniverse::set_id()
}
void DAGUniverse::initialize()
{
MaterialOverrides material_overrides;
TemperatureOverrides temperature_overrides;
initialize(material_overrides, temperature_overrides);
}
void DAGUniverse::initialize(const MaterialOverrides& material_overrides,
const TemperatureOverrides& temperature_overrides,
const DensityOverrides& density_overrides)
{
#ifdef OPENMC_UWUW_ENABLED
// read uwuw materials from the .h5m file if present
@ -140,7 +209,7 @@ void DAGUniverse::initialize()
init_metadata();
init_geometry();
init_geometry(material_overrides, temperature_overrides, density_overrides);
}
void DAGUniverse::init_dagmc()
@ -176,7 +245,9 @@ void DAGUniverse::init_metadata()
MB_CHK_ERR_CONT(rval);
}
void DAGUniverse::init_geometry()
void DAGUniverse::init_geometry(const MaterialOverrides& material_overrides,
const TemperatureOverrides& temperature_overrides,
const DensityOverrides& density_overrides)
{
moab::ErrorCode rval;
@ -202,6 +273,9 @@ void DAGUniverse::init_geometry()
: dagmc_instance_->id_by_index(3, c->dag_index());
c->universe_ = this->id_;
c->fill_ = C_NONE; // no fill, single universe
if (dagmc_instance_->is_implicit_complement(vol_handle)) {
c->name_ = "implicit complement";
}
auto in_map = model::cell_map.find(c->id_);
if (in_map == model::cell_map.end()) {
@ -230,17 +304,69 @@ void DAGUniverse::init_geometry()
if (mat_str == "graveyard") {
graveyard = vol_handle;
}
// material void checks
if (mat_str == "void" || mat_str == "vacuum" || mat_str == "graveyard") {
if (material_overrides.count(c->id_)) {
override_assign_material(c, material_overrides);
} else if (mat_str == "void" || mat_str == "vacuum" ||
mat_str == "graveyard") {
c->material_.push_back(MATERIAL_VOID);
} else {
if (material_overrides_.count(c->id_)) {
override_assign_material(c);
} else if (uses_uwuw()) {
uwuw_assign_material(vol_handle, c);
} else {
legacy_assign_material(mat_str, c);
}
if (temperature_overrides.count(c->id_)) {
if (c->material_.empty() || c->material_[0] == MATERIAL_VOID) {
fatal_error(fmt::format("DAGMC cell {} was specified with a "
"temperature but no non-void material.",
c->id_));
}
c->sqrtkT_.clear();
const auto& temp_overrides = temperature_overrides.at(c->id_);
c->sqrtkT_.reserve(temp_overrides.size());
for (auto T : temp_overrides) {
c->sqrtkT_.push_back(std::sqrt(K_BOLTZMANN * T));
}
if (settings::verbosity >= 10) {
std::stringstream override_values;
for (size_t i = 0; i < temp_overrides.size(); ++i) {
if (i > 0) {
override_values << " ";
}
override_values << temp_overrides[i];
}
auto msg = fmt::format("Overriding DAGMC cell {} property "
"'temperature [K]' with value(s): {}",
c->id_, override_values.str());
write_message(msg, 10);
}
}
if (density_overrides.count(c->id_)) {
if (c->material_.empty() || c->material_[0] == MATERIAL_VOID) {
fatal_error(fmt::format("DAGMC cell {} was specified with a density "
"but no non-void material.",
c->id_));
}
// density_mult_ holds the true density until materials are finalized,
// at which point it is converted to a proper multiplier (same as CSG).
c->density_mult_ = density_overrides.at(c->id_);
if (settings::verbosity >= 10) {
const auto& dens = density_overrides.at(c->id_);
std::stringstream override_values;
for (size_t i = 0; i < dens.size(); ++i) {
if (i > 0)
override_values << " ";
override_values << dens[i];
}
write_message(fmt::format("Overriding DAGMC cell {} property "
"'density [g/cm³]' with value(s): {}",
c->id_, override_values.str()),
10);
}
}
// check for temperature assignment
@ -252,8 +378,10 @@ void DAGUniverse::init_geometry()
continue;
}
// assign cell temperature
const auto& mat = model::materials[model::material_map.at(c->material_[0])];
// assign cell temperature if not explicitly overridden
if (c->sqrtkT_.empty()) {
const auto& mat =
model::materials[model::material_map.at(c->material_[0])];
if (dagmc_instance_->has_prop(vol_handle, "temp")) {
rval = dagmc_instance_->prop_value(vol_handle, "temp", temp_value);
MB_CHK_ERR_CONT(rval);
@ -265,6 +393,7 @@ void DAGUniverse::init_geometry()
c->sqrtkT_.push_back(
std::sqrt(K_BOLTZMANN * settings::temperature_default));
}
}
model::cells.emplace_back(std::move(c));
}
@ -630,7 +759,8 @@ void DAGUniverse::uwuw_assign_material(
#endif // OPENMC_UWUW_ENABLED
}
void DAGUniverse::override_assign_material(std::unique_ptr<DAGCell>& c) const
void DAGUniverse::override_assign_material(std::unique_ptr<DAGCell>& c,
const MaterialOverrides& material_overrides) const
{
// if Cell ID matches an override key, use it to override the material
// assignment else if UWUW is used, get the material assignment from the DAGMC
@ -638,17 +768,30 @@ void DAGUniverse::override_assign_material(std::unique_ptr<DAGCell>& c) const
// Notify User that an override is being applied on a DAGMCCell
write_message(fmt::format("Applying override for DAGMCCell {}", c->id_), 8);
const auto& mat_overrides = material_overrides.at(c->id_);
if (settings::verbosity >= 10) {
auto msg = fmt::format("Assigning DAGMC cell {} material(s) based on "
"override information (see input XML).",
c->id_);
std::stringstream override_values;
for (size_t i = 0; i < mat_overrides.size(); ++i) {
if (i > 0) {
override_values << " ";
}
if (mat_overrides[i] == MATERIAL_VOID) {
override_values << "void";
} else {
override_values << mat_overrides[i];
}
}
auto msg = fmt::format("Overriding DAGMC cell {} property 'material' "
"with value(s): {}",
c->id_, override_values.str());
write_message(msg, 10);
}
// Override the material assignment for each cell instance using the legacy
// assignement
for (auto mat_id : material_overrides_.at(c->id_)) {
if (model::material_map.find(mat_id) == model::material_map.end()) {
for (auto mat_id : mat_overrides) {
if (mat_id != MATERIAL_VOID &&
model::material_map.find(mat_id) == model::material_map.end()) {
fatal_error(fmt::format(
"Material with ID '{}' not found for DAGMC cell {}", mat_id, c->id_));
}

View file

@ -70,28 +70,20 @@ def model(request):
openmc.reset_auto_ids()
def test_dagmc_replace_material_assignment(model):
mats = {}
mats["foo"] = openmc.Material(name="foo")
mats["foo"].add_nuclide("H1", 2.0)
mats["foo"].add_element("O", 1.0)
mats["foo"].set_density("g/cm3", 1.0)
mats["foo"].add_s_alpha_beta("c_H_in_H2O")
def test_dagmc_sync_cell_names(model):
dag_univ = None
for univ in model.geometry.get_all_universes().values():
if not isinstance(univ, openmc.DAGMCUniverse):
if isinstance(univ, openmc.DAGMCUniverse):
dag_univ = univ
break
cells_with_41 = []
for cell in univ.cells.values():
if cell.fill is None:
continue
if cell.fill.name == "41":
cells_with_41.append(cell.id)
univ.replace_material_assignment("41", mats["foo"])
for cell_id in cells_with_41:
assert univ.cells[cell_id] == mats["foo"]
assert dag_univ is not None
for cell_id, cell in dag_univ.cells.items():
assert cell.name == openmc.lib.cells[cell_id].name
assert any(cell.name == "implicit complement"
for cell in dag_univ.cells.values())
def test_dagmc_add_material_override_with_id(model):
@ -114,7 +106,7 @@ def test_dagmc_add_material_override_with_id(model):
cells_with_41.append(cell.id)
univ.add_material_override(cell.id, mats["foo"])
for cell_id in cells_with_41:
assert univ.cells[cell_id] == mats["foo"]
assert univ.cells[cell_id].fill == mats["foo"]
def test_dagmc_add_material_override_with_cell(model):
@ -137,7 +129,7 @@ def test_dagmc_add_material_override_with_cell(model):
cells_with_41.append(cell.id)
univ.add_material_override(cell, mats["foo"])
for cell_id in cells_with_41:
assert univ.cells[cell_id] == mats["foo"]
assert univ.cells[cell_id].fill == mats["foo"]
def test_model_differentiate_depletable_with_dagmc(model, run_in_tmpdir):
@ -174,57 +166,20 @@ def test_model_differentiate_with_dagmc(model):
assert len(model.materials) == 4*2 + 4
def test_bad_override_cell_id(model):
for univ in model.geometry.get_all_universes().values():
if isinstance(univ, openmc.DAGMCUniverse):
break
with pytest.raises(ValueError, match="Cell ID '1' not found in DAGMC universe"):
univ.material_overrides = {1: model.materials[0]}
def test_bad_override_type(model):
not_a_dag_cell = openmc.Cell()
for univ in model.geometry.get_all_universes().values():
if isinstance(univ, openmc.DAGMCUniverse):
break
with pytest.raises(ValueError, match="Unrecognized key type. Must be an integer or openmc.DAGMCCell object"):
univ.material_overrides = {not_a_dag_cell: model.materials[0]}
def test_bad_replacement_mat_name(model):
for univ in model.geometry.get_all_universes().values():
if isinstance(univ, openmc.DAGMCUniverse):
break
with pytest.raises(ValueError, match="No material with name 'not_a_mat' found in the DAGMC universe"):
univ.replace_material_assignment("not_a_mat", model.materials[0])
def test_dagmc_xml(model):
# Set the environment
mats = {}
mats["no-void fuel"] = openmc.Material(1, name="no-void fuel")
mats["no-void fuel"].add_nuclide("U235", 0.03)
mats["no-void fuel"].add_nuclide("U238", 0.97)
mats["no-void fuel"].add_nuclide("O16", 2.0)
mats["no-void fuel"].set_density("g/cm3", 10.0)
mats[5] = openmc.Material(name="41")
mats[5].add_nuclide("H1", 2.0)
mats[5].add_element("O", 1.0)
mats[5].set_density("g/cm3", 1.0)
mats[5].add_s_alpha_beta("c_H_in_H2O")
override_mat = openmc.Material(name="41")
override_mat.add_nuclide("H1", 2.0)
override_mat.add_element("O", 1.0)
override_mat.set_density("g/cm3", 1.0)
override_mat.add_s_alpha_beta("c_H_in_H2O")
model.materials.append(override_mat)
for univ in model.geometry.get_all_universes().values():
if isinstance(univ, openmc.DAGMCUniverse):
dag_univ = univ
break
for k, v in mats.items():
if isinstance(k, int):
dag_univ.add_material_override(k, v)
model.materials.append(v)
elif isinstance(k, str):
dag_univ.replace_material_assignment(k, v)
dag_univ.add_material_override(5, override_mat)
# Tesing the XML subelement generation
root = ET.Element('dagmc_universe')
@ -236,12 +191,24 @@ def test_dagmc_xml(model):
assert dagmc_ele.get('filename') == str(dag_univ.filename)
assert dagmc_ele.get('auto_geom_ids') == str(dag_univ.auto_geom_ids).lower()
override_eles = dagmc_ele.find('material_overrides').findall('cell_override')
assert len(override_eles) == 4
assert dagmc_ele.find('material_overrides') is None
for i, override_ele in enumerate(override_eles):
cell_id = override_ele.get('id')
assert dag_univ.material_overrides[int(cell_id)][0].id == int(override_ele.find('material_ids').text)
override_elements = dagmc_ele.findall('cell')
assert len(override_elements) == len(dag_univ.cells)
xml_cells = {int(elem.get('id')): elem for elem in override_elements}
for cell_id, cell in dag_univ.cells.items():
assert cell_id in xml_cells
xml_cell = xml_cells[cell_id]
if cell.fill_type == 'void':
assert xml_cell.get('material') == 'void'
elif cell.fill_type == 'material':
assert xml_cell.get('material') == str(cell.fill.id)
elif cell.fill_type == 'distribmat':
mat_list = xml_cell.find('material').text.split()
expected = ["void" if m is None else str(m.id) for m in cell.fill]
assert mat_list == expected
else:
pytest.fail(f"Unexpected DAGMC cell fill type: {cell.fill_type}")
model.export_to_model_xml()
@ -252,7 +219,147 @@ def test_dagmc_xml(model):
xml_dagmc_univ = univ
break
assert xml_dagmc_univ._material_overrides.keys() == dag_univ._material_overrides.keys()
assert xml_dagmc_univ.cells.keys() == dag_univ.cells.keys()
for xml_mats, model_mats in zip(xml_dagmc_univ._material_overrides.values(), dag_univ._material_overrides.values()):
assert all([xml_mat.id == orig_mat.id for xml_mat, orig_mat in zip(xml_mats, model_mats)])
for cell_id, cell in dag_univ.cells.items():
xml_cell = xml_dagmc_univ.cells[cell_id]
assert xml_cell.fill_type == cell.fill_type
if cell.fill_type == 'void':
assert xml_cell.fill is None
elif cell.fill_type == 'material':
assert xml_cell.fill.id == cell.fill.id
elif cell.fill_type == 'distribmat':
xml_ids = [m.id if m is not None else None for m in xml_cell.fill]
model_ids = [m.id if m is not None else None for m in cell.fill]
assert xml_ids == model_ids
else:
pytest.fail(f"Unexpected DAGMC cell fill type: {cell.fill_type}")
def test_dagmc_xml_reject_fill_override():
mats = {'1': openmc.Material(1), 'void': None}
elem = ET.fromstring(
'<dagmc_universe id="1" filename="dagmc.h5m">'
'<cell id="1" fill="2"/>'
'</dagmc_universe>'
)
with pytest.raises(ValueError, match="cannot specify 'fill'"):
openmc.DAGMCUniverse.from_xml_element(elem, mats)
def test_dagmc_xml_reject_region_override():
mats = {'1': openmc.Material(1), 'void': None}
elem = ET.fromstring(
'<dagmc_universe id="1" filename="dagmc.h5m">'
'<cell id="1" material="1" region="-1"/>'
'</dagmc_universe>'
)
with pytest.raises(ValueError, match="cannot specify 'region'"):
openmc.DAGMCUniverse.from_xml_element(elem, mats)
def _legacy_xml(cell_overrides):
"""Helper to build a <dagmc_universe> with old-format <material_overrides>."""
inner = ''.join(
f'<cell_override id="{cid}"><material_ids>{mids}</material_ids></cell_override>'
for cid, mids in cell_overrides.items()
)
return ET.fromstring(
f'<dagmc_universe id="1" filename="dagmc.h5m">'
f'<material_overrides>{inner}</material_overrides>'
f'</dagmc_universe>'
)
def test_dagmc_xml_legacy_single_material_compat():
mat = openmc.Material(1)
mats = {'1': mat, 'void': None}
elem = _legacy_xml({3: '1'})
with pytest.warns(DeprecationWarning, match="deprecated"):
univ = openmc.DAGMCUniverse.from_xml_element(elem, mats)
assert 3 in univ.cells
assert univ.cells[3].fill is mat
def test_dagmc_xml_legacy_distribmat_compat():
mat1, mat2 = openmc.Material(2), openmc.Material(3)
mats = {'2': mat1, '3': mat2, 'void': None}
elem = _legacy_xml({5: '2 3'})
with pytest.warns(DeprecationWarning):
univ = openmc.DAGMCUniverse.from_xml_element(elem, mats)
assert univ.cells[5].fill_type == 'distribmat'
assert list(univ.cells[5].fill) == [mat1, mat2]
def test_dagmc_xml_legacy_void_compat():
mats = {'void': None}
elem = _legacy_xml({7: 'void'})
with pytest.warns(DeprecationWarning):
univ = openmc.DAGMCUniverse.from_xml_element(elem, mats)
assert univ.cells[7].fill_type == 'void'
def test_dagmc_xml_legacy_both_raises():
mat = openmc.Material(1)
mats = {'1': mat, 'void': None}
elem = ET.fromstring(
'<dagmc_universe id="1" filename="dagmc.h5m">'
'<material_overrides>'
'<cell_override id="3"><material_ids>1</material_ids></cell_override>'
'</material_overrides>'
'<cell id="5" material="1"/>'
'</dagmc_universe>'
)
with pytest.raises(ValueError, match="both"):
openmc.DAGMCUniverse.from_xml_element(elem, mats)
def test_dagmc_xml_legacy_deprecation_warning():
mats = {'1': openmc.Material(1), 'void': None}
elem = _legacy_xml({3: '1'})
with pytest.warns(DeprecationWarning):
openmc.DAGMCUniverse.from_xml_element(elem, mats)
def test_dagmc_xml_legacy_roundtrip():
"""Old-format XML loads correctly and re-exports using the new <cell> format."""
mat = openmc.Material(1)
mats = {'1': mat, 'void': None}
elem = _legacy_xml({3: '1'})
with pytest.warns(DeprecationWarning):
univ = openmc.DAGMCUniverse.from_xml_element(elem, mats)
root = ET.Element('geometry')
univ.create_xml_subelement(root)
dagmc_elem = root.find('dagmc_universe')
assert dagmc_elem.find('material_overrides') is None
cell_elems = dagmc_elem.findall('cell')
assert len(cell_elems) == 1
assert int(cell_elems[0].get('id')) == 3
assert cell_elems[0].get('material') == '1'
def test_dagmc_xml_temperature_roundtrip():
mat = openmc.Material(1)
mats = {'1': mat, 'void': None}
elem = ET.fromstring(
'<dagmc_universe id="10" filename="dagmc.h5m">'
'<cell id="7" material="1" temperature="825.0"/>'
'</dagmc_universe>'
)
dag_univ = openmc.DAGMCUniverse.from_xml_element(elem, mats)
assert dag_univ.cells[7].fill.id == 1
assert dag_univ.cells[7].temperature == pytest.approx(825.0)
root = ET.Element('geometry')
dag_univ.create_xml_subelement(root)
dagmc_elem = root.find('dagmc_universe')
xml_cell = dagmc_elem.find('cell')
assert xml_cell.get('temperature') == '825.0'
dag_univ_roundtrip = openmc.DAGMCUniverse.from_xml_element(dagmc_elem, mats)
assert dag_univ_roundtrip.cells[7].fill.id == 1
assert dag_univ_roundtrip.cells[7].temperature == pytest.approx(825.0)

View file

@ -374,6 +374,49 @@ def test_rotation_from_xml(rotation):
np.testing.assert_allclose(new_cell.rotation, cell.rotation)
def test_dagmccell_from_xml_element():
"""DAGMCCell.from_xml_element parses material, temperature, density,
and volume; rejects unsupported attributes."""
mat = openmc.Material(1)
mat.add_nuclide('U235', 1.0)
mats = {'1': mat}
# In practice, from_xml_element is always called from DAGMCUniverse
# during XML parsing. A placeholder universe is used here so the test
# can exercise the method directly without a real DAGMC model file.
placeholder_univ = openmc.DAGMCUniverse('model.h5m')
# material + temperature + density round-trip
xml = '<cell id="5" name="fuel" material="1" temperature="900.0" density="10.5"/>'
cell = openmc.DAGMCCell.from_xml_element(ET.fromstring(xml), mats, placeholder_univ)
assert cell.id == 5
assert cell.name == 'fuel'
assert cell.fill is mat
assert cell.density == 10.5
assert cell.temperature == 900.0
# volume round-trip
xml = '<cell id="6" material="1" volume="42.0"/>'
cell = openmc.DAGMCCell.from_xml_element(ET.fromstring(xml), mats, placeholder_univ)
assert cell.volume == 42.0
# forbidden: region, fill, universe
for tag, val in [('region', '-1'), ('fill', '2'), ('universe', '0')]:
xml = f'<cell id="7" material="1" {tag}="{val}"/>'
with pytest.raises(ValueError, match=tag):
openmc.DAGMCCell.from_xml_element(ET.fromstring(xml), mats, placeholder_univ)
# forbidden: translation, rotation
for tag, val in [('translation', '1 0 0'), ('rotation', '0 0 90')]:
xml = f'<cell id="7" material="1" {tag}="{val}"/>'
with pytest.raises(ValueError, match=tag):
openmc.DAGMCCell.from_xml_element(ET.fromstring(xml), mats, placeholder_univ)
# missing material raises
xml = '<cell id="8"/>'
with pytest.raises(ValueError, match='material'):
openmc.DAGMCCell.from_xml_element(ET.fromstring(xml), mats, placeholder_univ)
def test_plot(run_in_tmpdir):
zcyl = openmc.ZCylinder()
c = openmc.Cell(region=-zcyl)