From 1154c07438cc5e66530464b6bf7ee5177d543ba5 Mon Sep 17 00:00:00 2001 From: Will Boyd Date: Thu, 6 Aug 2015 23:41:38 -0700 Subject: [PATCH] Refactored Tally._outer_product(...) and operator overloads per @paulromano suggestion --- openmc/tallies.py | 239 ++++++++++++++++++++++++---------------------- 1 file changed, 125 insertions(+), 114 deletions(-) diff --git a/openmc/tallies.py b/openmc/tallies.py index 325eca925e..733d053721 100644 --- a/openmc/tallies.py +++ b/openmc/tallies.py @@ -101,6 +101,7 @@ class Tally(object): self._mean = None self._std_dev = None self._with_batch_statistics = False + self._derived = False def __deepcopy__(self, memo): existing = memo.get(id(self)) @@ -119,6 +120,7 @@ class Tally(object): clone._std_dev = copy.deepcopy(self.std_dev, memo) clone._with_summary = self.with_summary clone._with_batch_statistics = self.with_batch_statistics + clone._derived = self.derived clone._filters = [] for filter in self.filters: @@ -281,6 +283,10 @@ class Tally(object): def with_batch_statistics(self): return self._with_batch_statistics + @property + def derived(self): + return self._derived + @estimator.setter def estimator(self, estimator): check_value('estimator', estimator, ['analog', 'tracklength']) @@ -410,8 +416,8 @@ class Tally(object): """ if score not in self.scores: - msg = 'Unable to remove score "{0}" from Tally ID="{1}" since the ' \ - 'Tally does not contain this score'.format(score, self.id) + msg = 'Unable to remove score "{0}" from Tally ID="{1}" since ' \ + 'the Tally does not contain this score'.format(score, self.id) ValueError(msg) self._scores.remove(score) @@ -1006,7 +1012,7 @@ class Tally(object): # Ensure that StatePoint.read_results() was called first if self.mean is None or self.std_dev is None: - msg = 'The Tally ID={0} has no data to return. Call the ' \ + msg = 'The Tally ID="{0}" has no data to return. Call the ' \ 'StatePoint.read_results() method before using ' \ 'Tally.get_pandas_dataframe(...)'.format(self.id) raise KeyError(msg) @@ -1427,7 +1433,7 @@ class Tally(object): # Pickle the Tally results to a file pickle.dump(tally_results, open(filename, 'wb')) - def _outer_product(self, other, new_tally, binary_op): + def _outer_product(self, other, binary_op): """Combines filters, scores and nuclides with another tally. This is a helper method for the tally arithmetic methods. The filters, @@ -1439,14 +1445,66 @@ class Tally(object): ---------- other : Tally The tally on the right hand side of the outer product - new_tally: Tally - The new tally to represent the outer product binary_op : {'+', '-', '*', '/', '^'} The binary operation in the outer product + Returns + ------- + Tally + A new Tally outer that is the outer product with this one. + + Raises + ------ + ValueError + When this method is called before the other tally is populated + with data by the StatePoint.read_results() method. + """ - new_tally.name = '({0} {1} {2})'.format(self.name, binary_op, other.name) + # Check that results have been read + if not other.derived and other.sum is None: + msg = 'Unable to use tally arithmetic with Tally ID="{0}" ' \ + 'since it does not contain any results.'.format(other.id) + raise ValueError(msg) + + new_name = '({0} {1} {2})'.format(self.name, binary_op, other.name) + new_tally = Tally(name=new_name) + new_tally.with_batch_statistics = True + + data = self._align_tally_data(other) + + 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 == '-': + data = self._align_tally_data(other) + 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 == '*': + data = self._align_tally_data(other) + 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'] + new_tally._std_dev = np.abs(new_tally.mean) * \ + np.sqrt(self_rel_err**2 + other_rel_err**2) + elif binary_op == '/': + data = self._align_tally_data(other) + 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'] + new_tally._std_dev = np.abs(new_tally.mean) * \ + np.sqrt(self_rel_err**2 + other_rel_err**2) + elif binary_op == '^': + data = self._align_tally_data(power) + mean_ratio = data['other']['mean'] / data['self']['mean'] + 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) if self.estimator == other.estimator: new_tally.estimator = self.estimator @@ -1485,6 +1543,8 @@ class Tally(object): new_nuclide = CrossNuclide(self_nuclide, other_nuclide, binary_op) new_tally.add_nuclide(new_nuclide) + return new_tally + def _align_tally_data(self, other): """Aligns data from two tallies for tally arithmetic. @@ -1508,6 +1568,7 @@ class Tally(object): A dictionary of dictionaries to "aligned" 'mean' and 'std. dev' NumPy arrays for each tally's data. + """ self_mean = copy.deepcopy(self.mean) @@ -1591,33 +1652,26 @@ class Tally(object): A new derived tally which is the sum of this tally and the other tally or scalar value in the addition. + Raises + ------ + ValueError + When this method is called before the Tally is populated with data + by the StatePoint.read_results() method. + """ # Check that results have been read - if self.sum is None: - msg = 'Unable to use tally arithmetic with Tally ID={0} ' \ + if not self.derived and self.sum is None: + msg = 'Unable to use tally arithmetic with Tally ID="{0}" ' \ 'since it does not contain any results.'.format(self.id) raise ValueError(msg) - new_tally = Tally(name='derived') - new_tally.with_batch_statistics = True - if isinstance(other, Tally): - - # Check that results have been read - if other.sum is None: - msg = 'Unable to use tally arithmetic with Tally ID={0} ' \ - 'since it does not contain any results.'.format(other.id) - raise ValueError(msg) - - self._outer_product(other, new_tally, binary_op='+') - data = self._align_tally_data(other) - 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) + new_tally = self._outer_product(other, binary_op='+') elif isinstance(other, Real): - + new_tally = Tally(name='derived') + new_tally.with_batch_statistics = True new_tally.name = self.name new_tally._mean = self._mean + other new_tally._std_dev = self._std_dev @@ -1634,7 +1688,7 @@ class Tally(object): new_tally.add_score(score) else: - msg = 'Unable to add {0} to Tally ID={1}'.format(other, self.id) + msg = 'Unable to add "{0}" to Tally ID="{1}"'.format(other, self.id) raise ValueError(msg) return new_tally @@ -1667,33 +1721,24 @@ class Tally(object): A new derived tally which is the difference of this tally and the other tally or scalar value in the subtraction. + Raises + ------ + ValueError + When this method is called before the Tally is populated with data + by the StatePoint.read_results() method. + """ # Check that results have been read - if self.sum is None: - msg = 'Unable to use tally arithmetic with Tally ID={0} ' \ + if not self.derived and self.sum is None: + msg = 'Unable to use tally arithmetic with Tally ID="{0}" ' \ 'since it does not contain any results.'.format(self.id) raise ValueError(msg) - new_tally = Tally(name='derived') - new_tally.with_batch_statistics = True - if isinstance(other, Tally): - - # Check that results have been read - if other.sum is None: - msg = 'Unable to use tally arithmetic with Tally ID={0} ' \ - 'since it does not contain any results.'.format(other.id) - raise ValueError(msg) - - self._outer_product(other, new_tally, binary_op='-') - data = self._align_tally_data(other) - 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) + new_tally = self._outer_product(other, binary_op='-') elif isinstance(other, Real): - new_tally.name = self.name new_tally._mean = self._mean - other new_tally._std_dev = self._std_dev @@ -1710,8 +1755,8 @@ class Tally(object): new_tally.add_score(score) else: - msg = 'Unable to subtract {0} from Tally ' \ - 'ID={1}'.format(other, self.id) + msg = 'Unable to subtract "{0}" from Tally ' \ + 'ID="{1}"'.format(other, self.id) raise ValueError(msg) return new_tally @@ -1744,35 +1789,24 @@ class Tally(object): A new derived tally which is the product of this tally and the other tally or scalar value in the multiplication. + Raises + ------ + ValueError + When this method is called before the Tally is populated with data + by the StatePoint.read_results() method. + """ # Check that results have been read - if self.sum is None: - msg = 'Unable to use tally arithmetic with Tally ID={0} ' \ + if not self.derived and self.sum is None: + msg = 'Unable to use tally arithmetic with Tally ID="{0}" ' \ 'since it does not contain any results.'.format(self.id) raise ValueError(msg) - new_tally = Tally(name='derived') - new_tally.with_batch_statistics = True - if isinstance(other, Tally): - - # Check that results have been read - if other.sum is None: - msg = 'Unable to use tally arithmetic with Tally ID={0} ' \ - 'since it does not contain any results.'.format(other.id) - raise ValueError(msg) - - self._outer_product(other, new_tally, binary_op='*') - data = self._align_tally_data(other) - 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'] - new_tally._std_dev = np.abs(new_tally.mean) * \ - np.sqrt(self_rel_err**2 + other_rel_err**2) + new_tally = self._outer_product(other, binary_op='*') elif isinstance(other, Real): - new_tally.name = self.name new_tally._mean = self._mean * other new_tally._std_dev = self._std_dev * np.abs(other) @@ -1789,8 +1823,8 @@ class Tally(object): new_tally.add_score(score) else: - msg = 'Unable to multiply Tally ID={1} ' \ - 'by {0}'.format(self.id, other) + msg = 'Unable to multiply Tally ID="{0}" ' \ + 'by "{1}"'.format(self.id, other) raise ValueError(msg) return new_tally @@ -1823,35 +1857,24 @@ class Tally(object): A new derived tally which is the dividend of this tally and the other tally or scalar value in the division. + Raises + ------ + ValueError + When this method is called before the Tally is populated with data + by the StatePoint.read_results() method. + """ # Check that results have been read - if self.sum is None: - msg = 'Unable to use tally arithmetic with Tally ID={0} ' \ + if not self.derived and self.sum is None: + msg = 'Unable to use tally arithmetic with Tally ID="{0}" ' \ 'since it does not contain any results.'.format(self.id) raise ValueError(msg) - new_tally = Tally(name='derived') - new_tally.with_batch_statistics = True - if isinstance(other, Tally): - - # Check that results have been read - if other.sum is None: - msg = 'Unable to use tally arithmetic with Tally ID={0} ' \ - 'since it does not contain any results.'.format(other.id) - raise ValueError(msg) - - self._outer_product(other, new_tally, binary_op='/') - data = self._align_tally_data(other) - 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'] - new_tally._std_dev = np.abs(new_tally.mean) * \ - np.sqrt(self_rel_err**2 + other_rel_err**2) + new_tally = self._outer_product(other, binary_op='/') elif isinstance(other, Real): - new_tally.name = self.name new_tally._mean = self._mean / other new_tally._std_dev = self._std_dev * np.abs(1. / other) @@ -1868,8 +1891,8 @@ class Tally(object): new_tally.add_score(score) else: - msg = 'Unable to divide Tally ID={0} ' \ - 'by {1}'.format(self.id, other) + msg = 'Unable to divide Tally ID="{0}" ' \ + 'by "{1}"'.format(self.id, other) raise ValueError(msg) return new_tally @@ -1902,36 +1925,24 @@ class Tally(object): A new derived tally which is this tally raised to the power of the other tally or scalar value in the exponentiation. + Raises + ------ + ValueError + When this method is called before the Tally is populated with data + by the StatePoint.read_results() method. + """ # Check that results have been read - if self.sum is None: - msg = 'Unable to use tally arithmetic with Tally ID={0} ' \ + if not self.derived and self.sum is None: + msg = 'Unable to use tally arithmetic with Tally ID="{0}" ' \ 'since it does not contain any results.'.format(self.id) raise ValueError(msg) - new_tally = Tally(name='derived') - new_tally.with_batch_statistics = True - if isinstance(power, Tally): - - # Check that results have been read - if power.sum is None: - msg = 'Unable to use tally arithmetic with Tally ID={0} ' \ - 'since it does not contain any results.'.format(power.id) - raise ValueError(msg) - - self._outer_product(power, new_tally, binary_op='^') - data = self._align_tally_data(power) - mean_ratio = data['other']['mean'] / data['self']['mean'] - 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) + new_tally = self._outer_product(power, binary_op='^') elif isinstance(power, Real): - new_tally.name = self.name new_tally._mean = self._mean ** power self_rel_err = self.std_dev / self.mean @@ -1949,8 +1960,8 @@ class Tally(object): new_tally.add_score(score) else: - msg = 'Unable to raise Tally ID={0} to ' \ - 'power {1}'.format(self.id, power) + msg = 'Unable to raise Tally ID="{0}" to ' \ + 'power "{1}"'.format(self.id, power) raise ValueError(msg) return new_tally @@ -2108,7 +2119,7 @@ class Tally(object): # Ensure that StatePoint.read_results() was called first if self.sum is None: - msg = 'Unable to use tally arithmetic with Tally ID={0} ' \ + msg = 'Unable to use tally arithmetic with Tally ID="{0}" ' \ 'since it does not contain any results.'.format(self.id) raise ValueError(msg)