From 7fa2be860af63d2118fb9a7451e5ad56f05f6129 Mon Sep 17 00:00:00 2001 From: "wbinventor@gmail.com" Date: Sat, 6 Feb 2016 15:04:56 -0500 Subject: [PATCH] Improved modular functionalization of tally merging --- openmc/filter.py | 23 +++- openmc/tallies.py | 287 ++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 267 insertions(+), 43 deletions(-) diff --git a/openmc/filter.py b/openmc/filter.py index 54814a6b6f..430b2415f0 100644 --- a/openmc/filter.py +++ b/openmc/filter.py @@ -257,9 +257,17 @@ class Filter(object): elif self.type == 'mesh': return False - # Different energy bins are not mergeable + # Different energy bins structures must be mutually exclusive and + # share only one shared bin edge at the minimum or maximum energy elif 'energy' in self.type: - return False + # This low energy edge coincides with other's high energy edge + if self.bins[0] == other.bins[-1]: + return True + # This high energy edge coincides with other's low energy edge + elif self.bins[-1] == other.bins[0]: + return True + else: + return False else: return True @@ -288,9 +296,14 @@ class Filter(object): merged_filter = copy.deepcopy(self) # Merge unique filter bins - merged_bins = list(set(np.concatenate((self.bins, other.bins)))) - merged_filter.bins = merged_bins - merged_filter.num_bins = len(merged_bins) + merged_bins = set(np.concatenate((self.bins, other.bins))) + merged_filter.bins = list(sorted(merged_bins)) + + # Count bins in the merged filter + if 'energy' in merged_filter.type: + merged_filter.num_bins = len(merged_bins) -1 + else: + merged_filter.num_bins = len(merged_bins) return merged_filter diff --git a/openmc/tallies.py b/openmc/tallies.py index 66b99cb00c..dabbeb04ae 100644 --- a/openmc/tallies.py +++ b/openmc/tallies.py @@ -673,66 +673,162 @@ class Tally(object): self._nuclides.remove(nuclide) - def can_merge(self, tally): - """Determine if another tally can be merged with this one + 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. Parameters ---------- - tally : Tally - Tally to check for merging + other : Tally + Tally to check for mergeable scores """ - if not isinstance(tally, Tally): + # Two tallys must have the same number of filters + if len(self.filters) != len(other.filters): return False - # Must have same estimator - if self.estimator != tally.estimator: - return False - - # Must have same nuclides - if len(self.nuclides) != len(tally.nuclides): - return False - - for nuclide in self.nuclides: - if nuclide not in tally.nuclides: - return False - - # Must have same or mergeable filters - if len(self.filters) != len(tally.filters): - return False - - # Check if only one tally contains a delayed group filter - tally1_dg = False - for filter1 in self.filters: - if filter1.type == 'delayedgroup': - tally1_dg = True - - tally2_dg = False - for filter2 in tally.filters: - if filter2.type == 'delayedgroup': - tally2_dg = True - # Return False if only one tally has a delayed group filter - if (tally1_dg or tally2_dg) and not (tally1_dg and tally2_dg): + tally1_dg = self.contains_filter('delayedgroup') + tally2_dg = other.contains_filter('delayedgroup') + if sum(tally1_dg, tally2_dg) == 1: return False # Look to see if all filters are the same, or one or more can be merged for filter1 in self.filters: mergeable_filter = False - for filter2 in tally.filters: - if filter1 == filter2 or filter1.can_merge(filter2): + for filter2 in other.filters: + + # If filters match, they are mergeable + if filter1 == filter2: mergeable_filter = True break + # If filters are first mergeable filters encountered + elif filter1.can_merge(filter2) and not merge_filters: + merge_filters = True + mergeable_filter = True + break + + # If filters are the second mergeable filters encountered + elif filter1.can_merge(filter2) and merge_filters: + return False + # If no mergeable filter was found, the tallies are not mergeable if not mergeable_filter: return False - # Tallies are mergeable if all conditional checks passed + # Tally filters are mergeable if all conditional checks passed return True + 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 + ---------- + other : Tally + 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 + if no_nuclides_match: + return True + if not no_nuclides_match and not all_nuclides_match: + return False + + 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 + ---------- + other : Tally + 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: + if score not in other.scores: + all_scores_match = False + else: + 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 + + # Either all scores should match, or none should + if no_scores_match: + return True + elif not no_scores_match and not all_scores_match: + return False + + def can_merge(self, other): + """Determine if another tally can be merged with this one + + Parameters + ---------- + other : Tally + Tally to check for merging + + """ + + if not isinstance(other, Tally): + return False + + # Must have same estimator + if self.estimator != other.estimator: + return False + + # Variables to indicate matching filter bins, nuclides and scores + merge_filters = self._can_merge_filters(other) + merge_nuclides = self._can_merge_nuclides(other) + merge_scores = self._can_merge_scores(other) + + # Tallies are mergeable if only one of filters, nuclides and + # scores is mergeable + if sum(merge_filters, merge_nuclides, merge_scores) == 1: + return True + else: + return False + def merge(self, tally): """Merge another tally with this one @@ -778,6 +874,116 @@ class Tally(object): return merged_tally + def join(self, other): + """Join another tally with this one + + Parameters + ---------- + tally : Tally + Tally to join with this one + + Returns + ------- + joined_tally : Tally + Joined tallies + + """ + + if not self.can_merge(other): + msg = 'Unable to join tally ID="{0}" with ' + \ + '"{1}"'.format(other.id, self.id) + raise ValueError(msg) + + # Create deep copy of tally to return as merged tally + joined_tally = copy.deepcopy(self) + + # Differentiate Tally with a new auto-generated Tally ID + joined_tally.id = None + + # Create deep copy of other tally to use for array concatenation + other_copy = copy.deepcopy(other) + + # If two tallies can be merged along a filter's bins + if self._can_merge_filters(other): + + # Search for mergeable filters + for i, filter1 in enumerate(self.filters): + for j, filter2 in enumerate(other.filters): + if filter1 != filter2 and filter1.can_merge(filter2): + other_copy._swap_filters(other_copy.filters[i], filter1) + joined_tally.filters[i] = filter1.merge(filter2) + join_axis = i + break + + # If two tallies can be merged along nuclide bins + elif self._can_merge_nuclides(other): + join_axis = self.num_filters + + # Add unique nuclides from other tally to merged tally + for nuclide in other.nuclides: + if nuclide not in joined_tally.nuclides: + joined_tally.add_score(nuclide) + + # If two tallies can be merged along score bins + elif self._can_merge_scores(other): + join_axis = self.num_filters + 1 + + # Add unique scores from other tally to merged tally + for score in other.scores: + if score not in joined_tally.scores: + joined_tally.add_score(score) + + else: + raise ValueError('Unable to merge tallies') + + # Update filter strides in joined tally + joined_tally._update_filter_strides() + + # 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') + joined_tally._sum = \ + np.concatenate((self_sum, other_sum), axis=join_axis) + joined_tally._sum = \ + np.reshape(joined_tally._sum, joined_tally.shape) + + # 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') + joined_tally._sum_sq = \ + np.concatenate((self_sum_sq, other_sum_sq), axis=join_axis) + joined_tally._sum_sq = \ + np.reshape(joined_tally._sum_sq, joined_tally.shape) + + # 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') + joined_tally._mean = \ + np.concatenate((self_mean, other_mean), axis=join_axis) + joined_tally._mean = \ + np.reshape(joined_tally._mean, joined_tally.shape) + + # 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') + joined_tally._std_dev = \ + np.concatenate((self_std_dev, other_std_dev), axis=join_axis) + joined_tally._std_dev = \ + np.reshape(joined_tally._std_dev, joined_tally.shape) + + # Sparsify joined tally if both tallies are sparse + joined_tally.sparse = self.sparse and other.sparse + + # Add triggers from other tally to merged tally + for trigger in other.triggers: + joined_tally.add_trigger(trigger) + + return joined_tally + def get_tally_xml(self): """Return XML representation of the tally @@ -1980,8 +2186,7 @@ class Tally(object): # Check that the filters exist in the tally and are not the same if filter1 == filter2: - msg = 'Unable to swap a filter with itself' - raise ValueError(msg) + return elif filter1 not in self.filters: msg = 'Unable to swap "{0}" filter1 in Tally ID="{1}" since it ' \ 'does not contain such a filter'.format(filter1.type, self.id) @@ -2012,6 +2217,8 @@ class Tally(object): else: filter2_bins = [filter2.get_bin(i) for i in range(filter2.num_bins)] + # FIXME: Why doesn't this swap data for sum and sum_sq??? + # Adjust the mean data array to relect the new filter order if self.mean is not None: for bin1, bin2 in itertools.product(filter1_bins, filter2_bins): @@ -2083,6 +2290,8 @@ class Tally(object): self.nuclides[nuclide1_index] = nuclide2 self.nuclides[nuclide2_index] = nuclide1 + # FIXME: Why doesn't this swap data for sum and sum_sq??? + # Adjust the mean data array to relect the new nuclide order if self.mean is not None: nuclide1_mean = self.mean[:, nuclide1_index, :].copy() @@ -2155,6 +2364,8 @@ class Tally(object): self.scores[score1_index] = score2 self.scores[score2_index] = score1 + # FIXME: Why doesn't this swap data for sum and sum_sq??? + # Adjust the mean data array to relect the new nuclide order if self.mean is not None: score1_mean = self.mean[:, :, score1_index].copy()