External transfer rates source term (#3088)

Co-authored-by: Paul Romano <paul.k.romano@gmail.com>
This commit is contained in:
Lorenzo Chierici 2025-05-12 22:48:21 +02:00 committed by GitHub
parent f615441f06
commit 7382b5d1c8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 751 additions and 184 deletions

View file

@ -206,14 +206,15 @@ total system energy.
The :class:`openmc.deplete.IndependentOperator` uses inner classes subclassed
from those listed above to perform similar calculations.
The following classes are used to define transfer rates to model continuous
removal or feed of nuclides during depletion.
The following classes are used to define external source rates or transfer rates
to model continuous removal or feed of nuclides during depletion.
.. autosummary::
:toctree: generated
:nosignatures:
:template: myclass.rst
transfer_rates.ExternalSourceRates
transfer_rates.TransferRates
Intermediate Classes

View file

@ -29,7 +29,7 @@ from .results import Results, _SECONDS_PER_MINUTE, _SECONDS_PER_HOUR, \
_SECONDS_PER_DAY, _SECONDS_PER_JULIAN_YEAR
from .pool import deplete
from .reaction_rates import ReactionRates
from .transfer_rates import TransferRates
from .transfer_rates import TransferRates, ExternalSourceRates
__all__ = [
@ -607,9 +607,14 @@ class Integrator(ABC):
next time step. Expected to be of the same shape as ``n0``
transfer_rates : openmc.deplete.TransferRates
Instance of TransferRates class to perform continuous transfer during depletion
Transfer rates for the depletion system used to model continuous
removal/feed between materials.
.. versionadded:: 0.14.0
external_source_rates : openmc.deplete.ExternalSourceRates
External source rates for the depletion system.
.. versionadded:: 0.15.3
"""
@ -686,6 +691,7 @@ class Integrator(ABC):
self.source_rates = np.asarray(source_rates)
self.transfer_rates = None
self.external_source_rates = None
if isinstance(solver, str):
# Delay importing of cram module, which requires this file
@ -731,11 +737,11 @@ class Integrator(ABC):
self._solver = func
def _timed_deplete(self, n, rates, dt, matrix_func=None):
def _timed_deplete(self, n, rates, dt, i=None, matrix_func=None):
start = time.time()
results = deplete(
self._solver, self.chain, n, rates, dt, matrix_func,
self.transfer_rates)
self._solver, self.chain, n, rates, dt, i, matrix_func,
self.transfer_rates, self.external_source_rates)
return time.time() - start, results
@abstractmethod
@ -885,13 +891,14 @@ class Integrator(ABC):
self.operator.finalize()
def add_transfer_rate(
self,
material: Union[str, int, Material],
components: Sequence[str],
transfer_rate: float,
transfer_rate_units: str = '1/s',
destination_material: Optional[Union[str, int, Material]] = None
):
self,
material: str | int | Material,
components: Sequence[str],
transfer_rate: float,
transfer_rate_units: str = '1/s',
timesteps: Sequence[int] | None = None,
destination_material: str | int | Material | None = None
):
"""Add transfer rates to depletable material.
Parameters
@ -905,18 +912,79 @@ class Integrator(ABC):
transfer_rate : float
Rate at which elements are transferred. A positive or negative values
set removal of feed rates, respectively.
destination_material : openmc.Material or str or int, Optional
Destination material to where nuclides get fed.
transfer_rate_units : {'1/s', '1/min', '1/h', '1/d', '1/a'}
Units for values specified in the transfer_rate argument. 's' means
seconds, 'min' means minutes, 'h' means hours, 'a' means Julian years.
timesteps : list of int, optional
List of timestep indices where to set external source rates.
Defaults to None, which means the external source rate is set for
all timesteps.
destination_material : openmc.Material or str or int, Optional
Destination material to where nuclides get fed.
"""
if self.transfer_rates is None:
self.transfer_rates = TransferRates(self.operator, self.operator.model)
if hasattr(self.operator, 'model'):
materials = self.operator.model.materials
elif hasattr(self.operator, 'materials'):
materials = self.operator.materials
self.transfer_rates = TransferRates(
self.operator, materials, len(self.timesteps))
if self.external_source_rates is not None and destination_material:
raise ValueError('Currently is not possible to set a transfer rate '
'with destination matrial in combination with '
'external source rates.')
self.transfer_rates.set_transfer_rate(
material, components, transfer_rate, transfer_rate_units,
timesteps, destination_material)
def add_external_source_rate(
self,
material: str | int | Material,
composition: dict[str, float],
rate: float,
rate_units: str = 'g/s',
timesteps: Sequence[int] | None = None
):
"""Add external source rates to depletable material.
Parameters
----------
material : openmc.Material or str or int
Depletable material
composition : dict of str to float
External source rate composition vector, where key can be an element
or a nuclide and value the corresponding weight percent.
rate : float
External source rate in units of mass per time. A positive or
negative value corresponds to a feed or removal rate, respectively.
units : {'g/s', 'g/min', 'g/h', 'g/d', 'g/a'}
Units for values specified in the `rate` argument. 's' for seconds,
'min' for minutes, 'h' for hours, 'a' for Julian years.
timesteps : list of int, optional
List of timestep indices where to set external source rates.
Defaults to None, which means the external source rate is set for
all timesteps.
"""
if self.external_source_rates is None:
if hasattr(self.operator, 'model'):
materials = self.operator.model.materials
elif hasattr(self.operator, 'materials'):
materials = self.operator.materials
self.external_source_rates = ExternalSourceRates(
self.operator, materials, len(self.timesteps))
if self.transfer_rates is not None and self.transfer_rates.index_transfer:
raise ValueError('Currently is not possible to set an external '
'source rate in combination with transfer rates '
'with destination matrial.')
self.external_source_rates.set_external_source_rate(
material, composition, rate, rate_units, timesteps)
self.transfer_rates.set_transfer_rate(material, components, transfer_rate,
transfer_rate_units, destination_material)
@add_params
class SIIntegrator(Integrator):
@ -1047,10 +1115,10 @@ class SIIntegrator(Integrator):
return inherited
def integrate(
self,
output: bool = True,
path: PathLike = "depletion_results.h5"
):
self,
output: bool = True,
path: PathLike = "depletion_results.h5"
):
"""Perform the entire depletion process across all steps
Parameters

View file

@ -703,7 +703,7 @@ class Chain:
# Return CSC representation instead of DOK
return matrix.tocsc()
def form_rr_term(self, tr_rates, mats):
def form_rr_term(self, tr_rates, current_timestep, mats):
"""Function to form the transfer rate term matrices.
.. versionadded:: 0.14.0
@ -712,6 +712,8 @@ class Chain:
----------
tr_rates : openmc.deplete.TransferRates
Instance of openmc.deplete.TransferRates
current_timestep : int
Current timestep index
mats : string or two-tuple of strings
Two cases are possible:
@ -740,32 +742,74 @@ class Chain:
for i, nuc in enumerate(self.nuclides):
elm = re.split(r'\d+', nuc.name)[0]
# Build transfer terms matrices
# Build transfer terms (nuclide transfer only)
if isinstance(mats, str):
mat = mats
components = tr_rates.get_components(mat)
components = tr_rates.get_components(mat, current_timestep)
if not components:
break
if elm in components:
matrix[i, i] = sum(tr_rates.get_transfer_rate(mat, elm))
matrix[i, i] = sum(
tr_rates.get_external_rate(mat, elm, current_timestep))
elif nuc.name in components:
matrix[i, i] = sum(tr_rates.get_transfer_rate(mat, nuc.name))
matrix[i, i] = sum(
tr_rates.get_external_rate(mat, nuc.name, current_timestep))
else:
matrix[i, i] = 0.0
#Build transfer terms matrices
# Build transfer terms (transfer from one material into another)
elif isinstance(mats, tuple):
dest_mat, mat = mats
if dest_mat in tr_rates.get_destination_material(mat, elm):
dest_mat_idx = tr_rates.get_destination_material(mat, elm).index(dest_mat)
matrix[i, i] = tr_rates.get_transfer_rate(mat, elm)[dest_mat_idx]
elif dest_mat in tr_rates.get_destination_material(mat, nuc.name):
dest_mat_idx = tr_rates.get_destination_material(mat, nuc.name).index(dest_mat)
matrix[i, i] = tr_rates.get_transfer_rate(mat, nuc.name)[dest_mat_idx]
components = tr_rates.get_components(mat, current_timestep, dest_mat)
if elm in components:
matrix[i, i] = tr_rates.get_external_rate(
mat, elm, current_timestep, dest_mat)[0]
elif nuc.name in components:
matrix[i, i] = tr_rates.get_external_rate(
mat, nuc.name, current_timestep, dest_mat)[0]
else:
matrix[i, i] = 0.0
#Nothing else is allowed
# Return CSC instead of DOK
return matrix.tocsc()
def form_ext_source_term(self, ext_source_rates, current_timestep, mat):
"""Function to form the external source rate term vectors.
.. versionadded:: 0.15.3
Parameters
----------
ext_source_rates : openmc.deplete.ExternalSourceRates
Instance of openmc.deplete.ExternalSourceRates
current_timestep : int
Current timestep index
mat : string
Material id
Returns
-------
scipy.sparse.csc_matrix
Sparse vector representing external source term.
"""
if not ext_source_rates.get_components(mat, current_timestep):
return
# Use DOK as intermediate representation
n = len(self)
vector = sp.dok_matrix((n, 1))
for i, nuc in enumerate(self.nuclides):
# Build source term vector
if nuc.name in ext_source_rates.get_components(mat, current_timestep):
vector[i] = sum(ext_source_rates.get_external_rate(
mat, nuc.name, current_timestep))
else:
vector[i] = 0.0
# Return CSC instead of DOK
return vector.tocsc()
def get_branch_ratios(self, reaction="(n,gamma)"):
"""Return a dictionary with reaction branching ratios

View file

@ -40,8 +40,8 @@ class PredictorIntegrator(Integrator):
Time in [s] for the entire depletion interval
source_rate : float
Power in [W] or source rate in [neutron/sec]
_i : int or None
Iteration index. Not used
_i : int, optional
Current iteration count. Not used
Returns
-------
@ -54,7 +54,7 @@ class PredictorIntegrator(Integrator):
with predictor
"""
proc_time, n_end = self._timed_deplete(n, rates, dt)
proc_time, n_end = self._timed_deplete(n, rates, dt, _i)
return proc_time, [n_end], []
@ -106,12 +106,12 @@ class CECMIntegrator(Integrator):
Eigenvalue and reaction rates from transport simulations
"""
# deplete across first half of interval
time0, n_middle = self._timed_deplete(n, rates, dt / 2)
time0, n_middle = self._timed_deplete(n, rates, dt / 2, _i)
res_middle = self.operator(n_middle, source_rate)
# deplete across entire interval with BOS concentrations,
# MOS reaction rates
time1, n_end = self._timed_deplete(n, res_middle.rates, dt)
time1, n_end = self._timed_deplete(n, res_middle.rates, dt, _i)
return time0 + time1, [n_middle, n_end], [res_middle]
@ -172,26 +172,26 @@ class CF4Integrator(Integrator):
"""
# Step 1: deplete with matrix 1/2*A(y0)
time1, n_eos1 = self._timed_deplete(
n_bos, bos_rates, dt, matrix_func=cf4_f1)
n_bos, bos_rates, dt, _i, matrix_func=cf4_f1)
res1 = self.operator(n_eos1, source_rate)
# Step 2: deplete with matrix 1/2*A(y1)
time2, n_eos2 = self._timed_deplete(
n_bos, res1.rates, dt, matrix_func=cf4_f1)
n_bos, res1.rates, dt, _i, matrix_func=cf4_f1)
res2 = self.operator(n_eos2, source_rate)
# Step 3: deplete with matrix -1/2*A(y0)+A(y2)
list_rates = list(zip(bos_rates, res2.rates))
time3, n_eos3 = self._timed_deplete(
n_eos1, list_rates, dt, matrix_func=cf4_f2)
n_eos1, list_rates, dt, _i, matrix_func=cf4_f2)
res3 = self.operator(n_eos3, source_rate)
# Step 4: deplete with two matrix exponentials
list_rates = list(zip(bos_rates, res1.rates, res2.rates, res3.rates))
time4, n_inter = self._timed_deplete(
n_bos, list_rates, dt, matrix_func=cf4_f3)
n_bos, list_rates, dt, _i, matrix_func=cf4_f3)
time5, n_eos5 = self._timed_deplete(
n_inter, list_rates, dt, matrix_func=cf4_f4)
n_inter, list_rates, dt, _i, matrix_func=cf4_f4)
return (time1 + time2 + time3 + time4 + time5,
[n_eos1, n_eos2, n_eos3, n_eos5],
@ -249,17 +249,17 @@ class CELIIntegrator(Integrator):
simulation
"""
# deplete to end using BOS rates
proc_time, n_ce = self._timed_deplete(n_bos, rates, dt)
proc_time, n_ce = self._timed_deplete(n_bos, rates, dt, _i)
res_ce = self.operator(n_ce, source_rate)
# deplete using two matrix exponentials
list_rates = list(zip(rates, res_ce.rates))
time_le1, n_inter = self._timed_deplete(
n_bos, list_rates, dt, matrix_func=celi_f1)
n_bos, list_rates, dt, _i, matrix_func=celi_f1)
time_le2, n_end = self._timed_deplete(
n_inter, list_rates, dt, matrix_func=celi_f2)
n_inter, list_rates, dt, _i, matrix_func=celi_f2)
return proc_time + time_le1 + time_le1, [n_ce, n_end], [res_ce]
@ -316,20 +316,20 @@ class EPCRK4Integrator(Integrator):
"""
# Step 1: deplete with matrix A(y0) / 2
time1, n1 = self._timed_deplete(n, rates, dt, matrix_func=rk4_f1)
time1, n1 = self._timed_deplete(n, rates, dt, _i, matrix_func=rk4_f1)
res1 = self.operator(n1, source_rate)
# Step 2: deplete with matrix A(y1) / 2
time2, n2 = self._timed_deplete(n, res1.rates, dt, matrix_func=rk4_f1)
time2, n2 = self._timed_deplete(n, res1.rates, dt, _i, matrix_func=rk4_f1)
res2 = self.operator(n2, source_rate)
# Step 3: deplete with matrix A(y2)
time3, n3 = self._timed_deplete(n, res2.rates, dt)
time3, n3 = self._timed_deplete(n, res2.rates, dt, _i)
res3 = self.operator(n3, source_rate)
# Step 4: deplete with matrix built from weighted rates
list_rates = list(zip(rates, res1.rates, res2.rates, res3.rates))
time4, n4 = self._timed_deplete(n, list_rates, dt, matrix_func=rk4_f4)
time4, n4 = self._timed_deplete(n, list_rates, dt, _i, matrix_func=rk4_f4)
return (time1 + time2 + time3 + time4, [n1, n2, n3, n4], [res1, res2, res3])
@ -414,9 +414,9 @@ class LEQIIntegrator(Integrator):
self._prev_rates, bos_res.rates, repeat(prev_dt), repeat(dt)))
time1, n_inter = self._timed_deplete(
n_bos, le_inputs, dt, matrix_func=leqi_f1)
n_bos, le_inputs, dt, i, matrix_func=leqi_f1)
time2, n_eos0 = self._timed_deplete(
n_inter, le_inputs, dt, matrix_func=leqi_f2)
n_inter, le_inputs, dt, i, matrix_func=leqi_f2)
res_inter = self.operator(n_eos0, source_rate)
@ -425,9 +425,9 @@ class LEQIIntegrator(Integrator):
repeat(prev_dt), repeat(dt)))
time3, n_inter = self._timed_deplete(
n_bos, qi_inputs, dt, matrix_func=leqi_f3)
n_bos, qi_inputs, dt, i, matrix_func=leqi_f3)
time4, n_eos1 = self._timed_deplete(
n_inter, qi_inputs, dt, matrix_func=leqi_f4)
n_inter, qi_inputs, dt, i, matrix_func=leqi_f4)
# store updated rates
self._prev_rates = copy.deepcopy(bos_res.rates)
@ -478,7 +478,7 @@ class SICELIIntegrator(SIIntegrator):
Eigenvalue and reaction rates from intermediate transport
simulations
"""
proc_time, n_eos = self._timed_deplete(n_bos, bos_rates, dt)
proc_time, n_eos = self._timed_deplete(n_bos, bos_rates, dt, _i)
n_inter = copy.deepcopy(n_eos)
# Begin iteration
@ -494,9 +494,9 @@ class SICELIIntegrator(SIIntegrator):
list_rates = list(zip(bos_rates, res_bar.rates))
time1, n_inter = self._timed_deplete(
n_bos, list_rates, dt, matrix_func=celi_f1)
n_bos, list_rates, dt, _i, matrix_func=celi_f1)
time2, n_inter = self._timed_deplete(
n_inter, list_rates, dt, matrix_func=celi_f2)
n_inter, list_rates, dt, _i, matrix_func=celi_f2)
proc_time += time1 + time2
# end iteration
@ -560,9 +560,9 @@ class SILEQIIntegrator(SIIntegrator):
inputs = list(zip(self._prev_rates, bos_rates,
repeat(prev_dt), repeat(dt)))
proc_time, n_inter = self._timed_deplete(
n_bos, inputs, dt, matrix_func=leqi_f1)
n_bos, inputs, dt, i, matrix_func=leqi_f1)
time1, n_eos = self._timed_deplete(
n_inter, inputs, dt, matrix_func=leqi_f2)
n_inter, inputs, dt, i, matrix_func=leqi_f2)
proc_time += time1
n_inter = copy.deepcopy(n_eos)
@ -580,9 +580,9 @@ class SILEQIIntegrator(SIIntegrator):
inputs = list(zip(self._prev_rates, bos_rates, res_bar.rates,
repeat(prev_dt), repeat(dt)))
time1, n_inter = self._timed_deplete(
n_bos, inputs, dt, matrix_func=leqi_f3)
n_bos, inputs, dt, i, matrix_func=leqi_f3)
time2, n_inter = self._timed_deplete(
n_inter, inputs, dt, matrix_func=leqi_f4)
n_inter, inputs, dt, i, matrix_func=leqi_f4)
proc_time += time1 + time2
return proc_time, [n_eos, n_inter], [res_bar]

View file

@ -5,7 +5,7 @@ Provided to avoid some circular imports
from itertools import repeat, starmap
from multiprocessing import Pool
from scipy.sparse import bmat
from scipy.sparse import bmat, hstack, vstack, csc_matrix
import numpy as np
from openmc.mpi import comm
@ -40,8 +40,8 @@ def _distribute(items):
return items[j:j + chunk_size]
j += chunk_size
def deplete(func, chain, n, rates, dt, matrix_func=None, transfer_rates=None,
*matrix_args):
def deplete(func, chain, n, rates, dt, current_timestep=None, matrix_func=None,
transfer_rates=None, external_source_rates=None, *matrix_args):
"""Deplete materials using given reaction rates for a specified time
Parameters
@ -58,15 +58,21 @@ def deplete(func, chain, n, rates, dt, matrix_func=None, transfer_rates=None,
Reaction rates (from transport operator)
dt : float
Time in [s] to deplete for
current_timestep : int
Current timestep index
maxtrix_func : callable, optional
Function to form the depletion matrix after calling ``matrix_func(chain,
rates, fission_yields)``, where ``fission_yields = {parent: {product:
yield_frac}}`` Expected to return the depletion matrix required by
``func``
transfer_rates : openmc.deplete.TransferRates, Optional
Object to perform continuous reprocessing.
Transfer rates for continuous removal/feed.
.. versionadded:: 0.14.0
external_source_rates : openmc.deplete.ExternalSourceRates, Optional
External source rates for continuous removal/feed.
.. versionadded:: 0.15.3
matrix_args: Any, optional
Additional arguments passed to matrix_func
@ -93,15 +99,17 @@ def deplete(func, chain, n, rates, dt, matrix_func=None, transfer_rates=None,
matrices = map(matrix_func, repeat(chain), rates, fission_yields,
*matrix_args)
if transfer_rates is not None:
if (transfer_rates is not None and
current_timestep in transfer_rates.external_timesteps):
# Calculate transfer rate terms as diagonal matrices
transfers = map(chain.form_rr_term, repeat(transfer_rates),
transfer_rates.local_mats)
repeat(current_timestep), transfer_rates.local_mats)
# Subtract transfer rate terms from Bateman matrices
matrices = [matrix - transfer for (matrix, transfer) in zip(matrices,
transfers)]
if len(transfer_rates.index_transfer) > 0:
if current_timestep in transfer_rates.index_transfer:
# Gather all on comm.rank 0
matrices = comm.gather(matrices)
n = comm.gather(n)
@ -112,10 +120,12 @@ def deplete(func, chain, n, rates, dt, matrix_func=None, transfer_rates=None,
n = [n_elm for n_mat in n for n_elm in n_mat]
# Calculate transfer rate terms as diagonal matrices
transfer_pair = {
mat_pair: chain.form_rr_term(transfer_rates, mat_pair)
for mat_pair in transfer_rates.index_transfer
}
transfer_pair = {}
for mat_pair in transfer_rates.index_transfer[current_timestep]:
transfer_matrix = chain.form_rr_term(transfer_rates,
current_timestep,
mat_pair)
transfer_pair[mat_pair] = transfer_matrix
# Combine all matrices together in a single matrix of matrices
# to be solved in one go
@ -129,7 +139,7 @@ def deplete(func, chain, n, rates, dt, matrix_func=None, transfer_rates=None,
if row == col:
# Fill the diagonals with the Bateman matrices
cols.append(matrices[row])
elif mat_pair in transfer_rates.index_transfer:
elif mat_pair in transfer_rates.index_transfer[current_timestep]:
# Fill the off-diagonals with the transfer pair matrices
cols.append(transfer_pair[mat_pair])
else:
@ -155,6 +165,25 @@ def deplete(func, chain, n, rates, dt, matrix_func=None, transfer_rates=None,
return n_result
if (external_source_rates is not None and
current_timestep in external_source_rates.external_timesteps):
# Calculate external source term vectors
sources = map(chain.form_ext_source_term, repeat(external_source_rates),
repeat(current_timestep), external_source_rates.local_mats)
# stack vector column at the end of the matrix
matrices = [
hstack([matrix, source])
for matrix, source in zip(matrices, sources)
]
# Add a last row of zeroes to the matrices and append 1 to the last row
# of the nuclide vectors
for i, matrix in enumerate(matrices):
if not np.equal(*matrix.shape):
matrices[i] = vstack([matrix, csc_matrix([0]*matrix.shape[1])])
n[i] = np.append(n[i], 1.0)
inputs = zip(matrices, n, repeat(dt))
if USE_MULTIPROCESSING:
@ -163,4 +192,10 @@ def deplete(func, chain, n, rates, dt, matrix_func=None, transfer_rates=None,
else:
n_result = list(starmap(func, inputs))
# Remove extra value at the end of the nuclide vectors
if (external_source_rates is not None and
current_timestep in external_source_rates.external_timesteps):
external_source_rates.reformat_nuclide_vectors(n)
external_source_rates.reformat_nuclide_vectors(n_result)
return n_result

View file

@ -1,31 +1,31 @@
from collections import defaultdict
from numbers import Real
import re
from typing import Sequence
import numpy as np
from openmc.checkvalue import check_type, check_value
from openmc import Material
from openmc.data import ELEMENT_SYMBOL
from openmc.data import ELEMENT_SYMBOL, isotopes, AVOGADRO, atomic_mass
from .results import _SECONDS_PER_MINUTE, _SECONDS_PER_HOUR, \
_SECONDS_PER_DAY, _SECONDS_PER_JULIAN_YEAR
class TransferRates:
"""Class for defining continuous removals and feeds.
Molten Salt Reactors (MSRs) benefit from continuous reprocessing,
which removes fission products and feeds fresh fuel into the system. MSRs
inspired the development of this class.
class ExternalRates:
"""External rates class for defining addition terms of depletion equation.
An instance of this class can be passed directly to an instance of one of
the :class:`openmc.deplete.Integrator` classes.
.. versionadded:: 0.14.0
.. versionadded:: 0.15.3
Parameters
----------
operator : openmc.TransportOperator
Depletion operator
model : openmc.Model
OpenMC model containing materials and geometry. If using
:class:`openmc.deplete.CoupledOperator`, the model must also contain
a :class:`opnemc.Settings` object.
materials : openmc.Materials
OpenMC materials.
number_of_timesteps : int
Total number of depletion timesteps
Attributes
----------
@ -33,22 +33,25 @@ class TransferRates:
All burnable material IDs.
local_mats : list of str
All burnable material IDs being managed by a single process
transfer_rates : dict of str to dict
Container of transfer rates, components (elements and/or nuclides) and
destination material
index_transfer : Set of pair of str
Pair of strings needed to build final matrix (destination_material, mat)
number_of_timesteps : int
Total number of depletion timesteps
external_rates : dict of str to dict
Container of timesteps, external rates, components (elements and/or
nuclides) and optionally destination material
external_timesteps : list of int
Container of all timesteps indeces with an external rate defined.
"""
def __init__(self, operator, model):
def __init__(self, operator, materials, number_of_timesteps):
self.materials = model.materials
self.materials = materials
self.burnable_mats = operator.burnable_mats
self.local_mats = operator.local_mats
self.number_of_timesteps = number_of_timesteps
#initialize transfer rates container dict
self.transfer_rates = {mat: {} for mat in self.burnable_mats}
self.index_transfer = set()
# initialize transfer rates container dict
self.external_rates = {mat: defaultdict(list) for mat in self.burnable_mats}
self.external_timesteps = []
def _get_material_id(self, val):
"""Helper method for getting material id from Material obj or name.
@ -71,7 +74,7 @@ class TransferRates:
check_value('Material ID', str(val), self.burnable_mats)
else:
check_value('Material name', val,
[mat.name for mat in self.materials if mat.depletable])
[mat.name for mat in self.materials if mat.depletable])
val = [mat.id for mat in self.materials if mat.name == val][0]
elif isinstance(val, int):
@ -79,7 +82,13 @@ class TransferRates:
return str(val)
def get_transfer_rate(self, material, component):
def get_external_rate(
self,
material: str | int | Material,
component: str,
timestep: int,
destination_material: str | int | Material | None = None
):
"""Return transfer rate for given material and element.
Parameters
@ -88,62 +97,114 @@ class TransferRates:
Depletable material
component : str
Element or nuclide to get transfer rate value
timestep : int
Current timestep index
destination_material : openmc.Material or str or int, Optional
Destination material to where nuclides get fed
Returns
-------
transfer_rate : list of floats
Transfer rate values
external_rate : list of floats
External rate values
"""
material_id = self._get_material_id(material)
check_type('component', component, str)
return [i[0] for i in self.transfer_rates[material_id][component]]
def get_destination_material(self, material, component):
"""Return destination material for given material and
component, if defined.
Parameters
----------
material : openmc.Material or str or int
Depletable material
component : str
Element or nuclide that gets transferred to another material.
Returns
-------
destination_material_id : list of str
Depletable material ID to where the element or nuclide gets
transferred
"""
material_id = self._get_material_id(material)
check_type('component', component, str)
if component in self.transfer_rates[material_id]:
return [i[1] for i in self.transfer_rates[material_id][component]]
if destination_material is not None:
dest_mat_id = self._get_material_id(destination_material)
return [i[1] for i in self.external_rates[material_id][component]
if timestep in i[0] and dest_mat_id == i[2]]
else:
return []
return [i[1] for i in self.external_rates[material_id][component]
if timestep in i[0]]
def get_components(self, material):
"""Extract removing elements and/or nuclides for a given material
def get_components(self, material, timestep, destination_material=None):
"""Extract removing elements and/or nuclides for a given material at a
given timestep
Parameters
----------
material : openmc.Material or str or int
Depletable material
timestep : int
Current timestep index
destination_material : openmc.Material or str or int, Optional
Destination material to where nuclides get fed
Returns
-------
elements : list
List of elements and nuclides where transfer rates exist
components : list
List of elements or nuclides with external rates set at a given
timestep
"""
material_id = self._get_material_id(material)
if material_id in self.transfer_rates:
return self.transfer_rates[material_id].keys()
if destination_material is not None:
dest_mat_id = self._get_material_id(destination_material)
else:
dest_mat_id = None
all_components = []
if material_id in self.external_rates:
mat_components = self.external_rates[material_id]
for component in mat_components:
if dest_mat_id:
# check for both timestep and destination material ids
if np.isin(timestep, [val[0] for val in mat_components[component]]) and \
np.isin(dest_mat_id, [val[2] for val in mat_components[component]]):
all_components.append(component)
else:
# check only for timesteps
if np.isin(timestep, [val[0] for val in mat_components[component]]):
all_components.append(component)
return all_components
class TransferRates(ExternalRates):
"""Class for defining continuous removals and feeds.
Molten Salt Reactors (MSRs) benefit from continuous reprocessing,
which removes fission products and feeds fresh fuel into the system. MSRs
inspired the development of this class.
An instance of this class can be passed directly to an instance of one of
the :class:`openmc.deplete.Integrator` classes.
.. versionadded:: 0.14.0
Parameters
----------
operator : openmc.TransportOperator
Depletion operator
materials : openmc.Materials
OpenMC materials.
number_of_timesteps : int
Total number of depletion timesteps
Attributes
----------
burnable_mats : list of str
All burnable material IDs.
local_mats : list of str
All burnable material IDs being managed by a single process
external_rates : dict of str to dict
Container of timesteps, transfer rates, components (elements and/or
nuclides) and destination material
external_timesteps : list of int
Container of all timesteps indeces with an external rate defined.
index_transfer : Set of pair of str
Pair of strings needed to build final matrix (destination_material, mat)
"""
def __init__(self, operator, materials, number_of_timesteps):
super().__init__(operator, materials, number_of_timesteps)
self.index_transfer = defaultdict(list)
self.chain_nuclides = [nuc.name for nuc in operator.chain.nuclides]
def set_transfer_rate(self, material, components, transfer_rate,
transfer_rate_units='1/s', destination_material=None):
transfer_rate_units='1/s', timesteps=None,
destination_material=None):
"""Set element and/or nuclide transfer rates in a depletable material.
Parameters
@ -157,11 +218,14 @@ class TransferRates:
transfer_rate : float
Rate at which elements and/or nuclides are transferred. A positive or
negative value corresponds to a removal or feed rate, respectively.
destination_material : openmc.Material or str or int, Optional
Destination material to where nuclides get fed.
transfer_rate_units : {'1/s', '1/min', '1/h', '1/d', '1/a'}
Units for values specified in the transfer_rate argument. 's' for
seconds, 'min' for minutes, 'h' for hours, 'a' for Julian years.
timesteps : list of int, Optional
List of timestep indeces where to set transfer rates.
Default to None means the transfer rate is set for all timesteps.
destination_material : openmc.Material or str or int, Optional
Destination material to where nuclides get fed.
"""
material_id = self._get_material_id(material)
@ -183,19 +247,25 @@ class TransferRates:
if transfer_rate_units in ('1/s', '1/sec'):
unit_conv = 1
elif transfer_rate_units in ('1/min', '1/minute'):
unit_conv = 60
unit_conv = _SECONDS_PER_MINUTE
elif transfer_rate_units in ('1/h', '1/hr', '1/hour'):
unit_conv = 60*60
unit_conv = _SECONDS_PER_HOUR
elif transfer_rate_units in ('1/d', '1/day'):
unit_conv = 24*60*60
unit_conv = _SECONDS_PER_DAY
elif transfer_rate_units in ('1/a', '1/year'):
unit_conv = 365.25*24*60*60
unit_conv = _SECONDS_PER_JULIAN_YEAR
else:
raise ValueError('Invalid transfer rate unit '
f'"{transfer_rate_units}"')
raise ValueError(f'Invalid transfer rate unit "{transfer_rate_units}"')
if timesteps is not None:
for timestep in timesteps:
check_value('timestep', timestep, range(self.number_of_timesteps))
timesteps = np.array(timesteps)
else:
timesteps = np.arange(self.number_of_timesteps)
for component in components:
current_components = self.transfer_rates[material_id].keys()
current_components = self.external_rates[material_id].keys()
split_component = re.split(r'\d+', component)
element = split_component[0]
if element not in ELEMENT_SYMBOL.values():
@ -219,11 +289,144 @@ class TransferRates:
f'where element {element} already has '
'a transfer rate.')
if component in self.transfer_rates[material_id]:
self.transfer_rates[material_id][component].append(
(transfer_rate / unit_conv, destination_material_id))
else:
self.transfer_rates[material_id][component] = [
(transfer_rate / unit_conv, destination_material_id)]
self.external_rates[material_id][component].append(
(timesteps, transfer_rate/unit_conv, destination_material_id))
if destination_material_id is not None:
self.index_transfer.add((destination_material_id, material_id))
for timestep in timesteps:
self.index_transfer[timestep].append(
(destination_material_id, material_id))
self.external_timesteps = np.unique(np.concatenate(
[self.external_timesteps, timesteps]))
class ExternalSourceRates(ExternalRates):
"""Class for defining external source rates.
An instance of this class can be passed directly to an instance of one of
the :class:`openmc.deplete.Integrator` classes.
.. versionadded:: 0.15.3
Parameters
----------
operator : openmc.TransportOperator
Depletion operator
materials : openmc.Materials
OpenMC materials.
number_of_timesteps : int
Total number of depletion timesteps
Attributes
----------
burnable_mats : list of str
All burnable material IDs.
local_mats : list of str
All burnable material IDs being managed by a single process
external_timesteps : list of int
Container of all timesteps indeces with an external rate defined.
external_rates : dict of str to dict
Container of timesteps external source rates, and components
(elements and/or nuclides)
"""
def reformat_nuclide_vectors(self, vectors):
"""Remove last element of nuclide vector that was added for handling
external source rates by the depletion solver.
Parameters
----------
vectors : list of array
List of nuclides vector to reformat
"""
for mat_index, i in enumerate(self.local_mats):
if self.external_rates[i]:
vectors[mat_index] = vectors[mat_index][:-1]
def set_external_source_rate(
self,
material: str | int | Material,
composition: dict[str, float],
rate: float,
rate_units: str = 'g/s',
timesteps: Sequence[int] | None = None
):
"""Set element and/or nuclide composition vector external source rates
to a depletable material.
Parameters
----------
material : openmc.Material or str or int
Depletable material
composition : dict of str to float
External source rate composition vector, where key can be an element
or a nuclide and value the corresponding weight percent.
rate : float
External source rate in units of mass per time. A positive or
negative value corresponds to a feed or removal rate, respectively.
units : {'g/s', 'g/min', 'g/h', 'g/d', 'g/a'}
Units for values specified in the `rate` argument. 's' for seconds,
'min' for minutes, 'h' for hours, 'a' for Julian years.
timesteps : list of int, optional
List of timestep indices where to set external source rates. Default
to None, which means the external source rate is set for all
timesteps.
"""
material_id = self._get_material_id(material)
check_type('rate', rate, Real)
check_type('composition', composition, dict, str)
if rate_units in ('g/s', 'g/sec'):
unit_conv = 1
elif rate_units in ('g/min', 'g/minute'):
unit_conv = _SECONDS_PER_MINUTE
elif rate_units in ('g/h', 'g/hr', 'g/hour'):
unit_conv = _SECONDS_PER_HOUR
elif rate_units in ('g/d', 'g/day'):
unit_conv = _SECONDS_PER_DAY
elif rate_units in ('g/a', 'g/year'):
unit_conv = _SECONDS_PER_JULIAN_YEAR
else:
raise ValueError(f'Invalid external source rate unit "{rate_units}"')
if timesteps is not None:
for timestep in timesteps:
check_value('timestep', timestep, range(self.number_of_timesteps))
timesteps = np.asarray(timesteps)
else:
timesteps = np.arange(self.number_of_timesteps)
components = composition.keys()
percents = composition.values()
norm_percents = [float(i) / sum(percents) for i in percents]
atoms_per_nuc = {}
for component, percent in zip(components, norm_percents):
split_component = re.split(r'\d+', component)
element = split_component[0]
if element not in ELEMENT_SYMBOL.values():
raise ValueError(f'{component} is not a valid nuclide or element.')
if len(split_component) == 1:
if not isotopes(component):
raise ValueError(f'Cannot add element {component} '
'as it is not naturally abundant. '
'Specify a nuclide vector instead.')
for nuc, frac in isotopes(component):
atoms_per_nuc[nuc] = (rate / atomic_mass(nuc) * AVOGADRO *
frac * percent / unit_conv)
else:
atoms_per_nuc[component] = (rate / atomic_mass(component) *
AVOGADRO * percent / unit_conv)
for nuc, val in atoms_per_nuc.items():
self.external_rates[material_id][nuc].append((timesteps, val, None))
self.external_timesteps = np.unique(np.concatenate(
[self.external_timesteps, timesteps]
))

View file

@ -1,8 +1,7 @@
""" TransferRates depletion test suite """
""" ExternalRates depletion test suite """
from pathlib import Path
import shutil
import sys
import numpy as np
import pytest
@ -11,7 +10,7 @@ import openmc.deplete
from openmc.deplete import CoupledOperator
from tests.regression_tests import config, assert_reaction_rates_equal, \
assert_atoms_equal, assert_same_mats
assert_atoms_equal
@pytest.fixture
@ -44,9 +43,11 @@ def model():
settings.particles = 100
settings.inactive = 0
settings.batches = 10
settings.seed = 1
return openmc.Model(geometry, materials, settings)
@pytest.mark.parametrize("rate, dest_mat, power, ref_result", [
(1e-5, None, 0.0, 'no_depletion_only_removal'),
(-1e-5, None, 0.0, 'no_depletion_only_feed'),
@ -83,6 +84,40 @@ def test_transfer_rates(run_in_tmpdir, model, rate, dest_mat, power, ref_result)
res_ref = openmc.deplete.Results(path_reference)
res_test = openmc.deplete.Results(path_test)
assert_same_mats(res_ref, res_test)
assert_atoms_equal(res_ref, res_test, 1e-6)
assert_atoms_equal(res_ref, res_test)
assert_reaction_rates_equal(res_ref, res_test)
@pytest.mark.parametrize("rate, power, ref_result", [
(1e-1, 0.0, 'no_depletion_with_ext_source'),
(1e-1, 174., 'depletion_with_ext_source'),
])
def test_external_source_rates(run_in_tmpdir, model, rate, power, ref_result):
"""Tests external_rates depletion class with external source rates"""
chain_file = Path(__file__).parents[2] / 'chain_simple.xml'
external_source_vector = {'U': 1}
op = CoupledOperator(model, chain_file)
op.round_number = True
integrator = openmc.deplete.PredictorIntegrator(
op, [1], power, timestep_units='d')
integrator.add_external_source_rate('f', external_source_vector, rate)
integrator.integrate()
# Get path to test and reference results
path_test = op.output_dir / 'depletion_results.h5'
path_reference = Path(__file__).with_name(f'ref_{ref_result}.h5')
# If updating results, do so and return
if config['update']:
shutil.copyfile(str(path_test), str(path_reference))
return
# Load the reference/test results
res_ref = openmc.deplete.Results(path_reference)
res_test = openmc.deplete.Results(path_test)
assert_atoms_equal(res_ref, res_test)
assert_reaction_rates_equal(res_ref, res_test)

View file

@ -0,0 +1,170 @@
""" Tests for ExternalSourceRates class """
from pathlib import Path
import pytest
import numpy as np
import re
import openmc
from openmc.data import AVOGADRO, atomic_mass
from openmc.deplete import CoupledOperator
from openmc.deplete.transfer_rates import ExternalSourceRates
from openmc.deplete.abc import (_SECONDS_PER_MINUTE, _SECONDS_PER_HOUR,
_SECONDS_PER_DAY, _SECONDS_PER_JULIAN_YEAR)
CHAIN_PATH = Path(__file__).parents[1] / "chain_simple.xml"
@pytest.fixture
def model():
openmc.reset_auto_ids()
f = openmc.Material(name="f")
f.add_element("U", 1, enrichment=4.25)
f.add_element("O", 2)
f.set_density("g/cm3", 10.4)
w = openmc.Material(name="w")
w.add_element("O", 1)
w.add_element("H", 2)
w.set_density("g/cm3", 1.0)
w.depletable = True
# material just to test multiple destination material
h = openmc.Material(name="h")
h.add_element("He", 1)
h.set_density("g/cm3", 1.78e-4)
radii = [0.42, 0.45]
f.volume = np.pi * radii[0] ** 2
w.volume = np.pi * (radii[1]**2 - radii[0]**2)
h.volume = 1
materials = openmc.Materials([f, w, h])
surf_f = openmc.Sphere(r=radii[0])
surf_w = openmc.Sphere(r=radii[1], boundary_type='vacuum')
surf_h = openmc.Sphere(x0=10, r=1, boundary_type='vacuum')
cell_f = openmc.Cell(fill=f, region=-surf_f)
cell_w = openmc.Cell(fill=w, region=+surf_f & -surf_w)
cell_h = openmc.Cell(fill=h, region=-surf_h)
geometry = openmc.Geometry([cell_f, cell_w, cell_h])
settings = openmc.Settings()
settings.particles = 1000
settings.inactive = 10
settings.batches = 50
return openmc.Model(geometry, materials, settings)
@pytest.mark.parametrize(
"case_name, external_source_vectors, external_source_rate, timesteps", [
('elements', [{'U': 0.9, 'Xe': 0.1}], 1, None),
('nuclides', [{'I135': 0.1, 'Gd156': 0.3, 'Gd157': 0.6}], 1, None),
('nuclides_elements', [{'I135': 0.01, 'Gd156': 0.1, 'Gd157': 0.01, 'U': 0.8,
'Xe': 0.08}], 1, None),
('elements_nuclides', [{'U': 0.78, 'Xe': 0.1, 'I135': 0.01, 'Gd156': 0.1,
'Gd157': 0.01}], 1, None),
('multiple_vectors', [{'U': 1.}, {'Xe': 1}], 1, None),
('timesteps', [{'U': 0.9, 'Xe': 0.1}], 1, [1]),
('rates_invalid_1', [{'Gb': 1.}], 1, None),
('rates_invalid_2', [{'Pu': 1.}], 1, None)
])
def test_get_set(model, case_name, external_source_vectors, external_source_rate,
timesteps):
"""Tests the get/set methods"""
op = CoupledOperator(model, CHAIN_PATH)
number_of_timesteps = 2
transfer = ExternalSourceRates(op, model.materials, number_of_timesteps)
if timesteps is None:
timesteps = np.arange(number_of_timesteps)
# Test by Openmc material, material name and material id
material= [m for m in model.materials if m.depletable][0]
for material_input in [material, material.name, material.id]:
for external_source_vector in external_source_vectors:
if case_name == 'rates_invalid_1':
with pytest.raises(ValueError, match='Gb is not a valid '
'nuclide or element.'):
transfer.set_external_source_rate(material_input,
external_source_vector,
external_source_rate)
elif case_name == 'rates_invalid_2':
with pytest.raises(ValueError, match='Cannot add element Pu'):
transfer.set_external_source_rate(material_input,
external_source_vector,
external_source_rate)
else:
transfer.set_external_source_rate(material_input,
external_source_vector,
external_source_rate,
timesteps=timesteps)
for component, percent in external_source_vector.items():
split_component = re.split(r'\d+', component)
if len(split_component) == 1:
for nuc, frac in openmc.data.isotopes(component):
val = external_source_rate * percent * frac * \
AVOGADRO / atomic_mass(nuc)
assert transfer.get_external_rate(
material_input, nuc, timesteps)[0] == pytest.approx(val)
else:
val = external_source_rate * percent * AVOGADRO / atomic_mass(component)
assert transfer.get_external_rate(
material_input, component, timesteps)[0] == pytest.approx(val)
assert np.all(transfer.external_timesteps == timesteps)
@pytest.mark.parametrize("units, unit_conv", [
('g/s', 1),
('g/sec', 1),
('g/min', _SECONDS_PER_MINUTE),
('g/minute', _SECONDS_PER_MINUTE),
('g/h', _SECONDS_PER_HOUR),
('g/hr', _SECONDS_PER_HOUR),
('g/hour', _SECONDS_PER_HOUR),
('g/d', _SECONDS_PER_DAY),
('g/day', _SECONDS_PER_DAY),
('g/a', _SECONDS_PER_JULIAN_YEAR),
('g/year', _SECONDS_PER_JULIAN_YEAR),
])
def test_units(units, unit_conv, model):
""" Units testing"""
# create external rate Xe
components = ['Xe135', 'U235']
external_source_rate = 1.0
number_of_timesteps = 2
op = CoupledOperator(model, CHAIN_PATH)
transfer = ExternalSourceRates(op, model.materials, number_of_timesteps)
timesteps = np.arange(number_of_timesteps)
for component in components:
rate = external_source_rate * unit_conv * atomic_mass(component) / AVOGADRO
transfer.set_external_source_rate('f', {component: 1}, rate, rate_units=units)
assert transfer.get_external_rate(
'f', component, timesteps)[0] == pytest.approx(external_source_rate)
def test_external_source(run_in_tmpdir, model):
"""Tests external source depletion class without neither reaction rates nor
decay but only external source rates"""
# create transfer rate for U
vector = {'U235': 1}
external_source = 10 # grams
op = CoupledOperator(model, CHAIN_PATH)
integrator = openmc.deplete.PredictorIntegrator(
op, [1, 1], 0.0, timestep_units = 'd')
integrator.add_external_source_rate('f', vector, external_source/(24*3600))
integrator.integrate()
# Get number of U238 atoms from results
results = openmc.deplete.Results('depletion_results.h5')
_, atoms = results.get_atoms(model.materials[0], "U235")
# Ensure number of atoms equal external source
assert atoms[1] - atoms[0] == pytest.approx(
external_source * AVOGADRO / atomic_mass('U235'))
assert atoms[2] - atoms[1] == pytest.approx(
external_source * AVOGADRO / atomic_mass('U235'))

View file

@ -16,6 +16,7 @@ CHAIN_PATH = Path(__file__).parents[1] / "chain_simple.xml"
@pytest.fixture
def model():
openmc.reset_auto_ids()
f = openmc.Material(name="f")
f.add_element("U", 1, percent_type="ao", enrichment=4.25)
f.add_element("O", 2)
@ -54,32 +55,35 @@ def model():
return openmc.Model(geometry, materials, settings)
@pytest.mark.parametrize("case_name, transfer_rates", [
('elements', {'U': 0.01, 'Xe': 0.1}),
('nuclides', {'I135': 0.01, 'Gd156': 0.1, 'Gd157': 0.01}),
@pytest.mark.parametrize("case_name, transfer_rates, timesteps", [
('elements', {'U': 0.01, 'Xe': 0.1}, None),
('nuclides', {'I135': 0.01, 'Gd156': 0.1, 'Gd157': 0.01}, None),
('nuclides_elements', {'I135': 0.01, 'Gd156': 0.1, 'Gd157': 0.01, 'U': 0.01,
'Xe': 0.1}),
'Xe': 0.1}, None),
('elements_nuclides', {'U': 0.01, 'Xe': 0.1, 'I135': 0.01, 'Gd156': 0.1,
'Gd157': 0.01}),
'Gd157': 0.01}, None),
('multiple_transfer', {'U': 0.01, 'Xe': 0.1, 'I135': 0.01, 'Gd156': 0.1,
'Gd157': 0.01}),
('rates_invalid_1', {'Gd': 0.01, 'Gd157': 0.01, 'Gd156': 0.01}),
('rates_invalid_2', {'Gd156': 0.01, 'Gd157': 0.01, 'Gd': 0.01}),
('rates_invalid_3', {'Gb156': 0.01}),
('rates_invalid_4', {'Gb': 0.01})
'Gd157': 0.01}, None),
('timesteps', {'U': 0.01, 'Xe': 0.1}, [1]),
('rates_invalid_1', {'Gd': 0.01, 'Gd157': 0.01, 'Gd156': 0.01}, None),
('rates_invalid_2', {'Gd156': 0.01, 'Gd157': 0.01, 'Gd': 0.01}, None),
('rates_invalid_3', {'Gb156': 0.01}, None),
('rates_invalid_4', {'Gb': 0.01}, None)
])
def test_get_set(model, case_name, transfer_rates):
def test_get_set(model, case_name, transfer_rates, timesteps):
"""Tests the get/set methods"""
openmc.reset_auto_ids()
op = CoupledOperator(model, CHAIN_PATH)
transfer = TransferRates(op, model)
number_of_timesteps = 2
transfer = TransferRates(op, model.materials, number_of_timesteps)
if timesteps is None:
timesteps = np.arange(number_of_timesteps)
# Test by Openmc material, material name and material id
material, dest_material, dest_material2 = [m for m in model.materials
if m.depletable]
for material_input in [material, material.name, material.id]:
for dest_material_input in [dest_material, dest_material.name,
for dest_material_input in [None, dest_material, dest_material.name,
dest_material.id]:
if case_name == 'rates_invalid_1':
with pytest.raises(ValueError, match='Cannot add transfer '
@ -117,14 +121,21 @@ def test_get_set(model, case_name, transfer_rates):
for component, transfer_rate in transfer_rates.items():
transfer.set_transfer_rate(material_input, [component],
transfer_rate,
timesteps=timesteps,
destination_material=\
dest_material_input)
assert transfer.get_transfer_rate(
material_input, component)[0] == transfer_rate
assert transfer.get_destination_material(
material_input, component)[0] == str(dest_material.id)
assert transfer.get_components(material_input) == \
transfer_rates.keys()
assert transfer.get_external_rate(
material_input, component, timesteps,
dest_material_input)[0] == transfer_rate
assert np.all(transfer.external_timesteps == timesteps)
if timesteps is not None:
for timestep in timesteps:
assert transfer.get_components(material_input, timestep,
dest_material_input) == list(transfer_rates.keys())
else:
assert transfer.get_components(material_input, timesteps,
dest_material_input) == list(transfer_rates.keys())
if case_name == 'multiple_transfer':
for dest2_material_input in [dest_material2, dest_material2.name,
@ -135,10 +146,8 @@ def test_get_set(model, case_name, transfer_rates):
destination_material=\
dest2_material_input)
for id, dest_mat in zip([0,1],[dest_material,dest_material2]):
assert transfer.get_transfer_rate(
material_input, component)[id] == transfer_rate
assert transfer.get_destination_material(
material_input, component)[id] == str(dest_mat.id)
assert transfer.get_external_rate(
material_input, component, timesteps)[0] == transfer_rate
@pytest.mark.parametrize("transfer_rate_units, unit_conv", [
('1/s', 1),
@ -158,13 +167,15 @@ def test_units(transfer_rate_units, unit_conv, model):
# create transfer rate Xe
components = ['Xe', 'U235']
transfer_rate = 1e-5
number_of_timesteps = 2
op = CoupledOperator(model, CHAIN_PATH)
transfer = TransferRates(op, model)
transfer = TransferRates(op, model.materials, number_of_timesteps)
for component in components:
transfer.set_transfer_rate('f', [component], transfer_rate * unit_conv,
transfer_rate_units=transfer_rate_units)
assert transfer.get_transfer_rate('f', component)[0] == transfer_rate
for timestep in range(transfer.number_of_timesteps):
assert transfer.get_external_rate('f', component, timestep)[0] == transfer_rate
def test_transfer(run_in_tmpdir, model):