diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5973c3cd48d..c5051b8d0bb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-22.04 strategy: matrix: - python-version: ["3.10"] + python-version: ["3.11"] mpi: [n, y] omp: [n, y] dagmc: [n] @@ -35,34 +35,34 @@ jobs: vectfit: [n] include: - - python-version: "3.11" + - python-version: "3.12" omp: n mpi: n - - python-version: "3.12" + - python-version: "3.13" omp: n mpi: n - dagmc: y - python-version: "3.10" + python-version: "3.11" mpi: y omp: y - ncrystal: y - python-version: "3.10" + python-version: "3.11" mpi: n omp: n - libmesh: y - python-version: "3.10" + python-version: "3.11" mpi: y omp: y - libmesh: y - python-version: "3.10" + python-version: "3.11" mpi: n omp: y - event: y - python-version: "3.10" + python-version: "3.11" omp: y mpi: n - vectfit: y - python-version: "3.10" + python-version: "3.11" omp: n mpi: y name: "Python ${{ matrix.python-version }} (omp=${{ matrix.omp }}, diff --git a/.readthedocs.yaml b/.readthedocs.yaml index b7b69f9fe59..7b69b64b626 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -3,7 +3,7 @@ version: 2 build: os: "ubuntu-20.04" tools: - python: "3.10" + python: "3.12" sphinx: configuration: docs/source/conf.py diff --git a/Dockerfile b/Dockerfile index 4a94e3c0b11..583682ef595 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,7 +48,7 @@ ENV DD_REPO='https://github.com/pshriwise/double-down' ENV DD_INSTALL_DIR=$HOME/Double_down # DAGMC variables -ENV DAGMC_BRANCH='v3.2.3' +ENV DAGMC_BRANCH='v3.2.4' ENV DAGMC_REPO='https://github.com/svalinn/DAGMC' ENV DAGMC_INSTALL_DIR=$HOME/DAGMC/ diff --git a/docs/source/devguide/contributing.rst b/docs/source/devguide/contributing.rst index c08b1a362f7..cda0313892b 100644 --- a/docs/source/devguide/contributing.rst +++ b/docs/source/devguide/contributing.rst @@ -109,7 +109,7 @@ Leadership Team The TC consists of the following individuals: - `Paul Romano `_ -- `Sterling Harper `_ +- `Patrick Shriwise `_ - `Adam Nelson `_ - `Benoit Forget `_ diff --git a/docs/source/io_formats/geometry.rst b/docs/source/io_formats/geometry.rst index ac48e48d2d1..6d0a37a24fa 100644 --- a/docs/source/io_formats/geometry.rst +++ b/docs/source/io_formats/geometry.rst @@ -407,13 +407,33 @@ Each ```` 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: - .. note:: A geometry.xml file containing only a DAGMC model for a file named `dagmc.h5m` (no CSG) - looks as follows + :cell: + Material override information for a single cell. It contains the following + attributes and sub-elements: - .. code-block:: xml + :id: + The cell ID in the DAGMC geometry for which the material override will + apply. - - - - + :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. + + *Default*: None + +.. note:: A geometry.xml file containing only a DAGMC model for a file named + `dagmc.h5m` (no CSG) looks as follows: + + .. code-block:: xml + + + + + diff --git a/docs/source/io_formats/statepoint.rst b/docs/source/io_formats/statepoint.rst index 82d20a76302..fd00a3e7735 100644 --- a/docs/source/io_formats/statepoint.rst +++ b/docs/source/io_formats/statepoint.rst @@ -73,9 +73,9 @@ The current version of the statepoint file format is 18.1. **/tallies/meshes/mesh /** :Attributes: - **id** (*int*) -- ID of the mesh - - **type** (*char[]*) -- Type of mesh. :Datasets: - **name** (*char[]*) -- Name of the mesh. + - **type** (*char[]*) -- Type of mesh. - **dimension** (*int*) -- Number of mesh cells in each dimension. - **Regular Mesh Only:** - **lower_left** (*double[]*) -- Coordinates of lower-left corner of diff --git a/docs/source/methods/cross_sections.rst b/docs/source/methods/cross_sections.rst index 2cafa869162..21a1ef8dfeb 100644 --- a/docs/source/methods/cross_sections.rst +++ b/docs/source/methods/cross_sections.rst @@ -290,7 +290,7 @@ scattering information in the water while the fuel can be simulated with linear or even isotropic scattering. .. _logarithmic mapping technique: - https://laws.lanl.gov/vhosts/mcnp.lanl.gov/pdf_files/la-ur-14-24530.pdf + https://mcnp.lanl.gov/pdf_files/TechReport_2014_LANL_LA-UR-14-24530_Brown.pdf .. _Hwang: https://doi.org/10.13182/NSE87-A16381 .. _Josey: https://doi.org/10.1016/j.jcp.2015.08.013 .. _WMP Library: https://github.com/mit-crpg/WMP_Library diff --git a/docs/source/pythonapi/capi.rst b/docs/source/pythonapi/capi.rst index 995ad97fa74..9ceff83fde8 100644 --- a/docs/source/pythonapi/capi.rst +++ b/docs/source/pythonapi/capi.rst @@ -19,6 +19,7 @@ Functions finalize find_cell find_material + dagmc_universe_cell_ids global_bounding_box global_tallies hard_reset diff --git a/include/openmc/capi.h b/include/openmc/capi.h index 9401156a64f..8edd99c0785 100644 --- a/include/openmc/capi.h +++ b/include/openmc/capi.h @@ -29,6 +29,9 @@ int openmc_cell_set_temperature( int32_t index, double T, const int32_t* instance, bool set_contained = false); int openmc_cell_set_translation(int32_t index, const double xyz[]); int openmc_cell_set_rotation(int32_t index, const double rot[], size_t rot_len); +int openmc_dagmc_universe_get_cell_ids( + int32_t univ_id, int32_t* ids, size_t* n); +int openmc_dagmc_universe_get_num_cells(int32_t univ_id, size_t* n); int openmc_energy_filter_get_bins( int32_t index, const double** energies, size_t* n); int openmc_energy_filter_set_bins( diff --git a/include/openmc/cell.h b/include/openmc/cell.h index 70843140bad..032475ce982 100644 --- a/include/openmc/cell.h +++ b/include/openmc/cell.h @@ -320,7 +320,6 @@ class Cell { int32_t universe_; //!< Universe # this cell is in int32_t fill_; //!< Universe # filling this cell int32_t n_instances_ {0}; //!< Number of instances of this cell - GeometryType geom_type_; //!< Geometric representation type (CSG, DAGMC) //! \brief Index corresponding to this cell in distribcell arrays int distribcell_index_ {C_NONE}; @@ -350,6 +349,13 @@ class Cell { vector rotation_; vector offset_; //!< Distribcell offset table + + // Accessors + const GeometryType& geom_type() const { return geom_type_; } + GeometryType& geom_type() { return geom_type_; } + +private: + GeometryType geom_type_; //!< Geometric representation type (CSG, DAGMC) }; struct CellInstanceItem { diff --git a/include/openmc/dagmc.h b/include/openmc/dagmc.h index 2facf4fc05e..47fcfe237e3 100644 --- a/include/openmc/dagmc.h +++ b/include/openmc/dagmc.h @@ -29,6 +29,12 @@ void check_dagmc_root_univ(); #include "openmc/particle.h" #include "openmc/position.h" #include "openmc/surface.h" +#include "openmc/vector.h" + +#include // for shared_ptr, unique_ptr +#include +#include +#include // for pair class UWUW; @@ -133,6 +139,10 @@ class DAGUniverse : public Universe { void legacy_assign_material( std::string mat_string, std::unique_ptr& c) const; + //! 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& c) const; + //! Return the index into the model cells vector for a given DAGMC volume //! handle in the universe //! \param[in] vol MOAB handle to the DAGMC volume set @@ -184,6 +194,11 @@ class DAGUniverse : public Universe { //!< generate new material IDs for the universe bool has_graveyard_; //!< Indicates if the DAGMC geometry has a "graveyard" //!< volume + std::unordered_map> + material_overrides_; //!< Map of material overrides + //!< keys correspond to the DAGMCCell id + //!< values are a list of material ids used + //!< for the override }; //============================================================================== diff --git a/include/openmc/surface.h b/include/openmc/surface.h index af235301c14..498f71d4f9b 100644 --- a/include/openmc/surface.h +++ b/include/openmc/surface.h @@ -38,7 +38,6 @@ class Surface { int id_; //!< Unique ID std::string name_; //!< User-defined name unique_ptr bc_; //!< Boundary condition - GeometryType geom_type_; //!< Geometry type indicator (CSG or DAGMC) bool surf_source_ {false}; //!< Activate source banking for the surface? explicit Surface(pugi::xml_node surf_node); @@ -91,6 +90,13 @@ class Surface { //! Get the BoundingBox for this surface. virtual BoundingBox bounding_box(bool /*pos_side*/) const { return {}; } + // Accessors + const GeometryType& geom_type() const { return geom_type_; } + GeometryType& geom_type() { return geom_type_; } + +private: + GeometryType geom_type_; //!< Geometry type indicator (CSG or DAGMC) + protected: virtual void to_hdf5_inner(hid_t group_id) const = 0; }; diff --git a/openmc/__init__.py b/openmc/__init__.py index 566d287068f..bb972b4e6ad 100644 --- a/openmc/__init__.py +++ b/openmc/__init__.py @@ -15,6 +15,7 @@ from openmc.weight_windows import * from openmc.surface import * from openmc.universe import * +from openmc.dagmc import * from openmc.source import * from openmc.settings import * from openmc.lattice import * diff --git a/openmc/dagmc.py b/openmc/dagmc.py new file mode 100644 index 00000000000..8ab0aaf69e7 --- /dev/null +++ b/openmc/dagmc.py @@ -0,0 +1,625 @@ +from collections.abc import Iterable, Mapping +from numbers import Integral + +import h5py +import lxml.etree as ET +import numpy as np +import warnings + +import openmc +import openmc.checkvalue as cv +from ._xml import get_text +from .checkvalue import check_type, check_value +from .surface import _BOUNDARY_TYPES +from .bounding_box import BoundingBox +from .utility_funcs import input_path + + +class DAGMCUniverse(openmc.UniverseBase): + """A reference to a DAGMC file to be used in the model. + + .. versionadded:: 0.13.0 + + Parameters + ---------- + filename : str + Path to the DAGMC file used to represent this universe. + universe_id : int, optional + Unique identifier of the universe. If not specified, an identifier will + automatically be assigned. + name : str, optional + Name of the universe. If not specified, the name is the empty string. + auto_geom_ids : bool + Set IDs automatically on initialization (True) or report overlaps in ID + space between CSG and DAGMC (False) + 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 + Unique identifier of the universe + name : str + Name of the universe + filename : str + Path to the DAGMC file used to represent this universe. + auto_geom_ids : bool + Set IDs automatically on initialization (True) or report overlaps in ID + space between CSG and DAGMC (False) + auto_mat_ids : bool + Set IDs automatically on initialization (True) or report overlaps in ID + space between OpenMC and UWUW materials (False) + bounding_box : openmc.BoundingBox + Lower-left and upper-right coordinates of an axis-aligned bounding box + of the universe. + + .. versionadded:: 0.13.1 + material_names : list of str + Return a sorted list of materials names that are contained within the + DAGMC h5m file. This is useful when naming openmc.Material() objects as + each material name present in the DAGMC h5m file must have a matching + openmc.Material() with the same name. + + .. versionadded:: 0.13.2 + n_cells : int + The number of cells in the DAGMC model. This is the number of cells at + runtime and accounts for the implicit complement whether or not is it + present in the DAGMC file. + + .. versionadded:: 0.13.2 + n_surfaces : int + 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, + filename: cv.PathLike, + universe_id=None, + name='', + auto_geom_ids=False, + auto_mat_ids=False, + material_overrides=None): + 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__() + string += '{: <16}=\t{}\n'.format('\tGeom', 'DAGMC') + string += '{: <16}=\t{}\n'.format('\tFile', self.filename) + return string + + @property + def bounding_box(self): + with h5py.File(self.filename) as dagmc_file: + coords = dagmc_file['tstt']['nodes']['coordinates'][()] + lower_left_corner = coords.min(axis=0) + upper_right_corner = coords.max(axis=0) + return openmc.BoundingBox(lower_left_corner, upper_right_corner) + + @property + def filename(self): + return self._filename + + @filename.setter + def filename(self, val: cv.PathLike): + cv.check_type('DAGMC filename', val, cv.PathLike) + self._filename = input_path(val) + + @property + def material_overrides(self): + return self._material_overrides + + @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 + + def add_material_override(self, key, overrides=None): + """Add a material override to the universe. + + .. versionadded:: 0.15 + + Parameters + ---------- + key : openmc.DAGMCCell or int + Cell object or ID of the Cell to override + value : openmc.Material or Iterable of openmc.Material + Material(s) to be applied to the Cell passed as the key + + """ + # Ensure that they key is a valid type + if not isinstance(key, (int, openmc.DAGMCCell)): + raise ValueError("Unrecognized key type. " + "Must be an integer or openmc.DAGMCCell object") + + # Ensure that overrides is an iterable of openmc.Material + overrides = overrides if isinstance(overrides, openmc.Iterable) else [overrides] + cv.check_iterable_type('material objects', overrides, (openmc.Material, type(None))) + + # if a DAGMCCell is passed, redcue the key to the ID of the cell + if isinstance(key, openmc.DAGMCCell): + key = key.id + + if key not in self.cells: + raise ValueError(f"Cell ID '{key}' not found in DAGMC universe") + + self._material_overrides[key] = overrides + + @property + def auto_geom_ids(self): + return self._auto_geom_ids + + @auto_geom_ids.setter + def auto_geom_ids(self, val): + cv.check_type('DAGMC automatic geometry ids', val, bool) + self._auto_geom_ids = val + + @property + def auto_mat_ids(self): + return self._auto_mat_ids + + @auto_mat_ids.setter + def auto_mat_ids(self, val): + cv.check_type('DAGMC automatic material ids', val, bool) + self._auto_mat_ids = val + + @property + def material_names(self): + dagmc_file_contents = h5py.File(self.filename) + material_tags_hex = dagmc_file_contents['/tstt/tags/NAME'].get( + 'values') + material_tags_ascii = [] + for tag in material_tags_hex: + candidate_tag = tag.tobytes().decode().replace('\x00', '') + # tags might be for temperature or reflective surfaces + if candidate_tag.startswith('mat:'): + # if name ends with _comp remove it, it is not parsed + if candidate_tag.endswith('_comp'): + candidate_tag = candidate_tag[:-5] + # removes first 4 characters as openmc.Material name should be + # set without the 'mat:' part of the tag + material_tags_ascii.append(candidate_tag[4:]) + + return sorted(set(material_tags_ascii)) + + def _n_geom_elements(self, geom_type): + """ + Helper function for retrieving the number geometric entities in a DAGMC + file + + Parameters + ---------- + geom_type : str + The type of geometric entity to count. One of {'Volume', 'Surface'}. Returns + the runtime number of voumes in the DAGMC model (includes implicit complement). + + Returns + ------- + int + Number of geometry elements of the specified type + """ + cv.check_value('geometry type', geom_type, ('volume', 'surface')) + + def decode_str_tag(tag_val): + return tag_val.tobytes().decode().replace('\x00', '') + + with h5py.File(self.filename) as dagmc_file: + category_data = dagmc_file['tstt/tags/CATEGORY/values'] + category_strs = map(decode_str_tag, category_data) + n = sum([v == geom_type.capitalize() for v in category_strs]) + + # check for presence of an implicit complement in the file and + # increment the number of cells if it doesn't exist + if geom_type == 'volume': + name_data = dagmc_file['tstt/tags/NAME/values'] + name_strs = map(decode_str_tag, name_data) + if not sum(['impl_complement' in n for n in name_strs]): + n += 1 + return n + + @property + def n_cells(self): + return self._n_geom_elements('volume') + + @property + def n_surfaces(self): + return self._n_geom_elements('surface') + + def create_xml_subelement(self, xml_element, memo=None): + if memo is None: + memo = set() + + if self in memo: + return + + 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)) + + if self.auto_geom_ids: + dagmc_element.set('auto_geom_ids', 'true') + 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) + xml_element.append(dagmc_element) + + def bounding_region( + self, + bounded_type: str = 'box', + boundary_type: str = 'vacuum', + starting_id: int = 10000, + padding_distance: float = 0. + ): + """Creates a either a spherical or box shaped bounding region around + the DAGMC geometry. + + .. versionadded:: 0.13.1 + + Parameters + ---------- + bounded_type : str + The type of bounding surface(s) to use when constructing the region. + Options include a single spherical surface (sphere) or a rectangle + made from six planes (box). + boundary_type : str + Boundary condition that defines the behavior for particles hitting + the surface. Defaults to vacuum boundary condition. Passed into the + surface construction. + starting_id : int + Starting ID of the surface(s) used in the region. For bounded_type + 'box', the next 5 IDs will also be used. Defaults to 10000 to reduce + the chance of an overlap of surface IDs with the DAGMC geometry. + padding_distance : float + Distance between the bounding region surfaces and the minimal + bounding box. Allows for the region to be larger than the DAGMC + geometry. + + Returns + ------- + openmc.Region + Region instance + """ + + check_type('boundary type', boundary_type, str) + check_value('boundary type', boundary_type, _BOUNDARY_TYPES) + check_type('starting surface id', starting_id, Integral) + check_type('bounded type', bounded_type, str) + check_value('bounded type', bounded_type, ('box', 'sphere')) + + bbox = self.bounding_box.expand(padding_distance, True) + + if bounded_type == 'sphere': + radius = np.linalg.norm(bbox.upper_right - bbox.center) + bounding_surface = openmc.Sphere( + surface_id=starting_id, + x0=bbox.center[0], + y0=bbox.center[1], + z0=bbox.center[2], + boundary_type=boundary_type, + r=radius, + ) + + return -bounding_surface + + if bounded_type == 'box': + # defines plane surfaces for all six faces of the bounding box + lower_x = openmc.XPlane(bbox[0][0], surface_id=starting_id) + upper_x = openmc.XPlane(bbox[1][0], surface_id=starting_id+1) + lower_y = openmc.YPlane(bbox[0][1], surface_id=starting_id+2) + upper_y = openmc.YPlane(bbox[1][1], surface_id=starting_id+3) + lower_z = openmc.ZPlane(bbox[0][2], surface_id=starting_id+4) + upper_z = openmc.ZPlane(bbox[1][2], surface_id=starting_id+5) + + region = +lower_x & -upper_x & +lower_y & -upper_y & +lower_z & -upper_z + + for surface in region.get_surfaces().values(): + surface.boundary_type = boundary_type + + return region + + def bounded_universe(self, bounding_cell_id=10000, **kwargs): + """Returns an openmc.Universe filled with this DAGMCUniverse and bounded + with a cell. Defaults to a box cell with a vacuum surface however this + can be changed using the kwargs which are passed directly to + DAGMCUniverse.bounding_region(). + + Parameters + ---------- + bounding_cell_id : int + The cell ID number to use for the bounding cell, defaults to 10000 to reduce + the chance of overlapping ID numbers with the DAGMC geometry. + + Returns + ------- + openmc.Universe + Universe instance + """ + bounding_cell = openmc.Cell( + fill=self, cell_id=bounding_cell_id, region=self.bounding_region(**kwargs)) + return openmc.Universe(cells=[bounding_cell]) + + @classmethod + def from_hdf5(cls, group): + """Create DAGMC universe from HDF5 group + + Parameters + ---------- + group : h5py.Group + Group in HDF5 file + + Returns + ------- + openmc.DAGMCUniverse + DAGMCUniverse instance + + """ + id = int(group.name.split('/')[-1].lstrip('universe ')) + fname = group['filename'][()].decode() + name = group['name'][()].decode() if 'name' in group else None + + out = cls(fname, universe_id=id, name=name) + + out.auto_geom_ids = bool(group.attrs['auto_geom_ids']) + out.auto_mat_ids = bool(group.attrs['auto_mat_ids']) + + return out + + @classmethod + def from_xml_element(cls, elem, mats = None): + """Generate DAGMC universe from XML element + + Parameters + ---------- + elem : lxml.etree._Element + `` element + mats : dict + Dictionary mapping material ID strings to :class:`openmc.Material` + instances (defined in :meth:`openmc.Geometry.from_xml`) + + Returns + ------- + openmc.DAGMCUniverse + DAGMCUniverse instance + + """ + id = int(get_text(elem, 'id')) + fname = get_text(elem, 'filename') + + out = cls(fname, universe_id=id) + + name = get_text(elem, 'name') + if name is not None: + out.name = name + + out.auto_geom_ids = bool(elem.get('auto_geom_ids')) + out.auto_mat_ids = bool(elem.get('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_text(elem, 'material_ids').split(' ') + mat_objs = [mats[mat_id] for mat_id in mat_ids] + out._material_overrides[cell_id] = mat_objs + + return out + + 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 + only to be used within the openmc.UniverseBase.clone() context. + """ + clone = openmc.DAGMCUniverse(name=self.name, filename=self.filename) + clone.volume = self.volume + clone.auto_geom_ids = self.auto_geom_ids + clone.auto_mat_ids = self.auto_mat_ids + return clone + + def add_cell(self, cell): + """Add a cell to the universe. + + Parameters + ---------- + cell : openmc.DAGMCCell + Cell to add + + """ + if not isinstance(cell, openmc.DAGMCCell): + msg = f'Unable to add a DAGMCCell to DAGMCUniverse ' \ + f'ID="{self._id}" since "{cell}" is not a DAGMCCell' + raise TypeError(msg) + + cell_id = cell.id + + if cell_id not in self._cells: + self._cells[cell_id] = cell + + def remove_cell(self, cell): + """Remove a cell from the universe. + + Parameters + ---------- + cell : openmc.Cell + Cell to remove + + """ + + if not isinstance(cell, openmc.DAGMCCell): + msg = f'Unable to remove a Cell from Universe ID="{self._id}" ' \ + f'since "{cell}" is not a Cell' + raise TypeError(msg) + + # If the Cell is in the Universe's list of Cells, delete it + self._cells.pop(cell.id, None) + + def sync_dagmc_cells(self, mats: Iterable[openmc.Material]): + """Synchronize DAGMC cell information between Python and C API + + .. versionadded:: 0.15.1 + + Parameters + ---------- + mats : iterable of openmc.Material + Iterable of materials to assign to the DAGMC cells + + """ + import openmc.lib + if not openmc.lib.is_initialized: + raise RuntimeError("This universe must be part of an openmc.Model " + "initialized via Model.init_lib before calling " + "this method.") + + dagmc_cell_ids = openmc.lib.dagmc.dagmc_universe_cell_ids(self.id) + if len(dagmc_cell_ids) != self.n_cells: + raise ValueError( + f"Number of cells in DAGMC universe {self.id} does not match " + f"the number of cells in the Python universe." + ) + + mats_per_id = {mat.id: mat for mat in mats} + for dag_cell_id in dagmc_cell_ids: + dag_cell = openmc.lib.cells[dag_cell_id] + if isinstance(dag_cell.fill, Iterable): + 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)) + + +class DAGMCCell(openmc.Cell): + """A cell class for DAGMC-based geometries. + + .. versionadded:: 0.15.1 + + Parameters + ---------- + cell_id : int or None, optional + Unique identifier for the cell. If None, an identifier will be + automatically assigned. + name : str, optional + Name of the cell. + fill : openmc.Material or None, optional + Material filling the cell. If None, the cell is filled with vacuum. + + Attributes + ---------- + DAG_parent_universe : int + The parent universe of the cell. + + """ + def __init__(self, cell_id=None, name='', fill=None): + super().__init__(cell_id, name, fill, None) + + @property + def DAG_parent_universe(self): + """Get the parent universe of the cell.""" + return self._parent_universe + + @DAG_parent_universe.setter + def DAG_parent_universe(self, universe): + """Set the parent universe of the cell.""" + self._parent_universe = universe.id + + def bounding_box(self): + return BoundingBox.infinite() + + def get_all_cells(self, memo=None): + return {} + + def get_all_universes(self, memo=None): + return {} + + def clone(self, clone_materials=True, clone_regions=True, memo=None): + warnings.warn("clone is not available for cells in a DAGMC universe") + return self + + def plot(self, *args, **kwargs): + 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.") + + @classmethod + def from_xml_element(cls, elem, surfaces, materials, get_universe): + raise TypeError("from_xml_element is not available for DAGMC cells.") diff --git a/openmc/data/njoy.py b/openmc/data/njoy.py index 1bf44891ef4..f5ef836f9ad 100644 --- a/openmc/data/njoy.py +++ b/openmc/data/njoy.py @@ -426,7 +426,7 @@ def make_ace(filename, temperatures=None, acer=True, xsdir=None, def make_ace_thermal(filename, filename_thermal, temperatures=None, - ace='ace', xsdir=None, output_dir=None, error=0.001, + ace=None, xsdir=None, output_dir=None, error=0.001, iwt=2, evaluation=None, evaluation_thermal=None, table_name=None, zaids=None, nmix=None, **kwargs): """Generate thermal scattering ACE file from ENDF files @@ -441,7 +441,7 @@ def make_ace_thermal(filename, filename_thermal, temperatures=None, Temperatures in Kelvin to produce data at. If omitted, data is produced at all temperatures given in the ENDF thermal scattering sublibrary. ace : str, optional - Path of ACE file to write + Path of ACE file to write. Default to ``"ace"``. xsdir : str, optional Path of xsdir file to write. Defaults to ``"xsdir"`` in the same directory as ``ace`` @@ -589,7 +589,7 @@ def make_ace_thermal(filename, filename_thermal, temperatures=None, commands += 'stop\n' run(commands, tapein, tapeout, **kwargs) - ace = output_dir / ace + ace = (output_dir / "ace") if ace is None else Path(ace) xsdir = (ace.parent / "xsdir") if xsdir is None else Path(xsdir) with ace.open('w') as ace_file, xsdir.open('w') as xsdir_file: # Concatenate ACE and xsdir files together diff --git a/openmc/geometry.py b/openmc/geometry.py index a52dc4418d2..28e1e48eca2 100644 --- a/openmc/geometry.py +++ b/openmc/geometry.py @@ -217,7 +217,7 @@ def get_universe(univ_id): # Add any DAGMC universes for e in elem.findall('dagmc_universe'): - dag_univ = openmc.DAGMCUniverse.from_xml_element(e) + dag_univ = openmc.DAGMCUniverse.from_xml_element(e, mats) universes[dag_univ.id] = dag_univ # Dictionary that maps each universe to a list of cells/lattices that diff --git a/openmc/lib/__init__.py b/openmc/lib/__init__.py index 9bb2efb38af..5fe35b9745d 100644 --- a/openmc/lib/__init__.py +++ b/openmc/lib/__init__.py @@ -68,6 +68,7 @@ def _uwuw_enabled(): from .math import * from .plot import * from .weight_windows import * +from .dagmc import * # Flag to denote whether or not openmc.lib.init has been called # TODO: Establish and use a flag in the C++ code to represent the status of the diff --git a/openmc/lib/dagmc.py b/openmc/lib/dagmc.py new file mode 100644 index 00000000000..18ec81a4be8 --- /dev/null +++ b/openmc/lib/dagmc.py @@ -0,0 +1,43 @@ +from ctypes import c_int, c_int32, POINTER, c_size_t + +import numpy as np + +from . import _dll +from .error import _error_handler + + +__all__ = [ + 'dagmc_universe_cell_ids' +] + +# DAGMC functions +_dll.openmc_dagmc_universe_get_cell_ids.argtypes = [c_int32, POINTER(c_int32), POINTER(c_size_t)] +_dll.openmc_dagmc_universe_get_cell_ids.restype = c_int +_dll.openmc_dagmc_universe_get_cell_ids.errcheck = _error_handler +_dll.openmc_dagmc_universe_get_num_cells.argtypes = [c_int32, POINTER(c_size_t)] +_dll.openmc_dagmc_universe_get_num_cells.restype = c_int +_dll.openmc_dagmc_universe_get_num_cells.errcheck = _error_handler + + +def dagmc_universe_cell_ids(universe_id: int) -> np.ndarray: + """Return an array of cell IDs for a DAGMC universe. + + Parameters + ---------- + dagmc_id : int + ID of the DAGMC universe to get cell IDs from. + + Returns + ------- + numpy.ndarray + DAGMC cell IDs for the universe. + + """ + n = c_size_t() + _dll.openmc_dagmc_universe_get_num_cells(universe_id, n) + cell_ids = np.empty(n.value, dtype=np.int32) + + _dll.openmc_dagmc_universe_get_cell_ids( + universe_id, cell_ids.ctypes.data_as(POINTER(c_int32)), n + ) + return cell_ids diff --git a/openmc/mesh.py b/openmc/mesh.py index 0b6f6b84e4b..e4d83d81d3d 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -99,7 +99,7 @@ def from_hdf5(cls, group: h5py.Group): Instance of a MeshBase subclass """ - mesh_type = 'regular' if 'type' not in group.attrs else group.attrs['type'].decode() + mesh_type = 'regular' if 'type' not in group.keys() else group['type'][()].decode() mesh_id = int(group.name.split('/')[-1].lstrip('mesh ')) mesh_name = '' if not 'name' in group else group['name'][()].decode() diff --git a/openmc/model/model.py b/openmc/model/model.py index 49cb0a83ef5..1c261061aef 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -1,7 +1,6 @@ from __future__ import annotations from collections.abc import Iterable from functools import lru_cache -import os from pathlib import Path from numbers import Integral from tempfile import NamedTemporaryFile @@ -15,7 +14,7 @@ import openmc._xml as xml from openmc.dummy_comm import DummyCommunicator from openmc.executor import _process_CLI_arguments -from openmc.checkvalue import check_type, check_value, PathLike +from openmc.checkvalue import check_type, check_value from openmc.exceptions import InvalidIDError from openmc.utility_funcs import change_directory @@ -65,22 +64,11 @@ class Model: def __init__(self, geometry=None, materials=None, settings=None, tallies=None, plots=None): - self.geometry = openmc.Geometry() - self.materials = openmc.Materials() - self.settings = openmc.Settings() - self.tallies = openmc.Tallies() - self.plots = openmc.Plots() - - if geometry is not None: - self.geometry = geometry - if materials is not None: - self.materials = materials - if settings is not None: - self.settings = settings - if tallies is not None: - self.tallies = tallies - if plots is not None: - self.plots = plots + self.geometry = openmc.Geometry() if geometry is None else geometry + self.materials = openmc.Materials() if materials is None else materials + self.settings = openmc.Settings() if settings is None else settings + self.tallies = openmc.Tallies() if tallies is None else tallies + self.plots = openmc.Plots() if plots is None else plots @property def geometry(self) -> openmc.Geometry | None: @@ -324,6 +312,28 @@ def init_lib(self, threads=None, geometry_debug=False, restart_file=None, # communicator openmc.lib.init(args=args, intracomm=intracomm, output=output) + def sync_dagmc_universes(self): + """Synchronize all DAGMC universes in the current geometry. + + 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`. + + .. versionadded:: 0.15.1 + + """ + if self.is_initialized: + if self.materials: + materials = self.materials + else: + materials = list(self.geometry.get_all_materials().values()) + for univ in self.geometry.get_all_universes().values(): + if isinstance(univ, openmc.DAGMCUniverse): + univ.sync_dagmc_cells(materials) + else: + raise ValueError("The model must be initialized before calling " + "this method") + def finalize_lib(self): """Finalize simulation and free memory allocated for the C API @@ -1154,51 +1164,86 @@ def update_material_volumes(self, names_or_ids, volume): self._change_py_lib_attribs(names_or_ids, volume, 'material', 'volume') - def differentiate_depletable_mats(self, diff_volume_method: str): + def differentiate_depletable_mats(self, diff_volume_method: str = None): """Assign distribmats for each depletable material .. versionadded:: 0.14.0 + .. versionchanged:: 0.15.1 + diff_volume_method default is None, do not set volumes on the new + material ovjects. Is now a convenience method for + differentiate_mats(diff_volume_method, depletable_only=True) + + Parameters + ---------- + diff_volume_method : str + Specifies how the volumes of the new materials should be found. + - None: Do not assign volumes to the new materials (Default) + - 'divide_equally': Divide the original material volume equally between the new materials + - 'match cell': Set the volume of the material to the volume of the cell they fill + """ + self.differentiate_mats(diff_volume_method, depletable_only=True) + + def differentiate_mats(self, diff_volume_method: str = None, depletable_only: bool = True): + """Assign distribmats for each material + + .. versionadded:: 0.15.1 + Parameters ---------- diff_volume_method : str Specifies how the volumes of the new materials should be found. - Default is to 'divide equally' which divides the original material - volume equally between the new materials, 'match cell' sets the - volume of the material to volume of the cell they fill. + - None: Do not assign volumes to the new materials (Default) + - 'divide_equally': Divide the original material volume equally between the new materials + - 'match cell': Set the volume of the material to the volume of the cell they fill + depletable_only : bool + Default is True, only depletable materials will be differentiated. If False, all materials will be + differentiated. """ + check_value('volume differentiation method', diff_volume_method, ("divide equally", "match cell", None)) + # Count the number of instances for each cell and material self.geometry.determine_paths(instances_only=True) - # Extract all depletable materials which have multiple instances - distribmats = set( - [mat for mat in self.materials - if mat.depletable and mat.num_instances > 1]) - - if diff_volume_method == 'divide equally': - for mat in distribmats: - if mat.volume is None: - raise RuntimeError("Volume not specified for depletable " - f"material with ID={mat.id}.") - mat.volume /= mat.num_instances - - if distribmats: - # Assign distribmats to cells - for cell in self.geometry.get_all_material_cells().values(): - if cell.fill in distribmats: - mat = cell.fill - if diff_volume_method == 'divide equally': - cell.fill = [mat.clone() for _ in range(cell.num_instances)] - elif diff_volume_method == 'match cell': - for _ in range(cell.num_instances): - cell.fill = mat.clone() + # Find all or depletable_only materials which have multiple instance + distribmats = set() + for mat in self.materials: + # Differentiate all materials with multiple instances + diff_mat = mat.num_instances > 1 + # If depletable_only is True, differentiate only depletable materials + if depletable_only: + diff_mat = diff_mat and mat.depletable + if diff_mat: + # Assign volumes to the materials according to requirements + if diff_volume_method == "divide equally": + if mat.volume is None: + raise RuntimeError( + "Volume not specified for " + f"material with ID={mat.id}.") + else: + mat.volume /= mat.num_instances + elif diff_volume_method == "match cell": + for cell in self.geometry.get_all_material_cells().values(): + if cell.fill == mat: if not cell.volume: raise ValueError( f"Volume of cell ID={cell.id} not specified. " "Set volumes of cells prior to using " - "diff_volume_method='match cell'." - ) - cell.fill.volume = cell.volume + "diff_volume_method='match cell'.") + distribmats.add(mat) + + if not distribmats: + return + + # Assign distribmats to cells + for cell in self.geometry.get_all_material_cells().values(): + if cell.fill in distribmats: + mat = cell.fill + if diff_volume_method != 'match cell': + cell.fill = [mat.clone() for _ in range(cell.num_instances)] + elif diff_volume_method == 'match cell': + cell.fill = mat.clone() + cell.fill.volume = cell.volume if self.materials is not None: self.materials = openmc.Materials( diff --git a/openmc/model/surface_composite.py b/openmc/model/surface_composite.py index 1f25b9ca636..88433118b7f 100644 --- a/openmc/model/surface_composite.py +++ b/openmc/model/surface_composite.py @@ -729,7 +729,7 @@ def __neg__(self): class XConeOneSided(CompositeSurface): - """One-sided cone parallel the x-axis + r"""One-sided cone parallel the x-axis A one-sided cone is composed of a normal cone surface and a "disambiguation" surface that eliminates the ambiguity as to which region of space is @@ -742,15 +742,16 @@ class XConeOneSided(CompositeSurface): Parameters ---------- x0 : float, optional - x-coordinate of the apex. Defaults to 0. + x-coordinate of the apex in [cm]. y0 : float, optional - y-coordinate of the apex. Defaults to 0. + y-coordinate of the apex in [cm]. z0 : float, optional - z-coordinate of the apex. Defaults to 0. + z-coordinate of the apex in [cm]. r2 : float, optional - Parameter related to the aperture [:math:`\\rm cm^2`]. - It can be interpreted as the increase in the radius squared per cm along - the cone's axis of revolution. + The square of the slope of the cone. It is defined as + :math:`\left(\frac{r}{h}\right)^2` for a radius, :math:`r` and an axial + distance :math:`h` from the apex. An easy way to define this quantity is + to take the square of the radius of the cone (in cm) 1 cm from the apex. up : bool Whether to select the side of the cone that extends to infinity in the positive direction of the coordinate axis (the positive half-space of @@ -783,7 +784,7 @@ def __neg__(self): class YConeOneSided(CompositeSurface): - """One-sided cone parallel the y-axis + r"""One-sided cone parallel the y-axis A one-sided cone is composed of a normal cone surface and a "disambiguation" surface that eliminates the ambiguity as to which region of space is @@ -796,15 +797,16 @@ class YConeOneSided(CompositeSurface): Parameters ---------- x0 : float, optional - x-coordinate of the apex. Defaults to 0. + x-coordinate of the apex in [cm]. y0 : float, optional - y-coordinate of the apex. Defaults to 0. + y-coordinate of the apex in [cm]. z0 : float, optional - z-coordinate of the apex. Defaults to 0. + z-coordinate of the apex in [cm]. r2 : float, optional - Parameter related to the aperture [:math:`\\rm cm^2`]. - It can be interpreted as the increase in the radius squared per cm along - the cone's axis of revolution. + The square of the slope of the cone. It is defined as + :math:`\left(\frac{r}{h}\right)^2` for a radius, :math:`r` and an axial + distance :math:`h` from the apex. An easy way to define this quantity is + to take the square of the radius of the cone (in cm) 1 cm from the apex. up : bool Whether to select the side of the cone that extends to infinity in the positive direction of the coordinate axis (the positive half-space of @@ -836,7 +838,7 @@ def __init__(self, x0=0., y0=0., z0=0., r2=1., up=True, **kwargs): class ZConeOneSided(CompositeSurface): - """One-sided cone parallel the z-axis + r"""One-sided cone parallel the z-axis A one-sided cone is composed of a normal cone surface and a "disambiguation" surface that eliminates the ambiguity as to which region of space is @@ -849,15 +851,16 @@ class ZConeOneSided(CompositeSurface): Parameters ---------- x0 : float, optional - x-coordinate of the apex. Defaults to 0. + x-coordinate of the apex in [cm]. y0 : float, optional - y-coordinate of the apex. Defaults to 0. + y-coordinate of the apex in [cm]. z0 : float, optional - z-coordinate of the apex. Defaults to 0. + z-coordinate of the apex in [cm]. r2 : float, optional - Parameter related to the aperture [:math:`\\rm cm^2`]. - It can be interpreted as the increase in the radius squared per cm along - the cone's axis of revolution. + The square of the slope of the cone. It is defined as + :math:`\left(\frac{r}{h}\right)^2` for a radius, :math:`r` and an axial + distance :math:`h` from the apex. An easy way to define this quantity is + to take the square of the radius of the cone (in cm) 1 cm from the apex. up : bool Whether to select the side of the cone that extends to infinity in the positive direction of the coordinate axis (the positive half-space of @@ -1252,11 +1255,11 @@ def _get_convex_hull_surfs(self, qhull): else: op = operator.neg if basis == 'xy': - surf = openmc.Plane(a=dx, b=dy, d=-c) + surf = openmc.Plane(a=dx, b=dy, c=0.0, d=-c) elif basis == 'yz': - surf = openmc.Plane(b=dx, c=dy, d=-c) + surf = openmc.Plane(a=0.0, b=dx, c=dy, d=-c) elif basis == 'xz': - surf = openmc.Plane(a=dx, c=dy, d=-c) + surf = openmc.Plane(a=dx, b=0.0, c=dy, d=-c) else: y0 = -c/dy r2 = dy**2 / dx**2 diff --git a/openmc/polynomial.py b/openmc/polynomial.py index 1259c61ca26..341cdff476d 100644 --- a/openmc/polynomial.py +++ b/openmc/polynomial.py @@ -92,7 +92,7 @@ class Zernike(Polynomial): Parameters ---------- coef : Iterable of float - A list of coefficients of each term in radial only Zernike polynomials + A list of coefficients of each term in Zernike polynomials radius : float Domain of Zernike polynomials to be applied on. Default is 1. diff --git a/openmc/surface.py b/openmc/surface.py index c95025f949b..6919952ec9c 100644 --- a/openmc/surface.py +++ b/openmc/surface.py @@ -1753,7 +1753,7 @@ def evaluate(self, point): class Cone(QuadricMixin, Surface): - """A conical surface parallel to the x-, y-, or z-axis. + r"""A conical surface parallel to the x-, y-, or z-axis. .. Note:: This creates a double cone, which is two one-sided cones that meet at their apex. @@ -1763,24 +1763,22 @@ class Cone(QuadricMixin, Surface): Parameters ---------- x0 : float, optional - x-coordinate of the apex in [cm]. Defaults to 0. + x-coordinate of the apex in [cm]. y0 : float, optional - y-coordinate of the apex in [cm]. Defaults to 0. + y-coordinate of the apex in [cm]. z0 : float, optional - z-coordinate of the apex in [cm]. Defaults to 0. + z-coordinate of the apex in [cm]. r2 : float, optional - Parameter related to the aperture [:math:`\\rm cm^2`]. - It can be interpreted as the increase in the radius squared per cm along - the cone's axis of revolution. + The square of the slope of the cone. It is defined as + :math:`\left(\frac{r}{h}\right)^2` for a radius, :math:`r` and an axial + distance :math:`h` from the apex. An easy way to define this quantity is + to take the square of the radius of the cone (in cm) 1 cm from the apex. dx : float, optional x-component of the vector representing the axis of the cone. - Defaults to 0. dy : float, optional y-component of the vector representing the axis of the cone. - Defaults to 0. dz : float, optional z-component of the vector representing the axis of the cone. - Defaults to 1. surface_id : int, optional Unique identifier for the surface. If not specified, an identifier will automatically be assigned. @@ -1805,7 +1803,7 @@ class Cone(QuadricMixin, Surface): z0 : float z-coordinate of the apex in [cm] r2 : float - Parameter related to the aperature [cm^2] + Parameter related to the aperture dx : float x-component of the vector representing the axis of the cone. dy : float @@ -1911,7 +1909,7 @@ def to_xml_element(self): class XCone(QuadricMixin, Surface): - """A cone parallel to the x-axis of the form :math:`(y - y_0)^2 + (z - z_0)^2 = + r"""A cone parallel to the x-axis of the form :math:`(y - y_0)^2 + (z - z_0)^2 = r^2 (x - x_0)^2`. .. Note:: @@ -1921,15 +1919,16 @@ class XCone(QuadricMixin, Surface): Parameters ---------- x0 : float, optional - x-coordinate of the apex in [cm]. Defaults to 0. + x-coordinate of the apex in [cm]. y0 : float, optional - y-coordinate of the apex in [cm]. Defaults to 0. + y-coordinate of the apex in [cm]. z0 : float, optional - z-coordinate of the apex in [cm]. Defaults to 0. + z-coordinate of the apex in [cm]. r2 : float, optional - Parameter related to the aperture [:math:`\\rm cm^2`]. - It can be interpreted as the increase in the radius squared per cm along - the cone's axis of revolution. + The square of the slope of the cone. It is defined as + :math:`\left(\frac{r}{h}\right)^2` for a radius, :math:`r` and an axial + distance :math:`h` from the apex. An easy way to define this quantity is + to take the square of the radius of the cone (in cm) 1 cm from the apex. boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles @@ -1953,7 +1952,7 @@ class XCone(QuadricMixin, Surface): z0 : float z-coordinate of the apex in [cm] r2 : float - Parameter related to the aperature + Parameter related to the aperture boundary_type : {'transmission', 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. @@ -2012,7 +2011,7 @@ def evaluate(self, point): class YCone(QuadricMixin, Surface): - """A cone parallel to the y-axis of the form :math:`(x - x_0)^2 + (z - z_0)^2 = + r"""A cone parallel to the y-axis of the form :math:`(x - x_0)^2 + (z - z_0)^2 = r^2 (y - y_0)^2`. .. Note:: @@ -2022,15 +2021,16 @@ class YCone(QuadricMixin, Surface): Parameters ---------- x0 : float, optional - x-coordinate of the apex in [cm]. Defaults to 0. + x-coordinate of the apex in [cm]. y0 : float, optional - y-coordinate of the apex in [cm]. Defaults to 0. + y-coordinate of the apex in [cm]. z0 : float, optional - z-coordinate of the apex in [cm]. Defaults to 0. + z-coordinate of the apex in [cm]. r2 : float, optional - Parameter related to the aperture [:math:`\\rm cm^2`]. - It can be interpreted as the increase in the radius squared per cm along - the cone's axis of revolution. + The square of the slope of the cone. It is defined as + :math:`\left(\frac{r}{h}\right)^2` for a radius, :math:`r` and an axial + distance :math:`h` from the apex. An easy way to define this quantity is + to take the square of the radius of the cone (in cm) 1 cm from the apex. boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles @@ -2054,7 +2054,7 @@ class YCone(QuadricMixin, Surface): z0 : float z-coordinate of the apex in [cm] r2 : float - Parameter related to the aperature + Parameter related to the aperture boundary_type : {'transmission', 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. @@ -2113,7 +2113,7 @@ def evaluate(self, point): class ZCone(QuadricMixin, Surface): - """A cone parallel to the z-axis of the form :math:`(x - x_0)^2 + (y - y_0)^2 = + r"""A cone parallel to the z-axis of the form :math:`(x - x_0)^2 + (y - y_0)^2 = r^2 (z - z_0)^2`. .. Note:: @@ -2123,15 +2123,16 @@ class ZCone(QuadricMixin, Surface): Parameters ---------- x0 : float, optional - x-coordinate of the apex in [cm]. Defaults to 0. + x-coordinate of the apex in [cm]. y0 : float, optional - y-coordinate of the apex in [cm]. Defaults to 0. + y-coordinate of the apex in [cm]. z0 : float, optional - z-coordinate of the apex in [cm]. Defaults to 0. + z-coordinate of the apex in [cm]. r2 : float, optional - Parameter related to the aperature [cm^2]. - This is the square of the radius of the cone 1 cm from. - This can also be treated as the square of the slope of the cone relative to its axis. + The square of the slope of the cone. It is defined as + :math:`\left(\frac{r}{h}\right)^2` for a radius, :math:`r` and an axial + distance :math:`h` from the apex. An easy way to define this quantity is + to take the square of the radius of the cone (in cm) 1 cm from the apex. boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles @@ -2155,7 +2156,7 @@ class ZCone(QuadricMixin, Surface): z0 : float z-coordinate of the apex in [cm] r2 : float - Parameter related to the aperature + Parameter related to the aperture. boundary_type : {'transmission', 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. diff --git a/openmc/universe.py b/openmc/universe.py index 648b773df6f..85ce6fd9656 100644 --- a/openmc/universe.py +++ b/openmc/universe.py @@ -2,22 +2,16 @@ import math from abc import ABC, abstractmethod from collections.abc import Iterable -from numbers import Integral, Real +from numbers import Real from pathlib import Path from tempfile import TemporaryDirectory import warnings -import h5py -import lxml.etree as ET import numpy as np import openmc import openmc.checkvalue as cv -from ._xml import get_text -from .checkvalue import check_type, check_value from .mixin import IDManagerMixin -from .surface import _BOUNDARY_TYPES -from .utility_funcs import input_path class UniverseBase(ABC, IDManagerMixin): @@ -55,6 +49,10 @@ def __repr__(self): def name(self): return self._name + @property + def cells(self): + return self._cells + @name.setter def name(self, name): if name is not None: @@ -135,6 +133,130 @@ def create_xml_subelement(self, xml_element, memo=None): """ + def _determine_paths(self, path='', instances_only=False): + """Count the number of instances for each cell in the universe, and + record the count in the :attr:`Cell.num_instances` properties.""" + + univ_path = path + f'u{self.id}' + + for cell in self.cells.values(): + cell_path = f'{univ_path}->c{cell.id}' + fill = cell._fill + fill_type = cell.fill_type + + # If universe-filled, recursively count cells in filling universe + if fill_type == 'universe': + fill._determine_paths(cell_path + '->', instances_only) + # If lattice-filled, recursively call for all universes in lattice + elif fill_type == 'lattice': + latt = fill + + # Count instances in each universe in the lattice + for index in latt._natural_indices: + latt_path = '{}->l{}({})->'.format( + cell_path, latt.id, ",".join(str(x) for x in index)) + univ = latt.get_universe(index) + univ._determine_paths(latt_path, instances_only) + + else: + if fill_type == 'material': + mat = fill + elif fill_type == 'distribmat': + mat = fill[cell._num_instances] + else: + mat = None + + if mat is not None: + mat._num_instances += 1 + if not instances_only: + mat._paths.append(f'{cell_path}->m{mat.id}') + + # Append current path + cell._num_instances += 1 + if not instances_only: + cell._paths.append(cell_path) + + def add_cells(self, cells): + """Add multiple cells to the universe. + + Parameters + ---------- + cells : Iterable of openmc.Cell + Cells to add + + """ + + if not isinstance(cells, Iterable): + msg = f'Unable to add Cells to Universe ID="{self._id}" since ' \ + f'"{cells}" is not iterable' + raise TypeError(msg) + + for cell in cells: + self.add_cell(cell) + + @abstractmethod + def add_cell(self, cell): + pass + + @abstractmethod + def remove_cell(self, cell): + pass + + def clear_cells(self): + """Remove all cells from the universe.""" + + self._cells.clear() + + def get_all_cells(self, memo=None): + """Return all cells that are contained within the universe + + Returns + ------- + cells : dict + Dictionary whose keys are cell IDs and values are :class:`Cell` + instances + + """ + + if memo is None: + memo = set() + elif self in memo: + return {} + memo.add(self) + + # Add this Universe's cells to the dictionary + cells = {} + cells.update(self._cells) + + # Append all Cells in each Cell in the Universe to the dictionary + for cell in self._cells.values(): + cells.update(cell.get_all_cells(memo)) + + return cells + + def get_all_materials(self, memo=None): + """Return all materials that are contained within the universe + + Returns + ------- + materials : dict + Dictionary whose keys are material IDs and values are + :class:`Material` instances + + """ + + if memo is None: + memo = set() + + materials = {} + + # Append all Cells in each Cell in the Universe to the dictionary + cells = self.get_all_cells(memo) + for cell in cells.values(): + materials.update(cell.get_all_materials(memo)) + + return materials + @abstractmethod def _partial_deepcopy(self): """Deepcopy all parameters of an openmc.UniverseBase object except its cells. @@ -182,93 +304,6 @@ def clone(self, clone_materials=True, clone_regions=True, memo=None): return memo[self] - -class Universe(UniverseBase): - """A collection of cells that can be repeated. - - Parameters - ---------- - universe_id : int, optional - Unique identifier of the universe. If not specified, an identifier will - automatically be assigned - name : str, optional - Name of the universe. If not specified, the name is the empty string. - cells : Iterable of openmc.Cell, optional - Cells to add to the universe. By default no cells are added. - - Attributes - ---------- - id : int - Unique identifier of the universe - name : str - Name of the universe - cells : dict - Dictionary whose keys are cell IDs and values are :class:`Cell` - instances - volume : float - Volume of the universe in cm^3. This can either be set manually or - calculated in a stochastic volume calculation and added via the - :meth:`Universe.add_volume_information` method. - bounding_box : openmc.BoundingBox - Lower-left and upper-right coordinates of an axis-aligned bounding box - of the universe. - - """ - - def __init__(self, universe_id=None, name='', cells=None): - super().__init__(universe_id, name) - - if cells is not None: - self.add_cells(cells) - - def __repr__(self): - string = super().__repr__() - string += '{: <16}=\t{}\n'.format('\tGeom', 'CSG') - string += '{: <16}=\t{}\n'.format('\tCells', list(self._cells.keys())) - return string - - @property - def cells(self): - return self._cells - - @property - def bounding_box(self) -> openmc.BoundingBox: - regions = [c.region for c in self.cells.values() - if c.region is not None] - if regions: - return openmc.Union(regions).bounding_box - else: - return openmc.BoundingBox.infinite() - - @classmethod - def from_hdf5(cls, group, cells): - """Create universe from HDF5 group - - Parameters - ---------- - group : h5py.Group - Group in HDF5 file - cells : dict - Dictionary mapping cell IDs to instances of :class:`openmc.Cell`. - - Returns - ------- - openmc.Universe - Universe instance - - """ - universe_id = int(group.name.split('/')[-1].lstrip('universe ')) - cell_ids = group['cells'][()] - - # Create this Universe - universe = cls(universe_id) - - # Add each Cell to the Universe - for cell_id in cell_ids: - universe.add_cell(cells[cell_id]) - - return universe - def find(self, point): """Find cells/universes/lattices which contain a given point @@ -528,98 +563,37 @@ def plot(self, origin=None, width=None, pixels=40000, axes.imshow(img, extent=(x_min, x_max, y_min, y_max), **kwargs) return axes - def add_cell(self, cell): - """Add a cell to the universe. + def get_nuclides(self): + """Returns all nuclides in the universe - Parameters - ---------- - cell : openmc.Cell - Cell to add + Returns + ------- + nuclides : list of str + List of nuclide names """ - if not isinstance(cell, openmc.Cell): - msg = f'Unable to add a Cell to Universe ID="{self._id}" since ' \ - f'"{cell}" is not a Cell' - raise TypeError(msg) + nuclides = [] - cell_id = cell.id + # Append all Nuclides in each Cell in the Universe to the dictionary + for cell in self.cells.values(): + for nuclide in cell.get_nuclides(): + if nuclide not in nuclides: + nuclides.append(nuclide) - if cell_id not in self._cells: - self._cells[cell_id] = cell + return nuclides - def add_cells(self, cells): - """Add multiple cells to the universe. + def get_nuclide_densities(self): + """Return all nuclides contained in the universe - Parameters - ---------- - cells : Iterable of openmc.Cell - Cells to add + Returns + ------- + nuclides : dict + Dictionary whose keys are nuclide names and values are 2-tuples of + (nuclide, density) """ - - if not isinstance(cells, Iterable): - msg = f'Unable to add Cells to Universe ID="{self._id}" since ' \ - f'"{cells}" is not iterable' - raise TypeError(msg) - - for cell in cells: - self.add_cell(cell) - - def remove_cell(self, cell): - """Remove a cell from the universe. - - Parameters - ---------- - cell : openmc.Cell - Cell to remove - - """ - - if not isinstance(cell, openmc.Cell): - msg = f'Unable to remove a Cell from Universe ID="{self._id}" ' \ - f'since "{cell}" is not a Cell' - raise TypeError(msg) - - # If the Cell is in the Universe's list of Cells, delete it - self._cells.pop(cell.id, None) - - def clear_cells(self): - """Remove all cells from the universe.""" - - self._cells.clear() - - def get_nuclides(self): - """Returns all nuclides in the universe - - Returns - ------- - nuclides : list of str - List of nuclide names - - """ - - nuclides = [] - - # Append all Nuclides in each Cell in the Universe to the dictionary - for cell in self.cells.values(): - for nuclide in cell.get_nuclides(): - if nuclide not in nuclides: - nuclides.append(nuclide) - - return nuclides - - def get_nuclide_densities(self): - """Return all nuclides contained in the universe - - Returns - ------- - nuclides : dict - Dictionary whose keys are nuclide names and values are 2-tuples of - (nuclide, density) - - """ - nuclides = {} + nuclides = {} if self._atoms: volume = self.volume @@ -636,150 +610,20 @@ def get_nuclide_densities(self): return nuclides - def get_all_cells(self, memo=None): - """Return all cells that are contained within the universe - - Returns - ------- - cells : dict - Dictionary whose keys are cell IDs and values are :class:`Cell` - instances - - """ - - if memo is None: - memo = set() - elif self in memo: - return {} - memo.add(self) - - # Add this Universe's cells to the dictionary - cells = {} - cells.update(self._cells) - - # Append all Cells in each Cell in the Universe to the dictionary - for cell in self._cells.values(): - cells.update(cell.get_all_cells(memo)) - - return cells - - def get_all_materials(self, memo=None): - """Return all materials that are contained within the universe - - Returns - ------- - materials : dict - Dictionary whose keys are material IDs and values are - :class:`Material` instances - - """ - - if memo is None: - memo = set() - - materials = {} - - # Append all Cells in each Cell in the Universe to the dictionary - cells = self.get_all_cells(memo) - for cell in cells.values(): - materials.update(cell.get_all_materials(memo)) - - return materials - - def create_xml_subelement(self, xml_element, memo=None): - if memo is None: - memo = set() - - # Iterate over all Cells - for cell in self._cells.values(): - - # If the cell was already written, move on - if cell in memo: - continue - - memo.add(cell) - - # Create XML subelement for this Cell - cell_element = cell.create_xml_subelement(xml_element, memo) - - # Append the Universe ID to the subelement and add to Element - cell_element.set("universe", str(self._id)) - xml_element.append(cell_element) - - def _determine_paths(self, path='', instances_only=False): - """Count the number of instances for each cell in the universe, and - record the count in the :attr:`Cell.num_instances` properties.""" - - univ_path = path + f'u{self.id}' - - for cell in self.cells.values(): - cell_path = f'{univ_path}->c{cell.id}' - fill = cell._fill - fill_type = cell.fill_type - - # If universe-filled, recursively count cells in filling universe - if fill_type == 'universe': - fill._determine_paths(cell_path + '->', instances_only) - - # If lattice-filled, recursively call for all universes in lattice - elif fill_type == 'lattice': - latt = fill - - # Count instances in each universe in the lattice - for index in latt._natural_indices: - latt_path = '{}->l{}({})->'.format( - cell_path, latt.id, ",".join(str(x) for x in index)) - univ = latt.get_universe(index) - univ._determine_paths(latt_path, instances_only) - - else: - if fill_type == 'material': - mat = fill - elif fill_type == 'distribmat': - mat = fill[cell._num_instances] - else: - mat = None - - if mat is not None: - mat._num_instances += 1 - if not instances_only: - mat._paths.append(f'{cell_path}->m{mat.id}') - - # Append current path - cell._num_instances += 1 - if not instances_only: - cell._paths.append(cell_path) - - def _partial_deepcopy(self): - """Clone all of the openmc.Universe object's attributes except for its cells, - as they are copied within the clone function. This should only to be - used within the openmc.UniverseBase.clone() context. - """ - clone = openmc.Universe(name=self.name) - clone.volume = self.volume - return clone - -class DAGMCUniverse(UniverseBase): - """A reference to a DAGMC file to be used in the model. - .. versionadded:: 0.13.0 +class Universe(UniverseBase): + """A collection of cells that can be repeated. Parameters ---------- - filename : path-like - Path to the DAGMC file used to represent this universe. universe_id : int, optional Unique identifier of the universe. If not specified, an identifier will - automatically be assigned. + automatically be assigned name : str, optional Name of the universe. If not specified, the name is the empty string. - auto_geom_ids : bool - Set IDs automatically on initialization (True) or report overlaps in ID - space between CSG and DAGMC (False) - auto_mat_ids : bool - Set IDs automatically on initialization (True) or report overlaps in ID - space between OpenMC and UWUW materials (False) + cells : Iterable of openmc.Cell, optional + Cells to add to the universe. By default no cells are added. Attributes ---------- @@ -787,334 +631,133 @@ class DAGMCUniverse(UniverseBase): Unique identifier of the universe name : str Name of the universe - filename : str - Path to the DAGMC file used to represent this universe. - auto_geom_ids : bool - Set IDs automatically on initialization (True) or report overlaps in ID - space between CSG and DAGMC (False) - auto_mat_ids : bool - Set IDs automatically on initialization (True) or report overlaps in ID - space between OpenMC and UWUW materials (False) + cells : dict + Dictionary whose keys are cell IDs and values are :class:`Cell` + instances + volume : float + Volume of the universe in cm^3. This can either be set manually or + calculated in a stochastic volume calculation and added via the + :meth:`Universe.add_volume_information` method. bounding_box : openmc.BoundingBox Lower-left and upper-right coordinates of an axis-aligned bounding box of the universe. - .. versionadded:: 0.13.1 - material_names : list of str - Return a sorted list of materials names that are contained within the - DAGMC h5m file. This is useful when naming openmc.Material() objects - as each material name present in the DAGMC h5m file must have a - matching openmc.Material() with the same name. - - .. versionadded:: 0.13.2 - n_cells : int - The number of cells in the DAGMC model. This is the number of cells at - runtime and accounts for the implicit complement whether or not is it - present in the DAGMC file. - - .. versionadded:: 0.13.2 - n_surfaces : int - The number of surfaces in the model. - - .. versionadded:: 0.13.2 - """ - def __init__(self, - filename: cv.PathLike, - universe_id=None, - name='', - auto_geom_ids=False, - auto_mat_ids=False): + def __init__(self, universe_id=None, name='', cells=None): 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 + + if cells is not None: + self.add_cells(cells) def __repr__(self): string = super().__repr__() - string += '{: <16}=\t{}\n'.format('\tGeom', 'DAGMC') - string += '{: <16}=\t{}\n'.format('\tFile', self.filename) + string += '{: <16}=\t{}\n'.format('\tGeom', 'CSG') + string += '{: <16}=\t{}\n'.format('\tCells', list(self._cells.keys())) return string @property - def bounding_box(self): - with h5py.File(self.filename) as dagmc_file: - coords = dagmc_file['tstt']['nodes']['coordinates'][()] - lower_left_corner = coords.min(axis=0) - upper_right_corner = coords.max(axis=0) - return openmc.BoundingBox(lower_left_corner, upper_right_corner) - - @property - def filename(self): - return self._filename - - @filename.setter - def filename(self, val: cv.PathLike): - cv.check_type('DAGMC filename', val, cv.PathLike) - self._filename = input_path(val) - - @property - def auto_geom_ids(self): - return self._auto_geom_ids - - @auto_geom_ids.setter - def auto_geom_ids(self, val): - cv.check_type('DAGMC automatic geometry ids', val, bool) - self._auto_geom_ids = val - - @property - def auto_mat_ids(self): - return self._auto_mat_ids - - @auto_mat_ids.setter - def auto_mat_ids(self, val): - cv.check_type('DAGMC automatic material ids', val, bool) - self._auto_mat_ids = val - - @property - def material_names(self): - dagmc_file_contents = h5py.File(self.filename) - material_tags_hex = dagmc_file_contents['/tstt/tags/NAME'].get( - 'values') - material_tags_ascii = [] - for tag in material_tags_hex: - candidate_tag = tag.tobytes().decode().replace('\x00', '') - # tags might be for temperature or reflective surfaces - if candidate_tag.startswith('mat:'): - # removes first 4 characters as openmc.Material name should be - # set without the 'mat:' part of the tag - material_tags_ascii.append(candidate_tag[4:]) - - return sorted(set(material_tags_ascii)) - - def get_all_cells(self, memo=None): - return {} - - def get_all_materials(self, memo=None): - return {} + def bounding_box(self) -> openmc.BoundingBox: + regions = [c.region for c in self.cells.values() + if c.region is not None] + if regions: + return openmc.Union(regions).bounding_box + else: + return openmc.BoundingBox.infinite() - def _n_geom_elements(self, geom_type): - """ - Helper function for retrieving the number geometric entities in a DAGMC - file + @classmethod + def from_hdf5(cls, group, cells): + """Create universe from HDF5 group Parameters ---------- - geom_type : str - The type of geometric entity to count. One of {'Volume', 'Surface'}. Returns - the runtime number of voumes in the DAGMC model (includes implicit complement). + group : h5py.Group + Group in HDF5 file + cells : dict + Dictionary mapping cell IDs to instances of :class:`openmc.Cell`. Returns ------- - int - Number of geometry elements of the specified type - """ - cv.check_value('geometry type', geom_type, ('volume', 'surface')) - - def decode_str_tag(tag_val): - return tag_val.tobytes().decode().replace('\x00', '') - - with h5py.File(self.filename) as dagmc_file: - category_data = dagmc_file['tstt/tags/CATEGORY/values'] - category_strs = map(decode_str_tag, category_data) - n = sum([v == geom_type.capitalize() for v in category_strs]) - - # check for presence of an implicit complement in the file and - # increment the number of cells if it doesn't exist - if geom_type == 'volume': - name_data = dagmc_file['tstt/tags/NAME/values'] - name_strs = map(decode_str_tag, name_data) - if not sum(['impl_complement' in n for n in name_strs]): - n += 1 - return n + openmc.Universe + Universe instance - @property - def n_cells(self): - return self._n_geom_elements('volume') + """ + universe_id = int(group.name.split('/')[-1].lstrip('universe ')) + cell_ids = group['cells'][()] - @property - def n_surfaces(self): - return self._n_geom_elements('surface') + # Create this Universe + universe = cls(universe_id) - def create_xml_subelement(self, xml_element, memo=None): - if memo is None: - memo = set() + # Add each Cell to the Universe + for cell_id in cell_ids: + universe.add_cell(cells[cell_id]) - if self in memo: - return + return universe - memo.add(self) - # Set xml element values - dagmc_element = ET.Element('dagmc_universe') - dagmc_element.set('id', str(self.id)) - - if self.auto_geom_ids: - dagmc_element.set('auto_geom_ids', 'true') - if self.auto_mat_ids: - dagmc_element.set('auto_mat_ids', 'true') - dagmc_element.set('filename', str(self.filename)) - xml_element.append(dagmc_element) - - def bounding_region( - self, - bounded_type: str = 'box', - boundary_type: str = 'vacuum', - starting_id: int = 10000, - padding_distance: float = 0. - ): - """Creates a either a spherical or box shaped bounding region around - the DAGMC geometry. - - .. versionadded:: 0.13.1 + def add_cell(self, cell): + """Add a cell to the universe. Parameters ---------- - bounded_type : str - The type of bounding surface(s) to use when constructing the region. - Options include a single spherical surface (sphere) or a rectangle - made from six planes (box). - boundary_type : str - Boundary condition that defines the behavior for particles hitting - the surface. Defaults to vacuum boundary condition. Passed into the - surface construction. - starting_id : int - Starting ID of the surface(s) used in the region. For bounded_type - 'box', the next 5 IDs will also be used. Defaults to 10000 to reduce - the chance of an overlap of surface IDs with the DAGMC geometry. - padding_distance : float - Distance between the bounding region surfaces and the minimal - bounding box. Allows for the region to be larger than the DAGMC - geometry. + cell : openmc.Cell + Cell to add - Returns - ------- - openmc.Region - Region instance """ - check_type('boundary type', boundary_type, str) - check_value('boundary type', boundary_type, _BOUNDARY_TYPES) - check_type('starting surface id', starting_id, Integral) - check_type('bounded type', bounded_type, str) - check_value('bounded type', bounded_type, ('box', 'sphere')) - - bbox = self.bounding_box.expand(padding_distance, True) - - if bounded_type == 'sphere': - radius = np.linalg.norm(bbox.upper_right - bbox.center) - bounding_surface = openmc.Sphere( - surface_id=starting_id, - x0=bbox.center[0], - y0=bbox.center[1], - z0=bbox.center[2], - boundary_type=boundary_type, - r=radius, - ) - - return -bounding_surface - - if bounded_type == 'box': - # defines plane surfaces for all six faces of the bounding box - lower_x = openmc.XPlane(bbox[0][0], surface_id=starting_id) - upper_x = openmc.XPlane(bbox[1][0], surface_id=starting_id+1) - lower_y = openmc.YPlane(bbox[0][1], surface_id=starting_id+2) - upper_y = openmc.YPlane(bbox[1][1], surface_id=starting_id+3) - lower_z = openmc.ZPlane(bbox[0][2], surface_id=starting_id+4) - upper_z = openmc.ZPlane(bbox[1][2], surface_id=starting_id+5) - - region = +lower_x & -upper_x & +lower_y & -upper_y & +lower_z & -upper_z - - for surface in region.get_surfaces().values(): - surface.boundary_type = boundary_type - - return region - - def bounded_universe(self, bounding_cell_id=10000, **kwargs): - """Returns an openmc.Universe filled with this DAGMCUniverse and bounded - with a cell. Defaults to a box cell with a vacuum surface however this - can be changed using the kwargs which are passed directly to - DAGMCUniverse.bounding_region(). + if not isinstance(cell, openmc.Cell): + msg = f'Unable to add a Cell to Universe ID="{self._id}" since ' \ + f'"{cell}" is not a Cell' + raise TypeError(msg) - Parameters - ---------- - bounding_cell_id : int - The cell ID number to use for the bounding cell, defaults to 10000 to reduce - the chance of overlapping ID numbers with the DAGMC geometry. + cell_id = cell.id - Returns - ------- - openmc.Universe - Universe instance - """ - bounding_cell = openmc.Cell( - fill=self, cell_id=bounding_cell_id, region=self.bounding_region(**kwargs)) - return openmc.Universe(cells=[bounding_cell]) + if cell_id not in self._cells: + self._cells[cell_id] = cell - @classmethod - def from_hdf5(cls, group): - """Create DAGMC universe from HDF5 group + def remove_cell(self, cell): + """Remove a cell from the universe. Parameters ---------- - group : h5py.Group - Group in HDF5 file - - Returns - ------- - openmc.DAGMCUniverse - DAGMCUniverse instance + cell : openmc.Cell + Cell to remove """ - id = int(group.name.split('/')[-1].lstrip('universe ')) - fname = group['filename'][()].decode() - name = group['name'][()].decode() if 'name' in group else None - - out = cls(fname, universe_id=id, name=name) - - out.auto_geom_ids = bool(group.attrs['auto_geom_ids']) - out.auto_mat_ids = bool(group.attrs['auto_mat_ids']) - return out - - @classmethod - def from_xml_element(cls, elem): - """Generate DAGMC universe from XML element + if not isinstance(cell, openmc.Cell): + msg = f'Unable to remove a Cell from Universe ID="{self._id}" ' \ + f'since "{cell}" is not a Cell' + raise TypeError(msg) - Parameters - ---------- - elem : lxml.etree._Element - `` element + # If the Cell is in the Universe's list of Cells, delete it + self._cells.pop(cell.id, None) - Returns - ------- - openmc.DAGMCUniverse - DAGMCUniverse instance + def create_xml_subelement(self, xml_element, memo=None): + if memo is None: + memo = set() - """ - id = int(get_text(elem, 'id')) - fname = get_text(elem, 'filename') + # Iterate over all Cells + for cell in self._cells.values(): - out = cls(fname, universe_id=id) + # If the cell was already written, move on + if cell in memo: + continue - name = get_text(elem, 'name') - if name is not None: - out.name = name + memo.add(cell) - out.auto_geom_ids = bool(elem.get('auto_geom_ids')) - out.auto_mat_ids = bool(elem.get('auto_mat_ids')) + # Create XML subelement for this Cell + cell_element = cell.create_xml_subelement(xml_element, memo) - return out + # Append the Universe ID to the subelement and add to Element + cell_element.set("universe", str(self._id)) + xml_element.append(cell_element) 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 - only to be used within the openmc.UniverseBase.clone() context. + """Clone all of the openmc.Universe object's attributes except for its cells, + as they are copied within the clone function. This should only to be + used within the openmc.UniverseBase.clone() context. """ - clone = openmc.DAGMCUniverse(name=self.name, filename=self.filename) + clone = openmc.Universe(name=self.name) clone.volume = self.volume - clone.auto_geom_ids = self.auto_geom_ids - clone.auto_mat_ids = self.auto_mat_ids return clone diff --git a/pyproject.toml b/pyproject.toml index d0419d550f4..e4e8472f1aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ authors = [ ] description = "OpenMC" version = "0.15.1-dev" -requires-python = ">=3.10" +requires-python = ">=3.11" license = {file = "LICENSE"} classifiers = [ "Development Status :: 4 - Beta", @@ -21,9 +21,9 @@ classifiers = [ "Topic :: Scientific/Engineering", "Programming Language :: C++", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] dependencies = [ "numpy", diff --git a/src/cell.cpp b/src/cell.cpp index 88876678706..d4d28fb70e6 100644 --- a/src/cell.cpp +++ b/src/cell.cpp @@ -252,12 +252,12 @@ void Cell::to_hdf5(hid_t cell_group) const // default constructor CSGCell::CSGCell() { - geom_type_ = GeometryType::CSG; + geom_type() = GeometryType::CSG; } CSGCell::CSGCell(pugi::xml_node cell_node) { - geom_type_ = GeometryType::CSG; + geom_type() = GeometryType::CSG; if (check_for_node(cell_node, "id")) { id_ = std::stoi(get_node_value(cell_node, "id")); diff --git a/src/dagmc.cpp b/src/dagmc.cpp index b79676c3626..134e31ecf3c 100644 --- a/src/dagmc.cpp +++ b/src/dagmc.cpp @@ -72,6 +72,23 @@ 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 mat name for each assignement instances + vector instance_mats = + get_node_array(cell_node, "material_ids"); + + // Store mat name for each instances + material_overrides_.emplace(ref_assignment, instance_mats); + } + } + initialize(); } @@ -211,12 +228,13 @@ void DAGUniverse::init_geometry() if (mat_str == "graveyard") { graveyard = vol_handle; } - // material void checks if (mat_str == "void" || mat_str == "vacuum" || mat_str == "graveyard") { c->material_.push_back(MATERIAL_VOID); } else { - if (uses_uwuw()) { + 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); @@ -609,6 +627,33 @@ void DAGUniverse::uwuw_assign_material( fatal_error("DAGMC was not configured with UWUW."); #endif // OPENMC_UWUW } + +void DAGUniverse::override_assign_material(std::unique_ptr& c) 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 + // metadata + // Notify User that an override is being applied on a DAGMCCell + write_message(fmt::format("Applying override for DAGMCCell {}", c->id_), 8); + + if (settings::verbosity >= 10) { + auto msg = fmt::format("Assigning DAGMC cell {} material(s) based on " + "override information (see input XML).", + c->id_); + 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()) { + fatal_error(fmt::format( + "Material with ID '{}' not found for DAGMC cell {}", mat_id, c->id_)); + } + c->material_.push_back(mat_id); + } +} + //============================================================================== // DAGMC Cell implementation //============================================================================== @@ -616,7 +661,7 @@ void DAGUniverse::uwuw_assign_material( DAGCell::DAGCell(std::shared_ptr dag_ptr, int32_t dag_idx) : Cell {}, dagmc_ptr_(dag_ptr), dag_index_(dag_idx) { - geom_type_ = GeometryType::DAG; + geom_type() = GeometryType::DAG; }; std::pair DAGCell::distance( @@ -719,7 +764,7 @@ BoundingBox DAGCell::bounding_box() const DAGSurface::DAGSurface(std::shared_ptr dag_ptr, int32_t dag_idx) : Surface {}, dagmc_ptr_(dag_ptr), dag_index_(dag_idx) { - geom_type_ = GeometryType::DAG; + geom_type() = GeometryType::DAG; } // empty constructor moab::EntityHandle DAGSurface::mesh_handle() const @@ -818,12 +863,61 @@ int32_t next_cell(int32_t surf, int32_t curr_cell, int32_t univ) return univp->cell_index(new_vol); } +extern "C" int openmc_dagmc_universe_get_cell_ids( + int32_t univ_id, int32_t* ids, size_t* n) +{ + // make sure the universe id is a DAGMC Universe + const auto& univ = model::universes[model::universe_map[univ_id]]; + if (univ->geom_type() != GeometryType::DAG) { + set_errmsg(fmt::format("Universe {} is not a DAGMC Universe", univ_id)); + return OPENMC_E_INVALID_TYPE; + } + + std::vector dag_cell_ids; + for (const auto& cell_index : univ->cells_) { + const auto& cell = model::cells[cell_index]; + if (cell->geom_type() == GeometryType::CSG) { + set_errmsg(fmt::format("Cell {} is not a DAGMC Cell", cell->id_)); + return OPENMC_E_INVALID_TYPE; + } + dag_cell_ids.push_back(cell->id_); + } + std::copy(dag_cell_ids.begin(), dag_cell_ids.end(), ids); + *n = dag_cell_ids.size(); + return 0; +} + +extern "C" int openmc_dagmc_universe_get_num_cells(int32_t univ_id, size_t* n) +{ + // make sure the universe id is a DAGMC Universe + const auto& univ = model::universes[model::universe_map[univ_id]]; + if (univ->geom_type() != GeometryType::DAG) { + set_errmsg(fmt::format("Universe {} is not a DAGMC universe", univ_id)); + return OPENMC_E_INVALID_TYPE; + } + *n = univ->cells_.size(); + return 0; +} + } // namespace openmc #else namespace openmc { +extern "C" int openmc_dagmc_universe_get_cell_ids( + int32_t univ_id, int32_t* ids, size_t* n) +{ + set_errmsg("OpenMC was not configured with DAGMC"); + return OPENMC_E_UNASSIGNED; +}; + +extern "C" int openmc_dagmc_universe_get_num_cells(int32_t univ_id, size_t* n) +{ + set_errmsg("OpenMC was not configured with DAGMC"); + return OPENMC_E_UNASSIGNED; +}; + void read_dagmc_universes(pugi::xml_node node) { if (check_for_node(node, "dagmc_universe")) { diff --git a/src/external/quartic_solver.cpp b/src/external/quartic_solver.cpp index 0b280e83c29..915020ffaa3 100644 --- a/src/external/quartic_solver.cpp +++ b/src/external/quartic_solver.cpp @@ -1,4 +1,5 @@ #include +#define _USE_MATH_DEFINES // to make M_PI declared in Intel and MSVC compilers #include #include #include diff --git a/src/mesh.cpp b/src/mesh.cpp index 2ee616f28dc..97bf710caa6 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -1,7 +1,8 @@ #include "openmc/mesh.h" -#include // for copy, equal, min, min_element -#include // for ceil -#include // for size_t +#include // for copy, equal, min, min_element +#define _USE_MATH_DEFINES // to make M_PI declared in Intel and MSVC compilers +#include // for ceil +#include // for size_t #include #include @@ -245,7 +246,7 @@ void Mesh::to_hdf5(hid_t group) const hid_t mesh_group = create_group(group, group_name.c_str()); // Write mesh type - write_attribute(mesh_group, "type", this->get_mesh_type()); + write_dataset(mesh_group, "type", this->get_mesh_type()); // Write mesh ID write_attribute(mesh_group, "id", id_); @@ -307,7 +308,6 @@ Position StructuredMesh::sample_element( UnstructuredMesh::UnstructuredMesh(pugi::xml_node node) : Mesh(node) { - // check the mesh type if (check_for_node(node, "type")) { auto temp = get_node_value(node, "type", true, true); @@ -969,7 +969,6 @@ std::pair, vector> RegularMesh::plot( void RegularMesh::to_hdf5_inner(hid_t mesh_group) const { - write_dataset(mesh_group, "type", "regular"); write_dataset(mesh_group, "dimension", get_x_shape()); write_dataset(mesh_group, "lower_left", lower_left_); write_dataset(mesh_group, "upper_right", upper_right_); @@ -1155,7 +1154,6 @@ std::pair, vector> RectilinearMesh::plot( void RectilinearMesh::to_hdf5_inner(hid_t mesh_group) const { - write_dataset(mesh_group, "type", "rectilinear"); write_dataset(mesh_group, "x_grid", grid_[0]); write_dataset(mesh_group, "y_grid", grid_[1]); write_dataset(mesh_group, "z_grid", grid_[2]); @@ -1430,7 +1428,6 @@ std::pair, vector> CylindricalMesh::plot( void CylindricalMesh::to_hdf5_inner(hid_t mesh_group) const { - write_dataset(mesh_group, "type", "cylindrical"); write_dataset(mesh_group, "r_grid", grid_[0]); write_dataset(mesh_group, "phi_grid", grid_[1]); write_dataset(mesh_group, "z_grid", grid_[2]); @@ -1742,7 +1739,6 @@ std::pair, vector> SphericalMesh::plot( void SphericalMesh::to_hdf5_inner(hid_t mesh_group) const { - write_dataset(mesh_group, "type", SphericalMesh::mesh_type); write_dataset(mesh_group, "r_grid", grid_[0]); write_dataset(mesh_group, "theta_grid", grid_[1]); write_dataset(mesh_group, "phi_grid", grid_[2]); diff --git a/src/particle.cpp b/src/particle.cpp index 64c50c9438f..0ea8650143d 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -533,7 +533,7 @@ void Particle::cross_surface(const Surface& surf) // if we're crossing a CSG surface, make sure the DAG history is reset #ifdef DAGMC - if (surf.geom_type_ == GeometryType::CSG) + if (surf.geom_type() == GeometryType::CSG) history().reset(); #endif @@ -548,7 +548,7 @@ void Particle::cross_surface(const Surface& surf) #ifdef DAGMC // in DAGMC, we know what the next cell should be - if (surf.geom_type_ == GeometryType::DAG) { + if (surf.geom_type() == GeometryType::DAG) { int32_t i_cell = next_cell(std::abs(surface()), cell_last(n_coord() - 1), lowest_coord().universe) - 1; @@ -668,7 +668,8 @@ void Particle::cross_reflective_bc(const Surface& surf, Direction new_u) // the lower universes. // (unless we're using a dagmc model, which has exactly one universe) n_coord() = 1; - if (surf.geom_type_ != GeometryType::DAG && !neighbor_list_find_cell(*this)) { + if (surf.geom_type() != GeometryType::DAG && + !neighbor_list_find_cell(*this)) { mark_as_lost("Couldn't find particle after reflecting from surface " + std::to_string(surf.id_) + "."); return; diff --git a/src/photon.cpp b/src/photon.cpp index 4953e0ec157..4b2fb41978f 100644 --- a/src/photon.cpp +++ b/src/photon.cpp @@ -328,15 +328,19 @@ PhotonInteraction::PhotonInteraction(hid_t group) } // Take logarithm of energies and cross sections since they are log-log - // interpolated + // interpolated. Note that cross section libraries converted from ACE files + // represent zero as exp(-500) to avoid log-log interpolation errors. For + // values below exp(-499) we store the log as -900, for which exp(-900) + // evaluates to zero. + double limit = std::exp(-499.0); energy_ = xt::log(energy_); - coherent_ = xt::where(coherent_ > 0.0, xt::log(coherent_), -900.0); - incoherent_ = xt::where(incoherent_ > 0.0, xt::log(incoherent_), -900.0); + coherent_ = xt::where(coherent_ > limit, xt::log(coherent_), -900.0); + incoherent_ = xt::where(incoherent_ > limit, xt::log(incoherent_), -900.0); photoelectric_total_ = xt::where( - photoelectric_total_ > 0.0, xt::log(photoelectric_total_), -900.0); + photoelectric_total_ > limit, xt::log(photoelectric_total_), -900.0); pair_production_total_ = xt::where( - pair_production_total_ > 0.0, xt::log(pair_production_total_), -900.0); - heating_ = xt::where(heating_ > 0.0, xt::log(heating_), -900.0); + pair_production_total_ > limit, xt::log(pair_production_total_), -900.0); + heating_ = xt::where(heating_ > limit, xt::log(heating_), -900.0); } PhotonInteraction::~PhotonInteraction() diff --git a/src/plot.cpp b/src/plot.cpp index 348138570c1..85131d67a01 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -1,6 +1,8 @@ #include "openmc/plot.h" #include +#define _USE_MATH_DEFINES // to make M_PI declared in Intel and MSVC compilers +#include #include #include #include @@ -1301,7 +1303,7 @@ void ProjectionPlot::create_output() const int32_t i_surface = std::abs(p.surface()) - 1; if (i_surface > 0 && - model::surfaces[i_surface]->geom_type_ == GeometryType::DAG) { + model::surfaces[i_surface]->geom_type() == GeometryType::DAG) { #ifdef DAGMC int32_t i_cell = next_cell(i_surface, p.cell_last(p.n_coord() - 1), p.lowest_coord().universe); diff --git a/src/surface.cpp b/src/surface.cpp index 50ef2a12830..dbcaf849848 100644 --- a/src/surface.cpp +++ b/src/surface.cpp @@ -165,9 +165,9 @@ void Surface::to_hdf5(hid_t group_id) const { hid_t surf_group = create_group(group_id, fmt::format("surface {}", id_)); - if (geom_type_ == GeometryType::DAG) { + if (geom_type() == GeometryType::DAG) { write_string(surf_group, "geom_type", "dagmc", false); - } else if (geom_type_ == GeometryType::CSG) { + } else if (geom_type() == GeometryType::CSG) { write_string(surf_group, "geom_type", "csg", false); if (bc_) { @@ -189,11 +189,11 @@ void Surface::to_hdf5(hid_t group_id) const CSGSurface::CSGSurface() : Surface {} { - geom_type_ = GeometryType::CSG; + geom_type() = GeometryType::CSG; }; CSGSurface::CSGSurface(pugi::xml_node surf_node) : Surface {surf_node} { - geom_type_ = GeometryType::CSG; + geom_type() = GeometryType::CSG; }; //============================================================================== diff --git a/src/weight_windows.cpp b/src/weight_windows.cpp index 68f7550ae5a..e798a2f7abd 100644 --- a/src/weight_windows.cpp +++ b/src/weight_windows.cpp @@ -737,7 +737,7 @@ WeightWindowsGenerator::WeightWindowsGenerator(pugi::xml_node node) int32_t mesh_idx = model::mesh_map[mesh_id]; max_realizations_ = std::stoi(get_node_value(node, "max_realizations")); - int active_batches = settings::n_batches - settings::n_inactive; + int32_t active_batches = settings::n_batches - settings::n_inactive; if (max_realizations_ > active_batches) { auto msg = fmt::format("The maximum number of specified tally realizations ({}) is " diff --git a/tests/unit_tests/dagmc/test_model.py b/tests/unit_tests/dagmc/test_model.py new file mode 100644 index 00000000000..9fdfbcebc68 --- /dev/null +++ b/tests/unit_tests/dagmc/test_model.py @@ -0,0 +1,256 @@ +from pathlib import Path + +import lxml.etree as ET +import numpy as np +import pytest +import openmc +from openmc.utility_funcs import change_directory + +pytestmark = pytest.mark.skipif( + not openmc.lib._dagmc_enabled(), + reason="DAGMC CAD geometry is not enabled.") + + +@pytest.fixture() +def model(request): + pitch = 1.26 + + 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["41"] = openmc.Material(name="41") + mats["41"].add_nuclide("H1", 2.0) + mats["41"].add_element("O", 1.0) + mats["41"].set_density("g/cm3", 1.0) + mats["41"].add_s_alpha_beta("c_H_in_H2O") + + p = Path(request.fspath).parent / "dagmc.h5m" + + daguniv = openmc.DAGMCUniverse(p, auto_geom_ids=True) + + lattice = openmc.RectLattice() + lattice.dimension = [2, 2] + lattice.lower_left = [-pitch, -pitch] + lattice.pitch = [pitch, pitch] + lattice.universes = [ + [daguniv, daguniv], + [daguniv, daguniv]] + + box = openmc.model.RectangularParallelepiped(-pitch, pitch, -pitch, pitch, -5, 5) + + root = openmc.Universe(cells=[openmc.Cell(region=-box, fill=lattice)]) + + settings = openmc.Settings() + settings.batches = 100 + settings.inactive = 10 + settings.particles = 1000 + + ll, ur = root.bounding_box + mat_vol = openmc.VolumeCalculation([mats["no-void fuel"]], 1000000, ll, ur) + cell_vol = openmc.VolumeCalculation(list(root.cells.values()), 1000000, ll, ur) + settings.volume_calculations = [mat_vol, cell_vol] + + model = openmc.Model() + model.materials = openmc.Materials(mats.values()) + model.geometry = openmc.Geometry(root=root) + model.settings = settings + + with change_directory(tmpdir=True): + try: + model.init_lib() + model.sync_dagmc_universes() + yield model + finally: + model.finalize_lib() + 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") + + for univ in model.geometry.get_all_universes().values(): + if not isinstance(univ, openmc.DAGMCUniverse): + 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"] + + +def test_dagmc_add_material_override_with_id(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") + + for univ in model.geometry.get_all_universes().values(): + if not isinstance(univ, openmc.DAGMCUniverse): + 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.add_material_override(cell.id, mats["foo"]) + for cell_id in cells_with_41: + assert univ.cells[cell_id] == mats["foo"] + + +def test_dagmc_add_material_override_with_cell(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") + + for univ in model.geometry.get_all_universes().values(): + if not isinstance(univ, openmc.DAGMCUniverse): + 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.add_material_override(cell, mats["foo"]) + for cell_id in cells_with_41: + assert univ.cells[cell_id] == mats["foo"] + + +def test_model_differentiate_depletable_with_dagmc(model, run_in_tmpdir): + model.calculate_volumes() + + # Get the volume of the no-void fuel material before differentiation + volume_before = np.sum([m.volume for m in model.materials if m.name == "no-void fuel"]) + + # Differentiate the depletable materials + model.differentiate_depletable_mats(diff_volume_method="divide equally") + # Get the volume of the no-void fuel material after differentiation + volume_after = np.sum([m.volume for m in model.materials if "fuel" in m.name]) + assert np.isclose(volume_before, volume_after) + assert len(model.materials) == 4*2 +1 + + +def test_model_differentiate_with_dagmc(model): + root = model.geometry.root_universe + ll, ur = root.bounding_box + model.calculate_volumes() + # Get the volume of the no-void fuel material before differentiation + volume_before = np.sum([m.volume for m in model.materials if m.name == "no-void fuel"]) + + # Differentiate all the materials + model.differentiate_mats(depletable_only=False) + + # Get the volume of the no-void fuel material after differentiation + mat_vol = openmc.VolumeCalculation(model.materials, 1000000, ll, ur) + model.settings.volume_calculations = [mat_vol] + model.init_lib() # need to reinitialize the lib after differentiating the materials + model.calculate_volumes() + volume_after = np.sum([m.volume for m in model.materials if "fuel" in m.name]) + assert np.isclose(volume_before, volume_after) + 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") + + 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) + + # Tesing the XML subelement generation + root = ET.Element('dagmc_universe') + dag_univ.create_xml_subelement(root) + dagmc_ele = root.find('dagmc_universe') + + assert dagmc_ele.get('id') == str(dag_univ.id) + 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 + + 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) + + model.export_to_model_xml() + + xml_model = openmc.Model.from_model_xml() + + for univ in xml_model.geometry.get_all_universes().values(): + if isinstance(univ, openmc.DAGMCUniverse): + xml_dagmc_univ = univ + break + + assert xml_dagmc_univ._material_overrides.keys() == dag_univ._material_overrides.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)]) diff --git a/tools/ci/gha-install.py b/tools/ci/gha-install.py index 282389a8f19..d52c4c8254c 100644 --- a/tools/ci/gha-install.py +++ b/tools/ci/gha-install.py @@ -31,6 +31,7 @@ def install(omp=False, mpi=False, phdf5=False, dagmc=False, libmesh=False, ncrys if dagmc: cmake_cmd.append('-DOPENMC_USE_DAGMC=ON') + cmake_cmd.append('-DOPENMC_USE_UWUW=ON') dagmc_path = os.environ.get('HOME') + '/DAGMC' cmake_cmd.append('-DCMAKE_PREFIX_PATH=' + dagmc_path)