mirror of
https://github.com/openmc-dev/openmc.git
synced 2026-07-21 06:25:30 -04:00
538 lines
16 KiB
Python
538 lines
16 KiB
Python
from __future__ import annotations
|
|
from abc import ABC, abstractmethod
|
|
from collections.abc import Iterable
|
|
from numbers import Real
|
|
|
|
import numpy as np
|
|
|
|
import openmc
|
|
import openmc.checkvalue as cv
|
|
from .mixin import IDManagerMixin
|
|
from .plots import add_plot_params
|
|
|
|
|
|
class UniverseBase(ABC, IDManagerMixin):
|
|
"""A collection of cells that can be repeated.
|
|
|
|
Attributes
|
|
----------
|
|
id : int
|
|
Unique identifier of the universe
|
|
name : str
|
|
Name of the universe
|
|
"""
|
|
|
|
next_id = 1
|
|
used_ids = set()
|
|
|
|
def __init__(self, universe_id=None, name=''):
|
|
# Initialize Universe class attributes
|
|
self.id = universe_id
|
|
self.name = name
|
|
self._volume = None
|
|
self._atoms = {}
|
|
|
|
# Keys - Cell IDs
|
|
# Values - Cells
|
|
self._cells = {}
|
|
|
|
def __repr__(self):
|
|
string = 'Universe\n'
|
|
string += '{: <16}=\t{}\n'.format('\tID', self._id)
|
|
string += '{: <16}=\t{}\n'.format('\tName', self._name)
|
|
return string
|
|
|
|
@property
|
|
def name(self):
|
|
return self._name
|
|
|
|
@property
|
|
def cells(self):
|
|
return self._cells
|
|
|
|
@name.setter
|
|
def name(self, name):
|
|
if name is not None:
|
|
cv.check_type('universe name', name, str)
|
|
self._name = name
|
|
else:
|
|
self._name = ''
|
|
|
|
@property
|
|
def volume(self):
|
|
return self._volume
|
|
|
|
@volume.setter
|
|
def volume(self, volume):
|
|
if volume is not None:
|
|
cv.check_type('universe volume', volume, Real)
|
|
self._volume = volume
|
|
|
|
def add_volume_information(self, volume_calc):
|
|
"""Add volume information to a universe.
|
|
|
|
Parameters
|
|
----------
|
|
volume_calc : openmc.VolumeCalculation
|
|
Results from a stochastic volume calculation
|
|
|
|
"""
|
|
if volume_calc.domain_type == 'universe':
|
|
if self.id in volume_calc.volumes:
|
|
self._volume = volume_calc.volumes[self.id].n
|
|
self._atoms = volume_calc.atoms[self.id]
|
|
else:
|
|
raise ValueError(
|
|
'No volume information found for this universe.')
|
|
else:
|
|
raise ValueError('No volume information found for this universe.')
|
|
|
|
def get_all_universes(self, memo=None):
|
|
"""Return all universes that are contained within this one.
|
|
|
|
Returns
|
|
-------
|
|
universes : dict
|
|
Dictionary whose keys are universe IDs and values are
|
|
:class:`Universe` instances
|
|
|
|
"""
|
|
if memo is None:
|
|
memo = set()
|
|
elif self in memo:
|
|
return {}
|
|
memo.add(self)
|
|
|
|
# Append all Universes within each Cell to the dictionary
|
|
universes = {}
|
|
for cell in self.get_all_cells().values():
|
|
universes.update(cell.get_all_universes(memo))
|
|
|
|
return universes
|
|
|
|
@abstractmethod
|
|
def create_xml_subelement(self, xml_element, memo=None):
|
|
"""Add the universe xml representation to an incoming xml element
|
|
|
|
Parameters
|
|
----------
|
|
xml_element : lxml.etree._Element
|
|
XML element to be added to
|
|
|
|
memo : set or None
|
|
A set of object id's representing geometry entities already
|
|
written to the xml_element. This parameter is used internally
|
|
and should not be specified by users.
|
|
|
|
Returns
|
|
-------
|
|
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.
|
|
This should only be used from the openmc.UniverseBase.clone() context.
|
|
|
|
"""
|
|
|
|
def clone(self, clone_materials=True, clone_regions=True, memo=None):
|
|
"""Create a copy of this universe with a new unique ID, and clones
|
|
all cells within this universe.
|
|
|
|
Parameters
|
|
----------
|
|
clone_materials : bool
|
|
Whether to create separates copies of the materials filling cells
|
|
contained in this universe.
|
|
clone_regions : bool
|
|
Whether to create separates copies of the regions bounding cells
|
|
contained in this universe.
|
|
memo : dict or None
|
|
A nested dictionary of previously cloned objects. This parameter
|
|
is used internally and should not be specified by the user.
|
|
|
|
Returns
|
|
-------
|
|
clone : openmc.Universe
|
|
The clone of this universe
|
|
|
|
"""
|
|
if memo is None:
|
|
memo = {}
|
|
|
|
# If no memoize'd clone exists, instantiate one
|
|
if self not in memo:
|
|
clone = self._partial_deepcopy()
|
|
|
|
# Clone all cells for the universe clone
|
|
clone._cells = {}
|
|
for cell in self._cells.values():
|
|
clone.add_cell(cell.clone(clone_materials, clone_regions,
|
|
memo))
|
|
|
|
# Memoize the clone
|
|
memo[self] = clone
|
|
|
|
return memo[self]
|
|
|
|
def find(self, point):
|
|
"""Find cells/universes/lattices which contain a given point
|
|
|
|
Parameters
|
|
----------
|
|
point : 3-tuple of float
|
|
Cartesian coordinates of the point
|
|
|
|
Returns
|
|
-------
|
|
list
|
|
Sequence of universes, cells, and lattices which are traversed to
|
|
find the given point
|
|
|
|
"""
|
|
p = np.asarray(point)
|
|
for cell in self._cells.values():
|
|
if p in cell:
|
|
if cell.fill_type in ('material', 'distribmat', 'void'):
|
|
return [self, cell]
|
|
elif cell.fill_type == 'universe':
|
|
if cell.translation is not None:
|
|
p -= cell.translation
|
|
if cell.rotation is not None:
|
|
p[:] = cell.rotation_matrix.dot(p)
|
|
return [self, cell] + cell.fill.find(p)
|
|
else:
|
|
return [self, cell] + cell.fill.find(p)
|
|
return []
|
|
|
|
@add_plot_params
|
|
def plot(self, *args, **kwargs):
|
|
"""Display a slice plot of the universe.
|
|
"""
|
|
model = openmc.Model()
|
|
model.geometry = openmc.Geometry(self)
|
|
return model.plot(*args, **kwargs)
|
|
|
|
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 = {}
|
|
|
|
if self._atoms:
|
|
volume = self.volume
|
|
for name, atoms in self._atoms.items():
|
|
density = 1.0e-24 * atoms.n/volume # density in atoms/b-cm
|
|
nuclides[name] = (name, density)
|
|
else:
|
|
raise RuntimeError(
|
|
'Volume information is needed to calculate microscopic cross '
|
|
f'sections for universe {self.id}. This can be done by running '
|
|
'a stochastic volume calculation via the '
|
|
'openmc.VolumeCalculation object')
|
|
|
|
return nuclides
|
|
|
|
|
|
|
|
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 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 add_cell(self, cell):
|
|
"""Add a cell to the universe.
|
|
|
|
Parameters
|
|
----------
|
|
cell : openmc.Cell
|
|
Cell to add
|
|
|
|
"""
|
|
|
|
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)
|
|
|
|
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.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 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 _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
|