"""This module can be used to specify parameters used for coarse mesh finite
difference (CMFD) acceleration in OpenMC. CMFD was first proposed by [Smith]_
and is widely used in accelerating neutron transport problems.
References
----------
.. [Smith] K. Smith, "Nodal method storage reduction by non-linear
iteration", *Trans. Am. Nucl. Soc.*, **44**, 265 (1983).
"""
from collections.abc import Iterable, Mapping
from contextlib import contextmanager
from numbers import Real, Integral
import sys
import time
import warnings
import h5py
import numpy as np
from scipy import sparse
import openmc.lib
from .checkvalue import (check_type, check_length, check_value,
check_greater_than, check_less_than)
from .exceptions import OpenMCError
# See if mpi4py module can be imported, define have_mpi global variable
try:
from mpi4py import MPI
have_mpi = True
except ImportError:
have_mpi = False
# Maximum/minimum neutron energies
_ENERGY_MAX_NEUTRON = np.inf
_ENERGY_MIN_NEUTRON = 0.
# Tolerance for detecting zero flux values
_TINY_BIT = 1.e-8
# For non-accelerated regions on coarse mesh overlay
_CMFD_NOACCEL = -1
# Constant to represent a zero flux "albedo"
_ZERO_FLUX = 999.0
# Map that returns index of current direction in numpy current matrix
_CURRENTS = {
'out_left': 0, 'in_left': 1, 'out_right': 2, 'in_right': 3,
'out_back': 4, 'in_back': 5, 'out_front': 6, 'in_front': 7,
'out_bottom': 8, 'in_bottom': 9, 'out_top': 10, 'in_top': 11
}
[docs]class CMFDMesh:
"""A structured Cartesian mesh used for CMFD acceleration.
Attributes
----------
lower_left : Iterable of float
The lower-left corner of a regular structured mesh. If only two
coordinates are given, it is assumed that the mesh is an x-y mesh.
upper_right : Iterable of float
The upper-right corner of a regular structured mesh. If only two
coordinates are given, it is assumed that the mesh is an x-y mesh.
dimension : Iterable of int
The number of mesh cells in each direction for a regular structured
mesh.
width : Iterable of float
The width of mesh cells in each direction for a regular structured
mesh
energy : Iterable of float
Energy bins in eV, listed in ascending order (e.g. [0.0, 0.625e-1,
20.0e6]) for CMFD tallies and acceleration. If no energy bins are
listed, OpenMC automatically assumes a one energy group calculation
over the entire energy range.
albedo : Iterable of float
Surface ratio of incoming to outgoing partial currents on global
boundary conditions. They are listed in the following order: -x +x -y
+y -z +z.
map : Iterable of int
An optional acceleration map can be specified to overlay on the coarse
mesh spatial grid. If this option is used, a ``0`` is used for a
non-accelerated region and a ``1`` is used for an accelerated region.
For a simple 4x4 coarse mesh with a 2x2 fuel lattice surrounded by
reflector, the map is:
::
[0, 0, 0, 0,
0, 1, 1, 0,
0, 1, 1, 0,
0, 0, 0, 0]
Therefore a 2x2 system of equations is solved rather than a 4x4. This
is extremely important to use in reflectors as neutrons will not
contribute to any tallies far away from fission source neutron regions.
A ``1`` must be used to identify any fission source region.
mesh_type : str
Type of structured mesh to use. Acceptable values are:
* "regular" - Use RegularMesh to define CMFD mesh
* "rectilinear" - Use RectilinearMesh to define CMFD
grid : Iterable of Iterable of float
Grid used to define RectilinearMesh. First dimension must have length
3 where grid[0], grid[1], and grid[2] correspond to the x-, y-, and
z-grids respectively
"""
def __init__(self):
self._lower_left = None
self._upper_right = None
self._dimension = None
self._width = None
self._energy = None
self._albedo = None
self._map = None
self._mesh_type = 'regular'
self._grid = None
def __repr__(self):
outstr = type(self).__name__ + '\n'
if self._mesh_type == 'regular':
outstr += (self._get_repr(self._lower_left, "Lower left") + "\n" +
self._get_repr(self._upper_right, "Upper right") + "\n" +
self._get_repr(self._dimension, "Dimension") + "\n" +
self._get_repr(self._width, "Width") + "\n" +
self._get_repr(self._albedo, "Albedo"))
elif self._mesh_type == 'rectilinear':
outstr += (self._get_repr(self._grid[0], "X-grid") + "\n" +
self._get_repr(self._grid[1], "Y-grid") + "\n" +
self._get_repr(self._grid[2], "Z-grid"))
return outstr
def _get_repr(self, list_var, label):
outstr = f"\t{label:<11} = "
if list(list_var):
outstr += ", ".join(str(i) for i in list_var)
return outstr
@property
def lower_left(self):
return self._lower_left
@property
def upper_right(self):
return self._upper_right
@property
def dimension(self):
return self._dimension
@property
def width(self):
return self._width
@property
def energy(self):
return self._energy
@property
def albedo(self):
return self._albedo
@property
def map(self):
return self._map
@property
def mesh_type(self):
return self._mesh_type
@property
def grid(self):
return self._grid
@lower_left.setter
def lower_left(self, lower_left):
check_type('CMFD mesh lower_left', lower_left, Iterable, Real)
check_length('CMFD mesh lower_left', lower_left, 2, 3)
self._lower_left = lower_left
self._display_mesh_warning('regular', 'CMFD mesh lower_left')
@upper_right.setter
def upper_right(self, upper_right):
check_type('CMFD mesh upper_right', upper_right, Iterable, Real)
check_length('CMFD mesh upper_right', upper_right, 2, 3)
self._upper_right = upper_right
self._display_mesh_warning('regular', 'CMFD mesh upper_right')
@dimension.setter
def dimension(self, dimension):
check_type('CMFD mesh dimension', dimension, Iterable, Integral)
check_length('CMFD mesh dimension', dimension, 2, 3)
for d in dimension:
check_greater_than('CMFD mesh dimension', d, 0)
self._dimension = dimension
@width.setter
def width(self, width):
check_type('CMFD mesh width', width, Iterable, Real)
check_length('CMFD mesh width', width, 2, 3)
for w in width:
check_greater_than('CMFD mesh width', w, 0)
self._width = width
self._display_mesh_warning('regular', 'CMFD mesh width')
@energy.setter
def energy(self, energy):
check_type('CMFD mesh energy', energy, Iterable, Real)
for e in energy:
check_greater_than('CMFD mesh energy', e, 0, True)
self._energy = energy
@albedo.setter
def albedo(self, albedo):
check_type('CMFD mesh albedo', albedo, Iterable, Real)
check_length('CMFD mesh albedo', albedo, 6)
for a in albedo:
check_greater_than('CMFD mesh albedo', a, 0, True)
check_less_than('CMFD mesh albedo', a, 1, True)
self._albedo = albedo
@map.setter
def map(self, mesh_map):
check_type('CMFD mesh map', mesh_map, Iterable, Integral)
for m in mesh_map:
check_value('CMFD mesh map', m, [0, 1])
self._map = mesh_map
@mesh_type.setter
def mesh_type(self, mesh_type):
check_value('CMFD mesh type', mesh_type, ['regular', 'rectilinear'])
self._mesh_type = mesh_type
@grid.setter
def grid(self, grid):
grid_length = 3
dims = ['x', 'y', 'z']
check_length('CMFD mesh grid', grid, grid_length)
for i in range(grid_length):
check_type(f'CMFD mesh {dims[i]}-grid', grid[i], Iterable,
Real)
check_greater_than(f'CMFD mesh {dims[i]}-grid length',
len(grid[i]), 1)
self._grid = [np.array(g) for g in grid]
self._display_mesh_warning('rectilinear', 'CMFD mesh grid')
def _display_mesh_warning(self, mesh_type, variable_label):
if self._mesh_type != mesh_type:
warn_msg = (f'Setting {variable_label} if mesh type is not set to '
f'{mesh_type} will have no effect')
warnings.warn(warn_msg, RuntimeWarning)
[docs]class CMFDRun:
r"""Class for running CMFD acceleration through the C API.
Attributes
----------
tally_begin : int
Batch number at which CMFD tallies should begin accumulating
solver_begin: int
Batch number at which CMFD solver should start executing
ref_d : list of floats
List of reference diffusion coefficients to fix CMFD parameters to
display : dict
Dictionary indicating which CMFD results to output. Note that CMFD
k-effective will always be outputted. Acceptable keys are:
* "balance" - Whether to output RMS [%] of the residual from the
neutron balance equation on CMFD tallies (bool)
* "dominance" - Whether to output the estimated dominance ratio from
the CMFD iterations (bool)
* "entropy" - Whether to output the *entropy* of the CMFD predicted
fission source (bool)
* "source" - Whether to output the RMS [%] between the OpenMC fission
source and CMFD fission source (bool)
downscatter : bool
Indicate whether an effective downscatter cross section should be used
when using 2-group CMFD.
feedback : bool
Indicate whether or not the CMFD diffusion result is used to adjust the weight
of fission source neutrons on the next OpenMC batch. Defaults to False.
cmfd_ktol : float
Tolerance on the eigenvalue when performing CMFD power iteration
mesh : openmc.cmfd.CMFDMesh
Structured mesh to be used for acceleration
norm : float
Normalization factor applied to the CMFD fission source distribution
power_monitor : bool
View convergence of power iteration during CMFD acceleration
run_adjoint : bool
Perform adjoint calculation on the last batch
w_shift : float
Optional Wielandt shift parameter for accelerating power iterations. By
default, it is very large so there is effectively no impact.
stol : float
Tolerance on the fission source when performing CMFD power iteration
reset : list of int
List of batch numbers at which CMFD tallies should be reset
write_matrices : bool
Write sparse matrices that are used during CMFD acceleration (loss,
production) and resultant normalized flux vector phi to file
spectral : float
Optional spectral radius that can be used to accelerate the convergence
of Gauss-Seidel iterations during CMFD power iteration.
gauss_seidel_tolerance : Iterable of float
Two parameters specifying the absolute inner tolerance and the relative
inner tolerance for Gauss-Seidel iterations when performing CMFD.
adjoint_type : {'physical', 'math'}
Stores type of adjoint calculation that should be performed.
``run_adjoint`` must be true for an adjoint calculation to be
performed. Options are:
* "physical" - Create adjoint matrices from physical parameters of
CMFD problem
* "math" - Create adjoint matrices mathematically as the transpose of
loss and production CMFD matrices
window_type : {'expanding', 'rolling', 'none'}
Specifies type of tally window scheme to use to accumulate CMFD
tallies. Options are:
* "expanding" - Have an expanding window that doubles in size
to give more weight to more recent tallies as more generations are
simulated
* "rolling" - Have a fixed window size that aggregates tallies from
the same number of previous generations tallied
* "none" - Don't use a windowing scheme so that all tallies from last
time they were reset are used for the CMFD algorithm.
window_size : int
Size of window to use for tally window scheme. Only relevant when
window_type is set to "rolling"
indices : numpy.ndarray
Stores spatial and group dimensions as [nx, ny, nz, ng]
cmfd_src : numpy.ndarray
CMFD source distribution calculated from solving CMFD equations
entropy : list of floats
"Shannon entropy" from CMFD fission source, stored for each generation
that CMFD is invoked
balance : list of floats
RMS of neutron balance equations, stored for each generation that CMFD
is invoked
src_cmp : list of floats
RMS deviation of OpenMC and CMFD normalized source, stored for each
generation that CMFD is invoked
dom : list of floats
Dominance ratio from solving CMFD matrix equations, stored for each
generation that CMFD is invoked
k_cmfd : list of floats
List of CMFD k-effectives, stored for each generation that CMFD is
invoked
time_cmfd : float
Time for entire CMFD calculation, in seconds
time_cmfdbuild : float
Time for building CMFD matrices, in seconds
time_cmfdsolve : float
Time for solving CMFD matrix equations, in seconds
use_all_threads : bool
Whether to use all threads allocated to OpenMC for CMFD solver
intracomm : mpi4py.MPI.Intracomm or None
MPI intercommunicator for running MPI commands
"""
def __init__(self):
"""Constructor for CMFDRun class. Default values for instance variables
set in this method.
"""
# Variables that users can modify
self._tally_begin = 1
self._solver_begin = 1
self._ref_d = np.array([])
self._display = {'balance': False, 'dominance': False,
'entropy': False, 'source': False}
self._downscatter = False
self._feedback = False
self._cmfd_ktol = 1.e-8
self._mesh = None
self._norm = 1.
self._power_monitor = False
self._run_adjoint = False
self._w_shift = 1.e6
self._stol = 1.e-8
self._reset = []
self._write_matrices = False
self._spectral = 0.0
self._gauss_seidel_tolerance = [1.e-10, 1.e-5]
self._adjoint_type = 'physical'
self._window_type = 'none'
self._window_size = 10
self._intracomm = None
self._use_all_threads = False
# External variables used during runtime but users cannot control
self._set_reference_params = False
self._indices = np.zeros(4, dtype=np.int32)
self._egrid = None
self._albedo = None
self._coremap = None
self._mesh_id = None
self._tally_ids = None
self._energy_filters = None
self._cmfd_on = False
self._mat_dim = _CMFD_NOACCEL
self._keff_bal = None
self._keff = None
self._adj_keff = None
self._phi = None
self._adj_phi = None
self._openmc_src_rate = None
self._flux_rate = None
self._total_rate = None
self._p1scatt_rate = None
self._scatt_rate = None
self._nfiss_rate = None
self._current_rate = None
self._flux = None
self._totalxs = None
self._p1scattxs = None
self._scattxs = None
self._nfissxs = None
self._diffcof = None
self._dtilde = None
self._dhat = None
self._hxyz = None
self._current = None
self._cmfd_src = None
self._openmc_src = None
self._entropy = []
self._balance = []
self._src_cmp = []
self._dom = []
self._k_cmfd = []
self._resnb = None
self._reset_every = None
self._time_cmfd = None
self._time_cmfdbuild = None
self._time_cmfdsolve = None
# All index-related variables, for numpy vectorization
self._first_x_accel = None
self._last_x_accel = None
self._first_y_accel = None
self._last_y_accel = None
self._first_z_accel = None
self._last_z_accel = None
self._notfirst_x_accel = None
self._notlast_x_accel = None
self._notfirst_y_accel = None
self._notlast_y_accel = None
self._notfirst_z_accel = None
self._notlast_z_accel = None
self._is_adj_ref_left = None
self._is_adj_ref_right = None
self._is_adj_ref_back = None
self._is_adj_ref_front = None
self._is_adj_ref_bottom = None
self._is_adj_ref_top = None
self._accel_idxs = None
self._accel_neig_left_idxs = None
self._accel_neig_right_idxs = None
self._accel_neig_back_idxs = None
self._accel_neig_front_idxs = None
self._accel_neig_bot_idxs = None
self._accel_neig_top_idxs = None
self._loss_row = None
self._loss_col = None
self._prod_row = None
self._prod_col = None
@property
def tally_begin(self):
return self._tally_begin
@property
def solver_begin(self):
return self._solver_begin
@property
def ref_d(self):
return self._ref_d
@property
def display(self):
return self._display
@property
def downscatter(self):
return self._downscatter
@property
def feedback(self):
return self._feedback
@property
def cmfd_ktol(self):
return self._cmfd_ktol
@property
def mesh(self):
return self._mesh
@property
def norm(self):
return self._norm
@property
def adjoint_type(self):
return self._adjoint_type
@property
def window_type(self):
return self._window_type
@property
def window_size(self):
return self._window_size
@property
def power_monitor(self):
return self._power_monitor
@property
def run_adjoint(self):
return self._run_adjoint
@property
def w_shift(self):
return self._w_shift
@property
def stol(self):
return self._stol
@property
def spectral(self):
return self._spectral
@property
def reset(self):
return self._reset
@property
def write_matrices(self):
return self._write_matrices
@property
def gauss_seidel_tolerance(self):
return self._gauss_seidel_tolerance
@property
def indices(self):
return self._indices
@property
def use_all_threads(self):
return self._use_all_threads
@property
def cmfd_src(self):
return self._cmfd_src
@property
def dom(self):
return self._dom
@property
def src_cmp(self):
return self._src_cmp
@property
def balance(self):
return self._balance
@property
def entropy(self):
return self._entropy
@property
def k_cmfd(self):
return self._k_cmfd
@tally_begin.setter
def tally_begin(self, begin):
check_type('CMFD tally begin batch', begin, Integral)
check_greater_than('CMFD tally begin batch', begin, 0)
self._tally_begin = begin
@solver_begin.setter
def solver_begin(self, begin):
check_type('CMFD feedback begin batch', begin, Integral)
check_greater_than('CMFD feedback begin batch', begin, 0)
self._solver_begin = begin
@ref_d.setter
def ref_d(self, diff_params):
check_type('Reference diffusion params', diff_params,
Iterable, Real)
self._ref_d = np.array(diff_params)
@display.setter
def display(self, display):
check_type('display', display, Mapping)
for key, value in display.items():
check_value('display key', key,
('balance', 'entropy', 'dominance', 'source'))
check_type(f"display['{key}']", value, bool)
self._display[key] = value
@downscatter.setter
def downscatter(self, downscatter):
check_type('CMFD downscatter', downscatter, bool)
self._downscatter = downscatter
@feedback.setter
def feedback(self, feedback):
check_type('CMFD feedback', feedback, bool)
self._feedback = feedback
@cmfd_ktol.setter
def cmfd_ktol(self, cmfd_ktol):
check_type('CMFD eigenvalue tolerance', cmfd_ktol, Real)
self._cmfd_ktol = cmfd_ktol
@mesh.setter
def mesh(self, cmfd_mesh):
check_type('CMFD mesh', cmfd_mesh, CMFDMesh)
if cmfd_mesh.mesh_type == 'regular':
# Check dimension defined
if cmfd_mesh.dimension is None:
raise ValueError('CMFD regular mesh requires spatial '
'dimensions to be specified')
# Check lower left defined
if cmfd_mesh.lower_left is None:
raise ValueError('CMFD regular mesh requires lower left '
'coordinates to be specified')
# Check that both upper right and width both not defined
if cmfd_mesh.upper_right is not None and cmfd_mesh.width is not None:
raise ValueError('Both upper right coordinates and width '
'cannot be specified for CMFD regular mesh')
# Check that at least one of width or upper right is defined
if cmfd_mesh.upper_right is None and cmfd_mesh.width is None:
raise ValueError('CMFD regular mesh requires either upper right '
'coordinates or width to be specified')
# Check width and lower length are same dimension and define
# upper_right
if cmfd_mesh.width is not None:
check_length('CMFD mesh width', cmfd_mesh.width,
len(cmfd_mesh.lower_left))
cmfd_mesh.upper_right = np.array(cmfd_mesh.lower_left) + \
np.array(cmfd_mesh.width) * np.array(cmfd_mesh.dimension)
# Check upper_right and lower length are same dimension and define
# width
elif cmfd_mesh.upper_right is not None:
check_length('CMFD mesh upper right', cmfd_mesh.upper_right,
len(cmfd_mesh.lower_left))
# Check upper right coordinates are greater than lower left
if np.any(np.array(cmfd_mesh.upper_right) <=
np.array(cmfd_mesh.lower_left)):
raise ValueError('CMFD regular mesh requires upper right '
'coordinates to be greater than lower '
'left coordinates')
cmfd_mesh.width = np.true_divide((np.array(cmfd_mesh.upper_right) -
np.array(cmfd_mesh.lower_left)),
np.array(cmfd_mesh.dimension))
elif cmfd_mesh.mesh_type == 'rectilinear':
# Check dimension defined
if cmfd_mesh.grid is None:
raise ValueError('CMFD rectilinear mesh requires spatial '
'grid to be specified')
cmfd_mesh.dimension = [len(cmfd_mesh.grid[i]) - 1 for i in range(3)]
self._mesh = cmfd_mesh
@norm.setter
def norm(self, norm):
check_type('CMFD norm', norm, Real)
self._norm = norm
@adjoint_type.setter
def adjoint_type(self, adjoint_type):
check_type('CMFD adjoint type', adjoint_type, str)
check_value('CMFD adjoint type', adjoint_type,
['math', 'physical'])
self._adjoint_type = adjoint_type
@window_type.setter
def window_type(self, window_type):
check_type('CMFD window type', window_type, str)
check_value('CMFD window type', window_type,
['none', 'rolling', 'expanding'])
self._window_type = window_type
@window_size.setter
def window_size(self, window_size):
check_type('CMFD window size', window_size, Integral)
check_greater_than('CMFD window size', window_size, 0)
if self._window_type != 'rolling':
warn_msg = 'Window size will have no effect on CMFD simulation ' \
'unless window type is set to "rolling".'
warnings.warn(warn_msg, RuntimeWarning)
self._window_size = window_size
@power_monitor.setter
def power_monitor(self, power_monitor):
check_type('CMFD power monitor', power_monitor, bool)
self._power_monitor = power_monitor
@run_adjoint.setter
def run_adjoint(self, run_adjoint):
check_type('CMFD run adjoint', run_adjoint, bool)
self._run_adjoint = run_adjoint
@w_shift.setter
def w_shift(self, w_shift):
check_type('CMFD Wielandt shift', w_shift, Real)
self._w_shift = w_shift
@stol.setter
def stol(self, stol):
check_type('CMFD fission source tolerance', stol, Real)
self._stol = stol
@spectral.setter
def spectral(self, spectral):
check_type('CMFD spectral radius', spectral, Real)
self._spectral = spectral
@reset.setter
def reset(self, reset):
check_type('tally reset batches', reset, Iterable, Integral)
self._reset = reset
@write_matrices.setter
def write_matrices(self, write_matrices):
check_type('CMFD write matrices', write_matrices, bool)
self._write_matrices = write_matrices
@gauss_seidel_tolerance.setter
def gauss_seidel_tolerance(self, gauss_seidel_tolerance):
check_type('CMFD Gauss-Seidel tolerance', gauss_seidel_tolerance,
Iterable, Real)
check_length('Gauss-Seidel tolerance', gauss_seidel_tolerance, 2)
self._gauss_seidel_tolerance = gauss_seidel_tolerance
@use_all_threads.setter
def use_all_threads(self, use_all_threads):
check_type('CMFD use all threads', use_all_threads, bool)
self._use_all_threads = use_all_threads
[docs] def run(self, **kwargs):
"""Run OpenMC with coarse mesh finite difference acceleration
This method is called by the user to run CMFD once instance variables of
CMFDRun class are set
Parameters
----------
**kwargs
All keyword arguments are passed to
:func:`openmc.lib.run_in_memory`.
"""
with self.run_in_memory(**kwargs):
for _ in self.iter_batches():
pass
[docs] @contextmanager
def run_in_memory(self, **kwargs):
""" Context manager for running CMFD functions with OpenMC shared
library functions.
This function can be used with a 'with' statement to ensure the
CMFDRun class is properly initialized/finalized. For example::
from openmc import cmfd
cmfd_run = cmfd.CMFDRun()
with cmfd_run.run_in_memory():
do_stuff_before_simulation_start()
for _ in cmfd_run.iter_batches():
do_stuff_between_batches()
Parameters
----------
**kwargs
All keyword arguments passed to :func:`openmc.lib.run_in_memory`.
"""
# Store intracomm for part of CMFD routine where MPI reduce and
# broadcast calls are made
if 'intracomm' in kwargs and kwargs['intracomm'] is not None:
self._intracomm = kwargs['intracomm']
elif have_mpi:
self._intracomm = MPI.COMM_WORLD
# Run and pass arguments to C API run_in_memory function
with openmc.lib.run_in_memory(**kwargs):
self.init()
yield
self.finalize()
[docs] def iter_batches(self):
""" Iterator over batches.
This function returns a generator-iterator that allows Python code to
be run between batches when running an OpenMC simulation with CMFD.
It should be used in conjunction with
:func`openmc.cmfd.CMFDRun.run_in_memory` to ensure proper
initialization/finalization of CMFDRun instance.
"""
status = 0
while status == 0:
status = self.next_batch()
yield
[docs] def init(self):
""" Initialize CMFDRun instance by setting up CMFD parameters and
calling :func:`openmc.lib.simulation_init`
"""
# Configure CMFD parameters
self._configure_cmfd()
# Create tally objects
self._create_cmfd_tally()
if openmc.lib.master():
# Compute and store array indices used to build cross section
# arrays
self._precompute_array_indices()
# Compute and store row and column indices used to build CMFD
# matrices
self._precompute_matrix_indices()
# Initialize all variables used for linear solver in C++
self._initialize_linsolver()
# Initialize simulation
openmc.lib.simulation_init()
# Set cmfd_run variable to True through C API
openmc.lib.settings.cmfd_run = True
[docs] def next_batch(self):
""" Run next batch for CMFDRun.
Returns
-------
int
Status after running a batch (0=normal, 1=reached maximum number of
batches, 2=tally triggers reached)
"""
# Initialize CMFD batch
self._cmfd_init_batch()
# Run next batch
status = openmc.lib.next_batch()
# Perform CMFD calculations
self._execute_cmfd()
# Write CMFD data to statepoint
if openmc.lib.is_statepoint_batch():
self.statepoint_write()
return status
[docs] def finalize(self):
""" Finalize simulation by calling
:func:`openmc.lib.simulation_finalize` and print out CMFD timing
information.
"""
# Finalize simulation
openmc.lib.simulation_finalize()
if openmc.lib.master():
# Print out CMFD timing statistics
self._write_cmfd_timing_stats()
[docs] def statepoint_write(self, filename=None):
"""Write all simulation parameters to statepoint
Parameters
----------
filename : str
Filename of statepoint
"""
if filename is None:
batch_str_len = len(str(openmc.lib.settings.get_batches()))
batch_str = str(openmc.lib.current_batch()).zfill(batch_str_len)
filename = f'statepoint.{batch_str}.h5'
# Call C API statepoint_write to save source distribution with CMFD
# feedback
openmc.lib.statepoint_write(filename=filename)
# Append CMFD data to statepoint file using h5py
self._write_cmfd_statepoint(filename)
def _write_cmfd_statepoint(self, filename):
"""Append all CNFD simulation parameters to existing statepoint
Parameters
----------
filename : str
Filename of statepoint
"""
if openmc.lib.master():
with h5py.File(filename, 'a') as f:
if 'cmfd' not in f:
if openmc.lib.settings.verbosity >= 5:
print(f' Writing CMFD data to {filename}...')
sys.stdout.flush()
cmfd_group = f.create_group("cmfd")
cmfd_group.attrs['cmfd_on'] = self._cmfd_on
cmfd_group.attrs['feedback'] = self._feedback
cmfd_group.attrs['solver_begin'] = self._solver_begin
cmfd_group.attrs['mesh_id'] = self._mesh_id
cmfd_group.attrs['tally_begin'] = self._tally_begin
cmfd_group.attrs['time_cmfd'] = self._time_cmfd
cmfd_group.attrs['time_cmfdbuild'] = self._time_cmfdbuild
cmfd_group.attrs['time_cmfdsolve'] = self._time_cmfdsolve
cmfd_group.attrs['window_size'] = self._window_size
cmfd_group.attrs['window_type'] = self._window_type
cmfd_group.create_dataset('k_cmfd', data=self._k_cmfd)
cmfd_group.create_dataset('dom', data=self._dom)
cmfd_group.create_dataset('src_cmp', data=self._src_cmp)
cmfd_group.create_dataset('balance', data=self._balance)
cmfd_group.create_dataset('entropy', data=self._entropy)
cmfd_group.create_dataset('reset', data=self._reset)
cmfd_group.create_dataset('albedo', data=self._albedo)
cmfd_group.create_dataset('coremap', data=self._coremap)
cmfd_group.create_dataset('egrid', data=self._egrid)
cmfd_group.create_dataset('indices', data=self._indices)
cmfd_group.create_dataset('tally_ids',
data=self._tally_ids)
cmfd_group.create_dataset('current_rate',
data=self._current_rate)
cmfd_group.create_dataset('flux_rate',
data=self._flux_rate)
cmfd_group.create_dataset('nfiss_rate',
data=self._nfiss_rate)
cmfd_group.create_dataset('openmc_src_rate',
data=self._openmc_src_rate)
cmfd_group.create_dataset('p1scatt_rate',
data=self._p1scatt_rate)
cmfd_group.create_dataset('scatt_rate',
data=self._scatt_rate)
cmfd_group.create_dataset('total_rate',
data=self._total_rate)
elif openmc.settings.verbosity >= 5:
print(' CMFD data not written to statepoint file as it '
'already exists in {}'.format(filename), flush=True)
def _initialize_linsolver(self):
# Determine number of rows in CMFD matrix
ng = self._indices[3]
n = self._mat_dim*ng
# Create temp loss matrix to pass row/col indices to C++ linear solver
loss_row = self._loss_row
loss_col = self._loss_col
temp_data = np.ones(len(loss_row))
temp_loss = sparse.csr_matrix((temp_data, (loss_row, loss_col)),
shape=(n, n))
temp_loss.sort_indices()
# Pass coremap as 1-d array of 32-bit integers
coremap = np.swapaxes(self._coremap, 0, 2).flatten().astype(np.int32)
return openmc.lib._dll.openmc_initialize_linsolver(
temp_loss.indptr.astype(np.int32), len(temp_loss.indptr),
temp_loss.indices.astype(np.int32), len(temp_loss.indices), n,
self._spectral, coremap, self._use_all_threads
)
def _write_cmfd_output(self):
"""Write CMFD output to buffer at the end of each batch"""
# Display CMFD k-effective
outstr = '{:>11s}CMFD k: {:0.5f}'.format('', self._k_cmfd[-1])
# Display value of additional fields based on display dict
outstr += '\n'
if self._display['dominance']:
outstr += ('{:>11s}Dom Rat: {:0.5f}\n'
.format('', self._dom[-1]))
if self._display['entropy']:
outstr += ('{:>11s}CMFD Ent: {:0.5f}\n'
.format('', self._entropy[-1]))
if self._display['source']:
outstr += ('{:>11s}RMS Src: {:0.5f}\n'
.format('', self._src_cmp[-1]))
if self._display['balance']:
outstr += ('{:>11s}RMS Bal: {:0.5f}\n'
.format('', self._balance[-1]))
print(outstr)
sys.stdout.flush()
def _write_cmfd_timing_stats(self):
"""Write CMFD timing stats to buffer after finalizing simulation"""
outstr = ("=====================> "
"CMFD TIMING STATISTICS <====================\n\n"
" Time in CMFD = {:.5e} seconds\n"
" Building matrices = {:.5e} seconds\n"
" Solving matrices = {:.5e} seconds\n")
print(outstr.format(self._time_cmfd, self._time_cmfdbuild,
self._time_cmfdsolve))
sys.stdout.flush()
def _configure_cmfd(self):
"""Initialize CMFD parameters and set CMFD input variables"""
# Check if restarting simulation from statepoint file
if not openmc.lib.settings.restart_run:
# Define all variables necessary for running CMFD
self._initialize_cmfd()
else:
# Reset CMFD parameters from statepoint file
path_statepoint = openmc.lib.settings.path_statepoint
self._reset_cmfd(path_statepoint)
def _initialize_cmfd(self):
"""Sets values of CMFD instance variables based on user input,
separating between variables that only exist on all processes
and those that only exist on the master process
"""
# Print message to user and flush output to stdout
if openmc.lib.settings.verbosity >= 7 and openmc.lib.master():
print(' Configuring CMFD parameters for simulation')
sys.stdout.flush()
# Check if CMFD mesh is defined
if self._mesh is None:
raise ValueError('No CMFD mesh has been specified for '
'simulation')
# Set spatial dimensions of CMFD object
for i, n in enumerate(self._mesh.dimension):
self._indices[i] = n
# Check if in continuous energy mode
if not openmc.lib.settings.run_CE:
raise OpenMCError('CMFD must be run in continuous energy mode')
# Set number of energy groups
if self._mesh.energy is not None:
ng = len(self._mesh.energy)
self._egrid = np.array(self._mesh.energy)
self._indices[3] = ng - 1
self._energy_filters = True
else:
self._egrid = np.array([_ENERGY_MIN_NEUTRON, _ENERGY_MAX_NEUTRON])
self._indices[3] = 1
self._energy_filters = False
# Get acceleration map, otherwise set all regions to be accelerated
if self._mesh.map is not None:
check_length('CMFD coremap', self._mesh.map,
np.prod(self._indices[:3]))
if openmc.lib.master():
self._coremap = np.array(self._mesh.map)
else:
if openmc.lib.master():
self._coremap = np.ones(np.prod(self._indices[:3]), dtype=int)
# Check CMFD tallies accummulated before feedback turned on
if self._feedback and self._solver_begin < self._tally_begin:
raise ValueError('Tally begin must be less than or equal to '
'CMFD begin')
# Initialize parameters for CMFD tally windows
self._set_tally_window()
# Extract spatial and energy indices
nx, ny, nz, ng = self._indices
# Initialize CMFD source to all ones
self._cmfd_src = np.ones((nx, ny, nz, ng))
# Define all variables that will exist only on master process
if openmc.lib.master():
# Set global albedo
if self._mesh.albedo is not None:
self._albedo = np.array(self._mesh.albedo)
else:
self._albedo = np.array([1., 1., 1., 1., 1., 1.])
# Set up CMFD coremap
self._set_coremap()
# Allocate parameters that need to be stored for tally window
self._openmc_src_rate = np.zeros((nx, ny, nz, ng, 0))
self._flux_rate = np.zeros((nx, ny, nz, ng, 0))
self._total_rate = np.zeros((nx, ny, nz, ng, 0))
self._p1scatt_rate = np.zeros((nx, ny, nz, ng, 0))
self._scatt_rate = np.zeros((nx, ny, nz, ng, ng, 0))
self._nfiss_rate = np.zeros((nx, ny, nz, ng, ng, 0))
self._current_rate = np.zeros((nx, ny, nz, 12, ng, 0))
# Initialize timers
self._time_cmfd = 0.0
self._time_cmfdbuild = 0.0
self._time_cmfdsolve = 0.0
def _reset_cmfd(self, filename):
"""Reset all CMFD parameters from statepoint
Parameters
----------
filename : str
Filename of statepoint to read from
"""
with h5py.File(filename, 'r') as f:
if 'cmfd' not in f:
raise OpenMCError('Could not find CMFD parameters in ',
f'file {filename}')
else:
# Overwrite CMFD values from statepoint
if (openmc.lib.master() and
openmc.lib.settings.verbosity >= 5):
print(f' Loading CMFD data from {filename}...')
sys.stdout.flush()
cmfd_group = f['cmfd']
# Define variables that exist on all processes
self._cmfd_on = cmfd_group.attrs['cmfd_on']
self._feedback = cmfd_group.attrs['feedback']
self._solver_begin = cmfd_group.attrs['solver_begin']
self._tally_begin = cmfd_group.attrs['tally_begin']
self._k_cmfd = list(cmfd_group['k_cmfd'])
self._dom = list(cmfd_group['dom'])
self._src_cmp = list(cmfd_group['src_cmp'])
self._balance = list(cmfd_group['balance'])
self._entropy = list(cmfd_group['entropy'])
self._reset = list(cmfd_group['reset'])
self._egrid = cmfd_group['egrid'][()]
self._indices = cmfd_group['indices'][()]
default_egrid = np.array([_ENERGY_MIN_NEUTRON,
_ENERGY_MAX_NEUTRON])
self._energy_filters = not np.array_equal(self._egrid,
default_egrid)
self._window_size = cmfd_group.attrs['window_size']
self._window_type = cmfd_group.attrs['window_type']
self._reset_every = (self._window_type == 'expanding' or
self._window_type == 'rolling')
# Overwrite CMFD mesh properties
cmfd_mesh_name = 'mesh ' + str(cmfd_group.attrs['mesh_id'])
cmfd_mesh = f['tallies']['meshes'][cmfd_mesh_name]
self._mesh.mesh_type = cmfd_mesh['type'][()].decode()
if self._mesh.mesh_type == 'regular':
self._mesh.dimension = cmfd_mesh['dimension'][()]
self._mesh.lower_left = cmfd_mesh['lower_left'][()]
self._mesh.upper_right = cmfd_mesh['upper_right'][()]
self._mesh.width = cmfd_mesh['width'][()]
elif self._mesh.mesh_type == 'rectilinear':
x_grid = cmfd_mesh['x_grid'][()]
y_grid = cmfd_mesh['y_grid'][()]
z_grid = cmfd_mesh['z_grid'][()]
self._mesh.grid = [x_grid, y_grid, z_grid]
# Define variables that exist only on master process
if openmc.lib.master():
self._time_cmfd = cmfd_group.attrs['time_cmfd']
self._time_cmfdbuild = cmfd_group.attrs['time_cmfdbuild']
self._time_cmfdsolve = cmfd_group.attrs['time_cmfdsolve']
self._albedo = cmfd_group['albedo'][()]
self._coremap = cmfd_group['coremap'][()]
self._current_rate = cmfd_group['current_rate'][()]
self._flux_rate = cmfd_group['flux_rate'][()]
self._nfiss_rate = cmfd_group['nfiss_rate'][()]
self._openmc_src_rate = cmfd_group['openmc_src_rate'][()]
self._p1scatt_rate = cmfd_group['p1scatt_rate'][()]
self._scatt_rate = cmfd_group['scatt_rate'][()]
self._total_rate = cmfd_group['total_rate'][()]
self._mat_dim = np.max(self._coremap) + 1
def _set_tally_window(self):
"""Sets parameters to handle different tally window options"""
# Set parameters for expanding window
if self._window_type == 'expanding':
self._reset_every = True
self._window_size = 1
# Set parameters for rolling window
elif self.window_type == 'rolling':
self._reset_every = True
# Set parameters for default case, with no window
else:
self._window_size = 1
self._reset_every = False
def _cmfd_init_batch(self):
"""Handles CMFD options at the beginning of each batch"""
# Get current batch through C API
# Add 1 as next_batch has not been called yet
current_batch = openmc.lib.current_batch() + 1
# Check to activate CMFD solver and possible feedback
if self._solver_begin == current_batch:
self._cmfd_on = True
# Check to reset tallies
if ((len(self._reset) > 0 and current_batch in self._reset)
or self._reset_every):
self._cmfd_tally_reset()
def _execute_cmfd(self):
"""Runs CMFD calculation on master node"""
if openmc.lib.master():
# Start CMFD timer
time_start_cmfd = time.time()
if openmc.lib.current_batch() >= self._tally_begin:
# Calculate all cross sections based on tally window averages
self._compute_xs()
# Execute CMFD algorithm if CMFD on for current batch
if self._cmfd_on:
# Run CMFD on single processor on master
if openmc.lib.master():
# Create CMFD data based on OpenMC tallies
self._set_up_cmfd()
# Call solver
self._cmfd_solver_execute()
# Store k-effective
self._k_cmfd.append(self._keff)
# Check to perform adjoint on last batch
batches = openmc.lib.settings.get_batches()
if openmc.lib.current_batch() == batches and self._run_adjoint:
self._cmfd_solver_execute(adjoint=True)
# Calculate fission source
self._calc_fission_source()
# Calculate weight factors through C++ and manipulate CMFD
# source into a 1-D vector that matches C++ array ordering
src_flipped = np.flip(self._cmfd_src, axis=3)
src_swapped = np.swapaxes(src_flipped, 0, 2)
args = self._feedback, src_swapped.flatten()
openmc.lib._dll.openmc_cmfd_reweight(*args)
# Stop CMFD timer
if openmc.lib.master():
time_stop_cmfd = time.time()
self._time_cmfd += time_stop_cmfd - time_start_cmfd
if self._cmfd_on:
# Write CMFD output if CMFD on for current batch
self._write_cmfd_output()
def _cmfd_tally_reset(self):
"""Resets all CMFD tallies in memory"""
# Print message
if (openmc.lib.settings.verbosity >= 6 and openmc.lib.master() and
not self._reset_every):
print(' CMFD tallies reset')
sys.stdout.flush()
# Reset CMFD tallies
tallies = openmc.lib.tallies
for tally_id in self._tally_ids:
tallies[tally_id].reset()
def _set_up_cmfd(self):
"""Configures CMFD object for a CMFD eigenvalue calculation
"""
# Compute effective downscatter cross section
if self._downscatter:
self._compute_effective_downscatter()
# Check neutron balance
self._neutron_balance()
# Calculate dtilde
self._compute_dtilde()
# Calculate dhat
self._compute_dhat()
def _cmfd_solver_execute(self, adjoint=False):
"""Sets up and runs power iteration solver for CMFD
Parameters
----------
adjoint : bool
Whether or not to run an adjoint calculation
"""
# Start timer for build
time_start_buildcmfd = time.time()
# Build the loss and production matrices
if not adjoint:
# Build matrices without adjoint calculation
loss = self._build_loss_matrix(False)
prod = self._build_prod_matrix(False)
else:
# Build adjoint matrices by running adjoint calculation
if self._adjoint_type == 'physical':
loss = self._build_loss_matrix(True)
prod = self._build_prod_matrix(True)
# Build adjoint matrices as transpose of non-adjoint matrices
else:
loss = self._build_loss_matrix(False).transpose()
prod = self._build_prod_matrix(False).transpose()
# Write out the matrices.
if self._write_matrices:
if not adjoint:
self._write_matrix(loss, 'loss')
self._write_matrix(prod, 'prod')
else:
self._write_matrix(loss, 'adj_loss')
self._write_matrix(prod, 'adj_prod')
# Stop timer for build
time_stop_buildcmfd = time.time()
self._time_cmfdbuild += time_stop_buildcmfd - time_start_buildcmfd
# Begin power iteration
time_start_solvecmfd = time.time()
phi, keff, dom = self._execute_power_iter(loss, prod)
time_stop_solvecmfd = time.time()
self._time_cmfdsolve += time_stop_solvecmfd - time_start_solvecmfd
# Save results, normalizing phi to sum to 1
if adjoint:
self._adj_keff = keff
self._adj_phi = phi/np.sqrt(np.sum(phi*phi))
else:
self._keff = keff
self._phi = phi/np.sqrt(np.sum(phi*phi))
self._dom.append(dom)
# Write out flux vector
if self._write_matrices:
if adjoint:
self._write_vector(self._adj_phi, 'adj_fluxvec')
else:
self._write_vector(self._phi, 'fluxvec')
def _write_vector(self, vector, base_filename):
"""Write a 1-D numpy array to file and also save it in .npy format.
This particular format allows users to load the variable directly in a
Python session with np.load()
Parameters
----------
vector : numpy.ndarray
Vector that will be saved
base_filename : str
Filename to save vector as, without any file extension at the end.
Vector will be saved to file [base_filename].dat and in numpy
format as [base_filename].npy
"""
# Write each element in vector to file
np.savetxt(f'{base_filename}.dat', vector, fmt='%.8f')
# Save as numpy format
np.save(base_filename, vector)
def _write_matrix(self, matrix, base_filename):
"""Write a numpy matrix to file and also save it in .npz format. This
particular format allows users to load the variable directly in a
Python session with scipy.sparse.load_npz()
Parameters
----------
matrix : scipy.sparse.spmatrix
Sparse matrix that will be saved
base_filename : str
Filename to save matrix entries, without any file extension at the
end. Matrix entries will be saved to file [base_filename].dat and
in scipy format as [base_filename].npz
"""
# Write row, col, and data of each entry in sparse matrix. This ignores
# all zero-entries, and indices are written with zero-based indexing
with open(base_filename+'.dat', 'w') as fh:
for row in range(matrix.shape[0]):
# Get all cols for particular row in matrix
cols = matrix.indices[matrix.indptr[row]:matrix.indptr[row+1]]
# Get all data entries for particular row in matrix
data = matrix.data[matrix.indptr[row]:matrix.indptr[row+1]]
for i in range(len(cols)):
fh.write(f'{row:3d}, {cols[i]:3d}, {data[i]:0.8f}\n')
# Save matrix in scipy format
sparse.save_npz(base_filename, matrix)
def _calc_fission_source(self):
"""Calculates CMFD fission source from CMFD flux. If a coremap is
defined, there will be a discrepancy between the spatial indices in the
variables ``phi`` and ``nfissxs``, so ``phi`` needs to be mapped to the
spatial indices of the cross sections. This can be done in a vectorized
numpy manner or with for loops
"""
# Extract number of groups and number of accelerated regions
nx, ny, nz, ng = self._indices
n = self._mat_dim
# Compute cmfd_src in a vectorized manner by phi to the spatial
# indices of the actual problem so that cmfd_flux can be multiplied by
# nfissxs
# Reshape phi by number of groups
phi = self._phi.reshape((n, ng))
# Extract indices of coremap that are accelerated
idx = self._accel_idxs
# Initialize CMFD flux map that maps phi to actual spatial and
# group indices of problem
cmfd_flux = np.zeros((nx, ny, nz, ng))
# Loop over all groups and set CMFD flux based on indices of
# coremap and values of phi
for g in range(ng):
phi_g = phi[:,g]
cmfd_flux[idx + (g,)] = phi_g[self._coremap[idx]]
# Compute fission source
cmfd_src = (np.sum(self._nfissxs[:,:,:,:,:] *
cmfd_flux[:,:,:,:,np.newaxis], axis=3))
# Normalize source such that it sums to 1.0
self._cmfd_src = cmfd_src / np.sum(cmfd_src)
# Compute entropy
if openmc.lib.settings.entropy_on:
# Compute source times log_2(source)
source = self._cmfd_src[self._cmfd_src > 0] \
* np.log(self._cmfd_src[self._cmfd_src > 0])/np.log(2)
# Sum source and store
self._entropy.append(-1.0 * np.sum(source))
# Normalize source so average is 1.0
self._cmfd_src = self._cmfd_src/np.sum(self._cmfd_src) * self._norm
# Calculate differences between normalized sources
self._src_cmp.append(np.sqrt(1.0 / self._norm
* np.sum((self._cmfd_src - self._openmc_src)**2)))
def _build_loss_matrix(self, adjoint):
# Extract spatial and energy indices and define matrix dimension
ng = self._indices[3]
n = self._mat_dim*ng
# Define data entries used to build csr matrix
data = np.array([])
dtilde_left = self._dtilde[:,:,:,:,0]
dtilde_right = self._dtilde[:,:,:,:,1]
dtilde_back = self._dtilde[:,:,:,:,2]
dtilde_front = self._dtilde[:,:,:,:,3]
dtilde_bottom = self._dtilde[:,:,:,:,4]
dtilde_top = self._dtilde[:,:,:,:,5]
dhat_left = self._dhat[:,:,:,:,0]
dhat_right = self._dhat[:,:,:,:,1]
dhat_back = self._dhat[:,:,:,:,2]
dhat_front = self._dhat[:,:,:,:,3]
dhat_bottom = self._dhat[:,:,:,:,4]
dhat_top = self._dhat[:,:,:,:,5]
dx = self._hxyz[:,:,:,np.newaxis,0]
dy = self._hxyz[:,:,:,np.newaxis,1]
dz = self._hxyz[:,:,:,np.newaxis,2]
# Define net leakage coefficient for each surface in each matrix
# element
jnet = (((dtilde_right + dhat_right)-(-1.0 * dtilde_left + dhat_left))
/ dx +
((dtilde_front + dhat_front)-(-1.0 * dtilde_back + dhat_back))
/ dy +
((dtilde_top + dhat_top)-(-1.0 * dtilde_bottom + dhat_bottom))
/ dz)
for g in range(ng):
# Define leakage terms that relate terms to their neighbors to the
# left
dtilde = self._dtilde[:,:,:,g,0][self._accel_neig_left_idxs]
dhat = self._dhat[:,:,:,g,0][self._accel_neig_left_idxs]
dx = self._hxyz[:,:,:,0][self._accel_neig_left_idxs]
vals = (-1.0 * dtilde - dhat) / dx
# Store data to add to CSR matrix
data = np.append(data, vals)
# Define leakage terms that relate terms to their neighbors to the
# right
dtilde = self._dtilde[:,:,:,g,1][self._accel_neig_right_idxs]
dhat = self._dhat[:,:,:,g,1][self._accel_neig_right_idxs]
dx = self._hxyz[:,:,:,0][self._accel_neig_right_idxs]
vals = (-1.0 * dtilde + dhat) / dx
# Store data to add to CSR matrix
data = np.append(data, vals)
# Define leakage terms that relate terms to their neighbors in the
# back
dtilde = self._dtilde[:,:,:,g,2][self._accel_neig_back_idxs]
dhat = self._dhat[:,:,:,g,2][self._accel_neig_back_idxs]
dy = self._hxyz[:,:,:,1][self._accel_neig_back_idxs]
vals = (-1.0 * dtilde - dhat) / dy
# Store data to add to CSR matrix
data = np.append(data, vals)
# Define leakage terms that relate terms to their neighbors in the
# front
dtilde = self._dtilde[:,:,:,g,3][self._accel_neig_front_idxs]
dhat = self._dhat[:,:,:,g,3][self._accel_neig_front_idxs]
dy = self._hxyz[:,:,:,1][self._accel_neig_front_idxs]
vals = (-1.0 * dtilde + dhat) / dy
# Store data to add to CSR matrix
data = np.append(data, vals)
# Define leakage terms that relate terms to their neighbors to the
# bottom
dtilde = self._dtilde[:,:,:,g,4][self._accel_neig_bot_idxs]
dhat = self._dhat[:,:,:,g,4][self._accel_neig_bot_idxs]
dz = self._hxyz[:,:,:,2][self._accel_neig_bot_idxs]
vals = (-1.0 * dtilde - dhat) / dz
# Store data to add to CSR matrix
data = np.append(data, vals)
# Define leakage terms that relate terms to their neighbors to the
# top
dtilde = self._dtilde[:,:,:,g,5][self._accel_neig_top_idxs]
dhat = self._dhat[:,:,:,g,5][self._accel_neig_top_idxs]
dz = self._hxyz[:,:,:,2][self._accel_neig_top_idxs]
vals = (-1.0 * dtilde + dhat) / dz
# Store data to add to CSR matrix
data = np.append(data, vals)
# Define terms that relate to loss of neutrons in a cell. These
# correspond to all the diagonal entries of the loss matrix
jnet_g = jnet[:,:,:,g][self._accel_idxs]
total_xs = self._totalxs[:,:,:,g][self._accel_idxs]
scatt_xs = self._scattxs[:,:,:,g,g][self._accel_idxs]
vals = jnet_g + total_xs - scatt_xs
# Store data to add to CSR matrix
data = np.append(data, vals)
# Define terms that relate to in-scattering from group to group.
# These terms relate a mesh index to all mesh indices with the same
# spatial dimensions but belong to a different energy group
for h in range(ng):
if h != g:
# Get scattering macro xs, transposed
if adjoint:
scatt_xs = self._scattxs[:,:,:,g,h][self._accel_idxs]
# Get scattering macro xs
else:
scatt_xs = self._scattxs[:,:,:,h,g][self._accel_idxs]
vals = -1.0 * scatt_xs
# Store data to add to CSR matrix
data = np.append(data, vals)
# Create csr matrix
loss_row = self._loss_row
loss_col = self._loss_col
loss = sparse.csr_matrix((data, (loss_row, loss_col)), shape=(n, n))
loss.sort_indices()
return loss
def _build_prod_matrix(self, adjoint):
# Extract spatial and energy indices and define matrix dimension
ng = self._indices[3]
n = self._mat_dim*ng
# Define rows, columns, and data used to build csr matrix
data = np.array([])
# Define terms that relate to fission production from group to group.
for g in range(ng):
for h in range(ng):
# Get nu-fission macro xs, transposed
if adjoint:
vals = (self._nfissxs[:, :, :, g, h])[self._accel_idxs]
# Get nu-fission macro xs
else:
vals = (self._nfissxs[:, :, :, h, g])[self._accel_idxs]
# Store rows, cols, and data to add to CSR matrix
data = np.append(data, vals)
# Create csr matrix
prod_row = self._prod_row
prod_col = self._prod_col
prod = sparse.csr_matrix((data, (prod_row, prod_col)), shape=(n, n))
prod.sort_indices()
return prod
def _execute_power_iter(self, loss, prod):
"""Main power iteration routine for the CMFD calculation
Parameters
----------
loss : scipy.sparse.spmatrix
Sparse matrix storing elements of CMFD loss matrix
prod : scipy.sparse.spmatrix
Sparse matrix storing elements of CMFD production matrix
Returns
-------
phi_n : numpy.ndarray
Flux vector of CMFD problem
k_n : float
Eigenvalue of CMFD problem
dom : float
Dominance ratio of CMFD problem
"""
# Get problem size
n = loss.shape[0]
# Set up tolerances for C++ solver
atoli = self._gauss_seidel_tolerance[0]
rtoli = self._gauss_seidel_tolerance[1]
toli = rtoli * 100
# Set up flux vectors, intital guess set to 1
phi_n = np.ones((n,))
phi_o = np.ones((n,))
# Set up source vectors
s_n = np.zeros((n,))
s_o = np.zeros((n,))
# Set initial guess
k_n = openmc.lib.keff()[0]
k_o = k_n
dw = self._w_shift
k_s = k_o + dw
k_ln = 1.0/(1.0/k_n - 1.0/k_s)
k_lo = k_ln
# Set norms to 0
norm_n = 0.0
norm_o = 0.0
# Maximum number of power iterations
maxits = 10000
# Perform Wielandt shift
loss -= 1.0/k_s*prod
# Begin power iteration
for i in range(maxits):
# Check if reach max number of iterations
if i == maxits - 1:
raise OpenMCError('Reached maximum iterations in CMFD power '
'iteration solver.')
# Compute source vector
s_o = prod.dot(phi_o)
# Normalize source vector
s_o /= k_lo
# Compute new flux with C++ solver
innerits = openmc.lib._dll.openmc_run_linsolver(loss.data, s_o,
phi_n, toli)
# Compute new source vector
s_n = prod.dot(phi_n)
# Compute new shifted eigenvalue
k_ln = np.sum(s_n) / np.sum(s_o)
# Compute new eigenvalue
k_n = 1.0/(1.0/k_ln + 1.0/k_s)
# Renormalize the old source
s_o *= k_lo
# Check convergence
iconv, norm_n = self._check_convergence(s_n, s_o, k_n, k_o, i+1,
innerits)
# If converged, calculate dominance ratio and break from loop
if iconv:
dom = norm_n / norm_o
return phi_n, k_n, dom
# Record old values if not converged
phi_o = phi_n
k_o = k_n
k_lo = k_ln
norm_o = norm_n
# Update tolerance for inner iterations
toli = max(atoli, rtoli*norm_n)
def _check_convergence(self, s_n, s_o, k_n, k_o, iteration, innerits):
"""Checks the convergence of the CMFD problem
Parameters
----------
s_n : numpy.ndarray
Source vector from current iteration
s_o : numpy.ndarray
Source vector from previous iteration
k_n : float
K-effective from current iteration
k_o : float
K-effective from previous iteration
iteration : int
Iteration number
innerits : int
Number of iterations required for convergence in inner GS loop
Returns
-------
iconv : bool
Whether the power iteration has reached convergence
serr : float
Error in source from previous iteration to current iteration, used
for dominance ratio calculations
"""
# Calculate error in keff
kerr = abs(k_o - k_n) / k_n
# Calculate max error in source
with np.errstate(divide='ignore', invalid='ignore'):
serr = np.sqrt(np.sum(np.where(s_n > 0, ((s_n-s_o) / s_n)**2, 0))
/ len(s_n))
# Check for convergence
iconv = kerr < self._cmfd_ktol and serr < self._stol
# Print out to user
if self._power_monitor and openmc.lib.master():
print('{:8s}{:20s}{:25s}{:s}{:s}'.format(
' {:d}:'.format(iteration),
'k-eff: {:0.8f}'.format(k_n),
'k-error: {:.5e}'.format(kerr),
'src-error: {:.5e}'.format(serr),
' {:d}'.format(innerits)
), flush=True)
return iconv, serr
def _set_coremap(self):
"""Sets the core mapping information. All regions marked with zero
are set to CMFD_NOACCEL, while all regions marked with 1 are set to a
unique index that maps each fuel region to a row number when building
CMFD matrices
"""
# Set number of accelerated regions in problem. This will be related to
# the dimension of CMFD matrices
self._mat_dim = np.sum(self._coremap)
# Define coremap as cumulative sum over accelerated regions,
# otherwise set value to _CMFD_NOACCEL
self._coremap = np.where(self._coremap == 0, _CMFD_NOACCEL,
np.cumsum(self._coremap) - 1)
# Reshape coremap to three dimensional array
# Indices of coremap in user input switched in x and z axes
nx, ny, nz = self._indices[:3]
self._coremap = self._coremap.reshape(nz, ny, nx)
self._coremap = np.swapaxes(self._coremap, 0, 2)
def _compute_xs(self):
"""Takes CMFD tallies from OpenMC and computes macroscopic cross
sections, flux, and diffusion coefficients for each mesh cell using
a tally window scheme
"""
# Update window size for expanding window if necessary
num_cmfd_batches = openmc.lib.current_batch() - self._tally_begin + 1
if (self._window_type == 'expanding' and
num_cmfd_batches == self._window_size * 2):
self._window_size *= 2
# Discard tallies from oldest batch if window limit reached
tally_windows = self._flux_rate.shape[-1] + 1
if tally_windows > self._window_size:
self._flux_rate = self._flux_rate[...,1:]
self._total_rate = self._total_rate[...,1:]
self._p1scatt_rate = self._p1scatt_rate[...,1:]
self._scatt_rate = self._scatt_rate[...,1:]
self._nfiss_rate = self._nfiss_rate[...,1:]
self._current_rate = self._current_rate[...,1:]
self._openmc_src_rate = self._openmc_src_rate[...,1:]
tally_windows -= 1
# Extract spatial and energy indices
nx, ny, nz, ng = self._indices
# Get tallies in-memory
tallies = openmc.lib.tallies
# Set conditional numpy array as boolean vector based on coremap
is_accel = self._coremap != _CMFD_NOACCEL
# Get flux from CMFD tally 0
tally_id = self._tally_ids[0]
flux = tallies[tally_id].results[:,0,1]
# Define target tally reshape dimensions. This defines how openmc
# tallies are ordered by dimension
target_tally_shape = [nz, ny, nx, ng, 1]
# Reshape flux array to target shape. Swap x and z axes so that
# flux shape is now [nx, ny, nz, ng, 1]
reshape_flux = np.swapaxes(flux.reshape(target_tally_shape), 0, 2)
# Flip energy axis as tally results are given in reverse order of
# energy group
reshape_flux = np.flip(reshape_flux, axis=3)
# Bank flux to flux_rate
self._flux_rate = np.append(self._flux_rate, reshape_flux, axis=4)
# Compute flux as aggregate of banked flux_rate over tally window
self._flux = np.where(is_accel[..., np.newaxis],
np.sum(self._flux_rate, axis=4), 0.0)
# Detect zero flux, abort if located and cmfd is on
zero_flux = np.logical_and(self._flux < _TINY_BIT,
is_accel[..., np.newaxis])
if np.any(zero_flux) and self._cmfd_on:
# Get index of first zero flux in flux array
idx = np.argwhere(zero_flux)[0]
# Throw error message (one-based indexing)
# Index of group is flipped
err_message = 'Detected zero flux without coremap overlay' + \
' at mesh: (' + \
', '.join(str(i+1) for i in idx[:-1]) + \
') in group ' + str(ng-idx[-1])
raise OpenMCError(err_message)
# Get total reaction rate (rr) from CMFD tally 0
totalrr = tallies[tally_id].results[:,1,1]
# Reshape total reaction rate array to target shape. Swap x and z axes
# so that shape is now [nx, ny, nz, ng, 1]
reshape_totalrr = np.swapaxes(totalrr.reshape(target_tally_shape),
0, 2)
# Total reaction rate is flipped in energy axis as tally results are
# given in reverse order of energy group
reshape_totalrr = np.flip(reshape_totalrr, axis=3)
# Bank total reaction rate to total_rate
self._total_rate = np.append(self._total_rate, reshape_totalrr,
axis=4)
# Compute total xs as aggregate of banked total_rate over tally window
# divided by flux
self._totalxs = np.divide(np.sum(self._total_rate, axis=4),
self._flux, where=self._flux > 0,
out=np.zeros_like(self._totalxs))
# Get scattering rr from CMFD tally 1
# flux is repeated to account for extra dimensionality of scattering xs
tally_id = self._tally_ids[1]
scattrr = tallies[tally_id].results[:,0,1]
# Define target tally reshape dimensions for xs with incoming
# and outgoing energies
target_tally_shape = [nz, ny, nx, ng, ng, 1]
# Reshape scattrr array to target shape. Swap x and z axes so that
# shape is now [nx, ny, nz, ng, ng, 1]
reshape_scattrr = np.swapaxes(scattrr.reshape(target_tally_shape),
0, 2)
# Scattering rr is flipped in both incoming and outgoing energy axes
# as tally results are given in reverse order of energy group
reshape_scattrr = np.flip(reshape_scattrr, axis=3)
reshape_scattrr = np.flip(reshape_scattrr, axis=4)
# Bank scattering rr to scatt_rate
self._scatt_rate = np.append(self._scatt_rate, reshape_scattrr,
axis=5)
# Compute scattering xs as aggregate of banked scatt_rate over tally
# window divided by flux. Flux dimensionality increased to account for
# extra dimensionality of scattering xs
extended_flux = self._flux[:,:,:,:,np.newaxis]
self._scattxs = np.divide(np.sum(self._scatt_rate, axis=5),
extended_flux, where=extended_flux > 0,
out=np.zeros_like(self._scattxs))
# Get nu-fission rr from CMFD tally 1
nfissrr = tallies[tally_id].results[:,1,1]
num_realizations = tallies[tally_id].num_realizations
# Reshape nfissrr array to target shape. Swap x and z axes so that
# shape is now [nx, ny, nz, ng, ng, 1]
reshape_nfissrr = np.swapaxes(nfissrr.reshape(target_tally_shape),
0, 2)
# Nu-fission rr is flipped in both incoming and outgoing energy axes
# as tally results are given in reverse order of energy group
reshape_nfissrr = np.flip(reshape_nfissrr, axis=3)
reshape_nfissrr = np.flip(reshape_nfissrr, axis=4)
# Bank nu-fission rr to nfiss_rate
self._nfiss_rate = np.append(self._nfiss_rate, reshape_nfissrr,
axis=5)
# Compute nu-fission xs as aggregate of banked nfiss_rate over tally
# window divided by flux. Flux dimensionality increased to account for
# extra dimensionality of nu-fission xs
self._nfissxs = np.divide(np.sum(self._nfiss_rate, axis=5),
extended_flux, where=extended_flux > 0,
out=np.zeros_like(self._nfissxs))
# Openmc source distribution is sum of nu-fission rr in incoming
# energies
openmc_src = np.sum(reshape_nfissrr, axis=3)
# Bank OpenMC source distribution from current batch to
# openmc_src_rate
self._openmc_src_rate = np.append(self._openmc_src_rate, openmc_src,
axis=4)
# Compute source distribution over entire tally window
self._openmc_src = np.sum(self._openmc_src_rate, axis=4)
# Compute k_eff from source distribution
self._keff_bal = (np.sum(self._openmc_src) / num_realizations /
tally_windows)
# Normalize openmc source distribution
self._openmc_src /= np.sum(self._openmc_src) * self._norm
# Get surface currents from CMFD tally 2
tally_id = self._tally_ids[2]
current = tallies[tally_id].results[:,0,1]
# Define target tally reshape dimensions for current
target_tally_shape = [nz, ny, nx, 12, ng, 1]
# Reshape current array to target shape. Swap x and z axes so that
# shape is now [nx, ny, nz, 12, ng, 1]
reshape_current = np.swapaxes(current.reshape(target_tally_shape),
0, 2)
# Current is flipped in energy axis as tally results are given in
# reverse order of energy group
reshape_current = np.flip(reshape_current, axis=4)
# Bank current to current_rate
self._current_rate = np.append(self._current_rate, reshape_current,
axis=5)
# Compute current as aggregate of banked current_rate over tally window
self._current = np.where(is_accel[..., np.newaxis, np.newaxis],
np.sum(self._current_rate, axis=5), 0.0)
# Get p1 scatter rr from CMFD tally 3
tally_id = self._tally_ids[3]
p1scattrr = tallies[tally_id].results[:,0,1]
# Define target tally reshape dimensions for p1 scatter tally
target_tally_shape = [nz, ny, nx, 2, ng, 1]
# Reshape and extract only p1 data from tally results as there is
# no need for p0 data
reshape_p1scattrr = np.swapaxes(p1scattrr.reshape(target_tally_shape),
0, 2)[:,:,:,1,:,:]
# p1-scatter rr is flipped in energy axis as tally results are given in
# reverse order of energy group
reshape_p1scattrr = np.flip(reshape_p1scattrr, axis=3)
# Bank p1-scatter rr to p1scatt_rate
self._p1scatt_rate = np.append(self._p1scatt_rate, reshape_p1scattrr,
axis=4)
# Compute p1-scatter xs as aggregate of banked p1scatt_rate over tally
# window divided by flux
self._p1scattxs = np.divide(np.sum(self._p1scatt_rate, axis=4),
self._flux, where=self._flux > 0,
out=np.zeros_like(self._p1scattxs))
if self._set_reference_params:
# Set diffusion coefficients based on reference value
self._diffcof = np.where(self._flux > 0,
self._ref_d[None, None, None, :], 0.0)
else:
# Calculate and store diffusion coefficient
with np.errstate(divide='ignore', invalid='ignore'):
self._diffcof = np.where(self._flux > 0, 1.0 / (3.0 *
(self._totalxs-self._p1scattxs)), 0.)
def _compute_effective_downscatter(self):
"""Changes downscatter rate for zero upscatter"""
# Extract energy index
ng = self._indices[3]
# Return if not two groups
if ng != 2:
return
# Extract cross sections and flux for each group
flux1 = self._flux[:,:,:,0]
flux2 = self._flux[:,:,:,1]
sigt1 = self._totalxs[:,:,:,0]
sigt2 = self._totalxs[:,:,:,1]
# First energy index is incoming energy, second is outgoing energy
sigs11 = self._scattxs[:,:,:,0,0]
sigs21 = self._scattxs[:,:,:,1,0]
sigs12 = self._scattxs[:,:,:,0,1]
sigs22 = self._scattxs[:,:,:,1,1]
# Compute absorption xs
siga1 = sigt1 - sigs11 - sigs12
siga2 = sigt2 - sigs22 - sigs21
# Compute effective downscatter XS
sigs12_eff = sigs12 - sigs21 * np.divide(flux2, flux1,
where=flux1 > 0,
out=np.zeros_like(flux2))
# Recompute total cross sections and record
self._totalxs[:,:,:,0] = siga1 + sigs11 + sigs12_eff
self._totalxs[:,:,:,1] = siga2 + sigs22
# Record effective dowmscatter xs
self._scattxs[:,:,:,0,1] = sigs12_eff
# Zero out upscatter cross section
self._scattxs[:,:,:,1,0] = 0.0
def _neutron_balance(self):
"""Computes the RMS neutron balance over the CMFD mesh"""
# Extract energy indices
ng = self._indices[3]
# Get number of accelerated regions
num_accel = self._mat_dim
# Get openmc k-effective
keff = openmc.lib.keff()[0]
# Define leakage in each mesh cell and energy group
leakage = (((self._current[:,:,:,_CURRENTS['out_right'],:] -
self._current[:,:,:,_CURRENTS['in_right'],:]) -
(self._current[:,:,:,_CURRENTS['in_left'],:] -
self._current[:,:,:,_CURRENTS['out_left'],:])) +
((self._current[:,:,:,_CURRENTS['out_front'],:] -
self._current[:,:,:,_CURRENTS['in_front'],:]) -
(self._current[:,:,:,_CURRENTS['in_back'],:] -
self._current[:,:,:,_CURRENTS['out_back'],:])) +
((self._current[:,:,:,_CURRENTS['out_top'],:] -
self._current[:,:,:,_CURRENTS['in_top'],:]) -
(self._current[:,:,:,_CURRENTS['in_bottom'],:] -
self._current[:,:,:,_CURRENTS['out_bottom'],:])))
# Compute total rr
interactions = self._totalxs * self._flux
# Compute scattering rr by broadcasting flux in outgoing energy and
# summing over incoming energy
scattering = np.sum(self._scattxs * self._flux[:,:,:,:, np.newaxis],
axis=3)
# Compute fission rr by broadcasting flux in outgoing energy and
# summing over incoming energy
fission = np.sum(self._nfissxs * self._flux[:,:,:,:, np.newaxis],
axis=3)
# Compute residual
res = leakage + interactions - scattering - (1.0 / keff) * fission
# Normalize res by flux and bank res
self._resnb = np.divide(res, self._flux, where=self._flux > 0,
out=np.zeros_like(self._flux))
# Calculate RMS and record for this batch
self._balance.append(np.sqrt(
np.sum(np.multiply(self._resnb, self._resnb)) /
(ng * num_accel)))
def _precompute_array_indices(self):
"""Initializes cross section arrays and computes the indices
used to populate dtilde and dhat
"""
# Extract spatial indices
nx, ny, nz, ng = self._indices
# Allocate dimensions for each mesh cell
self._hxyz = np.zeros((nx, ny, nz, 3))
self._hxyz[:] = openmc.lib.meshes[self._mesh_id].width
# Allocate flux, cross sections and diffusion coefficient
self._flux = np.zeros((nx, ny, nz, ng))
self._totalxs = np.zeros((nx, ny, nz, ng))
self._p1scattxs = np.zeros((nx, ny, nz, ng))
self._scattxs = np.zeros((nx, ny, nz, ng, ng)) # Incoming, outgoing
self._nfissxs = np.zeros((nx, ny, nz, ng, ng)) # Incoming, outgoing
self._diffcof = np.zeros((nx, ny, nz, ng))
# Allocate dtilde and dhat
self._dtilde = np.zeros((nx, ny, nz, ng, 6))
self._dhat = np.zeros((nx, ny, nz, ng, 6))
# Set reference diffusion parameters
if self._ref_d.size > 0:
self._set_reference_params = True
# Check length of reference diffusion parameters equal to number of
# energy groups
if self._ref_d.size != self._indices[3]:
raise OpenMCError('Number of reference diffusion parameters '
'must equal number of CMFD energy groups')
# Logical for determining whether region of interest is accelerated
# region
is_accel = self._coremap != _CMFD_NOACCEL
# Logical for determining whether a zero flux "albedo" b.c. should be
# applied
x_inds, y_inds, z_inds = np.indices((nx, ny, nz))
# Define slice equivalent to is_accel[0,:,:]
slice_x = x_inds[:1,:,:]
slice_y = y_inds[:1,:,:]
slice_z = z_inds[:1,:,:]
bndry_accel = is_accel[(slice_x, slice_y, slice_z)]
self._first_x_accel = (slice_x[bndry_accel], slice_y[bndry_accel],
slice_z[bndry_accel])
# Define slice equivalent to is_accel[-1,:,:]
slice_x = x_inds[-1:,:,:]
slice_y = y_inds[-1:,:,:]
slice_z = z_inds[-1:,:,:]
bndry_accel = is_accel[(slice_x, slice_y, slice_z)]
self._last_x_accel = (slice_x[bndry_accel], slice_y[bndry_accel],
slice_z[bndry_accel])
# Define slice equivalent to is_accel[:,0,:]
slice_x = x_inds[:,:1,:]
slice_y = y_inds[:,:1,:]
slice_z = z_inds[:,:1,:]
bndry_accel = is_accel[(slice_x, slice_y, slice_z)]
self._first_y_accel = (slice_x[bndry_accel], slice_y[bndry_accel],
slice_z[bndry_accel])
# Define slice equivalent to is_accel[:,-1,:]
slice_x = x_inds[:,-1:,:]
slice_y = y_inds[:,-1:,:]
slice_z = z_inds[:,-1:,:]
bndry_accel = is_accel[(slice_x, slice_y, slice_z)]
self._last_y_accel = (slice_x[bndry_accel], slice_y[bndry_accel],
slice_z[bndry_accel])
# Define slice equivalent to is_accel[:,:,0]
slice_x = x_inds[:,:,:1]
slice_y = y_inds[:,:,:1]
slice_z = z_inds[:,:,:1]
bndry_accel = is_accel[(slice_x, slice_y, slice_z)]
self._first_z_accel = (slice_x[bndry_accel], slice_y[bndry_accel],
slice_z[bndry_accel])
# Define slice equivalent to is_accel[:,:,-1]
slice_x = x_inds[:,:,-1:]
slice_y = y_inds[:,:,-1:]
slice_z = z_inds[:,:,-1:]
bndry_accel = is_accel[(slice_x, slice_y, slice_z)]
self._last_z_accel = (slice_x[bndry_accel], slice_y[bndry_accel],
slice_z[bndry_accel])
# Define slice equivalent to is_accel[1:,:,:]
slice_x = x_inds[1:,:,:]
slice_y = y_inds[1:,:,:]
slice_z = z_inds[1:,:,:]
bndry_accel = is_accel[(slice_x, slice_y, slice_z)]
self._notfirst_x_accel = (slice_x[bndry_accel], slice_y[bndry_accel],
slice_z[bndry_accel])
# Define slice equivalent to is_accel[:-1,:,:]
slice_x = x_inds[:-1,:,:]
slice_y = y_inds[:-1,:,:]
slice_z = z_inds[:-1,:,:]
bndry_accel = is_accel[(slice_x, slice_y, slice_z)]
self._notlast_x_accel = (slice_x[bndry_accel], slice_y[bndry_accel],
slice_z[bndry_accel])
# Define slice equivalent to is_accel[:,1:,:]
slice_x = x_inds[:,1:,:]
slice_y = y_inds[:,1:,:]
slice_z = z_inds[:,1:,:]
bndry_accel = is_accel[(slice_x, slice_y, slice_z)]
self._notfirst_y_accel = (slice_x[bndry_accel], slice_y[bndry_accel],
slice_z[bndry_accel])
# Define slice equivalent to is_accel[:,:-1,:]
slice_x = x_inds[:,:-1,:]
slice_y = y_inds[:,:-1,:]
slice_z = z_inds[:,:-1,:]
bndry_accel = is_accel[(slice_x, slice_y, slice_z)]
self._notlast_y_accel = (slice_x[bndry_accel], slice_y[bndry_accel],
slice_z[bndry_accel])
# Define slice equivalent to is_accel[:,:,1:]
slice_x = x_inds[:,:,1:]
slice_y = y_inds[:,:,1:]
slice_z = z_inds[:,:,1:]
bndry_accel = is_accel[(slice_x, slice_y, slice_z)]
self._notfirst_z_accel = (slice_x[bndry_accel], slice_y[bndry_accel],
slice_z[bndry_accel])
# Define slice equivalent to is_accel[:,:,:-1]
slice_x = x_inds[:,:,:-1]
slice_y = y_inds[:,:,:-1]
slice_z = z_inds[:,:,:-1]
bndry_accel = is_accel[(slice_x, slice_y, slice_z)]
self._notlast_z_accel = (slice_x[bndry_accel], slice_y[bndry_accel],
slice_z[bndry_accel])
# Store logical for whether neighboring cell is reflector region
# in all directions
adj_reflector_left = np.roll(self._coremap, 1, axis=0) == _CMFD_NOACCEL
self._is_adj_ref_left = adj_reflector_left[
self._notfirst_x_accel + (np.newaxis,)]
adj_reflector_right = np.roll(self._coremap, -1, axis=0) == \
_CMFD_NOACCEL
self._is_adj_ref_right = adj_reflector_right[
self._notlast_x_accel + (np.newaxis,)]
adj_reflector_back = np.roll(self._coremap, 1, axis=1) == \
_CMFD_NOACCEL
self._is_adj_ref_back = adj_reflector_back[
self._notfirst_y_accel + (np.newaxis,)]
adj_reflector_front = np.roll(self._coremap, -1, axis=1) == \
_CMFD_NOACCEL
self._is_adj_ref_front = adj_reflector_front[
self._notlast_y_accel + (np.newaxis,)]
adj_reflector_bottom = np.roll(self._coremap, 1, axis=2) == \
_CMFD_NOACCEL
self._is_adj_ref_bottom = adj_reflector_bottom[
self._notfirst_z_accel + (np.newaxis,)]
adj_reflector_top = np.roll(self._coremap, -1, axis=2) == \
_CMFD_NOACCEL
self._is_adj_ref_top = adj_reflector_top[
self._notlast_z_accel + (np.newaxis,)]
def _precompute_matrix_indices(self):
"""Computes the indices and row/column data used to populate CMFD CSR
matrices. These indices are used in _build_loss_matrix and
_build_prod_matrix.
"""
# Extract energy group indices
ng = self._indices[3]
# Shift coremap in all directions to determine whether leakage term
# should be defined for particular cell in matrix
coremap_shift_left = np.pad(self._coremap, ((1,0),(0,0),(0,0)),
mode='constant',
constant_values=_CMFD_NOACCEL)[:-1,:,:]
coremap_shift_right = np.pad(self._coremap, ((0,1),(0,0),(0,0)),
mode='constant',
constant_values=_CMFD_NOACCEL)[1:,:,:]
coremap_shift_back = np.pad(self._coremap, ((0,0),(1,0),(0,0)),
mode='constant',
constant_values=_CMFD_NOACCEL)[:,:-1,:]
coremap_shift_front = np.pad(self._coremap, ((0,0),(0,1),(0,0)),
mode='constant',
constant_values=_CMFD_NOACCEL)[:,1:,:]
coremap_shift_bottom = np.pad(self._coremap, ((0,0),(0,0),(1,0)),
mode='constant',
constant_values=_CMFD_NOACCEL)[:,:,:-1]
coremap_shift_top = np.pad(self._coremap, ((0,0),(0,0),(0,1)),
mode='constant',
constant_values=_CMFD_NOACCEL)[:,:,1:]
# Create empty row and column vectors to store for loss matrix
row = np.array([], dtype=int)
col = np.array([], dtype=int)
# Store all indices used to populate production and loss matrix
is_accel = self._coremap != _CMFD_NOACCEL
self._accel_idxs = np.where(is_accel)
self._accel_neig_left_idxs = (np.where(is_accel &
(coremap_shift_left != _CMFD_NOACCEL)))
self._accel_neig_right_idxs = (np.where(is_accel &
(coremap_shift_right != _CMFD_NOACCEL)))
self._accel_neig_back_idxs = (np.where(is_accel &
(coremap_shift_back != _CMFD_NOACCEL)))
self._accel_neig_front_idxs = (np.where(is_accel &
(coremap_shift_front != _CMFD_NOACCEL)))
self._accel_neig_bot_idxs = (np.where(is_accel &
(coremap_shift_bottom != _CMFD_NOACCEL)))
self._accel_neig_top_idxs = (np.where(is_accel &
(coremap_shift_top != _CMFD_NOACCEL)))
for g in range(ng):
# Extract row and column data of regions where a cell and its
# neighbor to the left are both fuel regions
idx_x = ng * (self._coremap[self._accel_neig_left_idxs]) + g
idx_y = ng * (coremap_shift_left[self._accel_neig_left_idxs]) + g
row = np.append(row, idx_x)
col = np.append(col, idx_y)
# Extract row and column data of regions where a cell and its
# neighbor to the right are both fuel regions
idx_x = ng * (self._coremap[self._accel_neig_right_idxs]) + g
idx_y = ng * (coremap_shift_right[self._accel_neig_right_idxs]) + g
row = np.append(row, idx_x)
col = np.append(col, idx_y)
# Extract row and column data of regions where a cell and its
# neighbor to the back are both fuel regions
idx_x = ng * (self._coremap[self._accel_neig_back_idxs]) + g
idx_y = ng * (coremap_shift_back[self._accel_neig_back_idxs]) + g
row = np.append(row, idx_x)
col = np.append(col, idx_y)
# Extract row and column data of regions where a cell and its
# neighbor to the front are both fuel regions
idx_x = ng * (self._coremap[self._accel_neig_front_idxs]) + g
idx_y = ng * (coremap_shift_front[self._accel_neig_front_idxs]) + g
row = np.append(row, idx_x)
col = np.append(col, idx_y)
# Extract row and column data of regions where a cell and its
# neighbor to the bottom are both fuel regions
idx_x = ng * (self._coremap[self._accel_neig_bot_idxs]) + g
idx_y = ng * (coremap_shift_bottom[self._accel_neig_bot_idxs]) \
+ g
row = np.append(row, idx_x)
col = np.append(col, idx_y)
# Extract row and column data of regions where a cell and its
# neighbor to the top are both fuel regions
idx_x = ng * (self._coremap[self._accel_neig_top_idxs]) + g
idx_y = ng * (coremap_shift_top[self._accel_neig_top_idxs]) + g
row = np.append(row, idx_x)
col = np.append(col, idx_y)
# Extract all regions where a cell is a fuel region
idx_x = ng * (self._coremap[self._accel_idxs]) + g
idx_y = idx_x
row = np.append(row, idx_x)
col = np.append(col, idx_y)
for h in range(ng):
if h != g:
# Extract all regions where a cell is a fuel region
idx_x = ng * (self._coremap[self._accel_idxs]) + g
idx_y = ng * (self._coremap[self._accel_idxs]) + h
row = np.append(row, idx_x)
col = np.append(col, idx_y)
# Store row and col as rows and columns of production matrix
self._loss_row = row
self._loss_col = col
# Create empty row and column vectors to store for production matrix
row = np.array([], dtype=int)
col = np.array([], dtype=int)
for g in range(ng):
for h in range(ng):
# Extract all regions where a cell is a fuel region
idx_x = ng * (self._coremap[self._accel_idxs]) + g
idx_y = ng * (self._coremap[self._accel_idxs]) + h
# Store rows, cols, and data to add to CSR matrix
row = np.append(row, idx_x)
col = np.append(col, idx_y)
# Store row and col as rows and columns of production matrix
self._prod_row = row
self._prod_col = col
def _compute_dtilde(self):
"""Computes the diffusion coupling coefficient using a vectorized numpy
approach. Aggregate values for the dtilde multidimensional array are
populated by first defining values on the problem boundary, and then
for all other regions. For indices not lying on a boundary, dtilde
values are distinguished between regions that neighbor a reflector
region and regions that don't neighbor a reflector
"""
# Logical for determining whether a zero flux "albedo" b.c. should be
# applied
is_zero_flux_alb = abs(self._albedo - _ZERO_FLUX) < _TINY_BIT
# Define dtilde at left surface for all mesh cells on left boundary
# Separate between zero flux b.c. and alebdo b.c.
boundary = self._first_x_accel
boundary_grps = boundary + (slice(None),)
D = self._diffcof[boundary_grps]
dx = self._hxyz[boundary + (np.newaxis, 0)]
if is_zero_flux_alb[0]:
self._dtilde[boundary_grps + (0,)] = 2.0 * D / dx
else:
alb = self._albedo[0]
self._dtilde[boundary_grps + (0,)] = ((2.0 * D * (1.0 - alb))
/ (4.0 * D * (1.0 + alb) +
(1.0 - alb) * dx))
# Define dtilde at right surface for all mesh cells on right boundary
# Separate between zero flux b.c. and alebdo b.c.
boundary = self._last_x_accel
boundary_grps = boundary + (slice(None),)
D = self._diffcof[boundary_grps]
dx = self._hxyz[boundary + (np.newaxis, 0)]
if is_zero_flux_alb[1]:
self._dtilde[boundary_grps + (1,)] = 2.0 * D / dx
else:
alb = self._albedo[1]
self._dtilde[boundary_grps + (1,)] = ((2.0 * D * (1.0 - alb))
/ (4.0 * D * (1.0 + alb) +
(1.0 - alb) * dx))
# Define dtilde at back surface for all mesh cells on back boundary
# Separate between zero flux b.c. and alebdo b.c.
boundary = self._first_y_accel
boundary_grps = boundary + (slice(None),)
D = self._diffcof[boundary_grps]
dy = self._hxyz[boundary + (np.newaxis, 1)]
if is_zero_flux_alb[2]:
self._dtilde[boundary_grps + (2,)] = 2.0 * D / dy
else:
alb = self._albedo[2]
self._dtilde[boundary_grps + (2,)] = ((2.0 * D * (1.0 - alb))
/ (4.0 * D * (1.0 + alb) +
(1.0 - alb) * dy))
# Define dtilde at front surface for all mesh cells on front boundary
# Separate between zero flux b.c. and alebdo b.c.
boundary = self._last_y_accel
boundary_grps = boundary + (slice(None),)
D = self._diffcof[boundary_grps]
dy = self._hxyz[boundary + (np.newaxis, 1)]
if is_zero_flux_alb[3]:
self._dtilde[boundary_grps + (3,)] = 2.0 * D / dy
else:
alb = self._albedo[3]
self._dtilde[boundary_grps + (3,)] = ((2.0 * D * (1.0 - alb))
/ (4.0 * D * (1.0 + alb) +
(1.0 - alb) * dy))
# Define dtilde at bottom surface for all mesh cells on bottom boundary
# Separate between zero flux b.c. and alebdo b.c.
boundary = self._first_z_accel
boundary_grps = boundary + (slice(None),)
D = self._diffcof[boundary_grps]
dz = self._hxyz[boundary + (np.newaxis, 2)]
if is_zero_flux_alb[4]:
self._dtilde[boundary_grps + (4,)] = 2.0 * D / dz
else:
alb = self._albedo[4]
self._dtilde[boundary_grps + (4,)] = ((2.0 * D * (1.0 - alb))
/ (4.0 * D * (1.0 + alb) +
(1.0 - alb) * dz))
# Define dtilde at top surface for all mesh cells on top boundary
# Separate between zero flux b.c. and alebdo b.c.
boundary = self._last_z_accel
boundary_grps = boundary + (slice(None),)
D = self._diffcof[boundary_grps]
dz = self._hxyz[boundary + (np.newaxis, 2)]
if is_zero_flux_alb[5]:
self._dtilde[boundary_grps + (5,)] = 2.0 * D / dz
else:
alb = self._albedo[5]
self._dtilde[boundary_grps + (5,)] = ((2.0 * D * (1 - alb))
/ (4.0 * D * (1.0 + alb) +
(1.0 - alb) * dz))
# Define reflector albedo for all cells on the left surface, in case
# a cell borders a reflector region on the left
current_in_left = self._current[:,:,:,_CURRENTS['in_left'],:]
current_out_left = self._current[:,:,:,_CURRENTS['out_left'],:]
ref_albedo = np.divide(current_in_left, current_out_left,
where=current_out_left > 1.0e-10,
out=np.ones_like(current_out_left))
# Diffusion coefficient of neighbor to left
neig_dc = np.roll(self._diffcof, 1, axis=0)
# Cell dimensions of neighbor to left
neig_hxyz = np.roll(self._hxyz, 1, axis=0)
# Define dtilde at left surface for all mesh cells not on left boundary
# Dtilde is defined differently for regions that do and don't neighbor
# reflector regions
boundary = self._notfirst_x_accel
boundary_grps = boundary + (slice(None),)
D = self._diffcof[boundary_grps]
dx = self._hxyz[boundary + (np.newaxis, 0)]
neig_D = neig_dc[boundary_grps]
neig_dx = neig_hxyz[boundary + (np.newaxis, 0)]
alb = ref_albedo[boundary_grps]
is_adj_ref = self._is_adj_ref_left
dtilde = np.where(is_adj_ref, (2.0 * D * (1.0 - alb)) /
(4.0 * D * (1.0 + alb) + (1.0 - alb) * dx),
(2.0 * D * neig_D) / (neig_dx * D + dx * neig_D))
self._dtilde[boundary_grps + (0,)] = dtilde
# Define reflector albedo for all cells on the right surface, in case
# a cell borders a reflector region on the right
current_in_right = self._current[:,:,:,_CURRENTS['in_right'],:]
current_out_right = self._current[:,:,:,_CURRENTS['out_right'],:]
ref_albedo = np.divide(current_in_right, current_out_right,
where=current_out_right > 1.0e-10,
out=np.ones_like(current_out_right))
# Diffusion coefficient of neighbor to right
neig_dc = np.roll(self._diffcof, -1, axis=0)
# Cell dimensions of neighbor to right
neig_hxyz = np.roll(self._hxyz, -1, axis=0)
# Define dtilde at right surface for all mesh cells not on right
# boundary. Dtilde is defined differently for regions that do and don't
# neighbor reflector regions
boundary = self._notlast_x_accel
boundary_grps = boundary + (slice(None),)
D = self._diffcof[boundary_grps]
dx = self._hxyz[boundary + (np.newaxis, 0)]
neig_D = neig_dc[boundary_grps]
neig_dx = neig_hxyz[boundary + (np.newaxis, 0)]
alb = ref_albedo[boundary_grps]
is_adj_ref = self._is_adj_ref_right
dtilde = np.where(is_adj_ref, (2.0 * D * (1.0 - alb)) /
(4.0 * D * (1.0 + alb) + (1.0 - alb) * dx),
(2.0 * D * neig_D) / (neig_dx * D + dx * neig_D))
self._dtilde[boundary_grps + (1,)] = dtilde
# Define reflector albedo for all cells on the back surface, in case
# a cell borders a reflector region on the back
current_in_back = self._current[:,:,:,_CURRENTS['in_back'],:]
current_out_back = self._current[:,:,:,_CURRENTS['out_back'],:]
ref_albedo = np.divide(current_in_back, current_out_back,
where=current_out_back > 1.0e-10,
out=np.ones_like(current_out_back))
# Diffusion coefficient of neighbor to back
neig_dc = np.roll(self._diffcof, 1, axis=1)
# Cell dimensions of neighbor to back
neig_hxyz = np.roll(self._hxyz, 1, axis=1)
# Define dtilde at back surface for all mesh cells not on back boundary
# Dtilde is defined differently for regions that do and don't neighbor
# reflector regions
boundary = self._notfirst_y_accel
boundary_grps = boundary + (slice(None),)
D = self._diffcof[boundary_grps]
dy = self._hxyz[boundary + (np.newaxis, 1)]
neig_D = neig_dc[boundary_grps]
neig_dy = neig_hxyz[boundary + (np.newaxis, 1)]
alb = ref_albedo[boundary_grps]
is_adj_ref = self._is_adj_ref_back
dtilde = np.where(is_adj_ref, (2.0 * D * (1.0 - alb)) /
(4.0 * D * (1.0 + alb) + (1.0 - alb) * dy),
(2.0 * D * neig_D) / (neig_dy * D + dy * neig_D))
self._dtilde[boundary_grps + (2,)] = dtilde
# Define reflector albedo for all cells on the front surface, in case
# a cell borders a reflector region in the front
current_in_front = self._current[:,:,:,_CURRENTS['in_front'],:]
current_out_front = self._current[:,:,:,_CURRENTS['out_front'],:]
ref_albedo = np.divide(current_in_front, current_out_front,
where=current_out_front > 1.0e-10,
out=np.ones_like(current_out_front))
# Diffusion coefficient of neighbor to front
neig_dc = np.roll(self._diffcof, -1, axis=1)
# Cell dimensions of neighbor to front
neig_hxyz = np.roll(self._hxyz, -1, axis=1)
# Define dtilde at front surface for all mesh cells not on front
# boundary. Dtilde is defined differently for regions that do and don't
# neighbor reflector regions
boundary = self._notlast_y_accel
boundary_grps = boundary + (slice(None),)
D = self._diffcof[boundary_grps]
dy = self._hxyz[boundary + (np.newaxis, 1)]
neig_D = neig_dc[boundary_grps]
neig_dy = neig_hxyz[boundary + (np.newaxis, 1)]
alb = ref_albedo[boundary_grps]
is_adj_ref = self._is_adj_ref_front
dtilde = np.where(is_adj_ref, (2.0 * D * (1.0 - alb)) /
(4.0 * D * (1.0 + alb) + (1.0 - alb) * dy),
(2.0 * D * neig_D) / (neig_dy * D + dy * neig_D))
self._dtilde[boundary_grps + (3,)] = dtilde
# Define reflector albedo for all cells on the bottom surface, in case
# a cell borders a reflector region on the bottom
current_in_bottom = self._current[:,:,:,_CURRENTS['in_bottom'],:]
current_out_bottom = self._current[:,:,:,_CURRENTS['out_bottom'],:]
ref_albedo = np.divide(current_in_bottom, current_out_bottom,
where=current_out_bottom > 1.0e-10,
out=np.ones_like(current_out_bottom))
# Diffusion coefficient of neighbor to bottom
neig_dc = np.roll(self._diffcof, 1, axis=2)
# Cell dimensions of neighbor to bottom
neig_hxyz = np.roll(self._hxyz, 1, axis=2)
# Define dtilde at bottom surface for all mesh cells not on bottom
# boundary. Dtilde is defined differently for regions that do and don't
# neighbor reflector regions
boundary = self._notfirst_z_accel
boundary_grps = boundary + (slice(None),)
D = self._diffcof[boundary_grps]
dz = self._hxyz[boundary + (np.newaxis, 2)]
neig_D = neig_dc[boundary_grps]
neig_dz = neig_hxyz[boundary + (np.newaxis, 2)]
alb = ref_albedo[boundary_grps]
is_adj_ref = self._is_adj_ref_bottom
dtilde = np.where(is_adj_ref, (2.0 * D * (1.0 - alb)) /
(4.0 * D * (1.0 + alb) + (1.0 - alb) * dz),
(2.0 * D * neig_D) / (neig_dz * D + dz * neig_D))
self._dtilde[boundary_grps + (4,)] = dtilde
# Define reflector albedo for all cells on the top surface, in case
# a cell borders a reflector region on the top
current_in_top = self._current[:,:,:,_CURRENTS['in_top'],:]
current_out_top = self._current[:,:,:,_CURRENTS['out_top'],:]
ref_albedo = np.divide(current_in_top, current_out_top,
where=current_out_top > 1.0e-10,
out=np.ones_like(current_out_top))
# Diffusion coefficient of neighbor to top
neig_dc = np.roll(self._diffcof, -1, axis=2)
# Cell dimensions of neighbor to top
neig_hxyz = np.roll(self._hxyz, -1, axis=2)
# Define dtilde at top surface for all mesh cells not on top boundary
# Dtilde is defined differently for regions that do and don't neighbor
# reflector regions
boundary = self._notlast_z_accel
boundary_grps = boundary + (slice(None),)
D = self._diffcof[boundary_grps]
dz = self._hxyz[boundary + (np.newaxis, 2)]
neig_D = neig_dc[boundary_grps]
neig_dz = neig_hxyz[boundary + (np.newaxis, 2)]
alb = ref_albedo[boundary_grps]
is_adj_ref = self._is_adj_ref_top
dtilde = np.where(is_adj_ref, (2.0 * D * (1.0 - alb)) /
(4.0 * D * (1.0 + alb) + (1.0 - alb) * dz),
(2.0 * D * neig_D) / (neig_dz * D + dz * neig_D))
self._dtilde[boundary_grps + (5,)] = dtilde
def _compute_dhat(self):
"""Computes the nonlinear coupling coefficient using a vectorized numpy
approach. Aggregate values for the dhat multidimensional array are
populated by first defining values on the problem boundary, and then
for all other regions. For indices not lying by a boundary, dhat values
are distinguished between regions that neighbor a reflector region and
regions that don't neighbor a reflector
"""
# Define current in each direction
current_in_left = self._current[:,:,:,_CURRENTS['in_left'],:]
current_out_left = self._current[:,:,:,_CURRENTS['out_left'],:]
current_in_right = self._current[:,:,:,_CURRENTS['in_right'],:]
current_out_right = self._current[:,:,:,_CURRENTS['out_right'],:]
current_in_back = self._current[:,:,:,_CURRENTS['in_back'],:]
current_out_back = self._current[:,:,:,_CURRENTS['out_back'],:]
current_in_front = self._current[:,:,:,_CURRENTS['in_front'],:]
current_out_front = self._current[:,:,:,_CURRENTS['out_front'],:]
current_in_bottom = self._current[:,:,:,_CURRENTS['in_bottom'],:]
current_out_bottom = self._current[:,:,:,_CURRENTS['out_bottom'],:]
current_in_top = self._current[:,:,:,_CURRENTS['in_top'],:]
current_out_top = self._current[:,:,:,_CURRENTS['out_top'],:]
dx = self._hxyz[:,:,:,np.newaxis,0]
dy = self._hxyz[:,:,:,np.newaxis,1]
dz = self._hxyz[:,:,:,np.newaxis,2]
dxdydz = np.prod(self._hxyz, axis=3)[:,:,:,np.newaxis]
# Define net current on each face
net_current_left = (current_in_left - current_out_left) / dxdydz * dx
net_current_right = (current_out_right - current_in_right) / dxdydz * \
dx
net_current_back = (current_in_back - current_out_back) / dxdydz * dy
net_current_front = (current_out_front - current_in_front) / dxdydz * \
dy
net_current_bottom = (current_in_bottom - current_out_bottom) / \
dxdydz * dz
net_current_top = (current_out_top - current_in_top) / dxdydz * dz
# Define flux in each cell
cell_flux = self._flux / dxdydz
# Define dhat at left surface for all mesh cells on left boundary
boundary = self._first_x_accel
boundary_grps = boundary + (slice(None),)
net_current = net_current_left[boundary_grps]
dtilde = self._dtilde[boundary + (slice(None), 0)]
flux = cell_flux[boundary_grps]
self._dhat[boundary_grps + (0,)] = (net_current + dtilde * flux) / flux
# Define dhat at right surface for all mesh cells on right boundary
boundary = self._last_x_accel
boundary_grps = boundary + (slice(None),)
net_current = net_current_right[boundary_grps]
dtilde = self._dtilde[boundary + (slice(None), 1)]
flux = cell_flux[boundary_grps]
self._dhat[boundary_grps + (1,)] = (net_current - dtilde * flux) / flux
# Define dhat at back surface for all mesh cells on back boundary
boundary = self._first_y_accel
boundary_grps = boundary + (slice(None),)
net_current = net_current_back[boundary_grps]
dtilde = self._dtilde[boundary + (slice(None), 2)]
flux = cell_flux[boundary_grps]
self._dhat[boundary_grps + (2,)] = (net_current + dtilde * flux) / flux
# Define dhat at front surface for all mesh cells on front boundary
boundary = self._last_y_accel
boundary_grps = boundary + (slice(None),)
net_current = net_current_front[boundary_grps]
dtilde = self._dtilde[boundary + (slice(None), 3)]
flux = cell_flux[boundary_grps]
self._dhat[boundary_grps + (3,)] = (net_current - dtilde * flux) / flux
# Define dhat at bottom surface for all mesh cells on bottom boundary
boundary = self._first_z_accel
boundary_grps = boundary + (slice(None),)
net_current = net_current_bottom[boundary_grps]
dtilde = self._dtilde[boundary + (slice(None), 4)]
flux = cell_flux[boundary_grps]
self._dhat[boundary_grps + (4,)] = (net_current + dtilde * flux) / flux
# Define dhat at top surface for all mesh cells on top boundary
boundary = self._last_z_accel
boundary_grps = boundary + (slice(None),)
net_current = net_current_top[boundary_grps]
dtilde = self._dtilde[boundary + (slice(None), 5)]
flux = cell_flux[boundary_grps]
self._dhat[boundary_grps + (5,)] = (net_current - dtilde * flux) / flux
# Cell flux of neighbor to left
neig_flux = np.roll(self._flux, 1, axis=0) / dxdydz
# Define dhat at left surface for all mesh cells not on left boundary
# Dhat is defined differently for regions that do and don't neighbor
# reflector regions
boundary = self._notfirst_x_accel
boundary_grps = boundary + (slice(None),)
net_current = net_current_left[boundary_grps]
dtilde = self._dtilde[boundary_grps + (0,)]
flux = cell_flux[boundary_grps]
flux_left = neig_flux[boundary_grps]
is_adj_ref = self._is_adj_ref_left
dhat = np.where(is_adj_ref, (net_current + dtilde * flux) / flux,
(net_current - dtilde * (flux_left - flux)) /
(flux_left + flux))
self._dhat[boundary_grps + (0,)] = dhat
# Cell flux of neighbor to right
neig_flux = np.roll(self._flux, -1, axis=0) / dxdydz
# Define dhat at right surface for all mesh cells not on right boundary
# Dhat is defined differently for regions that do and don't neighbor
# reflector regions
boundary = self._notlast_x_accel
boundary_grps = boundary + (slice(None),)
net_current = net_current_right[boundary_grps]
dtilde = self._dtilde[boundary_grps + (1,)]
flux = cell_flux[boundary_grps]
flux_right = neig_flux[boundary_grps]
is_adj_ref = self._is_adj_ref_right
dhat = np.where(is_adj_ref, (net_current - dtilde * flux) / flux,
(net_current + dtilde * (flux_right - flux)) /
(flux_right + flux))
self._dhat[boundary_grps + (1,)] = dhat
# Cell flux of neighbor to back
neig_flux = np.roll(self._flux, 1, axis=1) / dxdydz
# Define dhat at back surface for all mesh cells not on back boundary
# Dhat is defined differently for regions that do and don't neighbor
# reflector regions
boundary = self._notfirst_y_accel
boundary_grps = boundary + (slice(None),)
net_current = net_current_back[boundary_grps]
dtilde = self._dtilde[boundary_grps + (2,)]
flux = cell_flux[boundary_grps]
flux_back = neig_flux[boundary_grps]
is_adj_ref = self._is_adj_ref_back
dhat = np.where(is_adj_ref, (net_current + dtilde * flux) / flux,
(net_current - dtilde * (flux_back - flux)) /
(flux_back + flux))
self._dhat[boundary_grps + (2,)] = dhat
# Cell flux of neighbor to front
neig_flux = np.roll(self._flux, -1, axis=1) / dxdydz
# Define dhat at front surface for all mesh cells not on front boundary
# Dhat is defined differently for regions that do and don't neighbor
# reflector regions
boundary = self._notlast_y_accel
boundary_grps = boundary + (slice(None),)
net_current = net_current_front[boundary_grps]
dtilde = self._dtilde[boundary_grps + (3,)]
flux = cell_flux[boundary_grps]
flux_front = neig_flux[boundary_grps]
is_adj_ref = self._is_adj_ref_front
dhat = np.where(is_adj_ref, (net_current - dtilde * flux) / flux,
(net_current + dtilde * (flux_front - flux)) /
(flux_front + flux))
self._dhat[boundary_grps + (3,)] = dhat
# Cell flux of neighbor to bottom
neig_flux = np.roll(self._flux, 1, axis=2) / dxdydz
# Define dhat at bottom surface for all mesh cells not on bottom
# boundary. Dhat is defined differently for regions that do and don't
# neighbor reflector regions
boundary = self._notfirst_z_accel
boundary_grps = boundary + (slice(None),)
net_current = net_current_bottom[boundary_grps]
dtilde = self._dtilde[boundary_grps + (4,)]
flux = cell_flux[boundary_grps]
flux_bottom = neig_flux[boundary_grps]
is_adj_ref = self._is_adj_ref_bottom
dhat = np.where(is_adj_ref, (net_current + dtilde * flux) / flux,
(net_current - dtilde * (flux_bottom - flux)) /
(flux_bottom + flux))
self._dhat[boundary_grps + (4,)] = dhat
# Cell flux of neighbor to top
neig_flux = np.roll(self._flux, -1, axis=2) / dxdydz
# Define dhat at top surface for all mesh cells not on top boundary
# Dhat is defined differently for regions that do and don't neighbor
# reflector regions
boundary = self._notlast_z_accel
boundary_grps = boundary + (slice(None),)
net_current = net_current_top[boundary_grps]
dtilde = self._dtilde[boundary_grps + (5,)]
flux = cell_flux[boundary_grps]
flux_top = neig_flux[boundary_grps]
is_adj_ref = self._is_adj_ref_top
dhat = np.where(is_adj_ref, (net_current - dtilde * flux) / flux,
(net_current + dtilde * (flux_top - flux)) /
(flux_top + flux))
self._dhat[boundary_grps + (5,)] = dhat
def _create_cmfd_tally(self):
"""Creates all tallies in-memory that are used to solve CMFD problem"""
# Create Mesh object based on CMFDMesh mesh_type, stored internally
if self._mesh.mesh_type == 'regular':
cmfd_mesh = openmc.lib.RegularMesh()
# Set dimension and parameters of mesh object
cmfd_mesh.dimension = self._mesh.dimension
cmfd_mesh.set_parameters(lower_left=self._mesh.lower_left,
upper_right=self._mesh.upper_right,
width=self._mesh.width)
elif self._mesh.mesh_type == 'rectilinear':
cmfd_mesh = openmc.lib.RectilinearMesh()
# Set grid of mesh object
x_grid, y_grid, z_grid = self._mesh.grid
cmfd_mesh.set_grid(x_grid, y_grid, z_grid)
# Store id of mesh object
self._mesh_id = cmfd_mesh.id
# Create mesh Filter object, stored internally
mesh_filter = openmc.lib.MeshFilter()
# Set mesh for Mesh Filter
mesh_filter.mesh = cmfd_mesh
# Set up energy filters, if applicable
if self._energy_filters:
# Create Energy Filter object, stored internally
energy_filter = openmc.lib.EnergyFilter()
# Set bins for Energy Filter
energy_filter.bins = self._egrid
# Create Energy Out Filter object, stored internally
energyout_filter = openmc.lib.EnergyoutFilter()
# Set bins for Energy Filter
energyout_filter.bins = self._egrid
# Create Mesh Surface Filter object, stored internally
meshsurface_filter = openmc.lib.MeshSurfaceFilter()
# Set mesh for Mesh Surface Filter
meshsurface_filter.mesh = cmfd_mesh
# Create Legendre Filter object, stored internally
legendre_filter = openmc.lib.LegendreFilter()
# Set order for Legendre Filter
legendre_filter.order = 1
# Create CMFD tallies, stored internally
n_tallies = 4
self._tally_ids = []
for i in range(n_tallies):
cmfd_tally = openmc.lib.Tally()
# Set nuclide bins
cmfd_tally.nuclides = ['total']
self._tally_ids.append(cmfd_tally.id)
# Set attributes of CMFD flux, total tally
if i == 0:
# Set filters for tally
if self._energy_filters:
cmfd_tally.filters = [mesh_filter, energy_filter]
else:
cmfd_tally.filters = [mesh_filter]
# Set scores, type, and estimator for tally
cmfd_tally.scores = ['flux', 'total']
cmfd_tally.type = 'volume'
cmfd_tally.estimator = 'analog'
# Set attributes of CMFD neutron production tally
elif i == 1:
# Set filters for tally
if self._energy_filters:
cmfd_tally.filters = [mesh_filter, energy_filter,
energyout_filter]
else:
cmfd_tally.filters = [mesh_filter]
# Set scores, type, and estimator for tally
cmfd_tally.scores = ['nu-scatter', 'nu-fission']
cmfd_tally.type = 'volume'
cmfd_tally.estimator = 'analog'
# Set attributes of CMFD surface current tally
elif i == 2:
# Set filters for tally
if self._energy_filters:
cmfd_tally.filters = [meshsurface_filter, energy_filter]
else:
cmfd_tally.filters = [meshsurface_filter]
# Set scores, type, and estimator for tally
cmfd_tally.scores = ['current']
cmfd_tally.type = 'mesh-surface'
cmfd_tally.estimator = 'analog'
# Set attributes of CMFD P1 scatter tally
elif i == 3:
# Set filters for tally
if self._energy_filters:
cmfd_tally.filters = [mesh_filter, legendre_filter,
energy_filter]
else:
cmfd_tally.filters = [mesh_filter, legendre_filter]
# Set scores for tally
cmfd_tally.scores = ['scatter']
cmfd_tally.type = 'volume'
cmfd_tally.estimator = 'analog'
# Set all tallies to be active from beginning
cmfd_tally.active = True
# Initialize CMFD mesh and energy grid in C++ for CMFD reweight
args = self._tally_ids[0], self._indices, self._norm
openmc.lib._dll.openmc_initialize_mesh_egrid(*args)