Merge branch 'develop' into centre_for_cylinder_spherical_meshes

This commit is contained in:
Patrick Shriwise 2023-03-06 13:37:24 -06:00 committed by GitHub
commit 527f5f70aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
163 changed files with 14849 additions and 35970 deletions

View file

@ -604,6 +604,10 @@ class AggregateFilter:
def num_bins(self):
return len(self.bins) if self.aggregate_filter else 0
@property
def shape(self):
return (self.num_bins,)
@type.setter
def type(self, filter_type):
if filter_type not in _FILTER_TYPES:

View file

@ -246,7 +246,7 @@ class CMFDMesh:
Real)
check_greater_than('CMFD mesh {}-grid length'.format(dims[i]),
len(grid[i]), 1)
self._grid = np.array(grid)
self._grid = [np.array(g) for g in grid]
self._display_mesh_warning('rectilinear', 'CMFD mesh grid')
def _display_mesh_warning(self, mesh_type, variable_label):
@ -1382,9 +1382,7 @@ class CMFDRun:
"""
# Write each element in vector to file
with open(base_filename+'.dat', 'w') as fh:
for val in vector:
fh.write('{:0.8f}\n'.format(val))
np.savetxt(f'{base_filename}.dat', vector, fmt='%.8f')
# Save as numpy format
np.save(base_filename, vector)

View file

@ -492,7 +492,7 @@ def isotopes(element):
# Get the nuclides present in nature
result = []
for kv in sorted(NATURAL_ABUNDANCE.items()):
for kv in NATURAL_ABUNDANCE.items():
if re.match(r'{}\d+'.format(element), kv[0]):
result.append(kv)

View file

@ -51,7 +51,7 @@ _THERMAL_DATA = {
'c_Li_in_FLiBe': ThermalTuple('liflib', [3006, 3007], 1),
'c_Mg24': ThermalTuple('mg24', [12024], 1),
'c_N_in_UN': ThermalTuple('n-un', [7014, 7015], 1),
'c_O_in_Al2O3': ThermalTuple('osap00', [92238], 1),
'c_O_in_Al2O3': ThermalTuple('osap00', [8016, 8017, 8018], 1),
'c_O_in_BeO': ThermalTuple('obeo', [8016, 8017, 8018], 1),
'c_O_in_D2O': ThermalTuple('od2o', [8016, 8017, 8018], 1),
'c_O_in_H2O_solid': ThermalTuple('oice', [8016, 8017, 8018], 1),
@ -64,8 +64,8 @@ _THERMAL_DATA = {
'c_Si_in_SiC': ThermalTuple('sisic', [14028, 14029, 14030], 1),
'c_SiO2_alpha': ThermalTuple('sio2-a', [8016, 8017, 8018, 14028, 14029, 14030], 3),
'c_SiO2_beta': ThermalTuple('sio2-b', [8016, 8017, 8018, 14028, 14029, 14030], 3),
'c_U_in_UN': ThermalTuple('u-un', [92238], 1),
'c_U_in_UO2': ThermalTuple('uuo2', [8016, 8017, 8018], 1),
'c_U_in_UN': ThermalTuple('u-un', [92233, 92234, 92235, 92236, 92238], 1),
'c_U_in_UO2': ThermalTuple('uuo2', [92233, 92234, 92235, 92236, 92238], 1),
'c_Y_in_YH2': ThermalTuple('yyh2', [39089], 1),
'c_Zr_in_ZrH': ThermalTuple('zrzrh', [40000, 40090, 40091, 40092, 40094, 40096], 1),
'c_Zr_in_ZrH2': ThermalTuple('zrzrh2', [40000, 40090, 40091, 40092, 40094, 40096], 1),

View file

@ -223,8 +223,13 @@ class OpenMCOperator(TransportOperator):
if mat.depletable:
burnable_mats.add(str(mat.id))
if mat.volume is None:
raise RuntimeError("Volume not specified for depletable "
"material with ID={}.".format(mat.id))
if mat.name is None:
msg = ("Volume not specified for depletable material "
f"with ID={mat.id}.")
else:
msg = ("Volume not specified for depletable material "
f"with ID={mat.id} Name={mat.name}.")
raise RuntimeError(msg)
volume[str(mat.id)] = mat.volume
self.heavy_metal += mat.fissionable_mass
@ -242,7 +247,6 @@ class OpenMCOperator(TransportOperator):
for nuc in model_nuclides:
if nuc not in nuclides:
nuclides.append(nuc)
return burnable_mats, volume, nuclides
def _load_previous_results(self):

View file

@ -15,7 +15,7 @@ USE_MULTIPROCESSING = True
NUM_PROCESSES = None
def deplete(func, chain, x, rates, dt, matrix_func=None):
def deplete(func, chain, x, rates, dt, matrix_func=None, *matrix_args):
"""Deplete materials using given reaction rates for a specified time
Parameters
@ -37,6 +37,8 @@ def deplete(func, chain, x, rates, dt, matrix_func=None):
``fission_yields = {parent: {product: yield_frac}}``
Expected to return the depletion matrix required by
``func``
matrix_args : Any, optional
Additional arguments passed to matrix_func
Returns
-------
@ -57,7 +59,8 @@ def deplete(func, chain, x, rates, dt, matrix_func=None):
if matrix_func is None:
matrices = map(chain.form_matrix, rates, fission_yields)
else:
matrices = map(matrix_func, repeat(chain), rates, fission_yields)
matrices = map(matrix_func, repeat(chain), rates, fission_yields,
*matrix_args)
inputs = zip(matrices, x, repeat(dt))

View file

@ -4,7 +4,7 @@ from xml.etree import ElementTree as ET
import openmc.checkvalue as cv
import openmc
from openmc.data import NATURAL_ABUNDANCE, atomic_mass, \
from openmc.data import NATURAL_ABUNDANCE, atomic_mass, zam, \
isotopes as natural_isotopes
@ -147,8 +147,8 @@ class Element(str):
# and sort to avoid different ordering between Python 2 and 3.
mutual_nuclides = natural_nuclides.intersection(library_nuclides)
absent_nuclides = natural_nuclides.difference(mutual_nuclides)
mutual_nuclides = sorted(list(mutual_nuclides))
absent_nuclides = sorted(list(absent_nuclides))
mutual_nuclides = sorted(mutual_nuclides, key=zam)
absent_nuclides = sorted(absent_nuclides, key=zam)
# If all naturally occurring isotopes are present in the library,
# add them based on their abundance
@ -195,7 +195,7 @@ class Element(str):
# If a cross_section library is not present, expand the element into
# its natural nuclides
else:
for nuclide in natural_nuclides:
for nuclide in sorted(natural_nuclides, key=zam):
abundances[nuclide] = NATURAL_ABUNDANCE[nuclide]
# Modify mole fractions if enrichment provided

View file

@ -1,5 +1,6 @@
from collections.abc import Iterable
from numbers import Integral
import os
import subprocess
import openmc
@ -23,7 +24,7 @@ def _process_CLI_arguments(volume=False, geometry_debug=False, particles=None,
Number of particles to simulate per generation.
plot : bool, optional
Run in plotting mode. Defaults to False.
restart_file : str, optional
restart_file : str or PathLike
Path to restart file to use
threads : int, optional
Number of OpenMP threads. If OpenMC is compiled with OpenMP threading
@ -42,7 +43,7 @@ def _process_CLI_arguments(volume=False, geometry_debug=False, particles=None,
mpi_args : list of str, optional
MPI execute command and any additional MPI arguments to pass,
e.g., ['mpiexec', '-n', '8'].
path_input : str or Pathlike
path_input : str or PathLike
Path to a single XML file or a directory containing XML files for the
OpenMC executable to read.
@ -73,8 +74,8 @@ def _process_CLI_arguments(volume=False, geometry_debug=False, particles=None,
if event_based:
args.append('-e')
if isinstance(restart_file, str):
args += ['-r', restart_file]
if isinstance(restart_file, (str, os.PathLike)):
args += ['-r', str(restart_file)]
if tracks:
args.append('-t')
@ -230,7 +231,7 @@ def calculate_volumes(threads=None, output=True, cwd='.',
cwd : str, optional
Path to working directory to run in. Defaults to the current working
directory.
path_input : str or Pathlike
path_input : str or PathLike
Path to a single XML file or a directory containing XML files for the
OpenMC executable to read.
@ -270,7 +271,7 @@ def run(particles=None, threads=None, geometry_debug=False,
:envvar:`OMP_NUM_THREADS` environment variable).
geometry_debug : bool, optional
Turn on geometry debugging during simulation. Defaults to False.
restart_file : str, optional
restart_file : str or PathLike
Path to restart file to use
tracks : bool, optional
Enables the writing of particles tracks. The number of particle tracks
@ -291,7 +292,7 @@ def run(particles=None, threads=None, geometry_debug=False,
.. versionadded:: 0.12
path_input : str or Pathlike
path_input : str or PathLike
Path to a single XML file or a directory containing XML files for the
OpenMC executable to read.

View file

@ -102,6 +102,8 @@ class Filter(IDManagerMixin, metaclass=FilterMeta):
Unique identifier for the filter
num_bins : Integral
The number of filter bins
shape : tuple
The shape of the filter
"""
@ -205,6 +207,10 @@ class Filter(IDManagerMixin, metaclass=FilterMeta):
def num_bins(self):
return len(self.bins)
@property
def shape(self):
return (self.num_bins,)
def check_bins(self, bins):
"""Make sure given bins are valid for this filter.
@ -252,6 +258,8 @@ class Filter(IDManagerMixin, metaclass=FilterMeta):
"""
filter_type = elem.get('type')
if filter_type is None:
filter_type = elem.find('type').text
# If the filter type matches this class's short_name, then
# there is no overridden from_xml_element method
@ -756,7 +764,7 @@ class ParticleFilter(Filter):
class MeshFilter(Filter):
"""Bins tally event locations onto a regular, rectangular mesh.
"""Bins tally event locations by mesh elements.
Parameters
----------
@ -839,6 +847,12 @@ class MeshFilter(Filter):
else:
self.bins = list(mesh.indices)
@property
def shape(self):
if isinstance(self, MeshSurfaceFilter):
return (self.num_bins,)
return self.mesh.dimension
@property
def translation(self):
return self._translation
@ -947,7 +961,7 @@ class MeshFilter(Filter):
class MeshSurfaceFilter(MeshFilter):
"""Filter events by surface crossings on a regular, rectangular mesh.
"""Filter events by surface crossings on a mesh.
Parameters
----------
@ -958,8 +972,6 @@ class MeshSurfaceFilter(MeshFilter):
Attributes
----------
bins : Integral
The mesh ID
mesh : openmc.MeshBase
The mesh object that events will be tallied onto
translation : Iterable of float
@ -968,10 +980,8 @@ class MeshSurfaceFilter(MeshFilter):
id : int
Unique identifier for the filter
bins : list of tuple
A list of mesh indices / surfaces for each filter bin, e.g. [(1, 1,
'x-min out'), (1, 1, 'x-min in'), ...]
num_bins : Integral
The number of filter bins
@ -1332,6 +1342,36 @@ class EnergyFilter(RealFilter):
cv.check_greater_than('filter value', v0, 0., equality=True)
cv.check_greater_than('filter value', v1, 0., equality=True)
def get_tabular(self, values, **kwargs):
"""Create a tabulated distribution based on tally results with an energy filter
This method provides an easy way to create a distribution in energy
(e.g., a source spectrum) based on tally results that were obtained from
using an :class:`~openmc.EnergyFilter`.
Parameters
----------
values : iterable of float
Array of numeric values, typically from a tally result
**kwargs
Keyword arguments passed to :class:`openmc.stats.Tabular`
Returns
-------
openmc.stats.Tabular
Tabular distribution with histogram interpolation
"""
probabilities = np.array(values, dtype=float)
probabilities /= probabilities.sum()
# Determine probability per eV, adding extra 0 at the end since it is a histogram
probability_per_ev = probabilities / np.diff(self.values)
probability_per_ev = np.append(probability_per_ev, 0.0)
kwargs.setdefault('interpolation', 'histogram')
return openmc.stats.Tabular(self.values, probability_per_ev, **kwargs)
@property
def lethargy_bin_width(self):
"""Calculates the base 10 log width of energy bins which is useful when
@ -1703,7 +1743,7 @@ class DistribcellFilter(Filter):
# Concatenate with DataFrame of distribcell instance IDs
if level_df is not None:
level_df = level_df.dropna(axis=1, how='all')
level_df = level_df.astype(np.int)
level_df = level_df.astype(int)
df = pd.concat([level_df, df], axis=1)
return df

View file

@ -278,7 +278,7 @@ class Geometry:
"""
# Using str and os.Pathlike here to avoid error when using just the imported PathLike
# Using str and os.PathLike here to avoid error when using just the imported PathLike
# TypeError: Subscripted generics cannot be used with class and instance checks
check_type('materials', materials, (str, os.PathLike, openmc.Materials))

View file

@ -143,6 +143,8 @@ class Material(IDManagerMixin):
string += '{: <16}=\t{}'.format('\tDensity', self._density)
string += f' [{self._density_units}]\n'
string += '{: <16}=\t{} [cm^3]\n'.format('\tVolume', self._volume)
string += '{: <16}\n'.format('\tS(a,b) Tables')
if self._ncrystal_cfg:

View file

@ -1,5 +1,6 @@
from abc import ABC, abstractmethod
from collections.abc import Iterable
from collections import OrderedDict
from math import pi
from numbers import Real, Integral
from pathlib import Path
@ -188,7 +189,7 @@ class StructuredMesh(MeshBase):
Returns a numpy.ndarray representing the mesh element centroid
coordinates with a shape equal to (ndim, dim1, ..., dimn). Can be
unpacked along the first dimension with xx, yy, zz = mesh.centroids.
"""
ndim = self.n_dimension
@ -1988,3 +1989,25 @@ class UnstructuredMesh(MeshBase):
length_multiplier = float(get_text(elem, 'length_multiplier', 1.0))
return cls(filename, library, mesh_id, '', length_multiplier)
def _read_meshes(elem):
"""Generate dictionary of meshes from a given XML node
Parameters
----------
elem : xml.etree.ElementTree.Element
XML element
Returns
-------
dict
A dictionary with mesh IDs as keys and openmc.MeshBase
instanaces as values
"""
out = dict()
for mesh_elem in elem.findall('mesh'):
mesh = MeshBase.from_xml_element(mesh_elem)
out[mesh.id] = mesh
return out

View file

@ -164,7 +164,7 @@ class EnergyGroups:
if groups == 'all':
return np.arange(self.num_groups)
else:
indices = np.zeros(len(groups), dtype=np.int)
indices = np.zeros(len(groups), dtype=int)
for i, group in enumerate(groups):
cv.check_greater_than('group', group, 0)

View file

@ -586,7 +586,7 @@ class MDGXS(MGXS):
if not isinstance(subdomains, str):
cv.check_iterable_type('subdomains', subdomains, Integral)
elif self.domain_type == 'distribcell':
subdomains = np.arange(self.num_subdomains, dtype=np.int)
subdomains = np.arange(self.num_subdomains, dtype=int)
elif self.domain_type == 'mesh':
xyz = [range(1, x + 1) for x in self.domain.dimension]
subdomains = list(itertools.product(*xyz))
@ -2473,7 +2473,7 @@ class MatrixMDGXS(MDGXS):
if not isinstance(subdomains, str):
cv.check_iterable_type('subdomains', subdomains, Integral)
elif self.domain_type == 'distribcell':
subdomains = np.arange(self.num_subdomains, dtype=np.int)
subdomains = np.arange(self.num_subdomains, dtype=int)
elif self.domain_type == 'mesh':
xyz = [range(1, x + 1) for x in self.domain.dimension]
subdomains = list(itertools.product(*xyz))

View file

@ -918,7 +918,7 @@ class MGXS:
# Sum the atomic number densities for all nuclides
if nuclides == 'sum':
nuclides = self.get_nuclides()
densities = np.zeros(1, dtype=np.float)
densities = np.zeros(1, dtype=float)
for nuclide in nuclides:
densities[0] += self.get_nuclide_density(nuclide)
@ -931,7 +931,7 @@ class MGXS:
# Tabulate the atomic number densities for each specified nuclide
else:
densities = np.zeros(len(nuclides), dtype=np.float)
densities = np.zeros(len(nuclides), dtype=float)
for i, nuclide in enumerate(nuclides):
densities[i] = self.get_nuclide_density(nuclide)
@ -1720,7 +1720,7 @@ class MGXS:
if not isinstance(subdomains, str):
cv.check_iterable_type('subdomains', subdomains, Integral)
elif self.domain_type == 'distribcell':
subdomains = np.arange(self.num_subdomains, dtype=np.int)
subdomains = np.arange(self.num_subdomains, dtype=int)
elif self.domain_type == 'mesh':
subdomains = list(self.domain.indices)
else:
@ -1887,7 +1887,7 @@ class MGXS:
if not isinstance(subdomains, str):
cv.check_iterable_type('subdomains', subdomains, Integral)
elif self.domain_type == 'distribcell':
subdomains = np.arange(self.num_subdomains, dtype=np.int)
subdomains = np.arange(self.num_subdomains, dtype=int)
elif self.domain_type == 'sum(distribcell)':
domain_filter = self.xs_tally.find_filter('sum(distribcell)')
subdomains = domain_filter.bins
@ -1900,7 +1900,7 @@ class MGXS:
if self.by_nuclide:
if nuclides == 'all':
nuclides = self.get_nuclides()
densities = np.zeros(len(nuclides), dtype=np.float)
densities = np.zeros(len(nuclides), dtype=float)
elif nuclides == 'sum':
nuclides = ['sum']
else:
@ -2447,7 +2447,7 @@ class MatrixMGXS(MGXS):
if not isinstance(subdomains, str):
cv.check_iterable_type('subdomains', subdomains, Integral)
elif self.domain_type == 'distribcell':
subdomains = np.arange(self.num_subdomains, dtype=np.int)
subdomains = np.arange(self.num_subdomains, dtype=int)
elif self.domain_type == 'mesh':
subdomains = list(self.domain.indices)
else:
@ -4785,7 +4785,7 @@ class ScatterMatrixXS(MatrixMGXS):
if not isinstance(subdomains, str):
cv.check_iterable_type('subdomains', subdomains, Integral)
elif self.domain_type == 'distribcell':
subdomains = np.arange(self.num_subdomains, dtype=np.int)
subdomains = np.arange(self.num_subdomains, dtype=int)
elif self.domain_type == 'mesh':
subdomains = list(self.domain.indices)
else:

View file

@ -124,10 +124,10 @@ class Model:
@lru_cache(maxsize=None)
def _materials_by_id(self):
"""Dictionary mapping material ID --> material"""
if self.materials is None:
mats = self.geometry.get_all_materials().values()
else:
if self.materials:
mats = self.materials
else:
mats = self.geometry.get_all_materials().values()
return {mat.id: mat for mat in mats}
@property
@ -248,7 +248,7 @@ class Model:
Parameters
----------
path : str or Pathlike
path : str or PathLike
Path to model.xml file
"""
tree = ET.parse(path)
@ -473,7 +473,7 @@ class Model:
Parameters
----------
path : str or Pathlike
path : str or PathLike
Location of the XML file to write (default is 'model.xml'). Can be a
directory or file path.
remove_surfs : bool
@ -620,7 +620,7 @@ class Model:
value set by the :envvar:`OMP_NUM_THREADS` environment variable).
geometry_debug : bool, optional
Turn on geometry debugging during simulation. Defaults to False.
restart_file : str, optional
restart_file : str or PathLike
Path to restart file to use
tracks : bool, optional
Enables the writing of particles tracks. The number of particle

View file

@ -11,10 +11,12 @@ from typing import Optional
from xml.etree import ElementTree as ET
import openmc.checkvalue as cv
from openmc.stats.multivariate import MeshSpatial
from . import RegularMesh, Source, VolumeCalculation, WeightWindows
from ._xml import clean_indentation, get_text, reorder_attributes
from openmc.checkvalue import PathLike
from .mesh import _read_meshes
class RunMode(Enum):
@ -990,6 +992,10 @@ class Settings:
def _create_source_subelement(self, root):
for source in self.source:
root.append(source.to_xml_element())
if isinstance(source.space, MeshSpatial):
path = f"./mesh[@id='{source.space.mesh.id}']"
if root.find(path) is None:
root.append(source.space.mesh.to_xml_element())
def _create_volume_calcs_subelement(self, root):
for calc in self.volume_calculations:
@ -1340,9 +1346,11 @@ class Settings:
threshold = float(get_text(elem, 'threshold'))
self.keff_trigger = {'type': trigger, 'threshold': threshold}
def _source_from_xml_element(self, root):
def _source_from_xml_element(self, root, meshes=None):
for elem in root.findall('source'):
self.source.append(Source.from_xml_element(elem))
src = Source.from_xml_element(elem, meshes)
# add newly constructed source object to the list
self.source.append(src)
def _volume_calcs_from_xml_element(self, root):
volume_elems = root.findall("volume_calc")
@ -1721,7 +1729,7 @@ class Settings:
settings._rel_max_lost_particles_from_xml_element(elem)
settings._generations_per_batch_from_xml_element(elem)
settings._keff_trigger_from_xml_element(elem)
settings._source_from_xml_element(elem)
settings._source_from_xml_element(elem, meshes)
settings._volume_calcs_from_xml_element(elem)
settings._output_from_xml_element(elem)
settings._statepoint_from_xml_element(elem)
@ -1781,4 +1789,5 @@ class Settings:
"""
tree = ET.parse(path)
root = tree.getroot()
return cls.from_xml_element(root)
meshes = _read_meshes(root)
return cls.from_xml_element(root, meshes)

View file

@ -258,13 +258,16 @@ class Source:
return element
@classmethod
def from_xml_element(cls, elem: ET.Element) -> 'openmc.Source':
def from_xml_element(cls, elem: ET.Element, meshes=None) -> 'openmc.Source':
"""Generate source from an XML element
Parameters
----------
elem : xml.etree.ElementTree.Element
XML element
meshes : dict
Dictionary with mesh IDs as keys and openmc.MeshBase instaces as
values
Returns
-------
@ -313,7 +316,7 @@ class Source:
space = elem.find('space')
if space is not None:
source.space = Spatial.from_xml_element(space)
source.space = Spatial.from_xml_element(space, meshes)
angle = elem.find('angle')
if angle is not None:

View file

@ -9,6 +9,7 @@ import numpy as np
import openmc.checkvalue as cv
from .._xml import get_text
from .univariate import Univariate, Uniform, PowerLaw
from ..mesh import MeshBase
class UnitSphere(ABC):
@ -261,7 +262,7 @@ class Spatial(ABC):
@classmethod
@abstractmethod
def from_xml_element(cls, elem):
def from_xml_element(cls, elem, meshes=None):
distribution = get_text(elem, 'type')
if distribution == 'cartesian':
return CartesianIndependent.from_xml_element(elem)
@ -273,6 +274,8 @@ class Spatial(ABC):
return Box.from_xml_element(elem)
elif distribution == 'point':
return Point.from_xml_element(elem)
elif distribution == 'mesh':
return MeshSpatial.from_xml_element(elem, meshes)
class CartesianIndependent(Spatial):
@ -617,6 +620,133 @@ class CylindricalIndependent(Spatial):
return cls(r, phi, z, origin=origin)
class MeshSpatial(Spatial):
"""Spatial distribution for a mesh.
This distribution specifies a mesh to sample over with source strengths
specified for each mesh element.
.. versionadded:: 0.13.3
Parameters
----------
mesh : openmc.MeshBase
The mesh instance used for sampling
strengths : iterable of float, optional
An iterable of values that represents the weights of each element. If no
source strengths are specified, they will be equal for all mesh
elements.
volume_normalized : bool, optional
Whether or not the strengths will be multiplied by element volumes at
runtime. Default is True.
Attributes
----------
mesh : openmc.MeshBase
The mesh instance used for sampling
strengths : numpy.ndarray or None
An array of source strengths for each mesh element
volume_normalized : bool
Whether or not the strengths will be multiplied by element volumes at
runtime.
"""
def __init__(self, mesh, strengths=None, volume_normalized=True):
self.mesh = mesh
self.strengths = strengths
self.volume_normalized = volume_normalized
@property
def mesh(self):
return self._mesh
@mesh.setter
def mesh(self, mesh):
if mesh is not None:
cv.check_type('mesh instance', mesh, MeshBase)
self._mesh = mesh
@property
def volume_normalized(self):
return self._volume_normalized
@volume_normalized.setter
def volume_normalized(self, volume_normalized):
cv.check_type('Multiply strengths by element volumes', volume_normalized, bool)
self._volume_normalized = volume_normalized
@property
def strengths(self):
return self._strengths
@strengths.setter
def strengths(self, given_strengths):
if given_strengths is not None:
cv.check_type('strengths array passed in', given_strengths, Iterable, Real)
self._strengths = np.asarray(given_strengths, dtype=float).flatten()
else:
self._strengths = None
@property
def num_strength_bins(self):
if self.strengths is None:
raise ValueError('Strengths are not set')
return self.strengths.size
def to_xml_element(self):
"""Return XML representation of the spatial distribution
Returns
-------
element : xml.etree.ElementTree.Element
XML element containing spatial distribution data
"""
element = ET.Element('space')
element.set('type', 'mesh')
element.set("mesh_id", str(self.mesh.id))
element.set("volume_normalized", str(self.volume_normalized))
if self.strengths is not None:
subelement = ET.SubElement(element, 'strengths')
subelement.text = ' '.join(str(e) for e in self.strengths)
return element
@classmethod
def from_xml_element(cls, elem, meshes):
"""Generate spatial distribution from an XML element
Parameters
----------
elem : xml.etree.ElementTree.Element
XML element
meshes : dict
A dictionary with mesh IDs as keys and openmc.MeshBase instances as
values
Returns
-------
openmc.stats.MeshSpatial
Spatial distribution generated from XML element
"""
mesh_id = int(elem.get('mesh_id'))
# check if this mesh has been read in from another location already
if mesh_id not in meshes:
raise RuntimeError(f'Could not locate mesh with ID "{mesh_id}"')
volume_normalized = elem.get("volume_normalized")
volume_normalized = get_text(elem, 'volume_normalized').lower() == 'true'
strengths = get_text(elem, 'strengths')
if strengths is not None:
strengths = [float(b) for b in get_text(elem, 'strengths').split()]
return cls(meshes[mesh_id], strengths, volume_normalized)
class Box(Spatial):
"""Uniform distribution of coordinates in a rectangular cuboid.

View file

@ -857,7 +857,6 @@ class Tabular(Univariate):
'or linear-linear interpolation.')
if self.interpolation == 'linear-linear':
mean = 0.0
self.normalize()
for i in range(1, len(self.x)):
y_min = self.p[i-1]
y_max = self.p[i]
@ -872,9 +871,13 @@ class Tabular(Univariate):
mean += exp_val
elif self.interpolation == 'histogram':
mean = 0.5 * (self.x[:-1] + self.x[1:])
mean *= np.diff(self.cdf())
mean = sum(mean)
x_l = self.x[:-1]
x_r = self.x[1:]
p_l = self.p[:-1]
mean = (0.5 * (x_l + x_r) * (x_r - x_l) * p_l).sum()
# Normalize for when integral of distribution is not 1
mean /= self.integral()
return mean

View file

@ -1405,7 +1405,7 @@ class Tally(IDManagerMixin):
return df
def get_reshaped_data(self, value='mean'):
def get_reshaped_data(self, value='mean', expand_dims=False):
"""Returns an array of tally data with one dimension per filter.
The tally data in OpenMC is stored as a 3D array with the dimensions
@ -1417,17 +1417,24 @@ class Tally(IDManagerMixin):
This builds and returns a reshaped version of the tally data array with
unique dimensions corresponding to each tally filter. For example,
suppose this tally has arrays of data with shape (8,5,5) corresponding
to two filters (2 and 4 bins, respectively), five nuclides and five
suppose this tally has arrays of data with shape (30,5,5) corresponding
to two filters (2 and 15 bins, respectively), five nuclides and five
scores. This method will return a version of the data array with the
with a new shape of (2,4,5,5) such that the first two dimensions
correspond directly to the two filters with two and four bins.
with a new shape of (2,15,5,5) such that the first two dimensions
correspond directly to the two filters with two and fifteen bins. If
expand_dims is True and our filter above with 15 bins is an instance of
:class:`openmc.MeshFilter` with a shape of (3,5,1). The resulting tally
data array will have a new shape of (2,3,5,1,5,5).
Parameters
----------
value : str
A string for the type of value to return - 'mean' (default),
'std_dev', 'rel_err', 'sum', or 'sum_sq' are accepted
expand_dims : bool, optional
Whether or not to expand the dimensions of filters with multiple
dimensions. This will result in more than one dimension per filter
for the returned data array.
Returns
-------
@ -1439,12 +1446,32 @@ class Tally(IDManagerMixin):
# Get the 3D array of data in filters, nuclides and scores
data = self.get_values(value=value)
# Build a new array shape with one dimension per filter
new_shape = tuple(f.num_bins for f in self.filters)
# Build a new array shape with one dimension per filter or expand
# multidimensional filters if desired
new_shape = tuple()
idx0 = None
for i, f in enumerate(self.filters):
if expand_dims:
# Mesh filter indices are backwards so we need to flip them
if isinstance(f, openmc.MeshFilter):
fshape = f.shape[::-1]
new_shape += fshape
idx0, idx1 = i, i + len(fshape) - 1
else:
new_shape += f.shape
else:
new_shape += (np.prod(f.shape),)
new_shape += (self.num_nuclides, self.num_scores)
# Reshape the data with one dimension for each filter
data = np.reshape(data, new_shape)
# If we had a MeshFilter we should swap the axes to have the same shape
# for the data and the filter
if idx0 is not None:
data = np.swapaxes(data, idx0, idx1)
return data
def hybrid_product(self, other, binary_op, filter_product=None,

View file

@ -91,6 +91,23 @@ class UniverseBase(ABC, IDManagerMixin):
else:
raise ValueError('No volume information found for this universe.')
def get_all_universes(self):
"""Return all universes that are contained within this one.
Returns
-------
universes : collections.OrderedDict
Dictionary whose keys are universe IDs and values are
:class:`Universe` instances
"""
# Append all Universes within each Cell to the dictionary
universes = OrderedDict()
for cell in self.get_all_cells().values():
universes.update(cell.get_all_universes())
return universes
@abstractmethod
def create_xml_subelement(self, xml_element, memo=None):
"""Add the universe xml representation to an incoming xml element
@ -111,6 +128,13 @@ class UniverseBase(ABC, IDManagerMixin):
"""
@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.
@ -138,8 +162,7 @@ class UniverseBase(ABC, IDManagerMixin):
# If no memoize'd clone exists, instantiate one
if self not in memo:
clone = deepcopy(self)
clone.id = None
clone = self._partial_deepcopy()
# Clone all cells for the universe clone
clone._cells = OrderedDict()
@ -532,23 +555,6 @@ class Universe(UniverseBase):
return materials
def get_all_universes(self):
"""Return all universes that are contained within this one.
Returns
-------
universes : collections.OrderedDict
Dictionary whose keys are universe IDs and values are
:class:`Universe` instances
"""
# Append all Universes within each Cell to the dictionary
universes = OrderedDict()
for cell in self.get_all_cells().values():
universes.update(cell.get_all_universes())
return universes
def create_xml_subelement(self, xml_element, memo=None):
# Iterate over all Cells
for cell in self._cells.values():
@ -611,6 +617,15 @@ class Universe(UniverseBase):
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.
@ -947,3 +962,14 @@ class DAGMCUniverse(UniverseBase):
out.auto_mat_ids = bool(elem.get('auto_mat_ids'))
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