2025-01-24 16:49:58 -06:00
|
|
|
from __future__ import annotations
|
2018-02-02 10:06:45 -06:00
|
|
|
from collections.abc import Iterable, MutableSequence
|
2015-04-16 14:22:03 -05:00
|
|
|
import copy
|
2025-02-24 16:11:18 -06:00
|
|
|
from functools import partial, reduce, wraps
|
2017-10-04 09:53:47 -05:00
|
|
|
from itertools import product
|
2025-11-12 11:41:37 -05:00
|
|
|
from math import sqrt, log
|
2015-08-06 23:12:22 -07:00
|
|
|
from numbers import Integral, Real
|
2017-12-19 21:54:08 +07:00
|
|
|
import operator
|
2019-03-12 09:58:35 -05:00
|
|
|
from pathlib import Path
|
2023-05-09 11:41:04 -04:00
|
|
|
import lxml.etree as ET
|
2015-05-25 09:17:36 -04:00
|
|
|
|
2020-04-06 15:16:09 -05:00
|
|
|
import h5py
|
2015-05-21 08:40:30 -04:00
|
|
|
import numpy as np
|
2017-06-02 06:41:20 -05:00
|
|
|
import pandas as pd
|
2025-11-12 11:41:37 -05:00
|
|
|
from scipy.stats import chi2, norm
|
2015-04-16 13:13:54 -05:00
|
|
|
|
2016-09-30 12:19:22 -04:00
|
|
|
import openmc
|
2015-09-11 00:26:11 -04:00
|
|
|
import openmc.checkvalue as cv
|
2026-04-28 17:10:03 -04:00
|
|
|
from openmc.filter import (
|
|
|
|
|
Filter,
|
|
|
|
|
DistribcellFilter,
|
|
|
|
|
EnergyFunctionFilter,
|
|
|
|
|
DelayedGroupFilter,
|
|
|
|
|
FilterMeta,
|
|
|
|
|
MeshFilter,
|
|
|
|
|
MeshBornFilter,
|
|
|
|
|
)
|
|
|
|
|
from openmc.arithmetic import (
|
|
|
|
|
CrossFilter,
|
|
|
|
|
AggregateFilter,
|
|
|
|
|
CrossScore,
|
|
|
|
|
AggregateScore,
|
|
|
|
|
CrossNuclide,
|
|
|
|
|
AggregateNuclide,
|
|
|
|
|
)
|
2026-01-02 06:43:25 -05:00
|
|
|
from ._sparse_compat import lil_array
|
2025-08-08 20:47:55 +03:00
|
|
|
from ._xml import clean_indentation, get_elem_list, get_text
|
2017-06-02 13:25:34 -05:00
|
|
|
from .mixin import IDManagerMixin
|
2022-01-20 17:51:16 -06:00
|
|
|
from .mesh import MeshBase
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2016-02-02 10:07:08 -06:00
|
|
|
|
2015-12-13 13:47:27 -05:00
|
|
|
# The tally arithmetic product types. The tensor product performs the full
|
|
|
|
|
# cross product of the data in two tallies with respect to a specified axis
|
|
|
|
|
# (filters, nuclides, or scores). The entrywise product performs the arithmetic
|
|
|
|
|
# operation entrywise across the entries in two tallies with respect to a
|
|
|
|
|
# specified axis.
|
2015-12-10 00:53:01 -05:00
|
|
|
_PRODUCT_TYPES = ['tensor', 'entrywise']
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2016-03-08 21:54:07 -06:00
|
|
|
# The following indicate acceptable types when setting Tally.scores,
|
|
|
|
|
# Tally.nuclides, and Tally.filters
|
2026-04-28 17:10:03 -04:00
|
|
|
_SCORE_CLASSES = (str, CrossScore, AggregateScore)
|
|
|
|
|
_NUCLIDE_CLASSES = (str, CrossNuclide, AggregateNuclide)
|
|
|
|
|
_FILTER_CLASSES = (Filter, CrossFilter, AggregateFilter)
|
2016-03-08 21:54:07 -06:00
|
|
|
|
2016-08-15 06:47:19 -05:00
|
|
|
# Valid types of estimators
|
2025-01-24 16:49:58 -06:00
|
|
|
ESTIMATOR_TYPES = {'tracklength', 'collision', 'analog'}
|
2016-08-15 06:47:19 -05:00
|
|
|
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2017-06-02 13:25:34 -05:00
|
|
|
class Tally(IDManagerMixin):
|
2015-06-13 17:21:41 +07:00
|
|
|
"""A tally defined by a set of scores that are accumulated for a list of
|
|
|
|
|
nuclides given a set of filters.
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
2016-04-13 22:17:16 -05:00
|
|
|
tally_id : int, optional
|
2015-06-26 20:37:44 +07:00
|
|
|
Unique identifier for the tally. If none is specified, an identifier
|
|
|
|
|
will automatically be assigned
|
|
|
|
|
name : str, optional
|
|
|
|
|
Name of the tally. If not specified, the name is the empty string.
|
2026-02-06 17:23:11 +01:00
|
|
|
scores : list of str, optional
|
|
|
|
|
List of scores, e.g. ['flux', 'fission']
|
|
|
|
|
filters : list of openmc.Filter, optional
|
|
|
|
|
List of filters for the tally
|
|
|
|
|
nuclides : list of str, optional
|
|
|
|
|
List of nuclides to score results for
|
|
|
|
|
estimator : {'analog', 'tracklength', 'collision'}, optional
|
|
|
|
|
Type of estimator for the tally
|
|
|
|
|
triggers : list of openmc.Trigger, optional
|
|
|
|
|
List of tally triggers
|
|
|
|
|
derivative : openmc.TallyDerivative, optional
|
|
|
|
|
A material perturbation derivative to apply to all scores in the tally
|
2015-06-13 17:21:41 +07:00
|
|
|
|
|
|
|
|
Attributes
|
|
|
|
|
----------
|
2016-04-13 22:17:16 -05:00
|
|
|
id : int
|
2015-06-13 17:21:41 +07:00
|
|
|
Unique identifier for the tally
|
|
|
|
|
name : str
|
|
|
|
|
Name of the tally
|
2023-06-09 07:24:00 -05:00
|
|
|
multiply_density : bool
|
|
|
|
|
Whether reaction rates should be multiplied by atom density
|
|
|
|
|
|
2023-10-31 16:55:19 -05:00
|
|
|
.. versionadded:: 0.14.0
|
2016-04-13 22:17:16 -05:00
|
|
|
filters : list of openmc.Filter
|
2015-06-13 17:21:41 +07:00
|
|
|
List of specified filters for the tally
|
2023-05-17 20:45:04 +01:00
|
|
|
nuclides : list of str
|
2015-06-13 17:21:41 +07:00
|
|
|
List of nuclides to score results for
|
|
|
|
|
scores : list of str
|
|
|
|
|
List of defined scores, e.g. 'flux', 'fission', etc.
|
2015-09-16 18:04:44 -04:00
|
|
|
estimator : {'analog', 'tracklength', 'collision'}
|
2025-01-24 16:49:58 -06:00
|
|
|
Type of estimator for the tally. If unset (None), OpenMC will automatically
|
|
|
|
|
select an appropriate estimator based on the tally filters and scores
|
|
|
|
|
with a preference for 'tracklength'.
|
2016-04-13 22:17:16 -05:00
|
|
|
triggers : list of openmc.Trigger
|
2015-06-13 17:21:41 +07:00
|
|
|
List of tally triggers
|
2016-04-13 22:17:16 -05:00
|
|
|
num_scores : int
|
2018-04-27 19:45:38 -04:00
|
|
|
Total number of scores
|
2016-04-13 22:17:16 -05:00
|
|
|
num_filter_bins : int
|
2015-06-13 17:21:41 +07:00
|
|
|
Total number of filter bins accounting for all filters
|
2016-04-13 22:17:16 -05:00
|
|
|
num_bins : int
|
2015-06-13 17:21:41 +07:00
|
|
|
Total number of bins for the tally
|
2016-04-13 22:17:16 -05:00
|
|
|
shape : 3-tuple of int
|
2016-02-02 10:07:08 -06:00
|
|
|
The shape of the tally data array ordered as the number of filter bins,
|
2015-12-14 09:32:10 -05:00
|
|
|
nuclide bins and score bins
|
2017-12-19 15:58:14 +07:00
|
|
|
filter_strides : list of int
|
|
|
|
|
Stride in memory for each filter
|
2016-04-13 22:17:16 -05:00
|
|
|
num_realizations : int
|
2015-06-13 17:21:41 +07:00
|
|
|
Total number of realizations
|
|
|
|
|
with_summary : bool
|
|
|
|
|
Whether or not a Summary has been linked
|
2016-04-13 22:17:16 -05:00
|
|
|
sum : numpy.ndarray
|
2015-06-13 17:21:41 +07:00
|
|
|
An array containing the sum of each independent realization for each bin
|
2016-04-13 22:17:16 -05:00
|
|
|
sum_sq : numpy.ndarray
|
2015-06-13 17:21:41 +07:00
|
|
|
An array containing the sum of each independent realization squared for
|
|
|
|
|
each bin
|
2025-11-12 11:41:37 -05:00
|
|
|
sum_third : numpy.ndarray
|
|
|
|
|
An array containing the sum of each independent realization to the third power for
|
|
|
|
|
each bin
|
|
|
|
|
sum_fourth : numpy.ndarray
|
|
|
|
|
An array containing the sum of each independent realization to the fourth power for
|
|
|
|
|
each bin
|
2016-04-13 22:17:16 -05:00
|
|
|
mean : numpy.ndarray
|
2015-06-13 17:21:41 +07:00
|
|
|
An array containing the sample mean for each bin
|
2016-04-13 22:17:16 -05:00
|
|
|
std_dev : numpy.ndarray
|
2015-09-12 21:02:13 -04:00
|
|
|
An array containing the sample standard deviation for each bin
|
2025-11-12 11:41:37 -05:00
|
|
|
vov : numpy.ndarray
|
|
|
|
|
An array containing the variance of the variance for each tally bin
|
|
|
|
|
higher_moments : bool
|
|
|
|
|
Whether or not the tally accumulates the sums third and fourth to compute higher-order moments
|
2025-05-03 08:19:10 -04:00
|
|
|
figure_of_merit : numpy.ndarray
|
|
|
|
|
An array containing the figure of merit for each bin
|
|
|
|
|
|
|
|
|
|
.. versionadded:: 0.15.3
|
2015-12-13 01:30:10 -05:00
|
|
|
derived : bool
|
|
|
|
|
Whether or not the tally is derived from one or more other tallies
|
|
|
|
|
sparse : bool
|
|
|
|
|
Whether or not the tally uses SciPy's LIL sparse matrix format for
|
|
|
|
|
compressed data storage
|
2016-11-13 13:49:30 -05:00
|
|
|
derivative : openmc.TallyDerivative
|
2016-01-29 16:54:07 -05:00
|
|
|
A material perturbation derivative to apply to all scores in the tally.
|
2015-09-13 00:31:50 -04:00
|
|
|
|
2015-06-13 17:21:41 +07:00
|
|
|
"""
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2017-06-02 13:25:34 -05:00
|
|
|
next_id = 1
|
|
|
|
|
used_ids = set()
|
|
|
|
|
|
2026-02-06 17:23:11 +01:00
|
|
|
def __init__(self, tally_id=None, name='', scores=None, filters=None,
|
|
|
|
|
nuclides=None, estimator=None, triggers=None,
|
|
|
|
|
derivative=None):
|
2014-10-23 22:31:55 -04:00
|
|
|
# Initialize Tally class attributes
|
2015-04-21 11:49:50 -05:00
|
|
|
self.id = tally_id
|
2015-04-27 08:20:30 -04:00
|
|
|
self.name = name
|
2016-03-08 21:54:07 -06:00
|
|
|
self._filters = cv.CheckedList(_FILTER_CLASSES, 'tally filters')
|
|
|
|
|
self._nuclides = cv.CheckedList(_NUCLIDE_CLASSES, 'tally nuclides')
|
|
|
|
|
self._scores = cv.CheckedList(_SCORE_CLASSES, 'tally scores')
|
2014-10-23 22:31:55 -04:00
|
|
|
self._estimator = None
|
2016-09-30 12:19:22 -04:00
|
|
|
self._triggers = cv.CheckedList(openmc.Trigger, 'tally triggers')
|
2016-01-29 16:54:07 -05:00
|
|
|
self._derivative = None
|
2023-06-09 07:24:00 -05:00
|
|
|
self._multiply_density = True
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2014-10-23 22:31:55 -04:00
|
|
|
self._num_realizations = 0
|
2015-05-29 07:57:19 -07:00
|
|
|
self._with_summary = False
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2014-10-23 22:31:55 -04:00
|
|
|
self._sum = None
|
|
|
|
|
self._sum_sq = None
|
2025-11-12 11:41:37 -05:00
|
|
|
self._sum_third = None
|
|
|
|
|
self._sum_fourth = None
|
2014-10-23 22:31:55 -04:00
|
|
|
self._mean = None
|
|
|
|
|
self._std_dev = None
|
2025-11-12 11:41:37 -05:00
|
|
|
self._vov = None
|
|
|
|
|
self._higher_moments = False
|
2025-05-03 08:19:10 -04:00
|
|
|
self._simulation_time = None
|
2015-05-31 13:41:13 -07:00
|
|
|
self._with_batch_statistics = False
|
2015-08-06 23:41:38 -07:00
|
|
|
self._derived = False
|
2015-12-12 23:44:15 -05:00
|
|
|
self._sparse = False
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2015-10-19 09:19:40 -04:00
|
|
|
self._sp_filename = None
|
2015-09-16 16:19:12 +07:00
|
|
|
self._results_read = False
|
|
|
|
|
|
2026-02-06 17:23:11 +01:00
|
|
|
if filters is not None:
|
|
|
|
|
self.filters = filters
|
|
|
|
|
if nuclides is not None:
|
|
|
|
|
self.nuclides = nuclides
|
|
|
|
|
if scores is not None:
|
|
|
|
|
self.scores = scores
|
|
|
|
|
if estimator is not None:
|
|
|
|
|
self.estimator = estimator
|
|
|
|
|
if triggers is not None:
|
|
|
|
|
self.triggers = triggers
|
|
|
|
|
if derivative is not None:
|
|
|
|
|
self.derivative = derivative
|
|
|
|
|
|
2025-01-24 16:49:58 -06:00
|
|
|
def __eq__(self, other):
|
|
|
|
|
if other.id != self.id:
|
|
|
|
|
return False
|
|
|
|
|
if other.name != self.name:
|
|
|
|
|
return False
|
|
|
|
|
# estimators are automatically set based on the tally filters and scores
|
|
|
|
|
# during OpenMC initialization if this value is None, so it is not
|
|
|
|
|
# considered a requirement for equivalence if it is unset on either
|
|
|
|
|
# tally as it implies that the user is allowing OpenMC to select an
|
|
|
|
|
# appropriate estimator. If the value is explicitly set on both tallies,
|
|
|
|
|
# then the values must match for the tallies to be considered equivalent.
|
|
|
|
|
if self.estimator is not None and other.estimator is not None and other.estimator != self.estimator:
|
|
|
|
|
return False
|
|
|
|
|
if other.filters != self.filters:
|
|
|
|
|
return False
|
|
|
|
|
# for tallies are loaded from statpoint files
|
|
|
|
|
# an empty nuclide list is equivalent to a list with 'total'
|
|
|
|
|
other_nuclides = other.nuclides.copy()
|
|
|
|
|
self_nuclides = self.nuclides.copy()
|
|
|
|
|
if 'total' in other_nuclides:
|
|
|
|
|
other_nuclides.remove('total')
|
|
|
|
|
if 'total' in self_nuclides:
|
|
|
|
|
self_nuclides.remove('total')
|
|
|
|
|
if other_nuclides != self_nuclides:
|
|
|
|
|
return False
|
|
|
|
|
for attr in {'scores', 'triggers', 'derivative', 'multiply_density'}:
|
|
|
|
|
if getattr(other, attr) != getattr(self, attr):
|
|
|
|
|
return False
|
|
|
|
|
return True
|
|
|
|
|
|
2015-10-08 14:23:19 -04:00
|
|
|
def __repr__(self):
|
2020-03-19 15:21:15 -05:00
|
|
|
parts = ['Tally']
|
2020-03-25 07:10:35 -05:00
|
|
|
parts.append('{: <15}=\t{}'.format('ID', self.id))
|
|
|
|
|
parts.append('{: <15}=\t{}'.format('Name', self.name))
|
2016-01-29 16:54:07 -05:00
|
|
|
if self.derivative is not None:
|
2020-03-25 07:10:35 -05:00
|
|
|
parts.append('{: <15}=\t{}'.format('Derivative ID', self.derivative.id))
|
2016-12-04 16:42:11 -05:00
|
|
|
filters = ', '.join(type(f).__name__ for f in self.filters)
|
2020-03-25 07:10:35 -05:00
|
|
|
parts.append('{: <15}=\t{}'.format('Filters', filters))
|
2020-03-19 15:21:15 -05:00
|
|
|
nuclides = ' '.join(str(nuclide) for nuclide in self.nuclides)
|
2020-03-25 07:10:35 -05:00
|
|
|
parts.append('{: <15}=\t{}'.format('Nuclides', nuclides))
|
|
|
|
|
parts.append('{: <15}=\t{}'.format('Scores', self.scores))
|
|
|
|
|
parts.append('{: <15}=\t{}'.format('Estimator', self.estimator))
|
2023-06-09 07:24:00 -05:00
|
|
|
parts.append('{: <15}=\t{}'.format('Multiply dens.', self.multiply_density))
|
2020-03-25 07:10:35 -05:00
|
|
|
return '\n\t'.join(parts)
|
2015-10-08 14:23:19 -04:00
|
|
|
|
2025-02-24 16:11:18 -06:00
|
|
|
@staticmethod
|
|
|
|
|
def ensure_results(f):
|
|
|
|
|
"""A decorator to be applied to any method that might use tally results.
|
|
|
|
|
Results will be loaded if appropriate based on the tally properties.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
f function: Tally method to wrap
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
function: Wrapped function that reads tally results before calling
|
|
|
|
|
the methodif necessary
|
|
|
|
|
"""
|
|
|
|
|
@wraps(f)
|
|
|
|
|
def read(self):
|
|
|
|
|
if self._sp_filename is not None and not self.derived:
|
|
|
|
|
self._read_results()
|
|
|
|
|
return f(self)
|
|
|
|
|
return read
|
|
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
@property
|
|
|
|
|
def name(self):
|
|
|
|
|
return self._name
|
2015-05-31 14:22:42 -07:00
|
|
|
|
2023-06-17 04:34:02 +01:00
|
|
|
@name.setter
|
|
|
|
|
def name(self, name):
|
|
|
|
|
cv.check_type('tally name', name, str, none_ok=True)
|
|
|
|
|
self._name = name
|
|
|
|
|
|
2023-06-09 07:24:00 -05:00
|
|
|
@property
|
|
|
|
|
def multiply_density(self):
|
|
|
|
|
return self._multiply_density
|
|
|
|
|
|
2023-06-17 04:34:02 +01:00
|
|
|
@multiply_density.setter
|
|
|
|
|
def multiply_density(self, value):
|
|
|
|
|
cv.check_type('multiply density', value, bool)
|
|
|
|
|
self._multiply_density = value
|
|
|
|
|
|
2025-11-12 11:41:37 -05:00
|
|
|
@property
|
|
|
|
|
def higher_moments(self) -> bool:
|
|
|
|
|
return self._higher_moments
|
|
|
|
|
|
|
|
|
|
@higher_moments.setter
|
|
|
|
|
def higher_moments(self, value):
|
|
|
|
|
cv.check_type("higher_moments", value, bool)
|
|
|
|
|
self._higher_moments = value
|
|
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
@property
|
|
|
|
|
def filters(self):
|
|
|
|
|
return self._filters
|
2015-05-31 13:26:10 -07:00
|
|
|
|
2023-06-17 04:34:02 +01:00
|
|
|
@filters.setter
|
|
|
|
|
def filters(self, filters):
|
|
|
|
|
cv.check_type('tally filters', filters, MutableSequence)
|
|
|
|
|
|
|
|
|
|
# If the filter is already in the Tally, raise an error
|
|
|
|
|
visited_filters = set()
|
|
|
|
|
for f in filters:
|
|
|
|
|
if f in visited_filters:
|
|
|
|
|
msg = (f'Unable to add a duplicate filter "{f}" to Tally '
|
|
|
|
|
f'ID="{self.id}" since duplicate filters are not '
|
|
|
|
|
'supported in the OpenMC Python API')
|
|
|
|
|
raise ValueError(msg)
|
|
|
|
|
visited_filters.add(f)
|
|
|
|
|
|
|
|
|
|
self._filters = cv.CheckedList(_FILTER_CLASSES, 'tally filters', filters)
|
|
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
@property
|
2025-02-24 16:11:18 -06:00
|
|
|
@ensure_results
|
2015-07-29 00:06:07 -07:00
|
|
|
def nuclides(self):
|
|
|
|
|
return self._nuclides
|
2015-05-31 16:30:15 -07:00
|
|
|
|
2023-06-17 04:34:02 +01:00
|
|
|
@nuclides.setter
|
|
|
|
|
def nuclides(self, nuclides):
|
|
|
|
|
cv.check_type('tally nuclides', nuclides, MutableSequence)
|
|
|
|
|
|
|
|
|
|
# If the nuclide is already in the Tally, raise an error
|
|
|
|
|
visited_nuclides = set()
|
|
|
|
|
for nuc in nuclides:
|
|
|
|
|
if nuc in visited_nuclides:
|
|
|
|
|
msg = (f'Unable to add a duplicate nuclide "{nuc}" to Tally ID='
|
|
|
|
|
f'"{self.id}" since duplicate nuclides are not supported '
|
|
|
|
|
'in the OpenMC Python API')
|
|
|
|
|
raise ValueError(msg)
|
|
|
|
|
visited_nuclides.add(nuc)
|
|
|
|
|
|
|
|
|
|
self._nuclides = cv.CheckedList(_NUCLIDE_CLASSES, 'tally nuclides',
|
|
|
|
|
nuclides)
|
|
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
@property
|
|
|
|
|
def num_nuclides(self):
|
2025-12-04 06:53:04 -06:00
|
|
|
return max(len(self._nuclides), 1)
|
2015-05-31 16:30:15 -07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
@property
|
|
|
|
|
def scores(self):
|
|
|
|
|
return self._scores
|
2015-05-31 11:43:57 -07:00
|
|
|
|
2023-06-17 04:34:02 +01:00
|
|
|
@scores.setter
|
|
|
|
|
def scores(self, scores):
|
|
|
|
|
cv.check_type('tally scores', scores, MutableSequence)
|
|
|
|
|
|
|
|
|
|
visited_scores = set()
|
|
|
|
|
for i, score in enumerate(scores):
|
|
|
|
|
# If the score is already in the Tally, raise an error
|
|
|
|
|
if score in visited_scores:
|
|
|
|
|
msg = (f'Unable to add a duplicate score "{score}" to Tally '
|
|
|
|
|
f'ID="{self.id}" since duplicate scores are not '
|
|
|
|
|
'supported in the OpenMC Python API')
|
|
|
|
|
raise ValueError(msg)
|
|
|
|
|
visited_scores.add(score)
|
|
|
|
|
|
|
|
|
|
# If score is a string, strip whitespace
|
|
|
|
|
if isinstance(score, str):
|
|
|
|
|
# Check to see if scores are deprecated before storing
|
|
|
|
|
for deprecated in ['scatter-', 'nu-scatter-', 'scatter-p',
|
|
|
|
|
'nu-scatter-p', 'scatter-y', 'nu-scatter-y',
|
|
|
|
|
'flux-y', 'total-y']:
|
|
|
|
|
if score.strip().startswith(deprecated):
|
|
|
|
|
msg = score.strip() + ' is no longer supported.'
|
|
|
|
|
raise ValueError(msg)
|
|
|
|
|
scores[i] = score.strip()
|
|
|
|
|
|
|
|
|
|
self._scores = cv.CheckedList(_SCORE_CLASSES, 'tally scores', scores)
|
|
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
@property
|
|
|
|
|
def num_scores(self):
|
|
|
|
|
return len(self._scores)
|
2015-05-31 11:43:57 -07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
@property
|
2016-01-14 15:08:28 -05:00
|
|
|
def num_filters(self):
|
|
|
|
|
return len(self.filters)
|
2015-05-31 16:30:15 -07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
@property
|
|
|
|
|
def num_filter_bins(self):
|
2017-12-19 21:54:08 +07:00
|
|
|
return reduce(operator.mul, (f.num_bins for f in self.filters), 1)
|
2015-05-31 11:43:57 -07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
@property
|
|
|
|
|
def num_bins(self):
|
2017-12-19 21:54:08 +07:00
|
|
|
return self.num_filter_bins * self.num_nuclides * self.num_scores
|
2015-05-31 12:13:59 -07:00
|
|
|
|
2015-12-14 09:32:10 -05:00
|
|
|
@property
|
|
|
|
|
def shape(self):
|
2015-12-24 20:44:41 -05:00
|
|
|
return (self.num_filter_bins, self.num_nuclides, self.num_scores)
|
2015-12-14 09:32:10 -05:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
@property
|
|
|
|
|
def estimator(self):
|
|
|
|
|
return self._estimator
|
2015-05-31 11:43:57 -07:00
|
|
|
|
2023-06-17 04:34:02 +01:00
|
|
|
@estimator.setter
|
|
|
|
|
def estimator(self, estimator):
|
2025-01-24 16:49:58 -06:00
|
|
|
# allow the estimator to be set to None (let OpenMC choose the estimator at runtime)
|
|
|
|
|
cv.check_value('estimator', estimator, ESTIMATOR_TYPES | {None})
|
2023-06-17 04:34:02 +01:00
|
|
|
self._estimator = estimator
|
|
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
@property
|
|
|
|
|
def triggers(self):
|
|
|
|
|
return self._triggers
|
2015-05-31 11:43:57 -07:00
|
|
|
|
2023-06-17 04:34:02 +01:00
|
|
|
@triggers.setter
|
|
|
|
|
def triggers(self, triggers):
|
|
|
|
|
cv.check_type('tally triggers', triggers, MutableSequence)
|
|
|
|
|
self._triggers = cv.CheckedList(openmc.Trigger, 'tally triggers',
|
|
|
|
|
triggers)
|
|
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
@property
|
2025-02-24 16:11:18 -06:00
|
|
|
@ensure_results
|
2015-07-29 00:06:07 -07:00
|
|
|
def num_realizations(self):
|
|
|
|
|
return self._num_realizations
|
2015-05-31 12:13:59 -07:00
|
|
|
|
2023-06-17 04:34:02 +01:00
|
|
|
@num_realizations.setter
|
|
|
|
|
def num_realizations(self, num_realizations):
|
|
|
|
|
cv.check_type('number of realizations', num_realizations, Integral)
|
|
|
|
|
cv.check_greater_than('number of realizations', num_realizations, 0, True)
|
|
|
|
|
self._num_realizations = num_realizations
|
|
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
@property
|
|
|
|
|
def with_summary(self):
|
|
|
|
|
return self._with_summary
|
2015-05-31 12:13:59 -07:00
|
|
|
|
2023-06-17 04:34:02 +01:00
|
|
|
@with_summary.setter
|
|
|
|
|
def with_summary(self, with_summary):
|
|
|
|
|
cv.check_type('with_summary', with_summary, bool)
|
|
|
|
|
self._with_summary = with_summary
|
|
|
|
|
|
2020-03-23 16:45:53 -05:00
|
|
|
def _read_results(self):
|
|
|
|
|
if self._results_read:
|
|
|
|
|
return
|
2015-10-19 09:19:40 -04:00
|
|
|
|
2020-03-23 16:45:53 -05:00
|
|
|
# Open the HDF5 statepoint file
|
|
|
|
|
with h5py.File(self._sp_filename, 'r') as f:
|
2025-01-24 16:49:58 -06:00
|
|
|
# Set number of realizations
|
|
|
|
|
group = f[f'tallies/tally {self.id}']
|
2025-02-24 16:11:18 -06:00
|
|
|
self._num_realizations = int(group['n_realizations'][()])
|
2025-01-24 16:49:58 -06:00
|
|
|
|
2025-12-04 06:53:04 -06:00
|
|
|
for filt in self.filters:
|
2026-04-28 17:10:03 -04:00
|
|
|
if isinstance(filt, DistribcellFilter):
|
2025-12-04 06:53:04 -06:00
|
|
|
filter_group = f[f'tallies/filters/filter {filt.id}']
|
|
|
|
|
filt._num_bins = int(filter_group['n_bins'][()])
|
|
|
|
|
|
2025-01-24 16:49:58 -06:00
|
|
|
# Update nuclides
|
|
|
|
|
nuclide_names = group['nuclides'][()]
|
2025-02-24 16:11:18 -06:00
|
|
|
self._nuclides = [name.decode().strip() for name in nuclide_names]
|
2025-11-12 11:41:37 -05:00
|
|
|
# Check for higher_moments attribute
|
|
|
|
|
if "higher_moments" in group.attrs:
|
|
|
|
|
self._higher_moments = bool(group.attrs["higher_moments"][()])
|
|
|
|
|
else:
|
|
|
|
|
self._higher_moments = False
|
2025-01-24 16:49:58 -06:00
|
|
|
|
2015-09-16 16:19:12 +07:00
|
|
|
# Extract Tally data from the file
|
2025-01-24 16:49:58 -06:00
|
|
|
data = group['results']
|
2020-03-23 16:45:53 -05:00
|
|
|
sum_ = data[:, :, 0]
|
2018-03-28 15:56:03 -05:00
|
|
|
sum_sq = data[:, :, 1]
|
2015-09-16 16:19:12 +07:00
|
|
|
|
|
|
|
|
# Reshape the results arrays
|
2020-03-23 16:45:53 -05:00
|
|
|
sum_ = np.reshape(sum_, self.shape)
|
2015-12-13 01:30:10 -05:00
|
|
|
sum_sq = np.reshape(sum_sq, self.shape)
|
2015-09-16 16:19:12 +07:00
|
|
|
|
|
|
|
|
# Set the data for this Tally
|
2020-03-23 16:45:53 -05:00
|
|
|
self._sum = sum_
|
2015-09-16 16:19:12 +07:00
|
|
|
self._sum_sq = sum_sq
|
|
|
|
|
|
2025-11-12 11:41:37 -05:00
|
|
|
if self._higher_moments:
|
|
|
|
|
# Extract additional Tally data when higher moments enabled
|
|
|
|
|
sum_third = data[:, :, 2]
|
|
|
|
|
sum_fourth = data[:, :, 3]
|
|
|
|
|
|
|
|
|
|
# Reshape the results arrays
|
|
|
|
|
sum_third = np.reshape(sum_third, self.shape)
|
|
|
|
|
sum_fourth = np.reshape(sum_fourth, self.shape)
|
|
|
|
|
|
|
|
|
|
# Set the additional data for this Tally
|
|
|
|
|
self._sum_third = sum_third
|
|
|
|
|
self._sum_fourth = sum_fourth
|
|
|
|
|
|
2015-12-13 01:30:10 -05:00
|
|
|
# Convert NumPy arrays to SciPy sparse LIL matrices
|
2015-12-12 23:44:15 -05:00
|
|
|
if self.sparse:
|
2026-01-02 06:43:25 -05:00
|
|
|
self._sum = lil_array(self._sum.flatten(), self._sum.shape)
|
|
|
|
|
self._sum_sq = lil_array(self._sum_sq.flatten(), self._sum_sq.shape)
|
|
|
|
|
self._sum_third = lil_array(self._sum_third.flatten(), self._sum_third.shape)
|
|
|
|
|
self._sum_fourth = lil_array(self._sum_fourth.flatten(), self._sum_fourth.shape)
|
2020-03-23 16:45:53 -05:00
|
|
|
|
2025-05-03 08:19:10 -04:00
|
|
|
# Read simulation time (needed for figure of merit)
|
|
|
|
|
self._simulation_time = f["runtime"]["simulation"][()]
|
|
|
|
|
|
2020-03-23 16:45:53 -05:00
|
|
|
# Indicate that Tally results have been read
|
|
|
|
|
self._results_read = True
|
2015-12-12 23:44:15 -05:00
|
|
|
|
2020-03-23 16:45:53 -05:00
|
|
|
@property
|
2025-02-24 16:11:18 -06:00
|
|
|
@ensure_results
|
2020-03-23 16:45:53 -05:00
|
|
|
def sum(self):
|
|
|
|
|
if not self._sp_filename or self.derived:
|
|
|
|
|
return None
|
2015-09-16 16:19:12 +07:00
|
|
|
|
2015-12-12 23:44:15 -05:00
|
|
|
if self.sparse:
|
2015-12-13 01:30:10 -05:00
|
|
|
return np.reshape(self._sum.toarray(), self.shape)
|
2015-12-12 23:44:15 -05:00
|
|
|
else:
|
|
|
|
|
return self._sum
|
2015-05-31 12:13:59 -07:00
|
|
|
|
2023-06-17 04:34:02 +01:00
|
|
|
@sum.setter
|
|
|
|
|
def sum(self, sum):
|
|
|
|
|
cv.check_type('sum', sum, Iterable)
|
|
|
|
|
self._sum = sum
|
|
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
@property
|
2025-02-24 16:11:18 -06:00
|
|
|
@ensure_results
|
2015-07-29 00:06:07 -07:00
|
|
|
def sum_sq(self):
|
2018-05-23 14:16:17 -06:00
|
|
|
if not self._sp_filename or self.derived:
|
2015-09-16 16:19:12 +07:00
|
|
|
return None
|
|
|
|
|
|
2015-12-12 23:44:15 -05:00
|
|
|
if self.sparse:
|
2015-12-13 01:30:10 -05:00
|
|
|
return np.reshape(self._sum_sq.toarray(), self.shape)
|
2015-12-12 23:44:15 -05:00
|
|
|
else:
|
|
|
|
|
return self._sum_sq
|
2015-05-31 12:13:59 -07:00
|
|
|
|
2023-06-17 04:34:02 +01:00
|
|
|
@sum_sq.setter
|
|
|
|
|
def sum_sq(self, sum_sq):
|
|
|
|
|
cv.check_type('sum_sq', sum_sq, Iterable)
|
|
|
|
|
self._sum_sq = sum_sq
|
|
|
|
|
|
2025-11-12 11:41:37 -05:00
|
|
|
@property
|
|
|
|
|
@ensure_results
|
|
|
|
|
def sum_third(self):
|
|
|
|
|
if not self._higher_moments:
|
|
|
|
|
raise ValueError(
|
|
|
|
|
"Higher moments have not been enabled for this tally. To make "
|
|
|
|
|
"higher moments available, set the higher_moments attribute to "
|
|
|
|
|
"True before running a simulation."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if not self._sp_filename or self.derived:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
if self.sparse:
|
|
|
|
|
return np.reshape(self._sum_third.toarray(), self.shape)
|
|
|
|
|
else:
|
|
|
|
|
return self._sum_third
|
|
|
|
|
|
|
|
|
|
@sum_third.setter
|
|
|
|
|
def sum_third(self, sum_third):
|
|
|
|
|
cv.check_type("sum_third", sum_third, Iterable)
|
|
|
|
|
self._sum_third = sum_third
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
@ensure_results
|
|
|
|
|
def sum_fourth(self):
|
|
|
|
|
if not self._higher_moments:
|
|
|
|
|
raise ValueError(
|
|
|
|
|
"Higher moments have not been enabled for this tally. To make "
|
|
|
|
|
"higher moments available, set the higher_moments attribute to "
|
|
|
|
|
"True before running a simulation."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if not self._sp_filename or self.derived:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
if self.sparse:
|
|
|
|
|
return np.reshape(self._sum_fourth.toarray(), self.shape)
|
|
|
|
|
else:
|
|
|
|
|
return self._sum_fourth
|
|
|
|
|
|
|
|
|
|
@sum_fourth.setter
|
|
|
|
|
def sum_fourth(self, sum_fourth):
|
|
|
|
|
cv.check_type("sum_fourth", sum_fourth, Iterable)
|
|
|
|
|
self._sum_fourth = sum_fourth
|
|
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
@property
|
|
|
|
|
def mean(self):
|
|
|
|
|
if self._mean is None:
|
2015-10-19 09:19:40 -04:00
|
|
|
if not self._sp_filename:
|
2015-09-16 16:19:12 +07:00
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
self._mean = self.sum / self.num_realizations
|
2015-12-12 23:44:15 -05:00
|
|
|
|
2015-12-13 01:30:10 -05:00
|
|
|
# Convert NumPy array to SciPy sparse LIL matrix
|
2015-12-12 23:44:15 -05:00
|
|
|
if self.sparse:
|
2026-01-02 06:43:25 -05:00
|
|
|
self._mean = lil_array(self._mean.flatten(), self._mean.shape)
|
2015-12-12 23:44:15 -05:00
|
|
|
|
|
|
|
|
if self.sparse:
|
2015-12-13 01:30:10 -05:00
|
|
|
return np.reshape(self._mean.toarray(), self.shape)
|
2015-12-12 23:44:15 -05:00
|
|
|
else:
|
|
|
|
|
return self._mean
|
2015-05-31 12:13:59 -07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
@property
|
|
|
|
|
def std_dev(self):
|
|
|
|
|
if self._std_dev is None:
|
2015-10-19 09:19:40 -04:00
|
|
|
if not self._sp_filename:
|
2015-09-16 16:19:12 +07:00
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
n = self.num_realizations
|
2015-09-21 10:01:26 +07:00
|
|
|
nonzero = np.abs(self.mean) > 0
|
2015-09-18 22:05:50 +07:00
|
|
|
self._std_dev = np.zeros_like(self.mean)
|
|
|
|
|
self._std_dev[nonzero] = np.sqrt((self.sum_sq[nonzero]/n -
|
|
|
|
|
self.mean[nonzero]**2)/(n - 1))
|
2015-12-12 23:44:15 -05:00
|
|
|
|
2015-12-13 01:30:10 -05:00
|
|
|
# Convert NumPy array to SciPy sparse LIL matrix
|
2015-12-12 23:44:15 -05:00
|
|
|
if self.sparse:
|
2026-01-02 06:43:25 -05:00
|
|
|
self._std_dev = lil_array(self._std_dev.flatten(), self._std_dev.shape)
|
2015-12-12 23:44:15 -05:00
|
|
|
|
2015-09-16 16:19:12 +07:00
|
|
|
self.with_batch_statistics = True
|
2015-12-12 23:44:15 -05:00
|
|
|
|
|
|
|
|
if self.sparse:
|
2015-12-13 01:30:10 -05:00
|
|
|
return np.reshape(self._std_dev.toarray(), self.shape)
|
2015-12-12 23:44:15 -05:00
|
|
|
else:
|
|
|
|
|
return self._std_dev
|
2015-05-31 12:13:59 -07:00
|
|
|
|
2025-11-12 11:41:37 -05:00
|
|
|
@property
|
|
|
|
|
def vov(self):
|
|
|
|
|
if self._vov is None:
|
|
|
|
|
n = self.num_realizations
|
|
|
|
|
sum1 = self.sum
|
|
|
|
|
sum2 = self.sum_sq
|
|
|
|
|
sum3 = self.sum_third
|
|
|
|
|
sum4 = self.sum_fourth
|
|
|
|
|
self._vov = np.zeros_like(sum1, dtype=float)
|
|
|
|
|
|
|
|
|
|
# Calculate the variance of the variance (Eq. 2.232 in
|
|
|
|
|
# https://doi.org/10.2172/2372634)
|
|
|
|
|
numerator = (sum4 - (4.0*sum3*sum1)/n
|
|
|
|
|
+ (6.0*sum2*(sum1**2))/(n**2)
|
|
|
|
|
- (3.0*(sum1)**4)/(n**3))
|
|
|
|
|
denominator = (sum2 - (1.0/n)*(sum1**2))**2
|
|
|
|
|
|
|
|
|
|
mask = denominator > 0.0
|
|
|
|
|
|
|
|
|
|
self._vov[mask] = numerator[mask]/denominator[mask] - 1.0/n
|
|
|
|
|
|
|
|
|
|
if self.sparse:
|
2026-01-02 06:43:25 -05:00
|
|
|
self._vov = lil_array(self._vov.flatten(), self._vov.shape)
|
2025-11-12 11:41:37 -05:00
|
|
|
|
|
|
|
|
if self.sparse:
|
|
|
|
|
return np.reshape(self._vov.toarray(), self.shape)
|
|
|
|
|
else:
|
|
|
|
|
return self._vov
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def m2(self):
|
|
|
|
|
n = self.num_realizations
|
|
|
|
|
return self.sum_sq/n - self.mean**2
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def m3(self):
|
|
|
|
|
n = self.num_realizations
|
|
|
|
|
mean = self.mean
|
|
|
|
|
sum2 = self.sum_sq/n
|
|
|
|
|
sum3 = self.sum_third/n
|
|
|
|
|
|
|
|
|
|
return sum3 - 3.0*mean*sum2 + 2.0*mean**3
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def m4(self):
|
|
|
|
|
n = self.num_realizations
|
|
|
|
|
mean = self.mean
|
|
|
|
|
sum2 = self.sum_sq/n
|
|
|
|
|
sum3 = self.sum_third/n
|
|
|
|
|
sum4 = self.sum_fourth/n
|
|
|
|
|
|
|
|
|
|
return sum4 - 4.0*mean*sum3 + 6.0*(mean**2)*sum2 - 3.0*mean**4
|
|
|
|
|
|
|
|
|
|
def skew(self, bias=False) -> np.ndarray:
|
|
|
|
|
"""Return the sample skewness of each tally bin.
|
|
|
|
|
|
|
|
|
|
This method computes and returns the unadjusted or adjusted
|
|
|
|
|
Fisher-Pearson coefficient of skewness.
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
|
|
|
|
bias : bool
|
|
|
|
|
If False, calculations are corrected for bias and the adjusted
|
|
|
|
|
Fisher-Pearson skewness (:math:`G_1`) is returned. If True,
|
|
|
|
|
calculations are not corrected for bias and the unadjusted skewness
|
|
|
|
|
(:math:`g_1`) is returned.
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
|
|
|
|
float
|
|
|
|
|
The skewness of each tally bin
|
|
|
|
|
"""
|
|
|
|
|
n = self.num_realizations
|
|
|
|
|
m2 = self.m2
|
|
|
|
|
m3 = self.m3
|
|
|
|
|
|
|
|
|
|
with np.errstate(divide="ignore", invalid="ignore"):
|
|
|
|
|
g1 = np.where(m2 > 0.0, m3/(m2**1.5), 0.0)
|
|
|
|
|
|
|
|
|
|
if bias:
|
|
|
|
|
return g1
|
|
|
|
|
else:
|
|
|
|
|
if n <= 2:
|
|
|
|
|
raise ValueError("Insufficient number of independent realizations"
|
|
|
|
|
f"for bias-corrected skewness: need n >= 3, got {n=}.")
|
|
|
|
|
else:
|
|
|
|
|
return sqrt(n*(n - 1))/(n - 2)*g1
|
|
|
|
|
|
|
|
|
|
def kurtosis(self, fisher=True, bias=False) -> np.ndarray:
|
|
|
|
|
r"""Return the sample kurtosis of each tally bin.
|
|
|
|
|
|
|
|
|
|
This method computes and returns the sample kurtosis using either
|
|
|
|
|
Pearson's or Fisher's definition, with or without finite-sample bias
|
|
|
|
|
correction. The value returned depends on the `bias` and `fisher`
|
|
|
|
|
arguments as follows:
|
|
|
|
|
|
|
|
|
|
- **bias=True, fisher=False**: Returns :math:`b_2` (Pearson's kurtosis)
|
|
|
|
|
This is the raw fourth standardized moment: :math:`m_4/m_2^2`. For a
|
|
|
|
|
normal distribution, :math:`b_2\approx 3`.
|
|
|
|
|
|
|
|
|
|
- **bias=True, fisher=True**: Returns :math:`g_2` (excess kurtosis) This
|
|
|
|
|
is :math:`b_2 - 3`, centered at 0 for normal distributions. Positive
|
|
|
|
|
values indicate heavier tails, negative values lighter tails.
|
|
|
|
|
|
|
|
|
|
- **bias=False, fisher=True** (default): Returns :math:`G_2` (adjusted
|
|
|
|
|
excess kurtosis). This applies finite-sample bias correction to
|
|
|
|
|
:math:`g_2`. This is the recommended estimator for statistical
|
|
|
|
|
inference.
|
|
|
|
|
|
|
|
|
|
- **bias=False, fisher=False**: Returns bias-corrected Pearson's
|
|
|
|
|
kurtosis. This is :math:`G_2 + 3`.
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
|
|
|
|
fisher : bool, optional
|
|
|
|
|
If True (default), Fisher's definition is used (excess kurtosis). If
|
|
|
|
|
False, Pearson's definition is used.
|
|
|
|
|
bias : bool, optional
|
|
|
|
|
If False (default), calculations are corrected for statistical bias
|
|
|
|
|
using finite-sample adjustments. If True, calculations use the
|
|
|
|
|
biased estimator (population formulas).
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
|
|
|
|
numpy.ndarray
|
|
|
|
|
The kurtosis of each tally bin
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
n = self.num_realizations
|
|
|
|
|
m2 = self.m2
|
|
|
|
|
m4 = self.m4
|
|
|
|
|
|
|
|
|
|
with np.errstate(divide="ignore", invalid="ignore"):
|
|
|
|
|
b2 = np.where(m2 > 0.0, m4/(m2**2), 0.0)
|
|
|
|
|
g2 = b2 - 3.0
|
|
|
|
|
|
|
|
|
|
if bias:
|
|
|
|
|
# Biased estimator (g2 or b2)
|
|
|
|
|
return g2 if fisher else b2
|
|
|
|
|
else:
|
|
|
|
|
# Unbiased estimator with finite-sample correction
|
|
|
|
|
if n <= 3:
|
|
|
|
|
raise ValueError("Insufficient number of independent realizations"
|
|
|
|
|
f"for bias-corrected kurtosis: need n >= 4, got {n=}.")
|
|
|
|
|
else:
|
|
|
|
|
G2 = ((n - 1)/((n - 2)*(n - 3)))*((n + 1)*g2 + 6.0)
|
|
|
|
|
return G2 if fisher else G2 + 3.0
|
|
|
|
|
|
|
|
|
|
def skewtest(self, alternative: str = "two-sided"):
|
|
|
|
|
"""Perform D'Agostino and Pearson's test for skewness.
|
|
|
|
|
|
|
|
|
|
This method tests the null hypothesis that the skewness of the
|
|
|
|
|
population that the sample was drawn from is the same as that of a
|
|
|
|
|
corresponding normal distribution.
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
|
|
|
|
alternative : {'two-sided', 'less', 'greater'}, optional
|
|
|
|
|
Defines the alternative hypothesis. The following options are
|
|
|
|
|
available:
|
|
|
|
|
|
|
|
|
|
* 'two-sided': the skewness of the distribution is different from
|
|
|
|
|
that of the normal distribution (i.e., non-zero)
|
|
|
|
|
* 'less': the skewness of the distribution is less than that of the
|
|
|
|
|
normal distribution
|
|
|
|
|
* 'greater': the skewness of the distribution is greater than that
|
|
|
|
|
of the normal distribution
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
|
|
|
|
statistic : np.ndarray
|
|
|
|
|
The computed z-score for the skewness test for each tally bin
|
|
|
|
|
pvalue : np.ndarray
|
|
|
|
|
The p-value for the hypothesis test for each tally bin
|
|
|
|
|
|
|
|
|
|
Notes
|
|
|
|
|
-----
|
2025-11-18 17:57:12 -06:00
|
|
|
This test is based on `D'Agostino and Pearson's test
|
|
|
|
|
<https://doi.org/10.1093/biomet/60.3.613>`_. The test requires at least
|
|
|
|
|
8 realizations to produce valid results.
|
2025-11-12 11:41:37 -05:00
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
n = self.num_realizations
|
|
|
|
|
if n < 8:
|
|
|
|
|
raise ValueError("Skewness test is not well-defined for n < 8.")
|
|
|
|
|
|
|
|
|
|
g1 = self.skew(bias=True)
|
|
|
|
|
|
|
|
|
|
# --- Z1 (skewness) ---
|
|
|
|
|
y = g1 * sqrt(((n + 1.0)*(n + 3.0))/(6.0*(n - 2.0)))
|
|
|
|
|
beta2 = (3.0*(n**2 + 27.0*n - 70.0)*(n + 1.0)*(n + 3.0)
|
|
|
|
|
)/((n - 2.0)*(n + 5.0)*(n + 7.0)*(n + 9.0))
|
|
|
|
|
W2 = -1.0 + sqrt(2.0*(beta2 - 1.0))
|
|
|
|
|
delta = 1.0 / sqrt(log(sqrt(W2)))
|
|
|
|
|
alpha = sqrt(2.0 / (W2 - 1.0))
|
|
|
|
|
Zb1 = np.where(
|
|
|
|
|
y >= 0.0,
|
|
|
|
|
delta*np.log((y/alpha) + np.sqrt((y/alpha)**2 + 1.0)),
|
|
|
|
|
-delta*np.log((-y/alpha) + np.sqrt((y/alpha)**2 + 1.0))
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# p-value
|
|
|
|
|
if alternative == "two-sided":
|
|
|
|
|
p = 2.0 * (1.0 - norm.cdf(np.abs(Zb1)))
|
|
|
|
|
elif alternative == "greater":
|
|
|
|
|
p = 1.0 - norm.cdf(Zb1)
|
|
|
|
|
elif alternative == "less":
|
|
|
|
|
p = norm.cdf(Zb1)
|
|
|
|
|
else:
|
|
|
|
|
raise ValueError("alternative must be 'two-sided', 'greater', or 'less'")
|
|
|
|
|
|
|
|
|
|
return Zb1, p
|
|
|
|
|
|
|
|
|
|
def kurtosistest(self, alternative: str = "two-sided"):
|
|
|
|
|
"""Perform D'Agostino and Pearson's test for kurtosis.
|
|
|
|
|
|
|
|
|
|
This method tests the null hypothesis that the kurtosis of the
|
|
|
|
|
population that the sample was drawn from is the same as that of a
|
|
|
|
|
corresponding normal distribution.
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
|
|
|
|
alternative : {'two-sided', 'less', 'greater'}, optional
|
2025-11-18 17:57:12 -06:00
|
|
|
Defines the alternative hypothesis. Default is 'two-sided'. The
|
|
|
|
|
following options are available:
|
2025-11-12 11:41:37 -05:00
|
|
|
|
|
|
|
|
* 'two-sided': the kurtosis of the distribution is different from
|
|
|
|
|
that of the normal distribution
|
|
|
|
|
* 'less': the kurtosis of the distribution is less than that of the
|
|
|
|
|
normal distribution
|
|
|
|
|
* 'greater': the kurtosis of the distribution is greater than that
|
|
|
|
|
of the normal distribution
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
|
|
|
|
statistic : np.ndarray
|
|
|
|
|
The computed z-score for the kurtosis test for each tally bin
|
|
|
|
|
pvalue : np.ndarray
|
|
|
|
|
The p-value for the hypothesis test for each tally bin
|
|
|
|
|
|
|
|
|
|
Raises
|
|
|
|
|
------
|
|
|
|
|
ValueError
|
|
|
|
|
If the number of realizations is less than 20, or if an invalid
|
|
|
|
|
alternative hypothesis is specified.
|
|
|
|
|
|
|
|
|
|
Notes
|
|
|
|
|
-----
|
2025-11-18 17:57:12 -06:00
|
|
|
This test is based on `D'Agostino and Pearson's test
|
|
|
|
|
<https://doi.org/10.1093/biomet/60.3.613>`_. The test is typically
|
|
|
|
|
recommended for at least 20 realizations to produce valid results.
|
2025-11-12 11:41:37 -05:00
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
n = self.num_realizations
|
|
|
|
|
if n < 20:
|
|
|
|
|
raise ValueError("Kurtosis test is typically recommended for n >= 20.")
|
|
|
|
|
|
|
|
|
|
b2 = self.kurtosis(bias=True, fisher=False)
|
|
|
|
|
|
|
|
|
|
# --- Z2 (kurtosis) ---
|
|
|
|
|
mean_b2 = 3.0 * (n - 1.0) / (n + 1.0)
|
|
|
|
|
var_b2 = (24.0*n*(n - 2.0)*(n - 3.0)/(
|
|
|
|
|
(n + 1.0)**2*(n + 3.0)*(n + 5.0)))
|
|
|
|
|
x = (b2 - mean_b2)/np.sqrt(var_b2)
|
|
|
|
|
moment = ((6.0*(n**2 - 5.0*n + 2.0))/((n + 7.0)*(n + 9.0))
|
|
|
|
|
)*sqrt((6.0*(n + 3.0)*(n + 5.0))/(n*(n - 2.0)*(n - 3.0)))
|
|
|
|
|
A = 6.0 + (8.0/moment)*((2.0/moment) + sqrt(1.0 + 4.0/(moment**2)))
|
|
|
|
|
Zb2 = (1.0- 2.0/(9.0*A) - ((1.0 - 2.0/A) / (1.0 + (x
|
|
|
|
|
)*sqrt(2.0/(A - 4.0))))**(1.0/3.0)) / sqrt(2.0/(9.0*A))
|
|
|
|
|
|
|
|
|
|
# p-value
|
|
|
|
|
if alternative == "two-sided":
|
|
|
|
|
p = 2.0 * (1.0 - norm.cdf(np.abs(Zb2)))
|
|
|
|
|
elif alternative == "greater":
|
|
|
|
|
p = 1.0 - norm.cdf(Zb2)
|
|
|
|
|
elif alternative == "less":
|
|
|
|
|
p = norm.cdf(Zb2)
|
|
|
|
|
else:
|
|
|
|
|
raise ValueError("alternative must be 'two-sided', 'greater', or 'less'")
|
|
|
|
|
|
|
|
|
|
return Zb2, p
|
|
|
|
|
|
|
|
|
|
def normaltest(self, alternative: str = "two-sided"):
|
|
|
|
|
"""Perform D'Agostino and Pearson's omnibus test for normality.
|
|
|
|
|
|
2025-11-18 17:57:12 -06:00
|
|
|
This method tests the null hypothesis that a sample comes from a normal
|
|
|
|
|
distribution. It combines skewness and kurtosis to produce an omnibus
|
|
|
|
|
test of normality.
|
2025-11-12 11:41:37 -05:00
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
|
|
|
|
alternative : {'two-sided', 'less', 'greater'}, optional
|
|
|
|
|
Defines the alternative hypothesis used for the component skewness
|
|
|
|
|
and kurtosis tests. Default is 'two-sided'. The following options
|
|
|
|
|
are available:
|
|
|
|
|
|
|
|
|
|
* 'two-sided': the distribution is different from normal
|
|
|
|
|
* 'less': used for the component tests
|
|
|
|
|
* 'greater': used for the component tests
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
|
|
|
|
statistic : np.ndarray
|
|
|
|
|
The computed z-score for the normality test for each tally bin
|
|
|
|
|
pvalue : np.ndarray
|
|
|
|
|
The p-value for the hypothesis test for each tally bin
|
|
|
|
|
|
|
|
|
|
Raises
|
|
|
|
|
------
|
|
|
|
|
ValueError
|
|
|
|
|
If the number of realizations is less than 20, or if an invalid
|
|
|
|
|
alternative hypothesis is specified.
|
|
|
|
|
|
|
|
|
|
Notes
|
|
|
|
|
-----
|
|
|
|
|
This test combines a test for skewness and a test for kurtosis to
|
2025-11-18 17:57:12 -06:00
|
|
|
produce an `omnibus test <https://doi.org/10.1093/biomet/60.3.613>`_.
|
|
|
|
|
The test statistic is:
|
2025-11-12 11:41:37 -05:00
|
|
|
|
|
|
|
|
.. math::
|
|
|
|
|
|
|
|
|
|
K^2 = Z_1^2 + Z_2^2
|
|
|
|
|
|
2025-11-18 17:57:12 -06:00
|
|
|
where :math:`Z_1` is the z-score from the skewness test and :math:`Z_2`
|
|
|
|
|
is the z-score from the kurtosis test. This statistic follows a
|
|
|
|
|
chi-square distribution with 2 degrees of freedom.
|
2025-11-12 11:41:37 -05:00
|
|
|
|
|
|
|
|
The test requires at least 20 realizations to produce valid results.
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
n = self.num_realizations
|
|
|
|
|
if n < 20:
|
|
|
|
|
raise ValueError("normaltest requires n >= 20 (per D'Agostino-Pearson).")
|
|
|
|
|
|
|
|
|
|
# Use the component tests
|
|
|
|
|
Z1, _ = self.skewtest(alternative)
|
|
|
|
|
Z2, _ = self.kurtosistest(alternative)
|
|
|
|
|
|
|
|
|
|
# Combine as chi-square with df=2 since we have skewness and kurtosis
|
|
|
|
|
K2 = Z1*Z1 + Z2*Z2
|
|
|
|
|
p = chi2.sf(K2, df=2)
|
|
|
|
|
return K2, p
|
|
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
@property
|
2025-05-03 08:19:10 -04:00
|
|
|
def figure_of_merit(self):
|
|
|
|
|
mean = self.mean
|
|
|
|
|
std_dev = self.std_dev
|
|
|
|
|
fom = np.zeros_like(mean)
|
|
|
|
|
nonzero = np.abs(mean) > 0
|
2025-11-12 11:41:37 -05:00
|
|
|
rel_err = std_dev[nonzero] / mean[nonzero]
|
|
|
|
|
fom[nonzero] = 1.0 / (rel_err**2 * self._simulation_time)
|
2025-05-03 08:19:10 -04:00
|
|
|
return fom
|
|
|
|
|
|
|
|
|
|
@property
|
2015-07-29 00:06:07 -07:00
|
|
|
def with_batch_statistics(self):
|
|
|
|
|
return self._with_batch_statistics
|
2015-05-31 12:13:59 -07:00
|
|
|
|
2023-06-17 04:34:02 +01:00
|
|
|
@with_batch_statistics.setter
|
|
|
|
|
def with_batch_statistics(self, with_batch_statistics):
|
|
|
|
|
cv.check_type('with_batch_statistics', with_batch_statistics, bool)
|
|
|
|
|
self._with_batch_statistics = with_batch_statistics
|
|
|
|
|
|
2015-08-06 23:41:38 -07:00
|
|
|
@property
|
|
|
|
|
def derived(self):
|
|
|
|
|
return self._derived
|
|
|
|
|
|
2015-11-08 22:03:30 -05:00
|
|
|
@property
|
2016-01-29 16:54:07 -05:00
|
|
|
def derivative(self):
|
|
|
|
|
return self._derivative
|
2015-11-08 22:03:30 -05:00
|
|
|
|
2016-01-29 16:54:07 -05:00
|
|
|
@derivative.setter
|
|
|
|
|
def derivative(self, deriv):
|
2020-03-23 16:45:53 -05:00
|
|
|
cv.check_type('tally derivative', deriv, openmc.TallyDerivative,
|
|
|
|
|
none_ok=True)
|
2016-11-13 13:49:30 -05:00
|
|
|
self._derivative = deriv
|
2015-11-08 22:03:30 -05:00
|
|
|
|
2023-06-17 04:34:02 +01:00
|
|
|
@property
|
|
|
|
|
def sparse(self):
|
|
|
|
|
return self._sparse
|
2015-05-31 14:22:42 -07:00
|
|
|
|
2015-12-13 11:14:01 -05:00
|
|
|
@sparse.setter
|
|
|
|
|
def sparse(self, sparse):
|
|
|
|
|
"""Convert tally data from NumPy arrays to SciPy list of lists (LIL)
|
|
|
|
|
sparse matrices, and vice versa.
|
|
|
|
|
|
|
|
|
|
This property may be used to reduce the amount of data in memory during
|
|
|
|
|
tally data processing. The tally data will be stored as SciPy LIL
|
|
|
|
|
matrices internally within the Tally object. All tally data access
|
|
|
|
|
properties and methods will return data as a dense NumPy array.
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
cv.check_type('sparse', sparse, bool)
|
|
|
|
|
|
|
|
|
|
# Convert NumPy arrays to SciPy sparse LIL matrices
|
|
|
|
|
if sparse and not self.sparse:
|
|
|
|
|
if self._sum is not None:
|
2026-01-02 06:43:25 -05:00
|
|
|
self._sum = lil_array(self._sum.flatten(), self._sum.shape)
|
2015-12-13 11:14:01 -05:00
|
|
|
if self._sum_sq is not None:
|
2026-01-02 06:43:25 -05:00
|
|
|
self._sum_sq = lil_array(self._sum_sq.flatten(), self._sum_sq.shape)
|
2025-11-12 11:41:37 -05:00
|
|
|
if self._sum_third is not None:
|
2026-01-02 06:43:25 -05:00
|
|
|
self._sum_third = lil_array(self._sum_third.flatten(), self._sum_third.shape)
|
2025-11-12 11:41:37 -05:00
|
|
|
if self._sum_fourth is not None:
|
2026-01-02 06:43:25 -05:00
|
|
|
self._sum_fourth = lil_array(self._sum_fourth.flatten(), self._sum_fourth.shape)
|
2015-12-13 11:14:01 -05:00
|
|
|
if self._mean is not None:
|
2026-01-02 06:43:25 -05:00
|
|
|
self._mean = lil_array(self._mean.flatten(), self._mean.shape)
|
2015-12-13 11:14:01 -05:00
|
|
|
if self._std_dev is not None:
|
2026-01-02 06:43:25 -05:00
|
|
|
self._std_dev = lil_array(self._std_dev.flatten(), self._std_dev.shape)
|
2016-01-06 08:45:25 -05:00
|
|
|
|
2015-12-13 11:14:01 -05:00
|
|
|
self._sparse = True
|
|
|
|
|
|
|
|
|
|
# Convert SciPy sparse LIL matrices to NumPy arrays
|
|
|
|
|
elif not sparse and self.sparse:
|
|
|
|
|
if self._sum is not None:
|
|
|
|
|
self._sum = np.reshape(self._sum.toarray(), self.shape)
|
|
|
|
|
if self._sum_sq is not None:
|
|
|
|
|
self._sum_sq = np.reshape(self._sum_sq.toarray(), self.shape)
|
2025-11-12 11:41:37 -05:00
|
|
|
if self._sum_third is not None:
|
|
|
|
|
self._sum_third = np.reshape(self._sum_third.toarray(), self.shape)
|
|
|
|
|
if self._sum_fourth is not None:
|
|
|
|
|
self._sum_fourth = np.reshape(self._sum_fourth.toarray(), self.shape)
|
2015-12-13 11:14:01 -05:00
|
|
|
if self._mean is not None:
|
|
|
|
|
self._mean = np.reshape(self._mean.toarray(), self.shape)
|
|
|
|
|
if self._std_dev is not None:
|
|
|
|
|
self._std_dev = np.reshape(self._std_dev.toarray(), self.shape)
|
|
|
|
|
self._sparse = False
|
|
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
def remove_score(self, score):
|
|
|
|
|
"""Remove a score from the tally
|
2015-05-31 13:26:10 -07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
Parameters
|
|
|
|
|
----------
|
|
|
|
|
score : str
|
|
|
|
|
Score to remove
|
2015-05-31 16:30:15 -07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
"""
|
2015-05-31 16:30:15 -07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
if score not in self.scores:
|
2021-07-29 18:25:37 +01:00
|
|
|
msg = f'Unable to remove score "{score}" from Tally ' \
|
2021-07-31 22:33:45 +01:00
|
|
|
f'ID="{self.id}" since the Tally does not contain this score'
|
2020-03-23 16:45:53 -05:00
|
|
|
raise ValueError(msg)
|
2015-05-31 12:13:59 -07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
self._scores.remove(score)
|
2015-05-31 12:13:59 -07:00
|
|
|
|
2016-01-14 18:03:20 -05:00
|
|
|
def remove_filter(self, old_filter):
|
2015-07-29 00:06:07 -07:00
|
|
|
"""Remove a filter from the tally
|
2015-05-31 16:30:15 -07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
Parameters
|
|
|
|
|
----------
|
2016-04-13 22:17:16 -05:00
|
|
|
old_filter : openmc.Filter
|
2015-07-29 00:06:07 -07:00
|
|
|
Filter to remove
|
2015-05-31 12:13:59 -07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
"""
|
2015-05-31 12:13:59 -07:00
|
|
|
|
2016-01-14 18:03:20 -05:00
|
|
|
if old_filter not in self.filters:
|
2021-07-29 18:25:37 +01:00
|
|
|
msg = f'Unable to remove filter "{old_filter}" from Tally ' \
|
2021-07-31 22:33:45 +01:00
|
|
|
f'ID="{self.id}" since the Tally does not contain this filter'
|
2020-03-23 16:45:53 -05:00
|
|
|
raise ValueError(msg)
|
2015-05-31 12:13:59 -07:00
|
|
|
|
2016-01-14 18:03:20 -05:00
|
|
|
self._filters.remove(old_filter)
|
2015-05-31 12:13:59 -07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
def remove_nuclide(self, nuclide):
|
|
|
|
|
"""Remove a nuclide from the tally
|
2015-05-31 11:43:57 -07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
Parameters
|
|
|
|
|
----------
|
2025-01-24 16:49:58 -06:00
|
|
|
nuclide : str
|
2015-07-29 00:06:07 -07:00
|
|
|
Nuclide to remove
|
2015-05-31 11:43:57 -07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
"""
|
2015-05-31 12:13:59 -07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
if nuclide not in self.nuclides:
|
2021-07-29 18:25:37 +01:00
|
|
|
msg = f'Unable to remove nuclide "{nuclide}" from Tally ' \
|
2021-07-31 22:33:45 +01:00
|
|
|
f'ID="{self.id}" since the Tally does not contain this nuclide'
|
2020-03-23 16:45:53 -05:00
|
|
|
raise ValueError(msg)
|
2015-05-31 12:13:59 -07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
self._nuclides.remove(nuclide)
|
2015-05-31 12:13:59 -07:00
|
|
|
|
2016-02-06 15:04:56 -05:00
|
|
|
def _can_merge_filters(self, other):
|
|
|
|
|
"""Determine if another tally's filters can be merged with this one's
|
|
|
|
|
|
|
|
|
|
The types of filters between the two tallies must match identically.
|
|
|
|
|
The bins in all of the filters must match identically, or be mergeable
|
|
|
|
|
in only one filter. This is a helper method for the can_merge(...)
|
|
|
|
|
and merge(...) methods.
|
2015-05-31 12:13:59 -07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
Parameters
|
|
|
|
|
----------
|
2016-04-13 22:17:16 -05:00
|
|
|
other : openmc.Tally
|
2016-02-06 15:09:02 -05:00
|
|
|
Tally to check for mergeable filters
|
2015-05-31 12:13:59 -07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
"""
|
2015-05-31 12:13:59 -07:00
|
|
|
|
2016-12-15 20:23:07 +00:00
|
|
|
# Two tallies must have the same number of filters
|
2016-02-06 15:04:56 -05:00
|
|
|
if len(self.filters) != len(other.filters):
|
2015-07-29 00:06:07 -07:00
|
|
|
return False
|
2015-05-31 13:15:02 -07:00
|
|
|
|
2015-10-23 17:27:28 -04:00
|
|
|
# Return False if only one tally has a delayed group filter
|
2026-04-28 17:10:03 -04:00
|
|
|
tally1_dg = self.contains_filter(DelayedGroupFilter)
|
|
|
|
|
tally2_dg = other.contains_filter(DelayedGroupFilter)
|
2020-03-23 16:45:53 -05:00
|
|
|
if tally1_dg != tally2_dg:
|
2015-10-23 17:27:28 -04:00
|
|
|
return False
|
|
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
# Look to see if all filters are the same, or one or more can be merged
|
|
|
|
|
for filter1 in self.filters:
|
2020-03-23 16:45:53 -05:00
|
|
|
mergeable = False
|
2015-05-31 13:15:02 -07:00
|
|
|
|
2016-02-06 15:04:56 -05:00
|
|
|
for filter2 in other.filters:
|
2020-03-23 16:45:53 -05:00
|
|
|
if filter1 == filter2 or filter1.can_merge(filter2):
|
|
|
|
|
mergeable = True
|
2015-07-29 00:06:07 -07:00
|
|
|
break
|
2015-05-31 13:15:02 -07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
# If no mergeable filter was found, the tallies are not mergeable
|
2020-03-23 16:45:53 -05:00
|
|
|
if not mergeable:
|
2015-07-29 00:06:07 -07:00
|
|
|
return False
|
2015-05-31 13:15:02 -07:00
|
|
|
|
2016-02-06 15:04:56 -05:00
|
|
|
# Tally filters are mergeable if all conditional checks passed
|
2015-07-29 00:06:07 -07:00
|
|
|
return True
|
2015-05-31 13:15:02 -07:00
|
|
|
|
2016-02-06 15:04:56 -05:00
|
|
|
def _can_merge_nuclides(self, other):
|
|
|
|
|
"""Determine if another tally's nuclides can be merged with this one's
|
|
|
|
|
|
|
|
|
|
The nuclides between the two tallies must be mutually exclusive or
|
|
|
|
|
identically matching. This is a helper method for the can_merge(...)
|
|
|
|
|
and merge(...) methods.
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
2016-04-13 22:17:16 -05:00
|
|
|
other : openmc.Tally
|
2016-02-06 15:04:56 -05:00
|
|
|
Tally to check for mergeable nuclides
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
no_nuclides_match = True
|
|
|
|
|
all_nuclides_match = True
|
|
|
|
|
|
|
|
|
|
# Search for each of this tally's nuclides in the other tally
|
|
|
|
|
for nuclide in self.nuclides:
|
|
|
|
|
if nuclide not in other.nuclides:
|
|
|
|
|
all_nuclides_match = False
|
|
|
|
|
else:
|
|
|
|
|
no_nuclides_match = False
|
|
|
|
|
|
|
|
|
|
# Search for each of the other tally's nuclides in this tally
|
|
|
|
|
for nuclide in other.nuclides:
|
|
|
|
|
if nuclide not in self.nuclides:
|
|
|
|
|
all_nuclides_match = False
|
|
|
|
|
else:
|
|
|
|
|
no_nuclides_match = False
|
|
|
|
|
|
|
|
|
|
# Either all nuclides should match, or none should
|
2016-07-01 22:57:14 +07:00
|
|
|
return no_nuclides_match or all_nuclides_match
|
2016-02-06 15:04:56 -05:00
|
|
|
|
|
|
|
|
def _can_merge_scores(self, other):
|
|
|
|
|
"""Determine if another tally's scores can be merged with this one's
|
|
|
|
|
|
|
|
|
|
The scores between the two tallies must be mutually exclusive or
|
|
|
|
|
identically matching. This is a helper method for the can_merge(...)
|
|
|
|
|
and merge(...) methods.
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
2016-04-13 22:17:16 -05:00
|
|
|
other : openmc.Tally
|
2016-02-06 15:04:56 -05:00
|
|
|
Tally to check for mergeable scores
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
no_scores_match = True
|
|
|
|
|
all_scores_match = True
|
|
|
|
|
|
|
|
|
|
# Search for each of this tally's scores in the other tally
|
|
|
|
|
for score in self.scores:
|
2016-05-30 14:23:06 -04:00
|
|
|
if score in other.scores:
|
2016-02-06 15:04:56 -05:00
|
|
|
no_scores_match = False
|
|
|
|
|
|
|
|
|
|
# Search for each of the other tally's scores in this tally
|
|
|
|
|
for score in other.scores:
|
|
|
|
|
if score not in self.scores:
|
|
|
|
|
all_scores_match = False
|
|
|
|
|
else:
|
|
|
|
|
no_scores_match = False
|
|
|
|
|
|
2016-08-02 18:53:44 -04:00
|
|
|
if score == 'current' and score not in self.scores:
|
|
|
|
|
return False
|
|
|
|
|
|
2016-02-06 20:53:52 -05:00
|
|
|
# Nuclides cannot be specified on 'flux' scores
|
|
|
|
|
if 'flux' in self.scores or 'flux' in other.scores:
|
|
|
|
|
if self.nuclides != other.nuclides:
|
|
|
|
|
return False
|
|
|
|
|
|
2016-02-06 15:04:56 -05:00
|
|
|
# Either all scores should match, or none should
|
2016-07-01 22:57:14 +07:00
|
|
|
return no_scores_match or all_scores_match
|
2016-02-06 15:04:56 -05:00
|
|
|
|
|
|
|
|
def can_merge(self, other):
|
|
|
|
|
"""Determine if another tally can be merged with this one
|
|
|
|
|
|
2016-02-06 20:53:52 -05:00
|
|
|
If results have been loaded from a statepoint, then tallies are only
|
|
|
|
|
mergeable along one and only one of filter bins, nuclides or scores.
|
|
|
|
|
|
2016-02-06 15:04:56 -05:00
|
|
|
Parameters
|
|
|
|
|
----------
|
2016-04-13 22:17:16 -05:00
|
|
|
other : openmc.Tally
|
2016-02-06 15:04:56 -05:00
|
|
|
Tally to check for merging
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
if not isinstance(other, Tally):
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
# Must have same estimator
|
|
|
|
|
if self.estimator != other.estimator:
|
|
|
|
|
return False
|
|
|
|
|
|
2016-02-07 13:24:14 -05:00
|
|
|
equal_filters = sorted(self.filters) == sorted(other.filters)
|
|
|
|
|
equal_nuclides = sorted(self.nuclides) == sorted(other.nuclides)
|
|
|
|
|
equal_scores = sorted(self.scores) == sorted(other.scores)
|
|
|
|
|
equality = [equal_filters, equal_nuclides, equal_scores]
|
|
|
|
|
|
|
|
|
|
# If all filters, nuclides and scores match then tallies are mergeable
|
2020-03-23 16:45:53 -05:00
|
|
|
if all(equality):
|
2016-02-07 13:24:14 -05:00
|
|
|
return True
|
|
|
|
|
|
2019-12-06 10:18:10 -06:00
|
|
|
# Variables to indicate filter bins, nuclides, and scores that can be merged
|
2020-03-23 16:45:53 -05:00
|
|
|
can_merge_filters = self._can_merge_filters(other)
|
|
|
|
|
can_merge_nuclides = self._can_merge_nuclides(other)
|
|
|
|
|
can_merge_scores = self._can_merge_scores(other)
|
|
|
|
|
mergeability = [can_merge_filters, can_merge_nuclides, can_merge_scores]
|
2016-02-06 15:04:56 -05:00
|
|
|
|
2016-02-07 13:24:14 -05:00
|
|
|
if not all(mergeability):
|
|
|
|
|
return False
|
|
|
|
|
|
2020-03-23 16:45:53 -05:00
|
|
|
# If the tally results have been read from the statepoint, at least two
|
|
|
|
|
# of filters, nuclides and scores must match
|
2016-02-06 20:53:52 -05:00
|
|
|
else:
|
2020-03-23 16:45:53 -05:00
|
|
|
return not self._results_read or sum(equality) >= 2
|
2016-02-06 15:04:56 -05:00
|
|
|
|
2016-02-06 15:09:02 -05:00
|
|
|
def merge(self, other):
|
2015-07-29 00:06:07 -07:00
|
|
|
"""Merge another tally with this one
|
2015-05-31 13:15:02 -07:00
|
|
|
|
2016-02-06 20:53:52 -05:00
|
|
|
If results have been loaded from a statepoint, then tallies are only
|
|
|
|
|
mergeable along one and only one of filter bins, nuclides or scores.
|
2016-02-06 15:04:56 -05:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
Parameters
|
|
|
|
|
----------
|
2016-04-13 22:17:16 -05:00
|
|
|
other : openmc.Tally
|
2015-07-29 00:06:07 -07:00
|
|
|
Tally to merge with this one
|
2015-05-31 13:15:02 -07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
Returns
|
|
|
|
|
-------
|
2016-04-13 22:17:16 -05:00
|
|
|
merged_tally : openmc.Tally
|
2015-07-29 00:06:07 -07:00
|
|
|
Merged tallies
|
2015-05-31 13:26:10 -07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
"""
|
2015-05-31 13:26:10 -07:00
|
|
|
|
2016-02-06 15:04:56 -05:00
|
|
|
if not self.can_merge(other):
|
2021-07-29 20:28:51 +01:00
|
|
|
msg = f'Unable to merge tally ID="{other.id}" with "{self.id}"'
|
2015-07-29 00:06:07 -07:00
|
|
|
raise ValueError(msg)
|
2015-05-31 14:22:42 -07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
# Create deep copy of tally to return as merged tally
|
|
|
|
|
merged_tally = copy.deepcopy(self)
|
2015-05-31 13:26:10 -07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
# Differentiate Tally with a new auto-generated Tally ID
|
|
|
|
|
merged_tally.id = None
|
2015-05-31 16:30:15 -07:00
|
|
|
|
2016-02-06 15:04:56 -05:00
|
|
|
# Create deep copy of other tally to use for array concatenation
|
|
|
|
|
other_copy = copy.deepcopy(other)
|
|
|
|
|
|
2016-02-11 17:17:36 -05:00
|
|
|
# Identify if filters, nuclides and scores are mergeable and/or equal
|
2016-02-07 13:24:14 -05:00
|
|
|
merge_filters = self._can_merge_filters(other)
|
|
|
|
|
merge_nuclides = self._can_merge_nuclides(other)
|
|
|
|
|
merge_scores = self._can_merge_scores(other)
|
|
|
|
|
equal_filters = sorted(self.filters) == sorted(other.filters)
|
|
|
|
|
equal_nuclides = sorted(self.nuclides) == sorted(other.nuclides)
|
|
|
|
|
equal_scores = sorted(self.scores) == sorted(other.scores)
|
|
|
|
|
|
2016-02-06 15:04:56 -05:00
|
|
|
# If two tallies can be merged along a filter's bins
|
2016-02-07 13:24:14 -05:00
|
|
|
if merge_filters and not equal_filters:
|
2016-02-06 15:04:56 -05:00
|
|
|
|
|
|
|
|
# Search for mergeable filters
|
|
|
|
|
for i, filter1 in enumerate(self.filters):
|
2016-07-01 22:57:14 +07:00
|
|
|
for filter2 in other.filters:
|
2016-02-06 15:04:56 -05:00
|
|
|
if filter1 != filter2 and filter1.can_merge(filter2):
|
2016-02-06 20:53:52 -05:00
|
|
|
other_copy._swap_filters(other_copy.filters[i], filter2)
|
|
|
|
|
merged_tally.filters[i] = filter1.merge(filter2)
|
2016-02-07 13:24:14 -05:00
|
|
|
join_right = filter1 < filter2
|
2016-02-06 20:53:52 -05:00
|
|
|
merge_axis = i
|
2016-02-06 15:04:56 -05:00
|
|
|
break
|
2015-05-31 13:15:02 -07:00
|
|
|
|
2016-02-06 15:04:56 -05:00
|
|
|
# If two tallies can be merged along nuclide bins
|
2016-02-07 13:24:14 -05:00
|
|
|
if merge_nuclides and not equal_nuclides:
|
2016-02-06 20:53:52 -05:00
|
|
|
merge_axis = self.num_filters
|
2016-02-07 13:24:14 -05:00
|
|
|
join_right = True
|
2016-02-06 15:04:56 -05:00
|
|
|
|
|
|
|
|
# Add unique nuclides from other tally to merged tally
|
|
|
|
|
for nuclide in other.nuclides:
|
2016-02-06 20:53:52 -05:00
|
|
|
if nuclide not in merged_tally.nuclides:
|
2016-03-09 08:29:55 -06:00
|
|
|
merged_tally.nuclides.append(nuclide)
|
2016-02-06 15:04:56 -05:00
|
|
|
|
|
|
|
|
# If two tallies can be merged along score bins
|
2016-02-07 13:24:14 -05:00
|
|
|
if merge_scores and not equal_scores:
|
2016-02-06 20:53:52 -05:00
|
|
|
merge_axis = self.num_filters + 1
|
2016-02-07 13:24:14 -05:00
|
|
|
join_right = True
|
2016-02-06 15:04:56 -05:00
|
|
|
|
|
|
|
|
# Add unique scores from other tally to merged tally
|
|
|
|
|
for score in other.scores:
|
2016-02-06 20:53:52 -05:00
|
|
|
if score not in merged_tally.scores:
|
2016-03-09 08:29:55 -06:00
|
|
|
merged_tally.scores.append(score)
|
2016-02-06 15:04:56 -05:00
|
|
|
|
2016-02-08 00:36:49 -05:00
|
|
|
# Add triggers from other tally to merged tally
|
|
|
|
|
for trigger in other.triggers:
|
2016-02-02 10:07:08 -06:00
|
|
|
merged_tally.triggers.append(trigger)
|
2015-05-31 13:15:02 -07:00
|
|
|
|
2016-02-08 00:36:49 -05:00
|
|
|
# If results have not been read, then return tally for input generation
|
2016-02-16 10:50:53 -05:00
|
|
|
if self._results_read is None:
|
2016-02-08 00:36:49 -05:00
|
|
|
return merged_tally
|
2016-02-26 19:44:03 -05:00
|
|
|
# Otherwise, this is a derived tally which needs merged results arrays
|
2016-02-08 00:36:49 -05:00
|
|
|
else:
|
|
|
|
|
self._derived = True
|
|
|
|
|
|
2016-02-06 15:04:56 -05:00
|
|
|
# Concatenate sum arrays if present in both tallies
|
|
|
|
|
if self.sum is not None and other_copy.sum is not None:
|
|
|
|
|
self_sum = self.get_reshaped_data(value='sum')
|
|
|
|
|
other_sum = other_copy.get_reshaped_data(value='sum')
|
2016-02-07 13:24:14 -05:00
|
|
|
|
|
|
|
|
if join_right:
|
2018-03-28 15:56:03 -05:00
|
|
|
merged_sum = np.concatenate((self_sum, other_sum),
|
|
|
|
|
axis=merge_axis)
|
2016-02-07 13:24:14 -05:00
|
|
|
else:
|
2018-03-28 15:56:03 -05:00
|
|
|
merged_sum = np.concatenate((other_sum, self_sum),
|
|
|
|
|
axis=merge_axis)
|
2016-02-07 13:24:14 -05:00
|
|
|
|
|
|
|
|
merged_tally._sum = np.reshape(merged_sum, merged_tally.shape)
|
2016-02-06 15:04:56 -05:00
|
|
|
|
|
|
|
|
# Concatenate sum_sq arrays if present in both tallies
|
|
|
|
|
if self.sum_sq is not None and other.sum_sq is not None:
|
|
|
|
|
self_sum_sq = self.get_reshaped_data(value='sum_sq')
|
|
|
|
|
other_sum_sq = other_copy.get_reshaped_data(value='sum_sq')
|
2016-02-07 13:24:14 -05:00
|
|
|
|
|
|
|
|
if join_right:
|
2018-03-28 15:56:03 -05:00
|
|
|
merged_sum_sq = np.concatenate((self_sum_sq, other_sum_sq),
|
|
|
|
|
axis=merge_axis)
|
2016-02-07 13:24:14 -05:00
|
|
|
else:
|
2018-03-28 15:56:03 -05:00
|
|
|
merged_sum_sq = np.concatenate((other_sum_sq, self_sum_sq),
|
|
|
|
|
axis=merge_axis)
|
2016-02-07 13:24:14 -05:00
|
|
|
|
|
|
|
|
merged_tally._sum_sq = np.reshape(merged_sum_sq, merged_tally.shape)
|
2016-02-06 15:04:56 -05:00
|
|
|
|
2025-11-12 11:41:37 -05:00
|
|
|
# Concatenate sum_third arrays if present in both tallies
|
|
|
|
|
if self._sum_third is not None and other._sum_third is not None:
|
|
|
|
|
self_sum_third = self.get_reshaped_data(value="sum_third")
|
|
|
|
|
other_sum_third = other_copy.get_reshaped_data(value="sum_third")
|
|
|
|
|
|
|
|
|
|
if join_right:
|
|
|
|
|
merged_sum_third = np.concatenate((self_sum_third, other_sum_third),
|
|
|
|
|
axis=merge_axis)
|
|
|
|
|
else:
|
|
|
|
|
merged_sum_third = np.concatenate((other_sum_third, self_sum_third),
|
|
|
|
|
axis=merge_axis)
|
|
|
|
|
|
|
|
|
|
merged_tally._sum_third = np.reshape(merged_sum_third, merged_tally.shape)
|
|
|
|
|
|
|
|
|
|
# Concatenate sum_fourth arrays if present in both tallies
|
|
|
|
|
if self._sum_fourth is not None and other._sum_fourth is not None:
|
|
|
|
|
self_sum_fourth = self.get_reshaped_data(value="sum_fourth")
|
|
|
|
|
other_sum_fourth = other_copy.get_reshaped_data(value="sum_fourth")
|
|
|
|
|
|
|
|
|
|
if join_right:
|
|
|
|
|
merged_sum_fourth = np.concatenate((self_sum_fourth, other_sum_fourth),
|
|
|
|
|
axis=merge_axis)
|
|
|
|
|
else:
|
|
|
|
|
merged_sum_fourth = np.concatenate((other_sum_fourth, self_sum_fourth),
|
|
|
|
|
axis=merge_axis)
|
|
|
|
|
|
|
|
|
|
merged_tally._sum_fourth = np.reshape(merged_sum_fourth, merged_tally.shape)
|
|
|
|
|
|
2016-02-06 15:04:56 -05:00
|
|
|
# Concatenate mean arrays if present in both tallies
|
|
|
|
|
if self.mean is not None and other.mean is not None:
|
|
|
|
|
self_mean = self.get_reshaped_data(value='mean')
|
|
|
|
|
other_mean = other_copy.get_reshaped_data(value='mean')
|
2016-02-07 13:24:14 -05:00
|
|
|
|
|
|
|
|
if join_right:
|
2018-03-28 15:56:03 -05:00
|
|
|
merged_mean = np.concatenate((self_mean, other_mean),
|
|
|
|
|
axis=merge_axis)
|
2016-02-07 13:24:14 -05:00
|
|
|
else:
|
2018-03-28 15:56:03 -05:00
|
|
|
merged_mean = np.concatenate((other_mean, self_mean),
|
|
|
|
|
axis=merge_axis)
|
2015-05-31 16:30:15 -07:00
|
|
|
|
2016-02-07 13:24:14 -05:00
|
|
|
merged_tally._mean = np.reshape(merged_mean, merged_tally.shape)
|
2015-05-31 13:15:02 -07:00
|
|
|
|
2016-02-06 15:04:56 -05:00
|
|
|
# Concatenate std. dev. arrays if present in both tallies
|
|
|
|
|
if self.std_dev is not None and other.std_dev is not None:
|
|
|
|
|
self_std_dev = self.get_reshaped_data(value='std_dev')
|
|
|
|
|
other_std_dev = other_copy.get_reshaped_data(value='std_dev')
|
2016-02-07 13:24:14 -05:00
|
|
|
|
|
|
|
|
if join_right:
|
2018-03-28 15:56:03 -05:00
|
|
|
merged_std_dev = np.concatenate((self_std_dev, other_std_dev),
|
|
|
|
|
axis=merge_axis)
|
2016-02-07 13:24:14 -05:00
|
|
|
else:
|
2018-03-28 15:56:03 -05:00
|
|
|
merged_std_dev = np.concatenate((other_std_dev, self_std_dev),
|
|
|
|
|
axis=merge_axis)
|
2016-02-07 13:24:14 -05:00
|
|
|
|
|
|
|
|
merged_tally._std_dev = np.reshape(merged_std_dev, merged_tally.shape)
|
2016-02-06 15:04:56 -05:00
|
|
|
|
2016-02-06 20:53:52 -05:00
|
|
|
# Sparsify merged tally if both tallies are sparse
|
|
|
|
|
merged_tally.sparse = self.sparse and other.sparse
|
2015-05-31 13:15:02 -07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
return merged_tally
|
2015-05-31 16:30:15 -07:00
|
|
|
|
2016-12-08 09:37:53 -05:00
|
|
|
def to_xml_element(self):
|
2015-07-29 00:06:07 -07:00
|
|
|
"""Return XML representation of the tally
|
2015-05-31 13:15:02 -07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
Returns
|
|
|
|
|
-------
|
2023-05-09 11:41:04 -04:00
|
|
|
element : lxml.etree._Element
|
2015-07-29 00:06:07 -07:00
|
|
|
XML element containing tally data
|
2015-05-31 13:15:02 -07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
"""
|
2015-05-31 16:38:09 -07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
element = ET.Element("tally")
|
2015-05-31 16:38:09 -07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
# Tally ID
|
|
|
|
|
element.set("id", str(self.id))
|
2015-05-31 16:38:09 -07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
# Optional Tally name
|
|
|
|
|
if self.name != '':
|
|
|
|
|
element.set("name", self.name)
|
2015-05-31 21:09:14 -07:00
|
|
|
|
2023-06-09 07:24:00 -05:00
|
|
|
# Multiply by density
|
|
|
|
|
if not self.multiply_density:
|
|
|
|
|
element.set("multiply_density", str(self.multiply_density).lower())
|
|
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
# Optional Tally filters
|
2017-03-27 10:03:27 -05:00
|
|
|
if len(self.filters) > 0:
|
|
|
|
|
subelement = ET.SubElement(element, "filters")
|
2017-05-23 13:07:40 -05:00
|
|
|
subelement.text = ' '.join(str(f.id) for f in self.filters)
|
2015-05-31 21:09:14 -07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
# Optional Nuclides
|
2017-11-29 22:52:43 -06:00
|
|
|
if self.nuclides:
|
2015-07-29 00:06:07 -07:00
|
|
|
subelement = ET.SubElement(element, "nuclides")
|
2017-11-29 22:52:43 -06:00
|
|
|
subelement.text = ' '.join(str(n) for n in self.nuclides)
|
2015-07-29 00:06:07 -07:00
|
|
|
|
|
|
|
|
# Scores
|
|
|
|
|
if len(self.scores) == 0:
|
2021-07-29 18:25:37 +01:00
|
|
|
msg = f'Unable to get XML for Tally ID="{self.id}" since it does ' \
|
|
|
|
|
'not contain any scores'
|
2015-07-29 00:06:07 -07:00
|
|
|
raise ValueError(msg)
|
|
|
|
|
|
2022-01-20 17:51:16 -06:00
|
|
|
subelement = ET.SubElement(element, "scores")
|
|
|
|
|
subelement.text = ' '.join(str(x) for x in self.scores)
|
2015-07-29 00:06:07 -07:00
|
|
|
|
|
|
|
|
# Tally estimator type
|
|
|
|
|
if self.estimator is not None:
|
|
|
|
|
subelement = ET.SubElement(element, "estimator")
|
|
|
|
|
subelement.text = self.estimator
|
|
|
|
|
|
|
|
|
|
# Optional Triggers
|
|
|
|
|
for trigger in self.triggers:
|
2022-01-20 17:51:16 -06:00
|
|
|
element.append(trigger.to_xml_element())
|
2015-07-29 00:06:07 -07:00
|
|
|
|
2016-01-29 16:54:07 -05:00
|
|
|
# Optional derivatives
|
|
|
|
|
if self.derivative is not None:
|
|
|
|
|
subelement = ET.SubElement(element, "derivative")
|
|
|
|
|
subelement.text = str(self.derivative.id)
|
|
|
|
|
|
2025-11-12 11:41:37 -05:00
|
|
|
# Optional higher moments accumulation
|
|
|
|
|
if self.higher_moments:
|
|
|
|
|
subelement = ET.SubElement(element, "higher_moments")
|
|
|
|
|
subelement.text = str(self.higher_moments).lower()
|
|
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
return element
|
|
|
|
|
|
2025-01-24 16:49:58 -06:00
|
|
|
def add_results(self, statepoint: cv.PathLike | openmc.StatePoint):
|
|
|
|
|
"""Add results from the provided statepoint file to this tally instance
|
|
|
|
|
|
|
|
|
|
.. versionadded:: 0.15.1
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
|
|
|
|
statepoint : openmc.PathLike or openmc.StatePoint
|
|
|
|
|
Statepoint used to update tally results
|
|
|
|
|
"""
|
2025-02-24 16:11:18 -06:00
|
|
|
# derived tallies are populated with data based on combined tallies
|
|
|
|
|
# and should not be modified
|
|
|
|
|
if self.derived:
|
|
|
|
|
return
|
|
|
|
|
|
2025-01-24 16:49:58 -06:00
|
|
|
if isinstance(statepoint, openmc.StatePoint):
|
2025-02-24 16:11:18 -06:00
|
|
|
self._sp_filename = Path(statepoint._f.filename)
|
2025-01-24 16:49:58 -06:00
|
|
|
else:
|
2025-02-24 16:11:18 -06:00
|
|
|
self._sp_filename = Path(str(statepoint))
|
|
|
|
|
|
|
|
|
|
# reset these properties to ensure that any results access after this
|
|
|
|
|
# point are based on the current statepoint file
|
|
|
|
|
self._sum = None
|
|
|
|
|
self._sum_sq = None
|
2025-11-12 11:41:37 -05:00
|
|
|
self._sum_third = None
|
|
|
|
|
self._sum_fourth = None
|
2025-02-24 16:11:18 -06:00
|
|
|
self._mean = None
|
|
|
|
|
self._std_dev = None
|
2025-11-12 11:41:37 -05:00
|
|
|
self._vov = None
|
|
|
|
|
self._higher_moments = False
|
2025-02-24 16:11:18 -06:00
|
|
|
self._num_realizations = 0
|
|
|
|
|
self._results_read = False
|
2025-01-24 16:49:58 -06:00
|
|
|
|
2022-01-20 17:51:16 -06:00
|
|
|
@classmethod
|
|
|
|
|
def from_xml_element(cls, elem, **kwargs):
|
|
|
|
|
"""Generate tally object from an XML element
|
|
|
|
|
|
2022-01-11 08:18:47 -06:00
|
|
|
.. versionadded:: 0.13.0
|
|
|
|
|
|
2022-01-20 17:51:16 -06:00
|
|
|
Parameters
|
|
|
|
|
----------
|
2023-05-09 11:41:04 -04:00
|
|
|
elem : lxml.etree._Element
|
2022-01-20 17:51:16 -06:00
|
|
|
XML element
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
|
|
|
|
openmc.Tally
|
|
|
|
|
Tally object
|
|
|
|
|
|
|
|
|
|
"""
|
2025-08-08 20:47:55 +03:00
|
|
|
tally_id = int(get_text(elem, "id"))
|
|
|
|
|
name = get_text(elem, "name", "")
|
2022-01-20 17:51:16 -06:00
|
|
|
tally = cls(tally_id=tally_id, name=name)
|
|
|
|
|
|
2023-06-09 07:24:00 -05:00
|
|
|
text = get_text(elem, 'multiply_density')
|
|
|
|
|
if text is not None:
|
|
|
|
|
tally.multiply_density = text in ('true', '1')
|
|
|
|
|
|
2022-01-20 17:51:16 -06:00
|
|
|
# Read filters
|
2025-08-08 20:47:55 +03:00
|
|
|
filter_ids = get_elem_list(elem, "filters", int)
|
|
|
|
|
if filter_ids is not None:
|
2022-01-20 17:51:16 -06:00
|
|
|
tally.filters = [kwargs['filters'][uid] for uid in filter_ids]
|
|
|
|
|
|
|
|
|
|
# Read nuclides
|
2025-08-08 20:47:55 +03:00
|
|
|
nuclides = get_elem_list(elem, "nuclides", str)
|
|
|
|
|
if nuclides is not None:
|
|
|
|
|
tally.nuclides = nuclides
|
2022-01-20 17:51:16 -06:00
|
|
|
|
|
|
|
|
# Read scores
|
2025-08-08 20:47:55 +03:00
|
|
|
scores = get_elem_list(elem, "scores", str)
|
|
|
|
|
if scores is not None:
|
|
|
|
|
tally.scores = scores
|
2022-01-20 17:51:16 -06:00
|
|
|
|
|
|
|
|
# Set estimator
|
2025-08-08 20:47:55 +03:00
|
|
|
estimator = get_text(elem, "estimator")
|
|
|
|
|
if estimator is not None:
|
|
|
|
|
tally.estimator = estimator
|
2022-01-20 17:51:16 -06:00
|
|
|
|
|
|
|
|
# Read triggers
|
|
|
|
|
tally.triggers = [
|
|
|
|
|
openmc.Trigger.from_xml_element(trigger_elem)
|
|
|
|
|
for trigger_elem in elem.findall('trigger')
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
# Read tally derivative
|
2025-08-08 20:47:55 +03:00
|
|
|
deriv = get_text(elem, "derivative")
|
|
|
|
|
if deriv is not None:
|
|
|
|
|
deriv_id = int(deriv)
|
2022-01-20 17:51:16 -06:00
|
|
|
tally.derivative = kwargs['derivatives'][deriv_id]
|
|
|
|
|
|
|
|
|
|
return tally
|
|
|
|
|
|
2016-02-03 17:20:12 -05:00
|
|
|
def contains_filter(self, filter_type):
|
|
|
|
|
"""Looks for a filter in the tally that matches a specified type
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
2016-09-30 12:19:22 -04:00
|
|
|
filter_type : openmc.FilterMeta
|
2016-09-10 23:37:46 -04:00
|
|
|
Type of the filter, e.g. MeshFilter
|
2016-02-03 17:20:12 -05:00
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
|
|
|
|
filter_found : bool
|
|
|
|
|
True if the tally contains a filter of the requested type;
|
|
|
|
|
otherwise false
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
for test_filter in self.filters:
|
2016-09-22 18:51:20 -04:00
|
|
|
if type(test_filter) is filter_type:
|
2020-03-23 16:45:53 -05:00
|
|
|
return True
|
|
|
|
|
return False
|
2016-02-03 17:20:12 -05:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
def find_filter(self, filter_type):
|
|
|
|
|
"""Return a filter in the tally that matches a specified type
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
2016-09-30 12:19:22 -04:00
|
|
|
filter_type : openmc.FilterMeta
|
2016-09-10 22:59:57 -04:00
|
|
|
Type of the filter, e.g. MeshFilter
|
2015-05-31 21:09:14 -07:00
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
2016-04-13 22:17:16 -05:00
|
|
|
filter_found : openmc.Filter
|
2015-07-29 00:06:07 -07:00
|
|
|
Filter from this tally with matching type, or None if no matching
|
|
|
|
|
Filter is found
|
|
|
|
|
|
|
|
|
|
Raises
|
|
|
|
|
------
|
|
|
|
|
ValueError
|
|
|
|
|
If no matching Filter is found
|
|
|
|
|
|
2015-05-31 21:09:14 -07:00
|
|
|
"""
|
|
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
# Look through all of this Tally's Filters for the type requested
|
|
|
|
|
for test_filter in self.filters:
|
2016-09-22 18:51:20 -04:00
|
|
|
if type(test_filter) is filter_type:
|
2020-03-23 16:45:53 -05:00
|
|
|
return test_filter
|
2015-07-29 00:06:07 -07:00
|
|
|
|
2016-09-19 23:22:16 -04:00
|
|
|
# Also check to see if the desired filter is wrapped up in an
|
|
|
|
|
# aggregate
|
2026-04-28 17:10:03 -04:00
|
|
|
elif isinstance(test_filter, AggregateFilter):
|
2016-09-19 23:22:16 -04:00
|
|
|
if isinstance(test_filter.aggregate_filter, filter_type):
|
2020-03-23 16:45:53 -05:00
|
|
|
return test_filter
|
2015-07-29 00:06:07 -07:00
|
|
|
|
|
|
|
|
# If we did not find the Filter, throw an Exception
|
2021-07-29 20:28:51 +01:00
|
|
|
msg = f'Unable to find filter type "{filter_type}" in Tally ' \
|
2021-07-31 22:33:45 +01:00
|
|
|
f'ID="{self.id}"'
|
2020-03-23 16:45:53 -05:00
|
|
|
raise ValueError(msg)
|
2015-07-29 00:06:07 -07:00
|
|
|
|
|
|
|
|
def get_nuclide_index(self, nuclide):
|
|
|
|
|
"""Returns the index in the Tally's results array for a Nuclide bin
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
|
|
|
|
nuclide : str
|
2016-06-09 14:52:58 -05:00
|
|
|
The name of the Nuclide (e.g., 'H1', 'U238')
|
2015-05-31 21:09:14 -07:00
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
2015-07-29 00:06:07 -07:00
|
|
|
nuclide_index : int
|
|
|
|
|
The index in the Tally data array for this nuclide.
|
|
|
|
|
|
|
|
|
|
Raises
|
|
|
|
|
------
|
|
|
|
|
KeyError
|
|
|
|
|
When the argument passed to the 'nuclide' parameter cannot be found
|
|
|
|
|
in the Tally.
|
|
|
|
|
|
2015-05-31 21:09:14 -07:00
|
|
|
"""
|
2025-01-24 16:49:58 -06:00
|
|
|
# Look for the user-requested nuclide in all of the Tally's nuclides
|
2015-07-29 00:06:07 -07:00
|
|
|
for i, test_nuclide in enumerate(self.nuclides):
|
2025-01-24 16:49:58 -06:00
|
|
|
if test_nuclide == nuclide:
|
|
|
|
|
return i
|
2015-07-29 00:06:07 -07:00
|
|
|
|
2021-07-29 18:25:37 +01:00
|
|
|
msg = (f'Unable to get the nuclide index for Tally since "{nuclide}" '
|
|
|
|
|
'is not one of the nuclides')
|
2020-03-23 16:45:53 -05:00
|
|
|
raise KeyError(msg)
|
2015-07-29 00:06:07 -07:00
|
|
|
|
|
|
|
|
def get_score_index(self, score):
|
|
|
|
|
"""Returns the index in the Tally's results array for a score bin
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
|
|
|
|
score : str
|
|
|
|
|
The score string (e.g., 'absorption', 'nu-fission')
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
|
|
|
|
score_index : int
|
|
|
|
|
The index in the Tally data array for this score.
|
|
|
|
|
|
|
|
|
|
Raises
|
|
|
|
|
------
|
|
|
|
|
ValueError
|
|
|
|
|
When the argument passed to the 'score' parameter cannot be found in
|
|
|
|
|
the Tally.
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
score_index = self.scores.index(score)
|
|
|
|
|
|
|
|
|
|
except ValueError:
|
2021-07-29 18:25:37 +01:00
|
|
|
msg = f'Unable to get the score index for Tally since "{score}" ' \
|
|
|
|
|
'is not one of the scores'
|
2015-07-29 00:06:07 -07:00
|
|
|
raise ValueError(msg)
|
|
|
|
|
|
|
|
|
|
return score_index
|
|
|
|
|
|
2015-09-11 00:26:11 -04:00
|
|
|
def get_filter_indices(self, filters=[], filter_bins=[]):
|
2015-10-03 00:48:14 -04:00
|
|
|
"""Get indices into the filter axis of this tally's data arrays.
|
|
|
|
|
|
2015-10-03 02:29:07 -04:00
|
|
|
This is a helper method for the Tally.get_values(...) method to
|
|
|
|
|
extract tally data. This method returns the indices into the filter
|
2015-10-03 00:48:14 -04:00
|
|
|
axis of the tally's data array (axis=0) for particular combinations
|
|
|
|
|
of filters and their corresponding bins.
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
2016-09-30 12:19:22 -04:00
|
|
|
filters : Iterable of openmc.FilterMeta
|
2016-09-10 22:59:57 -04:00
|
|
|
An iterable of filter types
|
|
|
|
|
(e.g., [MeshFilter, EnergyFilter]; default is [])
|
|
|
|
|
filter_bins : Iterable of tuple
|
2016-01-20 18:04:20 -05:00
|
|
|
A list of tuples of filter bins corresponding to the filter_types
|
|
|
|
|
parameter (e.g., [(1,), ((0., 0.625e-6),)]; default is []). Each
|
|
|
|
|
tuple contains bins for the corresponding filter type in the filters
|
2016-09-10 22:59:57 -04:00
|
|
|
parameter. Each bin is an integer ID for Material-, Surface-,
|
|
|
|
|
Cell-, Cellborn-, and Universe- Filters. Each bin is an integer
|
|
|
|
|
for the cell instance ID for DistribcellFilters. Each bin is a
|
|
|
|
|
2-tuple of floats for Energy- and Energyout- Filters corresponding
|
2016-01-20 18:04:20 -05:00
|
|
|
to the energy boundaries of the bin of interest. The bin is an
|
2016-09-10 22:59:57 -04:00
|
|
|
(x,y,z) 3-tuple for MeshFilters corresponding to the mesh cell
|
2016-01-20 18:04:20 -05:00
|
|
|
of interest. The order of the bins in the list must correspond to
|
|
|
|
|
the filter_types parameter.
|
2015-10-03 00:48:14 -04:00
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
2016-04-13 22:17:16 -05:00
|
|
|
numpy.ndarray
|
2015-10-03 00:48:14 -04:00
|
|
|
A NumPy array of the filter indices
|
2015-05-31 21:09:14 -07:00
|
|
|
|
2015-06-01 05:58:10 -07:00
|
|
|
"""
|
|
|
|
|
|
2026-04-28 17:10:03 -04:00
|
|
|
cv.check_type('filters', filters, Iterable, FilterMeta)
|
2016-09-10 22:59:57 -04:00
|
|
|
cv.check_type('filter_bins', filter_bins, Iterable, tuple)
|
2015-07-29 00:06:07 -07:00
|
|
|
|
2020-03-23 16:45:53 -05:00
|
|
|
# If user did not specify any specific Filters, use them all
|
|
|
|
|
if not filters:
|
|
|
|
|
return np.arange(self.num_filter_bins)
|
2015-06-01 05:58:10 -07:00
|
|
|
|
2020-03-23 16:45:53 -05:00
|
|
|
# Initialize empty list of indices for each bin in each Filter
|
|
|
|
|
filter_indices = []
|
2016-12-04 16:42:11 -05:00
|
|
|
|
2020-03-23 16:45:53 -05:00
|
|
|
# Loop over all of the Tally's Filters
|
|
|
|
|
for i, self_filter in enumerate(self.filters):
|
|
|
|
|
# If a user-requested Filter, get the user-requested bins
|
|
|
|
|
for j, test_filter in enumerate(filters):
|
|
|
|
|
if type(self_filter) is test_filter:
|
|
|
|
|
bins = filter_bins[j]
|
2022-02-16 08:39:18 -06:00
|
|
|
indices = np.array([self_filter.get_bin_index(b) for b in bins])
|
2020-03-23 16:45:53 -05:00
|
|
|
break
|
|
|
|
|
else:
|
2022-02-16 08:39:18 -06:00
|
|
|
indices = np.arange(self_filter.num_bins)
|
2015-07-29 00:06:07 -07:00
|
|
|
|
2020-03-23 16:45:53 -05:00
|
|
|
filter_indices.append(indices)
|
2015-06-01 05:58:10 -07:00
|
|
|
|
2020-03-23 16:45:53 -05:00
|
|
|
# Account for stride in each of the previous filters
|
|
|
|
|
for indices in filter_indices[:i]:
|
|
|
|
|
indices *= self_filter.num_bins
|
2015-06-01 05:58:10 -07:00
|
|
|
|
2020-03-23 16:45:53 -05:00
|
|
|
# Apply outer product sum between all filter bin indices
|
|
|
|
|
return list(map(sum, product(*filter_indices)))
|
2015-09-11 00:26:11 -04:00
|
|
|
|
|
|
|
|
def get_nuclide_indices(self, nuclides):
|
2015-10-03 00:48:14 -04:00
|
|
|
"""Get indices into the nuclide axis of this tally's data arrays.
|
|
|
|
|
|
2015-10-03 02:29:07 -04:00
|
|
|
This is a helper method for the Tally.get_values(...) method to
|
|
|
|
|
extract tally data. This method returns the indices into the nuclide
|
2015-10-03 00:48:14 -04:00
|
|
|
axis of the tally's data array (axis=1) for one or more nuclides.
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
|
|
|
|
nuclides : list of str
|
|
|
|
|
A list of nuclide name strings
|
2016-06-09 14:52:58 -05:00
|
|
|
(e.g., ['U235', 'U238']; default is [])
|
2015-10-03 00:48:14 -04:00
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
2016-04-13 22:17:16 -05:00
|
|
|
numpy.ndarray
|
2015-10-03 00:48:14 -04:00
|
|
|
A NumPy array of the nuclide indices
|
2015-09-11 00:26:11 -04:00
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
2017-12-24 16:06:05 +07:00
|
|
|
cv.check_iterable_type('nuclides', nuclides, str)
|
2015-09-11 00:26:11 -04:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
# If user did not specify any specific Nuclides, use them all
|
2020-03-23 16:45:53 -05:00
|
|
|
if not nuclides:
|
|
|
|
|
return np.arange(self.num_nuclides)
|
2015-06-01 05:58:10 -07:00
|
|
|
|
2020-03-23 16:45:53 -05:00
|
|
|
# Determine the score indices from any of the requested scores
|
2020-03-25 06:45:03 -05:00
|
|
|
nuclide_indices = np.zeros_like(nuclides, dtype=int)
|
2020-03-23 16:45:53 -05:00
|
|
|
for i, nuclide in enumerate(nuclides):
|
|
|
|
|
nuclide_indices[i] = self.get_nuclide_index(nuclide)
|
2015-09-11 00:26:11 -04:00
|
|
|
return nuclide_indices
|
|
|
|
|
|
|
|
|
|
def get_score_indices(self, scores):
|
2015-10-03 00:48:14 -04:00
|
|
|
"""Get indices into the score axis of this tally's data arrays.
|
|
|
|
|
|
2015-10-03 02:29:07 -04:00
|
|
|
This is a helper method for the Tally.get_values(...) method to
|
|
|
|
|
extract tally data. This method returns the indices into the score
|
2015-10-03 00:48:14 -04:00
|
|
|
axis of the tally's data array (axis=2) for one or more scores.
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
2020-03-23 16:45:53 -05:00
|
|
|
scores : list of str or openmc.CrossScore
|
2015-10-03 00:48:14 -04:00
|
|
|
A list of one or more score strings
|
|
|
|
|
(e.g., ['absorption', 'nu-fission']; default is [])
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
2016-04-13 22:17:16 -05:00
|
|
|
numpy.ndarray
|
2015-10-03 00:48:14 -04:00
|
|
|
A NumPy array of the score indices
|
2015-09-11 00:26:11 -04:00
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
2015-12-13 13:47:27 -05:00
|
|
|
for score in scores:
|
2026-04-28 17:10:03 -04:00
|
|
|
if not isinstance(score, (str, CrossScore)):
|
2021-07-29 18:25:37 +01:00
|
|
|
msg = f'Unable to get score indices for score "{score}" in ' \
|
2021-07-31 22:33:45 +01:00
|
|
|
f'ID="{self.id}" since it is not a string or CrossScore ' \
|
2021-07-29 18:25:37 +01:00
|
|
|
'Tally'
|
2015-12-13 13:47:27 -05:00
|
|
|
raise ValueError(msg)
|
2015-09-11 00:26:11 -04:00
|
|
|
|
2015-06-01 05:58:10 -07:00
|
|
|
# Determine the score indices from any of the requested scores
|
|
|
|
|
if scores:
|
2018-04-27 19:45:38 -04:00
|
|
|
score_indices = np.zeros(len(scores), dtype=int)
|
2015-07-29 00:06:07 -07:00
|
|
|
for i, score in enumerate(scores):
|
|
|
|
|
score_indices[i] = self.get_score_index(score)
|
2015-06-01 05:58:10 -07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
# If user did not specify any specific scores, use them all
|
|
|
|
|
else:
|
|
|
|
|
score_indices = np.arange(self.num_scores)
|
2015-06-01 05:58:10 -07:00
|
|
|
|
2015-09-11 00:26:11 -04:00
|
|
|
return score_indices
|
|
|
|
|
|
|
|
|
|
def get_values(self, scores=[], filters=[], filter_bins=[],
|
|
|
|
|
nuclides=[], value='mean'):
|
2015-10-03 00:48:14 -04:00
|
|
|
"""Returns one or more tallied values given a list of scores, filters,
|
|
|
|
|
filter bins and nuclides.
|
2015-09-11 00:26:11 -04:00
|
|
|
|
|
|
|
|
This method constructs a 3D NumPy array for the requested Tally data
|
|
|
|
|
indexed by filter bin, nuclide bin, and score index. The method will
|
2015-10-03 00:48:14 -04:00
|
|
|
order the data in the array as specified in the parameter lists.
|
2015-09-11 00:26:11 -04:00
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
2015-10-03 00:48:14 -04:00
|
|
|
scores : list of str
|
2015-09-11 00:26:11 -04:00
|
|
|
A list of one or more score strings
|
|
|
|
|
(e.g., ['absorption', 'nu-fission']; default is [])
|
2016-09-30 12:19:22 -04:00
|
|
|
filters : Iterable of openmc.FilterMeta
|
2016-09-10 22:59:57 -04:00
|
|
|
An iterable of filter types
|
|
|
|
|
(e.g., [MeshFilter, EnergyFilter]; default is [])
|
2015-09-11 00:26:11 -04:00
|
|
|
filter_bins : list of Iterables
|
2016-01-20 18:04:20 -05:00
|
|
|
A list of tuples of filter bins corresponding to the filter_types
|
|
|
|
|
parameter (e.g., [(1,), ((0., 0.625e-6),)]; default is []). Each
|
|
|
|
|
tuple contains bins for the corresponding filter type in the filters
|
|
|
|
|
parameter. Each bins is the integer ID for 'material', 'surface',
|
|
|
|
|
'cell', 'cellborn', and 'universe' Filters. Each bin is an integer
|
|
|
|
|
for the cell instance ID for 'distribcell' Filters. Each bin is a
|
|
|
|
|
2-tuple of floats for 'energy' and 'energyout' filters corresponding
|
|
|
|
|
to the energy boundaries of the bin of interest. The bin is an
|
|
|
|
|
(x,y,z) 3-tuple for 'mesh' filters corresponding to the mesh cell
|
|
|
|
|
of interest. The order of the bins in the list must correspond to
|
|
|
|
|
the filter_types parameter.
|
2015-10-03 00:48:14 -04:00
|
|
|
nuclides : list of str
|
2015-09-11 00:26:11 -04:00
|
|
|
A list of nuclide name strings
|
2016-06-09 14:52:58 -05:00
|
|
|
(e.g., ['U235', 'U238']; default is [])
|
2015-09-11 00:26:11 -04:00
|
|
|
value : str
|
|
|
|
|
A string for the type of value to return - 'mean' (default),
|
|
|
|
|
'std_dev', 'rel_err', 'sum', or 'sum_sq' are accepted
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
2016-04-13 22:17:16 -05:00
|
|
|
float or numpy.ndarray
|
2015-09-11 00:26:11 -04:00
|
|
|
A scalar or NumPy array of the Tally data indexed in the order
|
|
|
|
|
each filter, nuclide and score is listed in the parameters.
|
|
|
|
|
|
|
|
|
|
Raises
|
|
|
|
|
------
|
|
|
|
|
ValueError
|
2015-12-13 01:30:10 -05:00
|
|
|
When this method is called before the Tally is populated with data,
|
|
|
|
|
or the input parameters do not correspond to the Tally's attributes,
|
2015-09-11 00:26:11 -04:00
|
|
|
e.g., if the score(s) do not match those in the Tally.
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
2015-12-13 01:30:10 -05:00
|
|
|
# Ensure that the tally has data
|
2015-09-11 00:26:11 -04:00
|
|
|
if (value == 'mean' and self.mean is None) or \
|
|
|
|
|
(value == 'std_dev' and self.std_dev is None) or \
|
|
|
|
|
(value == 'rel_err' and self.mean is None) or \
|
|
|
|
|
(value == 'sum' and self.sum is None) or \
|
2025-11-12 11:41:37 -05:00
|
|
|
(value == 'sum_sq' and self.sum_sq is None) or \
|
|
|
|
|
(value == "sum_third" and self.sum_third is None) or \
|
|
|
|
|
(value == "sum_fourth" and self.sum_fourth is None):
|
2021-07-29 18:25:37 +01:00
|
|
|
msg = f'The Tally ID="{self.id}" has no data to return'
|
2015-09-11 00:26:11 -04:00
|
|
|
raise ValueError(msg)
|
|
|
|
|
|
|
|
|
|
# Get filter, nuclide and score indices
|
|
|
|
|
filter_indices = self.get_filter_indices(filters, filter_bins)
|
|
|
|
|
nuclide_indices = self.get_nuclide_indices(nuclides)
|
|
|
|
|
score_indices = self.get_score_indices(scores)
|
|
|
|
|
|
2015-07-29 18:47:42 -07:00
|
|
|
# Construct outer product of all three index types with each other
|
2015-07-29 00:06:07 -07:00
|
|
|
indices = np.ix_(filter_indices, nuclide_indices, score_indices)
|
2015-06-01 05:58:10 -07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
# Return the desired result from Tally
|
|
|
|
|
if value == 'mean':
|
|
|
|
|
data = self.mean[indices]
|
|
|
|
|
elif value == 'std_dev':
|
|
|
|
|
data = self.std_dev[indices]
|
|
|
|
|
elif value == 'rel_err':
|
|
|
|
|
data = self.std_dev[indices] / self.mean[indices]
|
|
|
|
|
elif value == 'sum':
|
|
|
|
|
data = self.sum[indices]
|
|
|
|
|
elif value == 'sum_sq':
|
|
|
|
|
data = self.sum_sq[indices]
|
2025-11-12 11:41:37 -05:00
|
|
|
elif value == "sum_third":
|
|
|
|
|
data = self.sum_third[indices]
|
|
|
|
|
elif value == "sum_fourth":
|
|
|
|
|
data = self.sum_fourth[indices]
|
2015-07-29 00:06:07 -07:00
|
|
|
else:
|
2021-07-29 18:25:37 +01:00
|
|
|
msg = f'Unable to return results from Tally ID="{value}" since ' \
|
2021-07-31 22:33:45 +01:00
|
|
|
f'the requested value "{self.id}" is not \'mean\', ' \
|
2025-11-12 11:41:37 -05:00
|
|
|
'\'std_dev\', \'rel_err\', \'sum\', \'sum_sq\', \'sum_third\' or \'sum_fourth\''
|
2015-07-29 00:06:07 -07:00
|
|
|
raise LookupError(msg)
|
2015-06-01 05:58:10 -07:00
|
|
|
|
2015-08-02 01:20:30 -07:00
|
|
|
return data
|
2015-07-29 00:06:07 -07:00
|
|
|
|
2016-03-06 18:55:49 -05:00
|
|
|
def get_pandas_dataframe(self, filters=True, nuclides=True, scores=True,
|
2017-03-06 12:19:26 -06:00
|
|
|
derivative=True, paths=True, float_format='{:.2e}'):
|
2015-07-29 00:06:07 -07:00
|
|
|
"""Build a Pandas DataFrame for the Tally data.
|
2015-06-01 05:58:10 -07:00
|
|
|
|
2015-07-29 18:47:42 -07:00
|
|
|
This method constructs a Pandas DataFrame object for the Tally data
|
2015-07-29 00:06:07 -07:00
|
|
|
with columns annotated by filter, nuclide and score bin information.
|
2015-09-07 15:45:20 -04:00
|
|
|
|
|
|
|
|
This capability has been tested for Pandas >=0.13.1. However, it is
|
|
|
|
|
recommended to use v0.16 or newer versions of Pandas since this method
|
|
|
|
|
uses the Multi-index Pandas feature.
|
2015-06-01 05:58:10 -07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
Parameters
|
|
|
|
|
----------
|
|
|
|
|
filters : bool
|
|
|
|
|
Include columns with filter bin information (default is True).
|
|
|
|
|
nuclides : bool
|
|
|
|
|
Include columns with nuclide bin information (default is True).
|
|
|
|
|
scores : bool
|
|
|
|
|
Include columns with score bin information (default is True).
|
2015-11-08 22:03:30 -05:00
|
|
|
derivative : bool
|
|
|
|
|
Include columns with differential tally info (default is True).
|
2017-03-06 12:19:26 -06:00
|
|
|
paths : bool, optional
|
2016-05-08 13:01:05 -04:00
|
|
|
Construct columns for distribcell tally filters (default is True).
|
|
|
|
|
The geometric information in the Summary object is embedded into a
|
|
|
|
|
Multi-index column with a geometric "path" to each distribcell
|
|
|
|
|
instance.
|
2016-04-13 22:17:16 -05:00
|
|
|
float_format : str
|
2016-02-07 16:06:33 -05:00
|
|
|
All floats in the DataFrame will be formatted using the given
|
|
|
|
|
format string before printing.
|
2015-05-31 11:43:57 -07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
Returns
|
|
|
|
|
-------
|
|
|
|
|
pandas.DataFrame
|
|
|
|
|
A Pandas DataFrame with each column annotated by filter, nuclide and
|
|
|
|
|
score bin information (if these parameters are True), and the mean
|
|
|
|
|
and standard deviation of the Tally's data.
|
2014-11-03 19:38:02 -05:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
Raises
|
|
|
|
|
------
|
|
|
|
|
KeyError
|
2015-07-29 18:47:42 -07:00
|
|
|
When this method is called before the Tally is populated with data
|
2015-05-25 10:11:05 -04:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
"""
|
2014-11-03 19:38:02 -05:00
|
|
|
|
2015-12-13 01:30:10 -05:00
|
|
|
# Ensure that the tally has data
|
2015-08-06 22:53:48 -07:00
|
|
|
if self.mean is None or self.std_dev is None:
|
2021-07-29 18:25:37 +01:00
|
|
|
msg = f'The Tally ID="{self.id}" has no data to return'
|
2015-07-29 00:06:07 -07:00
|
|
|
raise KeyError(msg)
|
2014-11-03 19:38:02 -05:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
# Initialize a pandas dataframe for the tally data
|
|
|
|
|
df = pd.DataFrame()
|
2014-11-03 19:38:02 -05:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
# Find the total length of the tally data array
|
|
|
|
|
data_size = self.mean.size
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
# Build DataFrame columns for filters if user requested them
|
|
|
|
|
if filters:
|
2015-10-03 00:48:14 -04:00
|
|
|
# Append each Filter's DataFrame to the overall DataFrame
|
2017-12-19 15:58:14 +07:00
|
|
|
for f, stride in zip(self.filters, self.filter_strides):
|
|
|
|
|
filter_df = f.get_pandas_dataframe(
|
|
|
|
|
data_size, stride, paths=paths)
|
2015-09-07 15:45:20 -04:00
|
|
|
df = pd.concat([df, filter_df], axis=1)
|
2015-05-05 16:47:05 -04:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
# Include DataFrame column for nuclides if user requested it
|
|
|
|
|
if nuclides:
|
|
|
|
|
nuclides = []
|
2016-02-26 20:38:42 -05:00
|
|
|
column_name = 'nuclide'
|
2015-05-05 16:47:05 -04:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
for nuclide in self.nuclides:
|
2025-01-24 16:49:58 -06:00
|
|
|
if isinstance(nuclide, openmc.AggregateNuclide):
|
2016-02-26 20:38:42 -05:00
|
|
|
nuclides.append(nuclide.name)
|
2021-07-29 18:25:37 +01:00
|
|
|
column_name = f'{nuclide.aggregate_op}(nuclide)'
|
2015-07-29 00:06:07 -07:00
|
|
|
else:
|
|
|
|
|
nuclides.append(nuclide)
|
2015-05-05 16:47:05 -04:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
# Tile the nuclide bins into a DataFrame column
|
|
|
|
|
nuclides = np.repeat(nuclides, len(self.scores))
|
|
|
|
|
tile_factor = data_size / len(nuclides)
|
2016-02-26 20:38:42 -05:00
|
|
|
df[column_name] = np.tile(nuclides, int(tile_factor))
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
# Include column for scores if user requested it
|
|
|
|
|
if scores:
|
2016-02-26 20:38:42 -05:00
|
|
|
scores = []
|
|
|
|
|
column_name = 'score'
|
|
|
|
|
|
|
|
|
|
for score in self.scores:
|
2026-04-28 17:10:03 -04:00
|
|
|
if isinstance(score, (str, CrossScore)):
|
2016-05-09 12:03:12 -04:00
|
|
|
scores.append(str(score))
|
2026-04-28 17:10:03 -04:00
|
|
|
elif isinstance(score, AggregateScore):
|
2016-02-26 20:38:42 -05:00
|
|
|
scores.append(score.name)
|
2021-07-29 18:25:37 +01:00
|
|
|
column_name = f'{score.aggregate_op}(score)'
|
2016-02-26 20:38:42 -05:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
tile_factor = data_size / len(self.scores)
|
2016-02-26 20:38:42 -05:00
|
|
|
df[column_name] = np.tile(scores, int(tile_factor))
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2015-11-08 22:03:30 -05:00
|
|
|
# Include columns for derivatives if user requested it
|
2016-01-29 16:54:07 -05:00
|
|
|
if derivative and (self.derivative is not None):
|
|
|
|
|
df['d_variable'] = self.derivative.variable
|
|
|
|
|
if self.derivative.material is not None:
|
|
|
|
|
df['d_material'] = self.derivative.material
|
|
|
|
|
if self.derivative.nuclide is not None:
|
|
|
|
|
df['d_nuclide'] = self.derivative.nuclide
|
2015-11-08 22:03:30 -05:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
# Append columns with mean, std. dev. for each tally bin
|
|
|
|
|
df['mean'] = self.mean.ravel()
|
|
|
|
|
df['std. dev.'] = self.std_dev.ravel()
|
|
|
|
|
|
|
|
|
|
df = df.dropna(axis=1)
|
2015-09-08 00:13:23 -04:00
|
|
|
|
|
|
|
|
# Expand the columns into Pandas MultiIndices for readability
|
|
|
|
|
if pd.__version__ >= '0.16':
|
2026-01-22 05:57:40 +02:00
|
|
|
columns = copy.deepcopy(list(df.columns.values))
|
2015-09-08 00:13:23 -04:00
|
|
|
|
|
|
|
|
# Convert all elements in columns list to tuples
|
|
|
|
|
for i, column in enumerate(columns):
|
|
|
|
|
if not isinstance(column, tuple):
|
|
|
|
|
columns[i] = (column,)
|
|
|
|
|
|
|
|
|
|
# Make each tuple the same length
|
|
|
|
|
max_len_column = len(max(columns, key=len))
|
|
|
|
|
for i, column in enumerate(columns):
|
|
|
|
|
delta_len = max_len_column - len(column)
|
|
|
|
|
if delta_len > 0:
|
|
|
|
|
new_column = list(column)
|
|
|
|
|
new_column.extend(['']*delta_len)
|
|
|
|
|
columns[i] = tuple(new_column)
|
|
|
|
|
|
2017-11-08 10:24:22 -06:00
|
|
|
# Create and set a MultiIndex for the DataFrame's columns, but only
|
|
|
|
|
# if any column actually is multi-level (e.g., a mesh filter)
|
|
|
|
|
if any(len(c) > 1 for c in columns):
|
|
|
|
|
df.columns = pd.MultiIndex.from_tuples(columns)
|
2015-09-08 00:13:23 -04:00
|
|
|
|
2016-02-07 16:06:33 -05:00
|
|
|
# Modify the df.to_string method so that it prints formatted strings.
|
2025-03-07 14:49:36 -06:00
|
|
|
# Credit to https://stackoverflow.com/users/3657742/chrisb for this trick
|
2016-02-07 16:06:33 -05:00
|
|
|
df.to_string = partial(df.to_string, float_format=float_format.format)
|
|
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
return df
|
|
|
|
|
|
2022-11-11 12:10:10 -05:00
|
|
|
def get_reshaped_data(self, value='mean', expand_dims=False):
|
2015-09-26 16:06:42 -04:00
|
|
|
"""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
|
|
|
|
|
corresponding to filters, nuclides and scores. As a result, tally data
|
2016-04-13 22:17:16 -05:00
|
|
|
can be opaque for a user to directly index (i.e., without use of
|
|
|
|
|
:meth:`openmc.Tally.get_values`) since one must know how to properly use
|
2015-09-26 16:06:42 -04:00
|
|
|
the number of bins and strides for each filter to index into the first
|
|
|
|
|
(filter) dimension.
|
|
|
|
|
|
|
|
|
|
This builds and returns a reshaped version of the tally data array with
|
|
|
|
|
unique dimensions corresponding to each tally filter. For example,
|
2022-11-11 12:10:10 -05:00
|
|
|
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
|
2015-10-03 02:29:07 -04:00
|
|
|
scores. This method will return a version of the data array with the
|
2022-11-11 12:10:10 -05:00
|
|
|
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).
|
2015-09-26 16:06:42 -04:00
|
|
|
|
|
|
|
|
Parameters
|
2015-10-28 08:25:21 -05:00
|
|
|
----------
|
2015-09-26 16:06:42 -04:00
|
|
|
value : str
|
|
|
|
|
A string for the type of value to return - 'mean' (default),
|
|
|
|
|
'std_dev', 'rel_err', 'sum', or 'sum_sq' are accepted
|
2022-11-11 12:10:10 -05:00
|
|
|
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.
|
2015-09-26 16:06:42 -04:00
|
|
|
|
2023-03-26 11:15:36 -05:00
|
|
|
.. versionadded:: 0.13.3
|
|
|
|
|
|
2015-09-26 16:06:42 -04:00
|
|
|
Returns
|
|
|
|
|
-------
|
2016-04-13 22:17:16 -05:00
|
|
|
numpy.ndarray
|
2015-10-03 00:48:14 -04:00
|
|
|
The tally data array indexed by filters, nuclides and scores.
|
2015-09-26 16:06:42 -04:00
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
# Get the 3D array of data in filters, nuclides and scores
|
|
|
|
|
data = self.get_values(value=value)
|
|
|
|
|
|
2022-11-11 12:10:10 -05:00
|
|
|
# Build a new array shape with one dimension per filter or expand
|
|
|
|
|
# multidimensional filters if desired
|
2022-10-31 12:47:13 -04:00
|
|
|
new_shape = tuple()
|
|
|
|
|
idx0 = None
|
|
|
|
|
for i, f in enumerate(self.filters):
|
2022-11-11 12:10:10 -05:00
|
|
|
if expand_dims:
|
|
|
|
|
# Mesh filter indices are backwards so we need to flip them
|
2026-04-28 17:10:03 -04:00
|
|
|
if type(f) in {MeshFilter, MeshBornFilter}:
|
2022-11-11 12:10:10 -05:00
|
|
|
fshape = f.shape[::-1]
|
|
|
|
|
new_shape += fshape
|
|
|
|
|
idx0, idx1 = i, i + len(fshape) - 1
|
|
|
|
|
else:
|
|
|
|
|
new_shape += f.shape
|
2022-10-31 12:47:13 -04:00
|
|
|
else:
|
2022-11-11 12:10:10 -05:00
|
|
|
new_shape += (np.prod(f.shape),)
|
2022-10-31 12:47:13 -04:00
|
|
|
|
2018-04-27 19:45:38 -04:00
|
|
|
new_shape += (self.num_nuclides, self.num_scores)
|
2015-09-26 16:06:42 -04:00
|
|
|
|
|
|
|
|
# Reshape the data with one dimension for each filter
|
|
|
|
|
data = np.reshape(data, new_shape)
|
2022-11-11 12:10:10 -05:00
|
|
|
|
|
|
|
|
# If we had a MeshFilter we should swap the axes to have the same shape
|
|
|
|
|
# for the data and the filter
|
2022-10-31 12:47:13 -04:00
|
|
|
if idx0 is not None:
|
|
|
|
|
data = np.swapaxes(data, idx0, idx1)
|
2022-11-11 12:10:10 -05:00
|
|
|
|
2015-09-26 16:06:42 -04:00
|
|
|
return data
|
|
|
|
|
|
2015-12-13 13:47:27 -05:00
|
|
|
def hybrid_product(self, other, binary_op, filter_product=None,
|
2016-07-01 22:57:14 +07:00
|
|
|
nuclide_product=None, score_product=None):
|
2015-07-29 18:47:42 -07:00
|
|
|
"""Combines filters, scores and nuclides with another tally.
|
|
|
|
|
|
2015-12-22 13:02:45 -08:00
|
|
|
This is a helper method for the tally arithmetic operator overloaded
|
|
|
|
|
methods. It is called a "hybrid product" because it performs a
|
|
|
|
|
combination of tensor (or Kronecker) and entrywise (or Hadamard)
|
|
|
|
|
products. The filters from both tallies are combined using an entrywise
|
2015-12-10 00:53:01 -05:00
|
|
|
(or Hadamard) product on matching filters. By default, if all nuclides
|
|
|
|
|
are identical in the two tallies, the entrywise product is performed
|
2015-12-22 13:02:45 -08:00
|
|
|
across nuclides; else the tensor product is performed. By default, if
|
|
|
|
|
all scores are identical in the two tallies, the entrywise product is
|
|
|
|
|
performed across scores; else the tensor product is performed. Users
|
|
|
|
|
can also call the method explicitly and specify the desired product.
|
2015-07-29 00:39:34 -07:00
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
2016-04-13 22:17:16 -05:00
|
|
|
other : openmc.Tally
|
2015-12-09 10:41:19 -05:00
|
|
|
The tally on the right hand side of the hybrid product
|
2015-08-06 23:03:50 -07:00
|
|
|
binary_op : {'+', '-', '*', '/', '^'}
|
2015-12-09 10:41:19 -05:00
|
|
|
The binary operation in the hybrid product
|
2015-12-22 13:02:45 -08:00
|
|
|
filter_product : {'tensor', 'entrywise' or None}
|
2015-12-10 00:53:01 -05:00
|
|
|
The type of product (tensor or entrywise) to be performed between
|
|
|
|
|
filter data. The default is the entrywise product. Currently only
|
|
|
|
|
the entrywise product is supported since a tally cannot contain
|
2015-12-19 12:08:03 -08:00
|
|
|
two of the same filter.
|
2015-12-22 13:02:45 -08:00
|
|
|
nuclide_product : {'tensor', 'entrywise' or None}
|
2015-12-10 00:53:01 -05:00
|
|
|
The type of product (tensor or entrywise) to be performed between
|
|
|
|
|
nuclide data. The default is the entrywise product if all nuclides
|
|
|
|
|
between the two tallies are the same; otherwise the default is
|
|
|
|
|
the tensor product.
|
2015-12-22 13:02:45 -08:00
|
|
|
score_product : {'tensor', 'entrywise' or None}
|
2015-12-10 00:53:01 -05:00
|
|
|
The type of product (tensor or entrywise) to be performed between
|
|
|
|
|
score data. The default is the entrywise product if all scores
|
|
|
|
|
between the two tallies are the same; otherwise the default is
|
|
|
|
|
the tensor product.
|
2015-07-29 18:47:42 -07:00
|
|
|
|
2015-08-06 23:41:38 -07:00
|
|
|
Returns
|
|
|
|
|
-------
|
2016-04-13 22:17:16 -05:00
|
|
|
openmc.Tally
|
2015-12-09 10:41:19 -05:00
|
|
|
A new Tally that is the hybrid product with this one.
|
2015-08-06 23:41:38 -07:00
|
|
|
|
|
|
|
|
Raises
|
|
|
|
|
------
|
|
|
|
|
ValueError
|
2015-08-15 09:45:14 +07:00
|
|
|
When this method is called before the other tally is populated
|
2015-12-13 01:30:10 -05:00
|
|
|
with data.
|
2015-08-06 23:41:38 -07:00
|
|
|
|
2015-07-29 00:39:34 -07:00
|
|
|
"""
|
|
|
|
|
|
2015-12-10 00:53:01 -05:00
|
|
|
# Set default value for filter product if it was not set
|
2015-12-13 13:47:27 -05:00
|
|
|
if filter_product is None:
|
2015-12-10 00:53:01 -05:00
|
|
|
filter_product = 'entrywise'
|
|
|
|
|
elif filter_product == 'tensor':
|
|
|
|
|
msg = 'Unable to perform Tally arithmetic with a tensor product' \
|
2015-12-22 13:02:45 -08:00
|
|
|
'for the filter data as this is not currently supported.'
|
2015-12-10 00:53:01 -05:00
|
|
|
raise ValueError(msg)
|
|
|
|
|
|
|
|
|
|
# Set default value for nuclide product if it was not set
|
2015-12-13 13:47:27 -05:00
|
|
|
if nuclide_product is None:
|
2015-12-10 00:53:01 -05:00
|
|
|
if self.nuclides == other.nuclides:
|
|
|
|
|
nuclide_product = 'entrywise'
|
|
|
|
|
else:
|
|
|
|
|
nuclide_product = 'tensor'
|
|
|
|
|
|
|
|
|
|
# Set default value for score product if it was not set
|
2015-12-13 13:47:27 -05:00
|
|
|
if score_product is None:
|
2015-12-10 00:53:01 -05:00
|
|
|
if self.scores == other.scores:
|
|
|
|
|
score_product = 'entrywise'
|
|
|
|
|
else:
|
|
|
|
|
score_product = 'tensor'
|
|
|
|
|
|
|
|
|
|
# Check product types
|
|
|
|
|
cv.check_value('filter product', filter_product, _PRODUCT_TYPES)
|
|
|
|
|
cv.check_value('nuclide product', nuclide_product, _PRODUCT_TYPES)
|
|
|
|
|
cv.check_value('score product', score_product, _PRODUCT_TYPES)
|
|
|
|
|
|
2015-08-06 23:41:38 -07:00
|
|
|
# Check that results have been read
|
|
|
|
|
if not other.derived and other.sum is None:
|
2021-07-29 18:25:37 +01:00
|
|
|
msg = f'Unable to use tally arithmetic with Tally ' \
|
2021-07-31 22:33:45 +01:00
|
|
|
f'ID="{other.id}" since it does not contain any results.'
|
2015-08-06 23:41:38 -07:00
|
|
|
raise ValueError(msg)
|
|
|
|
|
|
2015-09-11 00:26:11 -04:00
|
|
|
new_tally = Tally()
|
2015-08-11 18:44:22 -07:00
|
|
|
new_tally._derived = True
|
2016-01-14 15:08:28 -05:00
|
|
|
new_tally.with_batch_statistics = True
|
|
|
|
|
new_tally._num_realizations = self.num_realizations
|
|
|
|
|
new_tally._estimator = self.estimator
|
|
|
|
|
new_tally._with_summary = self.with_summary
|
|
|
|
|
new_tally._sp_filename = self._sp_filename
|
2015-08-06 23:41:38 -07:00
|
|
|
|
2015-10-03 00:48:14 -04:00
|
|
|
# Construct a combined derived name from the two tally operands
|
2015-09-11 00:26:11 -04:00
|
|
|
if self.name != '' and other.name != '':
|
2021-07-29 18:25:37 +01:00
|
|
|
new_name = f'({self.name} {binary_op} {other.name})'
|
2015-09-11 00:26:11 -04:00
|
|
|
new_tally.name = new_name
|
|
|
|
|
|
2015-12-10 00:53:01 -05:00
|
|
|
# Query the mean and std dev so the tally data is read in from file
|
2015-12-10 01:03:17 -05:00
|
|
|
# if it has not already been read in.
|
2015-12-13 13:47:27 -05:00
|
|
|
self.mean, self.std_dev, other.mean, other.std_dev
|
2015-12-10 00:53:01 -05:00
|
|
|
|
2015-11-20 13:06:20 -05:00
|
|
|
# Create copies of self and other tallies to rearrange for tally
|
|
|
|
|
# arithmetic
|
|
|
|
|
self_copy = copy.deepcopy(self)
|
|
|
|
|
other_copy = copy.deepcopy(other)
|
|
|
|
|
|
2015-12-13 11:14:01 -05:00
|
|
|
self_copy.sparse = False
|
|
|
|
|
other_copy.sparse = False
|
2015-09-11 00:26:11 -04:00
|
|
|
|
2015-12-10 00:53:01 -05:00
|
|
|
# Align the tally data based on desired hybrid product
|
|
|
|
|
data = self_copy._align_tally_data(other_copy, filter_product,
|
|
|
|
|
nuclide_product, score_product)
|
2015-08-06 23:41:38 -07:00
|
|
|
|
2015-12-10 00:53:01 -05:00
|
|
|
# Perform tally arithmetic operation
|
2015-08-06 23:41:38 -07:00
|
|
|
if binary_op == '+':
|
|
|
|
|
new_tally._mean = data['self']['mean'] + data['other']['mean']
|
|
|
|
|
new_tally._std_dev = np.sqrt(data['self']['std. dev.']**2 +
|
|
|
|
|
data['other']['std. dev.']**2)
|
|
|
|
|
elif binary_op == '-':
|
|
|
|
|
new_tally._mean = data['self']['mean'] - data['other']['mean']
|
|
|
|
|
new_tally._std_dev = np.sqrt(data['self']['std. dev.']**2 +
|
|
|
|
|
data['other']['std. dev.']**2)
|
|
|
|
|
elif binary_op == '*':
|
2018-03-16 14:07:38 -05:00
|
|
|
with np.errstate(divide='ignore', invalid='ignore'):
|
|
|
|
|
self_rel_err = data['self']['std. dev.'] / data['self']['mean']
|
|
|
|
|
other_rel_err = data['other']['std. dev.'] / data['other']['mean']
|
2015-08-06 23:41:38 -07:00
|
|
|
new_tally._mean = data['self']['mean'] * data['other']['mean']
|
|
|
|
|
new_tally._std_dev = np.abs(new_tally.mean) * \
|
|
|
|
|
np.sqrt(self_rel_err**2 + other_rel_err**2)
|
|
|
|
|
elif binary_op == '/':
|
2018-03-16 14:07:38 -05:00
|
|
|
with np.errstate(divide='ignore', invalid='ignore'):
|
|
|
|
|
self_rel_err = data['self']['std. dev.'] / data['self']['mean']
|
|
|
|
|
other_rel_err = data['other']['std. dev.'] / data['other']['mean']
|
|
|
|
|
new_tally._mean = data['self']['mean'] / data['other']['mean']
|
2015-08-06 23:41:38 -07:00
|
|
|
new_tally._std_dev = np.abs(new_tally.mean) * \
|
|
|
|
|
np.sqrt(self_rel_err**2 + other_rel_err**2)
|
|
|
|
|
elif binary_op == '^':
|
2018-03-16 14:07:38 -05:00
|
|
|
with np.errstate(divide='ignore', invalid='ignore'):
|
|
|
|
|
mean_ratio = data['other']['mean'] / data['self']['mean']
|
2015-08-06 23:41:38 -07:00
|
|
|
first_term = mean_ratio * data['self']['std. dev.']
|
|
|
|
|
second_term = \
|
|
|
|
|
np.log(data['self']['mean']) * data['other']['std. dev.']
|
|
|
|
|
new_tally._mean = data['self']['mean'] ** data['other']['mean']
|
|
|
|
|
new_tally._std_dev = np.abs(new_tally.mean) * \
|
|
|
|
|
np.sqrt(first_term**2 + second_term**2)
|
2015-07-29 18:47:42 -07:00
|
|
|
|
2015-12-10 00:53:01 -05:00
|
|
|
# Convert any infs and nans to zero
|
|
|
|
|
new_tally._mean[np.isinf(new_tally._mean)] = 0
|
|
|
|
|
new_tally._mean = np.nan_to_num(new_tally._mean)
|
|
|
|
|
new_tally._std_dev[np.isinf(new_tally._std_dev)] = 0
|
|
|
|
|
new_tally._std_dev = np.nan_to_num(new_tally._std_dev)
|
|
|
|
|
|
|
|
|
|
# Set tally attributes
|
2015-11-20 13:06:20 -05:00
|
|
|
if self_copy.estimator == other_copy.estimator:
|
|
|
|
|
new_tally.estimator = self_copy.estimator
|
|
|
|
|
if self_copy.with_summary and other_copy.with_summary:
|
|
|
|
|
new_tally.with_summary = self_copy.with_summary
|
|
|
|
|
if self_copy.num_realizations == other_copy.num_realizations:
|
|
|
|
|
new_tally.num_realizations = self_copy.num_realizations
|
2015-07-29 00:39:34 -07:00
|
|
|
|
2015-12-10 00:53:01 -05:00
|
|
|
# Add filters to the new tally
|
|
|
|
|
if filter_product == 'entrywise':
|
2015-11-20 13:06:20 -05:00
|
|
|
for self_filter in self_copy.filters:
|
2016-02-02 10:07:08 -06:00
|
|
|
new_tally.filters.append(self_filter)
|
2015-07-29 00:39:34 -07:00
|
|
|
else:
|
2015-12-10 00:53:01 -05:00
|
|
|
all_filters = [self_copy.filters, other_copy.filters]
|
2017-10-04 09:53:47 -05:00
|
|
|
for self_filter, other_filter in product(*all_filters):
|
2026-04-28 17:10:03 -04:00
|
|
|
new_filter = CrossFilter(self_filter, other_filter,
|
2016-09-30 12:19:22 -04:00
|
|
|
binary_op)
|
2016-02-02 10:07:08 -06:00
|
|
|
new_tally.filters.append(new_filter)
|
2015-09-11 00:26:11 -04:00
|
|
|
|
2015-12-10 00:53:01 -05:00
|
|
|
# Add nuclides to the new tally
|
|
|
|
|
if nuclide_product == 'entrywise':
|
|
|
|
|
for self_nuclide in self_copy.nuclides:
|
2016-02-02 10:07:08 -06:00
|
|
|
new_tally.nuclides.append(self_nuclide)
|
2015-12-10 00:53:01 -05:00
|
|
|
else:
|
|
|
|
|
all_nuclides = [self_copy.nuclides, other_copy.nuclides]
|
2017-10-04 09:53:47 -05:00
|
|
|
for self_nuclide, other_nuclide in product(*all_nuclides):
|
2026-04-28 17:10:03 -04:00
|
|
|
new_nuclide = CrossNuclide(self_nuclide, other_nuclide,
|
2017-10-04 09:53:47 -05:00
|
|
|
binary_op)
|
2016-02-02 10:07:08 -06:00
|
|
|
new_tally.nuclides.append(new_nuclide)
|
2015-09-11 00:26:11 -04:00
|
|
|
|
2017-10-04 09:53:47 -05:00
|
|
|
# Define helper function that handles score units appropriately
|
|
|
|
|
# depending on the binary operator
|
|
|
|
|
def cross_score(score1, score2, binary_op):
|
|
|
|
|
if binary_op == '+' or binary_op == '-':
|
|
|
|
|
if score1 == score2:
|
|
|
|
|
return score1
|
|
|
|
|
else:
|
2026-04-28 17:10:03 -04:00
|
|
|
return CrossScore(score1, score2, binary_op)
|
2017-10-04 09:53:47 -05:00
|
|
|
else:
|
2026-04-28 17:10:03 -04:00
|
|
|
return CrossScore(score1, score2, binary_op)
|
2017-10-04 09:53:47 -05:00
|
|
|
|
2015-12-10 00:53:01 -05:00
|
|
|
# Add scores to the new tally
|
|
|
|
|
if score_product == 'entrywise':
|
2015-11-20 13:06:20 -05:00
|
|
|
for self_score in self_copy.scores:
|
2017-10-04 09:53:47 -05:00
|
|
|
new_score = cross_score(self_score, self_score, binary_op)
|
|
|
|
|
new_tally.scores.append(new_score)
|
2015-07-29 00:39:34 -07:00
|
|
|
else:
|
2015-11-20 13:06:20 -05:00
|
|
|
all_scores = [self_copy.scores, other_copy.scores]
|
2017-10-04 09:53:47 -05:00
|
|
|
for self_score, other_score in product(*all_scores):
|
|
|
|
|
new_score = cross_score(self_score, other_score, binary_op)
|
2016-02-02 10:07:08 -06:00
|
|
|
new_tally.scores.append(new_score)
|
2015-07-29 00:39:34 -07:00
|
|
|
|
2015-08-06 23:41:38 -07:00
|
|
|
return new_tally
|
|
|
|
|
|
2017-12-19 15:58:14 +07:00
|
|
|
@property
|
|
|
|
|
def filter_strides(self):
|
|
|
|
|
all_strides = []
|
2016-01-14 15:08:28 -05:00
|
|
|
stride = self.num_nuclides * self.num_scores
|
2016-01-14 18:03:20 -05:00
|
|
|
for self_filter in reversed(self.filters):
|
2017-12-19 15:58:14 +07:00
|
|
|
all_strides.append(stride)
|
2016-01-14 18:03:20 -05:00
|
|
|
stride *= self_filter.num_bins
|
2017-12-19 15:58:14 +07:00
|
|
|
return all_strides[::-1]
|
2016-01-14 15:08:28 -05:00
|
|
|
|
2015-12-10 00:53:01 -05:00
|
|
|
def _align_tally_data(self, other, filter_product, nuclide_product,
|
|
|
|
|
score_product):
|
2015-07-29 18:47:42 -07:00
|
|
|
"""Aligns data from two tallies for tally arithmetic.
|
|
|
|
|
|
|
|
|
|
This is a helper method to construct a dict of dicts of the "aligned"
|
|
|
|
|
data arrays from each tally for tally arithmetic. The method analyzes
|
2015-12-09 10:41:19 -05:00
|
|
|
the filters, scores and nuclides in both tallies and determines how to
|
2015-07-29 18:47:42 -07:00
|
|
|
appropriately align the data for vectorized arithmetic. For example,
|
|
|
|
|
if the two tallies have different filters, this method will use NumPy
|
|
|
|
|
'tile' and 'repeat' operations to the new data arrays such that all
|
|
|
|
|
possible combinations of the data in each tally's bins will be made
|
|
|
|
|
when the arithmetic operation is applied to the arrays.
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
2016-04-13 22:17:16 -05:00
|
|
|
other : openmc.Tally
|
2015-07-29 18:47:42 -07:00
|
|
|
The tally to outer product with this tally
|
2015-12-22 13:02:45 -08:00
|
|
|
filter_product : {'entrywise'}
|
|
|
|
|
The type of product to be performed between filter data. Currently,
|
|
|
|
|
only the entrywise product is supported for the filter product.
|
|
|
|
|
nuclide_product : {'tensor', 'entrywise'}
|
2015-12-10 01:03:17 -05:00
|
|
|
The type of product (tensor or entrywise) to be performed between
|
|
|
|
|
nuclide data.
|
2015-12-22 13:02:45 -08:00
|
|
|
score_product : {'tensor', 'entrywise'}
|
2015-12-10 01:03:17 -05:00
|
|
|
The type of product (tensor or entrywise) to be performed between
|
|
|
|
|
score data.
|
2015-07-29 18:47:42 -07:00
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
|
|
|
|
dict
|
|
|
|
|
A dictionary of dictionaries to "aligned" 'mean' and 'std. dev'
|
|
|
|
|
NumPy arrays for each tally's data.
|
|
|
|
|
|
|
|
|
|
"""
|
2015-06-13 17:21:41 +07:00
|
|
|
|
2015-12-09 10:41:19 -05:00
|
|
|
# Get the set of filters that each tally is missing
|
2017-11-29 10:42:21 -06:00
|
|
|
other_missing_filters = set(self.filters) - set(other.filters)
|
|
|
|
|
self_missing_filters = set(other.filters) - set(self.filters)
|
2015-12-09 10:41:19 -05:00
|
|
|
|
2015-12-10 00:53:01 -05:00
|
|
|
# Add filters present in self but not in other to other
|
2016-01-14 18:03:20 -05:00
|
|
|
for other_filter in other_missing_filters:
|
|
|
|
|
filter_copy = copy.deepcopy(other_filter)
|
|
|
|
|
other._mean = np.repeat(other.mean, filter_copy.num_bins, axis=0)
|
|
|
|
|
other._std_dev = np.repeat(other.std_dev, filter_copy.num_bins, axis=0)
|
2016-02-02 10:07:08 -06:00
|
|
|
other.filters.append(filter_copy)
|
2015-12-09 10:41:19 -05:00
|
|
|
|
2015-12-10 00:53:01 -05:00
|
|
|
# Add filters present in other but not in self to self
|
2016-01-14 18:03:20 -05:00
|
|
|
for self_filter in self_missing_filters:
|
|
|
|
|
filter_copy = copy.deepcopy(self_filter)
|
|
|
|
|
self._mean = np.repeat(self.mean, filter_copy.num_bins, axis=0)
|
|
|
|
|
self._std_dev = np.repeat(self.std_dev, filter_copy.num_bins, axis=0)
|
2016-02-02 10:07:08 -06:00
|
|
|
self.filters.append(filter_copy)
|
2015-12-09 10:41:19 -05:00
|
|
|
|
|
|
|
|
# Align other filters with self filters
|
2016-01-14 18:03:20 -05:00
|
|
|
for i, self_filter in enumerate(self.filters):
|
|
|
|
|
other_index = other.filters.index(self_filter)
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2015-12-09 10:41:19 -05:00
|
|
|
# If necessary, swap other filter
|
|
|
|
|
if other_index != i:
|
2016-01-14 18:03:20 -05:00
|
|
|
other._swap_filters(self_filter, other.filters[i])
|
2015-09-11 00:26:11 -04:00
|
|
|
|
2015-12-10 00:53:01 -05:00
|
|
|
# Repeat and tile the data by nuclide in preparation for performing
|
|
|
|
|
# the tensor product across nuclides.
|
|
|
|
|
if nuclide_product == 'tensor':
|
2017-11-29 10:42:21 -06:00
|
|
|
self._mean = np.repeat(self.mean, other.num_nuclides, axis=1)
|
|
|
|
|
self._std_dev = np.repeat(self.std_dev, other.num_nuclides, axis=1)
|
|
|
|
|
other._mean = np.tile(other.mean, (1, self.num_nuclides, 1))
|
|
|
|
|
other._std_dev = np.tile(other.std_dev, (1, self.num_nuclides, 1))
|
2015-12-10 00:53:01 -05:00
|
|
|
|
|
|
|
|
# Add nuclides to each tally such that each tally contains the complete
|
2016-02-02 10:07:08 -06:00
|
|
|
# set of nuclides necessary to perform an entrywise product. New
|
2015-12-13 11:14:01 -05:00
|
|
|
# nuclides added to a tally will have all their scores set to zero.
|
2015-12-10 00:53:01 -05:00
|
|
|
else:
|
|
|
|
|
|
|
|
|
|
# Get the set of nuclides that each tally is missing
|
2017-11-29 10:42:21 -06:00
|
|
|
other_missing_nuclides = set(self.nuclides) - set(other.nuclides)
|
|
|
|
|
self_missing_nuclides = set(other.nuclides) - set(self.nuclides)
|
2015-12-10 00:53:01 -05:00
|
|
|
|
|
|
|
|
# Add nuclides present in self but not in other to other
|
|
|
|
|
for nuclide in other_missing_nuclides:
|
2017-11-29 10:42:21 -06:00
|
|
|
other._mean = np.insert(other.mean, other.num_nuclides, 0, axis=1)
|
|
|
|
|
other._std_dev = np.insert(other.std_dev, other.num_nuclides, 0,
|
|
|
|
|
axis=1)
|
2016-02-02 10:07:08 -06:00
|
|
|
other.nuclides.append(nuclide)
|
2015-12-10 00:53:01 -05:00
|
|
|
|
|
|
|
|
# Add nuclides present in other but not in self to self
|
|
|
|
|
for nuclide in self_missing_nuclides:
|
2017-11-29 10:42:21 -06:00
|
|
|
self._mean = np.insert(self.mean, self.num_nuclides, 0, axis=1)
|
|
|
|
|
self._std_dev = np.insert(self.std_dev, self.num_nuclides, 0,
|
|
|
|
|
axis=1)
|
2016-02-02 10:07:08 -06:00
|
|
|
self.nuclides.append(nuclide)
|
2015-12-10 00:53:01 -05:00
|
|
|
|
|
|
|
|
# Align other nuclides with self nuclides
|
|
|
|
|
for i, nuclide in enumerate(self.nuclides):
|
2015-12-13 13:47:27 -05:00
|
|
|
other_index = other.get_nuclide_index(nuclide)
|
2015-12-10 00:53:01 -05:00
|
|
|
|
|
|
|
|
# If necessary, swap other nuclide
|
|
|
|
|
if other_index != i:
|
2015-12-13 13:47:27 -05:00
|
|
|
other._swap_nuclides(nuclide, other.nuclides[i])
|
2015-12-10 00:53:01 -05:00
|
|
|
|
|
|
|
|
# Repeat and tile the data by score in preparation for performing
|
|
|
|
|
# the tensor product across scores.
|
|
|
|
|
if score_product == 'tensor':
|
2015-12-22 14:10:13 -08:00
|
|
|
self._mean = np.repeat(self.mean, other.num_scores, axis=2)
|
|
|
|
|
self._std_dev = np.repeat(self.std_dev, other.num_scores, axis=2)
|
|
|
|
|
other._mean = np.tile(other.mean, (1, 1, self.num_scores))
|
|
|
|
|
other._std_dev = np.tile(other.std_dev, (1, 1, self.num_scores))
|
2015-12-10 00:53:01 -05:00
|
|
|
|
|
|
|
|
# Add scores to each tally such that each tally contains the complete set
|
|
|
|
|
# of scores necessary to perform an entrywise product. New scores added
|
|
|
|
|
# to a tally will be set to zero.
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
|
|
# Get the set of scores that each tally is missing
|
2017-11-29 10:42:21 -06:00
|
|
|
other_missing_scores = set(self.scores) - set(other.scores)
|
|
|
|
|
self_missing_scores = set(other.scores) - set(self.scores)
|
2015-12-10 00:53:01 -05:00
|
|
|
|
|
|
|
|
# Add scores present in self but not in other to other
|
|
|
|
|
for score in other_missing_scores:
|
2015-12-22 14:10:13 -08:00
|
|
|
other._mean = np.insert(other.mean, other.num_scores, 0, axis=2)
|
|
|
|
|
other._std_dev = np.insert(other.std_dev, other.num_scores, 0, axis=2)
|
2016-02-02 10:07:08 -06:00
|
|
|
other.scores.append(score)
|
2015-12-10 00:53:01 -05:00
|
|
|
|
|
|
|
|
# Add scores present in other but not in self to self
|
|
|
|
|
for score in self_missing_scores:
|
2015-12-22 14:10:13 -08:00
|
|
|
self._mean = np.insert(self.mean, self.num_scores, 0, axis=2)
|
|
|
|
|
self._std_dev = np.insert(self.std_dev, self.num_scores, 0, axis=2)
|
2016-02-02 10:07:08 -06:00
|
|
|
self.scores.append(score)
|
2015-12-10 00:53:01 -05:00
|
|
|
|
|
|
|
|
# Align other scores with self scores
|
|
|
|
|
for i, score in enumerate(self.scores):
|
|
|
|
|
other_index = other.scores.index(score)
|
|
|
|
|
|
|
|
|
|
# If necessary, swap other score
|
|
|
|
|
if other_index != i:
|
2015-12-13 13:47:27 -05:00
|
|
|
other._swap_scores(score, other.scores[i])
|
2015-12-10 00:53:01 -05:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
data = {}
|
|
|
|
|
data['self'] = {}
|
|
|
|
|
data['other'] = {}
|
2015-12-13 13:47:27 -05:00
|
|
|
data['self']['mean'] = self.mean
|
|
|
|
|
data['other']['mean'] = other.mean
|
|
|
|
|
data['self']['std. dev.'] = self.std_dev
|
|
|
|
|
data['other']['std. dev.'] = other.std_dev
|
2015-07-29 00:06:07 -07:00
|
|
|
return data
|
2015-06-13 17:21:41 +07:00
|
|
|
|
2015-12-13 13:47:27 -05:00
|
|
|
def _swap_filters(self, filter1, filter2):
|
2015-10-03 01:04:42 -04:00
|
|
|
"""Reverse the ordering of two filters in this tally
|
|
|
|
|
|
2015-10-03 02:29:07 -04:00
|
|
|
This is a helper method for tally arithmetic which helps align the data
|
2015-12-22 13:02:45 -08:00
|
|
|
in two tallies with shared filters. This method reverses the order of
|
|
|
|
|
the two filters in place.
|
2015-10-03 01:04:42 -04:00
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
|
|
|
|
filter1 : Filter
|
|
|
|
|
The filter to swap with filter2
|
|
|
|
|
filter2 : Filter
|
|
|
|
|
The filter to swap with filter1
|
|
|
|
|
|
|
|
|
|
Raises
|
|
|
|
|
------
|
|
|
|
|
ValueError
|
|
|
|
|
If this is a derived tally or this method is called before the tally
|
2015-12-13 01:30:10 -05:00
|
|
|
is populated with data.
|
2015-10-03 00:48:14 -04:00
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
2016-07-31 21:43:34 -04:00
|
|
|
cv.check_type('filter1', filter1, _FILTER_CLASSES)
|
|
|
|
|
cv.check_type('filter2', filter2, _FILTER_CLASSES)
|
2015-10-03 00:48:14 -04:00
|
|
|
|
2015-12-13 13:47:27 -05:00
|
|
|
# Check that the filters exist in the tally and are not the same
|
2015-10-03 00:48:14 -04:00
|
|
|
if filter1 == filter2:
|
2016-02-06 15:04:56 -05:00
|
|
|
return
|
2015-10-03 00:48:14 -04:00
|
|
|
elif filter1 not in self.filters:
|
2021-07-29 18:25:37 +01:00
|
|
|
msg = f'Unable to swap "{filter1.type}" filter1 in Tally ' \
|
2021-07-31 22:33:45 +01:00
|
|
|
f'ID="{self.id}" since it does not contain such a filter'
|
2015-10-03 00:48:14 -04:00
|
|
|
raise ValueError(msg)
|
|
|
|
|
elif filter2 not in self.filters:
|
2021-07-29 18:25:37 +01:00
|
|
|
msg = f'Unable to swap "{filter2.type}" filter2 in Tally ' \
|
2021-07-31 22:33:45 +01:00
|
|
|
f'ID="{self.id}" since it does not contain such a filter'
|
2015-10-03 00:48:14 -04:00
|
|
|
raise ValueError(msg)
|
|
|
|
|
|
2015-10-03 01:04:42 -04:00
|
|
|
# Construct lists of tuples for the bins in each of the two filters
|
2016-09-10 22:59:57 -04:00
|
|
|
filters = [type(filter1), type(filter2)]
|
2026-04-28 17:10:03 -04:00
|
|
|
if isinstance(filter1, DistribcellFilter):
|
2016-11-28 17:52:38 -05:00
|
|
|
filter1_bins = [b for b in range(filter1.num_bins)]
|
2026-04-28 17:10:03 -04:00
|
|
|
elif isinstance(filter1, EnergyFunctionFilter):
|
2016-12-04 16:42:11 -05:00
|
|
|
filter1_bins = [None]
|
2015-10-03 00:48:14 -04:00
|
|
|
else:
|
2018-03-28 15:56:03 -05:00
|
|
|
filter1_bins = filter1.bins
|
2015-10-03 00:48:14 -04:00
|
|
|
|
2026-04-28 17:10:03 -04:00
|
|
|
if isinstance(filter2, DistribcellFilter):
|
2016-11-28 17:52:38 -05:00
|
|
|
filter2_bins = [b for b in range(filter2.num_bins)]
|
2026-04-28 17:10:03 -04:00
|
|
|
elif isinstance(filter2, EnergyFunctionFilter):
|
2016-12-04 16:42:11 -05:00
|
|
|
filter2_bins = [None]
|
2015-10-03 00:48:14 -04:00
|
|
|
else:
|
2018-03-28 15:56:03 -05:00
|
|
|
filter2_bins = filter2.bins
|
2015-10-03 00:48:14 -04:00
|
|
|
|
2017-04-26 12:55:13 -04:00
|
|
|
# Create variables to store views of data in the misaligned structure
|
|
|
|
|
mean = {}
|
|
|
|
|
std_dev = {}
|
|
|
|
|
|
|
|
|
|
# Store the data from the misaligned structure
|
2017-10-04 09:53:47 -05:00
|
|
|
for i, (bin1, bin2) in enumerate(product(filter1_bins, filter2_bins)):
|
2017-04-26 12:55:13 -04:00
|
|
|
filter_bins = [(bin1,), (bin2,)]
|
|
|
|
|
|
|
|
|
|
if self.mean is not None:
|
|
|
|
|
mean[i] = self.get_values(
|
2015-11-19 20:54:31 -05:00
|
|
|
filters=filters, filter_bins=filter_bins, value='mean')
|
2017-04-26 12:55:13 -04:00
|
|
|
|
|
|
|
|
if self.std_dev is not None:
|
|
|
|
|
std_dev[i] = self.get_values(
|
2015-11-19 20:54:31 -05:00
|
|
|
filters=filters, filter_bins=filter_bins, value='std_dev')
|
2017-04-26 12:55:13 -04:00
|
|
|
|
|
|
|
|
# Swap the filters in the copied version of this Tally
|
|
|
|
|
filter1_index = self.filters.index(filter1)
|
|
|
|
|
filter2_index = self.filters.index(filter2)
|
|
|
|
|
self.filters[filter1_index] = filter2
|
|
|
|
|
self.filters[filter2_index] = filter1
|
|
|
|
|
|
|
|
|
|
# Realign the data
|
2017-10-04 09:53:47 -05:00
|
|
|
for i, (bin1, bin2) in enumerate(product(filter1_bins, filter2_bins)):
|
2017-04-26 12:55:13 -04:00
|
|
|
filter_bins = [(bin1,), (bin2,)]
|
|
|
|
|
indices = self.get_filter_indices(filters, filter_bins)
|
|
|
|
|
|
|
|
|
|
if self.mean is not None:
|
|
|
|
|
self.mean[indices, :, :] = mean[i]
|
|
|
|
|
|
|
|
|
|
if self.std_dev is not None:
|
|
|
|
|
self.std_dev[indices, :, :] = std_dev[i]
|
2015-10-03 00:48:14 -04:00
|
|
|
|
2015-12-13 13:47:27 -05:00
|
|
|
def _swap_nuclides(self, nuclide1, nuclide2):
|
2015-12-10 00:53:01 -05:00
|
|
|
"""Reverse the ordering of two nuclides in this tally
|
|
|
|
|
|
|
|
|
|
This is a helper method for tally arithmetic which helps align the data
|
|
|
|
|
in two tallies with shared nuclides. This method reverses the order of
|
|
|
|
|
the two nuclides in place.
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
|
|
|
|
nuclide1 : Nuclide
|
|
|
|
|
The nuclide to swap with nuclide2
|
|
|
|
|
|
|
|
|
|
nuclide2 : Nuclide
|
|
|
|
|
The nuclide to swap with nuclide1
|
|
|
|
|
|
|
|
|
|
Raises
|
|
|
|
|
------
|
|
|
|
|
ValueError
|
|
|
|
|
If this is a derived tally or this method is called before the tally
|
2015-12-13 01:30:10 -05:00
|
|
|
is populated with data.
|
2015-12-10 00:53:01 -05:00
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
# Check that results have been read
|
|
|
|
|
if not self.derived and self.sum is None:
|
2021-07-29 18:25:37 +01:00
|
|
|
msg = f'Unable to use tally arithmetic with Tally ID="{self.id}" ' \
|
|
|
|
|
'since it does not contain any results.'
|
2015-12-10 00:53:01 -05:00
|
|
|
raise ValueError(msg)
|
|
|
|
|
|
2016-07-31 21:43:34 -04:00
|
|
|
cv.check_type('nuclide1', nuclide1, _NUCLIDE_CLASSES)
|
|
|
|
|
cv.check_type('nuclide2', nuclide2, _NUCLIDE_CLASSES)
|
2015-12-13 13:47:27 -05:00
|
|
|
|
|
|
|
|
# Check that the nuclides exist in the tally and are not the same
|
|
|
|
|
if nuclide1 == nuclide2:
|
|
|
|
|
msg = 'Unable to swap a nuclide with itself'
|
|
|
|
|
raise ValueError(msg)
|
|
|
|
|
elif nuclide1 not in self.nuclides:
|
2021-07-29 18:25:37 +01:00
|
|
|
msg = f'Unable to swap nuclide1 "{nuclide1.name}" in Tally ' \
|
2021-07-31 22:33:45 +01:00
|
|
|
f'ID="{self.id}" since it does not contain such a nuclide'
|
2015-12-13 13:47:27 -05:00
|
|
|
raise ValueError(msg)
|
|
|
|
|
elif nuclide2 not in self.nuclides:
|
2021-07-31 22:33:45 +01:00
|
|
|
msg = f'Unable to swap "{nuclide2.name}" nuclide2 in Tally ' \
|
|
|
|
|
f'ID="{self.id}" since it does not contain such a nuclide'
|
2015-12-13 13:47:27 -05:00
|
|
|
raise ValueError(msg)
|
|
|
|
|
|
2015-12-10 00:53:01 -05:00
|
|
|
# Swap the nuclides in the Tally
|
2015-12-13 13:47:27 -05:00
|
|
|
nuclide1_index = self.get_nuclide_index(nuclide1)
|
|
|
|
|
nuclide2_index = self.get_nuclide_index(nuclide2)
|
2015-12-10 00:53:01 -05:00
|
|
|
self.nuclides[nuclide1_index] = nuclide2
|
|
|
|
|
self.nuclides[nuclide2_index] = nuclide1
|
|
|
|
|
|
2015-12-13 15:07:46 -05:00
|
|
|
# Adjust the mean data array to relect the new nuclide order
|
|
|
|
|
if self.mean is not None:
|
2015-12-25 12:23:53 -08:00
|
|
|
nuclide1_mean = self.mean[:, nuclide1_index, :].copy()
|
|
|
|
|
nuclide2_mean = self.mean[:, nuclide2_index, :].copy()
|
|
|
|
|
self.mean[:, nuclide2_index, :] = nuclide1_mean
|
|
|
|
|
self.mean[:, nuclide1_index, :] = nuclide2_mean
|
2015-12-13 15:07:46 -05:00
|
|
|
|
|
|
|
|
# Adjust the std_dev data array to relect the new nuclide order
|
|
|
|
|
if self.std_dev is not None:
|
2015-12-25 12:23:53 -08:00
|
|
|
nuclide1_std_dev = self.std_dev[:, nuclide1_index, :].copy()
|
|
|
|
|
nuclide2_std_dev = self.std_dev[:, nuclide2_index, :].copy()
|
|
|
|
|
self.std_dev[:, nuclide2_index, :] = nuclide1_std_dev
|
|
|
|
|
self.std_dev[:, nuclide1_index, :] = nuclide2_std_dev
|
2015-12-13 13:47:27 -05:00
|
|
|
|
|
|
|
|
def _swap_scores(self, score1, score2):
|
2015-12-10 00:53:01 -05:00
|
|
|
"""Reverse the ordering of two scores in this tally
|
|
|
|
|
|
|
|
|
|
This is a helper method for tally arithmetic which helps align the data
|
2015-12-22 13:02:45 -08:00
|
|
|
in two tallies with shared scores. This method reverses the order
|
2015-12-10 00:53:01 -05:00
|
|
|
of the two scores in place.
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
2015-12-13 13:47:27 -05:00
|
|
|
score1 : str or CrossScore
|
2015-12-10 00:53:01 -05:00
|
|
|
The score to swap with score2
|
2015-10-03 00:48:14 -04:00
|
|
|
|
2015-12-13 13:47:27 -05:00
|
|
|
score2 : str or CrossScore
|
2015-12-10 00:53:01 -05:00
|
|
|
The score to swap with score1
|
|
|
|
|
|
|
|
|
|
Raises
|
|
|
|
|
------
|
|
|
|
|
ValueError
|
|
|
|
|
If this is a derived tally or this method is called before the tally
|
2015-12-13 01:30:10 -05:00
|
|
|
is populated with data.
|
2015-12-10 00:53:01 -05:00
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
# Check that results have been read
|
|
|
|
|
if not self.derived and self.sum is None:
|
2020-03-23 16:45:53 -05:00
|
|
|
msg = 'Unable to use tally arithmetic with Tally ID="{}" ' \
|
2015-12-10 00:53:01 -05:00
|
|
|
'since it does not contain any results.'.format(self.id)
|
|
|
|
|
raise ValueError(msg)
|
|
|
|
|
|
2015-12-13 13:47:27 -05:00
|
|
|
# Check that the scores are valid
|
2026-04-28 17:10:03 -04:00
|
|
|
if not isinstance(score1, (str, CrossScore)):
|
2020-03-23 16:45:53 -05:00
|
|
|
msg = 'Unable to swap score1 "{}" in Tally ID="{}" since it is ' \
|
2015-12-13 13:47:27 -05:00
|
|
|
'not a string or CrossScore'.format(score1, self.id)
|
|
|
|
|
raise ValueError(msg)
|
2026-04-28 17:10:03 -04:00
|
|
|
elif not isinstance(score2, (str, CrossScore)):
|
2020-03-23 16:45:53 -05:00
|
|
|
msg = 'Unable to swap score2 "{}" in Tally ID="{}" since it is ' \
|
2015-12-13 13:47:27 -05:00
|
|
|
'not a string or CrossScore'.format(score2, self.id)
|
|
|
|
|
raise ValueError(msg)
|
|
|
|
|
|
|
|
|
|
# Check that the scores exist in the tally and are not the same
|
|
|
|
|
if score1 == score2:
|
|
|
|
|
msg = 'Unable to swap a score with itself'
|
|
|
|
|
raise ValueError(msg)
|
|
|
|
|
elif score1 not in self.scores:
|
2020-03-23 16:45:53 -05:00
|
|
|
msg = 'Unable to swap score1 "{}" in Tally ID="{}" since it ' \
|
2015-12-13 13:47:27 -05:00
|
|
|
'does not contain such a score'.format(score1, self.id)
|
|
|
|
|
raise ValueError(msg)
|
|
|
|
|
elif score2 not in self.scores:
|
2020-03-23 16:45:53 -05:00
|
|
|
msg = 'Unable to swap score2 "{}" in Tally ID="{}" since it ' \
|
2015-12-13 13:47:27 -05:00
|
|
|
'does not contain such a score'.format(score2, self.id)
|
|
|
|
|
raise ValueError(msg)
|
|
|
|
|
|
2015-12-10 00:53:01 -05:00
|
|
|
# Swap the scores in the Tally
|
2015-12-13 13:47:27 -05:00
|
|
|
score1_index = self.get_score_index(score1)
|
|
|
|
|
score2_index = self.get_score_index(score2)
|
2015-12-10 00:53:01 -05:00
|
|
|
self.scores[score1_index] = score2
|
|
|
|
|
self.scores[score2_index] = score1
|
|
|
|
|
|
2015-12-13 15:07:46 -05:00
|
|
|
# Adjust the mean data array to relect the new nuclide order
|
|
|
|
|
if self.mean is not None:
|
2015-12-25 12:23:53 -08:00
|
|
|
score1_mean = self.mean[:, :, score1_index].copy()
|
|
|
|
|
score2_mean = self.mean[:, :, score2_index].copy()
|
|
|
|
|
self.mean[:, :, score2_index] = score1_mean
|
|
|
|
|
self.mean[:, :, score1_index] = score2_mean
|
2015-12-13 15:07:46 -05:00
|
|
|
|
|
|
|
|
# Adjust the std_dev data array to relect the new nuclide order
|
|
|
|
|
if self.std_dev is not None:
|
2015-12-25 12:23:53 -08:00
|
|
|
score1_std_dev = self.std_dev[:, :, score1_index].copy()
|
|
|
|
|
score2_std_dev = self.std_dev[:, :, score2_index].copy()
|
|
|
|
|
self.std_dev[:, :, score2_index] = score1_std_dev
|
|
|
|
|
self.std_dev[:, :, score1_index] = score2_std_dev
|
2015-10-03 00:48:14 -04:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
def __add__(self, other):
|
2015-07-29 18:47:42 -07:00
|
|
|
"""Adds this tally to another tally or scalar value.
|
|
|
|
|
|
|
|
|
|
This method builds a new tally with data that is the sum of this
|
|
|
|
|
tally's data and that from the other tally or scalar value. If the
|
|
|
|
|
filters, scores and nuclides in the two tallies are not the same, then
|
|
|
|
|
they are combined in all possible ways in the new derived tally.
|
|
|
|
|
|
|
|
|
|
Uncertainty propagation is used to compute the standard deviation
|
|
|
|
|
for the new tally's data. It is important to note that this makes
|
|
|
|
|
the assumption that the tally data is independently distributed.
|
|
|
|
|
In most use cases, this is *not* true and may lead to under-prediction
|
|
|
|
|
of the uncertainty. The uncertainty propagation model is from the
|
|
|
|
|
following source:
|
|
|
|
|
|
|
|
|
|
https://en.wikipedia.org/wiki/Propagation_of_uncertainty
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
2016-04-13 22:17:16 -05:00
|
|
|
other : openmc.Tally or float
|
2015-07-29 18:47:42 -07:00
|
|
|
The tally or scalar value to add to this tally
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
2016-04-13 22:17:16 -05:00
|
|
|
openmc.Tally
|
2015-07-29 18:47:42 -07:00
|
|
|
A new derived tally which is the sum of this tally and the other
|
|
|
|
|
tally or scalar value in the addition.
|
|
|
|
|
|
2015-08-06 23:41:38 -07:00
|
|
|
Raises
|
|
|
|
|
------
|
|
|
|
|
ValueError
|
2015-12-13 01:30:10 -05:00
|
|
|
When this method is called before the Tally is populated with data.
|
2015-08-06 23:41:38 -07:00
|
|
|
|
2015-07-29 18:47:42 -07:00
|
|
|
"""
|
2015-06-13 17:21:41 +07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
# Check that results have been read
|
2015-08-06 23:41:38 -07:00
|
|
|
if not self.derived and self.sum is None:
|
2020-03-23 16:45:53 -05:00
|
|
|
msg = 'Unable to use tally arithmetic with Tally ID="{}" ' \
|
2015-07-29 00:06:07 -07:00
|
|
|
'since it does not contain any results.'.format(self.id)
|
|
|
|
|
raise ValueError(msg)
|
2015-05-11 17:43:33 -04:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
if isinstance(other, Tally):
|
2015-12-10 00:53:01 -05:00
|
|
|
new_tally = self.hybrid_product(other, binary_op='+')
|
2015-05-11 18:59:14 -04:00
|
|
|
|
2015-12-13 11:14:01 -05:00
|
|
|
# If both tally operands were sparse, sparsify the new tally
|
|
|
|
|
if self.sparse and other.sparse:
|
|
|
|
|
new_tally.sparse = True
|
2015-05-11 18:59:14 -04:00
|
|
|
|
2015-08-06 23:12:22 -07:00
|
|
|
elif isinstance(other, Real):
|
2015-08-06 23:41:38 -07:00
|
|
|
new_tally = Tally(name='derived')
|
2015-08-15 09:45:14 +07:00
|
|
|
new_tally._derived = True
|
2015-08-06 23:41:38 -07:00
|
|
|
new_tally.with_batch_statistics = True
|
2015-07-29 18:47:42 -07:00
|
|
|
new_tally.name = self.name
|
2015-09-06 18:51:43 -04:00
|
|
|
new_tally._mean = self.mean + other
|
|
|
|
|
new_tally._std_dev = self.std_dev
|
2015-07-29 00:06:07 -07:00
|
|
|
new_tally.estimator = self.estimator
|
|
|
|
|
new_tally.with_summary = self.with_summary
|
2016-07-01 22:57:14 +07:00
|
|
|
new_tally.num_realizations = self.num_realizations
|
2015-06-13 17:21:41 +07:00
|
|
|
|
2016-03-09 08:53:22 -06:00
|
|
|
new_tally.filters = copy.deepcopy(self.filters)
|
|
|
|
|
new_tally.nuclides = copy.deepcopy(self.nuclides)
|
|
|
|
|
new_tally.scores = copy.deepcopy(self.scores)
|
2015-05-11 17:43:33 -04:00
|
|
|
|
2015-12-13 11:14:01 -05:00
|
|
|
# If this tally operand is sparse, sparsify the new tally
|
|
|
|
|
new_tally.sparse = self.sparse
|
2015-12-13 01:30:10 -05:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
else:
|
2024-04-29 22:45:37 +01:00
|
|
|
msg = f'Unable to add "{other}" to Tally ID="{self.id}"'
|
2015-05-11 17:43:33 -04:00
|
|
|
raise ValueError(msg)
|
|
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
return new_tally
|
2015-05-11 17:43:33 -04:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
def __sub__(self, other):
|
2015-07-29 18:47:42 -07:00
|
|
|
"""Subtracts another tally or scalar value from this tally.
|
|
|
|
|
|
|
|
|
|
This method builds a new tally with data that is the difference of
|
|
|
|
|
this tally's data and that from the other tally or scalar value. If the
|
|
|
|
|
filters, scores and nuclides in the two tallies are not the same, then
|
|
|
|
|
they are combined in all possible ways in the new derived tally.
|
|
|
|
|
|
|
|
|
|
Uncertainty propagation is used to compute the standard deviation
|
|
|
|
|
for the new tally's data. It is important to note that this makes
|
|
|
|
|
the assumption that the tally data is independently distributed.
|
|
|
|
|
In most use cases, this is *not* true and may lead to under-prediction
|
|
|
|
|
of the uncertainty. The uncertainty propagation model is from the
|
|
|
|
|
following source:
|
|
|
|
|
|
|
|
|
|
https://en.wikipedia.org/wiki/Propagation_of_uncertainty
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
2016-04-13 22:17:16 -05:00
|
|
|
other : openmc.Tally or float
|
2015-07-29 18:47:42 -07:00
|
|
|
The tally or scalar value to subtract from this tally
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
2016-04-13 22:17:16 -05:00
|
|
|
openmc.Tally
|
2015-07-29 18:47:42 -07:00
|
|
|
A new derived tally which is the difference of this tally and the
|
|
|
|
|
other tally or scalar value in the subtraction.
|
|
|
|
|
|
2015-08-06 23:41:38 -07:00
|
|
|
Raises
|
|
|
|
|
------
|
|
|
|
|
ValueError
|
2015-12-13 01:30:10 -05:00
|
|
|
When this method is called before the Tally is populated with data.
|
2015-08-06 23:41:38 -07:00
|
|
|
|
2015-07-29 18:47:42 -07:00
|
|
|
"""
|
2015-05-11 18:59:14 -04:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
# Check that results have been read
|
2015-08-06 23:41:38 -07:00
|
|
|
if not self.derived and self.sum is None:
|
2020-03-23 16:45:53 -05:00
|
|
|
msg = 'Unable to use tally arithmetic with Tally ID="{}" ' \
|
2015-07-29 00:06:07 -07:00
|
|
|
'since it does not contain any results.'.format(self.id)
|
|
|
|
|
raise ValueError(msg)
|
2015-05-11 17:43:33 -04:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
if isinstance(other, Tally):
|
2015-12-10 00:53:01 -05:00
|
|
|
new_tally = self.hybrid_product(other, binary_op='-')
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2015-12-13 11:14:01 -05:00
|
|
|
# If both tally operands were sparse, sparsify the new tally
|
|
|
|
|
if self.sparse and other.sparse:
|
|
|
|
|
new_tally.sparse = True
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2015-08-06 23:12:22 -07:00
|
|
|
elif isinstance(other, Real):
|
2015-08-11 18:44:22 -07:00
|
|
|
new_tally = Tally(name='derived')
|
2015-08-15 09:45:14 +07:00
|
|
|
new_tally._derived = True
|
2015-07-29 18:47:42 -07:00
|
|
|
new_tally.name = self.name
|
2015-09-06 18:51:43 -04:00
|
|
|
new_tally._mean = self.mean - other
|
|
|
|
|
new_tally._std_dev = self.std_dev
|
2015-07-29 00:06:07 -07:00
|
|
|
new_tally.estimator = self.estimator
|
|
|
|
|
new_tally.with_summary = self.with_summary
|
2016-07-01 22:57:14 +07:00
|
|
|
new_tally.num_realizations = self.num_realizations
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2016-03-09 08:25:47 -06:00
|
|
|
new_tally.filters = copy.deepcopy(self.filters)
|
|
|
|
|
new_tally.nuclides = copy.deepcopy(self.nuclides)
|
|
|
|
|
new_tally.scores = copy.deepcopy(self.scores)
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2015-12-13 11:14:01 -05:00
|
|
|
# If this tally operand is sparse, sparsify the new tally
|
|
|
|
|
new_tally.sparse = self.sparse
|
2015-12-13 01:30:10 -05:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
else:
|
2024-04-29 22:45:37 +01:00
|
|
|
msg = f'Unable to subtract "{other}" from Tally ID="{self.id}"'
|
2015-07-29 00:06:07 -07:00
|
|
|
raise ValueError(msg)
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
return new_tally
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
def __mul__(self, other):
|
2015-07-29 18:47:42 -07:00
|
|
|
"""Multiplies this tally with another tally or scalar value.
|
|
|
|
|
|
|
|
|
|
This method builds a new tally with data that is the product of
|
|
|
|
|
this tally's data and that from the other tally or scalar value. If the
|
|
|
|
|
filters, scores and nuclides in the two tallies are not the same, then
|
|
|
|
|
they are combined in all possible ways in the new derived tally.
|
|
|
|
|
|
|
|
|
|
Uncertainty propagation is used to compute the standard deviation
|
|
|
|
|
for the new tally's data. It is important to note that this makes
|
|
|
|
|
the assumption that the tally data is independently distributed.
|
|
|
|
|
In most use cases, this is *not* true and may lead to under-prediction
|
|
|
|
|
of the uncertainty. The uncertainty propagation model is from the
|
|
|
|
|
following source:
|
|
|
|
|
|
|
|
|
|
https://en.wikipedia.org/wiki/Propagation_of_uncertainty
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
2016-04-13 22:17:16 -05:00
|
|
|
other : openmc.Tally or float
|
2015-07-29 18:47:42 -07:00
|
|
|
The tally or scalar value to multiply with this tally
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
2016-04-13 22:17:16 -05:00
|
|
|
openmc.Tally
|
2015-07-29 18:47:42 -07:00
|
|
|
A new derived tally which is the product of this tally and the
|
|
|
|
|
other tally or scalar value in the multiplication.
|
|
|
|
|
|
2015-08-06 23:41:38 -07:00
|
|
|
Raises
|
|
|
|
|
------
|
|
|
|
|
ValueError
|
2015-12-13 01:30:10 -05:00
|
|
|
When this method is called before the Tally is populated with data.
|
2015-08-06 23:41:38 -07:00
|
|
|
|
2015-07-29 18:47:42 -07:00
|
|
|
"""
|
2015-05-05 16:47:05 -04:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
# Check that results have been read
|
2015-08-06 23:41:38 -07:00
|
|
|
if not self.derived and self.sum is None:
|
2020-03-23 16:45:53 -05:00
|
|
|
msg = 'Unable to use tally arithmetic with Tally ID="{}" ' \
|
2015-07-29 00:06:07 -07:00
|
|
|
'since it does not contain any results.'.format(self.id)
|
|
|
|
|
raise ValueError(msg)
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
if isinstance(other, Tally):
|
2015-12-10 00:53:01 -05:00
|
|
|
new_tally = self.hybrid_product(other, binary_op='*')
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2015-12-13 11:14:01 -05:00
|
|
|
# If original tally operands were sparse, sparsify the new tally
|
|
|
|
|
if self.sparse and other.sparse:
|
|
|
|
|
new_tally.sparse = True
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2015-08-06 23:12:22 -07:00
|
|
|
elif isinstance(other, Real):
|
2015-08-11 18:44:22 -07:00
|
|
|
new_tally = Tally(name='derived')
|
2015-08-15 09:45:14 +07:00
|
|
|
new_tally._derived = True
|
2015-07-29 18:47:42 -07:00
|
|
|
new_tally.name = self.name
|
2015-09-06 18:51:43 -04:00
|
|
|
new_tally._mean = self.mean * other
|
|
|
|
|
new_tally._std_dev = self.std_dev * np.abs(other)
|
2015-07-29 00:06:07 -07:00
|
|
|
new_tally.estimator = self.estimator
|
|
|
|
|
new_tally.with_summary = self.with_summary
|
2016-07-01 22:57:14 +07:00
|
|
|
new_tally.num_realizations = self.num_realizations
|
2015-05-29 07:45:59 -07:00
|
|
|
|
2016-03-09 08:25:47 -06:00
|
|
|
new_tally.filters = copy.deepcopy(self.filters)
|
|
|
|
|
new_tally.nuclides = copy.deepcopy(self.nuclides)
|
|
|
|
|
new_tally.scores = copy.deepcopy(self.scores)
|
2015-06-13 17:21:41 +07:00
|
|
|
|
2015-12-13 11:14:01 -05:00
|
|
|
# If this tally operand is sparse, sparsify the new tally
|
|
|
|
|
new_tally.sparse = self.sparse
|
2015-12-13 01:30:10 -05:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
else:
|
2024-04-29 22:45:37 +01:00
|
|
|
msg = f'Unable to multiply Tally ID="{self.id}" by "{other}"'
|
2015-07-29 00:06:07 -07:00
|
|
|
raise ValueError(msg)
|
2015-05-25 18:50:52 -04:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
return new_tally
|
2015-05-25 18:50:52 -04:00
|
|
|
|
2015-10-21 10:44:36 -04:00
|
|
|
def __truediv__(self, other):
|
2015-07-29 18:47:42 -07:00
|
|
|
"""Divides this tally by another tally or scalar value.
|
|
|
|
|
|
|
|
|
|
This method builds a new tally with data that is the dividend of
|
|
|
|
|
this tally's data and that from the other tally or scalar value. If the
|
|
|
|
|
filters, scores and nuclides in the two tallies are not the same, then
|
|
|
|
|
they are combined in all possible ways in the new derived tally.
|
|
|
|
|
|
|
|
|
|
Uncertainty propagation is used to compute the standard deviation
|
|
|
|
|
for the new tally's data. It is important to note that this makes
|
|
|
|
|
the assumption that the tally data is independently distributed.
|
|
|
|
|
In most use cases, this is *not* true and may lead to under-prediction
|
|
|
|
|
of the uncertainty. The uncertainty propagation model is from the
|
|
|
|
|
following source:
|
|
|
|
|
|
|
|
|
|
https://en.wikipedia.org/wiki/Propagation_of_uncertainty
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
2016-04-13 22:17:16 -05:00
|
|
|
other : openmc.Tally or float
|
2015-07-29 18:47:42 -07:00
|
|
|
The tally or scalar value to divide this tally by
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
2016-04-13 22:17:16 -05:00
|
|
|
openmc.Tally
|
2015-07-29 18:47:42 -07:00
|
|
|
A new derived tally which is the dividend of this tally and the
|
|
|
|
|
other tally or scalar value in the division.
|
|
|
|
|
|
2015-08-06 23:41:38 -07:00
|
|
|
Raises
|
|
|
|
|
------
|
|
|
|
|
ValueError
|
2015-12-13 01:30:10 -05:00
|
|
|
When this method is called before the Tally is populated with data.
|
2015-08-06 23:41:38 -07:00
|
|
|
|
2015-07-29 18:47:42 -07:00
|
|
|
"""
|
2015-05-25 18:50:52 -04:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
# Check that results have been read
|
2015-08-06 23:41:38 -07:00
|
|
|
if not self.derived and self.sum is None:
|
2020-03-23 16:45:53 -05:00
|
|
|
msg = 'Unable to use tally arithmetic with Tally ID="{}" ' \
|
2015-07-29 00:06:07 -07:00
|
|
|
'since it does not contain any results.'.format(self.id)
|
|
|
|
|
raise ValueError(msg)
|
2015-05-25 18:50:52 -04:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
if isinstance(other, Tally):
|
2015-12-10 00:53:01 -05:00
|
|
|
new_tally = self.hybrid_product(other, binary_op='/')
|
2015-05-25 18:50:52 -04:00
|
|
|
|
2015-12-13 11:14:01 -05:00
|
|
|
# If original tally operands were sparse, sparsify the new tally
|
|
|
|
|
if self.sparse and other.sparse:
|
|
|
|
|
new_tally.sparse = True
|
2015-05-25 18:50:52 -04:00
|
|
|
|
2015-08-06 23:12:22 -07:00
|
|
|
elif isinstance(other, Real):
|
2015-08-11 18:44:22 -07:00
|
|
|
new_tally = Tally(name='derived')
|
2015-08-15 09:45:14 +07:00
|
|
|
new_tally._derived = True
|
2015-07-29 18:47:42 -07:00
|
|
|
new_tally.name = self.name
|
2015-09-06 18:51:43 -04:00
|
|
|
new_tally._mean = self.mean / other
|
|
|
|
|
new_tally._std_dev = self.std_dev * np.abs(1. / other)
|
2025-01-24 16:49:58 -06:00
|
|
|
if self.estimator is not None:
|
|
|
|
|
new_tally.estimator = self.estimator
|
2015-07-29 00:06:07 -07:00
|
|
|
new_tally.with_summary = self.with_summary
|
2016-07-01 22:57:14 +07:00
|
|
|
new_tally.num_realizations = self.num_realizations
|
2015-07-29 00:06:07 -07:00
|
|
|
|
2016-03-09 08:25:47 -06:00
|
|
|
new_tally.filters = copy.deepcopy(self.filters)
|
|
|
|
|
new_tally.nuclides = copy.deepcopy(self.nuclides)
|
|
|
|
|
new_tally.scores = copy.deepcopy(self.scores)
|
2015-05-25 18:50:52 -04:00
|
|
|
|
2015-12-13 11:14:01 -05:00
|
|
|
# If this tally operand is sparse, sparsify the new tally
|
|
|
|
|
new_tally.sparse = self.sparse
|
2015-12-13 01:30:10 -05:00
|
|
|
|
2015-05-25 18:50:52 -04:00
|
|
|
else:
|
2024-04-29 22:45:37 +01:00
|
|
|
msg = f'Unable to divide Tally ID="{self.id}" by "{other}"'
|
2015-07-29 00:06:07 -07:00
|
|
|
raise ValueError(msg)
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
return new_tally
|
2015-05-25 18:50:52 -04:00
|
|
|
|
2015-10-28 21:22:22 -04:00
|
|
|
def __div__(self, other):
|
|
|
|
|
return self.__truediv__(other)
|
|
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
def __pow__(self, power):
|
2015-07-29 18:47:42 -07:00
|
|
|
"""Raises this tally to another tally or scalar value power.
|
|
|
|
|
|
|
|
|
|
This method builds a new tally with data that is the power of
|
|
|
|
|
this tally's data to that from the other tally or scalar value. If the
|
|
|
|
|
filters, scores and nuclides in the two tallies are not the same, then
|
|
|
|
|
they are combined in all possible ways in the new derived tally.
|
|
|
|
|
|
|
|
|
|
Uncertainty propagation is used to compute the standard deviation
|
|
|
|
|
for the new tally's data. It is important to note that this makes
|
|
|
|
|
the assumption that the tally data is independently distributed.
|
|
|
|
|
In most use cases, this is *not* true and may lead to under-prediction
|
|
|
|
|
of the uncertainty. The uncertainty propagation model is from the
|
|
|
|
|
following source:
|
|
|
|
|
|
|
|
|
|
https://en.wikipedia.org/wiki/Propagation_of_uncertainty
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
2016-04-13 22:17:16 -05:00
|
|
|
power : openmc.Tally or float
|
2015-07-29 18:47:42 -07:00
|
|
|
The tally or scalar value exponent
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
2016-04-13 22:17:16 -05:00
|
|
|
openmc.Tally
|
2015-07-29 18:47:42 -07:00
|
|
|
A new derived tally which is this tally raised to the power of the
|
|
|
|
|
other tally or scalar value in the exponentiation.
|
|
|
|
|
|
2015-08-06 23:41:38 -07:00
|
|
|
Raises
|
|
|
|
|
------
|
|
|
|
|
ValueError
|
2015-12-13 01:30:10 -05:00
|
|
|
When this method is called before the Tally is populated with data.
|
2015-08-06 23:41:38 -07:00
|
|
|
|
2015-07-29 18:47:42 -07:00
|
|
|
"""
|
2015-05-29 07:45:59 -07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
# Check that results have been read
|
2015-08-06 23:41:38 -07:00
|
|
|
if not self.derived and self.sum is None:
|
2020-03-23 16:45:53 -05:00
|
|
|
msg = 'Unable to use tally arithmetic with Tally ID="{}" ' \
|
2015-07-29 00:06:07 -07:00
|
|
|
'since it does not contain any results.'.format(self.id)
|
|
|
|
|
raise ValueError(msg)
|
2015-05-29 13:34:23 -07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
if isinstance(power, Tally):
|
2015-12-10 00:53:01 -05:00
|
|
|
new_tally = self.hybrid_product(power, binary_op='^')
|
2015-07-29 00:06:07 -07:00
|
|
|
|
2016-01-14 15:08:28 -05:00
|
|
|
# If original tally operand was sparse, sparsify the new tally
|
|
|
|
|
if self.sparse:
|
2015-12-13 11:14:01 -05:00
|
|
|
new_tally.sparse = True
|
2015-07-29 00:06:07 -07:00
|
|
|
|
2015-08-06 23:12:22 -07:00
|
|
|
elif isinstance(power, Real):
|
2015-08-11 18:44:22 -07:00
|
|
|
new_tally = Tally(name='derived')
|
2015-08-15 09:45:14 +07:00
|
|
|
new_tally._derived = True
|
2015-07-29 18:47:42 -07:00
|
|
|
new_tally.name = self.name
|
2015-07-29 00:06:07 -07:00
|
|
|
new_tally._mean = self._mean ** power
|
|
|
|
|
self_rel_err = self.std_dev / self.mean
|
|
|
|
|
new_tally._std_dev = np.abs(new_tally._mean * power * self_rel_err)
|
|
|
|
|
new_tally.estimator = self.estimator
|
|
|
|
|
new_tally.with_summary = self.with_summary
|
2016-07-01 22:57:14 +07:00
|
|
|
new_tally.num_realizations = self.num_realizations
|
2015-07-29 00:06:07 -07:00
|
|
|
|
2016-03-09 08:25:47 -06:00
|
|
|
new_tally.filters = copy.deepcopy(self.filters)
|
|
|
|
|
new_tally.nuclides = copy.deepcopy(self.nuclides)
|
|
|
|
|
new_tally.scores = copy.deepcopy(self.scores)
|
2015-07-29 00:06:07 -07:00
|
|
|
|
2015-12-13 01:30:10 -05:00
|
|
|
# If original tally was sparse, sparsify the exponentiated tally
|
2015-12-13 11:14:01 -05:00
|
|
|
new_tally.sparse = self.sparse
|
2015-12-13 01:30:10 -05:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
else:
|
2024-04-29 22:45:37 +01:00
|
|
|
msg = f'Unable to raise Tally ID="{self.id}" to power "{power}"'
|
2014-10-23 22:31:55 -04:00
|
|
|
raise ValueError(msg)
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
return new_tally
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
def __radd__(self, other):
|
2015-07-29 18:47:42 -07:00
|
|
|
"""Right addition with a scalar value.
|
|
|
|
|
|
|
|
|
|
This reverses the operands and calls the __add__ method.
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
2016-04-13 22:17:16 -05:00
|
|
|
other : float
|
2015-07-29 18:47:42 -07:00
|
|
|
The scalar value to add to this tally
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
2016-04-13 22:17:16 -05:00
|
|
|
openmc.Tally
|
2015-07-29 18:47:42 -07:00
|
|
|
A new derived tally of this tally added with the scalar value.
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
return self + other
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
def __rsub__(self, other):
|
2015-07-29 18:47:42 -07:00
|
|
|
"""Right subtraction from a scalar value.
|
|
|
|
|
|
|
|
|
|
This reverses the operands and calls the __sub__ method.
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
2016-04-13 22:17:16 -05:00
|
|
|
other : float
|
2015-07-29 18:47:42 -07:00
|
|
|
The scalar value to subtract this tally from
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
2016-04-13 22:17:16 -05:00
|
|
|
openmc.Tally
|
2015-07-29 18:47:42 -07:00
|
|
|
A new derived tally of this tally subtracted from the scalar value.
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
return -1. * self + other
|
|
|
|
|
|
|
|
|
|
def __rmul__(self, other):
|
2015-07-29 18:47:42 -07:00
|
|
|
"""Right multiplication with a scalar value.
|
|
|
|
|
|
|
|
|
|
This reverses the operands and calls the __mul__ method.
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
2016-04-13 22:17:16 -05:00
|
|
|
other : float
|
2015-07-29 18:47:42 -07:00
|
|
|
The scalar value to multiply with this tally
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
2016-04-13 22:17:16 -05:00
|
|
|
openmc.Tally
|
2015-07-29 18:47:42 -07:00
|
|
|
A new derived tally of this tally multiplied by the scalar value.
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
return self * other
|
|
|
|
|
|
|
|
|
|
def __rdiv__(self, other):
|
2015-07-29 18:47:42 -07:00
|
|
|
"""Right division with a scalar value.
|
|
|
|
|
|
|
|
|
|
This reverses the operands and calls the __div__ method.
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
2016-04-13 22:17:16 -05:00
|
|
|
other : float
|
2015-07-29 18:47:42 -07:00
|
|
|
The scalar value to divide by this tally
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
2016-04-13 22:17:16 -05:00
|
|
|
openmc.Tally
|
2015-07-29 18:47:42 -07:00
|
|
|
A new derived tally of the scalar value divided by this tally.
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
2015-08-06 23:12:22 -07:00
|
|
|
return other * self**-1
|
2015-07-29 00:06:07 -07:00
|
|
|
|
2016-06-01 01:40:17 -05:00
|
|
|
def __abs__(self):
|
2015-07-29 18:47:42 -07:00
|
|
|
"""The absolute value of this tally.
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
2016-04-13 22:17:16 -05:00
|
|
|
openmc.Tally
|
2015-07-29 18:47:42 -07:00
|
|
|
A new derived tally which is the absolute value of this tally.
|
|
|
|
|
|
|
|
|
|
"""
|
2015-08-02 23:52:36 -07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
new_tally = copy.deepcopy(self)
|
|
|
|
|
new_tally._mean = np.abs(new_tally.mean)
|
|
|
|
|
return new_tally
|
|
|
|
|
|
|
|
|
|
def __neg__(self):
|
2015-08-02 23:52:36 -07:00
|
|
|
"""The negated value of this tally.
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
2016-04-13 22:17:16 -05:00
|
|
|
openmc.Tally
|
2015-08-02 23:52:36 -07:00
|
|
|
A new derived tally which is the negated value of this tally.
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
new_tally = self * -1
|
|
|
|
|
return new_tally
|
|
|
|
|
|
2018-03-28 15:56:03 -05:00
|
|
|
def get_slice(self, scores=[], filters=[], filter_bins=[], nuclides=[],
|
|
|
|
|
squeeze=False):
|
2015-08-02 23:52:36 -07:00
|
|
|
"""Build a sliced tally for the specified filters, scores and nuclides.
|
|
|
|
|
|
|
|
|
|
This method constructs a new tally to encapsulate a subset of the data
|
2015-09-06 18:51:43 -04:00
|
|
|
represented by this tally. The subset of data to include in the tally
|
2015-08-02 23:52:36 -07:00
|
|
|
slice is determined by the scores, filters and nuclides specified in
|
|
|
|
|
the input parameters.
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
2015-10-03 01:45:51 -04:00
|
|
|
scores : list of str
|
2018-03-28 15:56:03 -05:00
|
|
|
A list of one or more score strings (e.g., ['absorption',
|
|
|
|
|
'nu-fission']
|
2016-09-30 12:19:22 -04:00
|
|
|
filters : Iterable of openmc.FilterMeta
|
2018-03-28 15:56:03 -05:00
|
|
|
An iterable of filter types (e.g., [MeshFilter, EnergyFilter])
|
2015-09-03 07:33:33 -07:00
|
|
|
filter_bins : list of Iterables
|
2018-03-28 15:56:03 -05:00
|
|
|
A list of iterables of filter bins corresponding to the specified
|
|
|
|
|
filter types (e.g., [(1,), ((0., 0.625e-6),)]). Each iterable
|
|
|
|
|
contains bins to slice for the corresponding filter type in the
|
|
|
|
|
filters parameter. Each bin is the integer ID for 'material',
|
2016-01-20 18:04:20 -05:00
|
|
|
'surface', 'cell', 'cellborn', and 'universe' Filters. Each bin is
|
|
|
|
|
an integer for the cell instance ID for 'distribcell' Filters. Each
|
|
|
|
|
bin is a 2-tuple of floats for 'energy' and 'energyout' filters
|
|
|
|
|
corresponding to the energy boundaries of the bin of interest. The
|
|
|
|
|
bin is an (x,y,z) 3-tuple for 'mesh' filters corresponding to the
|
|
|
|
|
mesh cell of interest. The order of the bins in the list must
|
2018-03-28 15:56:03 -05:00
|
|
|
correspond to the `filters` argument.
|
2015-10-03 01:45:51 -04:00
|
|
|
nuclides : list of str
|
2018-03-28 15:56:03 -05:00
|
|
|
A list of nuclide name strings (e.g., ['U235', 'U238'])
|
|
|
|
|
squeeze : bool
|
|
|
|
|
Whether to remove filters with only a single bin in the sliced tally
|
2015-08-02 23:52:36 -07:00
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
2016-04-13 22:17:16 -05:00
|
|
|
openmc.Tally
|
2015-08-02 23:52:36 -07:00
|
|
|
A new tally which encapsulates the subset of data requested in the
|
|
|
|
|
order each filter, nuclide and score is listed in the parameters.
|
|
|
|
|
|
|
|
|
|
Raises
|
|
|
|
|
------
|
|
|
|
|
ValueError
|
2015-12-13 01:30:10 -05:00
|
|
|
When this method is called before the Tally is populated with data.
|
2015-08-02 23:52:36 -07:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
"""
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2015-12-13 01:30:10 -05:00
|
|
|
# Ensure that the tally has data
|
2015-09-11 00:26:11 -04:00
|
|
|
if not self.derived and self.sum is None:
|
2020-03-23 16:45:53 -05:00
|
|
|
msg = 'Unable to use tally arithmetic with Tally ID="{}" ' \
|
2015-07-29 23:18:59 -07:00
|
|
|
'since it does not contain any results.'.format(self.id)
|
2015-07-29 00:06:07 -07:00
|
|
|
raise ValueError(msg)
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2016-02-07 13:24:14 -05:00
|
|
|
# Create deep copy of tally to return as sliced tally
|
2015-07-29 00:06:07 -07:00
|
|
|
new_tally = copy.deepcopy(self)
|
2016-02-08 00:36:49 -05:00
|
|
|
new_tally._derived = True
|
2016-02-07 13:24:14 -05:00
|
|
|
|
|
|
|
|
# Differentiate Tally with a new auto-generated Tally ID
|
|
|
|
|
new_tally.id = None
|
|
|
|
|
|
2015-12-13 11:14:01 -05:00
|
|
|
new_tally.sparse = False
|
2015-08-02 01:20:30 -07:00
|
|
|
|
2016-01-27 16:44:52 -05:00
|
|
|
if not self.derived and self.sum is not None:
|
2015-09-11 00:26:11 -04:00
|
|
|
new_sum = self.get_values(scores, filters, filter_bins,
|
|
|
|
|
nuclides, 'sum')
|
|
|
|
|
new_tally.sum = new_sum
|
2016-01-27 16:44:52 -05:00
|
|
|
if not self.derived and self.sum_sq is not None:
|
2015-09-11 00:26:11 -04:00
|
|
|
new_sum_sq = self.get_values(scores, filters, filter_bins,
|
|
|
|
|
nuclides, 'sum_sq')
|
|
|
|
|
new_tally.sum_sq = new_sum_sq
|
2025-11-12 11:41:37 -05:00
|
|
|
if not self.derived and self._sum_third is not None:
|
|
|
|
|
new_sum_third = self.get_values(
|
|
|
|
|
scores, filters, filter_bins, nuclides, "sum_third"
|
|
|
|
|
)
|
|
|
|
|
new_tally._sum_third = new_sum_third
|
|
|
|
|
if not self.derived and self._sum_fourth is not None:
|
|
|
|
|
new_sum_fourth = self.get_values(
|
|
|
|
|
scores, filters, filter_bins, nuclides, "sum_fourth"
|
|
|
|
|
)
|
|
|
|
|
new_tally._sum_fourth = new_sum_fourth
|
2015-09-11 00:26:11 -04:00
|
|
|
if self.mean is not None:
|
|
|
|
|
new_mean = self.get_values(scores, filters, filter_bins,
|
|
|
|
|
nuclides, 'mean')
|
|
|
|
|
new_tally._mean = new_mean
|
|
|
|
|
if self.std_dev is not None:
|
|
|
|
|
new_std_dev = self.get_values(scores, filters, filter_bins,
|
2015-12-13 11:14:01 -05:00
|
|
|
nuclides, 'std_dev')
|
2015-09-11 00:26:11 -04:00
|
|
|
new_tally._std_dev = new_std_dev
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2015-08-02 01:20:30 -07:00
|
|
|
# SCORES
|
2015-08-01 22:49:53 -07:00
|
|
|
if scores:
|
|
|
|
|
score_indices = []
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2015-08-01 22:49:53 -07:00
|
|
|
# Determine the score indices from any of the requested scores
|
|
|
|
|
for score in self.scores:
|
|
|
|
|
if score not in scores:
|
|
|
|
|
score_index = self.get_score_index(score)
|
|
|
|
|
score_indices.append(score_index)
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2015-08-01 22:49:53 -07:00
|
|
|
# Loop over indices in reverse to remove excluded scores
|
2015-08-07 07:31:10 -07:00
|
|
|
for score_index in reversed(score_indices):
|
2015-08-01 22:49:53 -07:00
|
|
|
new_tally.remove_score(self.scores[score_index])
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2015-08-02 01:20:30 -07:00
|
|
|
# NUCLIDES
|
2015-07-29 00:06:07 -07:00
|
|
|
if nuclides:
|
|
|
|
|
nuclide_indices = []
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2015-07-29 23:49:12 -07:00
|
|
|
# Determine the nuclide indices from any of the requested nuclides
|
2015-05-25 10:11:05 -04:00
|
|
|
for nuclide in self.nuclides:
|
2025-01-24 16:49:58 -06:00
|
|
|
if nuclide not in nuclides:
|
|
|
|
|
nuclide_index = self.get_nuclide_index(nuclide)
|
2015-08-02 23:52:36 -07:00
|
|
|
nuclide_indices.append(nuclide_index)
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2015-07-29 00:06:07 -07:00
|
|
|
# Loop over indices in reverse to remove excluded Nuclides
|
2015-08-07 07:31:10 -07:00
|
|
|
for nuclide_index in reversed(nuclide_indices):
|
2015-07-29 00:06:07 -07:00
|
|
|
new_tally.remove_nuclide(self.nuclides[nuclide_index])
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2015-08-02 01:20:30 -07:00
|
|
|
# FILTERS
|
2015-08-01 22:49:53 -07:00
|
|
|
if filters:
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2015-08-01 22:49:53 -07:00
|
|
|
# Determine the filter indices from any of the requested filters
|
2015-08-02 01:20:30 -07:00
|
|
|
for i, filter_type in enumerate(filters):
|
2018-03-28 15:56:03 -05:00
|
|
|
f = new_tally.find_filter(filter_type)
|
2015-07-29 00:06:07 -07:00
|
|
|
|
2018-03-28 15:56:03 -05:00
|
|
|
# Remove filters with only a single bin if requested
|
|
|
|
|
if squeeze:
|
|
|
|
|
if len(filter_bins[i]) == 1:
|
|
|
|
|
new_tally.filters.remove(f)
|
|
|
|
|
continue
|
2015-09-06 19:45:12 -04:00
|
|
|
else:
|
2018-03-28 15:56:03 -05:00
|
|
|
raise RuntimeError('Cannot remove sliced filter with '
|
|
|
|
|
'more than one bin.')
|
2015-08-01 22:49:53 -07:00
|
|
|
|
2018-03-28 15:56:03 -05:00
|
|
|
# Remove and/or reorder filter bins to user specifications
|
|
|
|
|
bin_indices = [f.get_bin_index(b)
|
|
|
|
|
for b in filter_bins[i]]
|
|
|
|
|
bin_indices = np.unique(bin_indices)
|
2017-12-20 10:12:07 +07:00
|
|
|
|
2018-03-28 15:56:03 -05:00
|
|
|
# Set bins for sliced filter
|
|
|
|
|
new_filter = copy.copy(f)
|
|
|
|
|
new_filter.bins = [f.bins[i] for i in bin_indices]
|
2017-12-20 10:12:07 +07:00
|
|
|
|
|
|
|
|
# Set number of bins manually for mesh/distribcell filters
|
2026-04-28 17:10:03 -04:00
|
|
|
if filter_type is DistribcellFilter:
|
2018-03-28 15:56:03 -05:00
|
|
|
new_filter._num_bins = f._num_bins
|
2017-12-20 10:12:07 +07:00
|
|
|
|
|
|
|
|
# Replace existing filter with new one
|
|
|
|
|
for j, test_filter in enumerate(new_tally.filters):
|
|
|
|
|
if isinstance(test_filter, filter_type):
|
|
|
|
|
new_tally.filters[j] = new_filter
|
2015-08-01 22:49:53 -07:00
|
|
|
|
2015-12-13 01:30:10 -05:00
|
|
|
# If original tally was sparse, sparsify the sliced tally
|
2015-12-13 11:14:01 -05:00
|
|
|
new_tally.sparse = self.sparse
|
2015-07-29 00:06:07 -07:00
|
|
|
return new_tally
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2015-09-27 20:51:06 -04:00
|
|
|
def summation(self, scores=[], filter_type=None,
|
2015-11-19 16:47:55 -05:00
|
|
|
filter_bins=[], nuclides=[], remove_filter=False):
|
2015-10-03 01:45:51 -04:00
|
|
|
"""Vectorized sum of tally data across scores, filter bins and/or
|
2016-01-15 18:02:44 -05:00
|
|
|
nuclides using tally aggregation.
|
2015-09-27 20:51:06 -04:00
|
|
|
|
2015-10-03 01:45:51 -04:00
|
|
|
This method constructs a new tally to encapsulate the sum of the data
|
|
|
|
|
represented by the summation of the data in this tally. The tally data
|
|
|
|
|
sum is determined by the scores, filter bins and nuclides specified
|
2015-09-27 20:51:06 -04:00
|
|
|
in the input parameters.
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
2015-10-03 01:45:51 -04:00
|
|
|
scores : list of str
|
2015-09-27 20:51:06 -04:00
|
|
|
A list of one or more score strings to sum across
|
|
|
|
|
(e.g., ['absorption', 'nu-fission']; default is [])
|
2016-09-30 12:19:22 -04:00
|
|
|
filter_type : openmc.FilterMeta
|
2016-09-11 13:53:19 -04:00
|
|
|
Type of the filter, e.g. MeshFilter
|
2016-04-13 22:17:16 -05:00
|
|
|
filter_bins : Iterable of int or tuple
|
2015-10-03 01:45:51 -04:00
|
|
|
A list of the filter bins corresponding to the filter_type parameter
|
2015-09-27 20:51:06 -04:00
|
|
|
Each bin in the list is the integer ID for 'material', 'surface',
|
|
|
|
|
'cell', 'cellborn', and 'universe' Filters. Each bin is an integer
|
2016-01-11 12:33:00 -05:00
|
|
|
for the cell instance ID for 'distribcell' Filters. Each bin is a
|
2015-09-27 20:51:06 -04:00
|
|
|
2-tuple of floats for 'energy' and 'energyout' filters corresponding
|
|
|
|
|
to the energy boundaries of the bin of interest. Each bin is an
|
|
|
|
|
(x,y,z) 3-tuple for 'mesh' filters corresponding to the mesh cell of
|
|
|
|
|
interest.
|
2015-10-03 01:45:51 -04:00
|
|
|
nuclides : list of str
|
2015-09-27 20:51:06 -04:00
|
|
|
A list of nuclide name strings to sum across
|
2016-06-09 14:52:58 -05:00
|
|
|
(e.g., ['U235', 'U238']; default is [])
|
2015-11-19 16:47:55 -05:00
|
|
|
remove_filter : bool
|
|
|
|
|
If a filter is being summed over, this bool indicates whether to
|
2015-11-19 16:50:11 -05:00
|
|
|
remove that filter in the returned tally. Default is False.
|
2015-09-27 20:51:06 -04:00
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
2016-04-13 22:17:16 -05:00
|
|
|
openmc.Tally
|
2015-09-27 20:51:06 -04:00
|
|
|
A new tally which encapsulates the sum of data requested.
|
|
|
|
|
"""
|
|
|
|
|
|
2016-01-14 15:08:28 -05:00
|
|
|
# Create new derived Tally for summation
|
|
|
|
|
tally_sum = Tally()
|
|
|
|
|
tally_sum._derived = True
|
|
|
|
|
tally_sum._estimator = self.estimator
|
|
|
|
|
tally_sum._num_realizations = self.num_realizations
|
2016-01-15 18:02:44 -05:00
|
|
|
tally_sum._with_batch_statistics = self.with_batch_statistics
|
2016-01-14 15:08:28 -05:00
|
|
|
tally_sum._with_summary = self.with_summary
|
|
|
|
|
tally_sum._sp_filename = self._sp_filename
|
|
|
|
|
tally_sum._results_read = self._results_read
|
2015-09-27 20:51:06 -04:00
|
|
|
|
2016-01-14 15:08:28 -05:00
|
|
|
# Get tally data arrays reshaped with one dimension per filter
|
|
|
|
|
mean = self.get_reshaped_data(value='mean')
|
|
|
|
|
std_dev = self.get_reshaped_data(value='std_dev')
|
2015-09-27 20:51:06 -04:00
|
|
|
|
|
|
|
|
# Sum across any filter bins specified by the user
|
2026-04-28 17:10:03 -04:00
|
|
|
if isinstance(filter_type, FilterMeta):
|
2016-01-14 18:03:20 -05:00
|
|
|
find_filter = self.find_filter(filter_type)
|
2015-11-17 22:33:00 -05:00
|
|
|
|
|
|
|
|
# If user did not specify filter bins, sum across all bins
|
|
|
|
|
if len(filter_bins) == 0:
|
2016-01-14 18:03:20 -05:00
|
|
|
bin_indices = np.arange(find_filter.num_bins)
|
2016-01-14 15:08:28 -05:00
|
|
|
|
2026-04-28 17:10:03 -04:00
|
|
|
if isinstance(find_filter, DistribcellFilter):
|
2016-01-14 18:03:20 -05:00
|
|
|
filter_bins = np.arange(find_filter.num_bins)
|
2026-04-28 17:10:03 -04:00
|
|
|
elif isinstance(find_filter, EnergyFunctionFilter):
|
2016-12-04 16:42:11 -05:00
|
|
|
filter_bins = [None]
|
2016-01-11 12:33:00 -05:00
|
|
|
else:
|
2018-03-29 13:35:58 -05:00
|
|
|
filter_bins = find_filter.bins
|
2016-01-14 15:08:28 -05:00
|
|
|
|
|
|
|
|
# Only sum across bins specified by the user
|
2015-11-17 22:33:00 -05:00
|
|
|
else:
|
2016-01-14 15:08:28 -05:00
|
|
|
bin_indices = \
|
2016-01-14 18:03:20 -05:00
|
|
|
[find_filter.get_bin_index(bin) for bin in filter_bins]
|
2016-01-14 15:08:28 -05:00
|
|
|
|
|
|
|
|
# Sum across the bins in the user-specified filter
|
2016-01-14 18:03:20 -05:00
|
|
|
for i, self_filter in enumerate(self.filters):
|
2018-04-27 19:45:38 -04:00
|
|
|
if type(self_filter) == filter_type:
|
2016-11-28 17:52:38 -05:00
|
|
|
shape = mean.shape
|
2016-01-14 15:08:28 -05:00
|
|
|
mean = np.take(mean, indices=bin_indices, axis=i)
|
|
|
|
|
std_dev = np.take(std_dev, indices=bin_indices, axis=i)
|
2016-11-28 17:52:38 -05:00
|
|
|
|
|
|
|
|
# NumPy take introduces a new dimension in output array
|
|
|
|
|
# for some special cases that must be removed
|
|
|
|
|
if len(mean.shape) > len(shape):
|
|
|
|
|
mean = np.squeeze(mean, axis=i)
|
|
|
|
|
std_dev = np.squeeze(std_dev, axis=i)
|
|
|
|
|
|
2016-01-14 15:08:28 -05:00
|
|
|
mean = np.sum(mean, axis=i, keepdims=True)
|
|
|
|
|
std_dev = np.sum(std_dev**2, axis=i, keepdims=True)
|
|
|
|
|
std_dev = np.sqrt(std_dev)
|
|
|
|
|
|
|
|
|
|
# Add AggregateFilter to the tally sum
|
|
|
|
|
if not remove_filter:
|
2026-04-28 17:10:03 -04:00
|
|
|
filter_sum = AggregateFilter(self_filter,
|
2016-09-30 12:19:22 -04:00
|
|
|
[tuple(filter_bins)], 'sum')
|
2016-02-02 10:07:08 -06:00
|
|
|
tally_sum.filters.append(filter_sum)
|
2016-01-14 15:08:28 -05:00
|
|
|
|
|
|
|
|
# Add a copy of each filter not summed across to the tally sum
|
|
|
|
|
else:
|
2016-02-02 10:07:08 -06:00
|
|
|
tally_sum.filters.append(copy.deepcopy(self_filter))
|
2015-11-17 22:33:00 -05:00
|
|
|
|
2016-01-14 15:08:28 -05:00
|
|
|
# Add a copy of this tally's filters to the tally sum
|
|
|
|
|
else:
|
|
|
|
|
tally_sum._filters = copy.deepcopy(self.filters)
|
|
|
|
|
|
|
|
|
|
# Sum across any nuclides specified by the user
|
|
|
|
|
if len(nuclides) != 0:
|
|
|
|
|
nuclide_bins = [self.get_nuclide_index(nuclide) for nuclide in nuclides]
|
2016-01-14 16:05:45 -05:00
|
|
|
axis_index = self.num_filters
|
2016-01-14 15:08:28 -05:00
|
|
|
mean = np.take(mean, indices=nuclide_bins, axis=axis_index)
|
|
|
|
|
std_dev = np.take(std_dev, indices=nuclide_bins, axis=axis_index)
|
|
|
|
|
mean = np.sum(mean, axis=axis_index, keepdims=True)
|
|
|
|
|
std_dev = np.sum(std_dev**2, axis=axis_index, keepdims=True)
|
|
|
|
|
std_dev = np.sqrt(std_dev)
|
|
|
|
|
|
|
|
|
|
# Add AggregateNuclide to the tally sum
|
2026-04-28 17:10:03 -04:00
|
|
|
nuclide_sum = AggregateNuclide(nuclides, 'sum')
|
2016-02-02 10:07:08 -06:00
|
|
|
tally_sum.nuclides.append(nuclide_sum)
|
2016-01-14 15:08:28 -05:00
|
|
|
|
|
|
|
|
# Add a copy of this tally's nuclides to the tally sum
|
2015-09-27 20:51:06 -04:00
|
|
|
else:
|
2016-01-14 15:08:28 -05:00
|
|
|
tally_sum._nuclides = copy.deepcopy(self.nuclides)
|
|
|
|
|
|
|
|
|
|
# Sum across any scores specified by the user
|
|
|
|
|
if len(scores) != 0:
|
|
|
|
|
score_bins = [self.get_score_index(score) for score in scores]
|
2016-01-15 17:33:22 -05:00
|
|
|
axis_index = self.num_filters + 1
|
2016-01-14 15:08:28 -05:00
|
|
|
mean = np.take(mean, indices=score_bins, axis=axis_index)
|
|
|
|
|
std_dev = np.take(std_dev, indices=score_bins, axis=axis_index)
|
|
|
|
|
mean = np.sum(mean, axis=axis_index, keepdims=True)
|
|
|
|
|
std_dev = np.sum(std_dev**2, axis=axis_index, keepdims=True)
|
|
|
|
|
std_dev = np.sqrt(std_dev)
|
|
|
|
|
|
|
|
|
|
# Add AggregateScore to the tally sum
|
2026-04-28 17:10:03 -04:00
|
|
|
score_sum = AggregateScore(scores, 'sum')
|
2016-02-02 10:07:08 -06:00
|
|
|
tally_sum.scores.append(score_sum)
|
2016-01-14 15:08:28 -05:00
|
|
|
|
|
|
|
|
# Add a copy of this tally's scores to the tally sum
|
2015-11-17 22:33:00 -05:00
|
|
|
else:
|
2016-01-14 15:08:28 -05:00
|
|
|
tally_sum._scores = copy.deepcopy(self.scores)
|
|
|
|
|
|
|
|
|
|
# Reshape condensed data arrays with one dimension for all filters
|
|
|
|
|
mean = np.reshape(mean, tally_sum.shape)
|
|
|
|
|
std_dev = np.reshape(std_dev, tally_sum.shape)
|
|
|
|
|
|
|
|
|
|
# Assign tally sum's data with the new arrays
|
|
|
|
|
tally_sum._mean = mean
|
|
|
|
|
tally_sum._std_dev = std_dev
|
2015-09-27 20:51:06 -04:00
|
|
|
|
2015-12-13 01:30:10 -05:00
|
|
|
# If original tally was sparse, sparsify the tally summation
|
2015-12-13 11:14:01 -05:00
|
|
|
tally_sum.sparse = self.sparse
|
2015-09-27 20:51:06 -04:00
|
|
|
return tally_sum
|
|
|
|
|
|
2016-01-15 18:02:44 -05:00
|
|
|
def average(self, scores=[], filter_type=None,
|
2016-07-01 22:57:14 +07:00
|
|
|
filter_bins=[], nuclides=[], remove_filter=False):
|
2016-01-15 18:02:44 -05:00
|
|
|
"""Vectorized average of tally data across scores, filter bins and/or
|
|
|
|
|
nuclides using tally aggregation.
|
|
|
|
|
|
|
|
|
|
This method constructs a new tally to encapsulate the average of the
|
|
|
|
|
data represented by the average of the data in this tally. The tally
|
|
|
|
|
data average is determined by the scores, filter bins and nuclides
|
|
|
|
|
specified in the input parameters.
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
|
|
|
|
scores : list of str
|
|
|
|
|
A list of one or more score strings to average across
|
|
|
|
|
(e.g., ['absorption', 'nu-fission']; default is [])
|
2016-09-30 12:19:22 -04:00
|
|
|
filter_type : openmc.FilterMeta
|
2016-09-11 13:53:19 -04:00
|
|
|
Type of the filter, e.g. MeshFilter
|
2016-04-13 22:17:16 -05:00
|
|
|
filter_bins : Iterable of int or tuple
|
2016-01-15 18:02:44 -05:00
|
|
|
A list of the filter bins corresponding to the filter_type parameter
|
|
|
|
|
Each bin in the list is the integer ID for 'material', 'surface',
|
|
|
|
|
'cell', 'cellborn', and 'universe' Filters. Each bin is an integer
|
|
|
|
|
for the cell instance ID for 'distribcell' Filters. Each bin is a
|
|
|
|
|
2-tuple of floats for 'energy' and 'energyout' filters corresponding
|
|
|
|
|
to the energy boundaries of the bin of interest. Each bin is an
|
|
|
|
|
(x,y,z) 3-tuple for 'mesh' filters corresponding to the mesh cell of
|
|
|
|
|
interest.
|
|
|
|
|
nuclides : list of str
|
|
|
|
|
A list of nuclide name strings to average across
|
2016-06-09 14:52:58 -05:00
|
|
|
(e.g., ['U235', 'U238']; default is [])
|
2016-01-15 18:02:44 -05:00
|
|
|
remove_filter : bool
|
|
|
|
|
If a filter is being averaged over, this bool indicates whether to
|
|
|
|
|
remove that filter in the returned tally. Default is False.
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
2016-04-13 22:17:16 -05:00
|
|
|
openmc.Tally
|
2016-01-15 18:02:44 -05:00
|
|
|
A new tally which encapsulates the average of data requested.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
# Create new derived Tally for average
|
|
|
|
|
tally_avg = Tally()
|
|
|
|
|
tally_avg._derived = True
|
|
|
|
|
tally_avg._estimator = self.estimator
|
|
|
|
|
tally_avg._num_realizations = self.num_realizations
|
|
|
|
|
tally_avg._with_batch_statistics = self.with_batch_statistics
|
|
|
|
|
tally_avg._with_summary = self.with_summary
|
|
|
|
|
tally_avg._sp_filename = self._sp_filename
|
|
|
|
|
tally_avg._results_read = self._results_read
|
|
|
|
|
|
|
|
|
|
# Get tally data arrays reshaped with one dimension per filter
|
|
|
|
|
mean = self.get_reshaped_data(value='mean')
|
|
|
|
|
std_dev = self.get_reshaped_data(value='std_dev')
|
|
|
|
|
|
|
|
|
|
# Average across any filter bins specified by the user
|
2026-04-28 17:10:03 -04:00
|
|
|
if isinstance(filter_type, FilterMeta):
|
2016-01-15 18:02:44 -05:00
|
|
|
find_filter = self.find_filter(filter_type)
|
|
|
|
|
|
|
|
|
|
# If user did not specify filter bins, average across all bins
|
|
|
|
|
if len(filter_bins) == 0:
|
|
|
|
|
bin_indices = np.arange(find_filter.num_bins)
|
|
|
|
|
|
2026-04-28 17:10:03 -04:00
|
|
|
if isinstance(find_filter, DistribcellFilter):
|
2016-01-15 18:02:44 -05:00
|
|
|
filter_bins = np.arange(find_filter.num_bins)
|
2026-04-28 17:10:03 -04:00
|
|
|
elif isinstance(find_filter, EnergyFunctionFilter):
|
2016-12-04 16:42:11 -05:00
|
|
|
filter_bins = [None]
|
2016-01-15 18:02:44 -05:00
|
|
|
else:
|
2018-03-29 13:35:58 -05:00
|
|
|
filter_bins = find_filter.bins
|
2016-01-15 18:02:44 -05:00
|
|
|
|
|
|
|
|
# Only average across bins specified by the user
|
|
|
|
|
else:
|
|
|
|
|
bin_indices = \
|
|
|
|
|
[find_filter.get_bin_index(bin) for bin in filter_bins]
|
|
|
|
|
|
|
|
|
|
# Average across the bins in the user-specified filter
|
|
|
|
|
for i, self_filter in enumerate(self.filters):
|
2016-09-11 17:29:24 -04:00
|
|
|
if isinstance(self_filter, filter_type):
|
2016-11-28 17:52:38 -05:00
|
|
|
shape = mean.shape
|
2016-01-15 18:02:44 -05:00
|
|
|
mean = np.take(mean, indices=bin_indices, axis=i)
|
|
|
|
|
std_dev = np.take(std_dev, indices=bin_indices, axis=i)
|
2016-11-28 17:52:38 -05:00
|
|
|
|
|
|
|
|
# NumPy take introduces a new dimension in output array
|
|
|
|
|
# for some special cases that must be removed
|
|
|
|
|
if len(mean.shape) > len(shape):
|
|
|
|
|
mean = np.squeeze(mean, axis=i)
|
|
|
|
|
std_dev = np.squeeze(std_dev, axis=i)
|
|
|
|
|
|
2016-11-28 14:18:19 -05:00
|
|
|
mean = np.nanmean(mean, axis=i, keepdims=True)
|
|
|
|
|
std_dev = np.nanmean(std_dev**2, axis=i, keepdims=True)
|
2016-01-15 18:02:44 -05:00
|
|
|
std_dev /= len(bin_indices)
|
|
|
|
|
std_dev = np.sqrt(std_dev)
|
|
|
|
|
|
|
|
|
|
# Add AggregateFilter to the tally avg
|
|
|
|
|
if not remove_filter:
|
2026-04-28 17:10:03 -04:00
|
|
|
filter_sum = AggregateFilter(self_filter,
|
2016-09-30 12:19:22 -04:00
|
|
|
[tuple(filter_bins)], 'avg')
|
2016-03-09 08:29:55 -06:00
|
|
|
tally_avg.filters.append(filter_sum)
|
2016-01-15 18:02:44 -05:00
|
|
|
|
|
|
|
|
# Add a copy of each filter not averaged across to the tally avg
|
|
|
|
|
else:
|
2016-03-09 08:29:55 -06:00
|
|
|
tally_avg.filters.append(copy.deepcopy(self_filter))
|
2016-01-15 18:02:44 -05:00
|
|
|
|
|
|
|
|
# Add a copy of this tally's filters to the tally avg
|
|
|
|
|
else:
|
|
|
|
|
tally_avg._filters = copy.deepcopy(self.filters)
|
|
|
|
|
|
|
|
|
|
# Sum across any nuclides specified by the user
|
|
|
|
|
if len(nuclides) != 0:
|
|
|
|
|
nuclide_bins = [self.get_nuclide_index(nuclide) for nuclide in nuclides]
|
|
|
|
|
axis_index = self.num_filters
|
|
|
|
|
mean = np.take(mean, indices=nuclide_bins, axis=axis_index)
|
|
|
|
|
std_dev = np.take(std_dev, indices=nuclide_bins, axis=axis_index)
|
2016-11-28 14:18:19 -05:00
|
|
|
mean = np.nanmean(mean, axis=axis_index, keepdims=True)
|
|
|
|
|
std_dev = np.nanmean(std_dev**2, axis=axis_index, keepdims=True)
|
2016-01-15 18:02:44 -05:00
|
|
|
std_dev /= len(nuclide_bins)
|
|
|
|
|
std_dev = np.sqrt(std_dev)
|
|
|
|
|
|
|
|
|
|
# Add AggregateNuclide to the tally avg
|
2026-04-28 17:10:03 -04:00
|
|
|
nuclide_avg = AggregateNuclide(nuclides, 'avg')
|
2016-03-09 08:29:55 -06:00
|
|
|
tally_avg.nuclides.append(nuclide_avg)
|
2016-01-15 18:02:44 -05:00
|
|
|
|
|
|
|
|
# Add a copy of this tally's nuclides to the tally avg
|
|
|
|
|
else:
|
|
|
|
|
tally_avg._nuclides = copy.deepcopy(self.nuclides)
|
|
|
|
|
|
|
|
|
|
# Sum across any scores specified by the user
|
|
|
|
|
if len(scores) != 0:
|
|
|
|
|
score_bins = [self.get_score_index(score) for score in scores]
|
|
|
|
|
axis_index = self.num_filters + 1
|
|
|
|
|
mean = np.take(mean, indices=score_bins, axis=axis_index)
|
|
|
|
|
std_dev = np.take(std_dev, indices=score_bins, axis=axis_index)
|
2016-11-28 14:18:19 -05:00
|
|
|
mean = np.nanmean(mean, axis=axis_index, keepdims=True)
|
|
|
|
|
std_dev = np.nanmean(std_dev**2, axis=axis_index, keepdims=True)
|
2016-01-15 18:02:44 -05:00
|
|
|
std_dev /= len(score_bins)
|
|
|
|
|
std_dev = np.sqrt(std_dev)
|
|
|
|
|
|
|
|
|
|
# Add AggregateScore to the tally avg
|
2026-04-28 17:10:03 -04:00
|
|
|
score_sum = AggregateScore(scores, 'avg')
|
2016-03-09 08:29:55 -06:00
|
|
|
tally_avg.scores.append(score_sum)
|
2016-01-15 18:02:44 -05:00
|
|
|
|
|
|
|
|
# Add a copy of this tally's scores to the tally avg
|
|
|
|
|
else:
|
|
|
|
|
tally_avg._scores = copy.deepcopy(self.scores)
|
|
|
|
|
|
|
|
|
|
# Reshape condensed data arrays with one dimension for all filters
|
|
|
|
|
mean = np.reshape(mean, tally_avg.shape)
|
|
|
|
|
std_dev = np.reshape(std_dev, tally_avg.shape)
|
|
|
|
|
|
|
|
|
|
# Assign tally avg's data with the new arrays
|
|
|
|
|
tally_avg._mean = mean
|
|
|
|
|
tally_avg._std_dev = std_dev
|
|
|
|
|
|
|
|
|
|
# If original tally was sparse, sparsify the tally average
|
|
|
|
|
tally_avg.sparse = self.sparse
|
|
|
|
|
return tally_avg
|
|
|
|
|
|
2019-04-08 17:23:54 -04:00
|
|
|
def diagonalize_filter(self, new_filter, filter_position=-1):
|
2015-10-03 01:45:51 -04:00
|
|
|
"""Diagonalize the tally data array along a new axis of filter bins.
|
2015-09-11 00:26:11 -04:00
|
|
|
|
2015-10-03 02:29:07 -04:00
|
|
|
This is a helper method for the tally arithmetic methods. This method
|
2015-10-03 01:45:51 -04:00
|
|
|
adds the new filter to a derived tally constructed copied from this one.
|
|
|
|
|
The data in the derived tally arrays is "diagonalized" along the bins in
|
|
|
|
|
the new filter. This functionality is used by the openmc.mgxs module; to
|
|
|
|
|
transport-correct scattering matrices by subtracting a 'scatter-P1'
|
2018-04-27 19:45:38 -04:00
|
|
|
reaction rate tally with an energy filter from a 'scatter' reaction
|
2015-10-03 01:45:51 -04:00
|
|
|
rate tally with both energy and energyout filters.
|
2015-09-11 00:26:11 -04:00
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
2015-10-03 01:45:51 -04:00
|
|
|
new_filter : Filter
|
|
|
|
|
The filter along which to diagonalize the data in the new
|
2019-04-08 17:23:54 -04:00
|
|
|
filter_position : int
|
|
|
|
|
Where to place the new filter in the Tally.filters list. Defaults
|
|
|
|
|
to last position.
|
2015-09-11 00:26:11 -04:00
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
2016-04-13 22:17:16 -05:00
|
|
|
openmc.Tally
|
2022-02-10 00:20:58 +00:00
|
|
|
A new derived Tally with data diagonalized along the new filter.
|
2015-09-11 00:26:11 -04:00
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
2016-07-31 21:43:34 -04:00
|
|
|
cv.check_type('new_filter', new_filter, _FILTER_CLASSES)
|
2019-04-08 17:23:54 -04:00
|
|
|
cv.check_type('filter_position', filter_position, Integral)
|
2015-09-11 00:26:11 -04:00
|
|
|
|
|
|
|
|
if new_filter in self.filters:
|
2020-03-23 16:45:53 -05:00
|
|
|
msg = 'Unable to diagonalize Tally ID="{}" which already ' \
|
|
|
|
|
'contains a "{}" filter'.format(self.id, type(new_filter))
|
2015-09-11 00:26:11 -04:00
|
|
|
raise ValueError(msg)
|
|
|
|
|
|
2015-10-03 01:45:51 -04:00
|
|
|
# Add the new filter to a copy of this Tally
|
2015-09-11 00:26:11 -04:00
|
|
|
new_tally = copy.deepcopy(self)
|
2019-04-08 17:23:54 -04:00
|
|
|
new_tally.filters.insert(filter_position, new_filter)
|
2015-09-11 00:26:11 -04:00
|
|
|
|
2015-10-03 01:45:51 -04:00
|
|
|
# Determine "base" indices along the new "diagonal", and the factor
|
|
|
|
|
# by which the "base" indices should be repeated to account for all
|
|
|
|
|
# other filter bins in the diagonalized tally
|
2015-09-12 02:04:21 -04:00
|
|
|
indices = np.arange(0, new_filter.num_bins**2, new_filter.num_bins+1)
|
2018-04-27 19:45:38 -04:00
|
|
|
diag_factor = self.num_filter_bins // new_filter.num_bins
|
|
|
|
|
diag_indices = np.zeros(self.num_filter_bins, dtype=int)
|
2015-10-01 18:26:25 -04:00
|
|
|
|
2015-10-03 01:45:51 -04:00
|
|
|
# Determine the filter indices along the new "diagonal"
|
2015-10-01 18:26:25 -04:00
|
|
|
for i in range(diag_factor):
|
|
|
|
|
start = i * new_filter.num_bins
|
|
|
|
|
end = (i+1) * new_filter.num_bins
|
|
|
|
|
diag_indices[start:end] = indices + (i * new_filter.num_bins**2)
|
2015-09-11 00:26:11 -04:00
|
|
|
|
2015-10-03 01:45:51 -04:00
|
|
|
# Inject this Tally's data along the diagonal of the diagonalized Tally
|
2016-01-27 16:44:52 -05:00
|
|
|
if not self.derived and self.sum is not None:
|
2015-12-13 01:39:21 -05:00
|
|
|
new_tally._sum = np.zeros(new_tally.shape, dtype=np.float64)
|
2015-10-03 01:45:51 -04:00
|
|
|
new_tally._sum[diag_indices, :, :] = self.sum
|
2016-01-27 16:44:52 -05:00
|
|
|
if not self.derived and self.sum_sq is not None:
|
2015-12-13 01:39:21 -05:00
|
|
|
new_tally._sum_sq = np.zeros(new_tally.shape, dtype=np.float64)
|
2015-10-03 01:45:51 -04:00
|
|
|
new_tally._sum_sq[diag_indices, :, :] = self.sum_sq
|
2025-11-12 11:41:37 -05:00
|
|
|
if not self.derived and self._sum_third is not None:
|
|
|
|
|
new_tally._sum_third = np.zeros(new_tally.shape, dtype=np.float64)
|
|
|
|
|
new_tally._sum_third[diag_indices, :, :] = self.sum_third
|
|
|
|
|
if not self.derived and self._sum_fourth is not None:
|
|
|
|
|
new_tally._sum_fourth = np.zeros(new_tally.shape, dtype=np.float64)
|
|
|
|
|
new_tally._sum_fourth[diag_indices, :, :] = self.sum_fourth
|
2015-09-11 00:26:11 -04:00
|
|
|
if self.mean is not None:
|
2015-12-13 01:39:21 -05:00
|
|
|
new_tally._mean = np.zeros(new_tally.shape, dtype=np.float64)
|
2015-10-03 01:45:51 -04:00
|
|
|
new_tally._mean[diag_indices, :, :] = self.mean
|
2015-09-11 00:26:11 -04:00
|
|
|
if self.std_dev is not None:
|
2015-12-13 01:39:21 -05:00
|
|
|
new_tally._std_dev = np.zeros(new_tally.shape, dtype=np.float64)
|
2015-10-03 01:45:51 -04:00
|
|
|
new_tally._std_dev[diag_indices, :, :] = self.std_dev
|
2015-09-11 00:26:11 -04:00
|
|
|
|
2015-12-13 01:30:10 -05:00
|
|
|
# If original tally was sparse, sparsify the diagonalized tally
|
2015-12-13 11:14:01 -05:00
|
|
|
new_tally.sparse = self.sparse
|
2015-09-11 00:26:11 -04:00
|
|
|
return new_tally
|
|
|
|
|
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2016-04-29 16:14:02 -05:00
|
|
|
class Tallies(cv.CheckedList):
|
|
|
|
|
"""Collection of Tallies used for an OpenMC simulation.
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2016-04-29 16:14:02 -05:00
|
|
|
This class corresponds directly to the tallies.xml input file. It can be
|
|
|
|
|
thought of as a normal Python list where each member is a :class:`Tally`. It
|
|
|
|
|
behaves like a list as the following example demonstrates:
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2016-04-29 16:14:02 -05:00
|
|
|
>>> t1 = openmc.Tally()
|
|
|
|
|
>>> t2 = openmc.Tally()
|
|
|
|
|
>>> t3 = openmc.Tally()
|
|
|
|
|
>>> tallies = openmc.Tallies([t1])
|
|
|
|
|
>>> tallies.append(t2)
|
|
|
|
|
>>> tallies += [t3]
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2016-04-29 16:14:02 -05:00
|
|
|
Parameters
|
|
|
|
|
----------
|
|
|
|
|
tallies : Iterable of openmc.Tally
|
|
|
|
|
Tallies to add to the collection
|
2015-09-09 19:32:26 -04:00
|
|
|
|
2016-04-29 16:14:02 -05:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(self, tallies=None):
|
2017-12-24 16:34:25 +07:00
|
|
|
super().__init__(Tally, 'tallies collection')
|
2016-04-29 16:14:02 -05:00
|
|
|
if tallies is not None:
|
|
|
|
|
self += tallies
|
2015-09-09 19:32:26 -04:00
|
|
|
|
2016-04-29 16:14:02 -05:00
|
|
|
def append(self, tally, merge=False):
|
|
|
|
|
"""Append tally to collection
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
|
|
|
|
tally : openmc.Tally
|
|
|
|
|
Tally to append
|
|
|
|
|
merge : bool
|
|
|
|
|
Indicate whether the tally should be merged with an existing tally,
|
|
|
|
|
if possible. Defaults to False.
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2016-04-29 16:14:02 -05:00
|
|
|
"""
|
2015-05-11 19:21:36 -04:00
|
|
|
if merge:
|
|
|
|
|
# Look for a tally to merge with this one
|
2016-04-29 16:14:02 -05:00
|
|
|
for i, tally2 in enumerate(self):
|
2015-05-11 19:21:36 -04:00
|
|
|
# If a mergeable tally is found
|
|
|
|
|
if tally2.can_merge(tally):
|
2016-05-30 14:23:06 -04:00
|
|
|
# Replace tally2 with the merged tally
|
2015-05-11 19:21:36 -04:00
|
|
|
merged_tally = tally2.merge(tally)
|
2016-04-29 16:14:02 -05:00
|
|
|
self[i] = merged_tally
|
2025-12-04 06:53:04 -06:00
|
|
|
return
|
2015-05-11 19:21:36 -04:00
|
|
|
|
2025-12-04 06:53:04 -06:00
|
|
|
super().append(tally)
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2015-05-11 19:21:36 -04:00
|
|
|
def merge_tallies(self):
|
2015-06-26 20:37:44 +07:00
|
|
|
"""Merge any mergeable tallies together. Note that n-way merges are
|
2015-06-13 17:21:41 +07:00
|
|
|
possible.
|
|
|
|
|
|
|
|
|
|
"""
|
2015-05-11 19:21:36 -04:00
|
|
|
|
2016-04-29 16:14:02 -05:00
|
|
|
for i, tally1 in enumerate(self):
|
|
|
|
|
for j, tally2 in enumerate(self):
|
2015-05-11 19:21:36 -04:00
|
|
|
# Do not merge the same tally with itself
|
|
|
|
|
if i == j:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# If the two tallies are mergeable
|
|
|
|
|
if tally1.can_merge(tally2):
|
|
|
|
|
# Replace tally 1 with the merged tally
|
|
|
|
|
merged_tally = tally1.merge(tally2)
|
2016-04-29 16:14:02 -05:00
|
|
|
self[i] = merged_tally
|
2015-05-11 19:21:36 -04:00
|
|
|
|
2015-05-25 00:39:47 -04:00
|
|
|
# Remove tally 2 since it is no longer needed
|
2016-04-29 16:14:02 -05:00
|
|
|
self.pop(j)
|
2015-05-11 19:21:36 -04:00
|
|
|
|
|
|
|
|
# Continue iterating from the first loop
|
|
|
|
|
break
|
|
|
|
|
|
2025-01-24 16:49:58 -06:00
|
|
|
def add_results(self, statepoint: cv.PathLike | openmc.StatePoint):
|
|
|
|
|
"""Add results from the provided statepoint file
|
|
|
|
|
|
|
|
|
|
.. versionadded:: 0.15.1
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
|
|
|
|
statepoint : openmc.PathLike or openmc.StatePoint
|
|
|
|
|
Statepoint used to update tally results
|
|
|
|
|
"""
|
|
|
|
|
for tally in self:
|
|
|
|
|
tally.add_results(statepoint)
|
|
|
|
|
|
2016-12-09 16:53:08 -06:00
|
|
|
def _create_tally_subelements(self, root_element):
|
2016-04-29 16:14:02 -05:00
|
|
|
for tally in self:
|
2016-12-09 16:53:08 -06:00
|
|
|
root_element.append(tally.to_xml_element())
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2022-11-10 00:01:40 -06:00
|
|
|
def _create_mesh_subelements(self, root_element, memo=None):
|
|
|
|
|
already_written = memo if memo else set()
|
2016-04-29 16:14:02 -05:00
|
|
|
for tally in self:
|
|
|
|
|
for f in tally.filters:
|
2026-04-28 17:10:03 -04:00
|
|
|
if isinstance(f, MeshFilter):
|
2022-11-10 00:01:40 -06:00
|
|
|
if f.mesh.id in already_written:
|
|
|
|
|
continue
|
|
|
|
|
if len(f.mesh.name) > 0:
|
|
|
|
|
root_element.append(ET.Comment(f.mesh.name))
|
|
|
|
|
root_element.append(f.mesh.to_xml_element())
|
|
|
|
|
already_written.add(f.mesh.id)
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2017-03-27 10:03:27 -05:00
|
|
|
def _create_filter_subelements(self, root_element):
|
2017-03-31 15:22:04 -05:00
|
|
|
already_written = dict()
|
2017-03-27 10:03:27 -05:00
|
|
|
for tally in self:
|
|
|
|
|
for f in tally.filters:
|
|
|
|
|
if f not in already_written:
|
|
|
|
|
root_element.append(f.to_xml_element())
|
2017-03-31 15:22:04 -05:00
|
|
|
already_written[f] = f.id
|
2017-11-09 23:19:09 -05:00
|
|
|
elif f.id != already_written[f]:
|
2017-03-31 15:22:04 -05:00
|
|
|
# Set the IDs of identical filters with different
|
|
|
|
|
# user-defined IDs to the same value
|
|
|
|
|
f.id = already_written[f]
|
2017-03-27 10:03:27 -05:00
|
|
|
|
2016-12-09 16:53:08 -06:00
|
|
|
def _create_derivative_subelements(self, root_element):
|
2016-01-29 16:54:07 -05:00
|
|
|
# Get a list of all derivatives referenced in a tally.
|
|
|
|
|
derivs = []
|
2016-05-19 21:39:06 -04:00
|
|
|
for tally in self:
|
2016-01-29 16:54:07 -05:00
|
|
|
deriv = tally.derivative
|
|
|
|
|
if deriv is not None and deriv not in derivs:
|
|
|
|
|
derivs.append(deriv)
|
|
|
|
|
|
|
|
|
|
# Add the derivatives to the XML tree.
|
|
|
|
|
for d in derivs:
|
2016-12-09 16:53:08 -06:00
|
|
|
root_element.append(d.to_xml_element())
|
2016-01-29 16:54:07 -05:00
|
|
|
|
2022-11-10 00:01:40 -06:00
|
|
|
def to_xml_element(self, memo=None):
|
2022-11-03 18:27:53 -05:00
|
|
|
"""Creates a 'tallies' element to be written to an XML file.
|
|
|
|
|
"""
|
2024-05-24 18:56:20 -05:00
|
|
|
memo = memo if memo is not None else set()
|
2022-11-03 18:27:53 -05:00
|
|
|
element = ET.Element("tallies")
|
2022-11-10 00:01:40 -06:00
|
|
|
self._create_mesh_subelements(element, memo)
|
2022-11-03 18:27:53 -05:00
|
|
|
self._create_filter_subelements(element)
|
|
|
|
|
self._create_tally_subelements(element)
|
|
|
|
|
self._create_derivative_subelements(element)
|
|
|
|
|
|
|
|
|
|
# Clean the indentation in the file to be user-readable
|
|
|
|
|
clean_indentation(element)
|
|
|
|
|
|
|
|
|
|
return element
|
|
|
|
|
|
2017-03-31 15:22:04 -05:00
|
|
|
def export_to_xml(self, path='tallies.xml'):
|
2015-06-13 17:21:41 +07:00
|
|
|
"""Create a tallies.xml file that can be used for a simulation.
|
|
|
|
|
|
2016-12-15 20:23:07 +00:00
|
|
|
Parameters
|
|
|
|
|
----------
|
|
|
|
|
path : str
|
|
|
|
|
Path to file to write. Defaults to 'tallies.xml'.
|
|
|
|
|
|
2015-06-13 17:21:41 +07:00
|
|
|
"""
|
2022-11-03 18:27:53 -05:00
|
|
|
root_element = self.to_xml_element()
|
2014-10-13 07:48:28 -04:00
|
|
|
|
2019-03-12 09:58:35 -05:00
|
|
|
# Check if path is a directory
|
|
|
|
|
p = Path(path)
|
|
|
|
|
if p.is_dir():
|
|
|
|
|
p /= 'tallies.xml'
|
|
|
|
|
|
2014-10-23 22:31:55 -04:00
|
|
|
# Write the XML Tree to the tallies.xml file
|
2016-12-09 16:53:08 -06:00
|
|
|
tree = ET.ElementTree(root_element)
|
2019-03-12 09:58:35 -05:00
|
|
|
tree.write(str(p), xml_declaration=True, encoding='utf-8')
|
2022-01-20 17:51:16 -06:00
|
|
|
|
|
|
|
|
@classmethod
|
2022-11-10 00:01:40 -06:00
|
|
|
def from_xml_element(cls, elem, meshes=None):
|
2022-11-03 22:29:36 -05:00
|
|
|
"""Generate tallies from an XML element
|
2022-01-20 17:51:16 -06:00
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
2023-05-09 11:41:04 -04:00
|
|
|
elem : lxml.etree._Element
|
2022-11-03 22:29:36 -05:00
|
|
|
XML element
|
2022-12-13 23:16:57 -06:00
|
|
|
meshes : dict or None
|
|
|
|
|
A dictionary with mesh IDs as keys and mesh instances as values that
|
|
|
|
|
have already been read from XML. Pre-existing meshes are used
|
|
|
|
|
and new meshes are added to when creating tally objects.
|
2022-01-20 17:51:16 -06:00
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
|
|
|
|
openmc.Tallies
|
|
|
|
|
Tallies object
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
# Read mesh elements
|
2022-11-10 00:01:40 -06:00
|
|
|
meshes = {} if meshes is None else meshes
|
2022-11-03 22:41:28 -05:00
|
|
|
for e in elem.findall('mesh'):
|
|
|
|
|
mesh = MeshBase.from_xml_element(e)
|
2022-01-20 17:51:16 -06:00
|
|
|
meshes[mesh.id] = mesh
|
|
|
|
|
|
|
|
|
|
# Read filter elements
|
|
|
|
|
filters = {}
|
2022-11-03 22:41:28 -05:00
|
|
|
for e in elem.findall('filter'):
|
2026-04-28 17:10:03 -04:00
|
|
|
filter = Filter.from_xml_element(e, meshes=meshes)
|
2022-01-20 17:51:16 -06:00
|
|
|
filters[filter.id] = filter
|
|
|
|
|
|
|
|
|
|
# Read derivative elements
|
|
|
|
|
derivatives = {}
|
2022-11-03 22:41:28 -05:00
|
|
|
for e in elem.findall('derivative'):
|
|
|
|
|
deriv = openmc.TallyDerivative.from_xml_element(e)
|
2022-01-20 17:51:16 -06:00
|
|
|
derivatives[deriv.id] = deriv
|
|
|
|
|
|
|
|
|
|
# Read tally elements
|
|
|
|
|
tallies = []
|
2022-11-03 22:41:28 -05:00
|
|
|
for e in elem.findall('tally'):
|
2022-01-20 17:51:16 -06:00
|
|
|
tally = openmc.Tally.from_xml_element(
|
2022-11-03 22:41:28 -05:00
|
|
|
e, filters=filters, derivatives=derivatives
|
2022-01-20 17:51:16 -06:00
|
|
|
)
|
|
|
|
|
tallies.append(tally)
|
|
|
|
|
|
|
|
|
|
return cls(tallies)
|
2022-11-03 22:29:36 -05:00
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def from_xml(cls, path='tallies.xml'):
|
|
|
|
|
"""Generate tallies from XML file
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
|
|
|
|
path : str, optional
|
|
|
|
|
Path to tallies XML file
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
|
|
|
|
openmc.Tallies
|
|
|
|
|
Tallies object
|
|
|
|
|
|
|
|
|
|
"""
|
2024-01-16 22:54:03 -08:00
|
|
|
parser = ET.XMLParser(huge_tree=True)
|
|
|
|
|
tree = ET.parse(path, parser=parser)
|
2022-11-03 22:29:36 -05:00
|
|
|
root = tree.getroot()
|
|
|
|
|
return cls.from_xml_element(root)
|