Source code for openmc.mgxs.groups

from collections.abc import Iterable
import copy
from numbers import Real

import numpy as np

import openmc.checkvalue as cv
import openmc.mgxs


[docs] class EnergyGroups: """An energy group structure used for multigroup cross-sections. Parameters ---------- group_edges : Iterable of float or str The energy group boundaries in [eV] or the name of the group structure (Must be a valid key in the openmc.mgxs.GROUP_STRUCTURES dictionary). .. versionchanged:: 0.14.0 Changed to allow a string specifying the group structure name. Attributes ---------- group_edges : np.ndarray The energy group boundaries in [eV] num_groups : int The number of energy groups """ def __init__(self, group_edges): if isinstance(group_edges, str): self._name = group_edges.upper() group_edges = openmc.mgxs.GROUP_STRUCTURES[self._name] self.group_edges = group_edges def __deepcopy__(self, memo): existing = memo.get(id(self)) # If this is the first time we have tried to copy object, create copy if existing is None: clone = type(self).__new__(type(self)) clone._group_edges = copy.deepcopy(self.group_edges, memo) memo[id(self)] = clone return clone # If this object has been copied before, return the first copy made else: return existing def __eq__(self, other): if not isinstance(other, EnergyGroups): return False elif self.num_groups != other.num_groups: return False elif np.allclose(self.group_edges, other.group_edges): return True else: return False def __hash__(self): return hash(tuple(self.group_edges)) def __repr__(self): if hasattr(self, '_name'): return f"<EnergyGroups: {self.num_groups} groups ({self._name})>" else: return f"<EnergyGroups: {self.num_groups} groups>" @property def group_edges(self): return self._group_edges @group_edges.setter def group_edges(self, edges): cv.check_type('group edges', edges, Iterable, Real) cv.check_increasing('group edges', edges) cv.check_greater_than('number of group edges', len(edges), 1) self._group_edges = np.array(edges) @property def num_groups(self): return len(self.group_edges) - 1
[docs] def get_group(self, energy): """Returns the energy group in which the given energy resides. Parameters ---------- energy : float The energy of interest in eV Returns ------- Integral The energy group index, starting at 1 for the highest energies Raises ------ ValueError If the group edges have not yet been set. """ if self.group_edges is None: msg = 'Unable to get energy group for energy "{0}" eV since ' \ 'the group edges have not yet been set'.format(energy) raise ValueError(msg) index = np.where(self.group_edges > energy)[0][0] group = self.num_groups - index + 1 return group
[docs] def get_group_bounds(self, group): """Returns the energy boundaries for the energy group of interest. Parameters ---------- group : int The energy group index, starting at 1 for the highest energies Returns ------- 2-tuple The low and high energy bounds for the group in eV Raises ------ ValueError If the group edges have not yet been set. """ if self.group_edges is None: msg = 'Unable to get energy group bounds for group "{0}" since ' \ 'the group edges have not yet been set'.format(group) raise ValueError(msg) cv.check_greater_than('group', group, 0) cv.check_less_than('group', group, self.num_groups, equality=True) lower = self.group_edges[self.num_groups-group] upper = self.group_edges[self.num_groups-group+1] return lower, upper
[docs] def get_group_indices(self, groups='all'): """Returns the array indices for one or more energy groups. Parameters ---------- groups : str, tuple The energy groups of interest - a tuple of the energy group indices, starting at 1 for the highest energies (default is 'all') Returns ------- numpy.ndarray The ndarray array indices for each energy group of interest Raises ------ ValueError If the group edges have not yet been set, or if a group is requested that is outside the bounds of the number of energy groups. """ if self.group_edges is None: msg = 'Unable to get energy group indices for groups "{0}" since ' \ 'the group edges have not yet been set'.format(groups) raise ValueError(msg) if groups == 'all': return np.arange(self.num_groups) else: indices = np.zeros(len(groups), dtype=int) for i, group in enumerate(groups): cv.check_greater_than('group', group, 0) cv.check_less_than('group', group, self.num_groups, equality=True) indices[i] = group - 1 return indices
[docs] def get_condensed_groups(self, coarse_groups): """Return a coarsened version of this EnergyGroups object. This method merges together energy groups in this object into wider energy groups as defined by the list of groups specified by the user, and returns a new, coarse EnergyGroups object. Parameters ---------- coarse_groups : Iterable of 2-tuple The energy groups of interest - a list of 2-tuples, each directly corresponding to one of the new coarse groups. The values in the 2-tuples are upper/lower energy groups used to construct a new coarse group. For example, if [(1,2), (3,4)] was used as the coarse groups, fine groups 1 and 2 would be merged into coarse group 1 while fine groups 3 and 4 would be merged into coarse group 2. Returns ------- openmc.mgxs.EnergyGroups A coarsened version of this EnergyGroups object. Raises ------ ValueError If the group edges have not yet been set. """ cv.check_type('group edges', coarse_groups, Iterable) for group in coarse_groups: cv.check_type('group edges', group, Iterable) cv.check_length('group edges', group, 2) cv.check_greater_than('lower group', group[0], 1, True) cv.check_less_than('lower group', group[0], self.num_groups, True) cv.check_greater_than('upper group', group[0], 1, True) cv.check_less_than('upper group', group[0], self.num_groups, True) cv.check_less_than('lower group', group[0], group[1], False) # Compute the group indices into the coarse group group_bounds = [group[1] for group in coarse_groups] group_bounds.insert(0, coarse_groups[0][0]) # Determine the indices mapping the fine-to-coarse energy groups group_bounds = np.asarray(group_bounds) group_indices = np.flipud(self.num_groups - group_bounds) group_indices[-1] += 1 # Determine the edges between coarse energy groups and sort # in increasing order in case the user passed in unordered groups group_edges = self.group_edges[group_indices] group_edges = np.sort(group_edges) # Create a new condensed EnergyGroups object return EnergyGroups(group_edges)
[docs] def can_merge(self, other): """Determine if energy groups can be merged with another. Parameters ---------- other : openmc.mgxs.EnergyGroups EnergyGroups to compare with Returns ------- bool Whether the energy groups can be merged """ if not isinstance(other, EnergyGroups): return False # If the energy group structures match then groups are mergeable if self == other: return True # This low energy edge coincides with other's high energy edge if self.group_edges[0] == other.group_edges[-1]: return True # This high energy edge coincides with other's low energy edge elif self.group_edges[-1] == other.group_edges[0]: return True else: return False
[docs] def merge(self, other): """Merge this energy groups with another. Parameters ---------- other : openmc.mgxs.EnergyGroups EnergyGroups to merge with Returns ------- merged_groups : openmc.mgxs.EnergyGroups EnergyGroups resulting from the merge """ if not self.can_merge(other): raise ValueError('Unable to merge energy groups') # Create deep copy to return as merged energy groups merged_groups = copy.deepcopy(self) # Merge unique filter bins merged_edges = np.concatenate((self.group_edges, other.group_edges)) merged_edges = np.unique(merged_edges) merged_edges = sorted(merged_edges) # Assign merged edges to merged groups merged_groups.group_edges = list(merged_edges) return merged_groups
[docs] def convert_flux_groups(flux, source_groups, target_groups): """Convert flux spectrum between energy group structures. Uses flux-per-unit-lethargy conservation, which assumes constant flux per unit lethargy within each source group and distributes flux to target groups proportionally to their lethargy width. .. versionadded:: 0.15.4 Parameters ---------- flux : Iterable of float Flux values for source groups. Length must equal source_groups.num_groups. source_groups : EnergyGroups or str Energy group structure of the input flux with boundaries in [eV]. Can be an EnergyGroups instance or the name of a group structure (e.g., 'CCFE-709'). target_groups : EnergyGroups or str Target energy group structure with boundaries in [eV]. Can be an EnergyGroups instance or the name of a group structure (e.g., 'UKAEA-1102'). Returns ------- numpy.ndarray Flux values for target groups. Total flux is conserved for overlapping energy regions. Raises ------ TypeError If source_groups or target_groups is not EnergyGroups or str ValueError If flux length doesn't match source_groups, or flux contains negative, NaN, or infinite values See Also -------- EnergyGroups : Energy group structure class Notes ----- The assumption of constant flux per unit lethargy within each source group is physically reasonable for most reactor spectra but is not exact. For best accuracy, use source spectra with sufficiently fine energy resolution. Examples -------- Convert FNS 709-group flux to UKAEA-1102 structure: >>> import numpy as np >>> flux_709 = np.load('tests/fns_flux_709.npy') >>> flux_1102 = openmc.mgxs.convert_flux_groups(flux_709, 'CCFE-709', 'UKAEA-1102') Convert using EnergyGroups instances: >>> source = openmc.mgxs.EnergyGroups([1.0, 10.0, 100.0]) >>> target = openmc.mgxs.EnergyGroups([1.0, 5.0, 10.0, 50.0, 100.0]) >>> flux_target = openmc.mgxs.convert_flux_groups([1e8, 2e8], source, target) References ---------- .. [1] J. J. Duderstadt and L. J. Hamilton, "Nuclear Reactor Analysis," John Wiley & Sons, 1976. .. [2] M. Fleming and J.-Ch. Sublet, "FISPACT-II User Manual," UKAEA-R(18)001, UK Atomic Energy Authority, 2018. See GRPCONVERT keyword. """ # Handle string group structure names if isinstance(source_groups, str): source_groups = EnergyGroups(source_groups) if isinstance(target_groups, str): target_groups = EnergyGroups(target_groups) # Type validation cv.check_type('source_groups', source_groups, EnergyGroups) cv.check_type('target_groups', target_groups, EnergyGroups) # Convert flux to numpy array flux = np.asarray(flux, dtype=np.float64) if flux.ndim != 1: raise ValueError(f'flux must be 1-dimensional, got shape {flux.shape}') # Validate flux length matches source groups if len(flux) != source_groups.num_groups: raise ValueError( f'Length of flux ({len(flux)}) must equal number of source ' f'groups ({source_groups.num_groups})' ) # Check for invalid flux values if np.any(np.isnan(flux)): raise ValueError('flux contains NaN values') if np.any(np.isinf(flux)): raise ValueError('flux contains infinite values') if np.any(flux < 0): raise ValueError('flux values must be non-negative') # Get energy edges source_edges = source_groups.group_edges target_edges = target_groups.group_edges num_target = target_groups.num_groups # Initialize output array flux_target = np.zeros(num_target) # Main conversion loop: distribute flux using lethargy weighting for idx_src, flux_src in enumerate(flux): if flux_src == 0: continue e_low_src = source_edges[idx_src] e_high_src = source_edges[idx_src + 1] lethargy_src = np.log(e_high_src / e_low_src) for idx_tgt in range(num_target): e_low_tgt = target_edges[idx_tgt] e_high_tgt = target_edges[idx_tgt + 1] # Skip non-overlapping groups if e_high_tgt <= e_low_src or e_low_tgt >= e_high_src: continue # Calculate overlap region e_low_overlap = max(e_low_src, e_low_tgt) e_high_overlap = min(e_high_src, e_high_tgt) lethargy_overlap = np.log(e_high_overlap / e_low_overlap) # Distribute flux proportionally to lethargy fraction flux_target[idx_tgt] += flux_src * (lethargy_overlap / lethargy_src) return flux_target