From cbe9cc8c290d675b515fa40507fd4179c74a9b5b Mon Sep 17 00:00:00 2001 From: Joffrey Dorville Date: Fri, 1 Apr 2022 17:31:16 -0600 Subject: [PATCH 001/131] Kalbach-Mann slope calculation for ENDF files when the projectile is a neutron --- openmc/data/kalbach_mann.py | 528 ++++++++++++++++++++- openmc/data/njoy.py | 11 +- openmc/data/reaction.py | 29 +- tests/unit_tests/test_data_kalbach_mann.py | 280 +++++++++++ 4 files changed, 834 insertions(+), 14 deletions(-) create mode 100644 tests/unit_tests/test_data_kalbach_mann.py diff --git a/openmc/data/kalbach_mann.py b/openmc/data/kalbach_mann.py index f4c914d90..11e3acf40 100644 --- a/openmc/data/kalbach_mann.py +++ b/openmc/data/kalbach_mann.py @@ -5,6 +5,7 @@ from warnings import warn import numpy as np import openmc.checkvalue as cv +from openmc.mixin import EqualityMixin from openmc.stats import Tabular, Univariate, Discrete, Mixture from .function import Tabulated1D, INTERPOLATION_SCHEME from .angle_energy import AngleEnergy @@ -12,6 +13,468 @@ from .data import EV_PER_MEV from .endf import get_list_record, get_tab2_record +# Kalbach-Mann constants as defined in ENDF-6 manual BNL-203218-2018-INRE, +# Revision 215, File 6 description for LAW=1 and LANG=2. +_C1 = 0.04 # [1/MeV] +_C2 = 1.8E-6 # [1/MeV^3] +_C3 = 6.7E-7 # [1/MeV^4] +_ET1 = 130. # [MeV] +_ET3 = 41. # [MeV] +_M_NEUTRON = 1. +_M_PROTON = 1. +_M_DEUTERON = 1. +_M_TRITON = None +_M_3HE = None +_M_ALPHA = 0. +_SM_NEUTRON = 1/2. +_SM_PROTON = 1. +_SM_DEUTERON = 1. +_SM_TRITON = 1. +_SM_3HE = 1. +_SM_ALPHA = 2. + +# Kalbach-Mann M coefficients +_TABULATED_PARTICLE_M = { + 1: _M_NEUTRON, + 1001: _M_PROTON, + 1002: _M_DEUTERON, + 1003: _M_TRITON, + 2003: _M_3HE, + 2004: _M_ALPHA +} + +# Kalbach-Mann m coefficients +_TABULATED_PARTICLE_SM = { + 1: _SM_NEUTRON, + 1001: _SM_PROTON, + 1002: _SM_DEUTERON, + 1003: _SM_TRITON, + 2003: _SM_3HE, + 2004: _SM_ALPHA +} + +# Breaking energy as defined in ENDF-6 manual BNL-203218-2018-INRE, +# Revision 215, Appendix H, Table 3. +_BREAKING_ENERGY_NEUTRON = 0. +_BREAKING_ENERGY_PROTON = 0. +_BREAKING_ENERGY_DEUTERON = 2.224566 # [MeV] +_BREAKING_ENERGY_TRITON = 8.481798 # [MeV] +_BREAKING_ENERGY_3HE = 7.718043 # [MeV] +_BREAKING_ENERGY_ALPHA = 28.29566 # [MeV] + +_TABULATED_BREAKING_ENERGY = { + 1: _BREAKING_ENERGY_NEUTRON, + 1001: _BREAKING_ENERGY_PROTON, + 1002: _BREAKING_ENERGY_DEUTERON, + 1003: _BREAKING_ENERGY_TRITON, + 2003: _BREAKING_ENERGY_3HE, + 2004: _BREAKING_ENERGY_ALPHA +} + +# Abundant IZA translation in merged library +_IZA_TRANSLATION = { + 6000: 6012, +} + + +class AtomicRepresentation(EqualityMixin): + """Atomic representation of an isotope or a particle. + + Parameters + ---------- + z: int + Number of protons (atomic number) + a: int + Number of nucleons (mass number) + + Raises + ------ + IOError: + When the number of protons (z) declared is higher than the number + of nucleons (a) + + Attributes + ---------- + z: int + Number of protons (atomic number) + a: int + Number of nucleons (mass number) + n: int + Number of neutrons + breaking_energy: float + Energy required to break the isotope or particle into their + constituent nucleons from tabulated values + M: float + Kalbach-Mann M coefficient + m: float + Kalbach-Mann m coefficient + iza: int + ZA identifier defined as: + iza = Z x 1000 + A, + where Z is the number of protons and A the number of nucleons + + """ + def __init__(self, z, a): + self._consistency_check(z, a) + self._z = z + self._a = a + self.z = self._z + self.a = self._a + + def __add__(self, other): + """Adds two AtomicRepresentations. + + """ + z = self.z + other.z + a = self.a + other.a + return AtomicRepresentation(z=z, a=a) + + def __sub__(self, other): + """Substracts two AtomicRepresentations. + + """ + z = self.z - other.z + a = self.a - other.a + return AtomicRepresentation(z=z, a=a) + + @property + def a(self): + self._consistency_check(self._z, self._a) + return self._a + + @property + def z(self): + self._consistency_check(self._z, self._a) + return self._z + + @property + def n(self): + return self.a - self.z + + @property + def breaking_energy(self): + breaking_energy = None + if self.iza in _TABULATED_BREAKING_ENERGY: + breaking_energy = _TABULATED_BREAKING_ENERGY[self.iza] + return breaking_energy + + @property + def M(self): + M = None + if self.iza in _TABULATED_PARTICLE_M: + M = _TABULATED_PARTICLE_M[self.iza] + return M + + @property + def m(self): + m = None + if self.iza in _TABULATED_PARTICLE_SM: + m = _TABULATED_PARTICLE_SM[self.iza] + return m + + @property + def iza(self): + iza = self.z * 1000 + self.a + return iza + + @a.setter + def a(self, an): + cv.check_type('a', an, Integral) + cv.check_greater_than('a', an, 0, equality=True) + self._consistency_check(self._z, an) + self._a = an + + @z.setter + def z(self, zn): + cv.check_type('z', zn, Integral) + cv.check_greater_than('z', zn, 0, equality=True) + self._consistency_check(zn, self._a) + self._z = zn + + @staticmethod + def _consistency_check(z, a): + """Simple consistency check. + + Parameters + ---------- + z: int + Number of protons (atomic number) + a: int + Number of nucleons (mass number) + + Raises + ------ + IOError: + When the number of protons (z) declared is higher than the number + of nucleons (a) + + """ + if z > a: + raise IOError( + "Number of protons (%i) incompatible with number of " + "nucleons (%i)" % (z, a) + ) + + @classmethod + def from_iza(cls, iza): + """Instantiates an AtomicRepresentation from a ZA identifier. + + The ZA identifier is defined as: + iza = Z x 1000 + A, + where Z is the number of protons and A the number of nucleons. + + Parameters + ---------- + iza: int + ZA identifier + + Returns + ------- + AtomicRepresentation + Atomic representation of the isotope/particle + + """ + if iza in _IZA_TRANSLATION.keys(): + iza = _IZA_TRANSLATION[iza] + + z = int(iza/1000) + a = np.mod(iza, 1000) + + return cls(z, a) + + +def _calculate_separation_energy(compound, nucleus, particle): + """Calculates the separation energy as defined in ENDF-6 manual + BNL-203218-2018-INRE, Revision 215, File 6 description for LAW=1 + and LANG=2. This function can be used for the incident or emitted + particle of the following reaction: A + a -> C -> B + b + + Parameters + ---------- + compound: AtomicRepresentation + Atomic representation of the compound (C) + nucleus: AtomicRepresentation + Atomic representation of the nucleus (A or B) + particle: AtomicRepresentation + Atomic representation of the particle (a or b) + + Returns + ------- + separation_energy: float + Separation energy in MeV + + """ + coef_1 = 15.68 * (compound.a - nucleus.a) + coef_2 = 28.07 * ((compound.n - compound.z)**2 / float(compound.a) - \ + (nucleus.n - nucleus.z)**2 / float(nucleus.a)) + coef_3 = 18.56 * (compound.a**(2./3.) - nucleus.a**(2./3.)) + coef_4 = 33.22 * ((compound.n - compound.z)**2 / float(compound.a)**(4./3.) - \ + (nucleus.n - nucleus.z)**2 / float(nucleus.a)**(4./3.)) + coef_5 = 0.717 * (compound.z**2 / float(compound.a)**(1./3.) - \ + nucleus.z**2 / float(nucleus.a)**(1./3.)) + coef_6 = 1.211 * (compound.z**2 / float(compound.a) - \ + nucleus.z**2 / float(nucleus.a)) + + separation_energy = coef_1 - coef_2 - coef_3 + coef_4 \ + - coef_5 + coef_6 - particle.breaking_energy + + return separation_energy + + +def _return_entrance_channel_energy(e_p, awr_t, awr_p): + """Returns the entrance channel energy as defined in ENDF-6 manual + BNL-203218-2018-INRE, Revision 215, File 6 description for LAW=1 + and LANG=2. + + Parameters + ---------- + e_p: float + Energy of the incident projectile in the laboratory system in eV + awr_t: float + Atomic weight ratio of the target + awr_p: float + Atomic weight ratio of the projectile + + Returns + ------- + epsilon_p: float + Entrance channel energy in eV + + """ + epsilon_p = e_p * awr_t / (awr_t + awr_p) + return epsilon_p + + +def _return_emission_channel_energy(e_e, awr_r, awr_e): + """Returns the emission channel energy as defined in ENDF-6 manual + BNL-203218-2018-INRE, Revision 215, File 6 description for LAW=1 + and LANG=2. + + Parameters + ---------- + e_e: float + Energy of the emitted particle in the center of mass system in eV + awr_r: float + Atomic weight ratio of the residual nucleus + awr_e: float + Atomic weight ratio of the emitted particle + + Returns + ------- + epsilon_e: float + Emission channel energy in eV + + """ + epsilon_e = e_e * (awr_r + awr_e) / awr_r + return epsilon_e + + +def _calculate_kalbach_slope(energy_projectile, + energy_emitted, + projectile, + target, + compound, + emitted, + residual): + """Calculate the Kalbach slope for projectiles other than photons + as defined in ENDF-6 manual BNL-203218-2018-INRE, Revision 215, + File 6 description for LAW=1 and LANG=2. + + The entrance and emission channel energies are not calculated with + the AWR number, but approximated with the number of mass instead. + + Parameters + ---------- + energy_projectile: float + Energy of the projectile in the laboratory system in eV + energy_emitted: float + Energy of the emitted particle in the center of mass system in eV + projectile: AtomicRepresentation + Atomic representation of the projectile + target: AtomicRepresentation + Atomic representation of the target + compound: AtomicRepresentation + Atomic representation of the compound + emitted: AtomicRepresentation + Atomic representation of the emitted particle + residual: AtomicRepresentation + Atomic representation of the residual nucleus + + Returns + ------- + slope: float + Kalbach-Mann slope + + """ + epsilon_a = _return_entrance_channel_energy( + energy_projectile, + target.a, + projectile.a + ) / EV_PER_MEV + epsilon_b = _return_emission_channel_energy( + energy_emitted, + residual.a, + emitted.a + ) / EV_PER_MEV + + s_a = _calculate_separation_energy(compound, target, projectile) + s_b = _calculate_separation_energy(compound, residual, emitted) + + e_a = epsilon_a + s_a + e_b = epsilon_b + s_b + + r_1 = min(e_a, _ET1) + r_3 = min(e_a, _ET3) + + x_1 = r_1 * e_b / e_a + x_3 = r_3 * e_b / e_a + + slope = _C1 * x_1 \ + + _C2 * x_1**3 \ + + _C3 * projectile.M * emitted.m * x_3**4 + + return slope + + +def return_kalbach_slope(energy_projectile, + energy_emitted, + iza_projectile, + iza_emitted, + iza_target): + """Returns Kalbach-Mann slope from calculations. + + The associated reaction is defined as: + A + a -> C -> B + b + + Where: + + - A is the targeted nucleus, + - a is the projectile, + - C is the compound, + - B is the residual nucleus, + - b is the emitted particle. + + This function uses the concept of ZA identifier defined as: + iza = Z x 1000 + A, + where Z is the number of protons and A the number of nucleons. + + The Kalbach-Mann slope calculation is done as defined in ENDF-6 manual + BNL-203218-2018-INRE, Revision 215, File 6 description for LAW=1 and + LANG=2. One exception to this, is that the entrance and emission channel + energies are not calculated with the AWR number, but approximated with + the number of mass instead. + + Parameters + ---------- + energy_projectile: float + Energy of the projectile in the laboratory system in eV + energy_emitted: float + Energy of the emitted particle in the center of mass system in eV + iza_projectile: int + ZA identifier of the projectile + iza_emitted: int + ZA identifier of the emitted particle + iza_target: int + ZA identifier of the targeted nucleus + + Raises + ------ + NotImplementedError: + When the ZA identifier of the projectile is not equal to 1 + (ie. other than a neutron). + + Returns + ------- + slope: float + Kalbach-Mann slope given with the same format as ACE file. + + """ + # TODO: develop for photons as projectile + # TODO: test for other particles than neutron + if iza_projectile != 1: + raise NotImplementedError( + "Developed and tested for neutron projectile only." + ) + + projectile = AtomicRepresentation.from_iza(iza_projectile) + emitted = AtomicRepresentation.from_iza(iza_emitted) + target = AtomicRepresentation.from_iza(iza_target) + compound = projectile + target + residual = compound - emitted + + slope = _calculate_kalbach_slope( + energy_projectile, + energy_emitted, + projectile, + target, + compound, + emitted, + residual + ) + + return float("%7e" % slope) + + class KalbachMann(AngleEnergy): """Kalbach-Mann distribution @@ -319,7 +782,7 @@ class KalbachMann(AngleEnergy): n_energy_out = int(ace.xss[idx + 1]) data = ace.xss[idx + 2:idx + 2 + 5*n_energy_out].copy() data.shape = (5, n_energy_out) - data[0,:] *= EV_PER_MEV + data[0, :] *= EV_PER_MEV # Create continuous distribution eout_continuous = Tabular(data[0][n_discrete_lines:], @@ -352,13 +815,28 @@ class KalbachMann(AngleEnergy): return cls(breakpoints, interpolation, energy, energy_out, km_r, km_a) @classmethod - def from_endf(cls, file_obj): - """Generate Kalbach-Mann distribution from an ENDF evaluation + def from_endf(cls, file_obj, iza_emitted, iza_target, projectile_mass): + """Generate Kalbach-Mann distribution from an ENDF evaluation. + + If the projectile is a neutron, the slope is calculated when it is + not given explicitly. Parameters ---------- file_obj : file-like object ENDF file positioned at the start of the Kalbach-Mann distribution + iza_emitted : int + ZA identifier of the emitted particle + iza_target : int + ZA identifier of the target + projectile_mass: float + Mass of the projectile + + Warns + ----- + UserWarning + If the mass of the projectile is not equal to 1 (other than + a neutron), the slope is not calculated and set to 0 if missing. Returns ------- @@ -374,6 +852,7 @@ class KalbachMann(AngleEnergy): energy_out = [] precompound = [] slope = [] + calculated_slope = [] for i in range(ne): items, values = get_list_record(file_obj) energy[i] = items[1] @@ -385,19 +864,46 @@ class KalbachMann(AngleEnergy): values.shape = (n_energy_out, n_angle + 2) # Outgoing energy distribution at the i-th incoming energy - eout_i = values[:,0] - eout_p_i = values[:,1] + eout_i = values[:, 0] + eout_p_i = values[:, 1] energy_out_i = Tabular(eout_i, eout_p_i, INTERPOLATION_SCHEME[lep]) energy_out.append(energy_out_i) - # Precompound and slope factors for Kalbach-Mann - r_i = values[:,2] + # Precompound factors for Kalbach-Mann + r_i = values[:, 2] + + # Slope factors for Kalbach-Mann if n_angle == 2: - a_i = values[:,3] + a_i = values[:, 3] + calculated_slope.append(False) else: - a_i = np.zeros_like(r_i) + # Check if the projectile is not a neutron + if not np.isclose(projectile_mass, 1.0, atol=1.0e-12, rtol=0.): + warn( + "Kalbach-Mann slope calculation is only available with " + "neutrons as projectile. Slope coefficients are set to 0." + ) + a_i = np.zeros_like(r_i) + calculated_slope.append(False) + + else: + # TODO: retrieve IZA of the projectile + iza_projectile = 1 + a_i = [return_kalbach_slope(energy_projectile=energy[i], + energy_emitted=e, + iza_projectile=iza_projectile, + iza_emitted=iza_emitted, + iza_target=iza_target) + for e in eout_i] + calculated_slope.append(True) + precompound.append(Tabulated1D(eout_i, r_i)) slope.append(Tabulated1D(eout_i, a_i)) - return cls(tab2.breakpoints, tab2.interpolation, energy, - energy_out, precompound, slope) + km_distribution = cls(tab2.breakpoints, tab2.interpolation, energy, + energy_out, precompound, slope) + + # List of bool to indicate slope calculation by OpenMC + km_distribution._calculated_slope = calculated_slope + + return km_distribution diff --git a/openmc/data/njoy.py b/openmc/data/njoy.py index 305edaf4f..fbb26be05 100644 --- a/openmc/data/njoy.py +++ b/openmc/data/njoy.py @@ -127,7 +127,7 @@ acer / %%%%%%%%%%%%%%%%%%%%%%%% Write out in ACE format %%%%%%%%%%%%%%%%%%%%%%%% 1 0 1 .{ext} / '{library}: {zsymam} at {temperature}'/ {mat} {temperature} -1 1/ +1 1 {ismoothing}/ / """ @@ -248,7 +248,8 @@ def make_pendf(filename, pendf='pendf', error=0.001, stdout=False): def make_ace(filename, temperatures=None, acer=True, xsdir=None, output_dir=None, pendf=False, error=0.001, broadr=True, - heatr=True, gaspr=True, purr=True, evaluation=None, **kwargs): + heatr=True, gaspr=True, purr=True, evaluation=None, + smoothing=True, **kwargs): """Generate incident neutron ACE file from an ENDF file File names can be passed to @@ -298,6 +299,8 @@ def make_ace(filename, temperatures=None, acer=True, xsdir=None, evaluation : openmc.data.endf.Evaluation, optional If the ENDF file contains multiple material evaluations, this argument indicates which evaluation should be used. + smoothing : bool, optional + If the smoothing option in ACER is on (1) or off (0) in the card 6. **kwargs Keyword arguments passed to :func:`openmc.data.njoy.run` @@ -380,6 +383,10 @@ def make_ace(filename, temperatures=None, acer=True, xsdir=None, # acer if acer: + if smoothing is True: + ismoothing = 1 + else: + ismoothing = 0 nacer_in = nlast for i, temperature in enumerate(temperatures): # Extend input with an ACER run for each temperature diff --git a/openmc/data/reaction.py b/openmc/data/reaction.py index 5e4287f16..42fc576da 100644 --- a/openmc/data/reaction.py +++ b/openmc/data/reaction.py @@ -80,6 +80,14 @@ def _get_products(ev, mt): mt : int The MT value of the reaction to get products for + Raises + ------ + IOError: + When the Kalbach-Mann systematics is used, but the product + is not defined in the 'center-of-mass' system. The breakup logic + is not implemented which can lead to this error being raised while + the definition of the product is correct. + Returns ------- products : list of openmc.data.Product @@ -141,7 +149,26 @@ def _get_products(ev, mt): if lang == 1: p.distribution = [CorrelatedAngleEnergy.from_endf(file_obj)] elif lang == 2: - p.distribution = [KalbachMann.from_endf(file_obj)] + # Products need to be described in the center-of-mass system + product_center_of_mass = False + if reference_frame == 'center-of-mass': + product_center_of_mass = True + elif reference_frame == 'light-heavy': + product_center_of_mass = (awr <= 4.0) + # TODO: 'breakup' logic not implemented + + if product_center_of_mass is False: + raise IOError( + "Kalbach-Mann representation must be defined in the " + "'center-of-mass' system" + ) + + zat = ev.target["atomic_number"] * 1000 + ev.target["mass_number"] + projectile_mass = ev.projectile["mass"] + p.distribution = [KalbachMann.from_endf(file_obj, + za, + zat, + projectile_mass)] elif law == 2: # Discrete two-body scattering diff --git a/tests/unit_tests/test_data_kalbach_mann.py b/tests/unit_tests/test_data_kalbach_mann.py new file mode 100644 index 000000000..99869808d --- /dev/null +++ b/tests/unit_tests/test_data_kalbach_mann.py @@ -0,0 +1,280 @@ +"""Test of the Kalbach-Mann slope calculation when data are +retrieved from ENDF files.""" + +import os +import pytest +import numpy as np + +from openmc.data import IncidentNeutron +from openmc.data import AtomicRepresentation +from openmc.data.kalbach_mann import _BREAKING_ENERGY_TRITON +from openmc.data.kalbach_mann import _M_TRITON +from openmc.data.kalbach_mann import _SM_TRITON +from openmc.data.kalbach_mann import _calculate_separation_energy +from openmc.data.kalbach_mann import _return_emission_channel_energy +from openmc.data.kalbach_mann import _return_entrance_channel_energy +from openmc.data.kalbach_mann import _calculate_kalbach_slope +from openmc.data import return_kalbach_slope +from openmc.data import KalbachMann + +from . import needs_njoy + + +@pytest.fixture(scope='module') +def neutron(): + """Neutron AtomicRepresentation.""" + return AtomicRepresentation(z=0, a=1) + + +@pytest.fixture(scope='module') +def triton(): + """Triton AtomicRepresentation.""" + return AtomicRepresentation(z=1, a=3) + + +@pytest.fixture(scope='module') +def b10(): + """B10 AtomicRepresentation.""" + return AtomicRepresentation(z=5, a=10) + + +@pytest.fixture(scope='module') +def c12(): + """C12 AtomicRepresentation.""" + return AtomicRepresentation(z=6, a=12) + + +@pytest.fixture(scope='module') +def c13(): + """C13 AtomicRepresentation.""" + return AtomicRepresentation(z=6, a=13) + + +@pytest.fixture(scope='module') +def na23(): + """Na23 AtomicRepresentation.""" + return AtomicRepresentation(z=11, a=23) + + +def test_atomic_representation(neutron, triton, b10, c12, c13, na23): + """Test the AtomicRepresentation class.""" + # Test instanciation from_iza + assert b10 == AtomicRepresentation.from_iza(5010) + + # Test instanciation from_iza using IZA translation + assert c12 == AtomicRepresentation.from_iza(6000) + + # Test addition + assert c13 + b10 == na23 + + # Test substraction + assert c13 - c12 == neutron + assert c13 - b10 == triton + + # Test properties when no information for Kalbach-Mann are given + assert c13.a == 13 + assert c13.z == 6 + assert c13.n == 7 + assert c13.breaking_energy is None + assert c13.M is None + assert c13.m is None + assert c13.iza == 6013 + + # Test properties when information for Kalbach-Mann are given + assert triton.a == 3 + assert triton.z == 1 + assert triton.n == 2 + assert triton.breaking_energy == _BREAKING_ENERGY_TRITON + assert triton.M == _M_TRITON + assert triton.m == _SM_TRITON + assert triton.iza == 1003 + + # Test instanciation errors + with pytest.raises(IOError): + AtomicRepresentation(z=5, a=1) + with pytest.raises(ValueError): + AtomicRepresentation(z=-1, a=1) + with pytest.raises(IOError): + AtomicRepresentation(z=5, a=0) + with pytest.raises(IOError): + AtomicRepresentation(z=5, a=-2) + with pytest.raises(OSError): + neutron - triton + + +def test__calculate_separation_energy(triton, b10, c13): + """Comparison to hand-calculations on a simple example.""" + assert _calculate_separation_energy( + compound=c13, + nucleus=b10, + particle=triton + ) == pytest.approx(18.6880713) + + +def test__return_entrance_channel_energy(): + """Comparison to hand-calculations on a simple example.""" + assert _return_entrance_channel_energy( + e_p=5.2, + awr_t=13.7, + awr_p=7.2 + ) == pytest.approx(3.4086124) + + +def test__return_emission_channel_energy(): + """Comparison to hand-calculations on a simple example.""" + assert _return_emission_channel_energy( + e_e=6.8, + awr_r=49.2, + awr_e=5.4 + ) == pytest.approx(7.5463415) + + +def test__calculate_kalbach_slope(neutron, triton, b10, c12, c13): + """Comparison to hand-calculations for n + c12 -> c13 -> triton + b10.""" + energy_projectile = 10.2 # [eV] + energy_emitted = 5.4 # [eV] + + assert _calculate_kalbach_slope( + energy_projectile=energy_projectile, + energy_emitted=energy_emitted, + projectile=neutron, + target=c12, + compound=c13, + emitted=triton, + residual=b10 + ) == pytest.approx(0.8409921475) + + +def test_return_kalbach_slope(): + """Comparison to hand-calculations for n + c12 -> c13 -> triton + b10.""" + energy_projectile = 10.2 # [eV] + energy_emitted = 5.4 # [eV] + + # Check that NotImplementedError is raised if the projectile is not + # a neutron + with pytest.raises(NotImplementedError): + return_kalbach_slope( + energy_projectile=energy_projectile, + energy_emitted=energy_emitted, + iza_projectile=1000, + iza_emitted=1, + iza_target=6012 + ) + + assert return_kalbach_slope( + energy_projectile=energy_projectile, + energy_emitted=energy_emitted, + iza_projectile=1, + iza_emitted=1003, + iza_target=6012 + ) == pytest.approx(0.8409921475) + + +@pytest.mark.parametrize( + "hdf5_filename, endf_type, endf_filename", [ + ('O16.h5', 'neutrons', 'n-008_O_016.endf'), + ('Ca46.h5', 'neutrons', 'n-020_Ca_046.endf'), + ('Hg204.h5', 'neutrons', 'n-080_Hg_204.endf') + ] +) +def test_comparison_slope_hdf5(hdf5_filename, endf_type, endf_filename): + """Test the calculation of the Kalbach-Mann slope done by OpenMC + by comparing it to HDF5 data. The test is based on the first product + of MT=5 (neutron). The isotopes tested have been selected because the + corresponding products in ENDF/B-VII.1 are described using MF=6, LAW=1, + LANG=2 (ie. Kalbach-Mann systematics) and the slope is not given + explicitly. + + If an error occurs during the "validity check", this means that + the nuclear data evaluation has evolved and the distribution might + no longer be described using Kalbach-Mann systematics. Another + isotope needs to be identified and tested. + + Warning: This test is valid as long as ENDF files are not directly + used to generate the HDF5 files used in the tests. + + """ + # HDF5 data + hdf5_directory = os.path.dirname(os.environ['OPENMC_CROSS_SECTIONS']) + hdf5_path = os.path.join(hdf5_directory, hdf5_filename) + hdf5_data = IncidentNeutron.from_hdf5(hdf5_path) + hdf5_product = hdf5_data[5].products[0] + hdf5_distribution = hdf5_product.distribution[0] + + # ENDF data + endf_directory = os.environ['OPENMC_ENDF_DATA'] + endf_path = os.path.join(endf_directory, endf_type, endf_filename) + endf_data = IncidentNeutron.from_endf(endf_path) + endf_product = endf_data[5].products[0] + endf_distribution = endf_product.distribution[0] + + # Validity check + assert isinstance(endf_distribution, KalbachMann) + assert isinstance(hdf5_distribution, KalbachMann) + assert endf_product.particle == hdf5_product.particle + assert len(endf_distribution.slope) == len(hdf5_distribution.slope) + + # Results check + for i, hdf5_slope in enumerate(hdf5_distribution.slope): + + assert endf_distribution._calculated_slope[i] is True + + np.testing.assert_array_almost_equal( + endf_distribution.slope[i].y, + hdf5_slope.y, + decimal=6 + ) + + +@needs_njoy +@pytest.mark.parametrize( + "endf_type, endf_filename", [ + ('neutrons', 'n-008_O_016.endf'), + ('neutrons', 'n-020_Ca_046.endf'), + ('neutrons', 'n-080_Hg_204.endf') + ] +) +def test_comparison_slope_njoy(endf_type, endf_filename): + """Test the calculation of the Kalbach-Mann slope done by OpenMC + by comparing it to an NJOY calculation. The test is based on + the first product of MT=5 (neutron). The isotopes tested have + been selected because the corresponding products in ENDF/B-VII.1 + are described using MF=6, LAW=1, LANG=2 (ie. Kalbach-Mann + systematics) and the slope is not given explicitly. + + If an error occurs during the "validity check", this means that + the nuclear data evaluation has evolved and the distribution might + no longer be described using Kalbach-Mann systematics. Another + isotope needs to be identified and tested. + + """ + endf_directory = os.environ['OPENMC_ENDF_DATA'] + endf_path = os.path.join(endf_directory, endf_type, endf_filename) + + # ENDF data + endf_data = IncidentNeutron.from_endf(endf_path) + endf_product = endf_data[5].products[0] + endf_distribution = endf_product.distribution[0] + + # NJOY data + njoy_data = IncidentNeutron.from_njoy(endf_path, heatr=False, gaspr=False, + purr=False, smoothing=False) + njoy_product = njoy_data[5].products[0] + njoy_distribution = njoy_product.distribution[0] + + # Validity check + assert isinstance(endf_distribution, KalbachMann) + assert isinstance(njoy_distribution, KalbachMann) + assert endf_product.particle == njoy_product.particle + assert len(endf_distribution.slope) == len(njoy_distribution.slope) + + # Results check + for i, njoy_slope in enumerate(njoy_distribution.slope): + + assert endf_distribution._calculated_slope[i] is True + + np.testing.assert_array_almost_equal( + endf_distribution.slope[i].y, + njoy_slope.y, + decimal=6 + ) From 8f564052e5e6f818bd4d2b0e9ea20116c497622c Mon Sep 17 00:00:00 2001 From: Joffrey Dorville Date: Sat, 2 Apr 2022 00:05:32 -0600 Subject: [PATCH 002/131] Add the Kalbach-Mann slope documentation --- docs/source/pythonapi/data.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/pythonapi/data.rst b/docs/source/pythonapi/data.rst index 95fdfeca9..471be43d8 100644 --- a/docs/source/pythonapi/data.rst +++ b/docs/source/pythonapi/data.rst @@ -24,6 +24,7 @@ and product yields. FissionProductYields WindowedMultipole ProbabilityTables + AtomicRepresentation The following classes are used for storing atomic data (incident photon cross sections, atomic relaxation): @@ -68,6 +69,7 @@ Core Functions thin water_density zam + return_kalbach_slope One-dimensional Functions ------------------------- From af579895dd5933dc42c07bbd2c025e233c6abd8d Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 7 Apr 2022 10:55:33 -0500 Subject: [PATCH 003/131] Refactor K-M slope calculation --- openmc/data/kalbach_mann.py | 317 +++++---------------- tests/unit_tests/test_data_kalbach_mann.py | 63 +--- 2 files changed, 80 insertions(+), 300 deletions(-) diff --git a/openmc/data/kalbach_mann.py b/openmc/data/kalbach_mann.py index 11e3acf40..5c0423f30 100644 --- a/openmc/data/kalbach_mann.py +++ b/openmc/data/kalbach_mann.py @@ -13,70 +13,6 @@ from .data import EV_PER_MEV from .endf import get_list_record, get_tab2_record -# Kalbach-Mann constants as defined in ENDF-6 manual BNL-203218-2018-INRE, -# Revision 215, File 6 description for LAW=1 and LANG=2. -_C1 = 0.04 # [1/MeV] -_C2 = 1.8E-6 # [1/MeV^3] -_C3 = 6.7E-7 # [1/MeV^4] -_ET1 = 130. # [MeV] -_ET3 = 41. # [MeV] -_M_NEUTRON = 1. -_M_PROTON = 1. -_M_DEUTERON = 1. -_M_TRITON = None -_M_3HE = None -_M_ALPHA = 0. -_SM_NEUTRON = 1/2. -_SM_PROTON = 1. -_SM_DEUTERON = 1. -_SM_TRITON = 1. -_SM_3HE = 1. -_SM_ALPHA = 2. - -# Kalbach-Mann M coefficients -_TABULATED_PARTICLE_M = { - 1: _M_NEUTRON, - 1001: _M_PROTON, - 1002: _M_DEUTERON, - 1003: _M_TRITON, - 2003: _M_3HE, - 2004: _M_ALPHA -} - -# Kalbach-Mann m coefficients -_TABULATED_PARTICLE_SM = { - 1: _SM_NEUTRON, - 1001: _SM_PROTON, - 1002: _SM_DEUTERON, - 1003: _SM_TRITON, - 2003: _SM_3HE, - 2004: _SM_ALPHA -} - -# Breaking energy as defined in ENDF-6 manual BNL-203218-2018-INRE, -# Revision 215, Appendix H, Table 3. -_BREAKING_ENERGY_NEUTRON = 0. -_BREAKING_ENERGY_PROTON = 0. -_BREAKING_ENERGY_DEUTERON = 2.224566 # [MeV] -_BREAKING_ENERGY_TRITON = 8.481798 # [MeV] -_BREAKING_ENERGY_3HE = 7.718043 # [MeV] -_BREAKING_ENERGY_ALPHA = 28.29566 # [MeV] - -_TABULATED_BREAKING_ENERGY = { - 1: _BREAKING_ENERGY_NEUTRON, - 1001: _BREAKING_ENERGY_PROTON, - 1002: _BREAKING_ENERGY_DEUTERON, - 1003: _BREAKING_ENERGY_TRITON, - 2003: _BREAKING_ENERGY_3HE, - 2004: _BREAKING_ENERGY_ALPHA -} - -# Abundant IZA translation in merged library -_IZA_TRANSLATION = { - 6000: 6012, -} - - class AtomicRepresentation(EqualityMixin): """Atomic representation of an isotope or a particle. @@ -151,27 +87,6 @@ class AtomicRepresentation(EqualityMixin): def n(self): return self.a - self.z - @property - def breaking_energy(self): - breaking_energy = None - if self.iza in _TABULATED_BREAKING_ENERGY: - breaking_energy = _TABULATED_BREAKING_ENERGY[self.iza] - return breaking_energy - - @property - def M(self): - M = None - if self.iza in _TABULATED_PARTICLE_M: - M = _TABULATED_PARTICLE_M[self.iza] - return M - - @property - def m(self): - m = None - if self.iza in _TABULATED_PARTICLE_SM: - m = _TABULATED_PARTICLE_SM[self.iza] - return m - @property def iza(self): iza = self.z * 1000 + self.a @@ -234,16 +149,11 @@ class AtomicRepresentation(EqualityMixin): Atomic representation of the isotope/particle """ - if iza in _IZA_TRANSLATION.keys(): - iza = _IZA_TRANSLATION[iza] - - z = int(iza/1000) - a = np.mod(iza, 1000) - + z, a = divmod(iza, 1000) return cls(z, a) -def _calculate_separation_energy(compound, nucleus, particle): +def _separation_energy(compound, nucleus, particle): """Calculates the separation energy as defined in ENDF-6 manual BNL-203218-2018-INRE, Revision 215, File 6 description for LAW=1 and LANG=2. This function can be used for the incident or emitted @@ -251,158 +161,56 @@ def _calculate_separation_energy(compound, nucleus, particle): Parameters ---------- - compound: AtomicRepresentation + compound : AtomicRepresentation Atomic representation of the compound (C) - nucleus: AtomicRepresentation + nucleus : AtomicRepresentation Atomic representation of the nucleus (A or B) - particle: AtomicRepresentation + particle : AtomicRepresentation Atomic representation of the particle (a or b) Returns ------- - separation_energy: float + separation_energy : float Separation energy in MeV """ - coef_1 = 15.68 * (compound.a - nucleus.a) - coef_2 = 28.07 * ((compound.n - compound.z)**2 / float(compound.a) - \ - (nucleus.n - nucleus.z)**2 / float(nucleus.a)) - coef_3 = 18.56 * (compound.a**(2./3.) - nucleus.a**(2./3.)) - coef_4 = 33.22 * ((compound.n - compound.z)**2 / float(compound.a)**(4./3.) - \ - (nucleus.n - nucleus.z)**2 / float(nucleus.a)**(4./3.)) - coef_5 = 0.717 * (compound.z**2 / float(compound.a)**(1./3.) - \ - nucleus.z**2 / float(nucleus.a)**(1./3.)) - coef_6 = 1.211 * (compound.z**2 / float(compound.a) - \ - nucleus.z**2 / float(nucleus.a)) + # Determine A, Z, and N for compound and nucleus + A_c = compound.a + Z_c = compound.z + N_c = compound.n + A_a = nucleus.a + Z_a = nucleus.z + N_a = nucleus.n - separation_energy = coef_1 - coef_2 - coef_3 + coef_4 \ - - coef_5 + coef_6 - particle.breaking_energy + # Determine breakup energy of incident particle (ENDF-6 Formats Manual, + # Appendix H, Table 3) + iza_to_breaking_energy = { + 1: 0.0, + 1001: 0.0, + 1002: 2.224566, + 1003: 8.481798, + 2003: 7.718043, + 2004: 28.29566 + } + I_b = iza_to_breaking_energy[particle.iza] - return separation_energy + # Eq. 4 in in doi:10.1103/PhysRevC.37.2350 or ENDF-6 Formats Manual section + # 6.2.3.2 + return ( + 15.68 * (A_c - A_a) - + 28.07 * ((N_c - Z_c)**2 / A_c - (N_a - Z_a)**2 / A_a) - + 18.56 * (A_c**(2./3.) - A_a**(2./3.)) + + 33.22 * ((N_c - Z_c)**2 / A_c**(4./3.) - (N_a - Z_a)**2 / A_a**(4./3.)) - + 0.717 * (Z_c**2 / A_c**(1./3.) - Z_a**2 / A_a**(1./3.)) + + 1.211 * (Z_c**2 / A_c - Z_a**2 / A_a) - + I_b + ) -def _return_entrance_channel_energy(e_p, awr_t, awr_p): - """Returns the entrance channel energy as defined in ENDF-6 manual - BNL-203218-2018-INRE, Revision 215, File 6 description for LAW=1 - and LANG=2. - - Parameters - ---------- - e_p: float - Energy of the incident projectile in the laboratory system in eV - awr_t: float - Atomic weight ratio of the target - awr_p: float - Atomic weight ratio of the projectile - - Returns - ------- - epsilon_p: float - Entrance channel energy in eV - - """ - epsilon_p = e_p * awr_t / (awr_t + awr_p) - return epsilon_p - - -def _return_emission_channel_energy(e_e, awr_r, awr_e): - """Returns the emission channel energy as defined in ENDF-6 manual - BNL-203218-2018-INRE, Revision 215, File 6 description for LAW=1 - and LANG=2. - - Parameters - ---------- - e_e: float - Energy of the emitted particle in the center of mass system in eV - awr_r: float - Atomic weight ratio of the residual nucleus - awr_e: float - Atomic weight ratio of the emitted particle - - Returns - ------- - epsilon_e: float - Emission channel energy in eV - - """ - epsilon_e = e_e * (awr_r + awr_e) / awr_r - return epsilon_e - - -def _calculate_kalbach_slope(energy_projectile, - energy_emitted, - projectile, - target, - compound, - emitted, - residual): - """Calculate the Kalbach slope for projectiles other than photons - as defined in ENDF-6 manual BNL-203218-2018-INRE, Revision 215, - File 6 description for LAW=1 and LANG=2. - - The entrance and emission channel energies are not calculated with - the AWR number, but approximated with the number of mass instead. - - Parameters - ---------- - energy_projectile: float - Energy of the projectile in the laboratory system in eV - energy_emitted: float - Energy of the emitted particle in the center of mass system in eV - projectile: AtomicRepresentation - Atomic representation of the projectile - target: AtomicRepresentation - Atomic representation of the target - compound: AtomicRepresentation - Atomic representation of the compound - emitted: AtomicRepresentation - Atomic representation of the emitted particle - residual: AtomicRepresentation - Atomic representation of the residual nucleus - - Returns - ------- - slope: float - Kalbach-Mann slope - - """ - epsilon_a = _return_entrance_channel_energy( - energy_projectile, - target.a, - projectile.a - ) / EV_PER_MEV - epsilon_b = _return_emission_channel_energy( - energy_emitted, - residual.a, - emitted.a - ) / EV_PER_MEV - - s_a = _calculate_separation_energy(compound, target, projectile) - s_b = _calculate_separation_energy(compound, residual, emitted) - - e_a = epsilon_a + s_a - e_b = epsilon_b + s_b - - r_1 = min(e_a, _ET1) - r_3 = min(e_a, _ET3) - - x_1 = r_1 * e_b / e_a - x_3 = r_3 * e_b / e_a - - slope = _C1 * x_1 \ - + _C2 * x_1**3 \ - + _C3 * projectile.M * emitted.m * x_3**4 - - return slope - - -def return_kalbach_slope(energy_projectile, - energy_emitted, - iza_projectile, - iza_emitted, - iza_target): +def kalbach_slope(energy_projectile, energy_emitted, iza_projectile, + iza_emitted, iza_target): """Returns Kalbach-Mann slope from calculations. - + The associated reaction is defined as: A + a -> C -> B + b @@ -456,21 +264,42 @@ def return_kalbach_slope(energy_projectile, "Developed and tested for neutron projectile only." ) + # Special handling of elemental carbon + if iza_emitted == 6000: + iza_emitted = 6012 + if iza_target == 6000: + iza_target = 6012 + projectile = AtomicRepresentation.from_iza(iza_projectile) emitted = AtomicRepresentation.from_iza(iza_emitted) target = AtomicRepresentation.from_iza(iza_target) compound = projectile + target residual = compound - emitted - slope = _calculate_kalbach_slope( - energy_projectile, - energy_emitted, - projectile, - target, - compound, - emitted, - residual - ) + # Calculate entrance and emission channel energy in MeV, defined in section + # 6.2.3.2 in the ENDF-6 Formats Manual + epsilon_a = energy_projectile * target.a / (target.a + projectile.a) / EV_PER_MEV + epsilon_b = energy_emitted * (residual.a + emitted.a) \ + / (residual.a * EV_PER_MEV) + + # Calculate separation energies using Eq. 4 in doi:10.1103/PhysRevC.37.2350 + # or ENDF-6 Formats Manual section 6.2.3.2 + s_a = _separation_energy(compound, target, projectile) + s_b = _separation_energy(compound, residual, emitted) + + # See Eq. 10 in doi:10.1103/PhysRevC.37.2350 or section 6.2.3.2 in the + # ENDF-6 Formats Manual + iza_to_M = {1: 1.0, 1001: 1.0, 1002: 1.0, 2004: 0.0} + iza_to_m = {1: 0.5, 1001: 1.0, 1002: 1.0, 1003: 1.0, 2003: 1.0, 2004: 2.0} + M = iza_to_M[projectile.iza] + m = iza_to_m[emitted.iza] + e_a = epsilon_a + s_a + e_b = epsilon_b + s_b + r_1 = min(e_a, 130.) + r_3 = min(e_a, 41.) + x_1 = r_1 * e_b / e_a + x_3 = r_3 * e_b / e_a + slope = 0.04 * x_1 + 1.8e-6 * x_1**3 + 6.7e-7 * M * m * x_3**4 return float("%7e" % slope) @@ -889,11 +718,11 @@ class KalbachMann(AngleEnergy): else: # TODO: retrieve IZA of the projectile iza_projectile = 1 - a_i = [return_kalbach_slope(energy_projectile=energy[i], - energy_emitted=e, - iza_projectile=iza_projectile, - iza_emitted=iza_emitted, - iza_target=iza_target) + a_i = [kalbach_slope(energy_projectile=energy[i], + energy_emitted=e, + iza_projectile=iza_projectile, + iza_emitted=iza_emitted, + iza_target=iza_target) for e in eout_i] calculated_slope.append(True) diff --git a/tests/unit_tests/test_data_kalbach_mann.py b/tests/unit_tests/test_data_kalbach_mann.py index 99869808d..643fffcfc 100644 --- a/tests/unit_tests/test_data_kalbach_mann.py +++ b/tests/unit_tests/test_data_kalbach_mann.py @@ -7,14 +7,8 @@ import numpy as np from openmc.data import IncidentNeutron from openmc.data import AtomicRepresentation -from openmc.data.kalbach_mann import _BREAKING_ENERGY_TRITON -from openmc.data.kalbach_mann import _M_TRITON -from openmc.data.kalbach_mann import _SM_TRITON -from openmc.data.kalbach_mann import _calculate_separation_energy -from openmc.data.kalbach_mann import _return_emission_channel_energy -from openmc.data.kalbach_mann import _return_entrance_channel_energy -from openmc.data.kalbach_mann import _calculate_kalbach_slope -from openmc.data import return_kalbach_slope +from openmc.data.kalbach_mann import _separation_energy +from openmc.data import kalbach_slope from openmc.data import KalbachMann from . import needs_njoy @@ -61,9 +55,6 @@ def test_atomic_representation(neutron, triton, b10, c12, c13, na23): # Test instanciation from_iza assert b10 == AtomicRepresentation.from_iza(5010) - # Test instanciation from_iza using IZA translation - assert c12 == AtomicRepresentation.from_iza(6000) - # Test addition assert c13 + b10 == na23 @@ -75,18 +66,12 @@ def test_atomic_representation(neutron, triton, b10, c12, c13, na23): assert c13.a == 13 assert c13.z == 6 assert c13.n == 7 - assert c13.breaking_energy is None - assert c13.M is None - assert c13.m is None assert c13.iza == 6013 # Test properties when information for Kalbach-Mann are given assert triton.a == 3 assert triton.z == 1 assert triton.n == 2 - assert triton.breaking_energy == _BREAKING_ENERGY_TRITON - assert triton.M == _M_TRITON - assert triton.m == _SM_TRITON assert triton.iza == 1003 # Test instanciation errors @@ -102,50 +87,16 @@ def test_atomic_representation(neutron, triton, b10, c12, c13, na23): neutron - triton -def test__calculate_separation_energy(triton, b10, c13): +def test_separation_energy(triton, b10, c13): """Comparison to hand-calculations on a simple example.""" - assert _calculate_separation_energy( + assert _separation_energy( compound=c13, nucleus=b10, particle=triton ) == pytest.approx(18.6880713) -def test__return_entrance_channel_energy(): - """Comparison to hand-calculations on a simple example.""" - assert _return_entrance_channel_energy( - e_p=5.2, - awr_t=13.7, - awr_p=7.2 - ) == pytest.approx(3.4086124) - - -def test__return_emission_channel_energy(): - """Comparison to hand-calculations on a simple example.""" - assert _return_emission_channel_energy( - e_e=6.8, - awr_r=49.2, - awr_e=5.4 - ) == pytest.approx(7.5463415) - - -def test__calculate_kalbach_slope(neutron, triton, b10, c12, c13): - """Comparison to hand-calculations for n + c12 -> c13 -> triton + b10.""" - energy_projectile = 10.2 # [eV] - energy_emitted = 5.4 # [eV] - - assert _calculate_kalbach_slope( - energy_projectile=energy_projectile, - energy_emitted=energy_emitted, - projectile=neutron, - target=c12, - compound=c13, - emitted=triton, - residual=b10 - ) == pytest.approx(0.8409921475) - - -def test_return_kalbach_slope(): +def test_kalbach_slope(): """Comparison to hand-calculations for n + c12 -> c13 -> triton + b10.""" energy_projectile = 10.2 # [eV] energy_emitted = 5.4 # [eV] @@ -153,7 +104,7 @@ def test_return_kalbach_slope(): # Check that NotImplementedError is raised if the projectile is not # a neutron with pytest.raises(NotImplementedError): - return_kalbach_slope( + kalbach_slope( energy_projectile=energy_projectile, energy_emitted=energy_emitted, iza_projectile=1000, @@ -161,7 +112,7 @@ def test_return_kalbach_slope(): iza_target=6012 ) - assert return_kalbach_slope( + assert kalbach_slope( energy_projectile=energy_projectile, energy_emitted=energy_emitted, iza_projectile=1, From e7595c32015e73671a96a3b2cec6ab9b8ca1c491 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 7 Apr 2022 10:58:15 -0500 Subject: [PATCH 004/131] Don't throw away precision on K-M slope calculation --- openmc/data/kalbach_mann.py | 4 +--- tests/unit_tests/test_data_kalbach_mann.py | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/openmc/data/kalbach_mann.py b/openmc/data/kalbach_mann.py index 5c0423f30..932deea87 100644 --- a/openmc/data/kalbach_mann.py +++ b/openmc/data/kalbach_mann.py @@ -299,9 +299,7 @@ def kalbach_slope(energy_projectile, energy_emitted, iza_projectile, r_3 = min(e_a, 41.) x_1 = r_1 * e_b / e_a x_3 = r_3 * e_b / e_a - slope = 0.04 * x_1 + 1.8e-6 * x_1**3 + 6.7e-7 * M * m * x_3**4 - - return float("%7e" % slope) + return 0.04 * x_1 + 1.8e-6 * x_1**3 + 6.7e-7 * M * m * x_3**4 class KalbachMann(AngleEnergy): diff --git a/tests/unit_tests/test_data_kalbach_mann.py b/tests/unit_tests/test_data_kalbach_mann.py index 643fffcfc..27890dacd 100644 --- a/tests/unit_tests/test_data_kalbach_mann.py +++ b/tests/unit_tests/test_data_kalbach_mann.py @@ -173,7 +173,7 @@ def test_comparison_slope_hdf5(hdf5_filename, endf_type, endf_filename): np.testing.assert_array_almost_equal( endf_distribution.slope[i].y, hdf5_slope.y, - decimal=6 + decimal=5 ) @@ -227,5 +227,5 @@ def test_comparison_slope_njoy(endf_type, endf_filename): np.testing.assert_array_almost_equal( endf_distribution.slope[i].y, njoy_slope.y, - decimal=6 + decimal=5 ) From a8fd1ddf7144b657d7050eba01a4bd4255aed406 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 7 Apr 2022 11:11:51 -0500 Subject: [PATCH 005/131] Change iza --> za --- openmc/data/kalbach_mann.py | 119 +++++++++------------ tests/unit_tests/test_data_kalbach_mann.py | 20 ++-- 2 files changed, 61 insertions(+), 78 deletions(-) diff --git a/openmc/data/kalbach_mann.py b/openmc/data/kalbach_mann.py index 932deea87..8d1a5c2e1 100644 --- a/openmc/data/kalbach_mann.py +++ b/openmc/data/kalbach_mann.py @@ -18,36 +18,28 @@ class AtomicRepresentation(EqualityMixin): Parameters ---------- - z: int + z : int Number of protons (atomic number) - a: int + a : int Number of nucleons (mass number) Raises ------ - IOError: + IOError When the number of protons (z) declared is higher than the number of nucleons (a) Attributes ---------- - z: int + z : int Number of protons (atomic number) - a: int + a : int Number of nucleons (mass number) - n: int + n : int Number of neutrons - breaking_energy: float - Energy required to break the isotope or particle into their - constituent nucleons from tabulated values - M: float - Kalbach-Mann M coefficient - m: float - Kalbach-Mann m coefficient - iza: int - ZA identifier defined as: - iza = Z x 1000 + A, - where Z is the number of protons and A the number of nucleons + za : int + ZA identifier, 1000*Z + A, where Z is the atomic number and A the mass + number """ def __init__(self, z, a): @@ -88,9 +80,8 @@ class AtomicRepresentation(EqualityMixin): return self.a - self.z @property - def iza(self): - iza = self.z * 1000 + self.a - return iza + def za(self): + return self.z * 1000 + self.a @a.setter def a(self, an): @@ -112,9 +103,9 @@ class AtomicRepresentation(EqualityMixin): Parameters ---------- - z: int + z : int Number of protons (atomic number) - a: int + a : int Number of nucleons (mass number) Raises @@ -131,17 +122,14 @@ class AtomicRepresentation(EqualityMixin): ) @classmethod - def from_iza(cls, iza): + def from_za(cls, za): """Instantiates an AtomicRepresentation from a ZA identifier. - The ZA identifier is defined as: - iza = Z x 1000 + A, - where Z is the number of protons and A the number of nucleons. - Parameters ---------- - iza: int - ZA identifier + za : int + ZA identifier, 1000*Z + A, where Z is the atomic number and A the + mass number Returns ------- @@ -149,7 +137,7 @@ class AtomicRepresentation(EqualityMixin): Atomic representation of the isotope/particle """ - z, a = divmod(iza, 1000) + z, a = divmod(za, 1000) return cls(z, a) @@ -184,7 +172,7 @@ def _separation_energy(compound, nucleus, particle): # Determine breakup energy of incident particle (ENDF-6 Formats Manual, # Appendix H, Table 3) - iza_to_breaking_energy = { + za_to_breaking_energy = { 1: 0.0, 1001: 0.0, 1002: 2.224566, @@ -192,7 +180,7 @@ def _separation_energy(compound, nucleus, particle): 2003: 7.718043, 2004: 28.29566 } - I_b = iza_to_breaking_energy[particle.iza] + I_b = za_to_breaking_energy[particle.za] # Eq. 4 in in doi:10.1103/PhysRevC.37.2350 or ENDF-6 Formats Manual section # 6.2.3.2 @@ -207,8 +195,8 @@ def _separation_energy(compound, nucleus, particle): ) -def kalbach_slope(energy_projectile, energy_emitted, iza_projectile, - iza_emitted, iza_target): +def kalbach_slope(energy_projectile, energy_emitted, za_projectile, + za_emitted, za_target): """Returns Kalbach-Mann slope from calculations. The associated reaction is defined as: @@ -222,10 +210,6 @@ def kalbach_slope(energy_projectile, energy_emitted, iza_projectile, - B is the residual nucleus, - b is the emitted particle. - This function uses the concept of ZA identifier defined as: - iza = Z x 1000 + A, - where Z is the number of protons and A the number of nucleons. - The Kalbach-Mann slope calculation is done as defined in ENDF-6 manual BNL-203218-2018-INRE, Revision 215, File 6 description for LAW=1 and LANG=2. One exception to this, is that the entrance and emission channel @@ -234,45 +218,44 @@ def kalbach_slope(energy_projectile, energy_emitted, iza_projectile, Parameters ---------- - energy_projectile: float + energy_projectile : float Energy of the projectile in the laboratory system in eV - energy_emitted: float + energy_emitted : float Energy of the emitted particle in the center of mass system in eV - iza_projectile: int + za_projectile : int ZA identifier of the projectile - iza_emitted: int + za_emitted : int ZA identifier of the emitted particle - iza_target: int + za_target : int ZA identifier of the targeted nucleus Raises ------ - NotImplementedError: - When the ZA identifier of the projectile is not equal to 1 - (ie. other than a neutron). + NotImplementedError + When the projectile is not a neutron Returns ------- - slope: float + slope : float Kalbach-Mann slope given with the same format as ACE file. """ # TODO: develop for photons as projectile # TODO: test for other particles than neutron - if iza_projectile != 1: + if za_projectile != 1: raise NotImplementedError( "Developed and tested for neutron projectile only." ) # Special handling of elemental carbon - if iza_emitted == 6000: - iza_emitted = 6012 - if iza_target == 6000: - iza_target = 6012 + if za_emitted == 6000: + za_emitted = 6012 + if za_target == 6000: + za_target = 6012 - projectile = AtomicRepresentation.from_iza(iza_projectile) - emitted = AtomicRepresentation.from_iza(iza_emitted) - target = AtomicRepresentation.from_iza(iza_target) + projectile = AtomicRepresentation.from_za(za_projectile) + emitted = AtomicRepresentation.from_za(za_emitted) + target = AtomicRepresentation.from_za(za_target) compound = projectile + target residual = compound - emitted @@ -289,10 +272,10 @@ def kalbach_slope(energy_projectile, energy_emitted, iza_projectile, # See Eq. 10 in doi:10.1103/PhysRevC.37.2350 or section 6.2.3.2 in the # ENDF-6 Formats Manual - iza_to_M = {1: 1.0, 1001: 1.0, 1002: 1.0, 2004: 0.0} - iza_to_m = {1: 0.5, 1001: 1.0, 1002: 1.0, 1003: 1.0, 2003: 1.0, 2004: 2.0} - M = iza_to_M[projectile.iza] - m = iza_to_m[emitted.iza] + za_to_M = {1: 1.0, 1001: 1.0, 1002: 1.0, 2004: 0.0} + za_to_m = {1: 0.5, 1001: 1.0, 1002: 1.0, 1003: 1.0, 2003: 1.0, 2004: 2.0} + M = za_to_M[projectile.za] + m = za_to_m[emitted.za] e_a = epsilon_a + s_a e_b = epsilon_b + s_b r_1 = min(e_a, 130.) @@ -642,7 +625,7 @@ class KalbachMann(AngleEnergy): return cls(breakpoints, interpolation, energy, energy_out, km_r, km_a) @classmethod - def from_endf(cls, file_obj, iza_emitted, iza_target, projectile_mass): + def from_endf(cls, file_obj, za_emitted, za_target, projectile_mass): """Generate Kalbach-Mann distribution from an ENDF evaluation. If the projectile is a neutron, the slope is calculated when it is @@ -652,11 +635,11 @@ class KalbachMann(AngleEnergy): ---------- file_obj : file-like object ENDF file positioned at the start of the Kalbach-Mann distribution - iza_emitted : int + za_emitted : int ZA identifier of the emitted particle - iza_target : int + za_target : int ZA identifier of the target - projectile_mass: float + projectile_mass : float Mass of the projectile Warns @@ -714,13 +697,13 @@ class KalbachMann(AngleEnergy): calculated_slope.append(False) else: - # TODO: retrieve IZA of the projectile - iza_projectile = 1 + # TODO: retrieve ZA of the projectile + za_projectile = 1 a_i = [kalbach_slope(energy_projectile=energy[i], energy_emitted=e, - iza_projectile=iza_projectile, - iza_emitted=iza_emitted, - iza_target=iza_target) + za_projectile=za_projectile, + za_emitted=za_emitted, + za_target=za_target) for e in eout_i] calculated_slope.append(True) diff --git a/tests/unit_tests/test_data_kalbach_mann.py b/tests/unit_tests/test_data_kalbach_mann.py index 27890dacd..36282f7c5 100644 --- a/tests/unit_tests/test_data_kalbach_mann.py +++ b/tests/unit_tests/test_data_kalbach_mann.py @@ -52,8 +52,8 @@ def na23(): def test_atomic_representation(neutron, triton, b10, c12, c13, na23): """Test the AtomicRepresentation class.""" - # Test instanciation from_iza - assert b10 == AtomicRepresentation.from_iza(5010) + # Test instantiation from_za + assert b10 == AtomicRepresentation.from_za(5010) # Test addition assert c13 + b10 == na23 @@ -66,13 +66,13 @@ def test_atomic_representation(neutron, triton, b10, c12, c13, na23): assert c13.a == 13 assert c13.z == 6 assert c13.n == 7 - assert c13.iza == 6013 + assert c13.za == 6013 # Test properties when information for Kalbach-Mann are given assert triton.a == 3 assert triton.z == 1 assert triton.n == 2 - assert triton.iza == 1003 + assert triton.za == 1003 # Test instanciation errors with pytest.raises(IOError): @@ -107,17 +107,17 @@ def test_kalbach_slope(): kalbach_slope( energy_projectile=energy_projectile, energy_emitted=energy_emitted, - iza_projectile=1000, - iza_emitted=1, - iza_target=6012 + za_projectile=1000, + za_emitted=1, + za_target=6012 ) assert kalbach_slope( energy_projectile=energy_projectile, energy_emitted=energy_emitted, - iza_projectile=1, - iza_emitted=1003, - iza_target=6012 + za_projectile=1, + za_emitted=1003, + za_target=6012 ) == pytest.approx(0.8409921475) From 751d09836c5d0710552073f265694e14c8599a32 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 7 Apr 2022 13:20:56 -0500 Subject: [PATCH 006/131] Make 'a' and 'z' properties read-only --- openmc/data/kalbach_mann.py | 52 ++++------------------ tests/unit_tests/test_data_kalbach_mann.py | 8 ++-- 2 files changed, 13 insertions(+), 47 deletions(-) diff --git a/openmc/data/kalbach_mann.py b/openmc/data/kalbach_mann.py index 8d1a5c2e1..4c6b89e41 100644 --- a/openmc/data/kalbach_mann.py +++ b/openmc/data/kalbach_mann.py @@ -43,11 +43,17 @@ class AtomicRepresentation(EqualityMixin): """ def __init__(self, z, a): - self._consistency_check(z, a) + # Sanity checks on values + cv.check_type('z', z, Integral) + cv.check_greater_than('z', z, 0, equality=True) + cv.check_type('a', a, Integral) + cv.check_greater_than('a', a, 0, equality=True) + if z > a: + raise ValueError(f"Number of protons ({z}) must be less than or " + f"equal to number of nucleons ({a}).") + self._z = z self._a = a - self.z = self._z - self.a = self._a def __add__(self, other): """Adds two AtomicRepresentations. @@ -67,12 +73,10 @@ class AtomicRepresentation(EqualityMixin): @property def a(self): - self._consistency_check(self._z, self._a) return self._a @property def z(self): - self._consistency_check(self._z, self._a) return self._z @property @@ -83,44 +87,6 @@ class AtomicRepresentation(EqualityMixin): def za(self): return self.z * 1000 + self.a - @a.setter - def a(self, an): - cv.check_type('a', an, Integral) - cv.check_greater_than('a', an, 0, equality=True) - self._consistency_check(self._z, an) - self._a = an - - @z.setter - def z(self, zn): - cv.check_type('z', zn, Integral) - cv.check_greater_than('z', zn, 0, equality=True) - self._consistency_check(zn, self._a) - self._z = zn - - @staticmethod - def _consistency_check(z, a): - """Simple consistency check. - - Parameters - ---------- - z : int - Number of protons (atomic number) - a : int - Number of nucleons (mass number) - - Raises - ------ - IOError: - When the number of protons (z) declared is higher than the number - of nucleons (a) - - """ - if z > a: - raise IOError( - "Number of protons (%i) incompatible with number of " - "nucleons (%i)" % (z, a) - ) - @classmethod def from_za(cls, za): """Instantiates an AtomicRepresentation from a ZA identifier. diff --git a/tests/unit_tests/test_data_kalbach_mann.py b/tests/unit_tests/test_data_kalbach_mann.py index 36282f7c5..bd540addb 100644 --- a/tests/unit_tests/test_data_kalbach_mann.py +++ b/tests/unit_tests/test_data_kalbach_mann.py @@ -75,15 +75,15 @@ def test_atomic_representation(neutron, triton, b10, c12, c13, na23): assert triton.za == 1003 # Test instanciation errors - with pytest.raises(IOError): + with pytest.raises(ValueError): AtomicRepresentation(z=5, a=1) with pytest.raises(ValueError): AtomicRepresentation(z=-1, a=1) - with pytest.raises(IOError): + with pytest.raises(ValueError): AtomicRepresentation(z=5, a=0) - with pytest.raises(IOError): + with pytest.raises(ValueError): AtomicRepresentation(z=5, a=-2) - with pytest.raises(OSError): + with pytest.raises(ValueError): neutron - triton From f6858279123e7ed413df405f22e0cefb1274462c Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 26 Apr 2022 16:41:49 -0500 Subject: [PATCH 007/131] Make AtomicRepresentation private (leading underscore) --- docs/source/pythonapi/data.rst | 3 +-- openmc/data/kalbach_mann.py | 26 +++++++++++----------- tests/unit_tests/test_data_kalbach_mann.py | 25 ++++++++++----------- 3 files changed, 26 insertions(+), 28 deletions(-) diff --git a/docs/source/pythonapi/data.rst b/docs/source/pythonapi/data.rst index 471be43d8..a07fa9bc3 100644 --- a/docs/source/pythonapi/data.rst +++ b/docs/source/pythonapi/data.rst @@ -24,7 +24,6 @@ and product yields. FissionProductYields WindowedMultipole ProbabilityTables - AtomicRepresentation The following classes are used for storing atomic data (incident photon cross sections, atomic relaxation): @@ -65,11 +64,11 @@ Core Functions dose_coefficients gnd_name isotopes + kalbach_slope linearize thin water_density zam - return_kalbach_slope One-dimensional Functions ------------------------- diff --git a/openmc/data/kalbach_mann.py b/openmc/data/kalbach_mann.py index 4c6b89e41..10c002234 100644 --- a/openmc/data/kalbach_mann.py +++ b/openmc/data/kalbach_mann.py @@ -13,7 +13,7 @@ from .data import EV_PER_MEV from .endf import get_list_record, get_tab2_record -class AtomicRepresentation(EqualityMixin): +class _AtomicRepresentation(EqualityMixin): """Atomic representation of an isotope or a particle. Parameters @@ -56,20 +56,20 @@ class AtomicRepresentation(EqualityMixin): self._a = a def __add__(self, other): - """Adds two AtomicRepresentations. + """Adds two _AtomicRepresentations. """ z = self.z + other.z a = self.a + other.a - return AtomicRepresentation(z=z, a=a) + return _AtomicRepresentation(z=z, a=a) def __sub__(self, other): - """Substracts two AtomicRepresentations. + """Substracts two _AtomicRepresentations. """ z = self.z - other.z a = self.a - other.a - return AtomicRepresentation(z=z, a=a) + return _AtomicRepresentation(z=z, a=a) @property def a(self): @@ -89,7 +89,7 @@ class AtomicRepresentation(EqualityMixin): @classmethod def from_za(cls, za): - """Instantiates an AtomicRepresentation from a ZA identifier. + """Instantiate an _AtomicRepresentation from a ZA identifier. Parameters ---------- @@ -99,7 +99,7 @@ class AtomicRepresentation(EqualityMixin): Returns ------- - AtomicRepresentation + _AtomicRepresentation Atomic representation of the isotope/particle """ @@ -115,11 +115,11 @@ def _separation_energy(compound, nucleus, particle): Parameters ---------- - compound : AtomicRepresentation + compound : _AtomicRepresentation Atomic representation of the compound (C) - nucleus : AtomicRepresentation + nucleus : _AtomicRepresentation Atomic representation of the nucleus (A or B) - particle : AtomicRepresentation + particle : _AtomicRepresentation Atomic representation of the particle (a or b) Returns @@ -219,9 +219,9 @@ def kalbach_slope(energy_projectile, energy_emitted, za_projectile, if za_target == 6000: za_target = 6012 - projectile = AtomicRepresentation.from_za(za_projectile) - emitted = AtomicRepresentation.from_za(za_emitted) - target = AtomicRepresentation.from_za(za_target) + projectile = _AtomicRepresentation.from_za(za_projectile) + emitted = _AtomicRepresentation.from_za(za_emitted) + target = _AtomicRepresentation.from_za(za_target) compound = projectile + target residual = compound - emitted diff --git a/tests/unit_tests/test_data_kalbach_mann.py b/tests/unit_tests/test_data_kalbach_mann.py index bd540addb..2a579b703 100644 --- a/tests/unit_tests/test_data_kalbach_mann.py +++ b/tests/unit_tests/test_data_kalbach_mann.py @@ -6,8 +6,7 @@ import pytest import numpy as np from openmc.data import IncidentNeutron -from openmc.data import AtomicRepresentation -from openmc.data.kalbach_mann import _separation_energy +from openmc.data.kalbach_mann import _separation_energy, _AtomicRepresentation from openmc.data import kalbach_slope from openmc.data import KalbachMann @@ -17,43 +16,43 @@ from . import needs_njoy @pytest.fixture(scope='module') def neutron(): """Neutron AtomicRepresentation.""" - return AtomicRepresentation(z=0, a=1) + return _AtomicRepresentation(z=0, a=1) @pytest.fixture(scope='module') def triton(): """Triton AtomicRepresentation.""" - return AtomicRepresentation(z=1, a=3) + return _AtomicRepresentation(z=1, a=3) @pytest.fixture(scope='module') def b10(): """B10 AtomicRepresentation.""" - return AtomicRepresentation(z=5, a=10) + return _AtomicRepresentation(z=5, a=10) @pytest.fixture(scope='module') def c12(): """C12 AtomicRepresentation.""" - return AtomicRepresentation(z=6, a=12) + return _AtomicRepresentation(z=6, a=12) @pytest.fixture(scope='module') def c13(): """C13 AtomicRepresentation.""" - return AtomicRepresentation(z=6, a=13) + return _AtomicRepresentation(z=6, a=13) @pytest.fixture(scope='module') def na23(): """Na23 AtomicRepresentation.""" - return AtomicRepresentation(z=11, a=23) + return _AtomicRepresentation(z=11, a=23) def test_atomic_representation(neutron, triton, b10, c12, c13, na23): """Test the AtomicRepresentation class.""" # Test instantiation from_za - assert b10 == AtomicRepresentation.from_za(5010) + assert b10 == _AtomicRepresentation.from_za(5010) # Test addition assert c13 + b10 == na23 @@ -76,13 +75,13 @@ def test_atomic_representation(neutron, triton, b10, c12, c13, na23): # Test instanciation errors with pytest.raises(ValueError): - AtomicRepresentation(z=5, a=1) + _AtomicRepresentation(z=5, a=1) with pytest.raises(ValueError): - AtomicRepresentation(z=-1, a=1) + _AtomicRepresentation(z=-1, a=1) with pytest.raises(ValueError): - AtomicRepresentation(z=5, a=0) + _AtomicRepresentation(z=5, a=0) with pytest.raises(ValueError): - AtomicRepresentation(z=5, a=-2) + _AtomicRepresentation(z=5, a=-2) with pytest.raises(ValueError): neutron - triton From 7ca7b1c7cffd2c7469fc2c869857f6c14bcc133a Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 26 Apr 2022 16:53:17 -0500 Subject: [PATCH 008/131] Fix some docstrings --- openmc/data/kalbach_mann.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/openmc/data/kalbach_mann.py b/openmc/data/kalbach_mann.py index 10c002234..02403b3cc 100644 --- a/openmc/data/kalbach_mann.py +++ b/openmc/data/kalbach_mann.py @@ -25,7 +25,7 @@ class _AtomicRepresentation(EqualityMixin): Raises ------ - IOError + ValueError When the number of protons (z) declared is higher than the number of nucleons (a) @@ -56,17 +56,13 @@ class _AtomicRepresentation(EqualityMixin): self._a = a def __add__(self, other): - """Adds two _AtomicRepresentations. - - """ + """Add two _AtomicRepresentations""" z = self.z + other.z a = self.a + other.a return _AtomicRepresentation(z=z, a=a) def __sub__(self, other): - """Substracts two _AtomicRepresentations. - - """ + """Substract two _AtomicRepresentations""" z = self.z - other.z a = self.a - other.a return _AtomicRepresentation(z=z, a=a) From cdc1bd02179f9df7379225cf80af801ed6d5c679 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 26 Apr 2022 17:00:11 -0500 Subject: [PATCH 009/131] Changes in njoy.py --- openmc/data/njoy.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/openmc/data/njoy.py b/openmc/data/njoy.py index fbb26be05..70351a772 100644 --- a/openmc/data/njoy.py +++ b/openmc/data/njoy.py @@ -300,7 +300,7 @@ def make_ace(filename, temperatures=None, acer=True, xsdir=None, If the ENDF file contains multiple material evaluations, this argument indicates which evaluation should be used. smoothing : bool, optional - If the smoothing option in ACER is on (1) or off (0) in the card 6. + If the smoothing option (ACER card 6) is on (True) or off (False). **kwargs Keyword arguments passed to :func:`openmc.data.njoy.run` @@ -383,10 +383,7 @@ def make_ace(filename, temperatures=None, acer=True, xsdir=None, # acer if acer: - if smoothing is True: - ismoothing = 1 - else: - ismoothing = 0 + ismoothing = int(smoothing) nacer_in = nlast for i, temperature in enumerate(temperatures): # Extend input with an ACER run for each temperature From f4d0440d4f72e911ee59b82665de88336fef07f0 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 26 Apr 2022 17:07:25 -0500 Subject: [PATCH 010/131] Remove KM slope NJOY test, other small changes --- tests/unit_tests/test_data_kalbach_mann.py | 80 ++++------------------ 1 file changed, 13 insertions(+), 67 deletions(-) diff --git a/tests/unit_tests/test_data_kalbach_mann.py b/tests/unit_tests/test_data_kalbach_mann.py index 2a579b703..931e236bd 100644 --- a/tests/unit_tests/test_data_kalbach_mann.py +++ b/tests/unit_tests/test_data_kalbach_mann.py @@ -2,7 +2,9 @@ retrieved from ENDF files.""" import os +from pathlib import Path import pytest + import numpy as np from openmc.data import IncidentNeutron @@ -50,7 +52,7 @@ def na23(): def test_atomic_representation(neutron, triton, b10, c12, c13, na23): - """Test the AtomicRepresentation class.""" + """Test the _AtomicRepresentation class.""" # Test instantiation from_za assert b10 == _AtomicRepresentation.from_za(5010) @@ -121,10 +123,10 @@ def test_kalbach_slope(): @pytest.mark.parametrize( - "hdf5_filename, endf_type, endf_filename", [ - ('O16.h5', 'neutrons', 'n-008_O_016.endf'), - ('Ca46.h5', 'neutrons', 'n-020_Ca_046.endf'), - ('Hg204.h5', 'neutrons', 'n-080_Hg_204.endf') + "hdf5_filename, endf_filename", [ + ('O16.h5', 'n-008_O_016.endf'), + ('Ca46.h5', 'n-020_Ca_046.endf'), + ('Hg204.h5', 'n-080_Hg_204.endf') ] ) def test_comparison_slope_hdf5(hdf5_filename, endf_type, endf_filename): @@ -132,7 +134,7 @@ def test_comparison_slope_hdf5(hdf5_filename, endf_type, endf_filename): by comparing it to HDF5 data. The test is based on the first product of MT=5 (neutron). The isotopes tested have been selected because the corresponding products in ENDF/B-VII.1 are described using MF=6, LAW=1, - LANG=2 (ie. Kalbach-Mann systematics) and the slope is not given + LANG=2 (i.e., Kalbach-Mann systematics) and the slope is not given explicitly. If an error occurs during the "validity check", this means that @@ -145,15 +147,14 @@ def test_comparison_slope_hdf5(hdf5_filename, endf_type, endf_filename): """ # HDF5 data - hdf5_directory = os.path.dirname(os.environ['OPENMC_CROSS_SECTIONS']) - hdf5_path = os.path.join(hdf5_directory, hdf5_filename) - hdf5_data = IncidentNeutron.from_hdf5(hdf5_path) + hdf5_directory = Path(os.environ['OPENMC_CROSS_SECTIONS']).parent + hdf5_data = IncidentNeutron.from_hdf5(hdf5_directory / hdf5_filename) hdf5_product = hdf5_data[5].products[0] hdf5_distribution = hdf5_product.distribution[0] # ENDF data - endf_directory = os.environ['OPENMC_ENDF_DATA'] - endf_path = os.path.join(endf_directory, endf_type, endf_filename) + endf_directory = Path(os.environ['OPENMC_ENDF_DATA']) + endf_path = endf_directory / 'neutrons' / endf_filename endf_data = IncidentNeutron.from_endf(endf_path) endf_product = endf_data[5].products[0] endf_distribution = endf_product.distribution[0] @@ -166,65 +167,10 @@ def test_comparison_slope_hdf5(hdf5_filename, endf_type, endf_filename): # Results check for i, hdf5_slope in enumerate(hdf5_distribution.slope): - - assert endf_distribution._calculated_slope[i] is True + assert endf_distribution._calculated_slope[i] np.testing.assert_array_almost_equal( endf_distribution.slope[i].y, hdf5_slope.y, decimal=5 ) - - -@needs_njoy -@pytest.mark.parametrize( - "endf_type, endf_filename", [ - ('neutrons', 'n-008_O_016.endf'), - ('neutrons', 'n-020_Ca_046.endf'), - ('neutrons', 'n-080_Hg_204.endf') - ] -) -def test_comparison_slope_njoy(endf_type, endf_filename): - """Test the calculation of the Kalbach-Mann slope done by OpenMC - by comparing it to an NJOY calculation. The test is based on - the first product of MT=5 (neutron). The isotopes tested have - been selected because the corresponding products in ENDF/B-VII.1 - are described using MF=6, LAW=1, LANG=2 (ie. Kalbach-Mann - systematics) and the slope is not given explicitly. - - If an error occurs during the "validity check", this means that - the nuclear data evaluation has evolved and the distribution might - no longer be described using Kalbach-Mann systematics. Another - isotope needs to be identified and tested. - - """ - endf_directory = os.environ['OPENMC_ENDF_DATA'] - endf_path = os.path.join(endf_directory, endf_type, endf_filename) - - # ENDF data - endf_data = IncidentNeutron.from_endf(endf_path) - endf_product = endf_data[5].products[0] - endf_distribution = endf_product.distribution[0] - - # NJOY data - njoy_data = IncidentNeutron.from_njoy(endf_path, heatr=False, gaspr=False, - purr=False, smoothing=False) - njoy_product = njoy_data[5].products[0] - njoy_distribution = njoy_product.distribution[0] - - # Validity check - assert isinstance(endf_distribution, KalbachMann) - assert isinstance(njoy_distribution, KalbachMann) - assert endf_product.particle == njoy_product.particle - assert len(endf_distribution.slope) == len(njoy_distribution.slope) - - # Results check - for i, njoy_slope in enumerate(njoy_distribution.slope): - - assert endf_distribution._calculated_slope[i] is True - - np.testing.assert_array_almost_equal( - endf_distribution.slope[i].y, - njoy_slope.y, - decimal=5 - ) From e76bd84368d09c29252bedfbb22f631e60646b2b Mon Sep 17 00:00:00 2001 From: yardasol Date: Tue, 21 Jun 2022 18:16:34 -0500 Subject: [PATCH 011/131] setup and initial structure for FluxSpectraOperator class --- openmc/deplete/flux_operator.py | 281 ++++++++++++++++++++++++++++++++ 1 file changed, 281 insertions(+) create mode 100644 openmc/deplete/flux_operator.py diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/flux_operator.py new file mode 100644 index 000000000..17b43dcfd --- /dev/null +++ b/openmc/deplete/flux_operator.py @@ -0,0 +1,281 @@ +"""Pure depletion operator + +This module implements a pure depletion operator that uses user provided fluxes +and one-group cross sections. + +""" + + +import copy +from collections import OrderedDict +import os +from warnings import warn + +import numpy as np +from uncertainties import ufloat + +import openmc +from openmc.checkvalue import check_value +from openmc.data import DataLibrary +from openmc.exceptions import DataError +from openmc.mpi import comm +from .operator import Operator, _distribute, _find_cross_sections +from .abc import TransportOperator, OperatorResult +from .atom_number import AtomNumber +from .chain import _find_chain_file +from .reaction_rates import ReactionRates +from .results import Results + + + +class FluxSpectraDepletionOperator(TransportOperator, Operator): + """Depletion operator that uses a user provided flux spectrum to + calculate reaction rates. The flux provided must match the type of cross + section library in use (contunuous flux for continuous energy library, + multi-group flux for multi-group library) + + Instances of this class can be used to perform depletion using one group + cross sections and constant flux. Normally, a user needn't call methods of + this class directly. Instead, an instance of this class is passed to an + integrator class, such as :class:`openmc.deplete.CECMIntegrator` + + Parameters + ---------- + model : openmc.model.Model + OpenMC Model object. Must contain geometry and materials information. + flux_spectra : ??? + Unnormalized flux spectrum. + chain_file : str + Path to the depletion chain XML file. + fission_q : dict, optional + Dictionary of nuclides and their fission Q values [eV]. If not given, + values will be pulled from the ``chain_file``. + dilute_initial : float, optional + Initial atom density [atoms/cm^3] to add for nuclides that are zero + in initial condition to ensure they exist in the decay chain. + Only done for nuclides with reaction rates. + Defaults to 1.0e3. + prev_results : Results, optional + Results from a previous depletion calculation. + reduce_chain : bool, optional + If True, use :meth:`openmc.deplete.Chain.reduce` to reduce the + depletion chain up to ``reduce_chain_level``. Default is False. + reduce_chain_level : int, optional + Depth of the search when reducing the depletion chain. Only used + if ``reduce_chain`` evaluates to true. The default value of + ``None`` implies no limit on the depth. + + + Attributes + ---------- + dilute_initial : float + Initial atom density [atoms/cm^3] to add for nuclides that are zero + in initial condition to ensure they exist in the decay chain. + Only done for nuclides with reaction rates. + prev_res : Results or None + Results from a previous depletion calculation. ``None`` if no + results are to be used. + """ + + def __init__(self, model, flux_spectra, chain_file, fission_q=None, dilute_initial=1.0e3, + prev_results=None, reduce_chain=False, reduce_chain_level=None): + + # Determine cross sections / depletion chain + # TODO : add support for mg cross sections to _find_cross_sections + cross_sections = _find_cross_sections(model) + if chain_file is None: + chain_file = _find_chain_file(cross_sections) + + TransportOperator.__init__(chain_file, fission_q, dilute_initial, prev_results) + self.round_number = False + self.geometry = model.geometry + + # determine set of materials in the model + if not model.materials: + model.materials = openmc.Materials( + model.geometry.get_all_materials().values() + ) + self.materials = model.materials + self.flux_spectra = flux_spectra + + # Reduce the chain before we create more materials + if reduce_chain: + all_isotopes = set() + for material in self.materials: + if not material.depletable: + continue + for name, _dens_percent, _dens_type in material.nuclides: + all_isotopes.add(name) + self.chain = self.chain.reduce(all_isotopes, reduce_chain_level) + + # Clear out OpenMC, create task lists, distribute + self.burnable_mats, volume, nuclides = Operator._get_burnable_mats() + self.local_mats = _distribute(self.burnable_mats) + + # Generate map from local materials => material index + self._mat_index_map = { + lm: self.burnable_mats.index(lm) for lm in self.local_mats} + + # TODO: add support for loading previous results + + # Determine which nuclides have incident neutron data + # TODO : add support for mg cross sections to Operator._get_nuclides_with_data + self.nuclides_with_data = Operator._get_nuclides_with_data(mg_cross_sections) + + # Select nuclides with data that are also in the chain + self._burnable_nucs = [nuc.name for nuc in self.chain.nuclides + if nuc.name in self.nuclides_with_data] + + # Extract number densities from the geometry / previous depletion run + Operator._extract_number(self.local_mats, volume, nuclides, self.prev_res) + + # Create reaction rates array + self.reaction_rates = ReactionRates( + self.local_mats, self._burnable_nucs, self.chain.reactions) + + # TODO : set up rate, yield, and normalization helper objects + + + def __call__(self, vec, source_rate): + """Runs a simulation. + + Parameters + ---------- + vec : list of numpy.ndarray + Total atoms to be used in function. + source_rate : float + Power in [W] or source rate in [neutron/sec] + + Returns + ------- + openmc.deplete.OperatorResult + Eigenvalue and reaction rates resulting from transport operator + + """ + + + + # Update the number densities regardless of the source rate + self.number.set_density(vec) + self._update_materials() + + # Update tally nuclides data in preparation for transport solve + nuclides = Operator._get_tally_nuclides() + self._rate_helper.nuclides = nuclides + self._normalization_helper.nuclides = nuclides + self._yield_helper.update_tally_nuclides(nuclides) + + + + rates = self.reaction_rates + rates.fill(0.0) + + # Get k and uncertainty + # TODO : add functionality to get this from the transport solver + + # Form fast map + nuc_ind = [rates.index_nuc[nuc] for nuc in nuclides] + react_ind = [rates.index_rx[react] for react in self.chain.reactions] + + # Create arrays to store fission Q values, reaction rates, and nuclide + # numbers, zeroed out in material iteration + number = np.empty(rates.n_nuc) + + # Extract results + for i, mat in enumerate(self.local_mats): + # Get tally index + mat_index = self._mat_index_map[mat] + + # TODO : add machinery to multiply + # each cross section by the corresponding flux + + ... + + + # TODO: add flow control to determine if we need to scale + # the reaction rates + # Scale reaction rates to obtain units of reactions/sec + rates *= self._normalization_helper.factor(source_rate) + + # Store new fission yields on the chain + self.chain.fission_yields = fission_yields + + return OperatorResult(keff, rates) + + + def initial_condition(self): + """Performs final setup and returns initial condition. + + Returns + ------- + list of numpy.ndarray + Total density for initial conditions. + """ + + # TODO : implement these functions so they can store the + # cross section data + materials = [openmc.lib.materials[int(i)] + for i in self.burnable_mats] + self._rate_helper.generate_tallies(materials, self.chain.reactions) + self._normalization_helper.prepare( + self.chain.nuclides, self.reaction_rates.index_nuc) + # Tell fission yield helper what materials this process is + # responsible for + self._yield_helper.generate_tallies( + materials, tuple(sorted(self._mat_index_map.values()))) + + + # Return number density vector + return list(self.number.get_mat_slice(np.s_[:])) + + + + def write_bos_data(self, step): + """Document beginning of step data for a given step + + Called at the beginning of a depletion step and at + the final point in the simulation. + + Parameters + ---------- + step : int + Current depletion step including restarts + """ + ... + + + def _update_materials(self): + """Updates material compositions in OpenMC on all processes.""" + + for rank in range(comm.size): + number_i = comm.bcast(self.number, root=rank) + + for mat in number_i.materials: + nuclides = [] + densities = [] + for nuc in number_i.nuclides: + if nuc in self.nuclides_with_data: + val = 1.0e-24 * number_i.get_atom_density(mat, nuc) + + # If nuclide is zero, do not add to the problem. + if val > 0.0: + if self.round_number: + val_magnitude = np.floor(np.log10(val)) + val_scaled = val / 10**val_magnitude + val_round = round(val_scaled, 8) + + val = val_round * 10**val_magnitude + + nuclides.append(nuc) + densities.append(val) + else: + # Only output warnings if values are significantly + # negative. CRAM does not guarantee positive values. + if val < -1.0e-21: + print("WARNING: nuclide ", nuc, " in material ", mat, + " is negative (density = ", val, " at/barn-cm)") + number_i[mat, nuc] = 0.0 + + + #TODO Update densities on the Python side, otherwise the + # summary.h5 file contains densities at the first time step From 491798b24dc13dc839ef7e421c453aeff450929b Mon Sep 17 00:00:00 2001 From: yardasol Date: Mon, 27 Jun 2022 09:59:11 -0500 Subject: [PATCH 012/131] remove model parameter, replace with nuclides parameter We will be passing a data structure that already contains nuclide and cross section information to this class, so we will not need all the capabilities of an openmc Model. --- openmc/deplete/flux_operator.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/flux_operator.py index 17b43dcfd..dbc7eaa2a 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/flux_operator.py @@ -41,10 +41,10 @@ class FluxSpectraDepletionOperator(TransportOperator, Operator): Parameters ---------- - model : openmc.model.Model - OpenMC Model object. Must contain geometry and materials information. + nuclides : pandas.DataFrame + DataFrame contaning nuclide concentration as well as cross section data. flux_spectra : ??? - Unnormalized flux spectrum. + Flux spectrum chain_file : str Path to the depletion chain XML file. fission_q : dict, optional @@ -77,25 +77,17 @@ class FluxSpectraDepletionOperator(TransportOperator, Operator): results are to be used. """ - def __init__(self, model, flux_spectra, chain_file, fission_q=None, dilute_initial=1.0e3, + def __init__(self, nuclides, flux_spectra, chain_file, fission_q=None, dilute_initial=1.0e3, prev_results=None, reduce_chain=False, reduce_chain_level=None): # Determine cross sections / depletion chain # TODO : add support for mg cross sections to _find_cross_sections - cross_sections = _find_cross_sections(model) if chain_file is None: chain_file = _find_chain_file(cross_sections) TransportOperator.__init__(chain_file, fission_q, dilute_initial, prev_results) self.round_number = False - self.geometry = model.geometry - # determine set of materials in the model - if not model.materials: - model.materials = openmc.Materials( - model.geometry.get_all_materials().values() - ) - self.materials = model.materials self.flux_spectra = flux_spectra # Reduce the chain before we create more materials From 498deeea28d1d927599cb860f8717c7aba5cf76c Mon Sep 17 00:00:00 2001 From: yardasol Date: Mon, 27 Jun 2022 10:11:50 -0500 Subject: [PATCH 013/131] modify block reducing depletion chain This change modifies the block to handle information stored in the nuclides parameter --- openmc/deplete/flux_operator.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/flux_operator.py index dbc7eaa2a..d8688aa60 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/flux_operator.py @@ -80,6 +80,9 @@ class FluxSpectraDepletionOperator(TransportOperator, Operator): def __init__(self, nuclides, flux_spectra, chain_file, fission_q=None, dilute_initial=1.0e3, prev_results=None, reduce_chain=False, reduce_chain_level=None): + + # TODO : validate nuclides parameter + # Determine cross sections / depletion chain # TODO : add support for mg cross sections to _find_cross_sections if chain_file is None: @@ -90,15 +93,12 @@ class FluxSpectraDepletionOperator(TransportOperator, Operator): self.flux_spectra = flux_spectra - # Reduce the chain before we create more materials + # Reduce the chain if reduce_chain: - all_isotopes = set() - for material in self.materials: - if not material.depletable: - continue - for name, _dens_percent, _dens_type in material.nuclides: - all_isotopes.add(name) - self.chain = self.chain.reduce(all_isotopes, reduce_chain_level) + all_nuclides = set() + for nuclide_name in nuclides.index: + all_isotopes.add(nuclide_name) + self.chain = self.chain.reduce(all_nuclides, reduce_chain_level) # Clear out OpenMC, create task lists, distribute self.burnable_mats, volume, nuclides = Operator._get_burnable_mats() From 200ea764aced25a48f6e47b241236eb9744a2231 Mon Sep 17 00:00:00 2001 From: yardasol Date: Mon, 27 Jun 2022 10:40:43 -0500 Subject: [PATCH 014/131] make FluxSpectraDepletionOperator not a subclass of Operator --- openmc/deplete/flux_operator.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/flux_operator.py index d8688aa60..8981a5d01 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/flux_operator.py @@ -19,7 +19,6 @@ from openmc.checkvalue import check_value from openmc.data import DataLibrary from openmc.exceptions import DataError from openmc.mpi import comm -from .operator import Operator, _distribute, _find_cross_sections from .abc import TransportOperator, OperatorResult from .atom_number import AtomNumber from .chain import _find_chain_file @@ -28,7 +27,7 @@ from .results import Results -class FluxSpectraDepletionOperator(TransportOperator, Operator): +class FluxSpectraDepletionOperator(TransportOperator): """Depletion operator that uses a user provided flux spectrum to calculate reaction rates. The flux provided must match the type of cross section library in use (contunuous flux for continuous energy library, @@ -101,7 +100,7 @@ class FluxSpectraDepletionOperator(TransportOperator, Operator): self.chain = self.chain.reduce(all_nuclides, reduce_chain_level) # Clear out OpenMC, create task lists, distribute - self.burnable_mats, volume, nuclides = Operator._get_burnable_mats() + self.burnable_mats, volume, nuclides = self.get_burnable_mats() self.local_mats = _distribute(self.burnable_mats) # Generate map from local materials => material index @@ -110,16 +109,15 @@ class FluxSpectraDepletionOperator(TransportOperator, Operator): # TODO: add support for loading previous results - # Determine which nuclides have incident neutron data - # TODO : add support for mg cross sections to Operator._get_nuclides_with_data - self.nuclides_with_data = Operator._get_nuclides_with_data(mg_cross_sections) + # TODO : Determine which nuclides have incident neutron data + self.nuclides_with_data = self._get_nuclides_with_data() # Select nuclides with data that are also in the chain self._burnable_nucs = [nuc.name for nuc in self.chain.nuclides if nuc.name in self.nuclides_with_data] # Extract number densities from the geometry / previous depletion run - Operator._extract_number(self.local_mats, volume, nuclides, self.prev_res) + self._extract_number(self.local_mats, volume, nuclides, self.prev_res) # Create reaction rates array self.reaction_rates = ReactionRates( @@ -152,7 +150,7 @@ class FluxSpectraDepletionOperator(TransportOperator, Operator): self._update_materials() # Update tally nuclides data in preparation for transport solve - nuclides = Operator._get_tally_nuclides() + nuclides = self.._get_tally_nuclides() self._rate_helper.nuclides = nuclides self._normalization_helper.nuclides = nuclides self._yield_helper.update_tally_nuclides(nuclides) From fa6fd9384e07385bced4ca1b8bfa498600ebb496 Mon Sep 17 00:00:00 2001 From: yardasol Date: Mon, 27 Jun 2022 10:45:50 -0500 Subject: [PATCH 015/131] make nuclude concentrations and nuclide cross section data parameters separate --- openmc/deplete/flux_operator.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/flux_operator.py index 8981a5d01..78ef8b8c4 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/flux_operator.py @@ -40,8 +40,11 @@ class FluxSpectraDepletionOperator(TransportOperator): Parameters ---------- - nuclides : pandas.DataFrame - DataFrame contaning nuclide concentration as well as cross section data. + nuclides : dict of str to float + Dictionary with nuclides names as keys and concentration as values. + micro_xs : pandas.DataFrame + DataFrame with nuclides names as index and microscopic cross section + data in the columns. flux_spectra : ??? Flux spectrum chain_file : str @@ -76,16 +79,11 @@ class FluxSpectraDepletionOperator(TransportOperator): results are to be used. """ - def __init__(self, nuclides, flux_spectra, chain_file, fission_q=None, dilute_initial=1.0e3, + def __init__(self, nuclides, micro_xs, flux_spectra, chain_file, fission_q=None, dilute_initial=1.0e3, prev_results=None, reduce_chain=False, reduce_chain_level=None): - # TODO : validate nuclides parameter - - # Determine cross sections / depletion chain - # TODO : add support for mg cross sections to _find_cross_sections - if chain_file is None: - chain_file = _find_chain_file(cross_sections) + # TODO : validate nuclides and micro-xs parameters TransportOperator.__init__(chain_file, fission_q, dilute_initial, prev_results) self.round_number = False @@ -95,7 +93,7 @@ class FluxSpectraDepletionOperator(TransportOperator): # Reduce the chain if reduce_chain: all_nuclides = set() - for nuclide_name in nuclides.index: + for nuclide_name in nuclides.keys(): all_isotopes.add(nuclide_name) self.chain = self.chain.reduce(all_nuclides, reduce_chain_level) From 194c846bd72c6a722635104d9401ef1277161168 Mon Sep 17 00:00:00 2001 From: yardasol Date: Mon, 27 Jun 2022 11:10:06 -0500 Subject: [PATCH 016/131] remove local_mats attribute --- openmc/deplete/flux_operator.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/flux_operator.py index 78ef8b8c4..0e74eb824 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/flux_operator.py @@ -98,12 +98,7 @@ class FluxSpectraDepletionOperator(TransportOperator): self.chain = self.chain.reduce(all_nuclides, reduce_chain_level) # Clear out OpenMC, create task lists, distribute - self.burnable_mats, volume, nuclides = self.get_burnable_mats() - self.local_mats = _distribute(self.burnable_mats) - - # Generate map from local materials => material index - self._mat_index_map = { - lm: self.burnable_mats.index(lm) for lm in self.local_mats} + #self.burnable_mats, volume, nuclides = self.get_burnable_mats() # TODO: add support for loading previous results @@ -115,11 +110,11 @@ class FluxSpectraDepletionOperator(TransportOperator): if nuc.name in self.nuclides_with_data] # Extract number densities from the geometry / previous depletion run - self._extract_number(self.local_mats, volume, nuclides, self.prev_res) + self._extract_number('0', volume, nuclides, self.prev_res) # Create reaction rates array self.reaction_rates = ReactionRates( - self.local_mats, self._burnable_nucs, self.chain.reactions) + '0', self._burnable_nucs, self.chain.reactions) # TODO : set up rate, yield, and normalization helper objects @@ -170,7 +165,7 @@ class FluxSpectraDepletionOperator(TransportOperator): number = np.empty(rates.n_nuc) # Extract results - for i, mat in enumerate(self.local_mats): + for i, mat in enumerate(...): # Get tally index mat_index = self._mat_index_map[mat] From 9096b1e0f020c6cda8b20a9ae972f159f6e77105 Mon Sep 17 00:00:00 2001 From: yardasol Date: Mon, 27 Jun 2022 11:22:36 -0500 Subject: [PATCH 017/131] remove looping over materials in the _call_ method --- openmc/deplete/flux_operator.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/flux_operator.py index 0e74eb824..0dd87a32a 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/flux_operator.py @@ -110,7 +110,7 @@ class FluxSpectraDepletionOperator(TransportOperator): if nuc.name in self.nuclides_with_data] # Extract number densities from the geometry / previous depletion run - self._extract_number('0', volume, nuclides, self.prev_res) + self._extract_number('0', volume, list(nuclides.keys()), self.prev_res) # Create reaction rates array self.reaction_rates = ReactionRates( @@ -164,10 +164,15 @@ class FluxSpectraDepletionOperator(TransportOperator): # numbers, zeroed out in material iteration number = np.empty(rates.n_nuc) - # Extract results - for i, mat in enumerate(...): - # Get tally index - mat_index = self._mat_index_map[mat] + # Zero out reaction rates and nuclide numbers + number.fill(0.0) + + # Get new number densities + for nuc, i_nuc_results in zip(nuclides, nuc_ind): + number[i_nuc_results] = self.number['0', nuc] + + + # TODO : add machinery to multiply # each cross section by the corresponding flux From 1e631991303c74202b945f328f2a93339ab30eef Mon Sep 17 00:00:00 2001 From: yardasol Date: Mon, 27 Jun 2022 11:31:18 -0500 Subject: [PATCH 018/131] add ConstantFissionYieldHelper --- openmc/deplete/flux_operator.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/flux_operator.py index 0dd87a32a..593a1ccff 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/flux_operator.py @@ -24,6 +24,9 @@ from .atom_number import AtomNumber from .chain import _find_chain_file from .reaction_rates import ReactionRates from .results import Results +from .helpers import ( + DirectReactionRateHelper, ChainFissionHelper, ConstantFissionYieldHelper) + @@ -116,7 +119,12 @@ class FluxSpectraDepletionOperator(TransportOperator): self.reaction_rates = ReactionRates( '0', self._burnable_nucs, self.chain.reactions) - # TODO : set up rate, yield, and normalization helper objects + # Select and create fission yield helper + fission_helper = ConstantFissionYieldHelper + fission_yield_opts = ( + {} if fission_yield_opts is None else fission_yield_opts) + self._yield_helper = fission_helper.from_operator( + self, **fission_yield_opts) def __call__(self, vec, source_rate): @@ -143,13 +151,12 @@ class FluxSpectraDepletionOperator(TransportOperator): self._update_materials() # Update tally nuclides data in preparation for transport solve - nuclides = self.._get_tally_nuclides() + nuclides = self._get_tally_nuclides() self._rate_helper.nuclides = nuclides self._normalization_helper.nuclides = nuclides self._yield_helper.update_tally_nuclides(nuclides) - rates = self.reaction_rates rates.fill(0.0) @@ -165,13 +172,14 @@ class FluxSpectraDepletionOperator(TransportOperator): number = np.empty(rates.n_nuc) # Zero out reaction rates and nuclide numbers - number.fill(0.0) + number.fill(0.0) # Get new number densities for nuc, i_nuc_results in zip(nuclides, nuc_ind): number[i_nuc_results] = self.number['0', nuc] - + # Compute fission yields for this material + fission_yields.append(self._yield_helper.weighted_yields(i)) # TODO : add machinery to multiply From b9f75d961b534eb463a0f99655a1e6ad2889287f Mon Sep 17 00:00:00 2001 From: yardasol Date: Mon, 27 Jun 2022 11:48:50 -0500 Subject: [PATCH 019/131] clean up __call__ method --- openmc/deplete/flux_operator.py | 91 +++++++++++++++++++++++++-------- 1 file changed, 69 insertions(+), 22 deletions(-) diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/flux_operator.py index 593a1ccff..f7a29b5b8 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/flux_operator.py @@ -25,7 +25,8 @@ from .chain import _find_chain_file from .reaction_rates import ReactionRates from .results import Results from .helpers import ( - DirectReactionRateHelper, ChainFissionHelper, ConstantFissionYieldHelper) + FluxReactionRateHelper, ChainFissionHelper, SourceRateHelper, + ConstantFissionYieldHelper) @@ -150,27 +151,35 @@ class FluxSpectraDepletionOperator(TransportOperator): self.number.set_density(vec) self._update_materials() - # Update tally nuclides data in preparation for transport solve - nuclides = self._get_tally_nuclides() + # Update nuclides data in preparation for transport solve + nuclides = self._get_reaction_nuclides() self._rate_helper.nuclides = nuclides - self._normalization_helper.nuclides = nuclides self._yield_helper.update_tally_nuclides(nuclides) - rates = self.reaction_rates rates.fill(0.0) # Get k and uncertainty # TODO : add functionality to get this from the transport solver + keff = ... # Form fast map nuc_ind = [rates.index_nuc[nuc] for nuc in nuclides] react_ind = [rates.index_rx[react] for react in self.chain.reactions] + # Keep track of energy produced from all reactions in eV per source + # particle + self._normalization_helper.reset() + + # Store fission yield dictionaries + fission_yields = [] + # Create arrays to store fission Q values, reaction rates, and nuclide # numbers, zeroed out in material iteration number = np.empty(rates.n_nuc) + fission_ind = rates.index_rx.get("fission") + # Zero out reaction rates and nuclide numbers number.fill(0.0) @@ -178,18 +187,20 @@ class FluxSpectraDepletionOperator(TransportOperator): for nuc, i_nuc_results in zip(nuclides, nuc_ind): number[i_nuc_results] = self.number['0', nuc] + # TODO : implement rate helper for flux and xs inputs + reaction_rates = self._rate_helper.get_material_rates( + 0, nuc_ind, react_ind) + # Compute fission yields for this material - fission_yields.append(self._yield_helper.weighted_yields(i)) + fission_yields.append(self._yield_helper.weighted_yields(0)) + # Accumulate energy from fission + if fission_ind is not None: + self._normalization_helper.update(reaction_rates[:, fission_ind]) - # TODO : add machinery to multiply - # each cross section by the corresponding flux + # Divide by total number and store + rates[0] = self._rate_helper.divide_by_adens(number) - ... - - - # TODO: add flow control to determine if we need to scale - # the reaction rates # Scale reaction rates to obtain units of reactions/sec rates *= self._normalization_helper.factor(source_rate) @@ -210,16 +221,7 @@ class FluxSpectraDepletionOperator(TransportOperator): # TODO : implement these functions so they can store the # cross section data - materials = [openmc.lib.materials[int(i)] - for i in self.burnable_mats] self._rate_helper.generate_tallies(materials, self.chain.reactions) - self._normalization_helper.prepare( - self.chain.nuclides, self.reaction_rates.index_nuc) - # Tell fission yield helper what materials this process is - # responsible for - self._yield_helper.generate_tallies( - materials, tuple(sorted(self._mat_index_map.values()))) - # Return number density vector return list(self.number.get_mat_slice(np.s_[:])) @@ -275,3 +277,48 @@ class FluxSpectraDepletionOperator(TransportOperator): #TODO Update densities on the Python side, otherwise the # summary.h5 file contains densities at the first time step + + def _get_reaction_nuclides(self): + """Determine nuclides that should be tallied for reaction rates. + + This method returns a list of all nuclides that have neutron data and + are listed in the depletion chain. Technically, we should tally nuclides + that may not appear in the depletion chain because we still need to get + the fission reaction rate for these nuclides in order to normalize + power, but that is left as a future exercise. + + Returns + ------- + list of str + Tally nuclides + + """ + nuc_set = set() + + # Create the set of all nuclides in the decay chain in materials marked + # for burning in which the number density is greater than zero. + for nuc in self.number.nuclides: + if nuc in self.nuclides_with_data: + if np.sum(self.number[:, nuc]) > 0.0: + nuc_set.add(nuc) + + # Communicate which nuclides have nonzeros to rank 0 + if comm.rank == 0: + for i in range(1, comm.size): + nuc_newset = comm.recv(source=i, tag=i) + nuc_set |= nuc_newset + else: + comm.send(nuc_set, dest=0, tag=comm.rank) + + if comm.rank == 0: + # Sort nuclides in the same order as self.number + nuc_list = [nuc for nuc in self.number.nuclides + if nuc in nuc_set] + else: + nuc_list = None + + # Store list of tally nuclides on each process + nuc_list = comm.bcast(nuc_list) + return [nuc for nuc in nuc_list if nuc in self.chain] + + From 28a7b587aa9bb03b38ca22c40c5d8f965c9a3d4d Mon Sep 17 00:00:00 2001 From: yardasol Date: Mon, 27 Jun 2022 15:15:29 -0500 Subject: [PATCH 020/131] refine __call__; remove/disable _rate_helper attribute; add _normalization_helper, _yield_helper attributes --- openmc/deplete/flux_operator.py | 37 ++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/flux_operator.py index f7a29b5b8..02ef64cf0 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/flux_operator.py @@ -89,7 +89,7 @@ class FluxSpectraDepletionOperator(TransportOperator): # TODO : validate nuclides and micro-xs parameters - TransportOperator.__init__(chain_file, fission_q, dilute_initial, prev_results) + super.__init__(chain_file, fission_q, dilute_initial, prev_results) self.round_number = False self.flux_spectra = flux_spectra @@ -120,6 +120,12 @@ class FluxSpectraDepletionOperator(TransportOperator): self.reaction_rates = ReactionRates( '0', self._burnable_nucs, self.chain.reactions) + # Initialize normalization helper + if normalization_mode == "fission-q": + self._normalization_helper = ChainFissionHelper() + else: + self._normalization_helper = SourceRateHelper() + # Select and create fission yield helper fission_helper = ConstantFissionYieldHelper fission_yield_opts = ( @@ -145,15 +151,12 @@ class FluxSpectraDepletionOperator(TransportOperator): """ - - # Update the number densities regardless of the source rate self.number.set_density(vec) self._update_materials() # Update nuclides data in preparation for transport solve nuclides = self._get_reaction_nuclides() - self._rate_helper.nuclides = nuclides self._yield_helper.update_tally_nuclides(nuclides) rates = self.reaction_rates @@ -185,24 +188,31 @@ class FluxSpectraDepletionOperator(TransportOperator): # Get new number densities for nuc, i_nuc_results in zip(nuclides, nuc_ind): - number[i_nuc_results] = self.number['0', nuc] + number[i_nuc_results] = self.number[0, nuc] - # TODO : implement rate helper for flux and xs inputs - reaction_rates = self._rate_helper.get_material_rates( - 0, nuc_ind, react_ind) + # Store microscopic cross sections in rates array + for nuc in nuclides: + for rxn in self.chain.reactions: + rates.set('0', nuc, rxn, self.micro_xs[rxn].loc[nuc]) + + # Get reaction rate in reactions/sec + rates *= self.flux_spectra # Compute fission yields for this material fission_yields.append(self._yield_helper.weighted_yields(0)) # Accumulate energy from fission if fission_ind is not None: - self._normalization_helper.update(reaction_rates[:, fission_ind]) + self._normalization_helper.update(rxn_rates[:, fission_ind]) + ## These don't seem relevant so I'm commenting them out for now + ## will delete if they are indeed not useful # Divide by total number and store - rates[0] = self._rate_helper.divide_by_adens(number) + #rates[0] = self._rate_helper.divide_by_adens(number) # Scale reaction rates to obtain units of reactions/sec - rates *= self._normalization_helper.factor(source_rate) + #rates *= self._normalization_helper.factor(source_rate) + ## # Store new fission yields on the chain self.chain.fission_yields = fission_yields @@ -219,9 +229,8 @@ class FluxSpectraDepletionOperator(TransportOperator): Total density for initial conditions. """ - # TODO : implement these functions so they can store the - # cross section data - self._rate_helper.generate_tallies(materials, self.chain.reactions) + self._normalization_helper.prepare( + self.chain.nuclides, self.reaction_rates.index_nuc) # Return number density vector return list(self.number.get_mat_slice(np.s_[:])) From b85da509885338b7976859d2e8ada7ed2ea15263 Mon Sep 17 00:00:00 2001 From: yardasol Date: Mon, 27 Jun 2022 15:53:48 -0500 Subject: [PATCH 021/131] add basic type verification for nuclides, micro_xs parameters; comment out _normalization_helper attribute --- openmc/deplete/flux_operator.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/flux_operator.py index 02ef64cf0..e3bc6a5f3 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/flux_operator.py @@ -12,6 +12,7 @@ import os from warnings import warn import numpy as np +import pandas as pd from uncertainties import ufloat import openmc @@ -49,8 +50,8 @@ class FluxSpectraDepletionOperator(TransportOperator): micro_xs : pandas.DataFrame DataFrame with nuclides names as index and microscopic cross section data in the columns. - flux_spectra : ??? - Flux spectrum + flux_spectra : float + Flux spectrum [n cm^-2 s^-1] chain_file : str Path to the depletion chain XML file. fission_q : dict, optional @@ -85,13 +86,8 @@ class FluxSpectraDepletionOperator(TransportOperator): def __init__(self, nuclides, micro_xs, flux_spectra, chain_file, fission_q=None, dilute_initial=1.0e3, prev_results=None, reduce_chain=False, reduce_chain_level=None): - - - # TODO : validate nuclides and micro-xs parameters - super.__init__(chain_file, fission_q, dilute_initial, prev_results) self.round_number = False - self.flux_spectra = flux_spectra # Reduce the chain @@ -101,6 +97,10 @@ class FluxSpectraDepletionOperator(TransportOperator): all_isotopes.add(nuclide_name) self.chain = self.chain.reduce(all_nuclides, reduce_chain_level) + # Validate nuclides and micro-xs parameters + check_value('nuclides', nuclides, dict, str) + check_type('micro_xs', micro_xs, pd.DataFrame) + # Clear out OpenMC, create task lists, distribute #self.burnable_mats, volume, nuclides = self.get_burnable_mats() @@ -113,6 +113,7 @@ class FluxSpectraDepletionOperator(TransportOperator): self._burnable_nucs = [nuc.name for nuc in self.chain.nuclides if nuc.name in self.nuclides_with_data] + # TODO : implement _extract_number # Extract number densities from the geometry / previous depletion run self._extract_number('0', volume, list(nuclides.keys()), self.prev_res) @@ -121,10 +122,10 @@ class FluxSpectraDepletionOperator(TransportOperator): '0', self._burnable_nucs, self.chain.reactions) # Initialize normalization helper - if normalization_mode == "fission-q": - self._normalization_helper = ChainFissionHelper() - else: - self._normalization_helper = SourceRateHelper() + #if normalization_mode == "fission-q": + # self._normalization_helper = ChainFissionHelper() + #else: + # self._normalization_helper = SourceRateHelper() # Select and create fission yield helper fission_helper = ConstantFissionYieldHelper @@ -202,8 +203,8 @@ class FluxSpectraDepletionOperator(TransportOperator): fission_yields.append(self._yield_helper.weighted_yields(0)) # Accumulate energy from fission - if fission_ind is not None: - self._normalization_helper.update(rxn_rates[:, fission_ind]) + #if fission_ind is not None: + # self._normalization_helper.update(rxn_rates[:, fission_ind]) ## These don't seem relevant so I'm commenting them out for now ## will delete if they are indeed not useful @@ -229,8 +230,8 @@ class FluxSpectraDepletionOperator(TransportOperator): Total density for initial conditions. """ - self._normalization_helper.prepare( - self.chain.nuclides, self.reaction_rates.index_nuc) + #self._normalization_helper.prepare( + # self.chain.nuclides, self.reaction_rates.index_nuc) # Return number density vector return list(self.number.get_mat_slice(np.s_[:])) From d8902a603e7d66550c7ee4b3d0f782edf7594a12 Mon Sep 17 00:00:00 2001 From: yardasol Date: Mon, 27 Jun 2022 16:26:01 -0500 Subject: [PATCH 022/131] add volume parameter; add attributes to docstring; implement _get_all_depletion_nuclides, _get_nuclides_with_data --- openmc/deplete/flux_operator.py | 75 ++++++++++++++++++++++++++++++--- 1 file changed, 69 insertions(+), 6 deletions(-) diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/flux_operator.py index e3bc6a5f3..c12cfe084 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/flux_operator.py @@ -45,6 +45,8 @@ class FluxSpectraDepletionOperator(TransportOperator): Parameters ---------- + volume : float + Volume of the material being depleted in cm^3 nuclides : dict of str to float Dictionary with nuclides names as keys and concentration as values. micro_xs : pandas.DataFrame @@ -82,9 +84,22 @@ class FluxSpectraDepletionOperator(TransportOperator): prev_res : Results or None Results from a previous depletion calculation. ``None`` if no results are to be used. + number : openmc.deplete.AtomNumber + Total number of atoms in simulation. + nuclides_with_data : set of str + A set listing all unique nuclides available from cross_sections.xml. + chain : openmc.deplete.Chain + The depletion chain information necessary to form matrices and tallies. + reaction_rates : openmc.deplete.ReactionRates + Reaction rates from the last operator step. + heavy_metal : float + Initial heavy metal inventory [g] + prev_res : Results or None + Results from a previous depletion calculation. ``None`` if no + results are to be used. """ - def __init__(self, nuclides, micro_xs, flux_spectra, chain_file, fission_q=None, dilute_initial=1.0e3, + def __init__(self, volume, nuclides, micro_xs, flux_spectra, chain_file, fission_q=None, dilute_initial=1.0e3, prev_results=None, reduce_chain=False, reduce_chain_level=None): super.__init__(chain_file, fission_q, dilute_initial, prev_results) self.round_number = False @@ -101,12 +116,13 @@ class FluxSpectraDepletionOperator(TransportOperator): check_value('nuclides', nuclides, dict, str) check_type('micro_xs', micro_xs, pd.DataFrame) - # Clear out OpenMC, create task lists, distribute - #self.burnable_mats, volume, nuclides = self.get_burnable_mats() + self._micro_xs = micro_xs + + nuclides = self._get_all_depletion_nuclides(volume, nuclides) # TODO: add support for loading previous results - # TODO : Determine which nuclides have incident neutron data + # Determine which nuclides have cross section data self.nuclides_with_data = self._get_nuclides_with_data() # Select nuclides with data that are also in the chain @@ -115,7 +131,7 @@ class FluxSpectraDepletionOperator(TransportOperator): # TODO : implement _extract_number # Extract number densities from the geometry / previous depletion run - self._extract_number('0', volume, list(nuclides.keys()), self.prev_res) + self._extract_number('0', volume, nuclides, self.prev_res) # Create reaction rates array self.reaction_rates = ReactionRates( @@ -194,7 +210,7 @@ class FluxSpectraDepletionOperator(TransportOperator): # Store microscopic cross sections in rates array for nuc in nuclides: for rxn in self.chain.reactions: - rates.set('0', nuc, rxn, self.micro_xs[rxn].loc[nuc]) + rates.set('0', nuc, rxn, self._micro_xs[rxn].loc[nuc]) # Get reaction rate in reactions/sec rates *= self.flux_spectra @@ -331,4 +347,51 @@ class FluxSpectraDepletionOperator(TransportOperator): nuc_list = comm.bcast(nuc_list) return [nuc for nuc in nuc_list if nuc in self.chain] + def _get_all_depletion_nuclides(self, volume, nuclides): + """Determine nuclides that will show up in simulation + + Parameters + ---------- + volume : float + Volume of material in cm^3 + + Returns + ------- + nuclides : list of str + Nuclides in order of how they'll appear in the simulation. + + """ + + model_nuclides = set() + + self.heavy_metal = 0.0 + + bu_mat = openmc.Material() + bu_mat.volume = volume + for n, conc in nuclides: + bu_mat.add_nuclides(n,conc) + model_nuclides.add(n) + + self.heavy_metal = bu_mat.fissionable_mass + + # Sort the sets + model_nuclides = sorted(model_nuclides) + + # Construct a global nuclide dictionary, burned first + nuclides = list(self.chain.nuclide_dict) + for nuc in model_nuclides: + if nuc not in nuclides: + nuclides.append(nuc) + + return nuclides + + def _get_nuclides_with_data(self): + """Finds nuclides with cross section data""" + nuclides = set() + for name in self._micro_xs.index: + if name not in nuclides: + nuclides.add(name) + + return nuclides + From 13c9fdccf031c11abe809e6a67f993c120e762e1 Mon Sep 17 00:00:00 2001 From: yardasol Date: Mon, 27 Jun 2022 16:59:04 -0500 Subject: [PATCH 023/131] Add comments --- openmc/deplete/flux_operator.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/flux_operator.py index c12cfe084..d1be3f608 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/flux_operator.py @@ -170,9 +170,12 @@ class FluxSpectraDepletionOperator(TransportOperator): # Update the number densities regardless of the source rate self.number.set_density(vec) + ## TODO : make sure this function works w the current structure. self._update_materials() # Update nuclides data in preparation for transport solve + + ## TODO : make sure this function works w the current structure. nuclides = self._get_reaction_nuclides() self._yield_helper.update_tally_nuclides(nuclides) From a0f8b9937d85bae835f8a1e2d3a379d650e4585c Mon Sep 17 00:00:00 2001 From: yardasol Date: Tue, 28 Jun 2022 14:37:42 -0500 Subject: [PATCH 024/131] add keff handling; add comments from review with paul; add density to rxn rate calculation to get right units --- openmc/deplete/flux_operator.py | 98 ++++++++++++++++++++++++--------- 1 file changed, 73 insertions(+), 25 deletions(-) diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/flux_operator.py index d1be3f608..cd06894f6 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/flux_operator.py @@ -33,10 +33,8 @@ from .helpers import ( class FluxSpectraDepletionOperator(TransportOperator): - """Depletion operator that uses a user provided flux spectrum to - calculate reaction rates. The flux provided must match the type of cross - section library in use (contunuous flux for continuous energy library, - multi-group flux for multi-group library) + """Depletion operator that uses a user provided flux spectrum and one-group + cross sections calculate reaction rates. Instances of this class can be used to perform depletion using one group cross sections and constant flux. Normally, a user needn't call methods of @@ -49,6 +47,7 @@ class FluxSpectraDepletionOperator(TransportOperator): Volume of the material being depleted in cm^3 nuclides : dict of str to float Dictionary with nuclides names as keys and concentration as values. + Nuclide concentration is assumed to be in [units?] micro_xs : pandas.DataFrame DataFrame with nuclides names as index and microscopic cross section data in the columns. @@ -56,6 +55,9 @@ class FluxSpectraDepletionOperator(TransportOperator): Flux spectrum [n cm^-2 s^-1] chain_file : str Path to the depletion chain XML file. + keff : 2-tuple of float, optional + keff eigenvalue and uncertainty from transport calculation. + Defualt is None. fission_q : dict, optional Dictionary of nuclides and their fission Q values [eV]. If not given, values will be pulled from the ``chain_file``. @@ -99,7 +101,7 @@ class FluxSpectraDepletionOperator(TransportOperator): results are to be used. """ - def __init__(self, volume, nuclides, micro_xs, flux_spectra, chain_file, fission_q=None, dilute_initial=1.0e3, + def __init__(self, volume, nuclides, micro_xs, flux_spectra, chain_file, keff=None, fission_q=None, dilute_initial=1.0e3, prev_results=None, reduce_chain=False, reduce_chain_level=None): super.__init__(chain_file, fission_q, dilute_initial, prev_results) self.round_number = False @@ -117,6 +119,7 @@ class FluxSpectraDepletionOperator(TransportOperator): check_type('micro_xs', micro_xs, pd.DataFrame) self._micro_xs = micro_xs + self._keff = keff nuclides = self._get_all_depletion_nuclides(volume, nuclides) @@ -131,18 +134,13 @@ class FluxSpectraDepletionOperator(TransportOperator): # TODO : implement _extract_number # Extract number densities from the geometry / previous depletion run + #!!! consider removing this and just using the nuclides dict... self._extract_number('0', volume, nuclides, self.prev_res) # Create reaction rates array self.reaction_rates = ReactionRates( '0', self._burnable_nucs, self.chain.reactions) - # Initialize normalization helper - #if normalization_mode == "fission-q": - # self._normalization_helper = ChainFissionHelper() - #else: - # self._normalization_helper = SourceRateHelper() - # Select and create fission yield helper fission_helper = ConstantFissionYieldHelper fission_yield_opts = ( @@ -169,6 +167,7 @@ class FluxSpectraDepletionOperator(TransportOperator): """ # Update the number densities regardless of the source rate + #!!! See previous excamation point comment self.number.set_density(vec) ## TODO : make sure this function works w the current structure. self._update_materials() @@ -176,24 +175,18 @@ class FluxSpectraDepletionOperator(TransportOperator): # Update nuclides data in preparation for transport solve ## TODO : make sure this function works w the current structure. + # this nuclides varibale is all the nuclides that will show up via depletion + # that have xs data nuclides = self._get_reaction_nuclides() self._yield_helper.update_tally_nuclides(nuclides) rates = self.reaction_rates rates.fill(0.0) - # Get k and uncertainty - # TODO : add functionality to get this from the transport solver - keff = ... - # Form fast map nuc_ind = [rates.index_nuc[nuc] for nuc in nuclides] react_ind = [rates.index_rx[react] for react in self.chain.reactions] - # Keep track of energy produced from all reactions in eV per source - # particle - self._normalization_helper.reset() - # Store fission yield dictionaries fission_yields = [] @@ -210,10 +203,11 @@ class FluxSpectraDepletionOperator(TransportOperator): for nuc, i_nuc_results in zip(nuclides, nuc_ind): number[i_nuc_results] = self.number[0, nuc] - # Store microscopic cross sections in rates array + # Calculate macroscopic cross sections and store them in rates array for nuc in nuclides: + density = number.get_atom_density('0', nuc) for rxn in self.chain.reactions: - rates.set('0', nuc, rxn, self._micro_xs[rxn].loc[nuc]) + rates.set('0', nuc, rxn, self._micro_xs[rxn].loc[nuc] * density) # Get reaction rate in reactions/sec rates *= self.flux_spectra @@ -225,10 +219,15 @@ class FluxSpectraDepletionOperator(TransportOperator): #if fission_ind is not None: # self._normalization_helper.update(rxn_rates[:, fission_ind]) - ## These don't seem relevant so I'm commenting them out for now - ## will delete if they are indeed not useful # Divide by total number and store - #rates[0] = self._rate_helper.divide_by_adens(number) + # the reason we do this is bc in the equation, we multiply the depletion matrix + # by the nuclide vector. Since what we want is the depletion matrix, we need to + # divide the reaction rates by the number of atoms to get the right units. + mask = nonzero(number) + results = rates[0] + for col in range(results.shape[1]): + results[mask, col] /= number[mask] + rates[0] = results # Scale reaction rates to obtain units of reactions/sec #rates *= self._normalization_helper.factor(source_rate) @@ -237,7 +236,7 @@ class FluxSpectraDepletionOperator(TransportOperator): # Store new fission yields on the chain self.chain.fission_yields = fission_yields - return OperatorResult(keff, rates) + return OperatorResult(self._keff, rates) def initial_condition(self): @@ -397,4 +396,53 @@ class FluxSpectraDepletionOperator(TransportOperator): return nuclides + def _extract_number(self, local_mats, volume, nuclides, prev_res=None): + """Construct AtomNumber using geometry + + Parameters + ---------- + local_mats : list of str + Material IDs to be managed by this process + volume : OrderedDict of str to float + Volumes for the above materials in [cm^3] + nuclides : list of str + Nuclides to be used in the simulation. + prev_res : Results, optional + Results from a previous depletion calculation + + """ + self.number = AtomNumber(local_mats, nuclides, volume, len(self.chain)) + + if self.dilute_initial != 0.0: + for nuc in self._burnable_nucs: + self.number.set_atom_density(np.s_[:], nuc, self.dilute_initial) + + # Now extract and store the number densities + # From the geometry if no previous depletion results + if prev_res is None: + for mat in self.materials: + if str(mat.id) in local_mats: + self._set_number_from_mat(mat) + + # Else from previous depletion results + else: + for mat in self.materials: + if str(mat.id) in local_mats: + self._set_number_from_results(mat, prev_res) + + def _set_number_from_mat(self, mat): + """Extracts material and number densities from openmc.Material + + Parameters + ---------- + mat : openmc.Material + The material to read from + + """ + mat_id = str(mat.id) + + for nuclide, density in mat.get_nuclide_atom_densities().values(): + number = density * 1.0e24 + self.number.set_atom_density(mat_id, nuclide, number) + From 760ef42ce174ff421fafc85d2bdfa000d69bd2f1 Mon Sep 17 00:00:00 2001 From: yardasol Date: Tue, 28 Jun 2022 16:02:06 -0500 Subject: [PATCH 025/131] consistency and bug fixes - import check_type instead of check_value - remove unused classed from .helpers import statement - add unit spec to nuclides parameter - remove volume parameter from _get_all_depletion_nuclides - fix input sytax for ReactionRate object - add get_results_info function (to be implemented) - impement _extract_number function --- openmc/deplete/flux_operator.py | 88 ++++++++++++++++----------------- 1 file changed, 43 insertions(+), 45 deletions(-) diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/flux_operator.py index cd06894f6..84248c6fb 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/flux_operator.py @@ -16,7 +16,7 @@ import pandas as pd from uncertainties import ufloat import openmc -from openmc.checkvalue import check_value +from openmc.checkvalue import check_type from openmc.data import DataLibrary from openmc.exceptions import DataError from openmc.mpi import comm @@ -25,9 +25,7 @@ from .atom_number import AtomNumber from .chain import _find_chain_file from .reaction_rates import ReactionRates from .results import Results -from .helpers import ( - FluxReactionRateHelper, ChainFissionHelper, SourceRateHelper, - ConstantFissionYieldHelper) +from .helpers import ConstantFissionYieldHelper @@ -47,7 +45,7 @@ class FluxSpectraDepletionOperator(TransportOperator): Volume of the material being depleted in cm^3 nuclides : dict of str to float Dictionary with nuclides names as keys and concentration as values. - Nuclide concentration is assumed to be in [units?] + Nuclide concentration is assumed to be in [at/cm^3] micro_xs : pandas.DataFrame DataFrame with nuclides names as index and microscopic cross section data in the columns. @@ -102,10 +100,11 @@ class FluxSpectraDepletionOperator(TransportOperator): """ def __init__(self, volume, nuclides, micro_xs, flux_spectra, chain_file, keff=None, fission_q=None, dilute_initial=1.0e3, - prev_results=None, reduce_chain=False, reduce_chain_level=None): - super.__init__(chain_file, fission_q, dilute_initial, prev_results) + prev_results=None, reduce_chain=False, reduce_chain_level=None, fission_yield_opts=None): + super().__init__(chain_file, fission_q, dilute_initial, prev_results) self.round_number = False self.flux_spectra = flux_spectra + self._init_nuclides = nuclides # Reduce the chain if reduce_chain: @@ -115,13 +114,13 @@ class FluxSpectraDepletionOperator(TransportOperator): self.chain = self.chain.reduce(all_nuclides, reduce_chain_level) # Validate nuclides and micro-xs parameters - check_value('nuclides', nuclides, dict, str) + check_type('nuclides', nuclides, dict, str) check_type('micro_xs', micro_xs, pd.DataFrame) self._micro_xs = micro_xs self._keff = keff - nuclides = self._get_all_depletion_nuclides(volume, nuclides) + nuclides = self._get_all_depletion_nuclides(nuclides) # TODO: add support for loading previous results @@ -135,11 +134,11 @@ class FluxSpectraDepletionOperator(TransportOperator): # TODO : implement _extract_number # Extract number densities from the geometry / previous depletion run #!!! consider removing this and just using the nuclides dict... - self._extract_number('0', volume, nuclides, self.prev_res) + self._extract_number(['0'], {'0': volume}, nuclides, self.prev_res) # Create reaction rates array self.reaction_rates = ReactionRates( - '0', self._burnable_nucs, self.chain.reactions) + ['0'], self._burnable_nucs, self.chain.reactions) # Select and create fission yield helper fission_helper = ConstantFissionYieldHelper @@ -269,6 +268,23 @@ class FluxSpectraDepletionOperator(TransportOperator): """ ... + def get_results_info(self): + """Returns volume list, cell lists, and nuc lists. + + Returns + ------- + volume : dict of str to float + Volumes corresponding to materials in burn_list + nuc_list : list of str + A list of all nuclide names. Used for sorting the simulation. + burn_list : list of int + A list of all cell IDs to be burned. Used for sorting the + simulation. + full_burn_list : list of int + All burnable materials in the geometry. + """ + + def _update_materials(self): """Updates material compositions in OpenMC on all processes.""" @@ -349,13 +365,14 @@ class FluxSpectraDepletionOperator(TransportOperator): nuc_list = comm.bcast(nuc_list) return [nuc for nuc in nuc_list if nuc in self.chain] - def _get_all_depletion_nuclides(self, volume, nuclides): + def _get_all_depletion_nuclides(self, nuclides): """Determine nuclides that will show up in simulation Parameters ---------- - volume : float - Volume of material in cm^3 + nuclides : dict + dict whose keys are nuclide symbols as strings, + and whose values are nuclide concentrations in at/cm^3 Returns ------- @@ -366,15 +383,14 @@ class FluxSpectraDepletionOperator(TransportOperator): model_nuclides = set() - self.heavy_metal = 0.0 + #self.heavy_metal = 0.0 - bu_mat = openmc.Material() - bu_mat.volume = volume - for n, conc in nuclides: - bu_mat.add_nuclides(n,conc) + #bu_mat = openmc.Material() + #bu_mat.volume = volume + for n in nuclides: model_nuclides.add(n) - self.heavy_metal = bu_mat.fissionable_mass + #self.heavy_metal = bu_mat.fissionable_mass # Sort the sets model_nuclides = sorted(model_nuclides) @@ -391,8 +407,7 @@ class FluxSpectraDepletionOperator(TransportOperator): """Finds nuclides with cross section data""" nuclides = set() for name in self._micro_xs.index: - if name not in nuclides: - nuclides.add(name) + nuclides.add(name) return nuclides @@ -420,29 +435,12 @@ class FluxSpectraDepletionOperator(TransportOperator): # Now extract and store the number densities # From the geometry if no previous depletion results if prev_res is None: - for mat in self.materials: - if str(mat.id) in local_mats: - self._set_number_from_mat(mat) + for nuclide in nuclides: + if nuclide in self._init_nuclides: + self.number.set_atom_density('0', nuclide, self._init_nuclides[nuclide]) + elif nuclide not in self._burnable_nucs: + self.number.set_atom_density('0', nuclide, 0) # Else from previous depletion results else: - for mat in self.materials: - if str(mat.id) in local_mats: - self._set_number_from_results(mat, prev_res) - - def _set_number_from_mat(self, mat): - """Extracts material and number densities from openmc.Material - - Parameters - ---------- - mat : openmc.Material - The material to read from - - """ - mat_id = str(mat.id) - - for nuclide, density in mat.get_nuclide_atom_densities().values(): - number = density * 1.0e24 - self.number.set_atom_density(mat_id, nuclide, number) - - + raise RuntimeError("Loading from previous results not yet supported") From 22a29af6dc4283b5781a3802f6eff46f2d8357a8 Mon Sep 17 00:00:00 2001 From: yardasol Date: Tue, 28 Jun 2022 16:11:20 -0500 Subject: [PATCH 026/131] implement get_results_info function --- openmc/deplete/flux_operator.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/flux_operator.py index 84248c6fb..c4ceacc5a 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/flux_operator.py @@ -105,6 +105,7 @@ class FluxSpectraDepletionOperator(TransportOperator): self.round_number = False self.flux_spectra = flux_spectra self._init_nuclides = nuclides + self._volume = volume # Reduce the chain if reduce_chain: @@ -268,6 +269,7 @@ class FluxSpectraDepletionOperator(TransportOperator): """ ... + def get_results_info(self): """Returns volume list, cell lists, and nuc lists. @@ -283,7 +285,16 @@ class FluxSpectraDepletionOperator(TransportOperator): full_burn_list : list of int All burnable materials in the geometry. """ + nuc_list = self.number.burnable_nuclides + burn_list = ['0'] + volume = {'0': self._volume} + + # Combine volume dictionaries across processes + volume_list = comm.allgather(volume) + volume = {k: v for d in volume_list for k, v in d.items()} + + return volume, nuc_list, burn_list, burn_list def _update_materials(self): From 992d4c16611044f9554ddcd4ff86ad2a0d1a8a91 Mon Sep 17 00:00:00 2001 From: yardasol Date: Tue, 28 Jun 2022 16:11:43 -0500 Subject: [PATCH 027/131] add flux_operator to the __init__ module --- openmc/deplete/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openmc/deplete/__init__.py b/openmc/deplete/__init__.py index 5891555a2..753167741 100644 --- a/openmc/deplete/__init__.py +++ b/openmc/deplete/__init__.py @@ -8,6 +8,7 @@ A depletion front-end tool. from .nuclide import * from .chain import * from .operator import * +from .flux_operator import * from .reaction_rates import * from .atom_number import * from .stepresult import * From e170914e39dffe51da10693f8436f237f3a1ad01 Mon Sep 17 00:00:00 2001 From: yardasol Date: Tue, 28 Jun 2022 21:03:41 -0500 Subject: [PATCH 028/131] variable name and algorithm improvements - grammatical fixes in docstring for nuclide parameter - remove unnecessary for loop in the reduce_chain block - rename _get_all_depletion_nuclides to _get_all_nuclides_in_simulation - change output of the above to an attribute, _all_nuclides - change output of _get_reaction_nuclides to rxn_nuclides in __call__ - pass on creating statepoints - make loop in _get_reaction_nuclides more efficient - remove unnecessary loop in _get_nuclides_with_data --- openmc/deplete/flux_operator.py | 113 ++++++++++++++------------------ 1 file changed, 50 insertions(+), 63 deletions(-) diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/flux_operator.py index c4ceacc5a..48e9fda33 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/flux_operator.py @@ -28,8 +28,6 @@ from .results import Results from .helpers import ConstantFissionYieldHelper - - class FluxSpectraDepletionOperator(TransportOperator): """Depletion operator that uses a user provided flux spectrum and one-group cross sections calculate reaction rates. @@ -44,11 +42,11 @@ class FluxSpectraDepletionOperator(TransportOperator): volume : float Volume of the material being depleted in cm^3 nuclides : dict of str to float - Dictionary with nuclides names as keys and concentration as values. - Nuclide concentration is assumed to be in [at/cm^3] + Dictionary with nuclide names as keys and nuclide concentrations as + values. Nuclide concentration units are [at/cm^3]. micro_xs : pandas.DataFrame DataFrame with nuclides names as index and microscopic cross section - data in the columns. + data in the columns. Cross section units are [cm^-2]. flux_spectra : float Flux spectrum [n cm^-2 s^-1] chain_file : str @@ -99,20 +97,28 @@ class FluxSpectraDepletionOperator(TransportOperator): results are to be used. """ - def __init__(self, volume, nuclides, micro_xs, flux_spectra, chain_file, keff=None, fission_q=None, dilute_initial=1.0e3, - prev_results=None, reduce_chain=False, reduce_chain_level=None, fission_yield_opts=None): + def __init__(self, + volume, + nuclides, + micro_xs, + flux_spectra, + chain_file, + keff=None, + fission_q=None, + dilute_initial=1.0e3, + prev_results=None, + reduce_chain=False, + reduce_chain_level=None, + fission_yield_opts=None): super().__init__(chain_file, fission_q, dilute_initial, prev_results) - self.round_number = False self.flux_spectra = flux_spectra self._init_nuclides = nuclides self._volume = volume - # Reduce the chain + # Reduce the chain to only those nuclides present if reduce_chain: - all_nuclides = set() - for nuclide_name in nuclides.keys(): - all_isotopes.add(nuclide_name) - self.chain = self.chain.reduce(all_nuclides, reduce_chain_level) + init_nuc_names = set(nuclides.keys()) + self.chain = self.chain.reduce(init_nuc_names, reduce_chain_level) # Validate nuclides and micro-xs parameters check_type('nuclides', nuclides, dict, str) @@ -121,21 +127,24 @@ class FluxSpectraDepletionOperator(TransportOperator): self._micro_xs = micro_xs self._keff = keff - nuclides = self._get_all_depletion_nuclides(nuclides) + self._all_nuclides = self._get_all_nuclides_in_simulation() # TODO: add support for loading previous results # Determine which nuclides have cross section data + # This nuclides variables contains every nuclides + # for which there is an entry in the micro_xs parameter self.nuclides_with_data = self._get_nuclides_with_data() # Select nuclides with data that are also in the chain self._burnable_nucs = [nuc.name for nuc in self.chain.nuclides if nuc.name in self.nuclides_with_data] - # TODO : implement _extract_number # Extract number densities from the geometry / previous depletion run - #!!! consider removing this and just using the nuclides dict... - self._extract_number(['0'], {'0': volume}, nuclides, self.prev_res) + self._extract_number(['0'], + {'0': volume}, + self._all_nuclides, + self.prev_res) # Create reaction rates array self.reaction_rates = ReactionRates( @@ -167,7 +176,6 @@ class FluxSpectraDepletionOperator(TransportOperator): """ # Update the number densities regardless of the source rate - #!!! See previous excamation point comment self.number.set_density(vec) ## TODO : make sure this function works w the current structure. self._update_materials() @@ -175,16 +183,15 @@ class FluxSpectraDepletionOperator(TransportOperator): # Update nuclides data in preparation for transport solve ## TODO : make sure this function works w the current structure. - # this nuclides varibale is all the nuclides that will show up via depletion + # this nuclides varibale contains all the nuclides that will show up via depletion # that have xs data - nuclides = self._get_reaction_nuclides() - self._yield_helper.update_tally_nuclides(nuclides) + rxn_nuclides = self._get_reaction_nuclides() rates = self.reaction_rates rates.fill(0.0) # Form fast map - nuc_ind = [rates.index_nuc[nuc] for nuc in nuclides] + nuc_ind = [rates.index_nuc[nuc] for nuc in rxn_nuclides] react_ind = [rates.index_rx[react] for react in self.chain.reactions] # Store fission yield dictionaries @@ -267,7 +274,7 @@ class FluxSpectraDepletionOperator(TransportOperator): step : int Current depletion step including restarts """ - ... + pass def get_results_info(self): @@ -334,10 +341,10 @@ class FluxSpectraDepletionOperator(TransportOperator): # summary.h5 file contains densities at the first time step def _get_reaction_nuclides(self): - """Determine nuclides that should be tallied for reaction rates. + """Determine nuclides that should have reation rates This method returns a list of all nuclides that have neutron data and - are listed in the depletion chain. Technically, we should tally nuclides + are listed in the depletion chain. Technically, we should list nuclides that may not appear in the depletion chain because we still need to get the fission reaction rate for these nuclides in order to normalize power, but that is left as a future exercise. @@ -345,17 +352,15 @@ class FluxSpectraDepletionOperator(TransportOperator): Returns ------- list of str - Tally nuclides + nuclides with reaction rates """ - nuc_set = set() - # Create the set of all nuclides in the decay chain in materials marked # for burning in which the number density is greater than zero. - for nuc in self.number.nuclides: - if nuc in self.nuclides_with_data: - if np.sum(self.number[:, nuc]) > 0.0: - nuc_set.add(nuc) + nuc_set = set(self._all_nuclides).intersection(self.nuclides_with_data) + for nuc in nuc_set: + if not np.sum(self.number[:, nuc]) > 0.0: + nuc_set.remove(nuc) # Communicate which nuclides have nonzeros to rank 0 if comm.rank == 0: @@ -376,51 +381,33 @@ class FluxSpectraDepletionOperator(TransportOperator): nuc_list = comm.bcast(nuc_list) return [nuc for nuc in nuc_list if nuc in self.chain] - def _get_all_depletion_nuclides(self, nuclides): - """Determine nuclides that will show up in simulation - - Parameters - ---------- - nuclides : dict - dict whose keys are nuclide symbols as strings, - and whose values are nuclide concentrations in at/cm^3 + def _get_all_nuclides_in_simulation(self): + """Determine nuclides that will show up in simulation. + This is the union of the nuclides provided by the user and + the nuclides present in the depletion chain. Returns ------- - nuclides : list of str + all_nuclides : list of str Nuclides in order of how they'll appear in the simulation. """ - model_nuclides = set() - - #self.heavy_metal = 0.0 - - #bu_mat = openmc.Material() - #bu_mat.volume = volume - for n in nuclides: - model_nuclides.add(n) - - #self.heavy_metal = bu_mat.fissionable_mass - - # Sort the sets - model_nuclides = sorted(model_nuclides) + init_nuclides = sorted(self._init_nuclides.keys()) # Construct a global nuclide dictionary, burned first - nuclides = list(self.chain.nuclide_dict) - for nuc in model_nuclides: - if nuc not in nuclides: - nuclides.append(nuc) + all_nuclides = list(self.chain.nuclide_dict) + for nuc in init_nuclides: + if nuc not in all_nuclides: + all_nuclides.append(nuc) - return nuclides + return all_nuclides def _get_nuclides_with_data(self): """Finds nuclides with cross section data""" - nuclides = set() - for name in self._micro_xs.index: - nuclides.add(name) + nuclides_with_data = set(self._micro_xs.index) - return nuclides + return nuclides_with_data def _extract_number(self, local_mats, volume, nuclides, prev_res=None): """Construct AtomNumber using geometry From 1ea6734a7fab225103ae857329ab587b5093c886 Mon Sep 17 00:00:00 2001 From: yardasol Date: Wed, 29 Jun 2022 13:16:22 -0500 Subject: [PATCH 029/131] remove cruft comments, clean up docstrings --- openmc/deplete/flux_operator.py | 38 ++++++++++----------------------- 1 file changed, 11 insertions(+), 27 deletions(-) diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/flux_operator.py index 48e9fda33..be23a2dba 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/flux_operator.py @@ -29,8 +29,8 @@ from .helpers import ConstantFissionYieldHelper class FluxSpectraDepletionOperator(TransportOperator): - """Depletion operator that uses a user provided flux spectrum and one-group - cross sections calculate reaction rates. + """Depletion operator that uses a user-provided flux spectrum and one-group + cross sections to calculate reaction rates. Instances of this class can be used to perform depletion using one group cross sections and constant flux. Normally, a user needn't call methods of @@ -90,8 +90,6 @@ class FluxSpectraDepletionOperator(TransportOperator): The depletion chain information necessary to form matrices and tallies. reaction_rates : openmc.deplete.ReactionRates Reaction rates from the last operator step. - heavy_metal : float - Initial heavy metal inventory [g] prev_res : Results or None Results from a previous depletion calculation. ``None`` if no results are to be used. @@ -159,7 +157,7 @@ class FluxSpectraDepletionOperator(TransportOperator): def __call__(self, vec, source_rate): - """Runs a simulation. + """Obtain the reaction rates Parameters ---------- @@ -177,14 +175,9 @@ class FluxSpectraDepletionOperator(TransportOperator): # Update the number densities regardless of the source rate self.number.set_density(vec) - ## TODO : make sure this function works w the current structure. self._update_materials() - # Update nuclides data in preparation for transport solve - - ## TODO : make sure this function works w the current structure. - # this nuclides varibale contains all the nuclides that will show up via depletion - # that have xs data + # Get all nuclides for which we will calculate reaction rates rxn_nuclides = self._get_reaction_nuclides() rates = self.reaction_rates @@ -222,13 +215,10 @@ class FluxSpectraDepletionOperator(TransportOperator): # Compute fission yields for this material fission_yields.append(self._yield_helper.weighted_yields(0)) - # Accumulate energy from fission - #if fission_ind is not None: - # self._normalization_helper.update(rxn_rates[:, fission_ind]) - - # Divide by total number and store - # the reason we do this is bc in the equation, we multiply the depletion matrix - # by the nuclide vector. Since what we want is the depletion matrix, we need to + # Divide by total number of atoms and store + # the reason we do this is based on the mathematical equation; + # in the equation, we multiply the depletion matrix by the nuclide + # vector. Since what we want is the depletion matrix, we need to # divide the reaction rates by the number of atoms to get the right units. mask = nonzero(number) results = rates[0] @@ -236,10 +226,6 @@ class FluxSpectraDepletionOperator(TransportOperator): results[mask, col] /= number[mask] rates[0] = results - # Scale reaction rates to obtain units of reactions/sec - #rates *= self._normalization_helper.factor(source_rate) - ## - # Store new fission yields on the chain self.chain.fission_yields = fission_yields @@ -255,9 +241,6 @@ class FluxSpectraDepletionOperator(TransportOperator): Total density for initial conditions. """ - #self._normalization_helper.prepare( - # self.chain.nuclides, self.reaction_rates.index_nuc) - # Return number density vector return list(self.number.get_mat_slice(np.s_[:])) @@ -274,6 +257,7 @@ class FluxSpectraDepletionOperator(TransportOperator): step : int Current depletion step including restarts """ + # Since we aren't running a transport simulation, we simply pass pass @@ -341,7 +325,7 @@ class FluxSpectraDepletionOperator(TransportOperator): # summary.h5 file contains densities at the first time step def _get_reaction_nuclides(self): - """Determine nuclides that should have reation rates + """Determine nuclides that should have reaction rates This method returns a list of all nuclides that have neutron data and are listed in the depletion chain. Technically, we should list nuclides @@ -382,7 +366,7 @@ class FluxSpectraDepletionOperator(TransportOperator): return [nuc for nuc in nuc_list if nuc in self.chain] def _get_all_nuclides_in_simulation(self): - """Determine nuclides that will show up in simulation. + """Determine nuclides that will show up in the simulation. This is the union of the nuclides provided by the user and the nuclides present in the depletion chain. From 50acd579cbfec9817c731665d8b427638f8921a9 Mon Sep 17 00:00:00 2001 From: yardasol Date: Wed, 29 Jun 2022 14:18:07 -0500 Subject: [PATCH 030/131] added convenience functions for creating the mirco_xs parameter --- openmc/deplete/flux_operator.py | 71 ++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/flux_operator.py index be23a2dba..b74403b69 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/flux_operator.py @@ -22,7 +22,7 @@ from openmc.exceptions import DataError from openmc.mpi import comm from .abc import TransportOperator, OperatorResult from .atom_number import AtomNumber -from .chain import _find_chain_file +from .chain import _find_chain_file, REACTIONS from .reaction_rates import ReactionRates from .results import Results from .helpers import ConstantFissionYieldHelper @@ -288,6 +288,75 @@ class FluxSpectraDepletionOperator(TransportOperator): return volume, nuc_list, burn_list, burn_list + @staticmethod + def create_micro_xs_from_data_array(nuclides, reactions, data, units='barn'): + """ + Creates a ``micro_xs`` parameter from a dictionary. + + Parameters + ---------- + nuclides : list of str + List of nuclide symbols for that have data for at least one + reaction. + reactions : list of str + List of reactions. All reactions must match those in ``chain.REACTONS`` + data : ndarray of floats + Array containing one-group microscopic cross section information for each + nuclide and reaction. + units : {'barn', 'cm^2'}, optional + Units for microscopic cross section data. Defaults to ``barn``. + + Returns + ------- + micro_xs : pandas.DataFrame + A DataFrame object correctly formatted for use in ``FluxOperator`` + """ + + # Validate inputs + try: + assert data.shape == (len(nuclides), len(reactions)) + except AssertionError: + raise SyntaxError('Nuclides list of length {len(nuclides)} and + reactions array of length {len(reactions)} do not + match dimensions of data array of shape {data.shape}') + + check_iterable_type('nuclides', nuclides, str) + check_iterable_type('reactions', reactions, str) + check_type('data', data, float, expected_iter_type=np.array) + check_value('reactions', reactions, chain.REACTIONS) + + if units == 'barn': + data /= 1e24 + + return pd.DataFrame(index=nuclides, columns=reactions, data=data) + + + @staticmethod + def create_micro_xs_from_csv(csv_file, units='barn'): + """ + Create the ``micro_xs`` parameter from a ``.csv`` file. + + Parameters + ---------- + csv_file : str + Relative path to csv-file containing microscopic cross section + data. + units : {'barn', 'cm^2'}, optional + Units for microscopic cross section data. Defaults to ``barn``. + + Returns + ------- + micro_xs : pandas.DataFrame + A DataFrame object correctly formatted for use in ``FluxOperator`` + + """ + micro_xs = pd.DataFrame.read_csv(csv_file) + if units == 'barn': + micro_xs /= 1e24 + + return micro_xs + + def _update_materials(self): """Updates material compositions in OpenMC on all processes.""" From ecc38a1ae4ba2adc194bd0a0c61a3d088b56b242 Mon Sep 17 00:00:00 2001 From: yardasol Date: Wed, 29 Jun 2022 14:47:30 -0500 Subject: [PATCH 031/131] fix multi-line error message --- openmc/deplete/flux_operator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/flux_operator.py index b74403b69..ab6fbe565 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/flux_operator.py @@ -316,9 +316,9 @@ class FluxSpectraDepletionOperator(TransportOperator): try: assert data.shape == (len(nuclides), len(reactions)) except AssertionError: - raise SyntaxError('Nuclides list of length {len(nuclides)} and - reactions array of length {len(reactions)} do not - match dimensions of data array of shape {data.shape}') + raise SyntaxError('Nuclides list of length {len(nuclides)} and' + 'reactions array of length {len(reactions)} do not' + 'match dimensions of data array of shape {data.shape}') check_iterable_type('nuclides', nuclides, str) check_iterable_type('reactions', reactions, str) From 5fec7a2355819039007e509d4cc365dfcf6ac928 Mon Sep 17 00:00:00 2001 From: yardasol Date: Wed, 29 Jun 2022 15:05:22 -0500 Subject: [PATCH 032/131] pep8 fixes --- openmc/deplete/flux_operator.py | 53 +++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/flux_operator.py index ab6fbe565..3ca046cf2 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/flux_operator.py @@ -155,7 +155,6 @@ class FluxSpectraDepletionOperator(TransportOperator): self._yield_helper = fission_helper.from_operator( self, **fission_yield_opts) - def __call__(self, vec, source_rate): """Obtain the reaction rates @@ -207,7 +206,12 @@ class FluxSpectraDepletionOperator(TransportOperator): for nuc in nuclides: density = number.get_atom_density('0', nuc) for rxn in self.chain.reactions: - rates.set('0', nuc, rxn, self._micro_xs[rxn].loc[nuc] * density) + rates.set( + '0', + nuc, + rxn, + self._micro_xs[rxn].loc[nuc] * + density) # Get reaction rate in reactions/sec rates *= self.flux_spectra @@ -219,7 +223,8 @@ class FluxSpectraDepletionOperator(TransportOperator): # the reason we do this is based on the mathematical equation; # in the equation, we multiply the depletion matrix by the nuclide # vector. Since what we want is the depletion matrix, we need to - # divide the reaction rates by the number of atoms to get the right units. + # divide the reaction rates by the number of atoms to get the right + # units. mask = nonzero(number) results = rates[0] for col in range(results.shape[1]): @@ -231,7 +236,6 @@ class FluxSpectraDepletionOperator(TransportOperator): return OperatorResult(self._keff, rates) - def initial_condition(self): """Performs final setup and returns initial condition. @@ -244,8 +248,6 @@ class FluxSpectraDepletionOperator(TransportOperator): # Return number density vector return list(self.number.get_mat_slice(np.s_[:])) - - def write_bos_data(self, step): """Document beginning of step data for a given step @@ -260,7 +262,6 @@ class FluxSpectraDepletionOperator(TransportOperator): # Since we aren't running a transport simulation, we simply pass pass - def get_results_info(self): """Returns volume list, cell lists, and nuc lists. @@ -287,9 +288,9 @@ class FluxSpectraDepletionOperator(TransportOperator): return volume, nuc_list, burn_list, burn_list - @staticmethod - def create_micro_xs_from_data_array(nuclides, reactions, data, units='barn'): + def create_micro_xs_from_data_array( + nuclides, reactions, data, units='barn'): """ Creates a ``micro_xs`` parameter from a dictionary. @@ -316,9 +317,10 @@ class FluxSpectraDepletionOperator(TransportOperator): try: assert data.shape == (len(nuclides), len(reactions)) except AssertionError: - raise SyntaxError('Nuclides list of length {len(nuclides)} and' - 'reactions array of length {len(reactions)} do not' - 'match dimensions of data array of shape {data.shape}') + raise SyntaxError( + 'Nuclides list of length {len(nuclides)} and' + 'reactions array of length {len(reactions)} do not' + 'match dimensions of data array of shape {data.shape}') check_iterable_type('nuclides', nuclides, str) check_iterable_type('reactions', reactions, str) @@ -330,7 +332,6 @@ class FluxSpectraDepletionOperator(TransportOperator): return pd.DataFrame(index=nuclides, columns=reactions, data=data) - @staticmethod def create_micro_xs_from_csv(csv_file, units='barn'): """ @@ -356,7 +357,6 @@ class FluxSpectraDepletionOperator(TransportOperator): return micro_xs - def _update_materials(self): """Updates material compositions in OpenMC on all processes.""" @@ -383,14 +383,20 @@ class FluxSpectraDepletionOperator(TransportOperator): densities.append(val) else: # Only output warnings if values are significantly - # negative. CRAM does not guarantee positive values. + # negative. CRAM does not guarantee positive + # values. if val < -1.0e-21: - print("WARNING: nuclide ", nuc, " in material ", mat, - " is negative (density = ", val, " at/barn-cm)") + print( + "WARNING: nuclide ", + nuc, + " in material ", + mat, + " is negative (density = ", + val, + " at/barn-cm)") number_i[mat, nuc] = 0.0 - - #TODO Update densities on the Python side, otherwise the + # TODO Update densities on the Python side, otherwise the # summary.h5 file contains densities at the first time step def _get_reaction_nuclides(self): @@ -481,17 +487,20 @@ class FluxSpectraDepletionOperator(TransportOperator): if self.dilute_initial != 0.0: for nuc in self._burnable_nucs: - self.number.set_atom_density(np.s_[:], nuc, self.dilute_initial) + self.number.set_atom_density( + np.s_[:], nuc, self.dilute_initial) # Now extract and store the number densities # From the geometry if no previous depletion results if prev_res is None: for nuclide in nuclides: if nuclide in self._init_nuclides: - self.number.set_atom_density('0', nuclide, self._init_nuclides[nuclide]) + self.number.set_atom_density( + '0', nuclide, self._init_nuclides[nuclide]) elif nuclide not in self._burnable_nucs: self.number.set_atom_density('0', nuclide, 0) # Else from previous depletion results else: - raise RuntimeError("Loading from previous results not yet supported") + raise RuntimeError( + "Loading from previous results not yet supported") From 9d3573d89052e08894d3e671d2bdde3e7a828869 Mon Sep 17 00:00:00 2001 From: yardasol Date: Tue, 5 Jul 2022 16:30:55 -0500 Subject: [PATCH 033/131] unit tests for FluxDepleteOperator - fix value checking syntax in static methods - add convenicece function to D.R.Y. - initial unit tests - add csv file for toy 1g cross sections --- openmc/deplete/flux_operator.py | 34 ++++++--- tests/micro_xs_simple.csv | 13 ++++ .../unit_tests/test_flux_deplete_operator.py | 73 +++++++++++++++++++ 3 files changed, 110 insertions(+), 10 deletions(-) create mode 100644 tests/micro_xs_simple.csv create mode 100644 tests/unit_tests/test_flux_deplete_operator.py diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/flux_operator.py index 3ca046cf2..37100ac5b 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/flux_operator.py @@ -16,7 +16,7 @@ import pandas as pd from uncertainties import ufloat import openmc -from openmc.checkvalue import check_type +from openmc.checkvalue import check_type, check_value, check_iterable_type from openmc.data import DataLibrary from openmc.exceptions import DataError from openmc.mpi import comm @@ -27,8 +27,19 @@ from .reaction_rates import ReactionRates from .results import Results from .helpers import ConstantFissionYieldHelper +valid_rxns = list(REACTIONS.keys()) +valid_rxns += ['fission'] -class FluxSpectraDepletionOperator(TransportOperator): +# Convenience function for the micro_xs static methods +def _validate_micro_xs_inputs(nuclides, reactions, data): + check_iterable_type('nuclides', nuclides, str) + check_iterable_type('reactions', reactions, str) + check_type('data', data, np.ndarray, expected_iter_type=float) + [check_value('reactions', reaction, valid_rxns) for reaction in reactions] + + + +class FluxDepletionOperator(TransportOperator): """Depletion operator that uses a user-provided flux spectrum and one-group cross sections to calculate reaction rates. @@ -318,15 +329,13 @@ class FluxSpectraDepletionOperator(TransportOperator): assert data.shape == (len(nuclides), len(reactions)) except AssertionError: raise SyntaxError( - 'Nuclides list of length {len(nuclides)} and' - 'reactions array of length {len(reactions)} do not' - 'match dimensions of data array of shape {data.shape}') + f'Nuclides list of length {len(nuclides)} and ' + f'reactions array of length {len(reactions)} do not ' + f'match dimensions of data array of shape {data.shape}') - check_iterable_type('nuclides', nuclides, str) - check_iterable_type('reactions', reactions, str) - check_type('data', data, float, expected_iter_type=np.array) - check_value('reactions', reactions, chain.REACTIONS) + _validate_micro_xs_inputs(nuclides, reactions, data) + # Convert to cm^2 if units == 'barn': data /= 1e24 @@ -351,7 +360,12 @@ class FluxSpectraDepletionOperator(TransportOperator): A DataFrame object correctly formatted for use in ``FluxOperator`` """ - micro_xs = pd.DataFrame.read_csv(csv_file) + micro_xs = pd.read_csv(csv_file, index_col=0) + + _validate_micro_xs_inputs(list(micro_xs.index), + list(micro_xs.columns), + micro_xs.to_numpy()) + if units == 'barn': micro_xs /= 1e24 diff --git a/tests/micro_xs_simple.csv b/tests/micro_xs_simple.csv new file mode 100644 index 000000000..40142f543 --- /dev/null +++ b/tests/micro_xs_simple.csv @@ -0,0 +1,13 @@ +,fission,"(n,gamma)" +U234,0.1,0.0 +U235,0.1,0.0 +U238,0.9,0.0 +U236,0.4,0.0 +O16,0.0,0.0 +O17,0.0,0.0 +I135,0.0,0.1 +Xe135,0.0,0.9 +Xe136,0.0,0.0 +Cs135,0.0,0.0 +Gd157,0.0,0.1 +Gd156,0.0,0.1 diff --git a/tests/unit_tests/test_flux_deplete_operator.py b/tests/unit_tests/test_flux_deplete_operator.py new file mode 100644 index 000000000..a2b150493 --- /dev/null +++ b/tests/unit_tests/test_flux_deplete_operator.py @@ -0,0 +1,73 @@ +"""Basic unit tests for openmc.deplete.FluxDepletionOperator instantiation + +Modifies and resets environment variable OPENMC_CROSS_SECTIONS +to a custom file with new depletion_chain node +""" + +from pathlib import Path + +import pytest +from openmc.deplete.flux_operator import FluxDepletionOperator +import pandas as pd +import numpy as np + +FLUX_SPECTRA = 5e16 +CHAIN_PATH = Path(__file__).parents[1] / "chain_simple.xml" +ONE_GROUP_XS = Path(__file__).parents[1] / "micro_xs_simple.csv" + + +def test_create_micro_xs_from_data_array(): + nuclides = [ + 'U234', + 'U235', + 'U238', + 'U236', + 'O16', + 'O17', + 'I135', + 'Xe135', + 'Xe136', + 'Cs135', + 'Gd157', + 'Gd156'] + reactions = ['fission', '(n,gamma)'] + # These values are placeholders and are not at all + # physically meaningful. + data = np.array([[0.1, 0.], + [0.1, 0.], + [0.9, 0.], + [0.4, 0.], + [0., 0.], + [0., 0.], + [0., 0.1], + [0., 0.9], + [0., 0.], + [0., 0.], + [0., 0.1], + [0., 0.1]]) + + FluxDepletionOperator.create_micro_xs_from_data_array( + nuclides, reactions, data) + with pytest.raises(SyntaxError, match=r'Nuclides list of length \d* and ' + r'reactions array of length \d* do not ' + r'match dimensions of data array of shape \(\d*\,d*\)'): + FluxDepletionOperator.create_micro_xs_from_data_array( + nuclides, reactions, data[:, 0]) + + +def test_create_micro_xs_from_csv(): + FluxDepletionOperator.create_micro_xs_from_csv(ONE_GROUP_XS) + + +def test_operator_init(): + """The test uses a temporary dummy chain. This file will be removed + at the end of the test, and only contains a depletion_chain node.""" + nuclides = {'U234': 8.922411359424315e+18, + 'U235': 9.98240191860822e+20, + 'U238': 2.2192386373095893e+22, + 'U236': 4.5724195495061115e+18, + 'O16': 4.639065406771322e+22, + 'O17': 1.7588724018066158e+19} + micro_xs = FluxDepletionOperator.create_micro_xs_from_csv(ONE_GROUP_XS) + my_flux_operator = FluxDepletionOperator( + 1, nuclides, micro_xs, FLUX_SPECTRA, CHAIN_PATH) From 1d81b553de628889c6f0004ad4e5cb8f9975d57d Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 7 Jul 2022 14:30:48 -0500 Subject: [PATCH 034/131] allow None-valued keff in stepresult.py --- openmc/deplete/stepresult.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openmc/deplete/stepresult.py b/openmc/deplete/stepresult.py index 6e2373c45..80a664c6f 100644 --- a/openmc/deplete/stepresult.py +++ b/openmc/deplete/stepresult.py @@ -495,7 +495,13 @@ class StepResult: for mat_i in range(n_mat): results[i, mat_i, :] = x[i][mat_i] - results.k = [(r.k.nominal_value, r.k.std_dev) for r in op_results] + ks = [] + for r in op_results: + if isinstance(r.k, type(None)): + ks += [(None, None)] + else: + ks += [(r.k.nominal_value, r.k.std_dev)] + results.k = ks results.rates = [r.rates for r in op_results] results.time = t results.source_rate = source_rate From 516c117533818ca25d33ea72e39f61efbefa64f5 Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 7 Jul 2022 14:31:59 -0500 Subject: [PATCH 035/131] Add type checking for keff; typo fixes in flux_operator.py --- openmc/deplete/flux_operator.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/flux_operator.py index 37100ac5b..1da7cf9f9 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/flux_operator.py @@ -90,6 +90,9 @@ class FluxDepletionOperator(TransportOperator): Initial atom density [atoms/cm^3] to add for nuclides that are zero in initial condition to ensure they exist in the decay chain. Only done for nuclides with reaction rates. + round_number : bool + Whether or not to round output to OpenMC to 8 digits. + Useful in testing, as OpenMC is incredibly sensitive to exact values. prev_res : Results or None Results from a previous depletion calculation. ``None`` if no results are to be used. @@ -120,6 +123,7 @@ class FluxDepletionOperator(TransportOperator): reduce_chain_level=None, fission_yield_opts=None): super().__init__(chain_file, fission_q, dilute_initial, prev_results) + self.round_number = False self.flux_spectra = flux_spectra self._init_nuclides = nuclides self._volume = volume @@ -134,6 +138,10 @@ class FluxDepletionOperator(TransportOperator): check_type('micro_xs', micro_xs, pd.DataFrame) self._micro_xs = micro_xs + if not isinstance(keff, type(None)): + check_type('keff', keff, tuple, float) + keff = ufloat(keff) + self._keff = keff self._all_nuclides = self._get_all_nuclides_in_simulation() @@ -210,12 +218,13 @@ class FluxDepletionOperator(TransportOperator): number.fill(0.0) # Get new number densities - for nuc, i_nuc_results in zip(nuclides, nuc_ind): + for nuc, i_nuc_results in zip(rxn_nuclides, nuc_ind): number[i_nuc_results] = self.number[0, nuc] + # Calculate macroscopic cross sections and store them in rates array - for nuc in nuclides: - density = number.get_atom_density('0', nuc) + for nuc in rxn_nuclides: + density = self.number.get_atom_density('0', nuc) for rxn in self.chain.reactions: rates.set( '0', @@ -236,7 +245,7 @@ class FluxDepletionOperator(TransportOperator): # vector. Since what we want is the depletion matrix, we need to # divide the reaction rates by the number of atoms to get the right # units. - mask = nonzero(number) + mask = np.nonzero(number) results = rates[0] for col in range(results.shape[1]): results[mask, col] /= number[mask] From 596359339894f01f5be56bc2f28715d129d3dbbd Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 7 Jul 2022 15:12:38 -0500 Subject: [PATCH 036/131] created regression test for FluxDepleteOperator - new data file: tests/micro_xs_simple.csv - created regression test based on pincell example - moved old depletion regression test to new folder: tests/regression_tests/deplete/transport_deplete --- tests/micro_xs_simple.csv | 20 ++-- .../deplete/no_transport_deplete/test.py | 106 ++++++++++++++++++ .../deplete/test_reference.h5 | Bin 166224 -> 0 bytes .../example_geometry.py | 0 .../last_step_reference_materials.xml | 0 .../deplete/{ => transport_deplete}/test.py | 2 +- 6 files changed, 114 insertions(+), 14 deletions(-) create mode 100644 tests/regression_tests/deplete/no_transport_deplete/test.py delete mode 100644 tests/regression_tests/deplete/test_reference.h5 rename tests/regression_tests/deplete/{ => transport_deplete}/example_geometry.py (100%) rename tests/regression_tests/deplete/{ => transport_deplete}/last_step_reference_materials.xml (100%) rename tests/regression_tests/deplete/{ => transport_deplete}/test.py (98%) diff --git a/tests/micro_xs_simple.csv b/tests/micro_xs_simple.csv index 40142f543..bc43600ba 100644 --- a/tests/micro_xs_simple.csv +++ b/tests/micro_xs_simple.csv @@ -1,13 +1,7 @@ -,fission,"(n,gamma)" -U234,0.1,0.0 -U235,0.1,0.0 -U238,0.9,0.0 -U236,0.4,0.0 -O16,0.0,0.0 -O17,0.0,0.0 -I135,0.0,0.1 -Xe135,0.0,0.9 -Xe136,0.0,0.0 -Cs135,0.0,0.0 -Gd157,0.0,0.1 -Gd156,0.0,0.1 +nuclide,"(n,gamma)",fission +U234,23.518634203050645,0.49531930067650976 +U235,10.621118186344665,49.10955932965905 +U238,0.8652742788116043,0.10579281644765708 +U236,9.095623870006154,0.32315392339237936 +O16,0.0029535689693132015,0.0 +O17,0.05980775766572907,0.0 diff --git a/tests/regression_tests/deplete/no_transport_deplete/test.py b/tests/regression_tests/deplete/no_transport_deplete/test.py new file mode 100644 index 000000000..3fb2bf7f2 --- /dev/null +++ b/tests/regression_tests/deplete/no_transport_deplete/test.py @@ -0,0 +1,106 @@ +""" Full system test suite. """ + +from math import floor +import shutil +from pathlib import Path + +from difflib import unified_diff +import numpy as np +import pytest +import openmc +from openmc.data import JOULE_PER_EV +import openmc.deplete +from openmc.deplete import FluxDepletionOperator + + +from tests.regression_tests import config + + +@pytest.fixture(scope="module") +def vol_nuc(): + fuel = openmc.Material(name="uo2") + fuel.add_element("U", 1, percent_type="ao", enrichment=4.25) + fuel.add_element("O", 2) + fuel.set_density("g/cc", 10.4) + + fuel.volume = np.pi * 0.42 ** 2 + + nuclides = {} + for nuc, t in fuel.get_nuclide_atom_densities().items(): + nuclides[nuc] = t[1] * 1e24 + + # Load geometry from example + return (fuel.volume, nuclides) + + +@pytest.mark.parametrize("multiproc", [True, False]) +def test_full(run_in_tmpdir, vol_nuc, multiproc): + """Full system test suite. + + Runs an OpenMC depletion calculation and verifies + that the outputs match a reference file. Sensitive to changes in + OpenMC. + + """ + + # Create operator + micro_xs_file = Path(__file__).parents[3] / 'micro_xs_simple.csv' + micro_xs = FluxDepletionOperator.create_micro_xs_from_csv(micro_xs_file) + chain_file = Path(__file__).parents[3] / 'chain_simple.xml' + flux = 1164719970082145.0 # flux from pincell example + op = FluxDepletionOperator(vol_nuc[0], vol_nuc[1], micro_xs, flux, chain_file, dilute_initial=0.0) + + # Power and timesteps + dt = [30] # single step + power = 174 # W/cm + + # Perform simulation using the predictor algorithm + openmc.deplete.pool.USE_MULTIPROCESSING = multiproc + openmc.deplete.PredictorIntegrator(op, dt, power, timestep_units='d').integrate() + + # Get path to test and reference results + path_test = op.output_dir / 'depletion_results.h5' + path_reference = Path(__file__).with_name('test_reference.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_test = openmc.deplete.Results(path_test) + res_ref = openmc.deplete.Results(path_reference) + + # Assert same mats + for mat in res_ref[0].mat_to_ind: + assert mat in res_test[0].mat_to_ind, \ + "Material {} not in new results.".format(mat) + for nuc in res_ref[0].nuc_to_ind: + assert nuc in res_test[0].nuc_to_ind, \ + "Nuclide {} not in new results.".format(nuc) + + for mat in res_test[0].mat_to_ind: + assert mat in res_ref[0].mat_to_ind, \ + "Material {} not in old results.".format(mat) + for nuc in res_test[0].nuc_to_ind: + assert nuc in res_ref[0].nuc_to_ind, \ + "Nuclide {} not in old results.".format(nuc) + + tol = 1.0e-6 + for mat in res_test[0].mat_to_ind: + for nuc in res_test[0].nuc_to_ind: + _, y_test = res_test.get_atoms(mat, nuc) + _, y_old = res_ref.get_atoms(mat, nuc) + + # Test each point + correct = True + for i, ref in enumerate(y_old): + if ref != y_test[i]: + if ref != 0.0: + correct = np.abs(y_test[i] - ref) / ref <= tol + else: + correct = False + + assert correct, "Discrepancy in mat {} and nuc {}\n{}\n{}".format( + mat, nuc, y_old, y_test) + diff --git a/tests/regression_tests/deplete/test_reference.h5 b/tests/regression_tests/deplete/test_reference.h5 deleted file mode 100644 index d478d628ef662ac0ccb996ac9aa26a241df084e1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 166224 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≫yzV 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 Date: Thu, 7 Jul 2022 15:26:26 -0500 Subject: [PATCH 037/131] update docstring for no-transport depletion test --- .../regression_tests/deplete/no_transport_deplete/test.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/regression_tests/deplete/no_transport_deplete/test.py b/tests/regression_tests/deplete/no_transport_deplete/test.py index 3fb2bf7f2..51402dd60 100644 --- a/tests/regression_tests/deplete/no_transport_deplete/test.py +++ b/tests/regression_tests/deplete/no_transport_deplete/test.py @@ -35,11 +35,10 @@ def vol_nuc(): @pytest.mark.parametrize("multiproc", [True, False]) def test_full(run_in_tmpdir, vol_nuc, multiproc): - """Full system test suite. + """Transport free system test suite. - Runs an OpenMC depletion calculation and verifies - that the outputs match a reference file. Sensitive to changes in - OpenMC. + Runs an OpenMC transport-free depletion calculation and verifies + that the outputs match a reference file. """ From bbec47e1e5fb2c1911b2efaaddce80f2eb2e19ff Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 7 Jul 2022 15:29:05 -0500 Subject: [PATCH 038/131] move the depelteion regression tests to their own directories to avoid collection error --- .../no_transport_deplete => deplete_no_transport}/test.py | 4 ++-- .../example_geometry.py | 0 .../last_step_reference_materials.xml | 0 .../transport_deplete => deplete_with_transport}/test.py | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename tests/regression_tests/{deplete/no_transport_deplete => deplete_no_transport}/test.py (96%) rename tests/regression_tests/{deplete/transport_deplete => deplete_with_transport}/example_geometry.py (100%) rename tests/regression_tests/{deplete/transport_deplete => deplete_with_transport}/last_step_reference_materials.xml (100%) rename tests/regression_tests/{deplete/transport_deplete => deplete_with_transport}/test.py (98%) diff --git a/tests/regression_tests/deplete/no_transport_deplete/test.py b/tests/regression_tests/deplete_no_transport/test.py similarity index 96% rename from tests/regression_tests/deplete/no_transport_deplete/test.py rename to tests/regression_tests/deplete_no_transport/test.py index 51402dd60..98ce0facd 100644 --- a/tests/regression_tests/deplete/no_transport_deplete/test.py +++ b/tests/regression_tests/deplete_no_transport/test.py @@ -43,9 +43,9 @@ def test_full(run_in_tmpdir, vol_nuc, multiproc): """ # Create operator - micro_xs_file = Path(__file__).parents[3] / 'micro_xs_simple.csv' + micro_xs_file = Path(__file__).parents[2] / 'micro_xs_simple.csv' micro_xs = FluxDepletionOperator.create_micro_xs_from_csv(micro_xs_file) - chain_file = Path(__file__).parents[3] / 'chain_simple.xml' + chain_file = Path(__file__).parents[2] / 'chain_simple.xml' flux = 1164719970082145.0 # flux from pincell example op = FluxDepletionOperator(vol_nuc[0], vol_nuc[1], micro_xs, flux, chain_file, dilute_initial=0.0) diff --git a/tests/regression_tests/deplete/transport_deplete/example_geometry.py b/tests/regression_tests/deplete_with_transport/example_geometry.py similarity index 100% rename from tests/regression_tests/deplete/transport_deplete/example_geometry.py rename to tests/regression_tests/deplete_with_transport/example_geometry.py diff --git a/tests/regression_tests/deplete/transport_deplete/last_step_reference_materials.xml b/tests/regression_tests/deplete_with_transport/last_step_reference_materials.xml similarity index 100% rename from tests/regression_tests/deplete/transport_deplete/last_step_reference_materials.xml rename to tests/regression_tests/deplete_with_transport/last_step_reference_materials.xml diff --git a/tests/regression_tests/deplete/transport_deplete/test.py b/tests/regression_tests/deplete_with_transport/test.py similarity index 98% rename from tests/regression_tests/deplete/transport_deplete/test.py rename to tests/regression_tests/deplete_with_transport/test.py index caf292936..6c431fb33 100644 --- a/tests/regression_tests/deplete/transport_deplete/test.py +++ b/tests/regression_tests/deplete_with_transport/test.py @@ -52,7 +52,7 @@ def test_full(run_in_tmpdir, problem, multiproc): model = openmc.Model(geometry=geometry, settings=settings) # Create operator - chain_file = Path(__file__).parents[3] / 'chain_simple.xml' + chain_file = Path(__file__).parents[2] / 'chain_simple.xml' op = openmc.deplete.Operator(model, chain_file) op.round_number = True From 2053b6cf1ded5f53b9e5f38ef9e9daa0b3140dc6 Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 7 Jul 2022 16:00:47 -0500 Subject: [PATCH 039/131] update docstrings and comments in no transport regression test --- tests/regression_tests/deplete_no_transport/test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/regression_tests/deplete_no_transport/test.py b/tests/regression_tests/deplete_no_transport/test.py index 98ce0facd..3641edf58 100644 --- a/tests/regression_tests/deplete_no_transport/test.py +++ b/tests/regression_tests/deplete_no_transport/test.py @@ -1,4 +1,4 @@ -""" Full system test suite. """ +""" Transport-free depletion test suite """ from math import floor import shutil @@ -34,7 +34,7 @@ def vol_nuc(): @pytest.mark.parametrize("multiproc", [True, False]) -def test_full(run_in_tmpdir, vol_nuc, multiproc): +def test_no_transport(run_in_tmpdir, vol_nuc, multiproc): """Transport free system test suite. Runs an OpenMC transport-free depletion calculation and verifies From ff044e7b1346f65b83a5b0902bea78f96da25bd3 Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 7 Jul 2022 16:01:28 -0500 Subject: [PATCH 040/131] added __init__.py files to deplete regression test modules --- tests/regression_tests/deplete_no_transport/__init__.py | 0 tests/regression_tests/deplete_with_transport/__init__.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/regression_tests/deplete_no_transport/__init__.py create mode 100644 tests/regression_tests/deplete_with_transport/__init__.py diff --git a/tests/regression_tests/deplete_no_transport/__init__.py b/tests/regression_tests/deplete_no_transport/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/deplete_with_transport/__init__.py b/tests/regression_tests/deplete_with_transport/__init__.py new file mode 100644 index 000000000..e69de29bb From 3e5f47b0be136a3b29b9b029dfe0f993d29a748b Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 7 Jul 2022 16:22:30 -0500 Subject: [PATCH 041/131] Added references to the new class in the docs --- docs/source/pythonapi/deplete.rst | 10 +++--- docs/source/usersguide/depletion.rst | 51 +++++++++++++++++++++++----- 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/docs/source/pythonapi/deplete.rst b/docs/source/pythonapi/deplete.rst index 9f7d8c447..151e728d5 100644 --- a/docs/source/pythonapi/deplete.rst +++ b/docs/source/pythonapi/deplete.rst @@ -40,18 +40,18 @@ transport-depletion coupling algorithms `_. SICELIIntegrator SILEQIIntegrator -Each of these classes expects a "transport operator" to be passed. An operator -specific to OpenMC is available using the following class: - +Each of these classes expects a "transport operator" to be passed. .. autosummary:: :toctree: generated :nosignatures: :template: mycallable.rst Operator + FluxDepletionOperator -The :class:`Operator` must also have some knowledge of how nuclides transmute -and decay. This is handled by the :class:`Chain`. +The :class:`Operator` and :class:`FluxDepletionOperator` classes must also have +some knowledge of how nuclides transmute and decay. This is handled by the +:class:`Chain`. Minimal Example --------------- diff --git a/docs/source/usersguide/depletion.rst b/docs/source/usersguide/depletion.rst index 57619611f..f4f0ac869 100644 --- a/docs/source/usersguide/depletion.rst +++ b/docs/source/usersguide/depletion.rst @@ -19,14 +19,13 @@ transmutation equations and the method used for advancing time. At present, the :class:`openmc.deplete.Operator` (which uses the OpenMC transport solver), but in principle additional operator classes based on other transport codes could be implemented and no changes to the depletion solver itself would be needed. The -operator class requires a :class:`openmc.Geometry` instance and a -:class:`openmc.Settings` instance:: +operator class requires a :class:`openmc.model.Model` instance containing +material, geometry, and settings information:: - geom = openmc.Geometry() - settings = openmc.Settings() + model = openmc.model.Model() ... - op = openmc.deplete.Operator(geom, settings) + op = openmc.deplete.Operator(model) Any material that contains a fissionable nuclide is depleted by default, but this can behavior can be changed with the :attr:`Material.depletable` attribute. @@ -81,7 +80,7 @@ When constructing the :class:`~openmc.deplete.Operator`, you should indicate that normalization of tally results will be done based on the source rate rather than a power or power density:: - op = openmc.deplete.Operator(geometry, settings, normalization_mode='source-rate') + op = openmc.deplete.Operator(model, normalization_mode='source-rate') Finally, when creating a depletion integrator, use the ``source_rates`` argument:: @@ -127,7 +126,7 @@ A more complete way to model the energy deposition is to use the modified heating reactions described in :ref:`methods_heating`. These values can be used to normalize reaction rates instead of using the fission reaction rates with:: - op = openmc.deplete.Operator(geometry, settings, "chain.xml", + op = openmc.deplete.Operator(model, "chain.xml", normalization_mode="energy-deposition") These modified heating libraries can be generated by running the latest version @@ -160,7 +159,7 @@ the next transport step. This can be countered by instructing the operator to treat repeated instances of the same material as a unique material definition with:: - op = openmc.deplete.Operator(geometry, settings, chain_file, + op = openmc.deplete.Operator(model, chain_file, diff_burnable_mats=True) For our example problem, this would deplete fuel on the outer region of the @@ -177,3 +176,39 @@ across all material instances. This will increase the total memory usage and run time due to an increased number of tallies and material definitions. +Transport-independent depletion +------------------------------- + +.. note:: + + This is a brand-new feature and is under heavy development. API changes are + possible and likely in the near future. + +OpenMC also supports transport-independent depletion calculations using the +:class:`FluxDepletionOperator` class. Rather than taking a +:class:`openmc.model.Model` object, this class accepts a volume, +a dictionary of nuclide concentrations, a flux spectra, and one-group +microscopic cross sections as a pandas dataframe. The class includes +helper functions to constructe the dataframe from a csv file or from +data arrays:: + + ... + micro_xs = FluxDepletionOperator.create_micro_xs_from_csv(micro_xs_path) + nuclides = {'U234':8.92e18, + 'U235':9.98e20, + 'U238':2.22e22, + 'U236':4.57e18, + 'O16':4.64e22, + 'O17':1.76e19} + volume = 0.5 + flux = 1.16e15 + + op = FluxDepletionOperator(volume, nuclides, micro_xs, flux. chain_file) + + +A user can then define an integrator class as they would for a coupled +transport-depletion calculation and follow the steps from there. +present in the depletion chain. + +.. note:: Ideally, one-group cross section data should be available for every reaction + in the depletion chain. From c91a6d65d880666565414c3b43776d9cf65be3d3 Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 7 Jul 2022 16:35:51 -0500 Subject: [PATCH 042/131] make import of example_geometry relative to fix pytest error --- tests/regression_tests/deplete_with_transport/test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/regression_tests/deplete_with_transport/test.py b/tests/regression_tests/deplete_with_transport/test.py index 6c431fb33..0c2a4cd97 100644 --- a/tests/regression_tests/deplete_with_transport/test.py +++ b/tests/regression_tests/deplete_with_transport/test.py @@ -12,7 +12,7 @@ from openmc.data import JOULE_PER_EV import openmc.deplete from tests.regression_tests import config -from example_geometry import generate_problem +from .example_geometry import generate_problem @pytest.fixture(scope="module") From c67f889d99aa2dadbcf8016ac9c4733145a910ae Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 7 Jul 2022 16:44:09 -0500 Subject: [PATCH 043/131] typo fix in depete api page --- docs/source/pythonapi/deplete.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/pythonapi/deplete.rst b/docs/source/pythonapi/deplete.rst index 151e728d5..32b449a13 100644 --- a/docs/source/pythonapi/deplete.rst +++ b/docs/source/pythonapi/deplete.rst @@ -41,6 +41,7 @@ transport-depletion coupling algorithms `_. SILEQIIntegrator Each of these classes expects a "transport operator" to be passed. + .. autosummary:: :toctree: generated :nosignatures: From 1c9a5be73835bb00805b8d194dc69a5474e03ff0 Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 7 Jul 2022 17:44:59 -0500 Subject: [PATCH 044/131] add test_reference.h5 files --- .../deplete_no_transport/test_reference.h5 | Bin 0 -> 35688 bytes .../deplete_with_transport/test_reference.h5 | Bin 0 -> 166224 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/regression_tests/deplete_no_transport/test_reference.h5 create mode 100644 tests/regression_tests/deplete_with_transport/test_reference.h5 diff --git a/tests/regression_tests/deplete_no_transport/test_reference.h5 b/tests/regression_tests/deplete_no_transport/test_reference.h5 new file mode 100644 index 0000000000000000000000000000000000000000..e4fbf5029d56bd9169e688fb477bc7e6c0688d9e GIT binary patch literal 35688 zcmeHPU1(fI6rSBp>b6Erv}&w>D;ABl+O0-if0}IFRJSiS7^RCy-OXlew&sug)QEzL zKSTu;1nHZ3j4zr8g{mNdqU6C!qX_X$txBGxAktEiI&;tYa~o#zGsh8L52DuIa`fwpiqnxQoAXT=D3LS8-e7W-L0mFzc62_jKfp&z-lfv4E*jF)nwJ+N{eR?(G`t)~lDgNHgE7xN5vuHJ-El zZ1$!0y;EQP+U&1Ptcp46TEE!-ccFbm!{t}$vFrTjh;v%dk&4&$S+j(~pO6@~gT@6JLdfg%WC{H8b$^WX`y zIh=E0KE(K*lT}ub=H05z8J?- z*HC(dgoZ1YCY6$>Rv6!1I;zJK&4$H>RV`6Rg;^r51J@rudf;%m;6ap^3j9KftO7X>BM=qByeqU|A z6n`HYjZlfBTu|0yK7taOkARCW(>E6CR*El2*uNj;_Lj>Rygu^l+3f9u`g55qz2?je z=H~2#T>Buu_lx7**v(Wzc;_Q0AvXdp-nH^LLdi<;uJ$HsAiWOa2wTJEJ}_2gZ5HyT|njhc}$ zBU3q2Fw&HReQA?ffeYvPnBJ<7_mgPX-d{%9hM5Qjpl7d#z0PQrG|$)2v*+(PRimbP z9SS|$-y~Ttd^-i87v*@XbzfP1)db#;*!C>fPrB9>^jyk)J~-`-D*v3rssSMltqTNf z{}*Aa`uN-ojBM!H{%oeZJ98T+9Lx`R=jXvM@EH0FNqJtn5JDGrzCA9n{|d9e0y-Ff zD>X0EJQtjOYZkN4%}g>cX1rP1_nY-*($*L3iE})CKc!!2znJH2yvjYV>LXnoN!PiV zX~tjn1I2NZ)ItPYe&-d7$Kd%&`5o}Z_8Uv(3+!5$FZ7%j4lAI)k26=GifX`m67@lD zWVrCXC(!KJRq zbde%gw|DLwL%v}E2T+iW5*AN#S%$u(b zlp5$A>d)q$Fd-8 zlWEHQS@$JPxfTAMj|U+b2oeJ5CnRt{eChqK_j(ixhK9STZ&u%jE8OGK)h`wgLdXA( z4)JB+G5)-dhpCP^y58`h`)7aujB!zZ|ARBH`u(5dpGHdGuvN~*dHFcEn|iRc0mLSK zzT4U#*x#LWay2pi#Z;*)&6y2NrL@+6p4rz2w9CV|flMyZ^!m4{0Lp=f3sp10<)DXh zw!}Im3`LQGfFK|U2m*qDARq_`0)l`bAP5KoYl47l9~mvvnl$eVQ6Brj#a0*Q|ECCj zl((h*oLBajKun|{AP5Kof`A|(2nYg#fFK|U2m*qDAW(S(T+b6~7uodt{QLm=*!R1N z#};^AfO9tTe87dEqzeLqfFK|U2m*qDARq_`0&9mrx%-FFMeZAtj{Sn{6V~pYOUr_Q zARq_`0)l`bAPAI$fNR~j&~x8$UN=I|^}IRdST{l+<;$^M$_N62fFK|U2m*qDARq{= z9s)~UHy&MJy_j~a6J>q4diPXX5(ESRK|l}?1lAgXpATg}{^ZuF#23e!o9_E>SIehu zt>29P^m@zpXL~37 zIkGLg<-NalQ9sVSTqr5_%D0K#UT7j0H*$OE^XA{5oBr{5s3Y-ZTjP$`4)i1rp8PZW z&M(IjPwczt&f_0`mq^`^dau1bk$7>UKED5tCle2TbnSINUptjJ+4l5b*Z+NQ%Tph` ZdF8u5xa~}^omsZ?bO}3QAEcUN=l?V|d4T`` literal 0 HcmV?d00001 diff --git a/tests/regression_tests/deplete_with_transport/test_reference.h5 b/tests/regression_tests/deplete_with_transport/test_reference.h5 new file mode 100644 index 0000000000000000000000000000000000000000..d478d628ef662ac0ccb996ac9aa26a241df084e1 GIT binary patch literal 166224 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≫yzV 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 Date: Thu, 7 Jul 2022 20:35:09 -0500 Subject: [PATCH 045/131] test suite typo fixes --- tests/regression_tests/deplete_no_transport/test.py | 4 ++-- tests/unit_tests/test_deplete_resultslist.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/regression_tests/deplete_no_transport/test.py b/tests/regression_tests/deplete_no_transport/test.py index 3641edf58..1133a039c 100644 --- a/tests/regression_tests/deplete_no_transport/test.py +++ b/tests/regression_tests/deplete_no_transport/test.py @@ -26,8 +26,8 @@ def vol_nuc(): fuel.volume = np.pi * 0.42 ** 2 nuclides = {} - for nuc, t in fuel.get_nuclide_atom_densities().items(): - nuclides[nuc] = t[1] * 1e24 + for nuc, dens in fuel.get_nuclide_atom_densities().items(): + nuclides[nuc] = dens * 1e24 # Load geometry from example return (fuel.volume, nuclides) diff --git a/tests/unit_tests/test_deplete_resultslist.py b/tests/unit_tests/test_deplete_resultslist.py index 6ce15601f..9d81c4a15 100644 --- a/tests/unit_tests/test_deplete_resultslist.py +++ b/tests/unit_tests/test_deplete_resultslist.py @@ -11,7 +11,7 @@ import openmc.deplete @pytest.fixture def res(): """Load the reference results""" - filename = (Path(__file__).parents[1] / 'regression_tests' / 'deplete' + filename = (Path(__file__).parents[1] / 'regression_tests' / 'deplete_with_transport' / 'test_reference.h5') return openmc.deplete.Results(filename) From 5f8e1ff9d99f7dea76c097cf91f78344c9e3633d Mon Sep 17 00:00:00 2001 From: yardasol Date: Tue, 12 Jul 2022 11:44:12 -0500 Subject: [PATCH 046/131] Address paulromano's comments - various syntax adjustments and cleanups - remove unneeded import statements - add more information to new section in user's guide - typo fixes - remove dilute_initial - move _validate_micro_xs_inputs back into the class as a static method - update tests to reflect changes --- docs/source/usersguide/depletion.rst | 34 ++++---- openmc/deplete/flux_operator.py | 74 ++++++----------- openmc/deplete/helpers.py | 79 +++++++++++++++++++ .../unit_tests/test_flux_deplete_operator.py | 2 +- 4 files changed, 123 insertions(+), 66 deletions(-) diff --git a/docs/source/usersguide/depletion.rst b/docs/source/usersguide/depletion.rst index f4f0ac869..fe3f12dc4 100644 --- a/docs/source/usersguide/depletion.rst +++ b/docs/source/usersguide/depletion.rst @@ -181,25 +181,25 @@ Transport-independent depletion .. note:: - This is a brand-new feature and is under heavy development. API changes are + This feature is still under heavy development. API changes are possible and likely in the near future. -OpenMC also supports transport-independent depletion calculations using the -:class:`FluxDepletionOperator` class. Rather than taking a -:class:`openmc.model.Model` object, this class accepts a volume, +OpenMC supports running depletion calculations independent of the OpenMC +transport solver using the :class:`FluxDepletionOperator` class. Rather than +taking a :class:`openmc.model.Model` object, this class accepts a volume, a dictionary of nuclide concentrations, a flux spectra, and one-group -microscopic cross sections as a pandas dataframe. The class includes -helper functions to constructe the dataframe from a csv file or from +microscopic cross sections as a :class:`pandas.DataFrame`. The class includes +helper functions to construct the dataframe from a csv file or from data arrays:: ... micro_xs = FluxDepletionOperator.create_micro_xs_from_csv(micro_xs_path) - nuclides = {'U234':8.92e18, - 'U235':9.98e20, - 'U238':2.22e22, - 'U236':4.57e18, - 'O16':4.64e22, - 'O17':1.76e19} + nuclides = {'U234': 8.92e18, + 'U235': 9.98e20, + 'U238': 2.22e22, + 'U236': 4.57e18, + 'O16': 4.64e22, + 'O17': 1.76e19} volume = 0.5 flux = 1.16e15 @@ -207,8 +207,10 @@ data arrays:: A user can then define an integrator class as they would for a coupled -transport-depletion calculation and follow the steps from there. -present in the depletion chain. +transport-depletion calculation and follow the same steps from there. -.. note:: Ideally, one-group cross section data should be available for every reaction - in the depletion chain. +.. note:: Ideally, one-group cross section data should be available for every + reaction in the depletion chain. If a nuclide that has a reaction + associated with it in the depletion chain is present in the `nuclides` + parameter but not the cross section data, that reaction will not be + simulated. diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/flux_operator.py index 1da7cf9f9..562967e6c 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/flux_operator.py @@ -6,36 +6,22 @@ and one-group cross sections. """ -import copy -from collections import OrderedDict -import os from warnings import warn import numpy as np import pandas as pd from uncertainties import ufloat -import openmc from openmc.checkvalue import check_type, check_value, check_iterable_type -from openmc.data import DataLibrary -from openmc.exceptions import DataError from openmc.mpi import comm from .abc import TransportOperator, OperatorResult from .atom_number import AtomNumber -from .chain import _find_chain_file, REACTIONS +from .chain import REACTIONS from .reaction_rates import ReactionRates -from .results import Results from .helpers import ConstantFissionYieldHelper -valid_rxns = list(REACTIONS.keys()) -valid_rxns += ['fission'] - -# Convenience function for the micro_xs static methods -def _validate_micro_xs_inputs(nuclides, reactions, data): - check_iterable_type('nuclides', nuclides, str) - check_iterable_type('reactions', reactions, str) - check_type('data', data, np.ndarray, expected_iter_type=float) - [check_value('reactions', reaction, valid_rxns) for reaction in reactions] +valid_rxns = list(REACTIONS) +valid_rxns.append('fission') @@ -51,10 +37,10 @@ class FluxDepletionOperator(TransportOperator): Parameters ---------- volume : float - Volume of the material being depleted in cm^3 + Volume of the material being depleted in [cm^3] nuclides : dict of str to float Dictionary with nuclide names as keys and nuclide concentrations as - values. Nuclide concentration units are [at/cm^3]. + values. Nuclide concentration units are [atom/cm^3]. micro_xs : pandas.DataFrame DataFrame with nuclides names as index and microscopic cross section data in the columns. Cross section units are [cm^-2]. @@ -64,15 +50,10 @@ class FluxDepletionOperator(TransportOperator): Path to the depletion chain XML file. keff : 2-tuple of float, optional keff eigenvalue and uncertainty from transport calculation. - Defualt is None. + Default is None. fission_q : dict, optional Dictionary of nuclides and their fission Q values [eV]. If not given, values will be pulled from the ``chain_file``. - dilute_initial : float, optional - Initial atom density [atoms/cm^3] to add for nuclides that are zero - in initial condition to ensure they exist in the decay chain. - Only done for nuclides with reaction rates. - Defaults to 1.0e3. prev_results : Results, optional Results from a previous depletion calculation. reduce_chain : bool, optional @@ -86,10 +67,6 @@ class FluxDepletionOperator(TransportOperator): Attributes ---------- - dilute_initial : float - Initial atom density [atoms/cm^3] to add for nuclides that are zero - in initial condition to ensure they exist in the decay chain. - Only done for nuclides with reaction rates. round_number : bool Whether or not to round output to OpenMC to 8 digits. Useful in testing, as OpenMC is incredibly sensitive to exact values. @@ -117,12 +94,11 @@ class FluxDepletionOperator(TransportOperator): chain_file, keff=None, fission_q=None, - dilute_initial=1.0e3, prev_results=None, reduce_chain=False, reduce_chain_level=None, fission_yield_opts=None): - super().__init__(chain_file, fission_q, dilute_initial, prev_results) + super().__init__(chain_file, fission_q, 0.0, prev_results) self.round_number = False self.flux_spectra = flux_spectra self._init_nuclides = nuclides @@ -138,7 +114,7 @@ class FluxDepletionOperator(TransportOperator): check_type('micro_xs', micro_xs, pd.DataFrame) self._micro_xs = micro_xs - if not isinstance(keff, type(None)): + if keff is not None: check_type('keff', keff, tuple, float) keff = ufloat(keff) @@ -221,7 +197,6 @@ class FluxDepletionOperator(TransportOperator): for nuc, i_nuc_results in zip(rxn_nuclides, nuc_ind): number[i_nuc_results] = self.number[0, nuc] - # Calculate macroscopic cross sections and store them in rates array for nuc in rxn_nuclides: density = self.number.get_atom_density('0', nuc) @@ -230,8 +205,7 @@ class FluxDepletionOperator(TransportOperator): '0', nuc, rxn, - self._micro_xs[rxn].loc[nuc] * - density) + self._micro_xs[rxn, rxn] * density) # Get reaction rate in reactions/sec rates *= self.flux_spectra @@ -334,15 +308,13 @@ class FluxDepletionOperator(TransportOperator): """ # Validate inputs - try: - assert data.shape == (len(nuclides), len(reactions)) - except AssertionError: - raise SyntaxError( + if data.shape != (len(nuclides), len(reactions)): + raise ValueError( f'Nuclides list of length {len(nuclides)} and ' f'reactions array of length {len(reactions)} do not ' f'match dimensions of data array of shape {data.shape}') - _validate_micro_xs_inputs(nuclides, reactions, data) + FluxDepletionOperator._validate_micro_xs_inputs(nuclides, reactions, data) # Convert to cm^2 if units == 'barn': @@ -371,7 +343,7 @@ class FluxDepletionOperator(TransportOperator): """ micro_xs = pd.read_csv(csv_file, index_col=0) - _validate_micro_xs_inputs(list(micro_xs.index), + FluxDepletionOperator._validate_micro_xs_inputs(list(micro_xs.index), list(micro_xs.columns), micro_xs.to_numpy()) @@ -380,6 +352,16 @@ class FluxDepletionOperator(TransportOperator): return micro_xs + # Convenience function for the micro_xs static methods + @staticmethod + def _validate_micro_xs_inputs(nuclides, reactions, data): + check_iterable_type('nuclides', nuclides, str) + check_iterable_type('reactions', reactions, str) + check_type('data', data, np.ndarray, expected_iter_type=float) + for reaction in reactions: + check_value('reactions', reaction, valid_rxns) + + def _update_materials(self): """Updates material compositions in OpenMC on all processes.""" @@ -464,7 +446,7 @@ class FluxDepletionOperator(TransportOperator): return [nuc for nuc in nuc_list if nuc in self.chain] def _get_all_nuclides_in_simulation(self): - """Determine nuclides that will show up in the simulation. + """Determine nuclides that will show up in the depletion matrix. This is the union of the nuclides provided by the user and the nuclides present in the depletion chain. @@ -487,9 +469,8 @@ class FluxDepletionOperator(TransportOperator): def _get_nuclides_with_data(self): """Finds nuclides with cross section data""" - nuclides_with_data = set(self._micro_xs.index) + return set(self._micro_xs.index) - return nuclides_with_data def _extract_number(self, local_mats, volume, nuclides, prev_res=None): """Construct AtomNumber using geometry @@ -508,11 +489,6 @@ class FluxDepletionOperator(TransportOperator): """ self.number = AtomNumber(local_mats, nuclides, volume, len(self.chain)) - if self.dilute_initial != 0.0: - for nuc in self._burnable_nucs: - self.number.set_atom_density( - np.s_[:], nuc, self.dilute_initial) - # Now extract and store the number densities # From the geometry if no previous depletion results if prev_res is None: diff --git a/openmc/deplete/helpers.py b/openmc/deplete/helpers.py index 33bd182d7..5adb43998 100644 --- a/openmc/deplete/helpers.py +++ b/openmc/deplete/helpers.py @@ -2,6 +2,7 @@ Class for normalizing fission energy deposition """ import bisect +import pandas as pd from abc import abstractmethod from collections import defaultdict from copy import deepcopy @@ -124,6 +125,84 @@ class TalliedFissionYieldHelper(FissionYieldHelper): # ------------------------------------- +class FluxReactionRateHelper(ReactionRateHelper): + """Class for generating one-group reaction rates with flux and + one-group cross sections. + + This class does not generate tallies, and instead stores cross sections + for each nuclides and transmutation reaction relevant for a depletion + calculation. + + Parameters + ---------- + n_nucs : int + Number of burnable nuclides tracked by :class:`openmc.deplete.Operator` + n_react : int + Number of reactions tracked by :class:`openmc.deplete.Operator` + + Attributes + ---------- + flux : + + xs : + + + """ + def __init__(self, n_nuc, n_react): + super().__init__(n_nuc, n_react) + self._flux = None + self._micro_xs = None + + def generate_tallies(self, materials, scores): + """Unused in this case""" + + + @property + def flux(self): + """Flux in n cm^-2 s^-1""" + return self._flux + + @flux.setter + def flux(self, flux): + check_type("flux", flux, float) + self._flux = flux + + @property + def micro_xs(self): + """DataFrame of microscopic cross sections with requested reaction + for all nuclides""" + return self._micro_xs + + @micro_xs.setter + def micro_xs(self, micro_xs): + # TODO : validate micro_xs + self._micro_xs = micro_xs + + def get_material_rates(self, mat_id, nuc_index, react_index): + """Return 2D array of [nuclide, reaction] reaction rates + + Parameters + ---------- + mat_id : int + Unique ID for the requested material + nuc_index : list of str + Ordering of desired nuclides + react_index : list of str + Ordering of reactions + """ + self._results_cache.fill(0.0) + full_tally_res = self._rate_tally.mean[mat_id] + for i_tally, (i_nuc, i_react) in enumerate( + product(nuc_index, react_index)): + self._results_cache[i_nuc, i_react] = full_tally_res[i_tally] + + return self._results_cache + + + + + + class DirectReactionRateHelper(ReactionRateHelper): """Class for generating one-group reaction rates with direct tallies diff --git a/tests/unit_tests/test_flux_deplete_operator.py b/tests/unit_tests/test_flux_deplete_operator.py index a2b150493..99c4e7c8a 100644 --- a/tests/unit_tests/test_flux_deplete_operator.py +++ b/tests/unit_tests/test_flux_deplete_operator.py @@ -48,7 +48,7 @@ def test_create_micro_xs_from_data_array(): FluxDepletionOperator.create_micro_xs_from_data_array( nuclides, reactions, data) - with pytest.raises(SyntaxError, match=r'Nuclides list of length \d* and ' + with pytest.raises(ValueError, match=r'Nuclides list of length \d* and ' r'reactions array of length \d* do not ' r'match dimensions of data array of shape \(\d*\,d*\)'): FluxDepletionOperator.create_micro_xs_from_data_array( From 5b4453c3a9a15a6f0b1e47c61f40194ef1bd4db0 Mon Sep 17 00:00:00 2001 From: yardasol Date: Tue, 12 Jul 2022 15:39:29 -0500 Subject: [PATCH 047/131] remove dilute_inital from regression test --- tests/regression_tests/deplete_no_transport/test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/regression_tests/deplete_no_transport/test.py b/tests/regression_tests/deplete_no_transport/test.py index 1133a039c..cdc8c057c 100644 --- a/tests/regression_tests/deplete_no_transport/test.py +++ b/tests/regression_tests/deplete_no_transport/test.py @@ -47,7 +47,7 @@ def test_no_transport(run_in_tmpdir, vol_nuc, multiproc): micro_xs = FluxDepletionOperator.create_micro_xs_from_csv(micro_xs_file) chain_file = Path(__file__).parents[2] / 'chain_simple.xml' flux = 1164719970082145.0 # flux from pincell example - op = FluxDepletionOperator(vol_nuc[0], vol_nuc[1], micro_xs, flux, chain_file, dilute_initial=0.0) + op = FluxDepletionOperator(vol_nuc[0], vol_nuc[1], micro_xs, flux, chain_file) # Power and timesteps dt = [30] # single step From f5db4e3afb41ff1b56639ccf157e0256b0b5a19f Mon Sep 17 00:00:00 2001 From: yardasol Date: Tue, 12 Jul 2022 18:30:29 -0500 Subject: [PATCH 048/131] move reuasble code to new parent class; 1st pass - new class OpenMCOperator to contain shared bits of code between the currently existing Operator class and the as-of-yet to be created FluxDepletionOperator class - Operator is now a subclass of OpenMCOperator - move _distribute method to OpenMCOperator - move _get_burnable_mats, _extract_number, _set_number_from_mat, _set_number_from_results to OpenMCOperator - split initial condition into openmc.lib dependent and non-dependent parts; dependent part goes to Operator, non-dependent part goes to OpenMCOperator - move _update_materials to OpenMCOperator - move _get_tally_nuclides to OpenMCOperator - rename _unpack_tallies_and_normalize to _calculate_reaction_rates; move to OpenMCOperator - move get_results_info to OpenMCOperator --- openmc/deplete/__init__.py | 1 + openmc/deplete/openmc_operator.py | 522 ++++++++++++++++++++++++++++++ openmc/deplete/operator.py | 331 +------------------ 3 files changed, 535 insertions(+), 319 deletions(-) create mode 100644 openmc/deplete/openmc_operator.py diff --git a/openmc/deplete/__init__.py b/openmc/deplete/__init__.py index 5891555a2..95264308c 100644 --- a/openmc/deplete/__init__.py +++ b/openmc/deplete/__init__.py @@ -7,6 +7,7 @@ A depletion front-end tool. from .nuclide import * from .chain import * +from .openmc_operator import * from .operator import * from .reaction_rates import * from .atom_number import * diff --git a/openmc/deplete/openmc_operator.py b/openmc/deplete/openmc_operator.py new file mode 100644 index 000000000..303a5208f --- /dev/null +++ b/openmc/deplete/openmc_operator.py @@ -0,0 +1,522 @@ +"""OpenMC transport operator + +This module implements a transport operator for OpenMC so that it can be used by +depletion integrators. The implementation makes use of the Python bindings to +OpenMC's C API so that reading tally results and updating material number +densities is all done in-memory instead of through the filesystem. + +""" + +import copy +from abc import abstractmethod +from collections import OrderedDict +import os +from warnings import warn + +import numpy as np + +import openmc +from openmc.exceptions import DataError +from openmc.mpi import comm +from .abc import TransportOperator, OperatorResult +from .atom_number import AtomNumber +from .reaction_rates import ReactionRates +from .results import Results +from .helpers import ( + DirectReactionRateHelper, ChainFissionHelper, ConstantFissionYieldHelper, + FissionYieldCutoffHelper, AveragedFissionYieldHelper, EnergyScoreHelper, + SourceRateHelper, FluxCollapseHelper) + + +__all__ = ["OpenMCOperator", "OperatorResult"] + + +def _distribute(items): + """Distribute items across MPI communicator + + Parameters + ---------- + items : list + List of items of distribute + + Returns + ------- + list + Items assigned to process that called + + """ + min_size, extra = divmod(len(items), comm.size) + j = 0 + for i in range(comm.size): + chunk_size = min_size + int(i < extra) + if comm.rank == i: + return items[j:j + chunk_size] + j += chunk_size + +class OpenMCOperator(TransportOperator): + """OpenMC transport operator for depletion. + + Instances of this class can be used to perform depletion using OpenMC as the + transport operator. Normally, a user needn't call methods of this class + directly. Instead, an instance of this class is passed to an integrator + class, such as :class:`openmc.deplete.CECMIntegrator`. + + .. versionchanged:: 0.13.0 + The geometry and settings parameters have been replaced with a + model parameter that takes a :class:`~openmc.model.Model` object + + Parameters + ---------- + chain_file : str, optional + Path to the depletion chain XML file. Defaults to the file + listed under ``depletion_chain`` in + :envvar:`OPENMC_CROSS_SECTIONS` environment variable. + prev_results : Results, optional + Results from a previous depletion calculation. If this argument is + specified, the depletion calculation will start from the latest state + in the previous results. + fission_q : dict, optional + Dictionary of nuclides and their fission Q values [eV]. If not given, + values will be pulled from the ``chain_file``. Only applicable + if ``"normalization_mode" == "fission-q"`` + dilute_initial : float, optional + Initial atom density [atoms/cm^3] to add for nuclides that are zero + in initial condition to ensure they exist in the decay chain. + Only done for nuclides with reaction rates. + Defaults to 1.0e3. + + Attributes + ---------- + model : openmc.model.Model + OpenMC model object + geometry : openmc.Geometry + OpenMC geometry object + settings : openmc.Settings + OpenMC settings object + dilute_initial : float + Initial atom density [atoms/cm^3] to add for nuclides that + are zero in initial condition to ensure they exist in the decay + chain. Only done for nuclides with reaction rates. + output_dir : pathlib.Path + Path to output directory to save results. + round_number : bool + Whether or not to round output to OpenMC to 8 digits. + Useful in testing, as OpenMC is incredibly sensitive to exact values. + number : openmc.deplete.AtomNumber + Total number of atoms in simulation. + nuclides_with_data : set of str + A set listing all unique nuclides available from cross_sections.xml. + chain : openmc.deplete.Chain + The depletion chain information necessary to form matrices and tallies. + reaction_rates : openmc.deplete.ReactionRates + Reaction rates from the last operator step. + burnable_mats : list of str + All burnable material IDs + heavy_metal : float + Initial heavy metal inventory [g] + local_mats : list of str + All burnable material IDs being managed by a single process + prev_res : Results or None + Results from a previous depletion calculation. ``None`` if no + results are to be used. + cleanup_when_done : bool + Whether to finalize and clear the shared library memory when the + depletion operation is complete. Defaults to clearing the library. + """ + + ## modify + def __init__(self, chain_file=None, fission_q=None, dilute_initial=0.0, prev_results=None): + + super().__init__(chain_file, fission_q, dilute_initial, prev_results) + + ## clear, but we must keep the method + def __call__(self, vec, source_rate): + """Runs a simulation. + + Simulation will abort under the following circumstances: + + 1) No energy is computed using OpenMC tallies. + + Parameters + ---------- + vec : list of numpy.ndarray + Total atoms to be used in function. + source_rate : float + Power in [W] or source rate in [neutron/sec] + + Returns + ------- + openmc.deplete.OperatorResult + Eigenvalue and reaction rates resulting from transport operator + + """ + + ## clear, but must keep the method + def write_bos_data(step): + """Write a state-point file with beginning of step data + + Parameters + ---------- + step : int + Current depletion step including restarts + """ + pass + + ## keep + def _get_burnable_mats(self): + """Determine depletable materials, volumes, and nuclides + + Returns + ------- + burnable_mats : list of str + List of burnable material IDs + volume : OrderedDict of str to float + Volume of each material in [cm^3] + nuclides : list of str + Nuclides in order of how they'll appear in the simulation. + + """ + + burnable_mats = set() + model_nuclides = set() + volume = OrderedDict() + + self.heavy_metal = 0.0 + + # Iterate once through the geometry to get dictionaries + for mat in self.materials: + for nuclide in mat.get_nuclides(): + model_nuclides.add(nuclide) + if mat.depletable: + burnable_mats.add(str(mat.id)) + if mat.volume is None: + raise RuntimeError("Volume not specified for depletable " + "material with ID={}.".format(mat.id)) + volume[str(mat.id)] = mat.volume + self.heavy_metal += mat.fissionable_mass + + # Make sure there are burnable materials + if not burnable_mats: + raise RuntimeError( + "No depletable materials were found in the model.") + + # Sort the sets + burnable_mats = sorted(burnable_mats, key=int) + model_nuclides = sorted(model_nuclides) + + # Construct a global nuclide dictionary, burned first + nuclides = list(self.chain.nuclide_dict) + for nuc in model_nuclides: + if nuc not in nuclides: + nuclides.append(nuc) + + return burnable_mats, volume, nuclides + + ## keep + def _extract_number(self, local_mats, volume, nuclides, prev_res=None): + """Construct AtomNumber using geometry + + Parameters + ---------- + local_mats : list of str + Material IDs to be managed by this process + volume : OrderedDict of str to float + Volumes for the above materials in [cm^3] + nuclides : list of str + Nuclides to be used in the simulation. + prev_res : Results, optional + Results from a previous depletion calculation + + """ + self.number = AtomNumber(local_mats, nuclides, volume, len(self.chain)) + + if self.dilute_initial != 0.0: + for nuc in self._burnable_nucs: + self.number.set_atom_density(np.s_[:], nuc, self.dilute_initial) + + # Now extract and store the number densities + # From the geometry if no previous depletion results + if prev_res is None: + for mat in self.materials: + if str(mat.id) in local_mats: + self._set_number_from_mat(mat) + + # Else from previous depletion results + else: + for mat in self.materials: + if str(mat.id) in local_mats: + self._set_number_from_results(mat, prev_res) + + def _set_number_from_mat(self, mat): + """Extracts material and number densities from openmc.Material + + Parameters + ---------- + mat : openmc.Material + The material to read from + + """ + mat_id = str(mat.id) + + for nuclide, atom_per_bcm in mat.get_nuclide_atom_densities().items(): + atom_per_cc = atom_per_bcm * 1.0e24 + self.number.set_atom_density(mat_id, nuclide, atom_per_cc) + + def _set_number_from_results(self, mat, prev_res): + """Extracts material nuclides and number densities. + + If the nuclide concentration's evolution is tracked, the densities come + from depletion results. Else, densities are extracted from the geometry + in the summary. + + Parameters + ---------- + mat : openmc.Material + The material to read from + prev_res : Results + Results from a previous depletion calculation + + """ + mat_id = str(mat.id) + + # Get nuclide lists from geometry and depletion results + depl_nuc = prev_res[-1].nuc_to_ind + geom_nuc_densities = mat.get_nuclide_atom_densities() + + # Merge lists of nuclides, with the same order for every calculation + geom_nuc_densities.update(depl_nuc) + + for nuclide, atom_per_bcm in geom_nuc_densities.items(): + if nuclide in depl_nuc: + concentration = prev_res.get_atoms(mat_id, nuclide)[1][-1] + volume = prev_res[-1].volume[mat_id] + atom_per_cc = concentration / volume + else: + atom_per_cc = atom_per_bcm * 1.0e24 + + self.number.set_atom_density(mat_id, nuclide, atom_per_cc) + + ## keep, will need to modify + def initial_condition(self, materials): + """Performs final setup and returns initial condition. + + Parameters + ---------- + materials : ??? + + Returns + ------- + list of numpy.ndarray + Total density for initial conditions. + """ + + self._rate_helper.generate_tallies(materials, self.chain.reactions) + self._normalization_helper.prepare( + self.chain.nuclides, self.reaction_rates.index_nuc) + # Tell fission yield helper what materials this process is + # responsible for + self._yield_helper.generate_tallies( + materials, tuple(sorted(self._mat_index_map.values()))) + + # Return number density vector + return list(self.number.get_mat_slice(np.s_[:])) + + ## keep? there is a non-removable part of this + ## that needs the openmc c executable + def _update_materials(self): + """Updates material compositions in OpenMC on all processes.""" + + for rank in range(comm.size): + number_i = comm.bcast(self.number, root=rank) + + for mat in number_i.materials: + nuclides = [] + densities = [] + for nuc in number_i.nuclides: + if nuc in self.nuclides_with_data: + val = 1.0e-24 * number_i.get_atom_density(mat, nuc) + + # If nuclide is zero, do not add to the problem. + if val > 0.0: + if self.round_number: + val_magnitude = np.floor(np.log10(val)) + val_scaled = val / 10**val_magnitude + val_round = round(val_scaled, 8) + + val = val_round * 10**val_magnitude + + nuclides.append(nuc) + densities.append(val) + else: + # Only output warnings if values are significantly + # negative. CRAM does not guarantee positive values. + if val < -1.0e-21: + print("WARNING: nuclide ", nuc, " in material ", mat, + " is negative (density = ", val, " at/barn-cm)") + number_i[mat, nuc] = 0.0 + + # Update densities on C API side + mat_internal = openmc.lib.materials[int(mat)] + mat_internal.set_densities(nuclides, densities) + + #TODO Update densities on the Python side, otherwise the + # summary.h5 file contains densities at the first time step + + ## keep, but rename and modify docstring + ## get_tally_nuclides + def _get_tally_nuclides(self): + """Determine nuclides that should be tallied for reaction rates. + + This method returns a list of all nuclides that have neutron data and + are listed in the depletion chain. Technically, we should tally nuclides + that may not appear in the depletion chain because we still need to get + the fission reaction rate for these nuclides in order to normalize + power, but that is left as a future exercise. + + Returns + ------- + list of str + Tally nuclides + + """ + nuc_set = set() + + # Create the set of all nuclides in the decay chain in materials marked + # for burning in which the number density is greater than zero. + for nuc in self.number.nuclides: + if nuc in self.nuclides_with_data: + if np.sum(self.number[:, nuc]) > 0.0: + nuc_set.add(nuc) + + # Communicate which nuclides have nonzeros to rank 0 + if comm.rank == 0: + for i in range(1, comm.size): + nuc_newset = comm.recv(source=i, tag=i) + nuc_set |= nuc_newset + else: + comm.send(nuc_set, dest=0, tag=comm.rank) + + if comm.rank == 0: + # Sort nuclides in the same order as self.number + nuc_list = [nuc for nuc in self.number.nuclides + if nuc in nuc_set] + else: + nuc_list = None + + # Store list of tally nuclides on each process + nuc_list = comm.bcast(nuc_list) + return [nuc for nuc in nuc_list if nuc in self.chain] + + ## modify + ## the tally-specific methods + ## for the normalization and fission helper classes + ## will already work with the current implementation. + ## For transport-less depletion, we'll need to write + ## a new ReactionRateHelper + def _calculate_reaction_rates(self, source_rate): + """Unpack tallies from OpenMC and return an operator result + + This method uses OpenMC's C API bindings to determine the k-effective + value and reaction rates from the simulation. The reaction rates are + normalized by a helper class depending on the method being used. + + Parameters + ---------- + source_rate : float + Power in [W] or source rate in [neutron/sec] + + Returns + ------- + rates : openmc.deplete.ReactionRates + Reaction rates for nuclides + + """ + rates = self.reaction_rates + rates.fill(0.0) + + # Extract tally bins + nuclides = self._rate_helper.nuclides + + # Form fast map + nuc_ind = [rates.index_nuc[nuc] for nuc in nuclides] + react_ind = [rates.index_rx[react] for react in self.chain.reactions] + + # Keep track of energy produced from all reactions in eV per source + # particle + self._normalization_helper.reset() + self._yield_helper.unpack() + + # Store fission yield dictionaries + fission_yields = [] + + # Create arrays to store fission Q values, reaction rates, and nuclide + # numbers, zeroed out in material iteration + number = np.empty(rates.n_nuc) + + fission_ind = rates.index_rx.get("fission") + + # Extract results + for i, mat in enumerate(self.local_mats): + # Get tally index + mat_index = self._mat_index_map[mat] + + # Zero out reaction rates and nuclide numbers + number.fill(0.0) + + # Get new number densities + for nuc, i_nuc_results in zip(nuclides, nuc_ind): + number[i_nuc_results] = self.number[mat, nuc] + + tally_rates = self._rate_helper.get_material_rates( + mat_index, nuc_ind, react_ind) + + # Compute fission yields for this material + fission_yields.append(self._yield_helper.weighted_yields(i)) + + # Accumulate energy from fission + if fission_ind is not None: + self._normalization_helper.update(tally_rates[:, fission_ind]) + + # Divide by total number and store + rates[i] = self._rate_helper.divide_by_adens(number) + + # Scale reaction rates to obtain units of reactions/sec + rates *= self._normalization_helper.factor(source_rate) + + # Store new fission yields on the chain + self.chain.fission_yields = fission_yields + + return rates + + @abstractmethod + def _get_nuclides_with_data(self): + """Find nuclides with cross section data""" + + ## Keep + def get_results_info(self): + """Returns volume list, material lists, and nuc lists. + + Returns + ------- + volume : dict of str float + Volumes corresponding to materials in full_burn_dict + nuc_list : list of str + A list of all nuclide names. Used for sorting the simulation. + burn_list : list of int + A list of all material IDs to be burned. Used for sorting the simulation. + full_burn_list : list + List of all burnable material IDs + + """ + nuc_list = self.number.burnable_nuclides + burn_list = self.local_mats + + volume = {} + for i, mat in enumerate(burn_list): + volume[mat] = self.number.volume[i] + + # Combine volume dictionaries across processes + volume_list = comm.allgather(volume) + volume = {k: v for d in volume_list for k, v in d.items()} + + return volume, nuc_list, burn_list, self.burnable_mats diff --git a/openmc/deplete/operator.py b/openmc/deplete/operator.py index 7ef874e87..665de1999 100644 --- a/openmc/deplete/operator.py +++ b/openmc/deplete/operator.py @@ -24,6 +24,7 @@ from openmc.mpi import comm from .abc import TransportOperator, OperatorResult from .atom_number import AtomNumber from .chain import _find_chain_file +from .openmc_operator import OpenMCOperator, _distribute from .reaction_rates import ReactionRates from .results import Results from .helpers import ( @@ -34,30 +35,6 @@ from .helpers import ( __all__ = ["Operator", "OperatorResult"] - -def _distribute(items): - """Distribute items across MPI communicator - - Parameters - ---------- - items : list - List of items of distribute - - Returns - ------- - list - Items assigned to process that called - - """ - min_size, extra = divmod(len(items), comm.size) - j = 0 - for i in range(comm.size): - chunk_size = min_size + int(i < extra) - if comm.rank == i: - return items[j:j + chunk_size] - j += chunk_size - - def _find_cross_sections(model): """Determine cross sections to use for depletion""" if model.materials and model.materials.cross_sections is not None: @@ -74,7 +51,7 @@ def _find_cross_sections(model): return cross_sections -class Operator(TransportOperator): +class Operator(OpenMCOperator): """OpenMC transport operator for depletion. Instances of this class can be used to perform depletion using OpenMC as the @@ -267,7 +244,7 @@ class Operator(TransportOperator): # Clear out OpenMC, create task lists, distribute openmc.reset_auto_ids() - self.burnable_mats, volume, nuclides = self._get_burnable_mats() + self.burnable_mats, volume, nuclides = super()._get_burnable_mats() self.local_mats = _distribute(self.burnable_mats) # Generate map from local materials => material index @@ -298,7 +275,7 @@ class Operator(TransportOperator): if nuc.name in self.nuclides_with_data] # Extract number densities from the geometry / previous depletion run - self._extract_number(self.local_mats, volume, nuclides, self.prev_res) + super()._extract_number(self.local_mats, volume, nuclides, self.prev_res) # Create reaction rates array self.reaction_rates = ReactionRates( @@ -380,7 +357,7 @@ class Operator(TransportOperator): openmc.reset_auto_ids() # Update tally nuclides data in preparation for transport solve - nuclides = self._get_tally_nuclides() + nuclides = super()._get_tally_nuclides() self._rate_helper.nuclides = nuclides self._normalization_helper.nuclides = nuclides self._yield_helper.update_tally_nuclides(nuclides) @@ -390,7 +367,12 @@ class Operator(TransportOperator): openmc.lib.reset_timers() # Extract results - op_result = self._unpack_tallies_and_normalize(source_rate) + rates = self._calculate_reaction_rates(source_rate) + + # Get k and uncertainty + keff = ufloat(*openmc.lib.keff()) + + op_result = OperatorResult(keff, rates) return copy.deepcopy(op_result) @@ -434,138 +416,6 @@ class Operator(TransportOperator): cell.fill = [mat.clone() for i in range(cell.num_instances)] - def _get_burnable_mats(self): - """Determine depletable materials, volumes, and nuclides - - Returns - ------- - burnable_mats : list of str - List of burnable material IDs - volume : OrderedDict of str to float - Volume of each material in [cm^3] - nuclides : list of str - Nuclides in order of how they'll appear in the simulation. - - """ - - burnable_mats = set() - model_nuclides = set() - volume = OrderedDict() - - self.heavy_metal = 0.0 - - # Iterate once through the geometry to get dictionaries - for mat in self.materials: - for nuclide in mat.get_nuclides(): - model_nuclides.add(nuclide) - if mat.depletable: - burnable_mats.add(str(mat.id)) - if mat.volume is None: - raise RuntimeError("Volume not specified for depletable " - "material with ID={}.".format(mat.id)) - volume[str(mat.id)] = mat.volume - self.heavy_metal += mat.fissionable_mass - - # Make sure there are burnable materials - if not burnable_mats: - raise RuntimeError( - "No depletable materials were found in the model.") - - # Sort the sets - burnable_mats = sorted(burnable_mats, key=int) - model_nuclides = sorted(model_nuclides) - - # Construct a global nuclide dictionary, burned first - nuclides = list(self.chain.nuclide_dict) - for nuc in model_nuclides: - if nuc not in nuclides: - nuclides.append(nuc) - - return burnable_mats, volume, nuclides - - def _extract_number(self, local_mats, volume, nuclides, prev_res=None): - """Construct AtomNumber using geometry - - Parameters - ---------- - local_mats : list of str - Material IDs to be managed by this process - volume : OrderedDict of str to float - Volumes for the above materials in [cm^3] - nuclides : list of str - Nuclides to be used in the simulation. - prev_res : Results, optional - Results from a previous depletion calculation - - """ - self.number = AtomNumber(local_mats, nuclides, volume, len(self.chain)) - - if self.dilute_initial != 0.0: - for nuc in self._burnable_nucs: - self.number.set_atom_density(np.s_[:], nuc, self.dilute_initial) - - # Now extract and store the number densities - # From the geometry if no previous depletion results - if prev_res is None: - for mat in self.materials: - if str(mat.id) in local_mats: - self._set_number_from_mat(mat) - - # Else from previous depletion results - else: - for mat in self.materials: - if str(mat.id) in local_mats: - self._set_number_from_results(mat, prev_res) - - def _set_number_from_mat(self, mat): - """Extracts material and number densities from openmc.Material - - Parameters - ---------- - mat : openmc.Material - The material to read from - - """ - mat_id = str(mat.id) - - for nuclide, atom_per_bcm in mat.get_nuclide_atom_densities().items(): - atom_per_cc = atom_per_bcm * 1.0e24 - self.number.set_atom_density(mat_id, nuclide, atom_per_cc) - - def _set_number_from_results(self, mat, prev_res): - """Extracts material nuclides and number densities. - - If the nuclide concentration's evolution is tracked, the densities come - from depletion results. Else, densities are extracted from the geometry - in the summary. - - Parameters - ---------- - mat : openmc.Material - The material to read from - prev_res : Results - Results from a previous depletion calculation - - """ - mat_id = str(mat.id) - - # Get nuclide lists from geometry and depletion results - depl_nuc = prev_res[-1].nuc_to_ind - geom_nuc_densities = mat.get_nuclide_atom_densities() - - # Merge lists of nuclides, with the same order for every calculation - geom_nuc_densities.update(depl_nuc) - - for nuclide, atom_per_bcm in geom_nuc_densities.items(): - if nuclide in depl_nuc: - concentration = prev_res.get_atoms(mat_id, nuclide)[1][-1] - volume = prev_res[-1].volume[mat_id] - atom_per_cc = concentration / volume - else: - atom_per_cc = atom_per_bcm * 1.0e24 - - self.number.set_atom_density(mat_id, nuclide, atom_per_cc) - def initial_condition(self): """Performs final setup and returns initial condition. @@ -589,16 +439,8 @@ class Operator(TransportOperator): # Generate tallies in memory materials = [openmc.lib.materials[int(i)] for i in self.burnable_mats] - self._rate_helper.generate_tallies(materials, self.chain.reactions) - self._normalization_helper.prepare( - self.chain.nuclides, self.reaction_rates.index_nuc) - # Tell fission yield helper what materials this process is - # responsible for - self._yield_helper.generate_tallies( - materials, tuple(sorted(self._mat_index_map.values()))) - # Return number density vector - return list(self.number.get_mat_slice(np.s_[:])) + return super().initial_condition(materials) def finalize(self): """Finalize a depletion simulation and release resources.""" @@ -659,127 +501,6 @@ class Operator(TransportOperator): self.materials.export_to_xml() - def _get_tally_nuclides(self): - """Determine nuclides that should be tallied for reaction rates. - - This method returns a list of all nuclides that have neutron data and - are listed in the depletion chain. Technically, we should tally nuclides - that may not appear in the depletion chain because we still need to get - the fission reaction rate for these nuclides in order to normalize - power, but that is left as a future exercise. - - Returns - ------- - list of str - Tally nuclides - - """ - nuc_set = set() - - # Create the set of all nuclides in the decay chain in materials marked - # for burning in which the number density is greater than zero. - for nuc in self.number.nuclides: - if nuc in self.nuclides_with_data: - if np.sum(self.number[:, nuc]) > 0.0: - nuc_set.add(nuc) - - # Communicate which nuclides have nonzeros to rank 0 - if comm.rank == 0: - for i in range(1, comm.size): - nuc_newset = comm.recv(source=i, tag=i) - nuc_set |= nuc_newset - else: - comm.send(nuc_set, dest=0, tag=comm.rank) - - if comm.rank == 0: - # Sort nuclides in the same order as self.number - nuc_list = [nuc for nuc in self.number.nuclides - if nuc in nuc_set] - else: - nuc_list = None - - # Store list of tally nuclides on each process - nuc_list = comm.bcast(nuc_list) - return [nuc for nuc in nuc_list if nuc in self.chain] - - def _unpack_tallies_and_normalize(self, source_rate): - """Unpack tallies from OpenMC and return an operator result - - This method uses OpenMC's C API bindings to determine the k-effective - value and reaction rates from the simulation. The reaction rates are - normalized by a helper class depending on the method being used. - - Parameters - ---------- - source_rate : float - Power in [W] or source rate in [neutron/sec] - - Returns - ------- - openmc.deplete.OperatorResult - Eigenvalue and reaction rates resulting from transport operator - - """ - rates = self.reaction_rates - rates.fill(0.0) - - # Get k and uncertainty - keff = ufloat(*openmc.lib.keff()) - - # Extract tally bins - nuclides = self._rate_helper.nuclides - - # Form fast map - nuc_ind = [rates.index_nuc[nuc] for nuc in nuclides] - react_ind = [rates.index_rx[react] for react in self.chain.reactions] - - # Keep track of energy produced from all reactions in eV per source - # particle - self._normalization_helper.reset() - self._yield_helper.unpack() - - # Store fission yield dictionaries - fission_yields = [] - - # Create arrays to store fission Q values, reaction rates, and nuclide - # numbers, zeroed out in material iteration - number = np.empty(rates.n_nuc) - - fission_ind = rates.index_rx.get("fission") - - # Extract results - for i, mat in enumerate(self.local_mats): - # Get tally index - mat_index = self._mat_index_map[mat] - - # Zero out reaction rates and nuclide numbers - number.fill(0.0) - - # Get new number densities - for nuc, i_nuc_results in zip(nuclides, nuc_ind): - number[i_nuc_results] = self.number[mat, nuc] - - tally_rates = self._rate_helper.get_material_rates( - mat_index, nuc_ind, react_ind) - - # Compute fission yields for this material - fission_yields.append(self._yield_helper.weighted_yields(i)) - - # Accumulate energy from fission - if fission_ind is not None: - self._normalization_helper.update(tally_rates[:, fission_ind]) - - # Divide by total number and store - rates[i] = self._rate_helper.divide_by_adens(number) - - # Scale reaction rates to obtain units of reactions/sec - rates *= self._normalization_helper.factor(source_rate) - - # Store new fission yields on the chain - self.chain.fission_yields = fission_yields - - return OperatorResult(keff, rates) - def _get_nuclides_with_data(self, cross_sections): """Loads cross_sections.xml file to find nuclides with neutron data""" nuclides = set() @@ -792,31 +513,3 @@ class Operator(TransportOperator): nuclides.add(name) return nuclides - - def get_results_info(self): - """Returns volume list, material lists, and nuc lists. - - Returns - ------- - volume : dict of str float - Volumes corresponding to materials in full_burn_dict - nuc_list : list of str - A list of all nuclide names. Used for sorting the simulation. - burn_list : list of int - A list of all material IDs to be burned. Used for sorting the simulation. - full_burn_list : list - List of all burnable material IDs - - """ - nuc_list = self.number.burnable_nuclides - burn_list = self.local_mats - - volume = {} - for i, mat in enumerate(burn_list): - volume[mat] = self.number.volume[i] - - # Combine volume dictionaries across processes - volume_list = comm.allgather(volume) - volume = {k: v for d in volume_list for k, v in d.items()} - - return volume, nuc_list, burn_list, self.burnable_mats From 1e849fe16239883531c1bb27ce80d826f4583dad Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 13 Jul 2022 11:09:35 -0500 Subject: [PATCH 049/131] Apply @JoffreyDorville suggestions from code review Co-authored-by: Joffrey Dorville <54550047+JoffreyDorville@users.noreply.github.com> --- openmc/data/kalbach_mann.py | 6 +++--- tests/unit_tests/test_data_kalbach_mann.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openmc/data/kalbach_mann.py b/openmc/data/kalbach_mann.py index 02403b3cc..a036da694 100644 --- a/openmc/data/kalbach_mann.py +++ b/openmc/data/kalbach_mann.py @@ -133,7 +133,7 @@ def _separation_energy(compound, nucleus, particle): N_a = nucleus.n # Determine breakup energy of incident particle (ENDF-6 Formats Manual, - # Appendix H, Table 3) + # Appendix H, Table 3) in MeV za_to_breaking_energy = { 1: 0.0, 1001: 0.0, @@ -142,7 +142,7 @@ def _separation_energy(compound, nucleus, particle): 2003: 7.718043, 2004: 28.29566 } - I_b = za_to_breaking_energy[particle.za] + I_a = za_to_breaking_energy[particle.za] # Eq. 4 in in doi:10.1103/PhysRevC.37.2350 or ENDF-6 Formats Manual section # 6.2.3.2 @@ -153,7 +153,7 @@ def _separation_energy(compound, nucleus, particle): 33.22 * ((N_c - Z_c)**2 / A_c**(4./3.) - (N_a - Z_a)**2 / A_a**(4./3.)) - 0.717 * (Z_c**2 / A_c**(1./3.) - Z_a**2 / A_a**(1./3.)) + 1.211 * (Z_c**2 / A_c - Z_a**2 / A_a) - - I_b + I_a ) diff --git a/tests/unit_tests/test_data_kalbach_mann.py b/tests/unit_tests/test_data_kalbach_mann.py index 931e236bd..3b837af7d 100644 --- a/tests/unit_tests/test_data_kalbach_mann.py +++ b/tests/unit_tests/test_data_kalbach_mann.py @@ -75,7 +75,7 @@ def test_atomic_representation(neutron, triton, b10, c12, c13, na23): assert triton.n == 2 assert triton.za == 1003 - # Test instanciation errors + # Test instantiation errors with pytest.raises(ValueError): _AtomicRepresentation(z=5, a=1) with pytest.raises(ValueError): @@ -129,7 +129,7 @@ def test_kalbach_slope(): ('Hg204.h5', 'n-080_Hg_204.endf') ] ) -def test_comparison_slope_hdf5(hdf5_filename, endf_type, endf_filename): +def test_comparison_slope_hdf5(hdf5_filename, endf_filename): """Test the calculation of the Kalbach-Mann slope done by OpenMC by comparing it to HDF5 data. The test is based on the first product of MT=5 (neutron). The isotopes tested have been selected because the From 8f1db414589f11bbc0511c1ba81064a33d638cf9 Mon Sep 17 00:00:00 2001 From: yardasol Date: Wed, 13 Jul 2022 15:55:48 -0500 Subject: [PATCH 050/131] overhaul FluxTimexXSHelper class --- openmc/deplete/helpers.py | 42 +++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/openmc/deplete/helpers.py b/openmc/deplete/helpers.py index 5adb43998..67a88fa16 100644 --- a/openmc/deplete/helpers.py +++ b/openmc/deplete/helpers.py @@ -25,7 +25,7 @@ __all__ = ( "DirectReactionRateHelper", "ChainFissionHelper", "EnergyScoreHelper" "SourceRateHelper", "TalliedFissionYieldHelper", "ConstantFissionYieldHelper", "FissionYieldCutoffHelper", - "AveragedFissionYieldHelper", "FluxCollapseHelper") + "AveragedFissionYieldHelper", "FluxCollapseHelper", "FluxTimesXSHelper") class TalliedFissionYieldHelper(FissionYieldHelper): """Abstract class for computing fission yields with tallies @@ -125,16 +125,21 @@ class TalliedFissionYieldHelper(FissionYieldHelper): # ------------------------------------- -class FluxReactionRateHelper(ReactionRateHelper): +class FluxTimesXSHelper(ReactionRateHelper): """Class for generating one-group reaction rates with flux and one-group cross sections. This class does not generate tallies, and instead stores cross sections - for each nuclides and transmutation reaction relevant for a depletion - calculation. + for each nuclide and transmutation reaction relevant for a depletion + calculation. The reaction rate is calculated by multiplying the flux by the + cross sections. Parameters ---------- + flux : float + Neutron flux + micro_xs : pandas.DataFrame + Microscopic cross-section data n_nucs : int Number of burnable nuclides tracked by :class:`openmc.deplete.Operator` n_react : int @@ -142,16 +147,22 @@ class FluxReactionRateHelper(ReactionRateHelper): Attributes ---------- - flux : - - xs : - + nuc_ind_map : dict of int to str + Dictionary mapping the nuclide index to nuclide name + rxn_ind_map : dict of int to str + Dictionary mapping reaction index to reaction name + number : AtomNumber + AtomNumber object. Needed to convert the microscopic cross-sections + to macroscopic cross sections. """ - def __init__(self, n_nuc, n_react): + def __init__(self, flux, micro_xs, n_nuc, n_react): super().__init__(n_nuc, n_react) - self._flux = None - self._micro_xs = None + self._flux = flux + self._micro_xs = micro_xs + self.nuc_ind_map = None + self.rxn_ind_map = None + self.number = None def generate_tallies(self, materials, scores): """Unused in this case""" @@ -191,10 +202,11 @@ class FluxReactionRateHelper(ReactionRateHelper): Ordering of reactions """ self._results_cache.fill(0.0) - full_tally_res = self._rate_tally.mean[mat_id] - for i_tally, (i_nuc, i_react) in enumerate( - product(nuc_index, react_index)): - self._results_cache[i_nuc, i_react] = full_tally_res[i_tally] + for i, (i_nuc, i_react) in enumerate(product(nuc_index, react_index)): + nuc = self.nuc_ind_map[i_nuc] + rxn = self.rxn_ind_map[i_react] + density = self.number.get_atom_density(mat_id, nuc) + self._results_cache[i_nuc, i_react] = self._micro_xs[rxn][nuc] * density return self._results_cache From a1aa66c1fda8b103d1c8cb094911fc0d913e1c6b Mon Sep 17 00:00:00 2001 From: yardasol Date: Wed, 13 Jul 2022 15:56:29 -0500 Subject: [PATCH 051/131] make FluxDepletionOperator work with multiple materials; update regression test --- openmc/deplete/flux_operator.py | 330 +++++++++++++----- .../deplete_no_transport/test_reference.h5 | Bin 35688 -> 35688 bytes 2 files changed, 243 insertions(+), 87 deletions(-) diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/flux_operator.py index 562967e6c..5e23ec203 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/flux_operator.py @@ -5,25 +5,44 @@ and one-group cross sections. """ - +import copy +from collections import OrderedDict from warnings import warn import numpy as np import pandas as pd from uncertainties import ufloat +import openmc from openmc.checkvalue import check_type, check_value, check_iterable_type from openmc.mpi import comm from .abc import TransportOperator, OperatorResult from .atom_number import AtomNumber from .chain import REACTIONS from .reaction_rates import ReactionRates -from .helpers import ConstantFissionYieldHelper +from .helpers import ConstantFissionYieldHelper, SourceRateHelper, FluxTimesXSHelper valid_rxns = list(REACTIONS) valid_rxns.append('fission') - +def _distribute(items): + """Distribute items across MPI communicator + Parameters + ---------- + items : list + List of items of distribute + Returns + ------- + list + Items assigned to process that called + """ + min_size, extra = divmod(len(items), comm.size) + j = 0 + for i in range(comm.size): + chunk_size = min_size + int(i < extra) + if comm.rank == i: + return items[j:j + chunk_size] + j += chunk_size class FluxDepletionOperator(TransportOperator): """Depletion operator that uses a user-provided flux spectrum and one-group @@ -98,57 +117,76 @@ class FluxDepletionOperator(TransportOperator): reduce_chain=False, reduce_chain_level=None, fission_yield_opts=None): - super().__init__(chain_file, fission_q, 0.0, prev_results) - self.round_number = False - self.flux_spectra = flux_spectra - self._init_nuclides = nuclides - self._volume = volume - - # Reduce the chain to only those nuclides present - if reduce_chain: - init_nuc_names = set(nuclides.keys()) - self.chain = self.chain.reduce(init_nuc_names, reduce_chain_level) - # Validate nuclides and micro-xs parameters check_type('nuclides', nuclides, dict, str) check_type('micro_xs', micro_xs, pd.DataFrame) - self._micro_xs = micro_xs + self.cross_sections = micro_xs if keff is not None: check_type('keff', keff, tuple, float) keff = ufloat(keff) self._keff = keff + self.flux_spectra = flux_spectra + materials = self._consolidate_nuclides_to_material(nuclides, volume) - self._all_nuclides = self._get_all_nuclides_in_simulation() + diff_burnable_mats=False + # super().__init__(materials, diff_burnable_mats, chain_file, fission_q, dilute_initial, prev_results, helper_class_kwargs) - # TODO: add support for loading previous results + + ## this part goes to OpenMCOperator + super().__init__(chain_file, fission_q, 0.0, prev_results) + self.round_number = False + self.materials = materials + + # Reduce the chain to only those nuclides present + if reduce_chain: + init_nuclides = set() + for material in self.materials: + if not material.depletable: + continue + for name, _dens_percent, _dens_type in material.nuclides: + init_nuclides.add(name) + + self.chain = self.chain.reduce(init_nuclides, reduce_chain_level) + + if diff_burnable_mats: + ## to implement + self._differentiate_burnable_mats() # Determine which nuclides have cross section data # This nuclides variables contains every nuclides # for which there is an entry in the micro_xs parameter - self.nuclides_with_data = self._get_nuclides_with_data() + openmc.reset_auto_ids() + self.burnable_mats, volumes, all_nuclides = self._get_burnable_mats() + self._all_nuclides = all_nuclides + self.local_mats = _distribute(self.burnable_mats) + + self._mat_index_map = { + lm: self.burnable_mats.index(lm) for lm in self.local_mats} + + if self.prev_res is not None: + ## will be an abstract function for OpenMCOperator + self._load_previous_results() + + + self.nuclides_with_data = self._get_nuclides_with_data(self.cross_sections) # Select nuclides with data that are also in the chain self._burnable_nucs = [nuc.name for nuc in self.chain.nuclides if nuc.name in self.nuclides_with_data] # Extract number densities from the geometry / previous depletion run - self._extract_number(['0'], - {'0': volume}, - self._all_nuclides, + self._extract_number(self.local_mats, + volumes, + all_nuclides, self.prev_res) # Create reaction rates array self.reaction_rates = ReactionRates( - ['0'], self._burnable_nucs, self.chain.reactions) + self.local_mats, self._burnable_nucs, self.chain.reactions) - # Select and create fission yield helper - fission_helper = ConstantFissionYieldHelper - fission_yield_opts = ( - {} if fission_yield_opts is None else fission_yield_opts) - self._yield_helper = fission_helper.from_operator( - self, **fission_yield_opts) + self._get_helper_classes(None, fission_yield_opts) def __call__(self, vec, source_rate): """Obtain the reaction rates @@ -173,14 +211,31 @@ class FluxDepletionOperator(TransportOperator): # Get all nuclides for which we will calculate reaction rates rxn_nuclides = self._get_reaction_nuclides() + self._rate_helper.nuclides = rxn_nuclides + self._normalization_helper.nuclides = rxn_nuclides + self._yield_helper.update_tally_nuclides(rxn_nuclides) + + # Use the flux spectra as a "source rate" + rates = self._calculate_reaction_rates(self.flux_spectra) + keff = self._keff + + op_result = OperatorResult(keff, rates) + return copy.deepcopy(op_result) + + def _calculate_reaction_rates(self, source_rate): rates = self.reaction_rates rates.fill(0.0) + rxn_nuclides = self._rate_helper.nuclides + # Form fast map nuc_ind = [rates.index_nuc[nuc] for nuc in rxn_nuclides] react_ind = [rates.index_rx[react] for react in self.chain.reactions] + self._normalization_helper.reset() + self._yield_helper.unpack() + # Store fission yield dictionaries fission_yields = [] @@ -190,45 +245,55 @@ class FluxDepletionOperator(TransportOperator): fission_ind = rates.index_rx.get("fission") - # Zero out reaction rates and nuclide numbers - number.fill(0.0) + for i, mat in enumerate(self.local_mats): + mat_index = self._mat_index_map[mat] - # Get new number densities - for nuc, i_nuc_results in zip(rxn_nuclides, nuc_ind): - number[i_nuc_results] = self.number[0, nuc] + # Zero out reaction rates and nuclide numbers + number.fill(0.0) - # Calculate macroscopic cross sections and store them in rates array - for nuc in rxn_nuclides: - density = self.number.get_atom_density('0', nuc) - for rxn in self.chain.reactions: - rates.set( - '0', - nuc, - rxn, - self._micro_xs[rxn, rxn] * density) + # Get new number densities + for nuc, i_nuc_results in zip(rxn_nuclides, nuc_ind): + number[i_nuc_results] = self.number[mat, nuc] + # Calculate macroscopic cross sections and store them in rates array + rxn_rates = self._rate_helper.get_material_rates( + mat_index, nuc_ind, react_ind) + + ## replace + #for nuc in rxn_nuclides: + # density = self.number.get_atom_density(i, nuc) + # for rxn in self.chain.reactions: + # rates.set( + # i, + # nuc, + # rxn, + # self.cross_sections[rxn, nuc] * density) + + # Compute fission yields for this material + fission_yields.append(self._yield_helper.weighted_yields(i)) + + # Accumulate energy from fission + if fission_ind is not None: + self._normalization_helper.update(rxn_rates[:, fission_ind]) + + # Divide by total number of atoms and store + # the reason we do this is based on the mathematical equation; + # in the equation, we multiply the depletion matrix by the nuclide + # vector. Since what we want is the depletion matrix, we need to + # divide the reaction rates by the number of atoms to get the right + # units. + rates[i] = self._rate_helper.divide_by_adens(number) + + rates *= self._normalization_helper.factor(source_rate) + ##replace # Get reaction rate in reactions/sec - rates *= self.flux_spectra + #rates *= self.flux_spectra - # Compute fission yields for this material - fission_yields.append(self._yield_helper.weighted_yields(0)) - - # Divide by total number of atoms and store - # the reason we do this is based on the mathematical equation; - # in the equation, we multiply the depletion matrix by the nuclide - # vector. Since what we want is the depletion matrix, we need to - # divide the reaction rates by the number of atoms to get the right - # units. - mask = np.nonzero(number) - results = rates[0] - for col in range(results.shape[1]): - results[mask, col] /= number[mask] - rates[0] = results # Store new fission yields on the chain self.chain.fission_yields = fission_yields - return OperatorResult(self._keff, rates) + return rates def initial_condition(self): """Performs final setup and returns initial condition. @@ -272,9 +337,11 @@ class FluxDepletionOperator(TransportOperator): All burnable materials in the geometry. """ nuc_list = self.number.burnable_nuclides - burn_list = ['0'] + burn_list = self.local_mats - volume = {'0': self._volume} + volume = {} + for i, mat in enumerate(burn_list): + volume[mat] = self.number.volume[i] # Combine volume dictionaries across processes volume_list = comm.allgather(volume) @@ -404,6 +471,54 @@ class FluxDepletionOperator(TransportOperator): # TODO Update densities on the Python side, otherwise the # summary.h5 file contains densities at the first time step + def _consolidate_nuclides_to_material(self, nuclides, volume): + """Puts nuclide list into an openmc.Materials object. + + """ + openmc.reset_auto_ids() + mat = openmc.Material() + for nuc, conc in nuclides.items(): + mat.add_nuclide(nuc, conc / 1e24) #convert to at/b-cm + + mat.volume = volume + mat.depleteable = True + + return openmc.Materials([mat]) + + def _get_helper_classes(self, reaction_rate_opts, fission_yield_opts): + """Get helper classes for calculating reation rates and fission yields""" + rates = self.reaction_rates + # Get classes to assit working with tallies + nuc_ind_map = {ind: nuc for nuc, ind in rates.index_nuc.items()} + rxn_ind_map = {ind: rxn for rxn, ind in rates.index_rx.items()} + + + self._rate_helper = FluxTimesXSHelper(self.flux_spectra, self.cross_sections, self.reaction_rates.n_nuc, self.reaction_rates.n_react) + + self._rate_helper.nuc_ind_map = nuc_ind_map + self._rate_helper.rxn_ind_map = rxn_ind_map + # We'll need to find a way to update number as time goes on. + # perhaps in this classes version of _update_materials()? + self._rate_helper.number = self.number + + self._normalization_helper = SourceRateHelper() + + # Select and create fission yield helper + fission_helper = ConstantFissionYieldHelper + fission_yield_opts = ( + {} if fission_yield_opts is None else fission_yield_opts) + self._yield_helper = fission_helper.from_operator( + self, **fission_yield_opts) + + + def _load_previous_results(): + """Load in results from a previous depletion calculation.""" + pass + + def _differentiate_burnable_materials(): + """Assign distribmats for each burnable material""" + pass + def _get_reaction_nuclides(self): """Determine nuclides that should have reaction rates @@ -421,10 +536,12 @@ class FluxDepletionOperator(TransportOperator): """ # Create the set of all nuclides in the decay chain in materials marked # for burning in which the number density is greater than zero. - nuc_set = set(self._all_nuclides).intersection(self.nuclides_with_data) - for nuc in nuc_set: - if not np.sum(self.number[:, nuc]) > 0.0: - nuc_set.remove(nuc) + nuc_set = set() + + for nuc in self.number.nuclides: + if nuc in self.nuclides_with_data: + if np.sum(self.number[:,nuc]) > 0.0: + nuc_set.add(nuc) # Communicate which nuclides have nonzeros to rank 0 if comm.rank == 0: @@ -445,31 +562,57 @@ class FluxDepletionOperator(TransportOperator): nuc_list = comm.bcast(nuc_list) return [nuc for nuc in nuc_list if nuc in self.chain] - def _get_all_nuclides_in_simulation(self): - """Determine nuclides that will show up in the depletion matrix. - This is the union of the nuclides provided by the user and - the nuclides present in the depletion chain. - + def _get_burnable_mats(self): + """Determine depletable materials, volumes, and nuclides Returns ------- - all_nuclides : list of str + burnable_mats : list of str + List of burnable material IDs + volume : OrderedDict of str to float + Volume of each material in [cm^3] + nuclides : list of str Nuclides in order of how they'll appear in the simulation. - """ - init_nuclides = sorted(self._init_nuclides.keys()) + burnable_mats = set() + model_nuclides = set() + volume = OrderedDict() + + self.heavy_metal = 0.0 + + # Iterate once through the geometry to get dictionaries + for mat in self.materials: + for nuclide in mat.get_nuclides(): + model_nuclides.add(nuclide) + if mat.depletable: + burnable_mats.add(str(mat.id)) + if mat.volume is None: + raise RuntimeError("Volume not specified for depletable " + "material with ID={}.".format(mat.id)) + volume[str(mat.id)] = mat.volume + self.heavy_metal += mat.fissionable_mass + + # Make sure there are burnable materials + if not burnable_mats: + raise RuntimeError( + "No depletable materials were found in the model.") + + # Sort the sets + burnable_mats = sorted(burnable_mats, key=int) + model_nuclides = sorted(model_nuclides) # Construct a global nuclide dictionary, burned first - all_nuclides = list(self.chain.nuclide_dict) - for nuc in init_nuclides: - if nuc not in all_nuclides: - all_nuclides.append(nuc) + nuclides = list(self.chain.nuclide_dict) + for nuc in model_nuclides: + if nuc not in nuclides: + nuclides.append(nuc) - return all_nuclides + return burnable_mats, volume, nuclides - def _get_nuclides_with_data(self): - """Finds nuclides with cross section data""" - return set(self._micro_xs.index) + def _get_nuclides_with_data(self, cross_sections): + """Finds nuclides with cross section data + """ + return set(cross_sections.index) def _extract_number(self, local_mats, volume, nuclides, prev_res=None): @@ -489,17 +632,30 @@ class FluxDepletionOperator(TransportOperator): """ self.number = AtomNumber(local_mats, nuclides, volume, len(self.chain)) + if self.dilute_initial != 0.0: + for nuc in self._burnable_nucs: + self.number.set_atom_density(np.s_[:], nuc, self.dilute_initial) + # Now extract and store the number densities # From the geometry if no previous depletion results if prev_res is None: - for nuclide in nuclides: - if nuclide in self._init_nuclides: - self.number.set_atom_density( - '0', nuclide, self._init_nuclides[nuclide]) - elif nuclide not in self._burnable_nucs: - self.number.set_atom_density('0', nuclide, 0) - + for mat in self.materials: + if str(mat.id) in local_mats: + self._set_number_from_mat(mat) # Else from previous depletion results else: raise RuntimeError( "Loading from previous results not yet supported") + + def _set_number_from_mat(self, mat): + """Extracts material and number densities from openmc.Material + Parameters + ---------- + mat : openmc.Material + The material to read from + """ + mat_id = str(mat.id) + + for nuclide, atom_per_bcm in mat.get_nuclide_atom_densities().items(): + atom_per_cc = atom_per_bcm * 1.0e24 + self.number.set_atom_density(mat_id, nuclide, atom_per_cc) diff --git a/tests/regression_tests/deplete_no_transport/test_reference.h5 b/tests/regression_tests/deplete_no_transport/test_reference.h5 index e4fbf5029d56bd9169e688fb477bc7e6c0688d9e..55675f9d082cd29b63a028131888b31f384f5418 100644 GIT binary patch delta 77 zcmV-T0J8t+mICOO06j`1e;nL|!EK(X@L$Vel>G j7(O39_LDJ-8 Date: Wed, 13 Jul 2022 17:17:26 -0500 Subject: [PATCH 052/131] move reusable code to new parent class; 2nd pass; some minor docstring edits --- openmc/deplete/openmc_operator.py | 167 +++++++++++++---------------- openmc/deplete/operator.py | 168 +++++++++++++----------------- 2 files changed, 143 insertions(+), 192 deletions(-) diff --git a/openmc/deplete/openmc_operator.py b/openmc/deplete/openmc_operator.py index 303a5208f..a2296d88a 100644 --- a/openmc/deplete/openmc_operator.py +++ b/openmc/deplete/openmc_operator.py @@ -1,9 +1,6 @@ """OpenMC transport operator -This module implements a transport operator for OpenMC so that it can be used by -depletion integrators. The implementation makes use of the Python bindings to -OpenMC's C API so that reading tally results and updating material number -densities is all done in-memory instead of through the filesystem. +This module implements functions used by both OpenMC transport operators as well as pure depletion operators. """ @@ -11,9 +8,9 @@ import copy from abc import abstractmethod from collections import OrderedDict import os -from warnings import warn import numpy as np +from uncertainties import ufloat import openmc from openmc.exceptions import DataError @@ -22,10 +19,6 @@ from .abc import TransportOperator, OperatorResult from .atom_number import AtomNumber from .reaction_rates import ReactionRates from .results import Results -from .helpers import ( - DirectReactionRateHelper, ChainFissionHelper, ConstantFissionYieldHelper, - FissionYieldCutoffHelper, AveragedFissionYieldHelper, EnergyScoreHelper, - SourceRateHelper, FluxCollapseHelper) __all__ = ["OpenMCOperator", "OperatorResult"] @@ -124,45 +117,61 @@ class OpenMCOperator(TransportOperator): depletion operation is complete. Defaults to clearing the library. """ - ## modify - def __init__(self, chain_file=None, fission_q=None, dilute_initial=0.0, prev_results=None): + def __init__(self, materials=None, cross_sections=None, chain_file=None, prev_results=None, diff_burnable_mats=False, normalization_mode=None, fission_q=None, dilute_initial=0.0, fission_yield_mode=None, fission_yield_opts=None, + reaction_rate_mode=None, reaction_rate_opts=None, + reduce_chain=False, reduce_chain_level=None): super().__init__(chain_file, fission_q, dilute_initial, prev_results) + self.round_number=False + self.materials = materials + self.cross_sections = cross_sections - ## clear, but we must keep the method - def __call__(self, vec, source_rate): - """Runs a simulation. + # Reduce the chain to only those nuclides present + if reduce_chain: + init_nuclides = set() + for material in self.materials: + if not material.depletable: + continue + for name, _dens_percent, _dens_type in material.nuclides: + init_nuclides.add(name) - Simulation will abort under the following circumstances: + self.chain = self.chain.reduce(init_nuclides, reduce_chain_level) - 1) No energy is computed using OpenMC tallies. + if diff_burnable_mats: + self._differentiate_burnable_mats() - Parameters - ---------- - vec : list of numpy.ndarray - Total atoms to be used in function. - source_rate : float - Power in [W] or source rate in [neutron/sec] + # Determine which nuclides have cross section data + # This nuclides variables contains every nuclides + # for which there is an entry in the micro_xs parameter + openmc.reset_auto_ids() + self.burnable_mats, volumes, all_nuclides = self._get_burnable_mats() + self.local_mats = _distribute(self.burnable_mats) - Returns - ------- - openmc.deplete.OperatorResult - Eigenvalue and reaction rates resulting from transport operator + self._mat_index_map = { + lm: self.burnable_mats.index(lm) for lm in self.local_mats} - """ + if self.prev_res is not None: + self._load_previous_results() - ## clear, but must keep the method - def write_bos_data(step): - """Write a state-point file with beginning of step data - Parameters - ---------- - step : int - Current depletion step including restarts - """ - pass + self.nuclides_with_data = self._get_nuclides_with_data(self.cross_sections) + + # Select nuclides with data that are also in the chain + self._burnable_nucs = [nuc.name for nuc in self.chain.nuclides + if nuc.name in self.nuclides_with_data] + + # Extract number densities from the geometry / previous depletion run + self._extract_number(self.local_mats, + volumes, + all_nuclides, + self.prev_res) + + # Create reaction rates array + self.reaction_rates = ReactionRates( + self.local_mats, self._burnable_nucs, self.chain.reactions) + + self._get_helper_classes(reaction_rate_mode, reaction_rate_opts, normalization_mode, fission_yield_mode, fission_yield_opts) - ## keep def _get_burnable_mats(self): """Determine depletable materials, volumes, and nuclides @@ -212,8 +221,7 @@ class OpenMCOperator(TransportOperator): return burnable_mats, volume, nuclides - ## keep - def _extract_number(self, local_mats, volume, nuclides, prev_res=None): + def _extract_number(self, local_mats, volume, all_nuclides, prev_res=None): """Construct AtomNumber using geometry Parameters @@ -222,13 +230,13 @@ class OpenMCOperator(TransportOperator): Material IDs to be managed by this process volume : OrderedDict of str to float Volumes for the above materials in [cm^3] - nuclides : list of str + all_nuclides : list of str Nuclides to be used in the simulation. prev_res : Results, optional Results from a previous depletion calculation """ - self.number = AtomNumber(local_mats, nuclides, volume, len(self.chain)) + self.number = AtomNumber(local_mats, all_nuclides, volume, len(self.chain)) if self.dilute_initial != 0.0: for nuc in self._burnable_nucs: @@ -296,7 +304,6 @@ class OpenMCOperator(TransportOperator): self.number.set_atom_density(mat_id, nuclide, atom_per_cc) - ## keep, will need to modify def initial_condition(self, materials): """Performs final setup and returns initial condition. @@ -321,50 +328,11 @@ class OpenMCOperator(TransportOperator): # Return number density vector return list(self.number.get_mat_slice(np.s_[:])) - ## keep? there is a non-removable part of this - ## that needs the openmc c executable + @abstractmethod def _update_materials(self): """Updates material compositions in OpenMC on all processes.""" - for rank in range(comm.size): - number_i = comm.bcast(self.number, root=rank) - - for mat in number_i.materials: - nuclides = [] - densities = [] - for nuc in number_i.nuclides: - if nuc in self.nuclides_with_data: - val = 1.0e-24 * number_i.get_atom_density(mat, nuc) - - # If nuclide is zero, do not add to the problem. - if val > 0.0: - if self.round_number: - val_magnitude = np.floor(np.log10(val)) - val_scaled = val / 10**val_magnitude - val_round = round(val_scaled, 8) - - val = val_round * 10**val_magnitude - - nuclides.append(nuc) - densities.append(val) - else: - # Only output warnings if values are significantly - # negative. CRAM does not guarantee positive values. - if val < -1.0e-21: - print("WARNING: nuclide ", nuc, " in material ", mat, - " is negative (density = ", val, " at/barn-cm)") - number_i[mat, nuc] = 0.0 - - # Update densities on C API side - mat_internal = openmc.lib.materials[int(mat)] - mat_internal.set_densities(nuclides, densities) - - #TODO Update densities on the Python side, otherwise the - # summary.h5 file contains densities at the first time step - - ## keep, but rename and modify docstring - ## get_tally_nuclides - def _get_tally_nuclides(self): + def _get_reaction_nuclides(self): """Determine nuclides that should be tallied for reaction rates. This method returns a list of all nuclides that have neutron data and @@ -407,12 +375,6 @@ class OpenMCOperator(TransportOperator): nuc_list = comm.bcast(nuc_list) return [nuc for nuc in nuc_list if nuc in self.chain] - ## modify - ## the tally-specific methods - ## for the normalization and fission helper classes - ## will already work with the current implementation. - ## For transport-less depletion, we'll need to write - ## a new ReactionRateHelper def _calculate_reaction_rates(self, source_rate): """Unpack tallies from OpenMC and return an operator result @@ -434,11 +396,11 @@ class OpenMCOperator(TransportOperator): rates = self.reaction_rates rates.fill(0.0) - # Extract tally bins - nuclides = self._rate_helper.nuclides + # Extract reaction nuclides + rxn_nuclides = self._rate_helper.nuclides # Form fast map - nuc_ind = [rates.index_nuc[nuc] for nuc in nuclides] + nuc_ind = [rates.index_nuc[nuc] for nuc in rxn_nuclides] react_ind = [rates.index_rx[react] for react in self.chain.reactions] # Keep track of energy produced from all reactions in eV per source @@ -464,7 +426,7 @@ class OpenMCOperator(TransportOperator): number.fill(0.0) # Get new number densities - for nuc, i_nuc_results in zip(nuclides, nuc_ind): + for nuc, i_nuc_results in zip(rxn_nuclides, nuc_ind): number[i_nuc_results] = self.number[mat, nuc] tally_rates = self._rate_helper.get_material_rates( @@ -488,11 +450,26 @@ class OpenMCOperator(TransportOperator): return rates + @abstractmethod - def _get_nuclides_with_data(self): + def _get_helper_classes(self, reaction_rate_mode, reaction_rate_opts, normalization_mode, fission_yield_mode, fission_yield_opts): + """Create the ``_rate_helper``, ``normalization_helper``, and + ``_yield_helper`` attributes""" + + @abstractmethod + def _load_previous_results(self): + """Load reuslts from a previous depletion simulation""" + + @abstractmethod + def _get_nuclides_with_data(self, cross_sections): """Find nuclides with cross section data""" - ## Keep + @abstractmethod + def _differentiate_burnable_mats(self): + """Assign distribmats for each burnable material + + """ + def get_results_info(self): """Returns volume list, material lists, and nuc lists. diff --git a/openmc/deplete/operator.py b/openmc/deplete/operator.py index 665de1999..2770494ba 100644 --- a/openmc/deplete/operator.py +++ b/openmc/deplete/operator.py @@ -210,8 +210,6 @@ class Operator(OpenMCOperator): if fission_q is not None: warn("Fission Q dictionary will not be used") fission_q = None - super().__init__(chain_file, fission_q, dilute_initial, prev_results) - self.round_number = False self.model = model self.settings = model.settings self.geometry = model.geometry @@ -221,103 +219,15 @@ class Operator(OpenMCOperator): model.materials = openmc.Materials( model.geometry.get_all_materials().values() ) - self.materials = model.materials self.cleanup_when_done = True - # Reduce the chain before we create more materials - if reduce_chain: - all_isotopes = set() - for material in self.materials: - if not material.depletable: - continue - for name, _dens_percent, _dens_type in material.nuclides: - all_isotopes.add(name) - self.chain = self.chain.reduce(all_isotopes, reduce_chain_level) - - # Differentiate burnable materials with multiple instances - if diff_burnable_mats: - self._differentiate_burnable_mats() - self.materials = openmc.Materials( - model.geometry.get_all_materials().values() - ) - - # Clear out OpenMC, create task lists, distribute - openmc.reset_auto_ids() - self.burnable_mats, volume, nuclides = super()._get_burnable_mats() - self.local_mats = _distribute(self.burnable_mats) - - # Generate map from local materials => material index - self._mat_index_map = { - lm: self.burnable_mats.index(lm) for lm in self.local_mats} - - if self.prev_res is not None: - # Reload volumes into geometry - prev_results[-1].transfer_volumes(self.model) - - # Store previous results in operator - # Distribute reaction rates according to those tracked - # on this process - if comm.size == 1: - self.prev_res = prev_results - else: - self.prev_res = Results() - mat_indexes = _distribute(range(len(self.burnable_mats))) - for res_obj in prev_results: - new_res = res_obj.distribute(self.local_mats, mat_indexes) - self.prev_res.append(new_res) - - # Determine which nuclides have incident neutron data - self.nuclides_with_data = self._get_nuclides_with_data(cross_sections) - - # Select nuclides with data that are also in the chain - self._burnable_nucs = [nuc.name for nuc in self.chain.nuclides - if nuc.name in self.nuclides_with_data] - - # Extract number densities from the geometry / previous depletion run - super()._extract_number(self.local_mats, volume, nuclides, self.prev_res) - - # Create reaction rates array - self.reaction_rates = ReactionRates( - self.local_mats, self._burnable_nucs, self.chain.reactions) - - # Get classes to assist working with tallies - if reaction_rate_mode == "direct": - self._rate_helper = DirectReactionRateHelper( - self.reaction_rates.n_nuc, self.reaction_rates.n_react) - elif reaction_rate_mode == "flux": - if reaction_rate_opts is None: - reaction_rate_opts = {} - - # Ensure energy group boundaries were specified - if 'energies' not in reaction_rate_opts: - raise ValueError( - "Energy group boundaries must be specified in the " - "reaction_rate_opts argument when reaction_rate_mode is" - "set to 'flux'.") - - self._rate_helper = FluxCollapseHelper( - self.reaction_rates.n_nuc, - self.reaction_rates.n_react, - **reaction_rate_opts - ) - else: - raise ValueError("Invalid reaction rate mode.") - - if normalization_mode == "fission-q": - self._normalization_helper = ChainFissionHelper() - elif normalization_mode == "energy-deposition": - score = "heating" if self.settings.photon_transport else "heating-local" - self._normalization_helper = EnergyScoreHelper(score) - else: - self._normalization_helper = SourceRateHelper() - - # Select and create fission yield helper - fission_helper = self._fission_helpers[fission_yield_mode] - fission_yield_opts = ( - {} if fission_yield_opts is None else fission_yield_opts) - self._yield_helper = fission_helper.from_operator( - self, **fission_yield_opts) + super().__init__(model.materials, cross_sections, chain_file, prev_results, + diff_burnable_mats, normalization_mode, + fission_q, dilute_initial, + fission_yield_mode, fission_yield_opts, + reaction_rate_mode, reaction_rate_opts, + reduce_chain, reduce_chain_level) def __call__(self, vec, source_rate): """Runs a simulation. @@ -357,11 +267,12 @@ class Operator(OpenMCOperator): openmc.reset_auto_ids() # Update tally nuclides data in preparation for transport solve - nuclides = super()._get_tally_nuclides() + nuclides = self._get_reaction_nuclides() self._rate_helper.nuclides = nuclides self._normalization_helper.nuclides = nuclides self._yield_helper.update_tally_nuclides(nuclides) + # Run OpenMC openmc.lib.run() openmc.lib.reset_timers() @@ -416,6 +327,10 @@ class Operator(OpenMCOperator): cell.fill = [mat.clone() for i in range(cell.num_instances)] + self.materials = openmc.Materials( + model.geometry.get_all_materials().values() + ) + def initial_condition(self): """Performs final setup and returns initial condition. @@ -513,3 +428,62 @@ class Operator(OpenMCOperator): nuclides.add(name) return nuclides + + def _load_previous_results(self): + """Load reuslts from a previous depletion simulation""" + # Reload volumes into geometry + prev_results[-1].transfer_volumes(self.model) + + # Store previous results in operator + # Distribute reaction rates according to those tracked + # on this process + if comm.size == 1: + self.prev_res = prev_results + else: + self.prev_res = Results() + mat_indexes = _distribute(range(len(self.burnable_mats))) + for res_obj in prev_results: + new_res = res_obj.distribute(self.local_mats, mat_indexes) + self.prev_res.append(new_res) + + def _get_helper_classes(self, reaction_rate_mode, reaction_rate_opts, normalization_mode, fission_yield_mode, fission_yield_opts): + """Create the ``_rate_helper``, ``normalization_helper``, and + ``_yield_helper`` attributes""" + # Get classes to assist working with tallies + if reaction_rate_mode == "direct": + self._rate_helper = DirectReactionRateHelper( + self.reaction_rates.n_nuc, self.reaction_rates.n_react) + elif reaction_rate_mode == "flux": + if reaction_rate_opts is None: + reaction_rate_opts = {} + + # Ensure energy group boundaries were specified + if 'energies' not in reaction_rate_opts: + raise ValueError( + "Energy group boundaries must be specified in the " + "reaction_rate_opts argument when reaction_rate_mode is" + "set to 'flux'.") + + self._rate_helper = FluxCollapseHelper( + self.reaction_rates.n_nuc, + self.reaction_rates.n_react, + **reaction_rate_opts + ) + else: + raise ValueError("Invalid reaction rate mode.") + + if normalization_mode == "fission-q": + self._normalization_helper = ChainFissionHelper() + elif normalization_mode == "energy-deposition": + score = "heating" if self.settings.photon_transport else "heating-local" + self._normalization_helper = EnergyScoreHelper(score) + else: + self._normalization_helper = SourceRateHelper() + + # Select and create fission yield helper + fission_helper = self._fission_helpers[fission_yield_mode] + fission_yield_opts = ( + {} if fission_yield_opts is None else fission_yield_opts) + self._yield_helper = fission_helper.from_operator( + self, **fission_yield_opts) + From 6ff164131c12db9c9b7c3d79a93095593e29c9d9 Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 14 Jul 2022 12:53:41 -0500 Subject: [PATCH 053/131] clean up docstrings, function ordering --- openmc/deplete/openmc_operator.py | 141 +++++++++---- openmc/deplete/operator.py | 332 +++++++++++++++--------------- 2 files changed, 264 insertions(+), 209 deletions(-) diff --git a/openmc/deplete/openmc_operator.py b/openmc/deplete/openmc_operator.py index a2296d88a..0a64c6fee 100644 --- a/openmc/deplete/openmc_operator.py +++ b/openmc/deplete/openmc_operator.py @@ -7,7 +7,6 @@ This module implements functions used by both OpenMC transport operators as well import copy from abc import abstractmethod from collections import OrderedDict -import os import numpy as np from uncertainties import ufloat @@ -18,8 +17,6 @@ from openmc.mpi import comm from .abc import TransportOperator, OperatorResult from .atom_number import AtomNumber from .reaction_rates import ReactionRates -from .results import Results - __all__ = ["OpenMCOperator", "OperatorResult"] @@ -47,16 +44,11 @@ def _distribute(items): j += chunk_size class OpenMCOperator(TransportOperator): - """OpenMC transport operator for depletion. + """Abstrct class holding OpenMC-specific functions for running + depletion calculations. - Instances of this class can be used to perform depletion using OpenMC as the - transport operator. Normally, a user needn't call methods of this class - directly. Instead, an instance of this class is passed to an integrator - class, such as :class:`openmc.deplete.CECMIntegrator`. - - .. versionchanged:: 0.13.0 - The geometry and settings parameters have been replaced with a - model parameter that takes a :class:`~openmc.model.Model` object + Specific classes for running couples transport-depleton calculations or + depletion-only calculations are implemented as subclasses of OpenMCOperator Parameters ---------- @@ -68,6 +60,12 @@ class OpenMCOperator(TransportOperator): Results from a previous depletion calculation. If this argument is specified, the depletion calculation will start from the latest state in the previous results. + diff_burnable_mats : bool, optional + Whether to differentiate burnable materials with multiple instances. + Volumes are divided equally from the original material volume. + Default: False. + normalization_mode : str + Indicate how reaction rates should be normalized. fission_q : dict, optional Dictionary of nuclides and their fission Q values [eV]. If not given, values will be pulled from the ``chain_file``. Only applicable @@ -77,6 +75,30 @@ class OpenMCOperator(TransportOperator): in initial condition to ensure they exist in the decay chain. Only done for nuclides with reaction rates. Defaults to 1.0e3. + fission_yield_mode : str + Key indicating what fission product yield scheme to use. + fission_yield_opts : dict of str to option, optional + Optional arguments to pass to the helper determined by + ``fission_yield_mode``. Will be passed directly on to the + helper. Passing a value of None will use the defaults for + the associated helper. + reaction_rate_mode : str, optional + Indicate how one-group reaction rates should be calculated. + reaction_rate_opts : dict, optional + Keyword arguments that are passed to the reaction rate helper class. + reduce_chain : bool, optional + If True, use :meth:`openmc.deplete.Chain.reduce` to reduce the + depletion chain up to ``reduce_chain_level``. Default is False. + + .. versionadded:: 0.12 + reduce_chain_level : int, optional + Depth of the search when reducing the depletion chain. Only used + if ``reduce_chain`` evaluates to true. The default value of + ``None`` implies no limit on the depth. + + .. versionadded:: 0.12 + + Attributes ---------- @@ -112,9 +134,6 @@ class OpenMCOperator(TransportOperator): prev_res : Results or None Results from a previous depletion calculation. ``None`` if no results are to be used. - cleanup_when_done : bool - Whether to finalize and clear the shared library memory when the - depletion operation is complete. Defaults to clearing the library. """ def __init__(self, materials=None, cross_sections=None, chain_file=None, prev_results=None, diff_burnable_mats=False, normalization_mode=None, fission_q=None, dilute_initial=0.0, fission_yield_mode=None, fission_yield_opts=None, @@ -170,7 +189,11 @@ class OpenMCOperator(TransportOperator): self.reaction_rates = ReactionRates( self.local_mats, self._burnable_nucs, self.chain.reactions) - self._get_helper_classes(reaction_rate_mode, reaction_rate_opts, normalization_mode, fission_yield_mode, fission_yield_opts) + self._get_helper_classes(reaction_rate_mode, normalization_mode, fission_yield_mode, reaction_rate_opts, fission_yield_opts) + + @abstractmethod + def _differentiate_burnable_mats(self): + """Assign distribmats for each burnable material""" def _get_burnable_mats(self): """Determine depletable materials, volumes, and nuclides @@ -221,6 +244,15 @@ class OpenMCOperator(TransportOperator): return burnable_mats, volume, nuclides + + @abstractmethod + def _load_previous_results(self): + """Load reuslts from a previous depletion simulation""" + + @abstractmethod + def _get_nuclides_with_data(self, cross_sections): + """Find nuclides with cross section data""" + def _extract_number(self, local_mats, volume, all_nuclides, prev_res=None): """Construct AtomNumber using geometry @@ -304,12 +336,37 @@ class OpenMCOperator(TransportOperator): self.number.set_atom_density(mat_id, nuclide, atom_per_cc) + @abstractmethod + def _get_helper_classes(self, reaction_rate_mode, normalization_mode, fission_yield_mode, reaction_rate_opts, fission_yield_opts): + """Create the ``_rate_helper``, ``_normalization_helper``, and + ``_yield_helper`` objects. + + Parameters + ---------- + reaction_rate_mode : str + Indicates the subclass of :class:`ReactionRateHelper` to + instantiate. + normalization_mode : str + Indicates the subclass of :class:`NormalizationHelper` to + instatiate. + fission_yield_mode : str + Indicates the subclass of :class:`FissionYieldHelper` to instatiate. + reaction_rate_opts : dict + Keyword arguments that are passed to the :class:`ReactionRateHelper` + subclass. + fission_yield_opts : dict + Keyword arguments that are passed to the :class:`FissionYieldHelper` + subclass. + + """ + def initial_condition(self, materials): """Performs final setup and returns initial condition. Parameters ---------- - materials : ??? + materials : list of str + list of material IDs Returns ------- @@ -328,6 +385,22 @@ class OpenMCOperator(TransportOperator): # Return number density vector return list(self.number.get_mat_slice(np.s_[:])) + def _update_materials_and_nuclides(self, vec): + """Update the number density, material compositions, and nuclide + lists in helper objects""" + # Update the number densities regardless of the source rate + self.number.set_density(vec) + self._update_materials() + + # Prevent OpenMC from complaining about re-creating tallies + openmc.reset_auto_ids() + + # Update tally nuclides data in preparation for transport solve + nuclides = self._get_reaction_nuclides() + self._rate_helper.nuclides = nuclides + self._normalization_helper.nuclides = nuclides + self._yield_helper.update_tally_nuclides(nuclides) + @abstractmethod def _update_materials(self): """Updates material compositions in OpenMC on all processes.""" @@ -335,16 +408,16 @@ class OpenMCOperator(TransportOperator): def _get_reaction_nuclides(self): """Determine nuclides that should be tallied for reaction rates. - This method returns a list of all nuclides that have neutron data and - are listed in the depletion chain. Technically, we should tally nuclides - that may not appear in the depletion chain because we still need to get - the fission reaction rate for these nuclides in order to normalize - power, but that is left as a future exercise. + This method returns a list of all nuclides that have cross section data + and are listed in the depletion chain. Technically, we should count + nuclides that may not appear in the depletion chain because we still + need to get the fission reaction rate for these nuclides in order to + normalize power, but that is left as a future exercise. Returns ------- list of str - Tally nuclides + Nuclides with reaction rates """ nuc_set = set() @@ -371,7 +444,7 @@ class OpenMCOperator(TransportOperator): else: nuc_list = None - # Store list of tally nuclides on each process + # Store list of nuclides on each process nuc_list = comm.bcast(nuc_list) return [nuc for nuc in nuc_list if nuc in self.chain] @@ -450,26 +523,6 @@ class OpenMCOperator(TransportOperator): return rates - - @abstractmethod - def _get_helper_classes(self, reaction_rate_mode, reaction_rate_opts, normalization_mode, fission_yield_mode, fission_yield_opts): - """Create the ``_rate_helper``, ``normalization_helper``, and - ``_yield_helper`` attributes""" - - @abstractmethod - def _load_previous_results(self): - """Load reuslts from a previous depletion simulation""" - - @abstractmethod - def _get_nuclides_with_data(self, cross_sections): - """Find nuclides with cross section data""" - - @abstractmethod - def _differentiate_burnable_mats(self): - """Assign distribmats for each burnable material - - """ - def get_results_info(self): """Returns volume list, material lists, and nuc lists. diff --git a/openmc/deplete/operator.py b/openmc/deplete/operator.py index 2770494ba..30563623c 100644 --- a/openmc/deplete/operator.py +++ b/openmc/deplete/operator.py @@ -8,7 +8,6 @@ densities is all done in-memory instead of through the filesystem. """ import copy -from collections import OrderedDict import os from warnings import warn @@ -21,11 +20,9 @@ from openmc.data import DataLibrary from openmc.exceptions import DataError import openmc.lib from openmc.mpi import comm -from .abc import TransportOperator, OperatorResult -from .atom_number import AtomNumber +from .abc import OperatorResult from .chain import _find_chain_file from .openmc_operator import OpenMCOperator, _distribute -from .reaction_rates import ReactionRates from .results import Results from .helpers import ( DirectReactionRateHelper, ChainFissionHelper, ConstantFissionYieldHelper, @@ -229,81 +226,8 @@ class Operator(OpenMCOperator): reaction_rate_mode, reaction_rate_opts, reduce_chain, reduce_chain_level) - def __call__(self, vec, source_rate): - """Runs a simulation. - - Simulation will abort under the following circumstances: - - 1) No energy is computed using OpenMC tallies. - - Parameters - ---------- - vec : list of numpy.ndarray - Total atoms to be used in function. - source_rate : float - Power in [W] or source rate in [neutron/sec] - - Returns - ------- - openmc.deplete.OperatorResult - Eigenvalue and reaction rates resulting from transport operator - - """ - # Reset results in OpenMC - openmc.lib.reset() - - # Update the number densities regardless of the source rate - self.number.set_density(vec) - self._update_materials() - - # If the source rate is zero, return zero reaction rates without running - # a transport solve - if source_rate == 0.0: - rates = self.reaction_rates.copy() - rates.fill(0.0) - return OperatorResult(ufloat(0.0, 0.0), rates) - - # Prevent OpenMC from complaining about re-creating tallies - openmc.reset_auto_ids() - - # Update tally nuclides data in preparation for transport solve - nuclides = self._get_reaction_nuclides() - self._rate_helper.nuclides = nuclides - self._normalization_helper.nuclides = nuclides - self._yield_helper.update_tally_nuclides(nuclides) - - - # Run OpenMC - openmc.lib.run() - openmc.lib.reset_timers() - - # Extract results - rates = self._calculate_reaction_rates(source_rate) - - # Get k and uncertainty - keff = ufloat(*openmc.lib.keff()) - - op_result = OperatorResult(keff, rates) - - return copy.deepcopy(op_result) - - @staticmethod - def write_bos_data(step): - """Write a state-point file with beginning of step data - - Parameters - ---------- - step : int - Current depletion step including restarts - """ - openmc.lib.statepoint_write( - "openmc_simulation_n{}.h5".format(step), - write_source=False) - def _differentiate_burnable_mats(self): - """Assign distribmats for each burnable material - - """ + """Assign distribmats for each burnable material""" # Count the number of instances for each cell and material self.geometry.determine_paths(instances_only=True) @@ -331,6 +255,97 @@ class Operator(OpenMCOperator): model.geometry.get_all_materials().values() ) + def _load_previous_results(self): + """Load results from a previous depletion simulation""" + # Reload volumes into geometry + prev_results[-1].transfer_volumes(self.model) + + # Store previous results in operator + # Distribute reaction rates according to those tracked + # on this process + if comm.size == 1: + self.prev_res = prev_results + else: + self.prev_res = Results() + mat_indexes = _distribute(range(len(self.burnable_mats))) + for res_obj in prev_results: + new_res = res_obj.distribute(self.local_mats, mat_indexes) + self.prev_res.append(new_res) + + def _get_nuclides_with_data(self, cross_sections): + """Loads cross_sections.xml file to find nuclides with neutron data""" + nuclides = set() + data_lib = DataLibrary.from_xml(cross_sections) + for library in data_lib.libraries: + if library['type'] != 'neutron': + continue + for name in library['materials']: + if name not in nuclides: + nuclides.add(name) + + return nuclides + + def _get_helper_classes(self, reaction_rate_mode, normalization_mode, fission_yield_mode, + reaction_rate_opts, fission_yield_opts): + """Create the ``_rate_helper``, ``_normalization_helper``, and + ``_yield_helper`` objects. + + Parameters + ---------- + reaction_rate_mode : str + Indicates the subclass of :class:`ReactionRateHelper` to + instantiate. + normalization_mode : str + Indicates the subclass of :class:`NormalizationHelper` to + instatiate. + fission_yield_mode : str + Indicates the subclass of :class:`FissionYieldHelper` to instatiate. + reaction_rate_opts : dict + Keyword arguments that are passed to the :class:`ReactionRateHelper` + subclass. + fission_yield_opts : dict + Keyword arguments that are passed to the :class:`FissionYieldHelper` + subclass. + + """ + # Get classes to assist working with tallies + if reaction_rate_mode == "direct": + self._rate_helper = DirectReactionRateHelper( + self.reaction_rates.n_nuc, self.reaction_rates.n_react) + elif reaction_rate_mode == "flux": + if reaction_rate_opts is None: + reaction_rate_opts = {} + + # Ensure energy group boundaries were specified + if 'energies' not in reaction_rate_opts: + raise ValueError( + "Energy group boundaries must be specified in the " + "reaction_rate_opts argument when reaction_rate_mode is" + "set to 'flux'.") + + self._rate_helper = FluxCollapseHelper( + self.reaction_rates.n_nuc, + self.reaction_rates.n_react, + **reaction_rate_opts + ) + else: + raise ValueError("Invalid reaction rate mode.") + + if normalization_mode == "fission-q": + self._normalization_helper = ChainFissionHelper() + elif normalization_mode == "energy-deposition": + score = "heating" if self.settings.photon_transport else "heating-local" + self._normalization_helper = EnergyScoreHelper(score) + else: + self._normalization_helper = SourceRateHelper() + + # Select and create fission yield helper + fission_helper = self._fission_helpers[fission_yield_mode] + fission_yield_opts = ( + {} if fission_yield_opts is None else fission_yield_opts) + self._yield_helper = fission_helper.from_operator( + self, **fission_yield_opts) + def initial_condition(self): """Performs final setup and returns initial condition. @@ -357,10 +372,66 @@ class Operator(OpenMCOperator): return super().initial_condition(materials) - def finalize(self): - """Finalize a depletion simulation and release resources.""" - if self.cleanup_when_done: - openmc.lib.finalize() + def _generate_materials_xml(self): + """Creates materials.xml from self.number. + + Due to uncertainty with how MPI interacts with OpenMC API, this + constructs the XML manually. The long term goal is to do this + through direct memory writing. + + """ + # Sort nuclides according to order in AtomNumber object + nuclides = list(self.number.nuclides) + for mat in self.materials: + mat._nuclides.sort(key=lambda x: nuclides.index(x[0])) + + self.materials.export_to_xml() + + def __call__(self, vec, source_rate): + """Runs a simulation. + + Simulation will abort under the following circumstances: + + 1) No energy is computed using OpenMC tallies. + + Parameters + ---------- + vec : list of numpy.ndarray + Total atoms to be used in function. + source_rate : float + Power in [W] or source rate in [neutron/sec] + + Returns + ------- + openmc.deplete.OperatorResult + Eigenvalue and reaction rates resulting from transport operator + + """ + # Reset results in OpenMC + openmc.lib.reset() + + self._update_materials_and_nuclides(vec) + + # If the source rate is zero, return zero reaction rates without running + # a transport solve + if source_rate == 0.0: + rates = self.reaction_rates.copy() + rates.fill(0.0) + return OperatorResult(ufloat(0.0, 0.0), rates) + + # Run OpenMC + openmc.lib.run() + openmc.lib.reset_timers() + + # Extract results + rates = self._calculate_reaction_rates(source_rate) + + # Get k and uncertainty + keff = ufloat(*openmc.lib.keff()) + + op_result = OperatorResult(keff, rates) + + return copy.deepcopy(op_result) def _update_materials(self): """Updates material compositions in OpenMC on all processes.""" @@ -401,89 +472,20 @@ class Operator(OpenMCOperator): #TODO Update densities on the Python side, otherwise the # summary.h5 file contains densities at the first time step - def _generate_materials_xml(self): - """Creates materials.xml from self.number. - - Due to uncertainty with how MPI interacts with OpenMC API, this - constructs the XML manually. The long term goal is to do this - through direct memory writing. + @staticmethod + def write_bos_data(step): + """Write a state-point file with beginning of step data + Parameters + ---------- + step : int + Current depletion step including restarts """ - # Sort nuclides according to order in AtomNumber object - nuclides = list(self.number.nuclides) - for mat in self.materials: - mat._nuclides.sort(key=lambda x: nuclides.index(x[0])) - - self.materials.export_to_xml() - - def _get_nuclides_with_data(self, cross_sections): - """Loads cross_sections.xml file to find nuclides with neutron data""" - nuclides = set() - data_lib = DataLibrary.from_xml(cross_sections) - for library in data_lib.libraries: - if library['type'] != 'neutron': - continue - for name in library['materials']: - if name not in nuclides: - nuclides.add(name) - - return nuclides - - def _load_previous_results(self): - """Load reuslts from a previous depletion simulation""" - # Reload volumes into geometry - prev_results[-1].transfer_volumes(self.model) - - # Store previous results in operator - # Distribute reaction rates according to those tracked - # on this process - if comm.size == 1: - self.prev_res = prev_results - else: - self.prev_res = Results() - mat_indexes = _distribute(range(len(self.burnable_mats))) - for res_obj in prev_results: - new_res = res_obj.distribute(self.local_mats, mat_indexes) - self.prev_res.append(new_res) - - def _get_helper_classes(self, reaction_rate_mode, reaction_rate_opts, normalization_mode, fission_yield_mode, fission_yield_opts): - """Create the ``_rate_helper``, ``normalization_helper``, and - ``_yield_helper`` attributes""" - # Get classes to assist working with tallies - if reaction_rate_mode == "direct": - self._rate_helper = DirectReactionRateHelper( - self.reaction_rates.n_nuc, self.reaction_rates.n_react) - elif reaction_rate_mode == "flux": - if reaction_rate_opts is None: - reaction_rate_opts = {} - - # Ensure energy group boundaries were specified - if 'energies' not in reaction_rate_opts: - raise ValueError( - "Energy group boundaries must be specified in the " - "reaction_rate_opts argument when reaction_rate_mode is" - "set to 'flux'.") - - self._rate_helper = FluxCollapseHelper( - self.reaction_rates.n_nuc, - self.reaction_rates.n_react, - **reaction_rate_opts - ) - else: - raise ValueError("Invalid reaction rate mode.") - - if normalization_mode == "fission-q": - self._normalization_helper = ChainFissionHelper() - elif normalization_mode == "energy-deposition": - score = "heating" if self.settings.photon_transport else "heating-local" - self._normalization_helper = EnergyScoreHelper(score) - else: - self._normalization_helper = SourceRateHelper() - - # Select and create fission yield helper - fission_helper = self._fission_helpers[fission_yield_mode] - fission_yield_opts = ( - {} if fission_yield_opts is None else fission_yield_opts) - self._yield_helper = fission_helper.from_operator( - self, **fission_yield_opts) + openmc.lib.statepoint_write( + "openmc_simulation_n{}.h5".format(step), + write_source=False) + def finalize(self): + """Finalize a depletion simulation and release resources.""" + if self.cleanup_when_done: + openmc.lib.finalize() From 0a05f9dcb4320ae0c7dbd88547ca2bf9b35b68f5 Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 14 Jul 2022 12:54:51 -0500 Subject: [PATCH 054/131] pep8 fixes --- openmc/deplete/openmc_operator.py | 49 ++++++++++++++++++++++------- openmc/deplete/operator.py | 52 ++++++++++++++++++++++--------- 2 files changed, 75 insertions(+), 26 deletions(-) diff --git a/openmc/deplete/openmc_operator.py b/openmc/deplete/openmc_operator.py index 0a64c6fee..2f1b93294 100644 --- a/openmc/deplete/openmc_operator.py +++ b/openmc/deplete/openmc_operator.py @@ -43,6 +43,7 @@ def _distribute(items): return items[j:j + chunk_size] j += chunk_size + class OpenMCOperator(TransportOperator): """Abstrct class holding OpenMC-specific functions for running depletion calculations. @@ -136,12 +137,25 @@ class OpenMCOperator(TransportOperator): results are to be used. """ - def __init__(self, materials=None, cross_sections=None, chain_file=None, prev_results=None, diff_burnable_mats=False, normalization_mode=None, fission_q=None, dilute_initial=0.0, fission_yield_mode=None, fission_yield_opts=None, - reaction_rate_mode=None, reaction_rate_opts=None, - reduce_chain=False, reduce_chain_level=None): + def __init__( + self, + materials=None, + cross_sections=None, + chain_file=None, + prev_results=None, + diff_burnable_mats=False, + normalization_mode=None, + fission_q=None, + dilute_initial=0.0, + fission_yield_mode=None, + fission_yield_opts=None, + reaction_rate_mode=None, + reaction_rate_opts=None, + reduce_chain=False, + reduce_chain_level=None): super().__init__(chain_file, fission_q, dilute_initial, prev_results) - self.round_number=False + self.round_number = False self.materials = materials self.cross_sections = cross_sections @@ -172,8 +186,8 @@ class OpenMCOperator(TransportOperator): if self.prev_res is not None: self._load_previous_results() - - self.nuclides_with_data = self._get_nuclides_with_data(self.cross_sections) + self.nuclides_with_data = self._get_nuclides_with_data( + self.cross_sections) # Select nuclides with data that are also in the chain self._burnable_nucs = [nuc.name for nuc in self.chain.nuclides @@ -189,7 +203,12 @@ class OpenMCOperator(TransportOperator): self.reaction_rates = ReactionRates( self.local_mats, self._burnable_nucs, self.chain.reactions) - self._get_helper_classes(reaction_rate_mode, normalization_mode, fission_yield_mode, reaction_rate_opts, fission_yield_opts) + self._get_helper_classes( + reaction_rate_mode, + normalization_mode, + fission_yield_mode, + reaction_rate_opts, + fission_yield_opts) @abstractmethod def _differentiate_burnable_mats(self): @@ -244,7 +263,6 @@ class OpenMCOperator(TransportOperator): return burnable_mats, volume, nuclides - @abstractmethod def _load_previous_results(self): """Load reuslts from a previous depletion simulation""" @@ -268,11 +286,14 @@ class OpenMCOperator(TransportOperator): Results from a previous depletion calculation """ - self.number = AtomNumber(local_mats, all_nuclides, volume, len(self.chain)) + self.number = AtomNumber( + local_mats, all_nuclides, volume, len( + self.chain)) if self.dilute_initial != 0.0: for nuc in self._burnable_nucs: - self.number.set_atom_density(np.s_[:], nuc, self.dilute_initial) + self.number.set_atom_density( + np.s_[:], nuc, self.dilute_initial) # Now extract and store the number densities # From the geometry if no previous depletion results @@ -337,7 +358,13 @@ class OpenMCOperator(TransportOperator): self.number.set_atom_density(mat_id, nuclide, atom_per_cc) @abstractmethod - def _get_helper_classes(self, reaction_rate_mode, normalization_mode, fission_yield_mode, reaction_rate_opts, fission_yield_opts): + def _get_helper_classes( + self, + reaction_rate_mode, + normalization_mode, + fission_yield_mode, + reaction_rate_opts, + fission_yield_opts): """Create the ``_rate_helper``, ``_normalization_helper``, and ``_yield_helper`` objects. diff --git a/openmc/deplete/operator.py b/openmc/deplete/operator.py index 30563623c..7149cddad 100644 --- a/openmc/deplete/operator.py +++ b/openmc/deplete/operator.py @@ -32,6 +32,7 @@ from .helpers import ( __all__ = ["Operator", "OperatorResult"] + def _find_cross_sections(model): """Determine cross sections to use for depletion""" if model.materials and model.materials.cross_sections is not None: @@ -219,12 +220,21 @@ class Operator(OpenMCOperator): self.cleanup_when_done = True - super().__init__(model.materials, cross_sections, chain_file, prev_results, - diff_burnable_mats, normalization_mode, - fission_q, dilute_initial, - fission_yield_mode, fission_yield_opts, - reaction_rate_mode, reaction_rate_opts, - reduce_chain, reduce_chain_level) + super().__init__( + model.materials, + cross_sections, + chain_file, + prev_results, + diff_burnable_mats, + normalization_mode, + fission_q, + dilute_initial, + fission_yield_mode, + fission_yield_opts, + reaction_rate_mode, + reaction_rate_opts, + reduce_chain, + reduce_chain_level) def _differentiate_burnable_mats(self): """Assign distribmats for each burnable material""" @@ -240,7 +250,7 @@ class Operator(OpenMCOperator): for mat in distribmats: if mat.volume is None: raise RuntimeError("Volume not specified for depletable " - "material with ID={}.".format(mat.id)) + "material with ID={}.".format(mat.id)) mat.volume /= mat.num_instances if distribmats: @@ -252,8 +262,8 @@ class Operator(OpenMCOperator): for i in range(cell.num_instances)] self.materials = openmc.Materials( - model.geometry.get_all_materials().values() - ) + model.geometry.get_all_materials().values() + ) def _load_previous_results(self): """Load results from a previous depletion simulation""" @@ -285,8 +295,13 @@ class Operator(OpenMCOperator): return nuclides - def _get_helper_classes(self, reaction_rate_mode, normalization_mode, fission_yield_mode, - reaction_rate_opts, fission_yield_opts): + def _get_helper_classes( + self, + reaction_rate_mode, + normalization_mode, + fission_yield_mode, + reaction_rate_opts, + fission_yield_opts): """Create the ``_rate_helper``, ``_normalization_helper``, and ``_yield_helper`` objects. @@ -459,17 +474,24 @@ class Operator(OpenMCOperator): densities.append(val) else: # Only output warnings if values are significantly - # negative. CRAM does not guarantee positive values. + # negative. CRAM does not guarantee positive + # values. if val < -1.0e-21: - print("WARNING: nuclide ", nuc, " in material ", mat, - " is negative (density = ", val, " at/barn-cm)") + print( + "WARNING: nuclide ", + nuc, + " in material ", + mat, + " is negative (density = ", + val, + " at/barn-cm)") number_i[mat, nuc] = 0.0 # Update densities on C API side mat_internal = openmc.lib.materials[int(mat)] mat_internal.set_densities(nuclides, densities) - #TODO Update densities on the Python side, otherwise the + # TODO Update densities on the Python side, otherwise the # summary.h5 file contains densities at the first time step @staticmethod From 39472e286c34f01e1379c457beacf9dc84cd3abc Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 14 Jul 2022 12:56:19 -0500 Subject: [PATCH 055/131] add OpenMCOperator to docs, fix referecne to TalliedFissionYieldHelper --- docs/source/pythonapi/deplete.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/pythonapi/deplete.rst b/docs/source/pythonapi/deplete.rst index 9f7d8c447..11062cd87 100644 --- a/docs/source/pythonapi/deplete.rst +++ b/docs/source/pythonapi/deplete.rst @@ -204,6 +204,7 @@ prior to depleting materials :template: mycallable.rst abc.TransportOperator + openmc_operator.OpenMCOperator The following classes are abstract classes used to pass information from OpenMC simulations back on to the :class:`abc.TransportOperator` @@ -216,7 +217,7 @@ OpenMC simulations back on to the :class:`abc.TransportOperator` abc.NormalizationHelper abc.FissionYieldHelper abc.ReactionRateHelper - abc.TalliedFissionYieldHelper + helpers.TalliedFissionYieldHelper Custom integrators or depletion solvers can be developed by subclassing from the following abstract base classes: From 47344ca5cb18441e59910c734e2a7c5a88f6e7d1 Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 14 Jul 2022 14:22:33 -0500 Subject: [PATCH 056/131] spell checker typo fixes --- openmc/deplete/openmc_operator.py | 8 ++++---- openmc/deplete/operator.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openmc/deplete/openmc_operator.py b/openmc/deplete/openmc_operator.py index 2f1b93294..6b7ff2920 100644 --- a/openmc/deplete/openmc_operator.py +++ b/openmc/deplete/openmc_operator.py @@ -45,10 +45,10 @@ def _distribute(items): class OpenMCOperator(TransportOperator): - """Abstrct class holding OpenMC-specific functions for running + """Abstract class holding OpenMC-specific functions for running depletion calculations. - Specific classes for running couples transport-depleton calculations or + Specific classes for running coupled transport-depletion calculations or depletion-only calculations are implemented as subclasses of OpenMCOperator Parameters @@ -265,7 +265,7 @@ class OpenMCOperator(TransportOperator): @abstractmethod def _load_previous_results(self): - """Load reuslts from a previous depletion simulation""" + """Load results from a previous depletion simulation""" @abstractmethod def _get_nuclides_with_data(self, cross_sections): @@ -375,7 +375,7 @@ class OpenMCOperator(TransportOperator): instantiate. normalization_mode : str Indicates the subclass of :class:`NormalizationHelper` to - instatiate. + instantiate. fission_yield_mode : str Indicates the subclass of :class:`FissionYieldHelper` to instatiate. reaction_rate_opts : dict diff --git a/openmc/deplete/operator.py b/openmc/deplete/operator.py index 7149cddad..a86eb182f 100644 --- a/openmc/deplete/operator.py +++ b/openmc/deplete/operator.py @@ -314,7 +314,7 @@ class Operator(OpenMCOperator): Indicates the subclass of :class:`NormalizationHelper` to instatiate. fission_yield_mode : str - Indicates the subclass of :class:`FissionYieldHelper` to instatiate. + Indicates the subclass of :class:`FissionYieldHelper` to instantiate. reaction_rate_opts : dict Keyword arguments that are passed to the :class:`ReactionRateHelper` subclass. From 7b041114209c6073793ad8c33d87db7e07afda6b Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 14 Jul 2022 14:23:11 -0500 Subject: [PATCH 057/131] add small blurb about OpenMCOperator; move TalliedFissionYieldHelper to different section --- docs/source/pythonapi/deplete.rst | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/source/pythonapi/deplete.rst b/docs/source/pythonapi/deplete.rst index 11062cd87..d09a665d8 100644 --- a/docs/source/pythonapi/deplete.rst +++ b/docs/source/pythonapi/deplete.rst @@ -188,6 +188,7 @@ total system energy. helpers.EnergyScoreHelper helpers.FissionYieldCutoffHelper helpers.FluxCollapseHelper + helpers.TalliedFissionYieldHelper Abstract Base Classes --------------------- @@ -204,7 +205,16 @@ prior to depleting materials :template: mycallable.rst abc.TransportOperator - openmc_operator.OpenMCOperator + +Methods common to OpenMC-specific implementations are stored in :class:`openmc_operator.OpenMCOperator` + +.. autosummary:: + :toctree: generated + :nosignatures: + :template: mycallable.rst + + abc.TransportOperator + The following classes are abstract classes used to pass information from OpenMC simulations back on to the :class:`abc.TransportOperator` @@ -217,7 +227,6 @@ OpenMC simulations back on to the :class:`abc.TransportOperator` abc.NormalizationHelper abc.FissionYieldHelper abc.ReactionRateHelper - helpers.TalliedFissionYieldHelper Custom integrators or depletion solvers can be developed by subclassing from the following abstract base classes: From 80fbcdcdf380e583c346d140f0fa8724b526cfa9 Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 14 Jul 2022 15:14:35 -0500 Subject: [PATCH 058/131] move intermediate classes to their own section in the api docs --- docs/source/pythonapi/deplete.rst | 38 +++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/docs/source/pythonapi/deplete.rst b/docs/source/pythonapi/deplete.rst index d09a665d8..e275f38f2 100644 --- a/docs/source/pythonapi/deplete.rst +++ b/docs/source/pythonapi/deplete.rst @@ -188,8 +188,36 @@ total system energy. helpers.EnergyScoreHelper helpers.FissionYieldCutoffHelper helpers.FluxCollapseHelper + + +Intermediate Classes +-------------------- + +Specific implementations of abstract base classes may utilize some of +the same methods and data structures. These methods and data are stored +in intermediate classes. + +Methods common to tally-based implementation of :class:`FissionYieldHelper` +are stored in :class:`helpers.TalliedFissionYieldHelper` + +.. autosummary:: + :toctree: generated + :nosignatures: + :template: myclass.rst + helpers.TalliedFissionYieldHelper +Methods common to OpenMC-specific implementations of :class:`TransportOperator` +are stored in :class:`openmc_operator.OpenMCOperator` + +.. autosummary:: + :toctree: generated + :nosignatures: + :template: mycallable.rst + + openmc_operator.OpenMCOperator + + Abstract Base Classes --------------------- @@ -206,16 +234,6 @@ prior to depleting materials abc.TransportOperator -Methods common to OpenMC-specific implementations are stored in :class:`openmc_operator.OpenMCOperator` - -.. autosummary:: - :toctree: generated - :nosignatures: - :template: mycallable.rst - - abc.TransportOperator - - The following classes are abstract classes used to pass information from OpenMC simulations back on to the :class:`abc.TransportOperator` From 86caa75df49e63f4091ec6ac4423c8fe1dbc97a6 Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 14 Jul 2022 16:30:36 -0500 Subject: [PATCH 059/131] Add an alternate constructor using an openmc Model instance --- openmc/deplete/flux_operator.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/flux_operator.py index 5e23ec203..014770551 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/flux_operator.py @@ -105,6 +105,13 @@ class FluxDepletionOperator(TransportOperator): results are to be used. """ + # Alternate constructor using a full-fledges Model object + #def __init__(self, model, micro_xs, ...): + # ... + # mode.materials = openmc.Materials(model.geometry.get_all_materials().values()) + # super().__init__(model.materials, ...) + + def __init__(self, volume, nuclides, @@ -131,7 +138,7 @@ class FluxDepletionOperator(TransportOperator): materials = self._consolidate_nuclides_to_material(nuclides, volume) diff_burnable_mats=False - # super().__init__(materials, diff_burnable_mats, chain_file, fission_q, dilute_initial, prev_results, helper_class_kwargs) + # super().__init__(materials, cross_sections, diff_burnable_mats, chain_file, fission_q, dilute_initial, prev_results, helper_class_kwargs) ## this part goes to OpenMCOperator From 8bf4465b27ac32b2e1a7d152e93276620541d39d Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 14 Jul 2022 16:50:05 -0500 Subject: [PATCH 060/131] docstring updates --- openmc/deplete/openmc_operator.py | 49 +++++++++++++++++++++---------- openmc/deplete/operator.py | 25 ++++++++++++++-- 2 files changed, 56 insertions(+), 18 deletions(-) diff --git a/openmc/deplete/openmc_operator.py b/openmc/deplete/openmc_operator.py index 6b7ff2920..047a7f967 100644 --- a/openmc/deplete/openmc_operator.py +++ b/openmc/deplete/openmc_operator.py @@ -53,6 +53,11 @@ class OpenMCOperator(TransportOperator): Parameters ---------- + materials : openmc.Materials + List of all materials in the model + cross_sections : str or pandas.DataFrame + Path to continuous energy cross section library, or object containing + one-group cross-sections. chain_file : str, optional Path to the depletion chain XML file. Defaults to the file listed under ``depletion_chain`` in @@ -68,9 +73,7 @@ class OpenMCOperator(TransportOperator): normalization_mode : str Indicate how reaction rates should be normalized. fission_q : dict, optional - Dictionary of nuclides and their fission Q values [eV]. If not given, - values will be pulled from the ``chain_file``. Only applicable - if ``"normalization_mode" == "fission-q"`` + Dictionary of nuclides and their fission Q values [eV]. dilute_initial : float, optional Initial atom density [atoms/cm^3] to add for nuclides that are zero in initial condition to ensure they exist in the decay chain. @@ -90,25 +93,16 @@ class OpenMCOperator(TransportOperator): reduce_chain : bool, optional If True, use :meth:`openmc.deplete.Chain.reduce` to reduce the depletion chain up to ``reduce_chain_level``. Default is False. - - .. versionadded:: 0.12 reduce_chain_level : int, optional Depth of the search when reducing the depletion chain. Only used if ``reduce_chain`` evaluates to true. The default value of ``None`` implies no limit on the depth. - .. versionadded:: 0.12 - - Attributes ---------- - model : openmc.model.Model - OpenMC model object - geometry : openmc.Geometry - OpenMC geometry object - settings : openmc.Settings - OpenMC settings object + materials : openmc.Materials + All materials present in the model dilute_initial : float Initial atom density [atoms/cm^3] to add for nuclides that are zero in initial condition to ensure they exist in the decay @@ -135,6 +129,7 @@ class OpenMCOperator(TransportOperator): prev_res : Results or None Results from a previous depletion calculation. ``None`` if no results are to be used. + """ def __init__( @@ -269,7 +264,20 @@ class OpenMCOperator(TransportOperator): @abstractmethod def _get_nuclides_with_data(self, cross_sections): - """Find nuclides with cross section data""" + """Find nuclides with cross section data + + Parameters + ---------- + cross_sections : str or pandas.DataFrame + Path to continuous energy cross section library, or object + containing one-group cross-sections. + + Returns + ------- + nuclides : set of str + Set of nuclide names that have cross secton data + + """ def _extract_number(self, local_mats, volume, all_nuclides, prev_res=None): """Construct AtomNumber using geometry @@ -399,6 +407,7 @@ class OpenMCOperator(TransportOperator): ------- list of numpy.ndarray Total density for initial conditions. + """ self._rate_helper.generate_tallies(materials, self.chain.reactions) @@ -414,7 +423,15 @@ class OpenMCOperator(TransportOperator): def _update_materials_and_nuclides(self, vec): """Update the number density, material compositions, and nuclide - lists in helper objects""" + lists in helper objects + + Parameters + ---------- + vec : list of numpy.ndarray + Total atoms. + + """ + # Update the number densities regardless of the source rate self.number.set_density(vec) self._update_materials() diff --git a/openmc/deplete/operator.py b/openmc/deplete/operator.py index a86eb182f..d8b84f5a0 100644 --- a/openmc/deplete/operator.py +++ b/openmc/deplete/operator.py @@ -34,7 +34,14 @@ __all__ = ["Operator", "OperatorResult"] def _find_cross_sections(model): - """Determine cross sections to use for depletion""" + """Determine cross sections to use for depletion + + Parameters + ---------- + model : openmc.model.Model + Reactor model + + """ if model.materials and model.materials.cross_sections is not None: # Prefer info from Model class if available return model.materials.cross_sections @@ -283,7 +290,19 @@ class Operator(OpenMCOperator): self.prev_res.append(new_res) def _get_nuclides_with_data(self, cross_sections): - """Loads cross_sections.xml file to find nuclides with neutron data""" + """Loads cross_sections.xml file to find nuclides with neutron data + + Parameters + ---------- + cross_sections : str + Path to cross_sections.xml file + + Returns + ------- + nuclides : set of str + Set of nuclide names that have cross secton data + + """ nuclides = set() data_lib = DataLibrary.from_xml(cross_sections) for library in data_lib.libraries: @@ -368,6 +387,7 @@ class Operator(OpenMCOperator): ------- list of numpy.ndarray Total density for initial conditions. + """ # Create XML files @@ -502,6 +522,7 @@ class Operator(OpenMCOperator): ---------- step : int Current depletion step including restarts + """ openmc.lib.statepoint_write( "openmc_simulation_n{}.h5".format(step), From a5f15aec57334c7160642395149812e296a059a3 Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 14 Jul 2022 17:55:18 -0500 Subject: [PATCH 061/131] rearrange functions; move FluxTimesXSHelper to be inner class of FluxDepletionOperator We move FluxTimesXSHelper to be an inner class of FluxDepletionOperator so we can avoid needing to copy the number and cross_sections attributes. --- openmc/deplete/flux_operator.py | 580 ++++++++++++++++++-------------- openmc/deplete/helpers.py | 93 +---- 2 files changed, 330 insertions(+), 343 deletions(-) diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/flux_operator.py index 014770551..eaa59cbde 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/flux_operator.py @@ -8,6 +8,7 @@ and one-group cross sections. import copy from collections import OrderedDict from warnings import warn +from itertools import product import numpy as np import pandas as pd @@ -16,15 +17,16 @@ from uncertainties import ufloat import openmc from openmc.checkvalue import check_type, check_value, check_iterable_type from openmc.mpi import comm -from .abc import TransportOperator, OperatorResult +from .abc import TransportOperator, ReactionRateHelper, OperatorResult from .atom_number import AtomNumber from .chain import REACTIONS from .reaction_rates import ReactionRates -from .helpers import ConstantFissionYieldHelper, SourceRateHelper, FluxTimesXSHelper +from .helpers import ConstantFissionYieldHelper, SourceRateHelper valid_rxns = list(REACTIONS) valid_rxns.append('fission') +## delete def _distribute(items): """Distribute items across MPI communicator Parameters @@ -138,10 +140,23 @@ class FluxDepletionOperator(TransportOperator): materials = self._consolidate_nuclides_to_material(nuclides, volume) diff_burnable_mats=False - # super().__init__(materials, cross_sections, diff_burnable_mats, chain_file, fission_q, dilute_initial, prev_results, helper_class_kwargs) + #super().__init__( + # materials, + # cross_sections, + # chain_file, + # prev_results, + # diff_burnable_mats, + # normalization_mode, + # None, + # 0.0, + # None, + # fission_yield_opts, + # None, + # None, + # reduce_chain, + # reduce_chain_level) - - ## this part goes to OpenMCOperator + ##delete till end of function super().__init__(chain_file, fission_q, 0.0, prev_results) self.round_number = False self.materials = materials @@ -195,6 +210,219 @@ class FluxDepletionOperator(TransportOperator): self._get_helper_classes(None, fission_yield_opts) + def _consolidate_nuclides_to_material(self, nuclides, volume): + """Puts nuclide list into an openmc.Materials object. + + """ + openmc.reset_auto_ids() + mat = openmc.Material() + for nuc, conc in nuclides.items(): + mat.add_nuclide(nuc, conc / 1e24) #convert to at/b-cm + + mat.volume = volume + mat.depleteable = True + + return openmc.Materials([mat]) + + def _differentiate_burnable_materials(): + """Assign distribmats for each burnable material""" + pass + + ##delete + def _get_burnable_mats(self): + """Determine depletable materials, volumes, and nuclides + Returns + ------- + burnable_mats : list of str + List of burnable material IDs + volume : OrderedDict of str to float + Volume of each material in [cm^3] + nuclides : list of str + Nuclides in order of how they'll appear in the simulation. + """ + + burnable_mats = set() + model_nuclides = set() + volume = OrderedDict() + + self.heavy_metal = 0.0 + + # Iterate once through the geometry to get dictionaries + for mat in self.materials: + for nuclide in mat.get_nuclides(): + model_nuclides.add(nuclide) + if mat.depletable: + burnable_mats.add(str(mat.id)) + if mat.volume is None: + raise RuntimeError("Volume not specified for depletable " + "material with ID={}.".format(mat.id)) + volume[str(mat.id)] = mat.volume + self.heavy_metal += mat.fissionable_mass + + # Make sure there are burnable materials + if not burnable_mats: + raise RuntimeError( + "No depletable materials were found in the model.") + + # Sort the sets + burnable_mats = sorted(burnable_mats, key=int) + model_nuclides = sorted(model_nuclides) + + # Construct a global nuclide dictionary, burned first + nuclides = list(self.chain.nuclide_dict) + for nuc in model_nuclides: + if nuc not in nuclides: + nuclides.append(nuc) + + return burnable_mats, volume, nuclides + + def _load_previous_results(): + """Load in results from a previous depletion calculation.""" + pass + + def _get_nuclides_with_data(self, cross_sections): + """Finds nuclides with cross section data + """ + return set(cross_sections.index) + + ##delete + def _extract_number(self, local_mats, volume, nuclides, prev_res=None): + """Construct AtomNumber using geometry + + Parameters + ---------- + local_mats : list of str + Material IDs to be managed by this process + volume : OrderedDict of str to float + Volumes for the above materials in [cm^3] + nuclides : list of str + Nuclides to be used in the simulation. + prev_res : Results, optional + Results from a previous depletion calculation + + """ + self.number = AtomNumber(local_mats, nuclides, volume, len(self.chain)) + + if self.dilute_initial != 0.0: + for nuc in self._burnable_nucs: + self.number.set_atom_density(np.s_[:], nuc, self.dilute_initial) + + # Now extract and store the number densities + # From the geometry if no previous depletion results + if prev_res is None: + for mat in self.materials: + if str(mat.id) in local_mats: + self._set_number_from_mat(mat) + # Else from previous depletion results + else: + raise RuntimeError( + "Loading from previous results not yet supported") + + ##delete + def _set_number_from_mat(self, mat): + """Extracts material and number densities from openmc.Material + Parameters + ---------- + mat : openmc.Material + The material to read from + """ + mat_id = str(mat.id) + + for nuclide, atom_per_bcm in mat.get_nuclide_atom_densities().items(): + atom_per_cc = atom_per_bcm * 1.0e24 + self.number.set_atom_density(mat_id, nuclide, atom_per_cc) + + class FluxTimesXSHelper(ReactionRateHelper): + """Class for generating one-group reaction rates with flux and + one-group cross sections. + + This class does not generate tallies, and instead stores cross sections + for each nuclide and transmutation reaction relevant for a depletion + calculation. The reaction rate is calculated by multiplying the flux by the + cross sections. + + Parameters + ---------- + outer : openmc.deplete.FluxDepletionOperator + Reference to the object encapsulate FluxTimesXSHelper. + We pass this so we don't have to duplicate the ``number`` object. + n_nucs : int + Number of burnable nuclides tracked by :class:`openmc.deplete.Operator` + n_react : int + Number of reactions tracked by :class:`openmc.deplete.Operator` + + Attributes + ---------- + nuc_ind_map : dict of int to str + Dictionary mapping the nuclide index to nuclide name + rxn_ind_map : dict of int to str + Dictionary mapping reaction index to reaction name + + """ + def __init__(self, n_nuc, n_react, outer): + super().__init__(n_nuc, n_react) + self.outer = outer + self.nuc_ind_map = None + self.rxn_ind_map = None + + def generate_tallies(self, materials, scores): + """Unused in this case""" + + def get_material_rates(self, mat_id, nuc_index, react_index): + """Return 2D array of [nuclide, reaction] reaction rates + + Parameters + ---------- + mat_id : int + Unique ID for the requested material + nuc_index : list of str + Ordering of desired nuclides + react_index : list of str + Ordering of reactions + """ + self._results_cache.fill(0.0) + for i, (i_nuc, i_react) in enumerate(product(nuc_index, react_index)): + nuc = self.nuc_ind_map[i_nuc] + rxn = self.rxn_ind_map[i_react] + density = self.outer.number.get_atom_density(mat_id, nuc) + self._results_cache[i_nuc, i_react] = self.outer.cross_sections[rxn][nuc] * density + + return self._results_cache + + def _get_helper_classes(self, reaction_rate_opts, fission_yield_opts): + """Get helper classes for calculating reation rates and fission yields""" + rates = self.reaction_rates + # Get classes to assit working with tallies + nuc_ind_map = {ind: nuc for nuc, ind in rates.index_nuc.items()} + rxn_ind_map = {ind: rxn for rxn, ind in rates.index_rx.items()} + + self._rate_helper = self.FluxTimesXSHelper(self.reaction_rates.n_nuc, self.reaction_rates.n_react, self) + self._rate_helper.nuc_ind_map = nuc_ind_map + self._rate_helper.rxn_ind_map = rxn_ind_map + + self._normalization_helper = SourceRateHelper() + + # Select and create fission yield helper + fission_helper = ConstantFissionYieldHelper + fission_yield_opts = ( + {} if fission_yield_opts is None else fission_yield_opts) + self._yield_helper = fission_helper.from_operator( + self, **fission_yield_opts) + + def initial_condition(self): + """Performs final setup and returns initial condition. + + Returns + ------- + list of numpy.ndarray + Total density for initial conditions. + """ + + # Return number density vector + #return super().initial_condition() + ##delete + return list(self.number.get_mat_slice(np.s_[:])) + def __call__(self, vec, source_rate): """Obtain the reaction rates @@ -212,6 +440,17 @@ class FluxDepletionOperator(TransportOperator): """ + self._update_materials_and_nuclides(vec) + + # Use the flux spectra as a "source rate" + rates = self._calculate_reaction_rates(self.flux_spectra) + keff = self._keff + + op_result = OperatorResult(keff, rates) + return copy.deepcopy(op_result) + + ##delete + def _update_materials_and_nuclides(self, vec): # Update the number densities regardless of the source rate self.number.set_density(vec) self._update_materials() @@ -222,13 +461,92 @@ class FluxDepletionOperator(TransportOperator): self._normalization_helper.nuclides = rxn_nuclides self._yield_helper.update_tally_nuclides(rxn_nuclides) - # Use the flux spectra as a "source rate" - rates = self._calculate_reaction_rates(self.flux_spectra) - keff = self._keff + def _update_materials(self): + """Updates material compositions in OpenMC on all processes.""" - op_result = OperatorResult(keff, rates) - return copy.deepcopy(op_result) + for rank in range(comm.size): + number_i = comm.bcast(self.number, root=rank) + for mat in number_i.materials: + nuclides = [] + densities = [] + for nuc in number_i.nuclides: + if nuc in self.nuclides_with_data: + val = 1.0e-24 * number_i.get_atom_density(mat, nuc) + + # If nuclide is zero, do not add to the problem. + if val > 0.0: + if self.round_number: + val_magnitude = np.floor(np.log10(val)) + val_scaled = val / 10**val_magnitude + val_round = round(val_scaled, 8) + + val = val_round * 10**val_magnitude + + nuclides.append(nuc) + densities.append(val) + else: + # Only output warnings if values are significantly + # negative. CRAM does not guarantee positive + # values. + if val < -1.0e-21: + print( + "WARNING: nuclide ", + nuc, + " in material ", + mat, + " is negative (density = ", + val, + " at/barn-cm)") + number_i[mat, nuc] = 0.0 + + # TODO Update densities on the Python side, otherwise the + # summary.h5 file contains densities at the first time step + ##delete + def _get_reaction_nuclides(self): + """Determine nuclides that should have reaction rates + + This method returns a list of all nuclides that have neutron data and + are listed in the depletion chain. Technically, we should list nuclides + that may not appear in the depletion chain because we still need to get + the fission reaction rate for these nuclides in order to normalize + power, but that is left as a future exercise. + + Returns + ------- + list of str + nuclides with reaction rates + + """ + # Create the set of all nuclides in the decay chain in materials marked + # for burning in which the number density is greater than zero. + nuc_set = set() + + for nuc in self.number.nuclides: + if nuc in self.nuclides_with_data: + if np.sum(self.number[:,nuc]) > 0.0: + nuc_set.add(nuc) + + # Communicate which nuclides have nonzeros to rank 0 + if comm.rank == 0: + for i in range(1, comm.size): + nuc_newset = comm.recv(source=i, tag=i) + nuc_set |= nuc_newset + else: + comm.send(nuc_set, dest=0, tag=comm.rank) + + if comm.rank == 0: + # Sort nuclides in the same order as self.number + nuc_list = [nuc for nuc in self.number.nuclides + if nuc in nuc_set] + else: + nuc_list = None + + # Store list of tally nuclides on each process + nuc_list = comm.bcast(nuc_list) + return [nuc for nuc in nuc_list if nuc in self.chain] + + ##delete def _calculate_reaction_rates(self, source_rate): rates = self.reaction_rates @@ -302,18 +620,6 @@ class FluxDepletionOperator(TransportOperator): return rates - def initial_condition(self): - """Performs final setup and returns initial condition. - - Returns - ------- - list of numpy.ndarray - Total density for initial conditions. - """ - - # Return number density vector - return list(self.number.get_mat_slice(np.s_[:])) - def write_bos_data(self, step): """Document beginning of step data for a given step @@ -328,6 +634,7 @@ class FluxDepletionOperator(TransportOperator): # Since we aren't running a transport simulation, we simply pass pass + ##delete def get_results_info(self): """Returns volume list, cell lists, and nuc lists. @@ -436,233 +743,4 @@ class FluxDepletionOperator(TransportOperator): check_value('reactions', reaction, valid_rxns) - def _update_materials(self): - """Updates material compositions in OpenMC on all processes.""" - for rank in range(comm.size): - number_i = comm.bcast(self.number, root=rank) - - for mat in number_i.materials: - nuclides = [] - densities = [] - for nuc in number_i.nuclides: - if nuc in self.nuclides_with_data: - val = 1.0e-24 * number_i.get_atom_density(mat, nuc) - - # If nuclide is zero, do not add to the problem. - if val > 0.0: - if self.round_number: - val_magnitude = np.floor(np.log10(val)) - val_scaled = val / 10**val_magnitude - val_round = round(val_scaled, 8) - - val = val_round * 10**val_magnitude - - nuclides.append(nuc) - densities.append(val) - else: - # Only output warnings if values are significantly - # negative. CRAM does not guarantee positive - # values. - if val < -1.0e-21: - print( - "WARNING: nuclide ", - nuc, - " in material ", - mat, - " is negative (density = ", - val, - " at/barn-cm)") - number_i[mat, nuc] = 0.0 - - # TODO Update densities on the Python side, otherwise the - # summary.h5 file contains densities at the first time step - - def _consolidate_nuclides_to_material(self, nuclides, volume): - """Puts nuclide list into an openmc.Materials object. - - """ - openmc.reset_auto_ids() - mat = openmc.Material() - for nuc, conc in nuclides.items(): - mat.add_nuclide(nuc, conc / 1e24) #convert to at/b-cm - - mat.volume = volume - mat.depleteable = True - - return openmc.Materials([mat]) - - def _get_helper_classes(self, reaction_rate_opts, fission_yield_opts): - """Get helper classes for calculating reation rates and fission yields""" - rates = self.reaction_rates - # Get classes to assit working with tallies - nuc_ind_map = {ind: nuc for nuc, ind in rates.index_nuc.items()} - rxn_ind_map = {ind: rxn for rxn, ind in rates.index_rx.items()} - - - self._rate_helper = FluxTimesXSHelper(self.flux_spectra, self.cross_sections, self.reaction_rates.n_nuc, self.reaction_rates.n_react) - - self._rate_helper.nuc_ind_map = nuc_ind_map - self._rate_helper.rxn_ind_map = rxn_ind_map - # We'll need to find a way to update number as time goes on. - # perhaps in this classes version of _update_materials()? - self._rate_helper.number = self.number - - self._normalization_helper = SourceRateHelper() - - # Select and create fission yield helper - fission_helper = ConstantFissionYieldHelper - fission_yield_opts = ( - {} if fission_yield_opts is None else fission_yield_opts) - self._yield_helper = fission_helper.from_operator( - self, **fission_yield_opts) - - - def _load_previous_results(): - """Load in results from a previous depletion calculation.""" - pass - - def _differentiate_burnable_materials(): - """Assign distribmats for each burnable material""" - pass - - def _get_reaction_nuclides(self): - """Determine nuclides that should have reaction rates - - This method returns a list of all nuclides that have neutron data and - are listed in the depletion chain. Technically, we should list nuclides - that may not appear in the depletion chain because we still need to get - the fission reaction rate for these nuclides in order to normalize - power, but that is left as a future exercise. - - Returns - ------- - list of str - nuclides with reaction rates - - """ - # Create the set of all nuclides in the decay chain in materials marked - # for burning in which the number density is greater than zero. - nuc_set = set() - - for nuc in self.number.nuclides: - if nuc in self.nuclides_with_data: - if np.sum(self.number[:,nuc]) > 0.0: - nuc_set.add(nuc) - - # Communicate which nuclides have nonzeros to rank 0 - if comm.rank == 0: - for i in range(1, comm.size): - nuc_newset = comm.recv(source=i, tag=i) - nuc_set |= nuc_newset - else: - comm.send(nuc_set, dest=0, tag=comm.rank) - - if comm.rank == 0: - # Sort nuclides in the same order as self.number - nuc_list = [nuc for nuc in self.number.nuclides - if nuc in nuc_set] - else: - nuc_list = None - - # Store list of tally nuclides on each process - nuc_list = comm.bcast(nuc_list) - return [nuc for nuc in nuc_list if nuc in self.chain] - - def _get_burnable_mats(self): - """Determine depletable materials, volumes, and nuclides - Returns - ------- - burnable_mats : list of str - List of burnable material IDs - volume : OrderedDict of str to float - Volume of each material in [cm^3] - nuclides : list of str - Nuclides in order of how they'll appear in the simulation. - """ - - burnable_mats = set() - model_nuclides = set() - volume = OrderedDict() - - self.heavy_metal = 0.0 - - # Iterate once through the geometry to get dictionaries - for mat in self.materials: - for nuclide in mat.get_nuclides(): - model_nuclides.add(nuclide) - if mat.depletable: - burnable_mats.add(str(mat.id)) - if mat.volume is None: - raise RuntimeError("Volume not specified for depletable " - "material with ID={}.".format(mat.id)) - volume[str(mat.id)] = mat.volume - self.heavy_metal += mat.fissionable_mass - - # Make sure there are burnable materials - if not burnable_mats: - raise RuntimeError( - "No depletable materials were found in the model.") - - # Sort the sets - burnable_mats = sorted(burnable_mats, key=int) - model_nuclides = sorted(model_nuclides) - - # Construct a global nuclide dictionary, burned first - nuclides = list(self.chain.nuclide_dict) - for nuc in model_nuclides: - if nuc not in nuclides: - nuclides.append(nuc) - - return burnable_mats, volume, nuclides - - def _get_nuclides_with_data(self, cross_sections): - """Finds nuclides with cross section data - """ - return set(cross_sections.index) - - - def _extract_number(self, local_mats, volume, nuclides, prev_res=None): - """Construct AtomNumber using geometry - - Parameters - ---------- - local_mats : list of str - Material IDs to be managed by this process - volume : OrderedDict of str to float - Volumes for the above materials in [cm^3] - nuclides : list of str - Nuclides to be used in the simulation. - prev_res : Results, optional - Results from a previous depletion calculation - - """ - self.number = AtomNumber(local_mats, nuclides, volume, len(self.chain)) - - if self.dilute_initial != 0.0: - for nuc in self._burnable_nucs: - self.number.set_atom_density(np.s_[:], nuc, self.dilute_initial) - - # Now extract and store the number densities - # From the geometry if no previous depletion results - if prev_res is None: - for mat in self.materials: - if str(mat.id) in local_mats: - self._set_number_from_mat(mat) - # Else from previous depletion results - else: - raise RuntimeError( - "Loading from previous results not yet supported") - - def _set_number_from_mat(self, mat): - """Extracts material and number densities from openmc.Material - Parameters - ---------- - mat : openmc.Material - The material to read from - """ - mat_id = str(mat.id) - - for nuclide, atom_per_bcm in mat.get_nuclide_atom_densities().items(): - atom_per_cc = atom_per_bcm * 1.0e24 - self.number.set_atom_density(mat_id, nuclide, atom_per_cc) diff --git a/openmc/deplete/helpers.py b/openmc/deplete/helpers.py index 67a88fa16..33bd182d7 100644 --- a/openmc/deplete/helpers.py +++ b/openmc/deplete/helpers.py @@ -2,7 +2,6 @@ Class for normalizing fission energy deposition """ import bisect -import pandas as pd from abc import abstractmethod from collections import defaultdict from copy import deepcopy @@ -25,7 +24,7 @@ __all__ = ( "DirectReactionRateHelper", "ChainFissionHelper", "EnergyScoreHelper" "SourceRateHelper", "TalliedFissionYieldHelper", "ConstantFissionYieldHelper", "FissionYieldCutoffHelper", - "AveragedFissionYieldHelper", "FluxCollapseHelper", "FluxTimesXSHelper") + "AveragedFissionYieldHelper", "FluxCollapseHelper") class TalliedFissionYieldHelper(FissionYieldHelper): """Abstract class for computing fission yields with tallies @@ -125,96 +124,6 @@ class TalliedFissionYieldHelper(FissionYieldHelper): # ------------------------------------- -class FluxTimesXSHelper(ReactionRateHelper): - """Class for generating one-group reaction rates with flux and - one-group cross sections. - - This class does not generate tallies, and instead stores cross sections - for each nuclide and transmutation reaction relevant for a depletion - calculation. The reaction rate is calculated by multiplying the flux by the - cross sections. - - Parameters - ---------- - flux : float - Neutron flux - micro_xs : pandas.DataFrame - Microscopic cross-section data - n_nucs : int - Number of burnable nuclides tracked by :class:`openmc.deplete.Operator` - n_react : int - Number of reactions tracked by :class:`openmc.deplete.Operator` - - Attributes - ---------- - nuc_ind_map : dict of int to str - Dictionary mapping the nuclide index to nuclide name - rxn_ind_map : dict of int to str - Dictionary mapping reaction index to reaction name - number : AtomNumber - AtomNumber object. Needed to convert the microscopic cross-sections - to macroscopic cross sections. - - """ - def __init__(self, flux, micro_xs, n_nuc, n_react): - super().__init__(n_nuc, n_react) - self._flux = flux - self._micro_xs = micro_xs - self.nuc_ind_map = None - self.rxn_ind_map = None - self.number = None - - def generate_tallies(self, materials, scores): - """Unused in this case""" - - - @property - def flux(self): - """Flux in n cm^-2 s^-1""" - return self._flux - - @flux.setter - def flux(self, flux): - check_type("flux", flux, float) - self._flux = flux - - @property - def micro_xs(self): - """DataFrame of microscopic cross sections with requested reaction - for all nuclides""" - return self._micro_xs - - @micro_xs.setter - def micro_xs(self, micro_xs): - # TODO : validate micro_xs - self._micro_xs = micro_xs - - def get_material_rates(self, mat_id, nuc_index, react_index): - """Return 2D array of [nuclide, reaction] reaction rates - - Parameters - ---------- - mat_id : int - Unique ID for the requested material - nuc_index : list of str - Ordering of desired nuclides - react_index : list of str - Ordering of reactions - """ - self._results_cache.fill(0.0) - for i, (i_nuc, i_react) in enumerate(product(nuc_index, react_index)): - nuc = self.nuc_ind_map[i_nuc] - rxn = self.rxn_ind_map[i_react] - density = self.number.get_atom_density(mat_id, nuc) - self._results_cache[i_nuc, i_react] = self._micro_xs[rxn][nuc] * density - - return self._results_cache - - - - - - class DirectReactionRateHelper(ReactionRateHelper): """Class for generating one-group reaction rates with direct tallies From efa92ad865d7abee86bd21524164433d96957185 Mon Sep 17 00:00:00 2001 From: yardasol Date: Tue, 19 Jul 2022 10:05:47 -0500 Subject: [PATCH 062/131] Address pauromano's comments --- openmc/deplete/openmc_operator.py | 3 --- openmc/deplete/operator.py | 9 ++++----- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/openmc/deplete/openmc_operator.py b/openmc/deplete/openmc_operator.py index 047a7f967..e057b3467 100644 --- a/openmc/deplete/openmc_operator.py +++ b/openmc/deplete/openmc_operator.py @@ -4,15 +4,12 @@ This module implements functions used by both OpenMC transport operators as well """ -import copy from abc import abstractmethod from collections import OrderedDict import numpy as np -from uncertainties import ufloat import openmc -from openmc.exceptions import DataError from openmc.mpi import comm from .abc import TransportOperator, OperatorResult from .atom_number import AtomNumber diff --git a/openmc/deplete/operator.py b/openmc/deplete/operator.py index d8b84f5a0..b3c0756b8 100644 --- a/openmc/deplete/operator.py +++ b/openmc/deplete/operator.py @@ -269,20 +269,19 @@ class Operator(OpenMCOperator): for i in range(cell.num_instances)] self.materials = openmc.Materials( - model.geometry.get_all_materials().values() + self.model.geometry.get_all_materials().values() ) def _load_previous_results(self): """Load results from a previous depletion simulation""" # Reload volumes into geometry - prev_results[-1].transfer_volumes(self.model) + self.prev_res[-1].transfer_volumes(self.model) # Store previous results in operator # Distribute reaction rates according to those tracked # on this process - if comm.size == 1: - self.prev_res = prev_results - else: + if comm.size != 1: + prev_results = self.prev_res self.prev_res = Results() mat_indexes = _distribute(range(len(self.burnable_mats))) for res_obj in prev_results: From 3aad43a3baca31897a2b66bbcc9e3c69a73df688 Mon Sep 17 00:00:00 2001 From: yardasol Date: Tue, 19 Jul 2022 11:25:45 -0500 Subject: [PATCH 063/131] remove unneeded parameters from OpenMCOperator --- openmc/deplete/flux_operator.py | 369 ++---------------------------- openmc/deplete/openmc_operator.py | 36 +-- openmc/deplete/operator.py | 28 ++- 3 files changed, 43 insertions(+), 390 deletions(-) diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/flux_operator.py index eaa59cbde..9483c324c 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/flux_operator.py @@ -20,33 +20,14 @@ from openmc.mpi import comm from .abc import TransportOperator, ReactionRateHelper, OperatorResult from .atom_number import AtomNumber from .chain import REACTIONS +from .openmc_operator import OpenMCOperator from .reaction_rates import ReactionRates from .helpers import ConstantFissionYieldHelper, SourceRateHelper valid_rxns = list(REACTIONS) valid_rxns.append('fission') -## delete -def _distribute(items): - """Distribute items across MPI communicator - Parameters - ---------- - items : list - List of items of distribute - Returns - ------- - list - Items assigned to process that called - """ - min_size, extra = divmod(len(items), comm.size) - j = 0 - for i in range(comm.size): - chunk_size = min_size + int(i < extra) - if comm.rank == i: - return items[j:j + chunk_size] - j += chunk_size - -class FluxDepletionOperator(TransportOperator): +class FluxDepletionOperator(OpenMCOperator): """Depletion operator that uses a user-provided flux spectrum and one-group cross sections to calculate reaction rates. @@ -130,7 +111,7 @@ class FluxDepletionOperator(TransportOperator): check_type('nuclides', nuclides, dict, str) check_type('micro_xs', micro_xs, pd.DataFrame) - self.cross_sections = micro_xs + cross_sections = micro_xs if keff is not None: check_type('keff', keff, tuple, float) keff = ufloat(keff) @@ -140,75 +121,20 @@ class FluxDepletionOperator(TransportOperator): materials = self._consolidate_nuclides_to_material(nuclides, volume) diff_burnable_mats=False - #super().__init__( - # materials, - # cross_sections, - # chain_file, - # prev_results, - # diff_burnable_mats, - # normalization_mode, - # None, - # 0.0, - # None, - # fission_yield_opts, - # None, - # None, - # reduce_chain, - # reduce_chain_level) + helper_kwargs = dict() + helper_kwargs['fission_yield_opts'] = fission_yield_opts - ##delete till end of function - super().__init__(chain_file, fission_q, 0.0, prev_results) - self.round_number = False - self.materials = materials - - # Reduce the chain to only those nuclides present - if reduce_chain: - init_nuclides = set() - for material in self.materials: - if not material.depletable: - continue - for name, _dens_percent, _dens_type in material.nuclides: - init_nuclides.add(name) - - self.chain = self.chain.reduce(init_nuclides, reduce_chain_level) - - if diff_burnable_mats: - ## to implement - self._differentiate_burnable_mats() - - # Determine which nuclides have cross section data - # This nuclides variables contains every nuclides - # for which there is an entry in the micro_xs parameter - openmc.reset_auto_ids() - self.burnable_mats, volumes, all_nuclides = self._get_burnable_mats() - self._all_nuclides = all_nuclides - self.local_mats = _distribute(self.burnable_mats) - - self._mat_index_map = { - lm: self.burnable_mats.index(lm) for lm in self.local_mats} - - if self.prev_res is not None: - ## will be an abstract function for OpenMCOperator - self._load_previous_results() - - - self.nuclides_with_data = self._get_nuclides_with_data(self.cross_sections) - - # Select nuclides with data that are also in the chain - self._burnable_nucs = [nuc.name for nuc in self.chain.nuclides - if nuc.name in self.nuclides_with_data] - - # Extract number densities from the geometry / previous depletion run - self._extract_number(self.local_mats, - volumes, - all_nuclides, - self.prev_res) - - # Create reaction rates array - self.reaction_rates = ReactionRates( - self.local_mats, self._burnable_nucs, self.chain.reactions) - - self._get_helper_classes(None, fission_yield_opts) + super().__init__( + materials, + cross_sections, + chain_file, + prev_results, + diff_burnable_mats, + fission_q, + 0.0, + helper_kwargs, + reduce_chain, + reduce_chain_level) def _consolidate_nuclides_to_material(self, nuclides, volume): """Puts nuclide list into an openmc.Materials object. @@ -224,58 +150,10 @@ class FluxDepletionOperator(TransportOperator): return openmc.Materials([mat]) - def _differentiate_burnable_materials(): + def _differentiate_burnable_mats(): """Assign distribmats for each burnable material""" pass - ##delete - def _get_burnable_mats(self): - """Determine depletable materials, volumes, and nuclides - Returns - ------- - burnable_mats : list of str - List of burnable material IDs - volume : OrderedDict of str to float - Volume of each material in [cm^3] - nuclides : list of str - Nuclides in order of how they'll appear in the simulation. - """ - - burnable_mats = set() - model_nuclides = set() - volume = OrderedDict() - - self.heavy_metal = 0.0 - - # Iterate once through the geometry to get dictionaries - for mat in self.materials: - for nuclide in mat.get_nuclides(): - model_nuclides.add(nuclide) - if mat.depletable: - burnable_mats.add(str(mat.id)) - if mat.volume is None: - raise RuntimeError("Volume not specified for depletable " - "material with ID={}.".format(mat.id)) - volume[str(mat.id)] = mat.volume - self.heavy_metal += mat.fissionable_mass - - # Make sure there are burnable materials - if not burnable_mats: - raise RuntimeError( - "No depletable materials were found in the model.") - - # Sort the sets - burnable_mats = sorted(burnable_mats, key=int) - model_nuclides = sorted(model_nuclides) - - # Construct a global nuclide dictionary, burned first - nuclides = list(self.chain.nuclide_dict) - for nuc in model_nuclides: - if nuc not in nuclides: - nuclides.append(nuc) - - return burnable_mats, volume, nuclides - def _load_previous_results(): """Load in results from a previous depletion calculation.""" pass @@ -285,53 +163,6 @@ class FluxDepletionOperator(TransportOperator): """ return set(cross_sections.index) - ##delete - def _extract_number(self, local_mats, volume, nuclides, prev_res=None): - """Construct AtomNumber using geometry - - Parameters - ---------- - local_mats : list of str - Material IDs to be managed by this process - volume : OrderedDict of str to float - Volumes for the above materials in [cm^3] - nuclides : list of str - Nuclides to be used in the simulation. - prev_res : Results, optional - Results from a previous depletion calculation - - """ - self.number = AtomNumber(local_mats, nuclides, volume, len(self.chain)) - - if self.dilute_initial != 0.0: - for nuc in self._burnable_nucs: - self.number.set_atom_density(np.s_[:], nuc, self.dilute_initial) - - # Now extract and store the number densities - # From the geometry if no previous depletion results - if prev_res is None: - for mat in self.materials: - if str(mat.id) in local_mats: - self._set_number_from_mat(mat) - # Else from previous depletion results - else: - raise RuntimeError( - "Loading from previous results not yet supported") - - ##delete - def _set_number_from_mat(self, mat): - """Extracts material and number densities from openmc.Material - Parameters - ---------- - mat : openmc.Material - The material to read from - """ - mat_id = str(mat.id) - - for nuclide, atom_per_bcm in mat.get_nuclide_atom_densities().items(): - atom_per_cc = atom_per_bcm * 1.0e24 - self.number.set_atom_density(mat_id, nuclide, atom_per_cc) - class FluxTimesXSHelper(ReactionRateHelper): """Class for generating one-group reaction rates with flux and one-group cross sections. @@ -389,8 +220,11 @@ class FluxDepletionOperator(TransportOperator): return self._results_cache - def _get_helper_classes(self, reaction_rate_opts, fission_yield_opts): + def _get_helper_classes(self, helper_kwargs): """Get helper classes for calculating reation rates and fission yields""" + + fission_yield_opts = helper_kwargs['fission_yield_opts'] + rates = self.reaction_rates # Get classes to assit working with tallies nuc_ind_map = {ind: nuc for nuc, ind in rates.index_nuc.items()} @@ -419,9 +253,7 @@ class FluxDepletionOperator(TransportOperator): """ # Return number density vector - #return super().initial_condition() - ##delete - return list(self.number.get_mat_slice(np.s_[:])) + return super().initial_condition(self.materials) def __call__(self, vec, source_rate): """Obtain the reaction rates @@ -449,18 +281,6 @@ class FluxDepletionOperator(TransportOperator): op_result = OperatorResult(keff, rates) return copy.deepcopy(op_result) - ##delete - def _update_materials_and_nuclides(self, vec): - # Update the number densities regardless of the source rate - self.number.set_density(vec) - self._update_materials() - - # Get all nuclides for which we will calculate reaction rates - rxn_nuclides = self._get_reaction_nuclides() - self._rate_helper.nuclides = rxn_nuclides - self._normalization_helper.nuclides = rxn_nuclides - self._yield_helper.update_tally_nuclides(rxn_nuclides) - def _update_materials(self): """Updates material compositions in OpenMC on all processes.""" @@ -502,123 +322,6 @@ class FluxDepletionOperator(TransportOperator): # TODO Update densities on the Python side, otherwise the # summary.h5 file contains densities at the first time step - ##delete - def _get_reaction_nuclides(self): - """Determine nuclides that should have reaction rates - - This method returns a list of all nuclides that have neutron data and - are listed in the depletion chain. Technically, we should list nuclides - that may not appear in the depletion chain because we still need to get - the fission reaction rate for these nuclides in order to normalize - power, but that is left as a future exercise. - - Returns - ------- - list of str - nuclides with reaction rates - - """ - # Create the set of all nuclides in the decay chain in materials marked - # for burning in which the number density is greater than zero. - nuc_set = set() - - for nuc in self.number.nuclides: - if nuc in self.nuclides_with_data: - if np.sum(self.number[:,nuc]) > 0.0: - nuc_set.add(nuc) - - # Communicate which nuclides have nonzeros to rank 0 - if comm.rank == 0: - for i in range(1, comm.size): - nuc_newset = comm.recv(source=i, tag=i) - nuc_set |= nuc_newset - else: - comm.send(nuc_set, dest=0, tag=comm.rank) - - if comm.rank == 0: - # Sort nuclides in the same order as self.number - nuc_list = [nuc for nuc in self.number.nuclides - if nuc in nuc_set] - else: - nuc_list = None - - # Store list of tally nuclides on each process - nuc_list = comm.bcast(nuc_list) - return [nuc for nuc in nuc_list if nuc in self.chain] - - ##delete - def _calculate_reaction_rates(self, source_rate): - - rates = self.reaction_rates - rates.fill(0.0) - - rxn_nuclides = self._rate_helper.nuclides - - # Form fast map - nuc_ind = [rates.index_nuc[nuc] for nuc in rxn_nuclides] - react_ind = [rates.index_rx[react] for react in self.chain.reactions] - - self._normalization_helper.reset() - self._yield_helper.unpack() - - # Store fission yield dictionaries - fission_yields = [] - - # Create arrays to store fission Q values, reaction rates, and nuclide - # numbers, zeroed out in material iteration - number = np.empty(rates.n_nuc) - - fission_ind = rates.index_rx.get("fission") - - for i, mat in enumerate(self.local_mats): - mat_index = self._mat_index_map[mat] - - # Zero out reaction rates and nuclide numbers - number.fill(0.0) - - # Get new number densities - for nuc, i_nuc_results in zip(rxn_nuclides, nuc_ind): - number[i_nuc_results] = self.number[mat, nuc] - - # Calculate macroscopic cross sections and store them in rates array - rxn_rates = self._rate_helper.get_material_rates( - mat_index, nuc_ind, react_ind) - - ## replace - #for nuc in rxn_nuclides: - # density = self.number.get_atom_density(i, nuc) - # for rxn in self.chain.reactions: - # rates.set( - # i, - # nuc, - # rxn, - # self.cross_sections[rxn, nuc] * density) - - # Compute fission yields for this material - fission_yields.append(self._yield_helper.weighted_yields(i)) - - # Accumulate energy from fission - if fission_ind is not None: - self._normalization_helper.update(rxn_rates[:, fission_ind]) - - # Divide by total number of atoms and store - # the reason we do this is based on the mathematical equation; - # in the equation, we multiply the depletion matrix by the nuclide - # vector. Since what we want is the depletion matrix, we need to - # divide the reaction rates by the number of atoms to get the right - # units. - rates[i] = self._rate_helper.divide_by_adens(number) - - rates *= self._normalization_helper.factor(source_rate) - ##replace - # Get reaction rate in reactions/sec - #rates *= self.flux_spectra - - - # Store new fission yields on the chain - self.chain.fission_yields = fission_yields - - return rates def write_bos_data(self, step): """Document beginning of step data for a given step @@ -634,34 +337,6 @@ class FluxDepletionOperator(TransportOperator): # Since we aren't running a transport simulation, we simply pass pass - ##delete - def get_results_info(self): - """Returns volume list, cell lists, and nuc lists. - - Returns - ------- - volume : dict of str to float - Volumes corresponding to materials in burn_list - nuc_list : list of str - A list of all nuclide names. Used for sorting the simulation. - burn_list : list of int - A list of all cell IDs to be burned. Used for sorting the - simulation. - full_burn_list : list of int - All burnable materials in the geometry. - """ - nuc_list = self.number.burnable_nuclides - burn_list = self.local_mats - - volume = {} - for i, mat in enumerate(burn_list): - volume[mat] = self.number.volume[i] - - # Combine volume dictionaries across processes - volume_list = comm.allgather(volume) - volume = {k: v for d in volume_list for k, v in d.items()} - - return volume, nuc_list, burn_list, burn_list @staticmethod def create_micro_xs_from_data_array( diff --git a/openmc/deplete/openmc_operator.py b/openmc/deplete/openmc_operator.py index e057b3467..536f67c4b 100644 --- a/openmc/deplete/openmc_operator.py +++ b/openmc/deplete/openmc_operator.py @@ -67,8 +67,6 @@ class OpenMCOperator(TransportOperator): Whether to differentiate burnable materials with multiple instances. Volumes are divided equally from the original material volume. Default: False. - normalization_mode : str - Indicate how reaction rates should be normalized. fission_q : dict, optional Dictionary of nuclides and their fission Q values [eV]. dilute_initial : float, optional @@ -76,17 +74,8 @@ class OpenMCOperator(TransportOperator): in initial condition to ensure they exist in the decay chain. Only done for nuclides with reaction rates. Defaults to 1.0e3. - fission_yield_mode : str - Key indicating what fission product yield scheme to use. - fission_yield_opts : dict of str to option, optional - Optional arguments to pass to the helper determined by - ``fission_yield_mode``. Will be passed directly on to the - helper. Passing a value of None will use the defaults for - the associated helper. - reaction_rate_mode : str, optional - Indicate how one-group reaction rates should be calculated. - reaction_rate_opts : dict, optional - Keyword arguments that are passed to the reaction rate helper class. + helper_kwargs : dict + Arguments for helper classes reduce_chain : bool, optional If True, use :meth:`openmc.deplete.Chain.reduce` to reduce the depletion chain up to ``reduce_chain_level``. Default is False. @@ -136,13 +125,9 @@ class OpenMCOperator(TransportOperator): chain_file=None, prev_results=None, diff_burnable_mats=False, - normalization_mode=None, fission_q=None, dilute_initial=0.0, - fission_yield_mode=None, - fission_yield_opts=None, - reaction_rate_mode=None, - reaction_rate_opts=None, + helper_kwargs=None, reduce_chain=False, reduce_chain_level=None): @@ -195,12 +180,7 @@ class OpenMCOperator(TransportOperator): self.reaction_rates = ReactionRates( self.local_mats, self._burnable_nucs, self.chain.reactions) - self._get_helper_classes( - reaction_rate_mode, - normalization_mode, - fission_yield_mode, - reaction_rate_opts, - fission_yield_opts) + self._get_helper_classes(helper_kwargs) @abstractmethod def _differentiate_burnable_mats(self): @@ -363,13 +343,7 @@ class OpenMCOperator(TransportOperator): self.number.set_atom_density(mat_id, nuclide, atom_per_cc) @abstractmethod - def _get_helper_classes( - self, - reaction_rate_mode, - normalization_mode, - fission_yield_mode, - reaction_rate_opts, - fission_yield_opts): + def _get_helper_classes(self, helper_kwargs): """Create the ``_rate_helper``, ``_normalization_helper``, and ``_yield_helper`` objects. diff --git a/openmc/deplete/operator.py b/openmc/deplete/operator.py index b3c0756b8..c5538712d 100644 --- a/openmc/deplete/operator.py +++ b/openmc/deplete/operator.py @@ -227,19 +227,22 @@ class Operator(OpenMCOperator): self.cleanup_when_done = True + helper_kwargs = dict() + helper_kwargs['reaction_rate_mode'] = reaction_rate_mode + helper_kwargs['normalization_mode'] = normalization_mode + helper_kwargs['fission_yield_mode'] = fission_yield_mode + helper_kwargs['reaction_rate_opts'] = reaction_rate_opts + helper_kwargs['fission_yield_opts'] = fission_yield_opts + super().__init__( model.materials, cross_sections, chain_file, prev_results, diff_burnable_mats, - normalization_mode, fission_q, dilute_initial, - fission_yield_mode, - fission_yield_opts, - reaction_rate_mode, - reaction_rate_opts, + helper_kwargs, reduce_chain, reduce_chain_level) @@ -313,18 +316,13 @@ class Operator(OpenMCOperator): return nuclides - def _get_helper_classes( - self, - reaction_rate_mode, - normalization_mode, - fission_yield_mode, - reaction_rate_opts, - fission_yield_opts): + def _get_helper_classes(self, helper_kwargs): """Create the ``_rate_helper``, ``_normalization_helper``, and ``_yield_helper`` objects. Parameters ---------- + **kwargs : ... reaction_rate_mode : str Indicates the subclass of :class:`ReactionRateHelper` to instantiate. @@ -341,6 +339,12 @@ class Operator(OpenMCOperator): subclass. """ + reaction_rate_mode = helper_kwargs['reaction_rate_mode'] + normalization_mode = helper_kwargs['normalization_mode'] + fission_yield_mode = helper_kwargs['fission_yield_mode'] + reaction_rate_opts = helper_kwargs['reaction_rate_opts'] + fission_yield_opts = helper_kwargs['fission_yield_opts'] + # Get classes to assist working with tallies if reaction_rate_mode == "direct": self._rate_helper = DirectReactionRateHelper( From 3817750bff837c8c47b58c0e141ea57be5881df2 Mon Sep 17 00:00:00 2001 From: yardasol Date: Tue, 19 Jul 2022 12:20:22 -0500 Subject: [PATCH 064/131] fix Operator syntax in UG Chapter 8 example --- docs/source/usersguide/depletion.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/source/usersguide/depletion.rst b/docs/source/usersguide/depletion.rst index fe3f12dc4..07a7c9645 100644 --- a/docs/source/usersguide/depletion.rst +++ b/docs/source/usersguide/depletion.rst @@ -112,13 +112,16 @@ should be, including indirect components. Some examples are provided below:: # use a dictionary of fission_q values fission_q = {"U235": 202e+6} # energy in eV + # create a Model object + model = openmc.model.Model(geometry, settings) + # create a modified chain and write it to a new file chain = openmc.deplete.Chain.from_xml("chain.xml", fission_q) chain.export_to_xml("chain_mod_q.xml") - op = openmc.deplete.Operator(geometry, setting, "chain_mod_q.xml") + op = openmc.deplete.Operator(model, "chain_mod_q.xml") # alternatively, pass the modified fission Q directly to the operator - op = openmc.deplete.Operator(geometry, setting, "chain.xml", + op = openmc.deplete.Operator(model, "chain.xml", fission_q=fission_q) From 7e671d4e57918ef396fdf2a02cdc6008351a90f3 Mon Sep 17 00:00:00 2001 From: yardasol Date: Tue, 19 Jul 2022 12:33:56 -0500 Subject: [PATCH 065/131] Change defualt constructor to accept a Materials object The classmethod, from_nuclides, retains the volume-nuclide constructor --- docs/source/usersguide/depletion.rst | 24 +++--- openmc/deplete/flux_operator.py | 79 ++++++++++++++----- .../deplete_no_transport/test.py | 2 +- .../unit_tests/test_flux_deplete_operator.py | 2 +- 4 files changed, 76 insertions(+), 31 deletions(-) diff --git a/docs/source/usersguide/depletion.rst b/docs/source/usersguide/depletion.rst index 07a7c9645..c58a147a2 100644 --- a/docs/source/usersguide/depletion.rst +++ b/docs/source/usersguide/depletion.rst @@ -188,15 +188,22 @@ Transport-independent depletion possible and likely in the near future. OpenMC supports running depletion calculations independent of the OpenMC -transport solver using the :class:`FluxDepletionOperator` class. Rather than -taking a :class:`openmc.model.Model` object, this class accepts a volume, -a dictionary of nuclide concentrations, a flux spectra, and one-group -microscopic cross sections as a :class:`pandas.DataFrame`. The class includes -helper functions to construct the dataframe from a csv file or from -data arrays:: +transport solver using the :class:`FluxDepletionOperator` class. This class +has two ways to initalize it; the default constructor accepts an +:class:`openmc.Materials` object, a flux spectra, and one-group microscopic +cross sections as a :class:`pandas.Dataframe`, while the `from_nuclides` +method accepts a volume and dictionary of nuclide concentrations in place of +the :class:`openmc.Materials` object in addition to the other parameters. +The class includes helper functions to construct the dataframe from a csv file +or from data arrays:: ... + # load in the microscopic cross sections micro_xs = FluxDepletionOperator.create_micro_xs_from_csv(micro_xs_path) + flux = 1.16e15 + op = FluxDepletionOperator(materials, micro_xs, flux, chain_file) + + # alternate construtor nuclides = {'U234': 8.92e18, 'U235': 9.98e20, 'U238': 2.22e22, @@ -204,10 +211,7 @@ data arrays:: 'O16': 4.64e22, 'O17': 1.76e19} volume = 0.5 - flux = 1.16e15 - - op = FluxDepletionOperator(volume, nuclides, micro_xs, flux. chain_file) - + op = FluxDepletionOperator.from_nuclide_dict(volume, nuclides, micro_xs, flux, chain_file) A user can then define an integrator class as they would for a coupled transport-depletion calculation and follow the same steps from there. diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/flux_operator.py index 9483c324c..b52ebd159 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/flux_operator.py @@ -38,11 +38,8 @@ class FluxDepletionOperator(OpenMCOperator): Parameters ---------- - volume : float - Volume of the material being depleted in [cm^3] - nuclides : dict of str to float - Dictionary with nuclide names as keys and nuclide concentrations as - values. Nuclide concentration units are [atom/cm^3]. + materials : openmc.Materials + Materials to deplete. micro_xs : pandas.DataFrame DataFrame with nuclides names as index and microscopic cross section data in the columns. Cross section units are [cm^-2]. @@ -88,16 +85,62 @@ class FluxDepletionOperator(OpenMCOperator): results are to be used. """ - # Alternate constructor using a full-fledges Model object - #def __init__(self, model, micro_xs, ...): - # ... - # mode.materials = openmc.Materials(model.geometry.get_all_materials().values()) - # super().__init__(model.materials, ...) + @classmethod + def from_nuclides(cls, volume, nuclides, micro_xs, + flux_spectra, + chain_file, + keff=None, + fission_q=None, + prev_results=None, + reduce_chain=False, + reduce_chain_level=None, + fission_yield_opts=None): + """ + Alternate constructor from a dictionary of nuclide concentrations + volume : float + Volume of the material being depleted in [cm^3] + nuclides : dict of str to float + Dictionary with nuclide names as keys and nuclide concentrations as + values. Nuclide concentration units are [atom/cm^3]. + micro_xs : pandas.DataFrame + DataFrame with nuclides names as index and microscopic cross section + data in the columns. Cross section units are [cm^-2]. + flux_spectra : float + Flux spectrum [n cm^-2 s^-1] + chain_file : str + Path to the depletion chain XML file. + keff : 2-tuple of float, optional + keff eigenvalue and uncertainty from transport calculation. + Default is None. + fission_q : dict, optional + Dictionary of nuclides and their fission Q values [eV]. If not given, + values will be pulled from the ``chain_file``. + prev_results : Results, optional + Results from a previous depletion calculation. + reduce_chain : bool, optional + If True, use :meth:`openmc.deplete.Chain.reduce` to reduce the + depletion chain up to ``reduce_chain_level``. Default is False. + reduce_chain_level : int, optional + Depth of the search when reducing the depletion chain. Only used + if ``reduce_chain`` evaluates to true. The default value of + ``None`` implies no limit on the depth. + """ + check_type('nuclides', nuclides, dict, str) + materials = cls._consolidate_nuclides_to_material(nuclides, volume) + return cls(materials, + micro_xs, + flux_spectra, + chain_file, + keff, + fission_q, + prev_results, + reduce_chain, + reduce_chain_level, + fission_yield_opts) def __init__(self, - volume, - nuclides, + materials, micro_xs, flux_spectra, chain_file, @@ -107,18 +150,15 @@ class FluxDepletionOperator(OpenMCOperator): reduce_chain=False, reduce_chain_level=None, fission_yield_opts=None): - # Validate nuclides and micro-xs parameters - check_type('nuclides', nuclides, dict, str) + # Validate micro-xs parameters + check_type('materials', materials, openmc.Materials) check_type('micro_xs', micro_xs, pd.DataFrame) - - cross_sections = micro_xs if keff is not None: check_type('keff', keff, tuple, float) keff = ufloat(keff) self._keff = keff self.flux_spectra = flux_spectra - materials = self._consolidate_nuclides_to_material(nuclides, volume) diff_burnable_mats=False helper_kwargs = dict() @@ -126,7 +166,7 @@ class FluxDepletionOperator(OpenMCOperator): super().__init__( materials, - cross_sections, + micro_xs, chain_file, prev_results, diff_burnable_mats, @@ -136,7 +176,8 @@ class FluxDepletionOperator(OpenMCOperator): reduce_chain, reduce_chain_level) - def _consolidate_nuclides_to_material(self, nuclides, volume): + @staticmethod + def _consolidate_nuclides_to_material(nuclides, volume): """Puts nuclide list into an openmc.Materials object. """ diff --git a/tests/regression_tests/deplete_no_transport/test.py b/tests/regression_tests/deplete_no_transport/test.py index cdc8c057c..d326a17c1 100644 --- a/tests/regression_tests/deplete_no_transport/test.py +++ b/tests/regression_tests/deplete_no_transport/test.py @@ -47,7 +47,7 @@ def test_no_transport(run_in_tmpdir, vol_nuc, multiproc): micro_xs = FluxDepletionOperator.create_micro_xs_from_csv(micro_xs_file) chain_file = Path(__file__).parents[2] / 'chain_simple.xml' flux = 1164719970082145.0 # flux from pincell example - op = FluxDepletionOperator(vol_nuc[0], vol_nuc[1], micro_xs, flux, chain_file) + op = FluxDepletionOperator.from_nuclides(vol_nuc[0], vol_nuc[1], micro_xs, flux, chain_file) # Power and timesteps dt = [30] # single step diff --git a/tests/unit_tests/test_flux_deplete_operator.py b/tests/unit_tests/test_flux_deplete_operator.py index 99c4e7c8a..315e22cdc 100644 --- a/tests/unit_tests/test_flux_deplete_operator.py +++ b/tests/unit_tests/test_flux_deplete_operator.py @@ -69,5 +69,5 @@ def test_operator_init(): 'O16': 4.639065406771322e+22, 'O17': 1.7588724018066158e+19} micro_xs = FluxDepletionOperator.create_micro_xs_from_csv(ONE_GROUP_XS) - my_flux_operator = FluxDepletionOperator( + nuclide_flux_operator = FluxDepletionOperator.from_nuclides( 1, nuclides, micro_xs, FLUX_SPECTRA, CHAIN_PATH) From 030e14a799fc3f6000b7377b9cebb2b22a70d186 Mon Sep 17 00:00:00 2001 From: yardasol Date: Tue, 19 Jul 2022 12:55:05 -0500 Subject: [PATCH 066/131] update name of function in example --- docs/source/usersguide/depletion.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/usersguide/depletion.rst b/docs/source/usersguide/depletion.rst index c58a147a2..8b960f01f 100644 --- a/docs/source/usersguide/depletion.rst +++ b/docs/source/usersguide/depletion.rst @@ -211,7 +211,7 @@ or from data arrays:: 'O16': 4.64e22, 'O17': 1.76e19} volume = 0.5 - op = FluxDepletionOperator.from_nuclide_dict(volume, nuclides, micro_xs, flux, chain_file) + op = FluxDepletionOperator.from_nuclides(volume, nuclides, micro_xs, flux, chain_file) A user can then define an integrator class as they would for a coupled transport-depletion calculation and follow the same steps from there. From eefb4020afe5eb59cdcb2a64dc104226a883e88d Mon Sep 17 00:00:00 2001 From: yardasol Date: Tue, 19 Jul 2022 12:56:12 -0500 Subject: [PATCH 067/131] pep8 fixes --- openmc/deplete/flux_operator.py | 60 ++++++++++--------- .../deplete_no_transport/test.py | 13 ++-- 2 files changed, 38 insertions(+), 35 deletions(-) diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/flux_operator.py index b52ebd159..f63e72405 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/flux_operator.py @@ -27,6 +27,7 @@ from .helpers import ConstantFissionYieldHelper, SourceRateHelper valid_rxns = list(REACTIONS) valid_rxns.append('fission') + class FluxDepletionOperator(OpenMCOperator): """Depletion operator that uses a user-provided flux spectrum and one-group cross sections to calculate reaction rates. @@ -87,14 +88,14 @@ class FluxDepletionOperator(OpenMCOperator): @classmethod def from_nuclides(cls, volume, nuclides, micro_xs, - flux_spectra, - chain_file, - keff=None, - fission_q=None, - prev_results=None, - reduce_chain=False, - reduce_chain_level=None, - fission_yield_opts=None): + flux_spectra, + chain_file, + keff=None, + fission_q=None, + prev_results=None, + reduce_chain=False, + reduce_chain_level=None, + fission_yield_opts=None): """ Alternate constructor from a dictionary of nuclide concentrations @@ -129,15 +130,15 @@ class FluxDepletionOperator(OpenMCOperator): check_type('nuclides', nuclides, dict, str) materials = cls._consolidate_nuclides_to_material(nuclides, volume) return cls(materials, - micro_xs, - flux_spectra, - chain_file, - keff, - fission_q, - prev_results, - reduce_chain, - reduce_chain_level, - fission_yield_opts) + micro_xs, + flux_spectra, + chain_file, + keff, + fission_q, + prev_results, + reduce_chain, + reduce_chain_level, + fission_yield_opts) def __init__(self, materials, @@ -160,7 +161,7 @@ class FluxDepletionOperator(OpenMCOperator): self._keff = keff self.flux_spectra = flux_spectra - diff_burnable_mats=False + diff_burnable_mats = False helper_kwargs = dict() helper_kwargs['fission_yield_opts'] = fission_yield_opts @@ -184,7 +185,7 @@ class FluxDepletionOperator(OpenMCOperator): openmc.reset_auto_ids() mat = openmc.Material() for nuc, conc in nuclides.items(): - mat.add_nuclide(nuc, conc / 1e24) #convert to at/b-cm + mat.add_nuclide(nuc, conc / 1e24) # convert to at/b-cm mat.volume = volume mat.depleteable = True @@ -231,6 +232,7 @@ class FluxDepletionOperator(OpenMCOperator): Dictionary mapping reaction index to reaction name """ + def __init__(self, n_nuc, n_react, outer): super().__init__(n_nuc, n_react) self.outer = outer @@ -253,11 +255,13 @@ class FluxDepletionOperator(OpenMCOperator): Ordering of reactions """ self._results_cache.fill(0.0) - for i, (i_nuc, i_react) in enumerate(product(nuc_index, react_index)): + for i, (i_nuc, i_react) in enumerate( + product(nuc_index, react_index)): nuc = self.nuc_ind_map[i_nuc] rxn = self.rxn_ind_map[i_react] density = self.outer.number.get_atom_density(mat_id, nuc) - self._results_cache[i_nuc, i_react] = self.outer.cross_sections[rxn][nuc] * density + self._results_cache[i_nuc, + i_react] = self.outer.cross_sections[rxn][nuc] * density return self._results_cache @@ -271,7 +275,8 @@ class FluxDepletionOperator(OpenMCOperator): nuc_ind_map = {ind: nuc for nuc, ind in rates.index_nuc.items()} rxn_ind_map = {ind: rxn for rxn, ind in rates.index_rx.items()} - self._rate_helper = self.FluxTimesXSHelper(self.reaction_rates.n_nuc, self.reaction_rates.n_react, self) + self._rate_helper = self.FluxTimesXSHelper( + self.reaction_rates.n_nuc, self.reaction_rates.n_react, self) self._rate_helper.nuc_ind_map = nuc_ind_map self._rate_helper.rxn_ind_map = rxn_ind_map @@ -378,7 +383,6 @@ class FluxDepletionOperator(OpenMCOperator): # Since we aren't running a transport simulation, we simply pass pass - @staticmethod def create_micro_xs_from_data_array( nuclides, reactions, data, units='barn'): @@ -411,7 +415,8 @@ class FluxDepletionOperator(OpenMCOperator): f'reactions array of length {len(reactions)} do not ' f'match dimensions of data array of shape {data.shape}') - FluxDepletionOperator._validate_micro_xs_inputs(nuclides, reactions, data) + FluxDepletionOperator._validate_micro_xs_inputs( + nuclides, reactions, data) # Convert to cm^2 if units == 'barn': @@ -441,8 +446,8 @@ class FluxDepletionOperator(OpenMCOperator): micro_xs = pd.read_csv(csv_file, index_col=0) FluxDepletionOperator._validate_micro_xs_inputs(list(micro_xs.index), - list(micro_xs.columns), - micro_xs.to_numpy()) + list(micro_xs.columns), + micro_xs.to_numpy()) if units == 'barn': micro_xs /= 1e24 @@ -457,6 +462,3 @@ class FluxDepletionOperator(OpenMCOperator): check_type('data', data, np.ndarray, expected_iter_type=float) for reaction in reactions: check_value('reactions', reaction, valid_rxns) - - - diff --git a/tests/regression_tests/deplete_no_transport/test.py b/tests/regression_tests/deplete_no_transport/test.py index d326a17c1..f82a342cf 100644 --- a/tests/regression_tests/deplete_no_transport/test.py +++ b/tests/regression_tests/deplete_no_transport/test.py @@ -46,16 +46,18 @@ def test_no_transport(run_in_tmpdir, vol_nuc, multiproc): micro_xs_file = Path(__file__).parents[2] / 'micro_xs_simple.csv' micro_xs = FluxDepletionOperator.create_micro_xs_from_csv(micro_xs_file) chain_file = Path(__file__).parents[2] / 'chain_simple.xml' - flux = 1164719970082145.0 # flux from pincell example - op = FluxDepletionOperator.from_nuclides(vol_nuc[0], vol_nuc[1], micro_xs, flux, chain_file) + flux = 1164719970082145.0 # flux from pincell example + op = FluxDepletionOperator.from_nuclides( + vol_nuc[0], vol_nuc[1], micro_xs, flux, chain_file) # Power and timesteps - dt = [30] # single step - power = 174 # W/cm + dt = [30] # single step + power = 174 # W/cm # Perform simulation using the predictor algorithm openmc.deplete.pool.USE_MULTIPROCESSING = multiproc - openmc.deplete.PredictorIntegrator(op, dt, power, timestep_units='d').integrate() + openmc.deplete.PredictorIntegrator( + op, dt, power, timestep_units='d').integrate() # Get path to test and reference results path_test = op.output_dir / 'depletion_results.h5' @@ -102,4 +104,3 @@ def test_no_transport(run_in_tmpdir, vol_nuc, multiproc): assert correct, "Discrepancy in mat {} and nuc {}\n{}\n{}".format( mat, nuc, y_old, y_test) - From 61ad9c977c52df40e012a9818098dbce04974040 Mon Sep 17 00:00:00 2001 From: yardasol Date: Wed, 20 Jul 2022 11:04:39 -0500 Subject: [PATCH 068/131] fix Minimal example syntax --- docs/source/pythonapi/deplete.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/source/pythonapi/deplete.rst b/docs/source/pythonapi/deplete.rst index 1f4321642..cb9adfaaf 100644 --- a/docs/source/pythonapi/deplete.rst +++ b/docs/source/pythonapi/deplete.rst @@ -40,7 +40,8 @@ transport-depletion coupling algorithms `_. SICELIIntegrator SILEQIIntegrator -Each of these classes expects a "transport operator" to be passed. +Each of these classes expects a "transport operator" to be passed. Operators +specific to OpenMC are available using the following classes: .. autosummary:: :toctree: generated @@ -65,11 +66,12 @@ A minimal example for performing depletion would be: >>> import openmc.deplete >>> geometry = openmc.Geometry.from_xml() >>> settings = openmc.Settings.from_xml() + >>> model = openmc.model.Model(geometry, settings) # Representation of a depletion chain >>> chain_file = "chain_casl.xml" >>> operator = openmc.deplete.Operator( - ... geometry, settings, chain_file) + ... model, chain_file) # Set up 5 time steps of one day each >>> dt = [24 * 60 * 60] * 5 From 06f34b9f232256c0968b1a3ab10132754018bef9 Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 21 Jul 2022 11:00:45 -0500 Subject: [PATCH 069/131] address most of paulromano's comments --- docs/source/usersguide/depletion.rst | 8 +- openmc/deplete/flux_operator.py | 234 +++++++++--------- openmc/deplete/openmc_operator.py | 46 ++-- openmc/deplete/operator.py | 58 ++--- .../unit_tests/test_flux_deplete_operator.py | 2 +- 5 files changed, 159 insertions(+), 189 deletions(-) diff --git a/docs/source/usersguide/depletion.rst b/docs/source/usersguide/depletion.rst index 8b960f01f..4ce495623 100644 --- a/docs/source/usersguide/depletion.rst +++ b/docs/source/usersguide/depletion.rst @@ -113,7 +113,7 @@ should be, including indirect components. Some examples are provided below:: fission_q = {"U235": 202e+6} # energy in eV # create a Model object - model = openmc.model.Model(geometry, settings) + model = openmc.Model(geometry, settings) # create a modified chain and write it to a new file chain = openmc.deplete.Chain.from_xml("chain.xml", fission_q) @@ -180,7 +180,7 @@ across all material instances. number of tallies and material definitions. Transport-independent depletion -------------------------------- +=============================== .. note:: @@ -188,10 +188,10 @@ Transport-independent depletion possible and likely in the near future. OpenMC supports running depletion calculations independent of the OpenMC -transport solver using the :class:`FluxDepletionOperator` class. This class +transport solver using the :class:`~openmc.deplete.FluxDepletionOperator` class. This class has two ways to initalize it; the default constructor accepts an :class:`openmc.Materials` object, a flux spectra, and one-group microscopic -cross sections as a :class:`pandas.Dataframe`, while the `from_nuclides` +cross sections as a :class:`pandas.DataFrame`, while the `from_nuclides` method accepts a volume and dictionary of nuclide concentrations in place of the :class:`openmc.Materials` object in addition to the other parameters. The class includes helper functions to construct the dataframe from a csv file diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/flux_operator.py index f63e72405..cfcec242a 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/flux_operator.py @@ -1,6 +1,6 @@ """Pure depletion operator -This module implements a pure depletion operator that uses user provided fluxes +This module implements a pure depletion operator that uses user- provided fluxes and one-group cross sections. """ @@ -17,25 +17,23 @@ from uncertainties import ufloat import openmc from openmc.checkvalue import check_type, check_value, check_iterable_type from openmc.mpi import comm -from .abc import TransportOperator, ReactionRateHelper, OperatorResult -from .atom_number import AtomNumber +from .abc import ReactionRateHelper, OperatorResult from .chain import REACTIONS from .openmc_operator import OpenMCOperator -from .reaction_rates import ReactionRates from .helpers import ConstantFissionYieldHelper, SourceRateHelper -valid_rxns = list(REACTIONS) -valid_rxns.append('fission') +_valid_rxns = list(REACTIONS) +_valid_rxns.append('fission') class FluxDepletionOperator(OpenMCOperator): - """Depletion operator that uses a user-provided flux spectrum and one-group + """Depletion operator that uses a user-provided flux and one-group cross sections to calculate reaction rates. - Instances of this class can be used to perform depletion using one group + Instances of this class can be used to perform depletion using one-group cross sections and constant flux. Normally, a user needn't call methods of this class directly. Instead, an instance of this class is passed to an - integrator class, such as :class:`openmc.deplete.CECMIntegrator` + integrator class, such as :class:`openmc.deplete.CECMIntegrator`. Parameters ---------- @@ -44,8 +42,8 @@ class FluxDepletionOperator(OpenMCOperator): micro_xs : pandas.DataFrame DataFrame with nuclides names as index and microscopic cross section data in the columns. Cross section units are [cm^-2]. - flux_spectra : float - Flux spectrum [n cm^-2 s^-1] + flux : float + Neutron flux [n cm^-2 s^-1] chain_file : str Path to the depletion chain XML file. keff : 2-tuple of float, optional @@ -63,16 +61,27 @@ class FluxDepletionOperator(OpenMCOperator): Depth of the search when reducing the depletion chain. Only used if ``reduce_chain`` evaluates to true. The default value of ``None`` implies no limit on the depth. + fission_yield_opts : dict of str to option, optional + Optional arguments to pass to the `FissionYieldHelper`. Will be + passed directly on to the helper. Passing a value of None will use + the defaults for the associated helper. Attributes ---------- + materials : openmc.Materials + All materials present in the model + cross_sections : pandas.DataFrame + Object containing one-group cross-sections. + dilute_initial : float + Initial atom density [atoms/cm^3] to add for nuclides that + are zero in initial condition to ensure they exist in the decay + chain. Only done for nuclides with reaction rates. + output_dir : pathlib.Path + Path to output directory to save results. round_number : bool Whether or not to round output to OpenMC to 8 digits. Useful in testing, as OpenMC is incredibly sensitive to exact values. - prev_res : Results or None - Results from a previous depletion calculation. ``None`` if no - results are to be used. number : openmc.deplete.AtomNumber Total number of atoms in simulation. nuclides_with_data : set of str @@ -81,14 +90,55 @@ class FluxDepletionOperator(OpenMCOperator): The depletion chain information necessary to form matrices and tallies. reaction_rates : openmc.deplete.ReactionRates Reaction rates from the last operator step. + burnable_mats : list of str + All burnable material IDs + heavy_metal : float + Initial heavy metal inventory [g] + local_mats : list of str + All burnable material IDs being managed by a single process prev_res : Results or None Results from a previous depletion calculation. ``None`` if no results are to be used. - """ + + """ + + def __init__(self, + materials, + micro_xs, + flux, + chain_file, + keff=None, + fission_q=None, + prev_results=None, + reduce_chain=False, + reduce_chain_level=None, + fission_yield_opts=None): + # Validate micro-xs parameters + check_type('materials', materials, openmc.Materials) + check_type('micro_xs', micro_xs, pd.DataFrame) + if keff is not None: + check_type('keff', keff, tuple, float) + keff = ufloat(*keff) + + self._keff = keff + self.flux = flux + + helper_kwargs = dict() + helper_kwargs = {'fission_yield_opts': fission_yield_opts} + + super().__init__( + materials, + micro_xs, + chain_file, + prev_results, + fission_q=fission_q, + helper_kwargs=helper_kwargs, + reduce_chain=reduce_chain, + reduce_chain_level=reduce_chain_level) @classmethod def from_nuclides(cls, volume, nuclides, micro_xs, - flux_spectra, + flux, chain_file, keff=None, fission_q=None, @@ -107,8 +157,8 @@ class FluxDepletionOperator(OpenMCOperator): micro_xs : pandas.DataFrame DataFrame with nuclides names as index and microscopic cross section data in the columns. Cross section units are [cm^-2]. - flux_spectra : float - Flux spectrum [n cm^-2 s^-1] + flux : float + Neutron flux [n cm^-2 s^-1] chain_file : str Path to the depletion chain XML file. keff : 2-tuple of float, optional @@ -126,12 +176,17 @@ class FluxDepletionOperator(OpenMCOperator): Depth of the search when reducing the depletion chain. Only used if ``reduce_chain`` evaluates to true. The default value of ``None`` implies no limit on the depth. + fission_yield_opts : dict of str to option, optional + Optional arguments to pass to the `FissionYieldHelper`. Will be + passed directly on to the helper. Passing a value of None will use + the defaults for the associated helper. + """ check_type('nuclides', nuclides, dict, str) materials = cls._consolidate_nuclides_to_material(nuclides, volume) return cls(materials, micro_xs, - flux_spectra, + flux, chain_file, keff, fission_q, @@ -140,42 +195,6 @@ class FluxDepletionOperator(OpenMCOperator): reduce_chain_level, fission_yield_opts) - def __init__(self, - materials, - micro_xs, - flux_spectra, - chain_file, - keff=None, - fission_q=None, - prev_results=None, - reduce_chain=False, - reduce_chain_level=None, - fission_yield_opts=None): - # Validate micro-xs parameters - check_type('materials', materials, openmc.Materials) - check_type('micro_xs', micro_xs, pd.DataFrame) - if keff is not None: - check_type('keff', keff, tuple, float) - keff = ufloat(keff) - - self._keff = keff - self.flux_spectra = flux_spectra - - diff_burnable_mats = False - helper_kwargs = dict() - helper_kwargs['fission_yield_opts'] = fission_yield_opts - - super().__init__( - materials, - micro_xs, - chain_file, - prev_results, - diff_burnable_mats, - fission_q, - 0.0, - helper_kwargs, - reduce_chain, - reduce_chain_level) @staticmethod def _consolidate_nuclides_to_material(nuclides, volume): @@ -185,27 +204,18 @@ class FluxDepletionOperator(OpenMCOperator): openmc.reset_auto_ids() mat = openmc.Material() for nuc, conc in nuclides.items(): - mat.add_nuclide(nuc, conc / 1e24) # convert to at/b-cm + mat.add_nuclide(nuc, conc * 1e-24) # convert to at/b-cm mat.volume = volume - mat.depleteable = True + mat.depletable = True return openmc.Materials([mat]) - def _differentiate_burnable_mats(): - """Assign distribmats for each burnable material""" - pass - - def _load_previous_results(): - """Load in results from a previous depletion calculation.""" - pass - def _get_nuclides_with_data(self, cross_sections): - """Finds nuclides with cross section data - """ + """Finds nuclides with cross section data""" return set(cross_sections.index) - class FluxTimesXSHelper(ReactionRateHelper): + class _FluxDepletionRateHelper(ReactionRateHelper): """Class for generating one-group reaction rates with flux and one-group cross sections. @@ -216,13 +226,14 @@ class FluxDepletionOperator(OpenMCOperator): Parameters ---------- - outer : openmc.deplete.FluxDepletionOperator - Reference to the object encapsulate FluxTimesXSHelper. - We pass this so we don't have to duplicate the ``number`` object. n_nucs : int Number of burnable nuclides tracked by :class:`openmc.deplete.Operator` n_react : int Number of reactions tracked by :class:`openmc.deplete.Operator` + op : openmc.deplete.FluxDepletionOperator + Reference to the object encapsulate _FluxDepletionRateHelper. + We pass this so we don't have to duplicate the ``number`` object. + Attributes ---------- @@ -233,14 +244,17 @@ class FluxDepletionOperator(OpenMCOperator): """ - def __init__(self, n_nuc, n_react, outer): + def __init__(self, n_nuc, n_react, op, nuc_ind_map. rxn_ind_map) super().__init__(n_nuc, n_react) - self.outer = outer - self.nuc_ind_map = None - self.rxn_ind_map = None + self._op = op + rates = self.reaction_rates + # Get classes to assit working with tallies + self.nuc_ind_map = {ind: nuc for nuc, ind in rates.index_nuc.items()} + self.rxn_ind_map = {ind: rxn for rxn, ind in rates.index_rx.items()} def generate_tallies(self, materials, scores): """Unused in this case""" + pass def get_material_rates(self, mat_id, nuc_index, react_index): """Return 2D array of [nuclide, reaction] reaction rates @@ -255,37 +269,35 @@ class FluxDepletionOperator(OpenMCOperator): Ordering of reactions """ self._results_cache.fill(0.0) - for i, (i_nuc, i_react) in enumerate( - product(nuc_index, react_index)): + for i_nuc, i_react in product(nuc_index, react_index): nuc = self.nuc_ind_map[i_nuc] rxn = self.rxn_ind_map[i_react] - density = self.outer.number.get_atom_density(mat_id, nuc) + density = self._op.number.get_atom_density(mat_id, nuc) self._results_cache[i_nuc, - i_react] = self.outer.cross_sections[rxn][nuc] * density + i_react] = self._op.cross_sections[rxn][nuc] * density return self._results_cache def _get_helper_classes(self, helper_kwargs): - """Get helper classes for calculating reation rates and fission yields""" + """Get helper classes for calculating reation rates and fission yields - fission_yield_opts = helper_kwargs['fission_yield_opts'] + Parameters + ---------- + helper_kwargs : dict + Keyword arguments for helper classes - rates = self.reaction_rates - # Get classes to assit working with tallies - nuc_ind_map = {ind: nuc for nuc, ind in rates.index_nuc.items()} - rxn_ind_map = {ind: rxn for rxn, ind in rates.index_rx.items()} - self._rate_helper = self.FluxTimesXSHelper( + """ + + fission_yield_opts = helper_kwargs.get('fission_yield_opts', {}) + + self._rate_helper = self._FluxDepletionRateHelper( self.reaction_rates.n_nuc, self.reaction_rates.n_react, self) - self._rate_helper.nuc_ind_map = nuc_ind_map - self._rate_helper.rxn_ind_map = rxn_ind_map self._normalization_helper = SourceRateHelper() # Select and create fission yield helper fission_helper = ConstantFissionYieldHelper - fission_yield_opts = ( - {} if fission_yield_opts is None else fission_yield_opts) self._yield_helper = fission_helper.from_operator( self, **fission_yield_opts) @@ -320,8 +332,8 @@ class FluxDepletionOperator(OpenMCOperator): self._update_materials_and_nuclides(vec) - # Use the flux spectra as a "source rate" - rates = self._calculate_reaction_rates(self.flux_spectra) + # Use the flux as a "source rate" + rates = self._calculate_reaction_rates(self.flux) keff = self._keff op_result = OperatorResult(keff, rates) @@ -356,36 +368,14 @@ class FluxDepletionOperator(OpenMCOperator): # negative. CRAM does not guarantee positive # values. if val < -1.0e-21: - print( - "WARNING: nuclide ", - nuc, - " in material ", - mat, - " is negative (density = ", - val, - " at/barn-cm)") + print(f'WARNING: nuclide {nuc} in material' + f'{mat} is negative (density = {val}' + + ' at/barn-cm)') number_i[mat, nuc] = 0.0 - # TODO Update densities on the Python side, otherwise the - # summary.h5 file contains densities at the first time step - - def write_bos_data(self, step): - """Document beginning of step data for a given step - - Called at the beginning of a depletion step and at - the final point in the simulation. - - Parameters - ---------- - step : int - Current depletion step including restarts - """ - # Since we aren't running a transport simulation, we simply pass - pass - @staticmethod - def create_micro_xs_from_data_array( - nuclides, reactions, data, units='barn'): + def create_micro_xs_from_data_array(nuclides, reactions, data, units='barn'): """ Creates a ``micro_xs`` parameter from a dictionary. @@ -420,7 +410,7 @@ class FluxDepletionOperator(OpenMCOperator): # Convert to cm^2 if units == 'barn': - data /= 1e24 + data *= 1e-24 return pd.DataFrame(index=nuclides, columns=reactions, data=data) @@ -450,7 +440,7 @@ class FluxDepletionOperator(OpenMCOperator): micro_xs.to_numpy()) if units == 'barn': - micro_xs /= 1e24 + micro_xs *= 1e-24 return micro_xs @@ -461,4 +451,4 @@ class FluxDepletionOperator(OpenMCOperator): check_iterable_type('reactions', reactions, str) check_type('data', data, np.ndarray, expected_iter_type=float) for reaction in reactions: - check_value('reactions', reaction, valid_rxns) + check_value('reactions', reaction, _valid_rxns) diff --git a/openmc/deplete/openmc_operator.py b/openmc/deplete/openmc_operator.py index 536f67c4b..0ea1f093c 100644 --- a/openmc/deplete/openmc_operator.py +++ b/openmc/deplete/openmc_operator.py @@ -66,16 +66,14 @@ class OpenMCOperator(TransportOperator): diff_burnable_mats : bool, optional Whether to differentiate burnable materials with multiple instances. Volumes are divided equally from the original material volume. - Default: False. fission_q : dict, optional Dictionary of nuclides and their fission Q values [eV]. dilute_initial : float, optional Initial atom density [atoms/cm^3] to add for nuclides that are zero in initial condition to ensure they exist in the decay chain. Only done for nuclides with reaction rates. - Defaults to 1.0e3. helper_kwargs : dict - Arguments for helper classes + Keyword arguments for helper classes reduce_chain : bool, optional If True, use :meth:`openmc.deplete.Chain.reduce` to reduce the depletion chain up to ``reduce_chain_level``. Default is False. @@ -89,6 +87,10 @@ class OpenMCOperator(TransportOperator): ---------- materials : openmc.Materials All materials present in the model + cross_sections : str or pandas.DataFrame + Path to continuous energy cross section library, or object + containing one-group cross-sections. + dilute_initial : float Initial atom density [atoms/cm^3] to add for nuclides that are zero in initial condition to ensure they exist in the decay @@ -182,9 +184,9 @@ class OpenMCOperator(TransportOperator): self._get_helper_classes(helper_kwargs) - @abstractmethod def _differentiate_burnable_mats(self): """Assign distribmats for each burnable material""" + pass def _get_burnable_mats(self): """Determine depletable materials, volumes, and nuclides @@ -235,9 +237,9 @@ class OpenMCOperator(TransportOperator): return burnable_mats, volume, nuclides - @abstractmethod def _load_previous_results(self): """Load results from a previous depletion simulation""" + pass @abstractmethod def _get_nuclides_with_data(self, cross_sections): @@ -271,9 +273,7 @@ class OpenMCOperator(TransportOperator): Results from a previous depletion calculation """ - self.number = AtomNumber( - local_mats, all_nuclides, volume, len( - self.chain)) + self.number = AtomNumber(local_mats, all_nuclides, volume, len(self.chain)) if self.dilute_initial != 0.0: for nuc in self._burnable_nucs: @@ -349,20 +349,8 @@ class OpenMCOperator(TransportOperator): Parameters ---------- - reaction_rate_mode : str - Indicates the subclass of :class:`ReactionRateHelper` to - instantiate. - normalization_mode : str - Indicates the subclass of :class:`NormalizationHelper` to - instantiate. - fission_yield_mode : str - Indicates the subclass of :class:`FissionYieldHelper` to instatiate. - reaction_rate_opts : dict - Keyword arguments that are passed to the :class:`ReactionRateHelper` - subclass. - fission_yield_opts : dict - Keyword arguments that are passed to the :class:`FissionYieldHelper` - subclass. + helper_kwargs : dict + Keyword arguments for helper classes """ @@ -420,6 +408,20 @@ class OpenMCOperator(TransportOperator): def _update_materials(self): """Updates material compositions in OpenMC on all processes.""" + def write_bos_data(self, step): + """Document beginning of step data for a given step + + Called at the beginning of a depletion step and at + the final point in the simulation. + + Parameters + ---------- + step : int + Current depletion step including restarts + """ + # Since we aren't running a transport simulation, we simply pass + pass + def _get_reaction_nuclides(self): """Determine nuclides that should be tallied for reaction rates. diff --git a/openmc/deplete/operator.py b/openmc/deplete/operator.py index c5538712d..9b083ffe9 100644 --- a/openmc/deplete/operator.py +++ b/openmc/deplete/operator.py @@ -83,7 +83,6 @@ class Operator(OpenMCOperator): diff_burnable_mats : bool, optional Whether to differentiate burnable materials with multiple instances. Volumes are divided equally from the original material volume. - Default: False. normalization_mode : {"energy-deposition", "fission-q", "source-rate"} Indicate how tally results should be normalized. ``"energy-deposition"`` computes the total energy deposited in the system and uses the ratio of @@ -99,7 +98,6 @@ class Operator(OpenMCOperator): Initial atom density [atoms/cm^3] to add for nuclides that are zero in initial condition to ensure they exist in the decay chain. Only done for nuclides with reaction rates. - Defaults to 1.0e3. fission_yield_mode : {"constant", "cutoff", "average"} Key indicating what fission product yield scheme to use. The key determines what fission energy helper is used: @@ -228,11 +226,13 @@ class Operator(OpenMCOperator): self.cleanup_when_done = True helper_kwargs = dict() - helper_kwargs['reaction_rate_mode'] = reaction_rate_mode - helper_kwargs['normalization_mode'] = normalization_mode - helper_kwargs['fission_yield_mode'] = fission_yield_mode - helper_kwargs['reaction_rate_opts'] = reaction_rate_opts - helper_kwargs['fission_yield_opts'] = fission_yield_opts + helper_kwargs = { + 'reaction_rate_mode': reaction_rate_mode, + 'normalization_mode': normalization_mode, + 'fission_yield_mode': fission_yield_mode, + 'reaction_rate_opts': reaction_rate_opts, + 'fission_yield_opts': fission_yield_opts + } super().__init__( model.materials, @@ -322,37 +322,21 @@ class Operator(OpenMCOperator): Parameters ---------- - **kwargs : ... - reaction_rate_mode : str - Indicates the subclass of :class:`ReactionRateHelper` to - instantiate. - normalization_mode : str - Indicates the subclass of :class:`NormalizationHelper` to - instatiate. - fission_yield_mode : str - Indicates the subclass of :class:`FissionYieldHelper` to instantiate. - reaction_rate_opts : dict - Keyword arguments that are passed to the :class:`ReactionRateHelper` - subclass. - fission_yield_opts : dict - Keyword arguments that are passed to the :class:`FissionYieldHelper` - subclass. + helper_kwargs : dict + Keyword arguments for helper classes """ reaction_rate_mode = helper_kwargs['reaction_rate_mode'] normalization_mode = helper_kwargs['normalization_mode'] fission_yield_mode = helper_kwargs['fission_yield_mode'] - reaction_rate_opts = helper_kwargs['reaction_rate_opts'] - fission_yield_opts = helper_kwargs['fission_yield_opts'] + reaction_rate_opts = helper_kwargs.get('reaction_rate_opts', {}) + fission_yield_opts = helper_kwargs.get('fission_yield_opts', {}) # Get classes to assist working with tallies if reaction_rate_mode == "direct": self._rate_helper = DirectReactionRateHelper( self.reaction_rates.n_nuc, self.reaction_rates.n_react) elif reaction_rate_mode == "flux": - if reaction_rate_opts is None: - reaction_rate_opts = {} - # Ensure energy group boundaries were specified if 'energies' not in reaction_rate_opts: raise ValueError( @@ -378,8 +362,6 @@ class Operator(OpenMCOperator): # Select and create fission yield helper fission_helper = self._fission_helpers[fission_yield_mode] - fission_yield_opts = ( - {} if fission_yield_opts is None else fission_yield_opts) self._yield_helper = fission_helper.from_operator( self, **fission_yield_opts) @@ -405,8 +387,7 @@ class Operator(OpenMCOperator): openmc.lib.init(intracomm=comm) # Generate tallies in memory - materials = [openmc.lib.materials[int(i)] - for i in self.burnable_mats] + materials = [openmc.lib.materials[int(i)] for i in self.burnable_mats] return super().initial_condition(materials) @@ -500,15 +481,12 @@ class Operator(OpenMCOperator): # negative. CRAM does not guarantee positive # values. if val < -1.0e-21: - print( - "WARNING: nuclide ", - nuc, - " in material ", - mat, - " is negative (density = ", - val, - " at/barn-cm)") - number_i[mat, nuc] = 0.0 + print(f'WARNING: nuclide {nuc} in material' + f'{mat} is negative (density = {val}' + + ' at/barn-cm)') + + number_i[mat, nuc] = 0.0 # Update densities on C API side mat_internal = openmc.lib.materials[int(mat)] diff --git a/tests/unit_tests/test_flux_deplete_operator.py b/tests/unit_tests/test_flux_deplete_operator.py index 315e22cdc..3ae098490 100644 --- a/tests/unit_tests/test_flux_deplete_operator.py +++ b/tests/unit_tests/test_flux_deplete_operator.py @@ -11,7 +11,7 @@ from openmc.deplete.flux_operator import FluxDepletionOperator import pandas as pd import numpy as np -FLUX_SPECTRA = 5e16 +FLUX = 5e16 CHAIN_PATH = Path(__file__).parents[1] / "chain_simple.xml" ONE_GROUP_XS = Path(__file__).parents[1] / "micro_xs_simple.csv" From 0984b29f67099e876ae97435ba1fa85d7e86d06a Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 21 Jul 2022 12:14:47 -0500 Subject: [PATCH 070/131] add machinery to do constant-power depletion with FluxDepletionOperator --- openmc/deplete/abc.py | 10 +++- openmc/deplete/atom_number.py | 17 ++++++ openmc/deplete/flux_operator.py | 86 +++++++++++++++++++++++++++---- openmc/deplete/helpers.py | 4 +- openmc/deplete/openmc_operator.py | 2 +- 5 files changed, 104 insertions(+), 15 deletions(-) diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 8bbe126df..c00c80399 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -329,7 +329,7 @@ class NormalizationHelper(ABC): `fission_rates` for :meth:`update`. """ - def update(self, fission_rates): + def update(self, fission_rates, mat_index=None): """Update the normalization based on fission rates (only used for energy-based normalization) @@ -339,6 +339,8 @@ class NormalizationHelper(ABC): fission reaction rate for each isotope in the specified material. Should be ordered corresponding to initial ``rate_index`` used in :meth:`prepare` + mat_index : int + Material index """ @property @@ -523,6 +525,8 @@ class Integrator(ABC): Power density of the reactor in [W/gHM]. It is multiplied by initial heavy metal inventory to get total power if ``power`` is not speficied. + flux : float or iterable of float, optional + Neutron flux in [neur/s-cm^2] for each interval in :attr: `timesteps` source_rates : float or iterable of float, optional Source rate in [neutron/sec] for each interval in :attr:`timesteps` @@ -597,8 +601,10 @@ class Integrator(ABC): source_rates = power_density * operator.heavy_metal else: source_rates = [p*operator.heavy_metal for p in power_density] + elif flux is not None: + source_rates = flux elif source_rates is None: - raise ValueError("Either power, power_density, or source_rates must be set") + raise ValueError("Either power, power_density, flux, or source_rates must be set") if not isinstance(source_rates, Iterable): # Ensure that rate is single value if that is the case diff --git a/openmc/deplete/atom_number.py b/openmc/deplete/atom_number.py index 5430ddc35..78ceecca6 100644 --- a/openmc/deplete/atom_number.py +++ b/openmc/deplete/atom_number.py @@ -126,6 +126,23 @@ class AtomNumber: return [nuc for nuc, ind in self.index_nuc.items() if ind < self.n_nuc_burn] + def get_mat_volume(self, mat): + """Return material volume + + Parameters + ---------- + mat : str, int, openmc.Material, or slice + Material index. + + Returns + ------- + float + Material volume in [cm^3] + + """ + mat = self._get_mat_index(mat) + return self.volume[mat] + def get_atom_density(self, mat, nuc): """Return atom density of given material and nuclide diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/flux_operator.py index cfcec242a..9ff0dee5d 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/flux_operator.py @@ -54,6 +54,15 @@ class FluxDepletionOperator(OpenMCOperator): values will be pulled from the ``chain_file``. prev_results : Results, optional Results from a previous depletion calculation. + normalization_mode : {"constant-power", "constant-flux"} + Indicate how reaction rates should be calculated. + ``"constant-power"`` uses the fission Q values from the depletion chain to + compute the flux based on the power. ``"constant-flux"`` uses the value stored in `_normalization_helper` as the flux. + fission_q : dict, optional + Dictionary of nuclides and their fission Q values [eV]. If not given, + values will be pulled from the ``chain_file``. Only applicable + if ``"normalization_mode" == "constant-power"``. + reduce_chain : bool, optional If True, use :meth:`openmc.deplete.Chain.reduce` to reduce the depletion chain up to ``reduce_chain_level``. Default is False. @@ -108,6 +117,7 @@ class FluxDepletionOperator(OpenMCOperator): flux, chain_file, keff=None, + normalization_mode = 'constant-flux', fission_q=None, prev_results=None, reduce_chain=False, @@ -124,7 +134,8 @@ class FluxDepletionOperator(OpenMCOperator): self.flux = flux helper_kwargs = dict() - helper_kwargs = {'fission_yield_opts': fission_yield_opts} + helper_kwargs = {'normalization_mode': normalization_mode, + 'fission_yield_opts': fission_yield_opts} super().__init__( materials, @@ -152,7 +163,7 @@ class FluxDepletionOperator(OpenMCOperator): volume : float Volume of the material being depleted in [cm^3] nuclides : dict of str to float - Dictionary with nuclide names as keys and nuclide concentrations as + ,Dictionary with nuclide names as keys and nuclide concentrations as values. Nuclide concentration units are [atom/cm^3]. micro_xs : pandas.DataFrame DataFrame with nuclides names as index and microscopic cross section @@ -164,9 +175,15 @@ class FluxDepletionOperator(OpenMCOperator): keff : 2-tuple of float, optional keff eigenvalue and uncertainty from transport calculation. Default is None. + normalization_mode : {"constant-power", "constant-flux"} + Indicate how reaction rates should be calculated. + ``"constant-power"`` uses the fission Q values from the depletion + chain to compute the flux based on the power. ``"constant-flux"`` + uses the value stored in `_normalization_helper` as the flux. fission_q : dict, optional - Dictionary of nuclides and their fission Q values [eV]. If not given, - values will be pulled from the ``chain_file``. + Dictionary of nuclides and their fission Q values [eV]. If not + given, values will be pulled from the ``chain_file``. Only + applicable if ``"normalization_mode" == "constant-power"``. prev_results : Results, optional Results from a previous depletion calculation. reduce_chain : bool, optional @@ -215,6 +232,49 @@ class FluxDepletionOperator(OpenMCOperator): """Finds nuclides with cross section data""" return set(cross_sections.index) + class _FluxDepletionNormalizationHelper(ChainFissionHelper): + """Class for calculating one-group flux + based on a power. + + flux = Power / X, where X = volume * sum_i(Q_i * fission_micro_xs_i * density_i) + + Parameters + ---------- + op : openmc.deplete.FluxDepletionOperator + Reference to the object encapsulate _FluxDepletionNormalizationHelper. + We pass this so we don't have to duplicate the ``number`` object. + + """ + + def __init__(self, op): + self._op = op + rates = self.reaction_rates + self.nuc_ind_map = {ind: nuc for nuc, ind in rates.index_nuc.items()} + super().__init__() + + def update(self, fission_rates, mat_index=None): + """Update 'energy' produced with fission rates in a material. What this + actually calculates is the quantity X. + + Parameters + ---------- + fission_rates : numpy.ndarray + fission reaction rate for each isotope in the specified + material. Should be ordered corresponding to initial + ``rate_index`` used in :meth:`prepare` + mat_index : int + Material index + + """ + volume = self._op.number.get_mat_volume(mat_index) + densities = np.empty(shape(fission_rates)) + for i_nuc in nuc_ind_map: + nuc = self.nuc_ind_map[i_nuc] + densities[i_nuc] = self._op.number.get_atom_density(mat_index, nuc) + fission_rates = fission_rates * volume * densities + + super.update(fission_rates) + class _FluxDepletionRateHelper(ReactionRateHelper): """Class for generating one-group reaction rates with flux and one-group cross sections. @@ -246,11 +306,11 @@ class FluxDepletionOperator(OpenMCOperator): def __init__(self, n_nuc, n_react, op, nuc_ind_map. rxn_ind_map) super().__init__(n_nuc, n_react) - self._op = op - rates = self.reaction_rates - # Get classes to assit working with tallies + rates = op.reaction_rates + self.nuc_ind_map = {ind: nuc for nuc, ind in rates.index_nuc.items()} self.rxn_ind_map = {ind: rxn for rxn, ind in rates.index_rx.items()} + self._op = op def generate_tallies(self, materials, scores): """Unused in this case""" @@ -269,12 +329,14 @@ class FluxDepletionOperator(OpenMCOperator): Ordering of reactions """ self._results_cache.fill(0.0) + + volume = self._op.number.get_mat_volume(mat_id) for i_nuc, i_react in product(nuc_index, react_index): nuc = self.nuc_ind_map[i_nuc] rxn = self.rxn_ind_map[i_react] density = self._op.number.get_atom_density(mat_id, nuc) self._results_cache[i_nuc, - i_react] = self._op.cross_sections[rxn][nuc] * density + i_react] = self._op.cross_sections[rxn][nuc] * density * volume return self._results_cache @@ -286,15 +348,17 @@ class FluxDepletionOperator(OpenMCOperator): helper_kwargs : dict Keyword arguments for helper classes - """ + normalization_mode = helper_kwargs['normalization_mode'] fission_yield_opts = helper_kwargs.get('fission_yield_opts', {}) self._rate_helper = self._FluxDepletionRateHelper( self.reaction_rates.n_nuc, self.reaction_rates.n_react, self) - - self._normalization_helper = SourceRateHelper() + if normalization_mode == "constant-power": + self._normalization_helper = self.FluxDepletionNormalizationHelper() + else: + self._normalization_helper = SourceRateHelper() # Select and create fission yield helper fission_helper = ConstantFissionYieldHelper diff --git a/openmc/deplete/helpers.py b/openmc/deplete/helpers.py index 33bd182d7..0aaa3b08a 100644 --- a/openmc/deplete/helpers.py +++ b/openmc/deplete/helpers.py @@ -423,7 +423,7 @@ class ChainFissionHelper(EnergyNormalizationHelper): self._fission_q_vector = fission_qs - def update(self, fission_rates): + def update(self, fission_rates, mat_index=None): """Update energy produced with fission rates in a material Parameters @@ -432,6 +432,8 @@ class ChainFissionHelper(EnergyNormalizationHelper): fission reaction rate for each isotope in the specified material. Should be ordered corresponding to initial ``rate_index`` used in :meth:`prepare` + mat_index : int + Unused """ self._energy += dot(fission_rates, self._fission_q_vector) diff --git a/openmc/deplete/openmc_operator.py b/openmc/deplete/openmc_operator.py index 0ea1f093c..aafa1adf9 100644 --- a/openmc/deplete/openmc_operator.py +++ b/openmc/deplete/openmc_operator.py @@ -527,7 +527,7 @@ class OpenMCOperator(TransportOperator): # Accumulate energy from fission if fission_ind is not None: - self._normalization_helper.update(tally_rates[:, fission_ind]) + self._normalization_helper.update(tally_rates[:, fission_ind], mat_index=mat_index) # Divide by total number and store rates[i] = self._rate_helper.divide_by_adens(number) From 2f43656f34305fe8bca3390d22d91c37ce3f2bc0 Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 21 Jul 2022 12:27:36 -0500 Subject: [PATCH 071/131] update user guide --- docs/source/usersguide/depletion.rst | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/source/usersguide/depletion.rst b/docs/source/usersguide/depletion.rst index 4ce495623..4be554b3c 100644 --- a/docs/source/usersguide/depletion.rst +++ b/docs/source/usersguide/depletion.rst @@ -188,9 +188,19 @@ Transport-independent depletion possible and likely in the near future. OpenMC supports running depletion calculations independent of the OpenMC -transport solver using the :class:`~openmc.deplete.FluxDepletionOperator` class. This class -has two ways to initalize it; the default constructor accepts an -:class:`openmc.Materials` object, a flux spectra, and one-group microscopic +transport solver using the :class:`~openmc.deplete.FluxDepletionOperator` class. +This class supports both constant-flux and constant-power depletion. + +.. important:: + + Make sure you set the correct parameter in the :class:`openmc.abc.Integrator` class. Use the ``flux`` parameter when ``normalization_mode == constant-flux``, and use ``power`` or ``power_density`` when ``normalization_mode == constant-power``. + +.. warning:: + + The accuracy of results when using ``constant-power`` is entirely dependent on your depletion chain. Make sure it has sufficient data to resolve the dynamics of your particular scenario. + +This class has two ways to initalize it; the default constructor accepts an +:class:`openmc.Materials` object and one-group microscopic cross sections as a :class:`pandas.DataFrame`, while the `from_nuclides` method accepts a volume and dictionary of nuclide concentrations in place of the :class:`openmc.Materials` object in addition to the other parameters. @@ -201,7 +211,7 @@ or from data arrays:: # load in the microscopic cross sections micro_xs = FluxDepletionOperator.create_micro_xs_from_csv(micro_xs_path) flux = 1.16e15 - op = FluxDepletionOperator(materials, micro_xs, flux, chain_file) + op = FluxDepletionOperator(materials, micro_xs, chain_file) # alternate construtor nuclides = {'U234': 8.92e18, From 8adefaad71240557974e413af1d597ac6e65878a Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 21 Jul 2022 12:32:09 -0500 Subject: [PATCH 072/131] use source_rate in __call__ --- openmc/deplete/flux_operator.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/flux_operator.py index 9ff0dee5d..1774eb000 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/flux_operator.py @@ -27,11 +27,11 @@ _valid_rxns.append('fission') class FluxDepletionOperator(OpenMCOperator): - """Depletion operator that uses a user-provided flux and one-group + """Depletion operator that uses one-group cross sections to calculate reaction rates. Instances of this class can be used to perform depletion using one-group - cross sections and constant flux. Normally, a user needn't call methods of + cross sections and constant flux or constant power. Normally, a user needn't call methods of this class directly. Instead, an instance of this class is passed to an integrator class, such as :class:`openmc.deplete.CECMIntegrator`. @@ -42,8 +42,6 @@ class FluxDepletionOperator(OpenMCOperator): micro_xs : pandas.DataFrame DataFrame with nuclides names as index and microscopic cross section data in the columns. Cross section units are [cm^-2]. - flux : float - Neutron flux [n cm^-2 s^-1] chain_file : str Path to the depletion chain XML file. keff : 2-tuple of float, optional @@ -114,7 +112,6 @@ class FluxDepletionOperator(OpenMCOperator): def __init__(self, materials, micro_xs, - flux, chain_file, keff=None, normalization_mode = 'constant-flux', @@ -131,7 +128,6 @@ class FluxDepletionOperator(OpenMCOperator): keff = ufloat(*keff) self._keff = keff - self.flux = flux helper_kwargs = dict() helper_kwargs = {'normalization_mode': normalization_mode, @@ -149,7 +145,6 @@ class FluxDepletionOperator(OpenMCOperator): @classmethod def from_nuclides(cls, volume, nuclides, micro_xs, - flux, chain_file, keff=None, fission_q=None, @@ -168,8 +163,6 @@ class FluxDepletionOperator(OpenMCOperator): micro_xs : pandas.DataFrame DataFrame with nuclides names as index and microscopic cross section data in the columns. Cross section units are [cm^-2]. - flux : float - Neutron flux [n cm^-2 s^-1] chain_file : str Path to the depletion chain XML file. keff : 2-tuple of float, optional @@ -203,7 +196,6 @@ class FluxDepletionOperator(OpenMCOperator): materials = cls._consolidate_nuclides_to_material(nuclides, volume) return cls(materials, micro_xs, - flux, chain_file, keff, fission_q, @@ -396,8 +388,7 @@ class FluxDepletionOperator(OpenMCOperator): self._update_materials_and_nuclides(vec) - # Use the flux as a "source rate" - rates = self._calculate_reaction_rates(self.flux) + rates = self._calculate_reaction_rates(source_rate) keff = self._keff op_result = OperatorResult(keff, rates) From ce077567aeb621a3b3954150bdbbca702d5da321 Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 21 Jul 2022 14:08:53 -0500 Subject: [PATCH 073/131] bugfix in operator.py --- openmc/deplete/operator.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openmc/deplete/operator.py b/openmc/deplete/operator.py index 9b083ffe9..61573832c 100644 --- a/openmc/deplete/operator.py +++ b/openmc/deplete/operator.py @@ -337,6 +337,9 @@ class Operator(OpenMCOperator): self._rate_helper = DirectReactionRateHelper( self.reaction_rates.n_nuc, self.reaction_rates.n_react) elif reaction_rate_mode == "flux": + if reaction_rate_opts is None: + reaction_rate_opts = {} + # Ensure energy group boundaries were specified if 'energies' not in reaction_rate_opts: raise ValueError( @@ -362,6 +365,8 @@ class Operator(OpenMCOperator): # Select and create fission yield helper fission_helper = self._fission_helpers[fission_yield_mode] + if fission_yield_opts is None: + fission_yield_opts = {} self._yield_helper = fission_helper.from_operator( self, **fission_yield_opts) From 4667cee8b874b9e265482027bf13bf39ac37f786 Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 21 Jul 2022 16:49:19 -0500 Subject: [PATCH 074/131] Added helper function to generate one-group cross sections --- openmc/deplete/flux_operator.py | 106 ++++++++++++++++++++++++++------ 1 file changed, 88 insertions(+), 18 deletions(-) diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/flux_operator.py index 1774eb000..8b4999f23 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/flux_operator.py @@ -17,10 +17,11 @@ from uncertainties import ufloat import openmc from openmc.checkvalue import check_type, check_value, check_iterable_type from openmc.mpi import comm +from openmc.mgxs import EnergyGroups, ArbitraryXS, FissionXS from .abc import ReactionRateHelper, OperatorResult from .chain import REACTIONS from .openmc_operator import OpenMCOperator -from .helpers import ConstantFissionYieldHelper, SourceRateHelper +from .helpers import ChainFissionHelper, ConstantFissionYieldHelper, SourceRateHelper _valid_rxns = list(REACTIONS) _valid_rxns.append('fission') @@ -147,6 +148,7 @@ class FluxDepletionOperator(OpenMCOperator): def from_nuclides(cls, volume, nuclides, micro_xs, chain_file, keff=None, + normalization_mode='constant-flux', fission_q=None, prev_results=None, reduce_chain=False, @@ -197,12 +199,13 @@ class FluxDepletionOperator(OpenMCOperator): return cls(materials, micro_xs, chain_file, - keff, - fission_q, - prev_results, - reduce_chain, - reduce_chain_level, - fission_yield_opts) + keff=keff, + normalization_mode=normalization_mode, + fission_q=fission_q, + prev_results=prev_results, + reduce_chain=reduce_chain, + reduce_chain_level=reduce_chain_level, + fission_yield_opts=fission_yield_opts) @staticmethod @@ -245,17 +248,17 @@ class FluxDepletionOperator(OpenMCOperator): super().__init__() def update(self, fission_rates, mat_index=None): - """Update 'energy' produced with fission rates in a material. What this - actually calculates is the quantity X. + """Update 'energy' produced with fission rates in a material. What + this actually calculates is the quantity X. - Parameters - ---------- - fission_rates : numpy.ndarray - fission reaction rate for each isotope in the specified - material. Should be ordered corresponding to initial - ``rate_index`` used in :meth:`prepare` - mat_index : int - Material index + Parameters + ---------- + fission_rates : numpy.ndarray + fission reaction rate for each isotope in the specified + material. Should be ordered corresponding to initial + ``rate_index`` used in :meth:`prepare` + mat_index : int + Material index """ volume = self._op.number.get_mat_volume(mat_index) @@ -296,7 +299,7 @@ class FluxDepletionOperator(OpenMCOperator): """ - def __init__(self, n_nuc, n_react, op, nuc_ind_map. rxn_ind_map) + def __init__(self, n_nuc, n_react, op): super().__init__(n_nuc, n_react) rates = op.reaction_rates @@ -354,6 +357,8 @@ class FluxDepletionOperator(OpenMCOperator): # Select and create fission yield helper fission_helper = ConstantFissionYieldHelper + if fission_yield_opts is None: + fission_yield_opts = {} self._yield_helper = fission_helper.from_operator( self, **fission_yield_opts) @@ -507,3 +512,68 @@ class FluxDepletionOperator(OpenMCOperator): check_type('data', data, np.ndarray, expected_iter_type=float) for reaction in reactions: check_value('reactions', reaction, _valid_rxns) + + + @staticmethod + def generate_1g_cross_sections(model, reaction_domain, reactions=['(n,gamma)', '(n,2n)', '(n,p)', '(n,a)', '(n,3n)', '(n,4n)', 'fission'], energy_bounds=(0, 20e6), write_to_csv=True, filename='micro_xs.csv'): + """Helper function to generate a one-group cross-section dataframe + using OpenMC. Note that the ``openmc`` C executable must be compiled. + + Parameters + ---------- + model : openmc.model.Model + OpenMC model object. Must contain geometry, materials, and settings. + reaction_domain : openmc.Material or openmc.Cell or openmc.Universe or openmc.RegularMesh + Domain in which to tally reaction rates. + reactions : list of str, optional + Reaction names to tally + energy_bound : 2-tuple of float, optional + Bounds for the energy group. + write_to_csv : bool, optional + Option to write the DataFrame to a `.csv` file. If `False`, + returns the dataframe object. + filename : str + Name for csv file. Only applicable if ``write_to_csv == True`` + + Returns + ------- + None or pandas.DataFrame + + """ + groups = EnergyGroups() + groups.group_edges = np.array(list(energy_bounds)) + + # Set up the reaction tallies + tallies = openmc.Tallies() + xs = dict() + for rxn in reactions: + if rxn == 'fission': + xs[rxn] = FissionXS(domain=reaction_domain, groups=groups, by_nuclide=True) + else: + xs[rxn] = ArbitraryXS(rxn, domain=reaction_domain, groups=groups, by_nuclide=True) + tallies += xs[rxn].tallies.values() + + model.tallies = tallies + statepoint_path = model.run() + + sp = openmc.StatePoint(statepoint_path) + + for rxn in xs: + xs[rxn].load_from_statepoint(sp) + + sp.close() + + # Build the DataFrame + micro_xs = pd.DataFrame() + for rxn in xs: + df = xs[rxn].get_pandas_dataframe(xs_type='micro') + df.index = df['nuclide'] + df.drop(['nuclide', xs[rxn].domain_type, 'group in', 'std. dev.'], axis=1, inplace=True) + df.rename({'mean':rxn}, axis=1, inplace=True) + micro_xs = pd.concat([micro_xs, df], axis=1) + + if write_to_csv: + micro_xs.to_csv(filename) + return None + else: + return micro_xs From ef238b4a4fd3034c5bf6be3238b58322bc9fb8e9 Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 21 Jul 2022 16:50:29 -0500 Subject: [PATCH 075/131] test_flux_deplete_operator.py -> test_deplete_flux_operator.py --- ...est_flux_deplete_operator.py => test_deplete_flux_operator.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/unit_tests/{test_flux_deplete_operator.py => test_deplete_flux_operator.py} (100%) diff --git a/tests/unit_tests/test_flux_deplete_operator.py b/tests/unit_tests/test_deplete_flux_operator.py similarity index 100% rename from tests/unit_tests/test_flux_deplete_operator.py rename to tests/unit_tests/test_deplete_flux_operator.py From 34598609b805f8bc6566c654c57b587c36fee069 Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 21 Jul 2022 17:24:16 -0500 Subject: [PATCH 076/131] add flux parameter to Integrator.__init__ signature --- openmc/deplete/abc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index c00c80399..c9e0d566b 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -579,7 +579,7 @@ class Integrator(ABC): """ def __init__(self, operator, timesteps, power=None, power_density=None, - source_rates=None, timestep_units='s', solver="cram48"): + flux=None, source_rates=None, timestep_units='s', solver="cram48"): # Check number of stages previously used if operator.prev_res is not None: res = operator.prev_res[-1] From 61b1d82544fc95ce30a40f25eceffac7e17e3b07 Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 21 Jul 2022 17:25:05 -0500 Subject: [PATCH 077/131] update test suite --- .../deplete_no_transport/test.py | 20 ++++++++++++------ ...nce.h5 => test_reference_constant_flux.h5} | Bin 35688 -> 35688 bytes .../test_reference_constant_power.h5 | Bin 0 -> 35688 bytes .../unit_tests/test_deplete_flux_operator.py | 5 +++-- 4 files changed, 17 insertions(+), 8 deletions(-) rename tests/regression_tests/deplete_no_transport/{test_reference.h5 => test_reference_constant_flux.h5} (99%) create mode 100644 tests/regression_tests/deplete_no_transport/test_reference_constant_power.h5 diff --git a/tests/regression_tests/deplete_no_transport/test.py b/tests/regression_tests/deplete_no_transport/test.py index f82a342cf..3a9fbe135 100644 --- a/tests/regression_tests/deplete_no_transport/test.py +++ b/tests/regression_tests/deplete_no_transport/test.py @@ -33,8 +33,12 @@ def vol_nuc(): return (fuel.volume, nuclides) -@pytest.mark.parametrize("multiproc", [True, False]) -def test_no_transport(run_in_tmpdir, vol_nuc, multiproc): +@pytest.mark.parametrize("multiproc, normalization_mode, power, flux", [ + (True, 'constant-flux', None, 1164719970082145.0), + (False, 'constant-flux', None, 1164719970082145.0), + (True, 'constant-power', 174, None), + (False, 'constant-power', 174, None)]) +def test_no_transport_constant_flux(run_in_tmpdir, vol_nuc, multiproc): """Transport free system test suite. Runs an OpenMC transport-free depletion calculation and verifies @@ -46,22 +50,26 @@ def test_no_transport(run_in_tmpdir, vol_nuc, multiproc): micro_xs_file = Path(__file__).parents[2] / 'micro_xs_simple.csv' micro_xs = FluxDepletionOperator.create_micro_xs_from_csv(micro_xs_file) chain_file = Path(__file__).parents[2] / 'chain_simple.xml' - flux = 1164719970082145.0 # flux from pincell example op = FluxDepletionOperator.from_nuclides( - vol_nuc[0], vol_nuc[1], micro_xs, flux, chain_file) + vol_nuc[0], vol_nuc[1], micro_xs, chain_file, normalization_mode=normalization_mode) # Power and timesteps dt = [30] # single step + flux = 1164719970082145.0 # n/cm^2-s, flux from pincell example power = 174 # W/cm # Perform simulation using the predictor algorithm openmc.deplete.pool.USE_MULTIPROCESSING = multiproc openmc.deplete.PredictorIntegrator( - op, dt, power, timestep_units='d').integrate() + op, dt, power=power, flux=flux, timestep_units='d').integrate() # Get path to test and reference results path_test = op.output_dir / 'depletion_results.h5' - path_reference = Path(__file__).with_name('test_reference.h5') + if flux is None: + ref_path = 'test_reference_constant_flux.h5' + else: + ref_path = 'test_reference_constant_power.h5' + path_reference = Path(__file__).with_name(ref_path) # If updating results, do so and return if config['update']: diff --git a/tests/regression_tests/deplete_no_transport/test_reference.h5 b/tests/regression_tests/deplete_no_transport/test_reference_constant_flux.h5 similarity index 99% rename from tests/regression_tests/deplete_no_transport/test_reference.h5 rename to tests/regression_tests/deplete_no_transport/test_reference_constant_flux.h5 index 55675f9d082cd29b63a028131888b31f384f5418..1f4319b3e576dbb6ad692f8452e74c69e21c574d 100644 GIT binary patch delta 223 zcmaDcjp@ZSrVR;Q`X3+9s9ca4YnM{9?Q7>HX1fW~mg-)f;ANK-H1lfI);zlu(UO9g z06{y+-?NmH-anY!-=$dJQX0r$)hpo400ad_yX*stUYVsnh;Y7f4!pVC d;)2U92~fABLfpb(Ck=Ou=wzR69szU*0RRuxV#)vj delta 223 zcmaDcjp@ZSrVR;Q`saS-Z*A?&vNPY8=~?th(C+u|R@NWi!|hz+RK8!_Qf-&`LHDVI zoxI(*8CuCw{qH9CcPZ90fWU!N2N2b>e24v$>>HQQGN(B23O3Pij>~gSd;Bc9`@v!7 z5PwCzv(wHvN61C=CnY&MS09jY2!9&tY&~CE=BD&T=f}Y@?_@ulxkOEF6Yaay;)3p$ eUr@I=L*0@LaSMmtcerCjC;N2s2!I@d>>vP=sbQr6 diff --git a/tests/regression_tests/deplete_no_transport/test_reference_constant_power.h5 b/tests/regression_tests/deplete_no_transport/test_reference_constant_power.h5 new file mode 100644 index 0000000000000000000000000000000000000000..2c04addfbf4a1be16e5144fed5caad5ea1a57f06 GIT binary patch literal 35688 zcmeHPU2IfE6rSB}VS%dUCs+}~`Uh1}mjbdyXt%VLMH8(8$-cQuyU->5%eKXu5X1zN zqAxY^fkz&URvxszL5xWiG<{M?Y*KviFU0f_--wC9nS0LH+nw&+ZnxdGfpe4YnV&N= zXTCe%nVEa%_I#}Al?PUDTCL0#45~6!=>+j}m#_AZCr73zoNY&E=sLJ>}gH&7tKaS``60^2zd(bBbAlk`v(E=FwR}ZY|+Bw2T1JT0LE(!8AZy&ufNe`=8^#}AB*O% z$Z-~0rD}hEJ>sfTlRO{6UYi%-&2RBUO&17_Bx>-I{LXs3!ZB9pv#@=?&$B-tCzK*dcIdtwQG&qb=UH< zt*4J2y7A4Q_3p~Vs+gl*>lfSq&bE)JUPl#%xZZ&mC=lbghXY0$nI|Z2pav*^58|OUCgV)XG zaN33W5aWAVR#8Ekcgv>c-S&s58niRS@e#J8l3V$M$2atX8z`fCBgrW~Je*x$gyZo? zC_O|%MK@*ZwR3!MGb=1}fqB=)%iaI4bLQP-Uw3DytH1MDDigvSY`W$(5A(FDH83Iw z2rs{Lk@1J062C)u$!~51y!=j##}P__=L_X`z!%$Z%$G0aFt|XzTxUNde3=4H0(lVd z;>!S!BNQzaU+npFzI@@w#Mw$^o};)}hGoG)M8zS?}r z{XR4tq7s+6psdGS1SK>R0WZExY{}Lw6<;o}f4|J_&6h8DedN}&!Py68Qpr|&&6(^= zP1^~1?StIj7sk8cC#i(+&P7f_-UxW{u7$@DN|uUum5);kVQ$amOL5N)`1OhA8qOaS z$2;_Q=K3)Ad>7hA{Se+^#E8s?fEVw^wq}>YI9%xG4}(upJHy zJkdS74K9vwGiU=T)q^?JJ?w$Uq)Y~_JuFF z(<1XB;Ki37e!q;e7>5frPodt%-W@x4>&U^j9lOn|Xe0M%d5DtL(VCsRjINeO&B%z6 z@f0Z-Y0AOAgvl(yh4Xw&Z`H&5NwjP4FT-rZWQYRLv)98;XS7O!=WFQM^LLc0Q4_ol zg`VwiVyqW@jsnmNbG*g6udKdk0`Es`d* zFT_^$@VOZnS>3h$M6$Cp`3xr}JomtuSn^k7g))(xFay(H>>G#_&<~bX$V$Z9(Nase<>)gx) z<1hPx+_*_@n<5#||=##T1&FegQx%RLo-ya|gf-2cXxqcq!_>k{~i>)_ze5xG-0{#_~i~z7P(o}Cuz%;3Yb6t_? zA~~*3{TeTmPIlmX#jgI=j&xfJQHA$?NT4H-YAQXH(G}>-RQg1!>r}F%pNsaT&DRD> z^>+7XT2pTuEvI$&J4w<2-qiaX26EEO`$zrv1*itxAJm7qeZP=C_xlraED89_B1y8z zG-dv*`;wsC693M`gAnxj2?6vI5;!0}_x@KoJqq{(1D(`2tM3CP?s5L=3&n%balfNO zd>(j=yyN0ws$-5WH$3S6+2227Tom7b|K$5_|EKwvF*_VqsP@-S{7V{O(4ka^T@?)r@jE=%Jh~ zu}%p>QKTRs2nYg#fFK|U2m*qDARq_`0)oJjAmFu+3>Rrlg7<|ekNsew)dl(gDMBCR zZD~K}mHj0U6DbG?0)l`bAP5Kof`A|(2nYg#fFK|UlpX=E=LwYsHa*1851@~IzgO|d zEYAyY&PJXOcp)h1f`A|(2nYg#fFK|U2m*q@(jidn{^4SQ`-YguenIvLOLxzuWkEm? z5CjAPK|l}?1d2hxYuz~8bKenOH$uV3C1j^F(Gm%2-5whrCS{9acx@m<}c zH8u4QHr%ZLzHD#(r-M&4p4|R*-Hp$>H+}xW)w*BV&TrYy5w^3L?fi}He3R`w$aWrP PJ2&UC6ZS!>X?Fe(f}(Zj literal 0 HcmV?d00001 diff --git a/tests/unit_tests/test_deplete_flux_operator.py b/tests/unit_tests/test_deplete_flux_operator.py index 3ae098490..b7a225398 100644 --- a/tests/unit_tests/test_deplete_flux_operator.py +++ b/tests/unit_tests/test_deplete_flux_operator.py @@ -11,7 +11,6 @@ from openmc.deplete.flux_operator import FluxDepletionOperator import pandas as pd import numpy as np -FLUX = 5e16 CHAIN_PATH = Path(__file__).parents[1] / "chain_simple.xml" ONE_GROUP_XS = Path(__file__).parents[1] / "micro_xs_simple.csv" @@ -62,6 +61,7 @@ def test_create_micro_xs_from_csv(): def test_operator_init(): """The test uses a temporary dummy chain. This file will be removed at the end of the test, and only contains a depletion_chain node.""" + volume = 1 nuclides = {'U234': 8.922411359424315e+18, 'U235': 9.98240191860822e+20, 'U238': 2.2192386373095893e+22, @@ -70,4 +70,5 @@ def test_operator_init(): 'O17': 1.7588724018066158e+19} micro_xs = FluxDepletionOperator.create_micro_xs_from_csv(ONE_GROUP_XS) nuclide_flux_operator = FluxDepletionOperator.from_nuclides( - 1, nuclides, micro_xs, FLUX_SPECTRA, CHAIN_PATH) + volume, nuclides, micro_xs, CHAIN_PATH) + nuclide_flux_operator = FluxDepletionOperator(materials, micro_xs, CHAIN_PATH) From f4aefa9db63f54ea97a7e9f4587260a2de80baf4 Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 21 Jul 2022 17:27:08 -0500 Subject: [PATCH 078/131] added function to help users create one-group cross sections; typo and bug fixes --- openmc/deplete/flux_operator.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/flux_operator.py index 8b4999f23..615fa090d 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/flux_operator.py @@ -242,9 +242,9 @@ class FluxDepletionOperator(OpenMCOperator): """ def __init__(self, op): - self._op = op - rates = self.reaction_rates + rates = op.reaction_rates self.nuc_ind_map = {ind: nuc for nuc, ind in rates.index_nuc.items()} + self._op = op super().__init__() def update(self, fission_rates, mat_index=None): @@ -262,13 +262,13 @@ class FluxDepletionOperator(OpenMCOperator): """ volume = self._op.number.get_mat_volume(mat_index) - densities = np.empty(shape(fission_rates)) - for i_nuc in nuc_ind_map: + densities = np.empty(np.shape(fission_rates)) + for i_nuc in self.nuc_ind_map: nuc = self.nuc_ind_map[i_nuc] densities[i_nuc] = self._op.number.get_atom_density(mat_index, nuc) fission_rates = fission_rates * volume * densities - super.update(fission_rates) + super().update(fission_rates) class _FluxDepletionRateHelper(ReactionRateHelper): """Class for generating one-group reaction rates with flux and @@ -351,7 +351,7 @@ class FluxDepletionOperator(OpenMCOperator): self._rate_helper = self._FluxDepletionRateHelper( self.reaction_rates.n_nuc, self.reaction_rates.n_react, self) if normalization_mode == "constant-power": - self._normalization_helper = self.FluxDepletionNormalizationHelper() + self._normalization_helper = self._FluxDepletionNormalizationHelper(self) else: self._normalization_helper = SourceRateHelper() From cc871495268c71127396d12f02175928f6a6e877 Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 21 Jul 2022 17:39:26 -0500 Subject: [PATCH 079/131] Add function to load previous results --- openmc/deplete/flux_operator.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/flux_operator.py index 615fa090d..55888b233 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/flux_operator.py @@ -48,9 +48,6 @@ class FluxDepletionOperator(OpenMCOperator): keff : 2-tuple of float, optional keff eigenvalue and uncertainty from transport calculation. Default is None. - fission_q : dict, optional - Dictionary of nuclides and their fission Q values [eV]. If not given, - values will be pulled from the ``chain_file``. prev_results : Results, optional Results from a previous depletion calculation. normalization_mode : {"constant-power", "constant-flux"} @@ -61,7 +58,6 @@ class FluxDepletionOperator(OpenMCOperator): Dictionary of nuclides and their fission Q values [eV]. If not given, values will be pulled from the ``chain_file``. Only applicable if ``"normalization_mode" == "constant-power"``. - reduce_chain : bool, optional If True, use :meth:`openmc.deplete.Chain.reduce` to reduce the depletion chain up to ``reduce_chain_level``. Default is False. @@ -223,6 +219,23 @@ class FluxDepletionOperator(OpenMCOperator): return openmc.Materials([mat]) + def _load_previous_results(self): + """Load results from a previous depletion simulation""" + # Reload volumes into geometry + self.prev_res[-1].transfer_volumes(self.materials) + + # Store previous results in operator + # Distribute reaction rates according to those tracked + # on this process + if comm.size != 1: + prev_results = self.prev_res + self.prev_res = Results() + mat_indexes = _distribute(range(len(self.burnable_mats))) + for res_obj in prev_results: + new_res = res_obj.distribute(self.local_mats, mat_indexes) + self.prev_res.append(new_res) + + def _get_nuclides_with_data(self, cross_sections): """Finds nuclides with cross section data""" return set(cross_sections.index) From 657a94b0c10bb137dfa6bd58274ac363c726d536 Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 21 Jul 2022 17:43:37 -0500 Subject: [PATCH 080/131] clean up docs for micro_xs helper functions --- openmc/deplete/flux_operator.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/flux_operator.py index 55888b233..974255753 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/flux_operator.py @@ -463,12 +463,13 @@ class FluxDepletionOperator(OpenMCOperator): Array containing one-group microscopic cross section information for each nuclide and reaction. units : {'barn', 'cm^2'}, optional - Units for microscopic cross section data. Defaults to ``barn``. + Units of cross section values in ``data`` array. Defaults to ``barn``. Returns ------- micro_xs : pandas.DataFrame - A DataFrame object correctly formatted for use in ``FluxOperator`` + A DataFrame object correctly formatted for use in ``FluxOperator``. + Cross section data is in [cm^2] """ # Validate inputs @@ -498,12 +499,13 @@ class FluxDepletionOperator(OpenMCOperator): Relative path to csv-file containing microscopic cross section data. units : {'barn', 'cm^2'}, optional - Units for microscopic cross section data. Defaults to ``barn``. + Units of cross section values in the ``.csv`` file array. Defaults to ``barn``. Returns ------- micro_xs : pandas.DataFrame - A DataFrame object correctly formatted for use in ``FluxOperator`` + A DataFrame object correctly formatted for use in ``FluxOperator``. + Cross section data is in [cm^2] """ micro_xs = pd.read_csv(csv_file, index_col=0) From 018ed9f58a0c98525f981a18bc04a873ea965988 Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 21 Jul 2022 17:47:50 -0500 Subject: [PATCH 081/131] add flux parameter to SII integrator --- openmc/deplete/abc.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index c9e0d566b..24859151f 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -526,9 +526,9 @@ class Integrator(ABC): initial heavy metal inventory to get total power if ``power`` is not speficied. flux : float or iterable of float, optional - Neutron flux in [neur/s-cm^2] for each interval in :attr: `timesteps` + neutron flux in [neut/s-cm^2] for each interval in :attr:`timesteps` source_rates : float or iterable of float, optional - Source rate in [neutron/sec] for each interval in :attr:`timesteps` + source rate in [neutron/sec] for each interval in :attr:`timesteps` .. versionadded:: 0.12.1 timestep_units : {'s', 'min', 'h', 'd', 'MWd/kg'} @@ -866,6 +866,8 @@ class SIIntegrator(Integrator): Power density of the reactor in [W/gHM]. It is multiplied by initial heavy metal inventory to get total power if ``power`` is not speficied. + flux : float or iterable of float, optional + neutron flux in [neut/s-cm^2] for each interval in :attr:`timesteps` source_rates : float or iterable of float, optional Source rate in [neutron/sec] for each interval in :attr:`timesteps` @@ -922,12 +924,12 @@ class SIIntegrator(Integrator): """ def __init__(self, operator, timesteps, power=None, power_density=None, - source_rates=None, timestep_units='s', n_steps=10, + flux=None, source_rates=None, timestep_units='s', n_steps=10, solver="cram48"): check_type("n_steps", n_steps, Integral) check_greater_than("n_steps", n_steps, 0) super().__init__( - operator, timesteps, power, power_density, source_rates, + operator, timesteps, power, power_density, flux, source_rates, timestep_units=timestep_units, solver=solver) self.n_steps = n_steps From 18cc291c9db8845781b66bfa0ee234ad0adf0497 Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 21 Jul 2022 17:52:18 -0500 Subject: [PATCH 082/131] formatting adjustment in usersguide --- docs/source/usersguide/depletion.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/usersguide/depletion.rst b/docs/source/usersguide/depletion.rst index 4be554b3c..e95e098c4 100644 --- a/docs/source/usersguide/depletion.rst +++ b/docs/source/usersguide/depletion.rst @@ -199,9 +199,9 @@ This class supports both constant-flux and constant-power depletion. The accuracy of results when using ``constant-power`` is entirely dependent on your depletion chain. Make sure it has sufficient data to resolve the dynamics of your particular scenario. -This class has two ways to initalize it; the default constructor accepts an +This class has two ways to initalize it: the default constructor accepts an :class:`openmc.Materials` object and one-group microscopic -cross sections as a :class:`pandas.DataFrame`, while the `from_nuclides` +cross sections as a :class:`pandas.DataFrame`, while the ``from_nuclides`` method accepts a volume and dictionary of nuclide concentrations in place of the :class:`openmc.Materials` object in addition to the other parameters. The class includes helper functions to construct the dataframe from a csv file From 5d004230f47018f4a4612aa6182833f5811b0075 Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 21 Jul 2022 19:15:52 -0500 Subject: [PATCH 083/131] added refernce libraries for each case --- .../test_reference_constant_flux.h5 | Bin 35688 -> 35688 bytes .../test_reference_constant_power.h5 | Bin 35688 -> 35688 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/tests/regression_tests/deplete_no_transport/test_reference_constant_flux.h5 b/tests/regression_tests/deplete_no_transport/test_reference_constant_flux.h5 index 1f4319b3e576dbb6ad692f8452e74c69e21c574d..ff75648b8a3f02225665861a54d03d3acc53ee32 100644 GIT binary patch delta 22 dcmaDcjp@ZSrVV?#m`k?qpS-_oDGQLc2LOjn3yuH) delta 22 dcmaDcjp@ZSrVV?#mYEVPTt?Olm$rJ0|0<`3XlK* delta 22 dcmaDcjp@ZSrVV?#n2*%$oV>qlDGQLc2LOo!3$y?L From 8ec8826ed8a2b150e89e10720bd33699f063f389 Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 21 Jul 2022 19:19:51 -0500 Subject: [PATCH 084/131] fix regression test --- .../deplete_no_transport/test.py | 49 ++++++++++--------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/tests/regression_tests/deplete_no_transport/test.py b/tests/regression_tests/deplete_no_transport/test.py index 3a9fbe135..9ae0f84f4 100644 --- a/tests/regression_tests/deplete_no_transport/test.py +++ b/tests/regression_tests/deplete_no_transport/test.py @@ -17,46 +17,52 @@ from tests.regression_tests import config @pytest.fixture(scope="module") -def vol_nuc(): +def fuel(): fuel = openmc.Material(name="uo2") fuel.add_element("U", 1, percent_type="ao", enrichment=4.25) fuel.add_element("O", 2) fuel.set_density("g/cc", 10.4) + fuel.depletable=True fuel.volume = np.pi * 0.42 ** 2 - nuclides = {} - for nuc, dens in fuel.get_nuclide_atom_densities().items(): - nuclides[nuc] = dens * 1e24 - - # Load geometry from example - return (fuel.volume, nuclides) + return fuel -@pytest.mark.parametrize("multiproc, normalization_mode, power, flux", [ - (True, 'constant-flux', None, 1164719970082145.0), - (False, 'constant-flux', None, 1164719970082145.0), - (True, 'constant-power', 174, None), - (False, 'constant-power', 174, None)]) -def test_no_transport_constant_flux(run_in_tmpdir, vol_nuc, multiproc): +@pytest.mark.parametrize("multiproc, from_nuclides, normalization_mode, power, flux", [ + (True, True,'constant-flux', None, 1164719970082145.0), + (False, True, 'constant-flux', None, 1164719970082145.0), + (True, True, 'constant-power', 174, None), + (False, True, 'constant-power', 174, None), + (True, False,'constant-flux', None, 1164719970082145.0), + (False, False, 'constant-flux', None, 1164719970082145.0), + (True, False, 'constant-power', 174, None), + (False, False, 'constant-power', 174, None)]) +def test_no_transport_from_nuclides(run_in_tmpdir, fuel, multiproc, from_nuclides, normalization_mode, power, flux): """Transport free system test suite. Runs an OpenMC transport-free depletion calculation and verifies that the outputs match a reference file. """ - # Create operator micro_xs_file = Path(__file__).parents[2] / 'micro_xs_simple.csv' micro_xs = FluxDepletionOperator.create_micro_xs_from_csv(micro_xs_file) chain_file = Path(__file__).parents[2] / 'chain_simple.xml' - op = FluxDepletionOperator.from_nuclides( - vol_nuc[0], vol_nuc[1], micro_xs, chain_file, normalization_mode=normalization_mode) + + if from_nuclides: + nuclides = {} + for nuc, dens in fuel.get_nuclide_atom_densities().items(): + nuclides[nuc] = dens * 1e24 + + op = FluxDepletionOperator.from_nuclides( + fuel.volume, nuclides, micro_xs, chain_file, normalization_mode=normalization_mode) + + else: + op = FluxDepletionOperator(openmc.Materials([fuel]), micro_xs, chain_file, normalization_mode=normalization_mode) # Power and timesteps dt = [30] # single step - flux = 1164719970082145.0 # n/cm^2-s, flux from pincell example - power = 174 # W/cm # Perform simulation using the predictor algorithm openmc.deplete.pool.USE_MULTIPROCESSING = multiproc @@ -65,17 +71,12 @@ def test_no_transport_constant_flux(run_in_tmpdir, vol_nuc, multiproc): # Get path to test and reference results path_test = op.output_dir / 'depletion_results.h5' - if flux is None: + if flux is not None: ref_path = 'test_reference_constant_flux.h5' else: ref_path = 'test_reference_constant_power.h5' path_reference = Path(__file__).with_name(ref_path) - # 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_test = openmc.deplete.Results(path_test) res_ref = openmc.deplete.Results(path_reference) From 8102226e7055d5391910e406fcda4460d9544ecb Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 21 Jul 2022 20:27:52 -0500 Subject: [PATCH 085/131] fix unit test --- tests/unit_tests/test_deplete_flux_operator.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/unit_tests/test_deplete_flux_operator.py b/tests/unit_tests/test_deplete_flux_operator.py index b7a225398..9019d123d 100644 --- a/tests/unit_tests/test_deplete_flux_operator.py +++ b/tests/unit_tests/test_deplete_flux_operator.py @@ -8,6 +8,7 @@ from pathlib import Path import pytest from openmc.deplete.flux_operator import FluxDepletionOperator +from openmc import Material, Materials import pandas as pd import numpy as np @@ -71,4 +72,12 @@ def test_operator_init(): micro_xs = FluxDepletionOperator.create_micro_xs_from_csv(ONE_GROUP_XS) nuclide_flux_operator = FluxDepletionOperator.from_nuclides( volume, nuclides, micro_xs, CHAIN_PATH) + + fuel = Material(name="uo2") + fuel.add_element("U", 1, percent_type="ao", enrichment=4.25) + fuel.add_element("O", 2) + fuel.set_density("g/cc", 10.4) + fuel.depletable=True + fuel.volume = 1 + materials = Materials([fuel]) nuclide_flux_operator = FluxDepletionOperator(materials, micro_xs, CHAIN_PATH) From 443c2ac4c99c06866798ca14b2944d8d50e0141a Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 24 May 2022 11:59:30 -0500 Subject: [PATCH 086/131] Add integral method on Discrete, Tabular, and Mixture classes --- openmc/stats/univariate.py | 39 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index 5528bdb30..733b7eaa6 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -220,6 +220,16 @@ class Discrete(Univariate): p_arr = np.array([p_merged[x] for x in x_arr]) return cls(x_arr, p_arr) + def integral(self): + """Return integral of distribution + + Returns + ------- + float + Integral of discrete distribution + """ + return np.sum(self.p) + class Uniform(Univariate): """Distribution with constant probability over a finite interval [a,b] @@ -1027,6 +1037,22 @@ class Tabular(Univariate): p = params[len(params)//2:] return cls(x, p, interpolation) + def integral(self): + """Return integral of distribution + + Returns + ------- + float + Integral of tabular distrbution + """ + if self.interpolation == 'histogram': + return np.sum(np.diff(self.x) * self.p[:-1]) + elif self.interpolation == 'linear-linear': + return np.trapz(self.p, self.x) + else: + raise NotImplementedError( + f'integral() not supported for {self.inteprolation} interpolation') + class Legendre(Univariate): r"""Probability density given by a Legendre polynomial expansion @@ -1199,3 +1225,16 @@ class Mixture(Univariate): distribution.append(Univariate.from_xml_element(pair.find("dist"))) return cls(probability, distribution) + + def integral(self): + """Return integral of the distribution + + Returns + ------- + float + Integral of the distribution + """ + return sum([ + p*dist.integral() + for p, dist in zip(self.probability, self.distribution) + ]) From c4a9b2c8b13592e2e761985eef3f2eef3dc092f0 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 5 Apr 2022 11:34:09 -0500 Subject: [PATCH 087/131] Get gamma and xray decay sources as distributions --- openmc/data/decay.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/openmc/data/decay.py b/openmc/data/decay.py index 51a594bc8..2855a9434 100644 --- a/openmc/data/decay.py +++ b/openmc/data/decay.py @@ -9,7 +9,9 @@ from uncertainties import ufloat, UFloat import openmc.checkvalue as cv from openmc.mixin import EqualityMixin +from openmc.stats import Discrete, Tabular from .data import ATOMIC_SYMBOL, ATOMIC_NUMBER +from .function import INTERPOLATION_SCHEME from .endf import Evaluation, get_head_record, get_list_record, get_tab1_record @@ -495,3 +497,43 @@ class Decay(EqualityMixin): """ return cls(ev_or_filename) + + def get_sources(self): + sources = {} + name = self.nuclide['name'] + for particle, spectra in self.spectra.items(): + # Only handle gammas for now + if particle not in ('gamma', 'xray'): + continue + + # Create distribution for discrete + distributions = [] + if spectra['continuous_flag'] in ('discrete', 'both'): + energies = [] + intensities = [] + for discrete_data in spectra['discrete']: + energies.append(discrete_data['energy'].n) + intensities.append(discrete_data['intensity'].n) + energies = np.array(energies) + intensities = np.array(intensities) + dist_discrete = Discrete(energies, intensities) # <-- not normalized yet + dist_discrete._normalization = spectra['discrete_normalization'].n + distributions.append(dist_discrete) + + # Create distribution for continuous + if spectra['continuous_flag'] in ('continuous', 'both'): + f = spectra['continuous']['probability'] + if len(f.interpolation) > 1: + raise NotImplementedError("Multiple interpolation regions: {name}, {particle}") + interpolation = INTERPOLATION_SCHEME[f.interpolation[0]] + if interpolation not in ('histogram', 'linear-linear'): + raise NotImplementedError("Continuous spectra with {interpolation} interpolation ({name}, {particle}) not supported") + + dist_continuous = Tabular(f.x, f.y, interpolation) + dist_continuous._intensity = spectra['continuous_normalization'].n + distributions.append(dist_continuous) + + # Combine distribution for discrete and continuous + sources[particle] = distributions + + return sources From b7596245f2af6c5484c9bb73e323e39ee6fb2de9 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 5 Apr 2022 12:50:22 -0500 Subject: [PATCH 088/131] Combined decay distributions --- openmc/data/decay.py | 46 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/openmc/data/decay.py b/openmc/data/decay.py index 2855a9434..5de07f465 100644 --- a/openmc/data/decay.py +++ b/openmc/data/decay.py @@ -9,7 +9,7 @@ from uncertainties import ufloat, UFloat import openmc.checkvalue as cv from openmc.mixin import EqualityMixin -from openmc.stats import Discrete, Tabular +from openmc.stats import Discrete, Tabular, Mixture from .data import ATOMIC_SYMBOL, ATOMIC_NUMBER from .function import INTERPOLATION_SCHEME from .endf import Evaluation, get_head_record, get_list_record, get_tab1_record @@ -499,15 +499,27 @@ class Decay(EqualityMixin): return cls(ev_or_filename) def get_sources(self): + """Get radioactive decay source distributions + + Returns + ------- + sources : dict + Dictionary mapping particle types (e.g., 'photon') to instances of + :class:`openmc.stats.Univariate` + + """ sources = {} name = self.nuclide['name'] for particle, spectra in self.spectra.items(): # Only handle gammas for now if particle not in ('gamma', 'xray'): continue + # TODO: Set particle type based on 'particle' above + particle_type = 'photon' + if particle_type not in sources: + sources[particle_type] = [] # Create distribution for discrete - distributions = [] if spectra['continuous_flag'] in ('discrete', 'both'): energies = [] intensities = [] @@ -516,9 +528,10 @@ class Decay(EqualityMixin): intensities.append(discrete_data['intensity'].n) energies = np.array(energies) intensities = np.array(intensities) + intensities *= spectra['discrete_normalization'].n dist_discrete = Discrete(energies, intensities) # <-- not normalized yet - dist_discrete._normalization = spectra['discrete_normalization'].n - distributions.append(dist_discrete) + dist_discrete._intensity = intensities.sum() + sources[particle_type].append(dist_discrete) # Create distribution for continuous if spectra['continuous_flag'] in ('continuous', 'both'): @@ -531,9 +544,28 @@ class Decay(EqualityMixin): dist_continuous = Tabular(f.x, f.y, interpolation) dist_continuous._intensity = spectra['continuous_normalization'].n - distributions.append(dist_continuous) + sources[particle_type].append(dist_continuous) - # Combine distribution for discrete and continuous - sources[particle] = distributions + # Combine discrete distributions + for dist_list in sources.values(): + # Get list of discrete distributions + discrete_index = [i for i, d in enumerate(dist_list) if isinstance(d, Discrete)] + dist_discrete = [dist_list[i] for i in discrete_index] + if len(dist_discrete) > 1: + # Create combined discrete distribution + probs = [1.0] * len(dist_discrete) + combined_dist = Discrete.merge(dist_discrete, probs) + combined_dist._intensity = np.sum(combined_dist.p) + + for idx in reversed(discrete_index): + dist_list.pop(idx) + dist_list.append(combined_dist) + + # Combine discrete and continuous if present + if len(dist_list) > 1: + probs = [d._intensity for d in dist_list] + dist_list[:] = Mixture(probs, dist_list.copy()) + + sources = {k: (v[0] if v else None) for k, v in sources.items()} return sources From 1b2b5e169c7dcf9d504ed505edc76f96ee25e87d Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 23 May 2022 14:44:08 -0500 Subject: [PATCH 089/131] Map radiation type to particle type properly in Decay.get_sources --- openmc/data/decay.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/openmc/data/decay.py b/openmc/data/decay.py index 5de07f465..142be35e4 100644 --- a/openmc/data/decay.py +++ b/openmc/data/decay.py @@ -511,11 +511,21 @@ class Decay(EqualityMixin): sources = {} name = self.nuclide['name'] for particle, spectra in self.spectra.items(): - # Only handle gammas for now - if particle not in ('gamma', 'xray'): - continue - # TODO: Set particle type based on 'particle' above - particle_type = 'photon' + # Set particle type based on 'particle' above + particle_type = { + 'gamma': 'photon', + 'beta-': 'electron', + 'ec/beta+': 'positron', + 'alpha': 'alpha', + 'n': 'neutron', + 'sf': 'fragment', + 'p': 'proton', + 'e-': 'electron', + 'xray': 'photon', + 'anti-neutrino': 'anti-neutrino', + 'neutrino': 'neutrino', + }[particle] + if particle_type not in sources: sources[particle_type] = [] From 6388c8f994b6587cd2d0de1e3aea2d6c3b514ec6 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 24 May 2022 12:02:31 -0500 Subject: [PATCH 090/131] Separate out a function for combine_distributions --- openmc/data/decay.py | 73 +++++++++++++++++++++++++++++++++----------- 1 file changed, 55 insertions(+), 18 deletions(-) diff --git a/openmc/data/decay.py b/openmc/data/decay.py index 142be35e4..edf9c1c0b 100644 --- a/openmc/data/decay.py +++ b/openmc/data/decay.py @@ -1,4 +1,5 @@ from collections.abc import Iterable +from copy import deepcopy from io import StringIO from math import log import re @@ -552,30 +553,66 @@ class Decay(EqualityMixin): if interpolation not in ('histogram', 'linear-linear'): raise NotImplementedError("Continuous spectra with {interpolation} interpolation ({name}, {particle}) not supported") + # TODO: work normalization into tabular itself dist_continuous = Tabular(f.x, f.y, interpolation) dist_continuous._intensity = spectra['continuous_normalization'].n sources[particle_type].append(dist_continuous) # Combine discrete distributions - for dist_list in sources.values(): - # Get list of discrete distributions - discrete_index = [i for i, d in enumerate(dist_list) if isinstance(d, Discrete)] - dist_discrete = [dist_list[i] for i in discrete_index] + merged_sources = {} + for particle_type, dist_list in sources.items(): + merged_sources[particle_type] = combine_distributions( + dist_list, [1.0]*len(dist_list)) - if len(dist_discrete) > 1: - # Create combined discrete distribution - probs = [1.0] * len(dist_discrete) - combined_dist = Discrete.merge(dist_discrete, probs) - combined_dist._intensity = np.sum(combined_dist.p) + return merged_sources - for idx in reversed(discrete_index): - dist_list.pop(idx) - dist_list.append(combined_dist) - # Combine discrete and continuous if present - if len(dist_list) > 1: - probs = [d._intensity for d in dist_list] - dist_list[:] = Mixture(probs, dist_list.copy()) +def combine_distributions(dists, probs): + """Combine distributions with specified probabilities - sources = {k: (v[0] if v else None) for k, v in sources.items()} - return sources + This function can be used to combine multiple instances of + :class:`~openmc.stats.Discrete` and `~openmc.stats.Tabular` into a single + distribution. Multiple discrete distributions are merged into a single + distribution and the remainder of the distributions are put into a + :class:`~openmc.stats.Mixture` distribution. + + Parameters + ---------- + dists : iterable of openmc.stats.Univariate + Distributions to combine + probs : iterable of float + Probability (or intensity) of each distribution + + """ + # Get copy of distribution list so as not to modify the argument + dist_list = deepcopy(dists) + + # Get list of discrete/continuous distribution indices + discrete_index = [i for i, d in enumerate(dist_list) if isinstance(d, Discrete)] + cont_index = [i for i, d in enumerate(dist_list) if isinstance(d, Tabular)] + + # Apply probabilites to continuous distributions + for i in cont_index: + dist = dist_list[i] + dist.p *= probs[i] + dist._intensity *= probs[i] + + if discrete_index: + # Create combined discrete distribution + dist_discrete = [dist_list[i] for i in discrete_index] + discrete_probs = [probs[i] for i in discrete_index] + combined_dist = Discrete.merge(dist_discrete, discrete_probs) + combined_dist._intensity = np.sum(combined_dist.p) + + # Replace multiple discrete distributions with merged + for idx in reversed(discrete_index): + dist_list.pop(idx) + dist_list.append(combined_dist) + + # Combine discrete and continuous if present + if len(dist_list) > 1: + probs = [d._intensity for d in dist_list] + dist_list[:] = [Mixture(probs, dist_list.copy())] + dist_list[0]._intensity = sum(probs) + + return dist_list[0] From d4989ae64226f0e11d75220277bfb13e3bdb538f Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 24 May 2022 12:48:06 -0500 Subject: [PATCH 091/131] Use integral() methods instead of _intensity hidden attribute --- openmc/data/decay.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/openmc/data/decay.py b/openmc/data/decay.py index edf9c1c0b..874981e2b 100644 --- a/openmc/data/decay.py +++ b/openmc/data/decay.py @@ -511,6 +511,7 @@ class Decay(EqualityMixin): """ sources = {} name = self.nuclide['name'] + decay_constant = self.decay_constant.n for particle, spectra in self.spectra.items(): # Set particle type based on 'particle' above particle_type = { @@ -538,10 +539,9 @@ class Decay(EqualityMixin): energies.append(discrete_data['energy'].n) intensities.append(discrete_data['intensity'].n) energies = np.array(energies) - intensities = np.array(intensities) - intensities *= spectra['discrete_normalization'].n - dist_discrete = Discrete(energies, intensities) # <-- not normalized yet - dist_discrete._intensity = intensities.sum() + intensity = spectra['discrete_normalization'].n + rates = decay_constant * intensity * np.array(intensities) + dist_discrete = Discrete(energies, rates) sources[particle_type].append(dist_discrete) # Create distribution for continuous @@ -553,9 +553,9 @@ class Decay(EqualityMixin): if interpolation not in ('histogram', 'linear-linear'): raise NotImplementedError("Continuous spectra with {interpolation} interpolation ({name}, {particle}) not supported") - # TODO: work normalization into tabular itself - dist_continuous = Tabular(f.x, f.y, interpolation) - dist_continuous._intensity = spectra['continuous_normalization'].n + intensity = spectra['continuous_normalization'].n + rates = decay_constant * intensity * f.y + dist_continuous = Tabular(f.x, rates, interpolation) sources[particle_type].append(dist_continuous) # Combine discrete distributions @@ -595,14 +595,12 @@ def combine_distributions(dists, probs): for i in cont_index: dist = dist_list[i] dist.p *= probs[i] - dist._intensity *= probs[i] if discrete_index: # Create combined discrete distribution dist_discrete = [dist_list[i] for i in discrete_index] discrete_probs = [probs[i] for i in discrete_index] combined_dist = Discrete.merge(dist_discrete, discrete_probs) - combined_dist._intensity = np.sum(combined_dist.p) # Replace multiple discrete distributions with merged for idx in reversed(discrete_index): @@ -611,8 +609,7 @@ def combine_distributions(dists, probs): # Combine discrete and continuous if present if len(dist_list) > 1: - probs = [d._intensity for d in dist_list] + probs = [d.integral() for d in dist_list] dist_list[:] = [Mixture(probs, dist_list.copy())] - dist_list[0]._intensity = sum(probs) return dist_list[0] From 7c779f73f8aa70c43283e4bbed2404ece2b2ab3a Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 18 Jul 2022 07:18:23 -0500 Subject: [PATCH 092/131] Move combine_distributions to univariate.py --- docs/source/pythonapi/data.rst | 1 + openmc/data/decay.py | 49 +--------------------------------- openmc/stats/univariate.py | 49 ++++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 48 deletions(-) diff --git a/docs/source/pythonapi/data.rst b/docs/source/pythonapi/data.rst index 287738774..6f56938b8 100644 --- a/docs/source/pythonapi/data.rst +++ b/docs/source/pythonapi/data.rst @@ -61,6 +61,7 @@ Core Functions atomic_mass atomic_weight + combine_distributions decay_constant dose_coefficients gnd_name diff --git a/openmc/data/decay.py b/openmc/data/decay.py index 874981e2b..f358dadf7 100644 --- a/openmc/data/decay.py +++ b/openmc/data/decay.py @@ -1,5 +1,4 @@ from collections.abc import Iterable -from copy import deepcopy from io import StringIO from math import log import re @@ -10,7 +9,7 @@ from uncertainties import ufloat, UFloat import openmc.checkvalue as cv from openmc.mixin import EqualityMixin -from openmc.stats import Discrete, Tabular, Mixture +from openmc.stats import Discrete, Tabular, combine_distributions from .data import ATOMIC_SYMBOL, ATOMIC_NUMBER from .function import INTERPOLATION_SCHEME from .endf import Evaluation, get_head_record, get_list_record, get_tab1_record @@ -567,49 +566,3 @@ class Decay(EqualityMixin): return merged_sources -def combine_distributions(dists, probs): - """Combine distributions with specified probabilities - - This function can be used to combine multiple instances of - :class:`~openmc.stats.Discrete` and `~openmc.stats.Tabular` into a single - distribution. Multiple discrete distributions are merged into a single - distribution and the remainder of the distributions are put into a - :class:`~openmc.stats.Mixture` distribution. - - Parameters - ---------- - dists : iterable of openmc.stats.Univariate - Distributions to combine - probs : iterable of float - Probability (or intensity) of each distribution - - """ - # Get copy of distribution list so as not to modify the argument - dist_list = deepcopy(dists) - - # Get list of discrete/continuous distribution indices - discrete_index = [i for i, d in enumerate(dist_list) if isinstance(d, Discrete)] - cont_index = [i for i, d in enumerate(dist_list) if isinstance(d, Tabular)] - - # Apply probabilites to continuous distributions - for i in cont_index: - dist = dist_list[i] - dist.p *= probs[i] - - if discrete_index: - # Create combined discrete distribution - dist_discrete = [dist_list[i] for i in discrete_index] - discrete_probs = [probs[i] for i in discrete_index] - combined_dist = Discrete.merge(dist_discrete, discrete_probs) - - # Replace multiple discrete distributions with merged - for idx in reversed(discrete_index): - dist_list.pop(idx) - dist_list.append(combined_dist) - - # Combine discrete and continuous if present - if len(dist_list) > 1: - probs = [d.integral() for d in dist_list] - dist_list[:] = [Mixture(probs, dist_list.copy())] - - return dist_list[0] diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index 733b7eaa6..f571377bf 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -1,6 +1,7 @@ from abc import ABC, abstractmethod from collections import defaultdict from collections.abc import Iterable +from copy import deepcopy from numbers import Real from xml.etree import ElementTree as ET @@ -1238,3 +1239,51 @@ class Mixture(Univariate): p*dist.integral() for p, dist in zip(self.probability, self.distribution) ]) + + +def combine_distributions(dists, probs): + """Combine distributions with specified probabilities + + This function can be used to combine multiple instances of + :class:`~openmc.stats.Discrete` and `~openmc.stats.Tabular` into a single + distribution. Multiple discrete distributions are merged into a single + distribution and the remainder of the distributions are put into a + :class:`~openmc.stats.Mixture` distribution. + + Parameters + ---------- + dists : iterable of openmc.stats.Univariate + Distributions to combine + probs : iterable of float + Probability (or intensity) of each distribution + + """ + # Get copy of distribution list so as not to modify the argument + dist_list = deepcopy(dists) + + # Get list of discrete/continuous distribution indices + discrete_index = [i for i, d in enumerate(dist_list) if isinstance(d, Discrete)] + cont_index = [i for i, d in enumerate(dist_list) if isinstance(d, Tabular)] + + # Apply probabilites to continuous distributions + for i in cont_index: + dist = dist_list[i] + dist.p *= probs[i] + + if discrete_index: + # Create combined discrete distribution + dist_discrete = [dist_list[i] for i in discrete_index] + discrete_probs = [probs[i] for i in discrete_index] + combined_dist = Discrete.merge(dist_discrete, discrete_probs) + + # Replace multiple discrete distributions with merged + for idx in reversed(discrete_index): + dist_list.pop(idx) + dist_list.append(combined_dist) + + # Combine discrete and continuous if present + if len(dist_list) > 1: + probs = [d.integral() for d in dist_list] + dist_list[:] = [Mixture(probs, dist_list.copy())] + + return dist_list[0] From 5a9662aa9fbe0743a10a0f778910475dc9507a3a Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 18 Jul 2022 10:54:58 -0500 Subject: [PATCH 093/131] Add test for combine_distributions, use np.array in Discrete/Tabular setters --- openmc/stats/univariate.py | 22 +++++++++---------- tests/unit_tests/test_stats.py | 39 ++++++++++++++++++++++++++++++---- 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index f571377bf..e11c9b50e 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -96,9 +96,9 @@ class Discrete(Univariate): Attributes ---------- - x : Iterable of float + x : numpy.ndarray Values of the random variable - p : Iterable of float + p : numpy.ndarray Discrete probability for each value """ @@ -123,7 +123,7 @@ class Discrete(Univariate): if isinstance(x, Real): x = [x] cv.check_type('discrete values', x, Iterable, Real) - self._x = x + self._x = np.array(x, dtype=float) @p.setter def p(self, p): @@ -132,7 +132,7 @@ class Discrete(Univariate): cv.check_type('discrete probabilities', p, Iterable, Real) for pk in p: cv.check_greater_than('discrete probability', pk, 0.0, True) - self._p = p + self._p = np.array(p, dtype=float) def cdf(self): return np.insert(np.cumsum(self.p), 0, 0.0) @@ -836,9 +836,9 @@ class Tabular(Univariate): Attributes ---------- - x : Iterable of float + x : numpy.ndarray Tabulated values of the random variable - p : Iterable of float + p : numpy.ndarray Tabulated probabilities interpolation : {'histogram', 'linear-linear', 'linear-log', 'log-linear', 'log-log'}, optional Indicate whether the density function is constant between tabulated @@ -871,7 +871,7 @@ class Tabular(Univariate): @x.setter def x(self, x): cv.check_type('tabulated values', x, Iterable, Real) - self._x = x + self._x = np.array(x, dtype=float) @p.setter def p(self, p): @@ -879,7 +879,7 @@ class Tabular(Univariate): if not self._ignore_negative: for pk in p: cv.check_greater_than('tabulated probability', pk, 0.0, True) - self._p = p + self._p = np.array(p, dtype=float) @interpolation.setter def interpolation(self, interpolation): @@ -892,8 +892,8 @@ class Tabular(Univariate): 'distributions using histogram or ' 'linear-linear interpolation') c = np.zeros_like(self.x) - x = np.asarray(self.x) - p = np.asarray(self.p) + x = self.x + p = self.p if self.interpolation == 'histogram': c[1:] = p[:-1] * np.diff(x) @@ -933,7 +933,7 @@ class Tabular(Univariate): def normalize(self): """Normalize the probabilities stored on the distribution""" - self.p = np.asarray(self.p) / self.cdf().max() + self.p /= self.cdf().max() def sample(self, n_samples=1, seed=None): if not self.interpolation in ('histogram', 'linear-linear'): diff --git a/tests/unit_tests/test_stats.py b/tests/unit_tests/test_stats.py index b57f9578a..404cecd13 100644 --- a/tests/unit_tests/test_stats.py +++ b/tests/unit_tests/test_stats.py @@ -13,8 +13,8 @@ def test_discrete(): elem = d.to_xml_element('distribution') d = openmc.stats.Discrete.from_xml_element(elem) - assert d.x == x - assert d.p == p + np.testing.assert_array_equal(d.x, x) + np.testing.assert_array_equal(d.p, p) assert len(d) == len(x) d = openmc.stats.Univariate.from_xml_element(elem) @@ -76,8 +76,8 @@ def test_uniform(): assert len(d) == 2 t = d.to_tabular() - assert t.x == [a, b] - assert t.p == [1/(b-a), 1/(b-a)] + np.testing.assert_array_equal(t.x, [a, b]) + np.testing.assert_array_equal(t.p, [1/(b-a), 1/(b-a)]) assert t.interpolation == 'histogram' exp_mean = 0.5 * (a + b) @@ -396,3 +396,34 @@ def test_muir(): assert within_2_sigma / n_samples >= 0.95 within_3_sigma = np.count_nonzero(samples < 3*d.std_dev) assert within_3_sigma / n_samples >= 0.99 + + +def test_combine_distributions(): + # Combine two discrete (same data as in test_merge_discrete) + x1 = [0.0, 1.0, 10.0] + p1 = [0.3, 0.2, 0.5] + d1 = openmc.stats.Discrete(x1, p1) + x2 = [0.5, 1.0, 5.0] + p2 = [0.4, 0.5, 0.1] + d2 = openmc.stats.Discrete(x2, p2) + + # Merged distribution should have x values sorted and probabilities + # appropriately combined. Duplicate x values should appear once. + merged = openmc.stats.combine_distributions([d1, d2], [0.6, 0.4]) + assert isinstance(merged, openmc.stats.Discrete) + assert merged.x == pytest.approx([0.0, 0.5, 1.0, 5.0, 10.0]) + assert merged.p == pytest.approx( + [0.6*0.3, 0.4*0.4, 0.6*0.2 + 0.4*0.5, 0.4*0.1, 0.6*0.5]) + + # Probabilities add up but are not normalized + d1 = openmc.stats.Discrete([3.0], [1.0]) + triple = openmc.stats.combine_distributions([d1, d1, d1], [1.0, 2.0, 3.0]) + assert triple.x == pytest.approx([3.0]) + assert triple.p == pytest.approx([6.0]) + + # Combine discrete and tabular + t1 = openmc.stats.Tabular(x2, p2) + mixed = openmc.stats.combine_distributions([d1, t1], [0.5, 0.5]) + assert isinstance(mixed, openmc.stats.Mixture) + assert len(mixed.distribution) == 2 + assert len(mixed.probability) == 2 From a68a3ede6a6fc85d0b8ac6eacd7b139fbbe6278e Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 18 Jul 2022 13:30:31 -0500 Subject: [PATCH 094/131] Change get_sources() -> sources property and add test --- openmc/data/decay.py | 27 ++++++++++++---------- openmc/stats/univariate.py | 8 +++++++ tests/unit_tests/test_data_decay.py | 35 +++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 12 deletions(-) diff --git a/openmc/data/decay.py b/openmc/data/decay.py index f358dadf7..96c3f562a 100644 --- a/openmc/data/decay.py +++ b/openmc/data/decay.py @@ -316,6 +316,12 @@ class Decay(EqualityMixin): 'excited_state', 'mass', 'stable', 'spin', and 'parity'. spectra : dict Resulting radiation spectra for each radiation type. + sources : dict + Radioactive decay source distributions represented as a dictionary + mapping particle types (e.g., 'photon') to instances of + :class:`openmc.stats.Univariate`. + + .. versionadded:: 0.13.1 """ def __init__(self, ev_or_filename): @@ -498,16 +504,14 @@ class Decay(EqualityMixin): """ return cls(ev_or_filename) - def get_sources(self): - """Get radioactive decay source distributions + @property + def sources(self): + """Radioactive decay source distributions""" + # If property has been computed already, return it + # TODO: Replace with functools.cached_property when support is Python 3.9+ + if self._sources is not None: + return self._sources - Returns - ------- - sources : dict - Dictionary mapping particle types (e.g., 'photon') to instances of - :class:`openmc.stats.Univariate` - - """ sources = {} name = self.nuclide['name'] decay_constant = self.decay_constant.n @@ -563,6 +567,5 @@ class Decay(EqualityMixin): merged_sources[particle_type] = combine_distributions( dist_list, [1.0]*len(dist_list)) - return merged_sources - - + self._sources = merged_sources + return self._sources diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index e11c9b50e..66c6e5f06 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -224,6 +224,8 @@ class Discrete(Univariate): def integral(self): """Return integral of distribution + .. versionadded:: 0.13.1 + Returns ------- float @@ -1041,6 +1043,8 @@ class Tabular(Univariate): def integral(self): """Return integral of distribution + .. versionadded: 0.13.1 + Returns ------- float @@ -1230,6 +1234,8 @@ class Mixture(Univariate): def integral(self): """Return integral of the distribution + .. versionadded:: 0.13.1 + Returns ------- float @@ -1250,6 +1256,8 @@ def combine_distributions(dists, probs): distribution and the remainder of the distributions are put into a :class:`~openmc.stats.Mixture` distribution. + .. versionadded:: 0.13.1 + Parameters ---------- dists : iterable of openmc.stats.Univariate diff --git a/tests/unit_tests/test_data_decay.py b/tests/unit_tests/test_data_decay.py index 06b8e6bed..f0bda1bd9 100644 --- a/tests/unit_tests/test_data_decay.py +++ b/tests/unit_tests/test_data_decay.py @@ -23,6 +23,14 @@ def nb90(): return openmc.data.Decay.from_endf(filename) +@pytest.fixture(scope='module') +def ba137m(): + """Ba137_m1 decay data.""" + endf_data = os.environ['OPENMC_ENDF_DATA'] + filename = os.path.join(endf_data, 'decay', 'dec-056_Ba_137m1.endf') + return openmc.data.Decay.from_endf(filename) + + @pytest.fixture(scope='module') def u235_yields(): """U235 fission product yield data.""" @@ -48,6 +56,7 @@ def test_nb90_halflife(nb90): ufloat_close(nb90.decay_constant, log(2.)/nb90.half_life) ufloat_close(nb90.decay_energy, ufloat(2265527.5, 25159.400474401213)) + def test_nb90_nuclide(nb90): assert nb90.nuclide['atomic_number'] == 41 assert nb90.nuclide['mass_number'] == 90 @@ -91,3 +100,29 @@ def test_fpy(u235_yields): assert len(u235_yields.independent) == 3 thermal = u235_yields.independent[0] ufloat_close(thermal['I135'], ufloat(0.0292737, 0.000819663)) + + +def test_sources(ba137m, nb90): + # Running .sources twice should give same objects + sources = ba137m.sources + sources2 = ba137m.sources + for key in sources: + assert sources[key] is sources2[key] + + # Each source should be a univariate distribution + for dist in sources.values(): + assert isinstance(dist, openmc.stats.Univariate) + + # Check for presence of 662 keV gamma ray in decay of Ba137m + gamma_source = ba137m.sources['photon'] + assert isinstance(gamma_source, openmc.stats.Discrete) + b = np.isclose(gamma_source.x, 661657.) + assert np.count_nonzero(b) == 1 + + # Check value of decay/s/atom + idx = np.flatnonzero(b)[0] + assert gamma_source.p[idx] == pytest.approx(0.004069614) + + # Nb90 decays by β+ and should emit positrons, electrons, and photons + sources = nb90.sources + assert len(set(sources.keys()) ^ {'positron', 'electron', 'photon'}) == 0 From 3e5afd21eb67512c05111ad49e848b52f85dc981 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 22 Jul 2022 16:42:24 -0500 Subject: [PATCH 095/131] Add missing definition of _sources in Decay.__init__ --- openmc/data/decay.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openmc/data/decay.py b/openmc/data/decay.py index 96c3f562a..2c2505bf5 100644 --- a/openmc/data/decay.py +++ b/openmc/data/decay.py @@ -337,6 +337,7 @@ class Decay(EqualityMixin): self.modes = [] self.spectra = {} self.average_energies = {} + self._sources = None # Get head record items = get_head_record(file_obj) From 4818a296d4893a809baf5f3b171413dcdd3ff7ee Mon Sep 17 00:00:00 2001 From: yardasol Date: Wed, 27 Jul 2022 12:27:16 -0500 Subject: [PATCH 096/131] Made changes from paulromano's 2nd round of comments - Syntax improvements and fixes - removed `flux` parameter from Integrator - Moved generate_1g_cross_sections to a top level function in flux_operator.py - changed normalization modes: constant-flux -> source-rate; constant-power -> fission-q - fixed regression tests (fission-q reference solution was bad before, but is now much more reasonable and comparable to the source-rate reference solution) - added `nuc_units` parameter to the `from_nuclides` method. - docstring fixes - RST doc fixes - spelling fixes --- docs/source/usersguide/depletion.rst | 16 +- openmc/deplete/abc.py | 26 +- openmc/deplete/flux_operator.py | 247 +++++++++--------- openmc/deplete/helpers.py | 8 +- openmc/deplete/integrators.py | 2 +- openmc/deplete/openmc_operator.py | 5 +- openmc/deplete/operator.py | 16 +- .../deplete_no_transport/test.py | 31 ++- ...t_power.h5 => test_reference_fission_q.h5} | Bin 35688 -> 35688 bytes ..._flux.h5 => test_reference_source_rate.h5} | Bin .../unit_tests/test_deplete_flux_operator.py | 2 +- 11 files changed, 178 insertions(+), 175 deletions(-) rename tests/regression_tests/deplete_no_transport/{test_reference_constant_power.h5 => test_reference_fission_q.h5} (99%) rename tests/regression_tests/deplete_no_transport/{test_reference_constant_flux.h5 => test_reference_source_rate.h5} (100%) diff --git a/docs/source/usersguide/depletion.rst b/docs/source/usersguide/depletion.rst index e95e098c4..cfb373e13 100644 --- a/docs/source/usersguide/depletion.rst +++ b/docs/source/usersguide/depletion.rst @@ -19,10 +19,10 @@ transmutation equations and the method used for advancing time. At present, the :class:`openmc.deplete.Operator` (which uses the OpenMC transport solver), but in principle additional operator classes based on other transport codes could be implemented and no changes to the depletion solver itself would be needed. The -operator class requires a :class:`openmc.model.Model` instance containing +operator class requires a :class:`~openmc.Model` instance containing material, geometry, and settings information:: - model = openmc.model.Model() + model = openmc.Model() ... op = openmc.deplete.Operator(model) @@ -189,17 +189,18 @@ Transport-independent depletion OpenMC supports running depletion calculations independent of the OpenMC transport solver using the :class:`~openmc.deplete.FluxDepletionOperator` class. -This class supports both constant-flux and constant-power depletion. +This class supports both constant-flux (``source-rate`` normalization) and +constant-power depletion (``fission-q`` normalization). .. important:: - Make sure you set the correct parameter in the :class:`openmc.abc.Integrator` class. Use the ``flux`` parameter when ``normalization_mode == constant-flux``, and use ``power`` or ``power_density`` when ``normalization_mode == constant-power``. + Make sure you set the correct parameter in the :class:`openmc.abc.Integrator` class. Use the ``source_rates`` parameter when ``normalization_mode == source-rate``, and use ``power`` or ``power_density`` when ``normalization_mode == fission-q``. .. warning:: - The accuracy of results when using ``constant-power`` is entirely dependent on your depletion chain. Make sure it has sufficient data to resolve the dynamics of your particular scenario. + The accuracy of results when using ``fission-q`` is entirely dependent on your depletion chain. Make sure it has sufficient data to resolve the dynamics of your particular scenario. -This class has two ways to initalize it: the default constructor accepts an +This class has two ways to initialize it: the default constructor accepts an :class:`openmc.Materials` object and one-group microscopic cross sections as a :class:`pandas.DataFrame`, while the ``from_nuclides`` method accepts a volume and dictionary of nuclide concentrations in place of @@ -221,7 +222,8 @@ or from data arrays:: 'O16': 4.64e22, 'O17': 1.76e19} volume = 0.5 - op = FluxDepletionOperator.from_nuclides(volume, nuclides, micro_xs, flux, chain_file) + op = FluxDepletionOperator.from_nuclides(volume, nuclides, 'atom/cm3', + micro_xs, flux, chain_file) A user can then define an integrator class as they would for a coupled transport-depletion calculation and follow the same steps from there. diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 24859151f..90a353d93 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -524,11 +524,10 @@ class Integrator(ABC): power_density : float or iterable of float, optional Power density of the reactor in [W/gHM]. It is multiplied by initial heavy metal inventory to get total power if ``power`` - is not speficied. - flux : float or iterable of float, optional - neutron flux in [neut/s-cm^2] for each interval in :attr:`timesteps` + is not specified. source_rates : float or iterable of float, optional - source rate in [neutron/sec] for each interval in :attr:`timesteps` + Source rate in [neutron/sec] or neutron flux in [neut/s-cm^2] for each + interval in :attr:`timesteps` .. versionadded:: 0.12.1 timestep_units : {'s', 'min', 'h', 'd', 'MWd/kg'} @@ -579,7 +578,7 @@ class Integrator(ABC): """ def __init__(self, operator, timesteps, power=None, power_density=None, - flux=None, source_rates=None, timestep_units='s', solver="cram48"): + source_rates=None, timestep_units='s', solver="cram48"): # Check number of stages previously used if operator.prev_res is not None: res = operator.prev_res[-1] @@ -601,10 +600,8 @@ class Integrator(ABC): source_rates = power_density * operator.heavy_metal else: source_rates = [p*operator.heavy_metal for p in power_density] - elif flux is not None: - source_rates = flux elif source_rates is None: - raise ValueError("Either power, power_density, flux, or source_rates must be set") + raise ValueError("Either power, power_density, source_rates must be set") if not isinstance(source_rates, Iterable): # Ensure that rate is single value if that is the case @@ -865,11 +862,10 @@ class SIIntegrator(Integrator): power_density : float or iterable of float, optional Power density of the reactor in [W/gHM]. It is multiplied by initial heavy metal inventory to get total power if ``power`` - is not speficied. - flux : float or iterable of float, optional - neutron flux in [neut/s-cm^2] for each interval in :attr:`timesteps` + is not specified. source_rates : float or iterable of float, optional - Source rate in [neutron/sec] for each interval in :attr:`timesteps` + Source rate in [neutron/sec] or neutron flux in [neut/s-cm^2] for each + interval in :attr:`timesteps` .. versionadded:: 0.12.1 timestep_units : {'s', 'min', 'h', 'd', 'MWd/kg'} @@ -924,12 +920,12 @@ class SIIntegrator(Integrator): """ def __init__(self, operator, timesteps, power=None, power_density=None, - flux=None, source_rates=None, timestep_units='s', n_steps=10, + source_rates=None, timestep_units='s', n_steps=10, solver="cram48"): check_type("n_steps", n_steps, Integral) check_greater_than("n_steps", n_steps, 0) super().__init__( - operator, timesteps, power, power_density, flux, source_rates, + operator, timesteps, power, power_density, source_rates, timestep_units=timestep_units, solver=solver) self.n_steps = n_steps @@ -1014,7 +1010,7 @@ class DepSystemSolver(ABC): Parameters ---------- A : scipy.sparse.csr_matrix - Sparse transmutation matrix ``A[j, i]`` desribing rates at + Sparse transmutation matrix ``A[j, i]`` describing rates at which isotope ``i`` transmutes to isotope ``j`` n0 : numpy.ndarray Initial compositions, typically given in number of atoms in some diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/flux_operator.py index 974255753..410552671 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/flux_operator.py @@ -1,7 +1,7 @@ """Pure depletion operator -This module implements a pure depletion operator that uses user- provided fluxes -and one-group cross sections. +This module implements a pure depletion operator that user-provided one-group +cross sections. """ @@ -20,12 +20,84 @@ from openmc.mpi import comm from openmc.mgxs import EnergyGroups, ArbitraryXS, FissionXS from .abc import ReactionRateHelper, OperatorResult from .chain import REACTIONS -from .openmc_operator import OpenMCOperator +from .openmc_operator import OpenMCOperator, _distribute +from .results import Results from .helpers import ChainFissionHelper, ConstantFissionYieldHelper, SourceRateHelper _valid_rxns = list(REACTIONS) _valid_rxns.append('fission') +def generate_1g_cross_sections(model, + reaction_domain, + reactions=['(n,gamma)', + '(n,2n)', + '(n,p)', + '(n,a)', + '(n,3n)', + '(n,4n)', + 'fission'], + energy_bounds=(0, 20e6), + write_to_csv=False, + filename='micro_xs.csv'): + """Helper function to generate a one-group cross-section dataframe using + OpenMC. Note that the ``openmc`` executable must be compiled. + + Parameters + ---------- + model : openmc.model.Model + OpenMC model object. Must contain geometry, materials, and settings. + reaction_domain : openmc.Material or openmc.Cell or openmc.Universe or openmc.RegularMesh + Domain in which to tally reaction rates. + reactions : list of str, optional + Reaction names to tally + energy_bound : 2-tuple of float, optional + Bounds for the energy group. + write_to_csv : bool, optional + Option to write the DataFrame to a `.csv` file. + filename : str + Name for csv file. Only applicable if ``write_to_csv == True`` + + Returns + ------- + None or pandas.DataFrame + + """ + groups = EnergyGroups(energy_bounds) + + # Set up the reaction tallies + original_tallies = model.tallies + tallies = openmc.Tallies() + xs = {} + for rxn in reactions: + if rxn == 'fission': + xs[rxn] = FissionXS(domain=reaction_domain, groups=groups, by_nuclide=True) + else: + xs[rxn] = ArbitraryXS(rxn, domain=reaction_domain, groups=groups, by_nuclide=True) + tallies += xs[rxn].tallies.values() + + model.tallies = tallies + statepoint_path = model.run() + + # Revert to the original tallies + model.tallies = old_tallies + + with openmc.StatePoint(statepoint_path) as sp: + for rxn in xs: + xs[rxn].load_from_statepoint(sp) + + # Build the DataFrame + micro_xs = pd.DataFrame() + for rxn in xs: + df = xs[rxn].get_pandas_dataframe(xs_type='micro') + df.index = df['nuclide'] + df.drop(['nuclide', xs[rxn].domain_type, 'group in', 'std. dev.'], axis=1, inplace=True) + df.rename({'mean':rxn}, axis=1, inplace=True) + micro_xs = pd.concat([micro_xs, df], axis=1) + + if write_to_csv: + micro_xs.to_csv(filename) + + return micro_xs class FluxDepletionOperator(OpenMCOperator): """Depletion operator that uses one-group @@ -50,14 +122,16 @@ class FluxDepletionOperator(OpenMCOperator): Default is None. prev_results : Results, optional Results from a previous depletion calculation. - normalization_mode : {"constant-power", "constant-flux"} + normalization_mode : {"fission-q", "source-rate"} Indicate how reaction rates should be calculated. - ``"constant-power"`` uses the fission Q values from the depletion chain to - compute the flux based on the power. ``"constant-flux"`` uses the value stored in `_normalization_helper` as the flux. + ``"fission-q"`` uses the fission Q values from the depletion chain to + compute the flux based on the power. ``"source-rate"`` uses a the + source rate (assumed to be neutron flux) to calculate the + reaction rates. fission_q : dict, optional Dictionary of nuclides and their fission Q values [eV]. If not given, values will be pulled from the ``chain_file``. Only applicable - if ``"normalization_mode" == "constant-power"``. + if ``"normalization_mode" == "fission-q"``. reduce_chain : bool, optional If True, use :meth:`openmc.deplete.Chain.reduce` to reduce the depletion chain up to ``reduce_chain_level``. Default is False. @@ -104,14 +178,14 @@ class FluxDepletionOperator(OpenMCOperator): Results from a previous depletion calculation. ``None`` if no results are to be used. - """ + """ def __init__(self, materials, micro_xs, chain_file, keff=None, - normalization_mode = 'constant-flux', + normalization_mode='source-rate', fission_q=None, prev_results=None, reduce_chain=False, @@ -126,7 +200,8 @@ class FluxDepletionOperator(OpenMCOperator): self._keff = keff - helper_kwargs = dict() + if fission_yield_opts is None: + fission_yield_opts = {} helper_kwargs = {'normalization_mode': normalization_mode, 'fission_yield_opts': fission_yield_opts} @@ -141,10 +216,11 @@ class FluxDepletionOperator(OpenMCOperator): reduce_chain_level=reduce_chain_level) @classmethod - def from_nuclides(cls, volume, nuclides, micro_xs, + def from_nuclides(cls, volume, nuclides, nuc_units, + micro_xs, chain_file, keff=None, - normalization_mode='constant-flux', + normalization_mode='source-rate', fission_q=None, prev_results=None, reduce_chain=False, @@ -156,8 +232,10 @@ class FluxDepletionOperator(OpenMCOperator): volume : float Volume of the material being depleted in [cm^3] nuclides : dict of str to float - ,Dictionary with nuclide names as keys and nuclide concentrations as - values. Nuclide concentration units are [atom/cm^3]. + Dictionary with nuclide names as keys and nuclide concentrations as + values. + nuc_units : {'atom/cm3', 'atom/b-cm'} + Units for nuclide concentration. micro_xs : pandas.DataFrame DataFrame with nuclides names as index and microscopic cross section data in the columns. Cross section units are [cm^-2]. @@ -166,15 +244,16 @@ class FluxDepletionOperator(OpenMCOperator): keff : 2-tuple of float, optional keff eigenvalue and uncertainty from transport calculation. Default is None. - normalization_mode : {"constant-power", "constant-flux"} + normalization_mode : {"fission-q", "source-rate"} Indicate how reaction rates should be calculated. - ``"constant-power"`` uses the fission Q values from the depletion - chain to compute the flux based on the power. ``"constant-flux"`` - uses the value stored in `_normalization_helper` as the flux. + ``"fission-q"`` uses the fission Q values from the depletion + chain to compute the flux based on the power. ``"source-rate"`` uses + the source rate (assumed to be neutron flux) to calculate the + reaction rates. fission_q : dict, optional Dictionary of nuclides and their fission Q values [eV]. If not given, values will be pulled from the ``chain_file``. Only - applicable if ``"normalization_mode" == "constant-power"``. + applicable if ``"normalization_mode" == "fission-q"``. prev_results : Results, optional Results from a previous depletion calculation. reduce_chain : bool, optional @@ -191,7 +270,7 @@ class FluxDepletionOperator(OpenMCOperator): """ check_type('nuclides', nuclides, dict, str) - materials = cls._consolidate_nuclides_to_material(nuclides, volume) + materials = cls._consolidate_nuclides_to_material(nuclides, nuc_units, volume) return cls(materials, micro_xs, chain_file, @@ -205,14 +284,20 @@ class FluxDepletionOperator(OpenMCOperator): @staticmethod - def _consolidate_nuclides_to_material(nuclides, volume): + def _consolidate_nuclides_to_material(nuclides, nuc_units, volume): """Puts nuclide list into an openmc.Materials object. """ openmc.reset_auto_ids() mat = openmc.Material() - for nuc, conc in nuclides.items(): - mat.add_nuclide(nuc, conc * 1e-24) # convert to at/b-cm + if nuc_units == 'atom/b-cm': + for nuc, conc in nuclides.items(): + mat.add_nuclide(nuc, conc) + elif nuc_units == 'atom/cm3': + for nuc, conc in nuclides.items(): + mat.add_nuclide(nuc, conc * 1e-24) # convert to at/b-cm + else: + raise ValueError(f"Unit '{nuc_units}' is invalid.") mat.volume = volume mat.depletable = True @@ -273,13 +358,12 @@ class FluxDepletionOperator(OpenMCOperator): mat_index : int Material index - """ + """ volume = self._op.number.get_mat_volume(mat_index) - densities = np.empty(np.shape(fission_rates)) - for i_nuc in self.nuc_ind_map: - nuc = self.nuc_ind_map[i_nuc] + densities = np.empty_like(fission_rates) + for i_nuc, nuc in self.nuc_ind_map.items(): densities[i_nuc] = self._op.number.get_atom_density(mat_index, nuc) - fission_rates = fission_rates * volume * densities + fission_rates *= volume * densities super().update(fission_rates) @@ -294,10 +378,6 @@ class FluxDepletionOperator(OpenMCOperator): Parameters ---------- - n_nucs : int - Number of burnable nuclides tracked by :class:`openmc.deplete.Operator` - n_react : int - Number of reactions tracked by :class:`openmc.deplete.Operator` op : openmc.deplete.FluxDepletionOperator Reference to the object encapsulate _FluxDepletionRateHelper. We pass this so we don't have to duplicate the ``number`` object. @@ -312,9 +392,9 @@ class FluxDepletionOperator(OpenMCOperator): """ - def __init__(self, n_nuc, n_react, op): - super().__init__(n_nuc, n_react) + def __init__(self, op): rates = op.reaction_rates + super().__init__(rates.n_nuc, rates.n_react) self.nuc_ind_map = {ind: nuc for nuc, ind in rates.index_nuc.items()} self.rxn_ind_map = {ind: rxn for rxn, ind in rates.index_rx.items()} @@ -349,7 +429,7 @@ class FluxDepletionOperator(OpenMCOperator): return self._results_cache def _get_helper_classes(self, helper_kwargs): - """Get helper classes for calculating reation rates and fission yields + """Get helper classes for calculating reaction rates and fission yields Parameters ---------- @@ -359,19 +439,16 @@ class FluxDepletionOperator(OpenMCOperator): """ normalization_mode = helper_kwargs['normalization_mode'] - fission_yield_opts = helper_kwargs.get('fission_yield_opts', {}) + fission_yield_opts = helper_kwargs['fission_yield_opts'] - self._rate_helper = self._FluxDepletionRateHelper( - self.reaction_rates.n_nuc, self.reaction_rates.n_react, self) - if normalization_mode == "constant-power": + self._rate_helper = self._FluxDepletionRateHelper(self) + if normalization_mode == "fission-q": self._normalization_helper = self._FluxDepletionNormalizationHelper(self) else: self._normalization_helper = SourceRateHelper() # Select and create fission yield helper fission_helper = ConstantFissionYieldHelper - if fission_yield_opts is None: - fission_yield_opts = {} self._yield_helper = fission_helper.from_operator( self, **fission_yield_opts) @@ -395,7 +472,7 @@ class FluxDepletionOperator(OpenMCOperator): vec : list of numpy.ndarray Total atoms to be used in function. source_rate : float - Power in [W] or source rate in [neutron/sec] + Power in [W] or flux in [neut/s-cm^2] Returns ------- @@ -444,11 +521,11 @@ class FluxDepletionOperator(OpenMCOperator): print(f'WARNING: nuclide {nuc} in material' f'{mat} is negative (density = {val}' - ' at/barn-cm)') + ' atom/b-cm)') number_i[mat, nuc] = 0.0 @staticmethod - def create_micro_xs_from_data_array(nuclides, reactions, data, units='barn'): + def create_micro_xs_from_data_array(nuclides, reactions, data): """ Creates a ``micro_xs`` parameter from a dictionary. @@ -458,12 +535,10 @@ class FluxDepletionOperator(OpenMCOperator): List of nuclide symbols for that have data for at least one reaction. reactions : list of str - List of reactions. All reactions must match those in ``chain.REACTONS`` + List of reactions. All reactions must match those in ``chain.REACTIONS`` data : ndarray of floats - Array containing one-group microscopic cross section information for each - nuclide and reaction. - units : {'barn', 'cm^2'}, optional - Units of cross section values in ``data`` array. Defaults to ``barn``. + Array containing one-group microscopic cross section values, in + [barn], for each nuclide and reaction. Returns ------- @@ -483,8 +558,7 @@ class FluxDepletionOperator(OpenMCOperator): nuclides, reactions, data) # Convert to cm^2 - if units == 'barn': - data *= 1e-24 + data *= 1e-24 return pd.DataFrame(index=nuclides, columns=reactions, data=data) @@ -497,9 +571,7 @@ class FluxDepletionOperator(OpenMCOperator): ---------- csv_file : str Relative path to csv-file containing microscopic cross section - data. - units : {'barn', 'cm^2'}, optional - Units of cross section values in the ``.csv`` file array. Defaults to ``barn``. + data. Cross section values should be in [barn]. Returns ------- @@ -514,8 +586,7 @@ class FluxDepletionOperator(OpenMCOperator): list(micro_xs.columns), micro_xs.to_numpy()) - if units == 'barn': - micro_xs *= 1e-24 + micro_xs *= 1e-24 return micro_xs @@ -528,67 +599,3 @@ class FluxDepletionOperator(OpenMCOperator): for reaction in reactions: check_value('reactions', reaction, _valid_rxns) - - @staticmethod - def generate_1g_cross_sections(model, reaction_domain, reactions=['(n,gamma)', '(n,2n)', '(n,p)', '(n,a)', '(n,3n)', '(n,4n)', 'fission'], energy_bounds=(0, 20e6), write_to_csv=True, filename='micro_xs.csv'): - """Helper function to generate a one-group cross-section dataframe - using OpenMC. Note that the ``openmc`` C executable must be compiled. - - Parameters - ---------- - model : openmc.model.Model - OpenMC model object. Must contain geometry, materials, and settings. - reaction_domain : openmc.Material or openmc.Cell or openmc.Universe or openmc.RegularMesh - Domain in which to tally reaction rates. - reactions : list of str, optional - Reaction names to tally - energy_bound : 2-tuple of float, optional - Bounds for the energy group. - write_to_csv : bool, optional - Option to write the DataFrame to a `.csv` file. If `False`, - returns the dataframe object. - filename : str - Name for csv file. Only applicable if ``write_to_csv == True`` - - Returns - ------- - None or pandas.DataFrame - - """ - groups = EnergyGroups() - groups.group_edges = np.array(list(energy_bounds)) - - # Set up the reaction tallies - tallies = openmc.Tallies() - xs = dict() - for rxn in reactions: - if rxn == 'fission': - xs[rxn] = FissionXS(domain=reaction_domain, groups=groups, by_nuclide=True) - else: - xs[rxn] = ArbitraryXS(rxn, domain=reaction_domain, groups=groups, by_nuclide=True) - tallies += xs[rxn].tallies.values() - - model.tallies = tallies - statepoint_path = model.run() - - sp = openmc.StatePoint(statepoint_path) - - for rxn in xs: - xs[rxn].load_from_statepoint(sp) - - sp.close() - - # Build the DataFrame - micro_xs = pd.DataFrame() - for rxn in xs: - df = xs[rxn].get_pandas_dataframe(xs_type='micro') - df.index = df['nuclide'] - df.drop(['nuclide', xs[rxn].domain_type, 'group in', 'std. dev.'], axis=1, inplace=True) - df.rename({'mean':rxn}, axis=1, inplace=True) - micro_xs = pd.concat([micro_xs, df], axis=1) - - if write_to_csv: - micro_xs.to_csv(filename) - return None - else: - return micro_xs diff --git a/openmc/deplete/helpers.py b/openmc/deplete/helpers.py index 0aaa3b08a..7b863b69a 100644 --- a/openmc/deplete/helpers.py +++ b/openmc/deplete/helpers.py @@ -612,7 +612,7 @@ class FissionYieldCutoffHelper(TalliedFissionYieldHelper): Default: 0.0253 [eV] fast_energy : float, optional Energy of yield data corresponding to fast yields. - Default: 500 [kev] + Default: 500 [KeV] Attributes ---------- @@ -634,7 +634,7 @@ class FissionYieldCutoffHelper(TalliedFissionYieldHelper): Array of fission rate fractions with shape ``(n_mats, 2, n_nucs)``. ``results[:, 0]`` corresponds to the fraction of all fissions - that occured below ``cutoff``. The number + that occurred below ``cutoff``. The number of materials in the first axis corresponds to the number of materials burned by the :class:`openmc.deplete.Operator` @@ -790,7 +790,7 @@ class FissionYieldCutoffHelper(TalliedFissionYieldHelper): class AveragedFissionYieldHelper(TalliedFissionYieldHelper): r"""Class that computes fission yields based on average fission energy - Computes average energy at which fission events occured with + Computes average energy at which fission events occurred with .. math:: @@ -908,7 +908,7 @@ class AveragedFissionYieldHelper(TalliedFissionYieldHelper): Use the computed average energy of fission events to determine fission yields. If average energy is between two sets of yields, linearly - interpolate bewteen the two. + interpolate between the two. Otherwise take the closet set of yields. Parameters diff --git a/openmc/deplete/integrators.py b/openmc/deplete/integrators.py index c19ef076a..74a3cebdb 100644 --- a/openmc/deplete/integrators.py +++ b/openmc/deplete/integrators.py @@ -103,7 +103,7 @@ class CECMIntegrator(Integrator): op_results : list of openmc.deplete.OperatorResult Eigenvalue and reaction rates from transport simulations """ - # deplete across first half of inteval + # deplete across first half of interval time0, x_middle = self._timed_deplete(conc, rates, dt / 2) res_middle = self.operator(x_middle, source_rate) diff --git a/openmc/deplete/openmc_operator.py b/openmc/deplete/openmc_operator.py index aafa1adf9..7c0bb6678 100644 --- a/openmc/deplete/openmc_operator.py +++ b/openmc/deplete/openmc_operator.py @@ -90,7 +90,6 @@ class OpenMCOperator(TransportOperator): cross_sections : str or pandas.DataFrame Path to continuous energy cross section library, or object containing one-group cross-sections. - dilute_initial : float Initial atom density [atoms/cm^3] to add for nuclides that are zero in initial condition to ensure they exist in the decay @@ -527,7 +526,9 @@ class OpenMCOperator(TransportOperator): # Accumulate energy from fission if fission_ind is not None: - self._normalization_helper.update(tally_rates[:, fission_ind], mat_index=mat_index) + self._normalization_helper.update( + tally_rates[:, fission_ind], + mat_index=mat_index) # Divide by total number and store rates[i] = self._rate_helper.divide_by_adens(number) diff --git a/openmc/deplete/operator.py b/openmc/deplete/operator.py index 61573832c..db4e80b19 100644 --- a/openmc/deplete/operator.py +++ b/openmc/deplete/operator.py @@ -225,7 +225,10 @@ class Operator(OpenMCOperator): self.cleanup_when_done = True - helper_kwargs = dict() + if reaction_rate_opts is None: + reaction_rate_opts = {} + if fission_yield_opts is None: + fission_yield_opts = {} helper_kwargs = { 'reaction_rate_mode': reaction_rate_mode, 'normalization_mode': normalization_mode, @@ -329,17 +332,14 @@ class Operator(OpenMCOperator): reaction_rate_mode = helper_kwargs['reaction_rate_mode'] normalization_mode = helper_kwargs['normalization_mode'] fission_yield_mode = helper_kwargs['fission_yield_mode'] - reaction_rate_opts = helper_kwargs.get('reaction_rate_opts', {}) - fission_yield_opts = helper_kwargs.get('fission_yield_opts', {}) + reaction_rate_opts = helper_kwargs['reaction_rate_opts'] + fission_yield_opts = helper_kwargs['fission_yield_opts'] # Get classes to assist working with tallies if reaction_rate_mode == "direct": self._rate_helper = DirectReactionRateHelper( self.reaction_rates.n_nuc, self.reaction_rates.n_react) elif reaction_rate_mode == "flux": - if reaction_rate_opts is None: - reaction_rate_opts = {} - # Ensure energy group boundaries were specified if 'energies' not in reaction_rate_opts: raise ValueError( @@ -365,8 +365,6 @@ class Operator(OpenMCOperator): # Select and create fission yield helper fission_helper = self._fission_helpers[fission_yield_mode] - if fission_yield_opts is None: - fission_yield_opts = {} self._yield_helper = fission_helper.from_operator( self, **fission_yield_opts) @@ -489,7 +487,7 @@ class Operator(OpenMCOperator): print(f'WARNING: nuclide {nuc} in material' f'{mat} is negative (density = {val}' - ' at/barn-cm)') + ' atom/b-cm)') number_i[mat, nuc] = 0.0 diff --git a/tests/regression_tests/deplete_no_transport/test.py b/tests/regression_tests/deplete_no_transport/test.py index 9ae0f84f4..38d4aaf2e 100644 --- a/tests/regression_tests/deplete_no_transport/test.py +++ b/tests/regression_tests/deplete_no_transport/test.py @@ -3,8 +3,8 @@ from math import floor import shutil from pathlib import Path - from difflib import unified_diff + import numpy as np import pytest import openmc @@ -12,7 +12,6 @@ from openmc.data import JOULE_PER_EV import openmc.deplete from openmc.deplete import FluxDepletionOperator - from tests.regression_tests import config @@ -22,7 +21,7 @@ def fuel(): fuel.add_element("U", 1, percent_type="ao", enrichment=4.25) fuel.add_element("O", 2) fuel.set_density("g/cc", 10.4) - fuel.depletable=True + fuel.depletable = True fuel.volume = np.pi * 0.42 ** 2 @@ -30,14 +29,14 @@ def fuel(): @pytest.mark.parametrize("multiproc, from_nuclides, normalization_mode, power, flux", [ - (True, True,'constant-flux', None, 1164719970082145.0), - (False, True, 'constant-flux', None, 1164719970082145.0), - (True, True, 'constant-power', 174, None), - (False, True, 'constant-power', 174, None), - (True, False,'constant-flux', None, 1164719970082145.0), - (False, False, 'constant-flux', None, 1164719970082145.0), - (True, False, 'constant-power', 174, None), - (False, False, 'constant-power', 174, None)]) + (True, True,'source-rate', None, 1164719970082145.0), + (False, True, 'source-rate', None, 1164719970082145.0), + (True, True, 'fission-q', 174, None), + (False, True, 'fission-q', 174, None), + (True, False,'source-rate', None, 1164719970082145.0), + (False, False, 'source-rate', None, 1164719970082145.0), + (True, False, 'fission-q', 174, None), + (False, False, 'fission-q', 174, None)]) def test_no_transport_from_nuclides(run_in_tmpdir, fuel, multiproc, from_nuclides, normalization_mode, power, flux): """Transport free system test suite. @@ -53,10 +52,10 @@ def test_no_transport_from_nuclides(run_in_tmpdir, fuel, multiproc, from_nuclide if from_nuclides: nuclides = {} for nuc, dens in fuel.get_nuclide_atom_densities().items(): - nuclides[nuc] = dens * 1e24 + nuclides[nuc] = dens op = FluxDepletionOperator.from_nuclides( - fuel.volume, nuclides, micro_xs, chain_file, normalization_mode=normalization_mode) + fuel.volume, nuclides,'atom/b-cm', micro_xs, chain_file, normalization_mode=normalization_mode) else: op = FluxDepletionOperator(openmc.Materials([fuel]), micro_xs, chain_file, normalization_mode=normalization_mode) @@ -67,14 +66,14 @@ def test_no_transport_from_nuclides(run_in_tmpdir, fuel, multiproc, from_nuclide # Perform simulation using the predictor algorithm openmc.deplete.pool.USE_MULTIPROCESSING = multiproc openmc.deplete.PredictorIntegrator( - op, dt, power=power, flux=flux, timestep_units='d').integrate() + op, dt, power=power, source_rates=flux, timestep_units='d').integrate() # Get path to test and reference results path_test = op.output_dir / 'depletion_results.h5' if flux is not None: - ref_path = 'test_reference_constant_flux.h5' + ref_path = 'test_reference_source_rate.h5' else: - ref_path = 'test_reference_constant_power.h5' + ref_path = 'test_reference_fission_q.h5' path_reference = Path(__file__).with_name(ref_path) # Load the reference/test results diff --git a/tests/regression_tests/deplete_no_transport/test_reference_constant_power.h5 b/tests/regression_tests/deplete_no_transport/test_reference_fission_q.h5 similarity index 99% rename from tests/regression_tests/deplete_no_transport/test_reference_constant_power.h5 rename to tests/regression_tests/deplete_no_transport/test_reference_fission_q.h5 index c6e1a40808c135aafe1065b2cc95357c3fede0a2..6c1b3de849e1a8792b9ce9398c7e2e41ae410b5e 100644 GIT binary patch delta 195 zcmV;!06hQbmICOO0*|BlmMJyliu@d`x4IkRRgf8^dYK3F&`gw7m^Ie>Y) z5+(faJ(G}%EPtS$v_CI|yv+VQEJGD17m5@ghV(jJx%GE)j5EL0c^iFUp>2- xffA~M!#OE`I{yLRG(K~X!@6#6i8bXv~IbXv-2A#e3j8DX_-aLQBZdIMygbzjc?Tl_5w39Y6vgdQWX@7{t%6>_Ql5>{M zM!Sp4@}-uy9=5xcmArDD`bFoxhwbgm|Ju522o5;9;A)GD>qK^oDIr%a??D|92z7uv b)B(Xz2gpDjz`(#Td2*NFWS?#c7LYpt(gRg~ diff --git a/tests/regression_tests/deplete_no_transport/test_reference_constant_flux.h5 b/tests/regression_tests/deplete_no_transport/test_reference_source_rate.h5 similarity index 100% rename from tests/regression_tests/deplete_no_transport/test_reference_constant_flux.h5 rename to tests/regression_tests/deplete_no_transport/test_reference_source_rate.h5 diff --git a/tests/unit_tests/test_deplete_flux_operator.py b/tests/unit_tests/test_deplete_flux_operator.py index 9019d123d..8f17538ed 100644 --- a/tests/unit_tests/test_deplete_flux_operator.py +++ b/tests/unit_tests/test_deplete_flux_operator.py @@ -71,7 +71,7 @@ def test_operator_init(): 'O17': 1.7588724018066158e+19} micro_xs = FluxDepletionOperator.create_micro_xs_from_csv(ONE_GROUP_XS) nuclide_flux_operator = FluxDepletionOperator.from_nuclides( - volume, nuclides, micro_xs, CHAIN_PATH) + volume, nuclides, 'atom/cm3', micro_xs, CHAIN_PATH) fuel = Material(name="uo2") fuel.add_element("U", 1, percent_type="ao", enrichment=4.25) From b2fed3c3cec856b85f7fb75aa8f4a20e1e974a84 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 28 Jul 2022 17:06:13 -0500 Subject: [PATCH 097/131] Fix bugs in Discrete, Tabular, and Mixture sample methods --- openmc/stats/univariate.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index 66c6e5f06..c19f45365 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -139,7 +139,8 @@ class Discrete(Univariate): def sample(self, n_samples=1, seed=None): np.random.seed(seed) - return np.random.choice(self.x, n_samples, p=self.p) + p = self.p / self.p.sum() + return np.random.choice(self.x, n_samples, p=p) def normalize(self): """Normalize the probabilities stored on the distribution""" @@ -944,10 +945,11 @@ class Tabular(Univariate): 'linear-linear interpolation') np.random.seed(seed) xi = np.random.rand(n_samples) - cdf = self.cdf() - cdf /= cdf.max() + # always use normalized probabilities when sampling + cdf = self.cdf() p = self.p / cdf.max() + cdf /= cdf.max() # get CDF bins that are above the # sampled values @@ -962,7 +964,7 @@ class Tabular(Univariate): # the random number is less than the next cdf # entry x_i = self.x[cdf_idx] - p_i = self.p[cdf_idx] + p_i = p[cdf_idx] if self.interpolation == 'histogram': # mask where probability is greater than zero @@ -980,7 +982,7 @@ class Tabular(Univariate): # get variable and probability values for the # next entry x_i1 = self.x[cdf_idx + 1] - p_i1 = self.p[cdf_idx + 1] + p_i1 = p[cdf_idx + 1] # compute slope between entries m = (p_i1 - p_i) / (x_i1 - x_i) # set values for zero slope @@ -1166,9 +1168,10 @@ class Mixture(Univariate): def sample(self, n_samples=1, seed=None): np.random.seed(seed) - idx = np.random.choice(self.distribution, n_samples, p=self.probability) + idx = np.random.choice(range(len(self.distribution)), + n_samples, p=self.probability) - out = np.zeros_like(idx) + out = np.empty_like(idx, dtype=float) for i in np.unique(idx): n_dist_samples = np.count_nonzero(idx == i) samples = self.distribution[i].sample(n_dist_samples) From cc7aa092be7b7a9eed9c9ddd85a9e1edb0a1f14b Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 28 Jul 2022 17:07:06 -0500 Subject: [PATCH 098/131] Address @eepeterson comments on #2135 --- openmc/stats/univariate.py | 8 ++-- tests/unit_tests/test_stats.py | 69 ++++++++++++++++++++-------------- 2 files changed, 44 insertions(+), 33 deletions(-) diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index c19f45365..58b7c6172 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -1254,10 +1254,10 @@ def combine_distributions(dists, probs): """Combine distributions with specified probabilities This function can be used to combine multiple instances of - :class:`~openmc.stats.Discrete` and `~openmc.stats.Tabular` into a single - distribution. Multiple discrete distributions are merged into a single - distribution and the remainder of the distributions are put into a - :class:`~openmc.stats.Mixture` distribution. + :class:`~openmc.stats.Discrete` and `~openmc.stats.Tabular`. Multiple + discrete distributions are merged into a single distribution and the + remainder of the distributions are put into a :class:`~openmc.stats.Mixture` + distribution. .. versionadded:: 0.13.1 diff --git a/tests/unit_tests/test_stats.py b/tests/unit_tests/test_stats.py index 404cecd13..1cfcf00c3 100644 --- a/tests/unit_tests/test_stats.py +++ b/tests/unit_tests/test_stats.py @@ -6,6 +6,12 @@ import openmc import openmc.stats +def assert_sample_mean(samples, expected_mean): + std_dev = samples.std() / np.sqrt(samples.size) + assert np.abs(expected_mean - samples.mean()) < 3*std_dev + + + def test_discrete(): x = [0.0, 1.0, 10.0] p = [0.3, 0.2, 0.5] @@ -33,13 +39,11 @@ def test_discrete(): d3 = openmc.stats.Discrete(vals, probs) - # sample discrete distribution + # sample discrete distribution and check that the mean of the samples is + # within 3 std. dev. of the expected mean n_samples = 1_000_000 samples = d3.sample(n_samples, seed=100) - # check that the mean of the samples is within 3 std. dev. - # of the expected mean - std_dev = samples.std() / np.sqrt(n_samples) - assert np.abs(exp_mean - samples.mean()) < 3*std_dev + assert_sample_mean(samples, exp_mean) def test_merge_discrete(): @@ -80,13 +84,12 @@ def test_uniform(): np.testing.assert_array_equal(t.p, [1/(b-a), 1/(b-a)]) assert t.interpolation == 'histogram' + # Sample distribution and check that the mean of the samples is within 3 + # std. dev. of the expected mean exp_mean = 0.5 * (a + b) n_samples = 1_000_000 samples = d.sample(n_samples, seed=100) - # check that the mean of the samples is within 3 std. dev. - # of the expected mean - std_dev = samples.std() / np.sqrt(n_samples) - assert np.abs(exp_mean - samples.mean()) < 3*std_dev + assert_sample_mean(samples, exp_mean) def test_powerlaw(): @@ -102,13 +105,11 @@ def test_powerlaw(): exp_mean = 100.0 * (n+1) / (n+2) - # sample power law distribution + # sample power law distribution and check that the mean of the samples is + # within 3 std. dev. of the expected mean n_samples = 1_000_000 samples = d.sample(n_samples, seed=100) - # check that the mean of the samples is within 3 std. dev. - # of the expected mean - std_dev = samples.std() / np.sqrt(n_samples) - assert np.abs(exp_mean - samples.mean()) < 3*std_dev + assert_sample_mean(samples, exp_mean) def test_maxwell(): @@ -122,21 +123,15 @@ def test_maxwell(): exp_mean = 3/2 * theta - # sample maxwell distribution + # sample maxwell distribution and check that the mean of the samples is + # within 3 std. dev. of the expected mean n_samples = 1_000_000 samples = d.sample(n_samples, seed=100) - # check that the mean of the samples is within 3 std. dev. - # of the expected mean - std_dev = samples.std() / np.sqrt(n_samples) - assert np.abs(exp_mean - samples.mean()) < 3*std_dev + assert_sample_mean(samples, exp_mean) # A second sample with a different seed samples_2 = d.sample(n_samples, seed=200) - # check that the mean of the samples is within 3 std. dev. - # of the expected mean - std_dev = samples_2.std() / np.sqrt(n_samples) - assert np.abs(exp_mean - samples_2.mean()) < 3*std_dev - + assert_sample_mean(samples_2, exp_mean) assert samples_2.mean() != samples.mean() @@ -156,13 +151,11 @@ def test_watt(): # https://doi.org/10.1016/j.physletb.2003.09.048 exp_mean = 3/2 * a + a**2 * b / 4 - # sample Watt distribution + # sample Watt distribution and check that the mean of the samples is within + # 3 std. dev. of the expected mean n_samples = 1_000_000 samples = d.sample(n_samples, seed=100) - # check that the mean of the samples is within 3 std. dev. - # of the expected mean - std_dev = samples.std() / np.sqrt(n_samples) - assert np.abs(exp_mean - samples.mean()) < 3*std_dev + assert_sample_mean(samples, exp_mean) def test_tabular(): @@ -228,6 +221,11 @@ def test_mixture(): assert mix.distribution == [d1, d2] assert len(mix) == 4 + # Sample and make sure sample mean is close to expected mean + n_samples = 1_000_000 + samples = mix.sample(n_samples) + assert_sample_mean(samples, (2.5 + 5.0)/2) + elem = mix.to_xml_element('distribution') d = openmc.stats.Mixture.from_xml_element(elem) @@ -427,3 +425,16 @@ def test_combine_distributions(): assert isinstance(mixed, openmc.stats.Mixture) assert len(mixed.distribution) == 2 assert len(mixed.probability) == 2 + + # Combine 1 discrete and 2 tabular -- the tabular distributions should + # combine to produce a uniform distribution with mean 0.5. The combined + # distribution should have a mean of 0.25. + t1 = openmc.stats.Tabular([0., 1.], [2.0, 0.0]) + t2 = openmc.stats.Tabular([0., 1.], [0.0, 2.0]) + d1 = openmc.stats.Discrete([0.0], [1.0]) + combined = openmc.stats.combine_distributions([t1, t2, d1], [0.25, 0.25, 0.5]) + + # Sample the combined distribution and make sure the sample mean is within + # uncertainty of the expected value + samples = combined.sample(10) + assert_sample_mean(samples, 0.25) From 3e827530baef93dabcfa99a7141bba4db9997660 Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 28 Jul 2022 15:55:07 -0500 Subject: [PATCH 099/131] make changes from @paulromano's 3rd review - syntax fixes and adjustments - change name of FluxDepletionOperator to IndependentOperator - flux_operator.py -> independent_operator.py - new class, MicroXS, for creating (for now) one-group microscopic cross section DataFrames. This class takes the functionality that was previously in static functions in IndependentOperator - Associated changes to the test suite and online docs --- docs/source/pythonapi/deplete.rst | 5 +- docs/source/usersguide/depletion.rst | 64 +++-- openmc/deplete/__init__.py | 3 +- openmc/deplete/abc.py | 4 +- ...ux_operator.py => independent_operator.py} | 234 +++--------------- openmc/deplete/microxs.py | 178 +++++++++++++ openmc/deplete/openmc_operator.py | 8 +- openmc/deplete/operator.py | 4 +- .../deplete_no_transport/test.py | 18 +- tests/regression_tests/microxs/__init.py__ | 0 tests/regression_tests/microxs/test.py | 52 ++++ .../microxs/test_reference.csv | 7 + .../unit_tests/test_deplete_flux_operator.py | 83 ------- .../test_deplete_independent_operator.py | 36 +++ tests/unit_tests/test_deplete_microxs.py | 60 +++++ 15 files changed, 437 insertions(+), 319 deletions(-) rename openmc/deplete/{flux_operator.py => independent_operator.py} (67%) create mode 100644 openmc/deplete/microxs.py create mode 100644 tests/regression_tests/microxs/__init.py__ create mode 100644 tests/regression_tests/microxs/test.py create mode 100644 tests/regression_tests/microxs/test_reference.csv delete mode 100644 tests/unit_tests/test_deplete_flux_operator.py create mode 100644 tests/unit_tests/test_deplete_independent_operator.py create mode 100644 tests/unit_tests/test_deplete_microxs.py diff --git a/docs/source/pythonapi/deplete.rst b/docs/source/pythonapi/deplete.rst index cb9adfaaf..246cea59f 100644 --- a/docs/source/pythonapi/deplete.rst +++ b/docs/source/pythonapi/deplete.rst @@ -49,9 +49,9 @@ specific to OpenMC are available using the following classes: :template: mycallable.rst Operator - FluxDepletionOperator + IndependentOperator -The :class:`Operator` and :class:`FluxDepletionOperator` classes must also have +The :class:`Operator` and :class:`IndependentOperator` classes must also have some knowledge of how nuclides transmute and decay. This is handled by the :class:`Chain`. @@ -134,6 +134,7 @@ data, such as number densities and reaction rates for each material. :template: myclass.rst AtomNumber + MicroXS OperatorResult ReactionRates Results diff --git a/docs/source/usersguide/depletion.rst b/docs/source/usersguide/depletion.rst index cfb373e13..0bc2a5af9 100644 --- a/docs/source/usersguide/depletion.rst +++ b/docs/source/usersguide/depletion.rst @@ -97,7 +97,7 @@ Energy Deposition ----------------- The default energy deposition mode, ``"fission-q"``, instructs the -:class:`openmc.deplete.Operator` to normalize reaction rates using the product +:class:`~openmc.deplete.Operator` to normalize reaction rates using the product of fission reaction rates and fission Q values taken from the depletion chain. This approach does not consider indirect contributions to energy deposition, such as neutron heating and energy from secondary photons. In doing this, the @@ -113,7 +113,7 @@ should be, including indirect components. Some examples are provided below:: fission_q = {"U235": 202e+6} # energy in eV # create a Model object - model = openmc.Model(geometry, settings) + model = openmc.Model(geometry, settings) # create a modified chain and write it to a new file chain = openmc.deplete.Chain.from_xml("chain.xml", fission_q) @@ -133,7 +133,7 @@ to normalize reaction rates instead of using the fission reaction rates with:: normalization_mode="energy-deposition") These modified heating libraries can be generated by running the latest version -of :meth:`openmc.data.IncidentNeutron.from_njoy`, and will eventually be bundled +of :meth:`openmc.data.IncidentNeutron.from_njoy()`, and will eventually be bundled into the distributed libraries. Local Spectra and Repeated Materials @@ -188,31 +188,57 @@ Transport-independent depletion possible and likely in the near future. OpenMC supports running depletion calculations independent of the OpenMC -transport solver using the :class:`~openmc.deplete.FluxDepletionOperator` class. +transport solver using the :class:`~openmc.deplete.IndependentOperator` class. This class supports both constant-flux (``source-rate`` normalization) and constant-power depletion (``fission-q`` normalization). .. important:: - Make sure you set the correct parameter in the :class:`openmc.abc.Integrator` class. Use the ``source_rates`` parameter when ``normalization_mode == source-rate``, and use ``power`` or ``power_density`` when ``normalization_mode == fission-q``. + Make sure you set the correct parameter in the :class:`openmc.abc.Integrator` + class. Use the ``source_rates`` parameter when + ``normalization_mode == source-rate``, and use ``power`` or ``power_density`` + when ``normalization_mode == fission-q``. .. warning:: - The accuracy of results when using ``fission-q`` is entirely dependent on your depletion chain. Make sure it has sufficient data to resolve the dynamics of your particular scenario. + The accuracy of results when using ``fission-q`` is entirely dependent on + your depletion chain. Make sure it has sufficient data to resolve the + dynamics of your particular scenario. -This class has two ways to initialize it: the default constructor accepts an -:class:`openmc.Materials` object and one-group microscopic -cross sections as a :class:`pandas.DataFrame`, while the ``from_nuclides`` -method accepts a volume and dictionary of nuclide concentrations in place of -the :class:`openmc.Materials` object in addition to the other parameters. -The class includes helper functions to construct the dataframe from a csv file -or from data arrays:: +:class:`~openmc.deplete.IndependentOperator` class uses one-group microscopic cross sections to calculate reaction +rates. Users can generate one-group microscopic cross sections using the +:class:`~openmc.deplete.MicroXS` class:: + + import openmc + from openmc.deplete import MicroXS + + model = openmc.Model.from_xml() + + micro_xs = MicroXS.from_model(model, model.materials[0]) + + micro_xs.to_csv(micro_xs_path) + +:class:`~openmc.deplete.MicroXS` also includes functions to read in cross +section data directly from a ``.csv`` file or from data arrays:: + + micro_xs = MicroXS.from_csv(micro_xs_path) + + nuclides = ['U234', 'U235', 'U238'] + reactions = ['fission', '(n,gamma)'] + data = np.array([[0.1, 0.2], + [0.3, 0.4], + [0.01, 0.5]]) + micro_xs = MicroXS.from_array(nuclides, reactions, data) + +:class:`~openmc.deplete.IndependentOperator` has two ways to initialize it: +the default constructor accepts an :class:`openmc.Materials` object and +one-group microscopic cross sections as a :class:`~openmc.deplete.MicroXS` +object, while the :meth:`~openmc.deplete.IndependentOperator.from_nuclides` +method accepts a volume and dictionary of nuclide concentrations in place of the +:class:`openmc.Materials` object in addition to the other parameters:: - ... # load in the microscopic cross sections - micro_xs = FluxDepletionOperator.create_micro_xs_from_csv(micro_xs_path) - flux = 1.16e15 - op = FluxDepletionOperator(materials, micro_xs, chain_file) + op = IndependentOperator(materials, micro_xs, chain_file) # alternate construtor nuclides = {'U234': 8.92e18, @@ -222,8 +248,8 @@ or from data arrays:: 'O16': 4.64e22, 'O17': 1.76e19} volume = 0.5 - op = FluxDepletionOperator.from_nuclides(volume, nuclides, 'atom/cm3', - micro_xs, flux, chain_file) + op = IndependentOperator.from_nuclides(volume, nuclides, micro_xs, + chain_file, nuc_units='atom/cm3') A user can then define an integrator class as they would for a coupled transport-depletion calculation and follow the same steps from there. diff --git a/openmc/deplete/__init__.py b/openmc/deplete/__init__.py index deddf1100..12e4abe87 100644 --- a/openmc/deplete/__init__.py +++ b/openmc/deplete/__init__.py @@ -9,7 +9,8 @@ from .nuclide import * from .chain import * from .openmc_operator import * from .operator import * -from .flux_operator import * +from .independent_operator import * +from .microxs import * from .reaction_rates import * from .atom_number import * from .stepresult import * diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 90a353d93..cdbd1361a 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -526,7 +526,7 @@ class Integrator(ABC): initial heavy metal inventory to get total power if ``power`` is not specified. source_rates : float or iterable of float, optional - Source rate in [neutron/sec] or neutron flux in [neut/s-cm^2] for each + Source rate in [neutron/sec] or neutron flux in [neut/cm^2-s] for each interval in :attr:`timesteps` .. versionadded:: 0.12.1 @@ -864,7 +864,7 @@ class SIIntegrator(Integrator): initial heavy metal inventory to get total power if ``power`` is not specified. source_rates : float or iterable of float, optional - Source rate in [neutron/sec] or neutron flux in [neut/s-cm^2] for each + Source rate in [neutron/sec] or neutron flux in [neutron/s-cm^2] for each interval in :attr:`timesteps` .. versionadded:: 0.12.1 diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/independent_operator.py similarity index 67% rename from openmc/deplete/flux_operator.py rename to openmc/deplete/independent_operator.py index 410552671..98ef6656d 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/independent_operator.py @@ -1,7 +1,7 @@ -"""Pure depletion operator +"""Independent depletion operator -This module implements a pure depletion operator that user-provided one-group -cross sections. +This module implements a depletion operator that runs independently of any +transport solver by using user-provided one-group cross sections. """ @@ -11,110 +11,33 @@ from warnings import warn from itertools import product import numpy as np -import pandas as pd from uncertainties import ufloat import openmc -from openmc.checkvalue import check_type, check_value, check_iterable_type +from openmc.checkvalue import check_type from openmc.mpi import comm -from openmc.mgxs import EnergyGroups, ArbitraryXS, FissionXS from .abc import ReactionRateHelper, OperatorResult -from .chain import REACTIONS from .openmc_operator import OpenMCOperator, _distribute +from .microxs import MicroXS from .results import Results from .helpers import ChainFissionHelper, ConstantFissionYieldHelper, SourceRateHelper -_valid_rxns = list(REACTIONS) -_valid_rxns.append('fission') - -def generate_1g_cross_sections(model, - reaction_domain, - reactions=['(n,gamma)', - '(n,2n)', - '(n,p)', - '(n,a)', - '(n,3n)', - '(n,4n)', - 'fission'], - energy_bounds=(0, 20e6), - write_to_csv=False, - filename='micro_xs.csv'): - """Helper function to generate a one-group cross-section dataframe using - OpenMC. Note that the ``openmc`` executable must be compiled. - - Parameters - ---------- - model : openmc.model.Model - OpenMC model object. Must contain geometry, materials, and settings. - reaction_domain : openmc.Material or openmc.Cell or openmc.Universe or openmc.RegularMesh - Domain in which to tally reaction rates. - reactions : list of str, optional - Reaction names to tally - energy_bound : 2-tuple of float, optional - Bounds for the energy group. - write_to_csv : bool, optional - Option to write the DataFrame to a `.csv` file. - filename : str - Name for csv file. Only applicable if ``write_to_csv == True`` - - Returns - ------- - None or pandas.DataFrame - - """ - groups = EnergyGroups(energy_bounds) - - # Set up the reaction tallies - original_tallies = model.tallies - tallies = openmc.Tallies() - xs = {} - for rxn in reactions: - if rxn == 'fission': - xs[rxn] = FissionXS(domain=reaction_domain, groups=groups, by_nuclide=True) - else: - xs[rxn] = ArbitraryXS(rxn, domain=reaction_domain, groups=groups, by_nuclide=True) - tallies += xs[rxn].tallies.values() - - model.tallies = tallies - statepoint_path = model.run() - - # Revert to the original tallies - model.tallies = old_tallies - - with openmc.StatePoint(statepoint_path) as sp: - for rxn in xs: - xs[rxn].load_from_statepoint(sp) - - # Build the DataFrame - micro_xs = pd.DataFrame() - for rxn in xs: - df = xs[rxn].get_pandas_dataframe(xs_type='micro') - df.index = df['nuclide'] - df.drop(['nuclide', xs[rxn].domain_type, 'group in', 'std. dev.'], axis=1, inplace=True) - df.rename({'mean':rxn}, axis=1, inplace=True) - micro_xs = pd.concat([micro_xs, df], axis=1) - - if write_to_csv: - micro_xs.to_csv(filename) - - return micro_xs - -class FluxDepletionOperator(OpenMCOperator): - """Depletion operator that uses one-group - cross sections to calculate reaction rates. +class IndependentOperator(OpenMCOperator): + """Depletion operator that uses one-group cross sections to calculate + reaction rates. Instances of this class can be used to perform depletion using one-group - cross sections and constant flux or constant power. Normally, a user needn't call methods of - this class directly. Instead, an instance of this class is passed to an - integrator class, such as :class:`openmc.deplete.CECMIntegrator`. + cross sections and constant flux or constant power. Normally, a user needn't + call methods of this class directly. Instead, an instance of this class is + passed to an integrator class, such as + :class:`openmc.deplete.CECMIntegrator`. Parameters ---------- materials : openmc.Materials Materials to deplete. - micro_xs : pandas.DataFrame - DataFrame with nuclides names as index and microscopic cross section - data in the columns. Cross section units are [cm^-2]. + micro_xs : MicroXS + One-group microscopic cross sections in [b] . chain_file : str Path to the depletion chain XML file. keff : 2-tuple of float, optional @@ -134,23 +57,23 @@ class FluxDepletionOperator(OpenMCOperator): if ``"normalization_mode" == "fission-q"``. reduce_chain : bool, optional If True, use :meth:`openmc.deplete.Chain.reduce` to reduce the - depletion chain up to ``reduce_chain_level``. Default is False. + depletion chain up to ``reduce_chain_level``. reduce_chain_level : int, optional Depth of the search when reducing the depletion chain. Only used if ``reduce_chain`` evaluates to true. The default value of ``None`` implies no limit on the depth. fission_yield_opts : dict of str to option, optional - Optional arguments to pass to the `FissionYieldHelper`. Will be + Optional arguments to pass to the + :class:`openmc.deplete.helpers.FissionYieldHelper` object. Will be passed directly on to the helper. Passing a value of None will use the defaults for the associated helper. - Attributes ---------- materials : openmc.Materials All materials present in the model - cross_sections : pandas.DataFrame - Object containing one-group cross-sections. + cross_sections : MicroXS + Object containing one-group cross-sections in [cm^2]. dilute_initial : float Initial atom density [atoms/cm^3] to add for nuclides that are zero in initial condition to ensure they exist in the decay @@ -193,7 +116,7 @@ class FluxDepletionOperator(OpenMCOperator): fission_yield_opts=None): # Validate micro-xs parameters check_type('materials', materials, openmc.Materials) - check_type('micro_xs', micro_xs, pd.DataFrame) + check_type('micro_xs', micro_xs, MicroXS) if keff is not None: check_type('keff', keff, tuple, float) keff = ufloat(*keff) @@ -205,9 +128,11 @@ class FluxDepletionOperator(OpenMCOperator): helper_kwargs = {'normalization_mode': normalization_mode, 'fission_yield_opts': fission_yield_opts} + cross_sections = micro_xs * 1e-24 + cross_sections._units = 'cm^2' super().__init__( materials, - micro_xs, + cross_sections, chain_file, prev_results, fission_q=fission_q, @@ -216,9 +141,10 @@ class FluxDepletionOperator(OpenMCOperator): reduce_chain_level=reduce_chain_level) @classmethod - def from_nuclides(cls, volume, nuclides, nuc_units, + def from_nuclides(cls, volume, nuclides, micro_xs, chain_file, + nuc_units='atom/b-cm', keff=None, normalization_mode='source-rate', fission_q=None, @@ -234,13 +160,12 @@ class FluxDepletionOperator(OpenMCOperator): nuclides : dict of str to float Dictionary with nuclide names as keys and nuclide concentrations as values. - nuc_units : {'atom/cm3', 'atom/b-cm'} - Units for nuclide concentration. - micro_xs : pandas.DataFrame - DataFrame with nuclides names as index and microscopic cross section - data in the columns. Cross section units are [cm^-2]. + micro_xs : MicroXS + One-group microscopic cross sections. chain_file : str Path to the depletion chain XML file. + nuc_units : {'atom/cm3', 'atom/b-cm'} + Units for nuclide concentration. keff : 2-tuple of float, optional keff eigenvalue and uncertainty from transport calculation. Default is None. @@ -325,17 +250,16 @@ class FluxDepletionOperator(OpenMCOperator): """Finds nuclides with cross section data""" return set(cross_sections.index) - class _FluxDepletionNormalizationHelper(ChainFissionHelper): - """Class for calculating one-group flux - based on a power. + class _IndependentNormalizationHelper(ChainFissionHelper): + """Class for calculating one-group flux based on a power. flux = Power / X, where X = volume * sum_i(Q_i * fission_micro_xs_i * density_i) Parameters ---------- - op : openmc.deplete.FluxDepletionOperator - Reference to the object encapsulate _FluxDepletionNormalizationHelper. - We pass this so we don't have to duplicate the ``number`` object. + op : openmc.deplete.IndependentOperator + Reference to the object encapsulating _IndependentNormalizationHelper. + We pass this so we don't have to duplicate :attr:`IndependentOperator.number`. """ @@ -367,7 +291,7 @@ class FluxDepletionOperator(OpenMCOperator): super().update(fission_rates) - class _FluxDepletionRateHelper(ReactionRateHelper): + class _IndependentRateHelper(ReactionRateHelper): """Class for generating one-group reaction rates with flux and one-group cross sections. @@ -378,9 +302,9 @@ class FluxDepletionOperator(OpenMCOperator): Parameters ---------- - op : openmc.deplete.FluxDepletionOperator - Reference to the object encapsulate _FluxDepletionRateHelper. - We pass this so we don't have to duplicate the ``number`` object. + op : openmc.deplete.IndependentOperator + Reference to the object encapsulate _IndependentRateHelper. + We pass this so we don't have to duplicate the :attr:`IndependentOperator.number` object. Attributes @@ -441,9 +365,9 @@ class FluxDepletionOperator(OpenMCOperator): normalization_mode = helper_kwargs['normalization_mode'] fission_yield_opts = helper_kwargs['fission_yield_opts'] - self._rate_helper = self._FluxDepletionRateHelper(self) + self._rate_helper = self._IndependentRateHelper(self) if normalization_mode == "fission-q": - self._normalization_helper = self._FluxDepletionNormalizationHelper(self) + self._normalization_helper = self._IndependentNormalizationHelper(self) else: self._normalization_helper = SourceRateHelper() @@ -472,7 +396,7 @@ class FluxDepletionOperator(OpenMCOperator): vec : list of numpy.ndarray Total atoms to be used in function. source_rate : float - Power in [W] or flux in [neut/s-cm^2] + Power in [W] or flux in [neutron/cm^2-s] Returns ------- @@ -523,79 +447,3 @@ class FluxDepletionOperator(OpenMCOperator): ' atom/b-cm)') number_i[mat, nuc] = 0.0 - - @staticmethod - def create_micro_xs_from_data_array(nuclides, reactions, data): - """ - Creates a ``micro_xs`` parameter from a dictionary. - - Parameters - ---------- - nuclides : list of str - List of nuclide symbols for that have data for at least one - reaction. - reactions : list of str - List of reactions. All reactions must match those in ``chain.REACTIONS`` - data : ndarray of floats - Array containing one-group microscopic cross section values, in - [barn], for each nuclide and reaction. - - Returns - ------- - micro_xs : pandas.DataFrame - A DataFrame object correctly formatted for use in ``FluxOperator``. - Cross section data is in [cm^2] - """ - - # Validate inputs - if data.shape != (len(nuclides), len(reactions)): - raise ValueError( - f'Nuclides list of length {len(nuclides)} and ' - f'reactions array of length {len(reactions)} do not ' - f'match dimensions of data array of shape {data.shape}') - - FluxDepletionOperator._validate_micro_xs_inputs( - nuclides, reactions, data) - - # Convert to cm^2 - data *= 1e-24 - - return pd.DataFrame(index=nuclides, columns=reactions, data=data) - - @staticmethod - def create_micro_xs_from_csv(csv_file, units='barn'): - """ - Create the ``micro_xs`` parameter from a ``.csv`` file. - - Parameters - ---------- - csv_file : str - Relative path to csv-file containing microscopic cross section - data. Cross section values should be in [barn]. - - Returns - ------- - micro_xs : pandas.DataFrame - A DataFrame object correctly formatted for use in ``FluxOperator``. - Cross section data is in [cm^2] - - """ - micro_xs = pd.read_csv(csv_file, index_col=0) - - FluxDepletionOperator._validate_micro_xs_inputs(list(micro_xs.index), - list(micro_xs.columns), - micro_xs.to_numpy()) - - micro_xs *= 1e-24 - - return micro_xs - - # Convenience function for the micro_xs static methods - @staticmethod - def _validate_micro_xs_inputs(nuclides, reactions, data): - check_iterable_type('nuclides', nuclides, str) - check_iterable_type('reactions', reactions, str) - check_type('data', data, np.ndarray, expected_iter_type=float) - for reaction in reactions: - check_value('reactions', reaction, _valid_rxns) - diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py new file mode 100644 index 000000000..6abf8836f --- /dev/null +++ b/openmc/deplete/microxs.py @@ -0,0 +1,178 @@ +"""MicroXS module + +A pandas.DataFrame storing microscopic cross section data with +nuclides names as row indices and reaction names as column indices. +""" + +import tempfile +from pathlib import Path +from os import chdir + +from pandas import DataFrame, read_csv, concat +from numpy import ndarray + +from openmc.checkvalue import check_type, check_value, check_iterable_type +from openmc.mgxs import EnergyGroups, ArbitraryXS, FissionXS +from openmc import Tallies, StatePoint + +from .chain import REACTIONS + +_valid_rxns = list(REACTIONS) +_valid_rxns.append('fission') + + +class MicroXS(DataFrame): + """Stores microscopic cross section data for use in + independent depletion. + """ + + @classmethod + def from_model(cls, + model, + reaction_domain, + reactions=['(n,gamma)', + '(n,2n)', + '(n,p)', + '(n,a)', + '(n,3n)', + '(n,4n)', + 'fission'], + energy_bounds=(0, 20e6)): + """Generate a one-group cross-section dataframe using + OpenMC. Note that the ``openmc`` executable must be compiled. + + Parameters + ---------- + model : openmc.Model + OpenMC model object. Must contain geometry, materials, and settings. + reaction_domain : openmc.Material or openmc.Cell or openmc.Universe or openmc.RegularMesh + Domain in which to tally reaction rates. + reactions : list of str, optional + Reaction names to tally + energy_bound : 2-tuple of float, optional + Bounds for the energy group. + + Returns + ------- + MicroXS, cross section data in [b] + + """ + groups = EnergyGroups(energy_bounds) + + # Set up the reaction tallies + original_tallies = model.tallies + tallies = Tallies() + xs = {} + for rxn in reactions: + if rxn == 'fission': + xs[rxn] = FissionXS(domain=reaction_domain, groups=groups, by_nuclide=True) + else: + xs[rxn] = ArbitraryXS(rxn, domain=reaction_domain, groups=groups, by_nuclide=True) + tallies += xs[rxn].tallies.values() + + model.tallies = tallies + + # create temporary run + original_dir = Path.cwd() + temp_dir = tempfile.TemporaryDirectory() + chdir(tempfile.gettempdir()) + statepoint_path = model.run() + chdir(original_dir) + + with StatePoint(statepoint_path) as sp: + for rxn in xs: + xs[rxn].load_from_statepoint(sp) + + # Build the DataFrame + micro_xs = cls() + for rxn in xs: + df = xs[rxn].get_pandas_dataframe(xs_type='micro') + df.index = df['nuclide'] + df.drop(['nuclide', xs[rxn].domain_type, 'group in', 'std. dev.'], axis=1, inplace=True) + df.rename({'mean':rxn}, axis=1, inplace=True) + micro_xs = concat([micro_xs, df], axis=1) + + micro_xs._units = 'b' + + # Revert to the original tallies + model.tallies = original_tallies + + return micro_xs + + @classmethod + def from_array(cls, nuclides, reactions, data, units='b'): + """ + Creates a ``MicroXS`` object from arrays. + + Parameters + ---------- + nuclides : list of str + List of nuclide symbols for that have data for at least one + reaction. + reactions : list of str + List of reactions. All reactions must match those in + :data:`openmc.deplete.chain.REACTIONS` + data : ndarray of floats + Array containing one-group microscopic cross section values for + each nuclide and reaction + units : {'b', 'cm^2'} + Units of the cross section values in ``data`` + + Returns + ------- + MicroXS + """ + + # Validate inputs + if data.shape != (len(nuclides), len(reactions)): + raise ValueError( + f'Nuclides list of length {len(nuclides)} and ' + f'reactions array of length {len(reactions)} do not ' + f'match dimensions of data array of shape {data.shape}') + + cls._validate_micro_xs_inputs( + nuclides, reactions, data) + micro_xs = cls(index=nuclides, columns=reactions, data=data) + micro_xs._units = units + + return micro_xs + + @classmethod + def from_csv(cls, csv_file, units='b', **kwargs): + """ + Load a ``MicroXS`` object from a ``.csv`` file. + + Parameters + ---------- + csv_file : str + Relative path to csv-file containing microscopic cross section + data. + units : {'b', 'cm^2'} + Units of the cross section values in the ``.csv`` file + **kwargs : dict + Keyword arguments to pass to :func:`pandas.read_csv()`. + + Returns + ------- + MicroXS + + """ + if 'float_precision' not in kwargs: + kwargs['float_precision'] = 'round_trip' + + micro_xs = cls(read_csv(csv_file, index_col=0, **kwargs)) + + cls._validate_micro_xs_inputs(list(micro_xs.index), + list(micro_xs.columns), + micro_xs.to_numpy()) + micro_xs._units = units + + return micro_xs + + @staticmethod + def _validate_micro_xs_inputs(nuclides, reactions, data): + check_iterable_type('nuclides', nuclides, str) + check_iterable_type('reactions', reactions, str) + check_type('data', data, ndarray, expected_iter_type=float) + for reaction in reactions: + check_value('reactions', reaction, _valid_rxns) diff --git a/openmc/deplete/openmc_operator.py b/openmc/deplete/openmc_operator.py index 7c0bb6678..7235ab9bf 100644 --- a/openmc/deplete/openmc_operator.py +++ b/openmc/deplete/openmc_operator.py @@ -1,6 +1,6 @@ """OpenMC transport operator -This module implements functions used by both OpenMC transport operators as well as pure depletion operators. +This module implements functions shared by both OpenMC transport operators as well as indepenedent depletion operators. """ @@ -75,8 +75,8 @@ class OpenMCOperator(TransportOperator): helper_kwargs : dict Keyword arguments for helper classes reduce_chain : bool, optional - If True, use :meth:`openmc.deplete.Chain.reduce` to reduce the - depletion chain up to ``reduce_chain_level``. Default is False. + If True, use :meth:`openmc.deplete.Chain.reduce()` to reduce the + depletion chain up to ``reduce_chain_level``. reduce_chain_level : int, optional Depth of the search when reducing the depletion chain. Only used if ``reduce_chain`` evaluates to true. The default value of @@ -87,7 +87,7 @@ class OpenMCOperator(TransportOperator): ---------- materials : openmc.Materials All materials present in the model - cross_sections : str or pandas.DataFrame + cross_sections : str or MicroXS Path to continuous energy cross section library, or object containing one-group cross-sections. dilute_initial : float diff --git a/openmc/deplete/operator.py b/openmc/deplete/operator.py index db4e80b19..53a1db3e9 100644 --- a/openmc/deplete/operator.py +++ b/openmc/deplete/operator.py @@ -131,7 +131,7 @@ class Operator(OpenMCOperator): .. versionadded:: 0.12.1 reduce_chain : bool, optional If True, use :meth:`openmc.deplete.Chain.reduce` to reduce the - depletion chain up to ``reduce_chain_level``. Default is False. + depletion chain up to ``reduce_chain_level``. .. versionadded:: 0.12 reduce_chain_level : int, optional @@ -226,7 +226,7 @@ class Operator(OpenMCOperator): self.cleanup_when_done = True if reaction_rate_opts is None: - reaction_rate_opts = {} + reaction_rate_opts = {} if fission_yield_opts is None: fission_yield_opts = {} helper_kwargs = { diff --git a/tests/regression_tests/deplete_no_transport/test.py b/tests/regression_tests/deplete_no_transport/test.py index 38d4aaf2e..95b842537 100644 --- a/tests/regression_tests/deplete_no_transport/test.py +++ b/tests/regression_tests/deplete_no_transport/test.py @@ -1,19 +1,12 @@ """ Transport-free depletion test suite """ -from math import floor -import shutil from pathlib import Path -from difflib import unified_diff import numpy as np import pytest import openmc -from openmc.data import JOULE_PER_EV import openmc.deplete -from openmc.deplete import FluxDepletionOperator - -from tests.regression_tests import config - +from openmc.deplete import IndependentOperator, MicroXS @pytest.fixture(scope="module") def fuel(): @@ -27,7 +20,6 @@ def fuel(): return fuel - @pytest.mark.parametrize("multiproc, from_nuclides, normalization_mode, power, flux", [ (True, True,'source-rate', None, 1164719970082145.0), (False, True, 'source-rate', None, 1164719970082145.0), @@ -46,7 +38,7 @@ def test_no_transport_from_nuclides(run_in_tmpdir, fuel, multiproc, from_nuclide """ # Create operator micro_xs_file = Path(__file__).parents[2] / 'micro_xs_simple.csv' - micro_xs = FluxDepletionOperator.create_micro_xs_from_csv(micro_xs_file) + micro_xs = MicroXS.from_csv(micro_xs_file) chain_file = Path(__file__).parents[2] / 'chain_simple.xml' if from_nuclides: @@ -54,11 +46,11 @@ def test_no_transport_from_nuclides(run_in_tmpdir, fuel, multiproc, from_nuclide for nuc, dens in fuel.get_nuclide_atom_densities().items(): nuclides[nuc] = dens - op = FluxDepletionOperator.from_nuclides( - fuel.volume, nuclides,'atom/b-cm', micro_xs, chain_file, normalization_mode=normalization_mode) + op = IndependentOperator.from_nuclides( + fuel.volume, nuclides, micro_xs, chain_file, normalization_mode=normalization_mode) else: - op = FluxDepletionOperator(openmc.Materials([fuel]), micro_xs, chain_file, normalization_mode=normalization_mode) + op = IndependentOperator(openmc.Materials([fuel]), micro_xs, chain_file, normalization_mode=normalization_mode) # Power and timesteps dt = [30] # single step diff --git a/tests/regression_tests/microxs/__init.py__ b/tests/regression_tests/microxs/__init.py__ new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/microxs/test.py b/tests/regression_tests/microxs/test.py new file mode 100644 index 000000000..bebe1c302 --- /dev/null +++ b/tests/regression_tests/microxs/test.py @@ -0,0 +1,52 @@ +"""Test one-group cross section generation""" +import numpy as np +import pytest +import openmc + +from openmc.deplete import MicroXS + +@pytest.fixture(scope="module") +def model(): + fuel = openmc.Material(name="uo2") + fuel.add_element("U", 1, percent_type="ao", enrichment=4.25) + fuel.add_element("O", 2) + fuel.set_density("g/cc", 10.4) + + clad = openmc.Material(name="clad") + clad.add_element("Zr", 1) + clad.set_density("g/cc", 6) + + water = openmc.Material(name="water") + water.add_element("O", 1) + water.add_element("H", 2) + water.set_density("g/cc", 1.0) + water.add_s_alpha_beta("c_H_in_H2O") + + radii = [0.42, 0.45] + fuel.volume = np.pi * radii[0] ** 2 + clad.volume = np.pi * (radii[1]**2 - radii[0]**2) + water.volume = 1.24**2 - (np.pi * radii[1]**2) + + materials = openmc.Materials([fuel, clad, water]) + + pin_surfaces = [openmc.ZCylinder(r=r) for r in radii] + pin_univ = openmc.model.pin(pin_surfaces, materials) + bound_box = openmc.rectangular_prism(1.24, 1.24, boundary_type="reflective") + root_cell = openmc.Cell(fill=pin_univ, region=bound_box) + geometry = openmc.Geometry([root_cell]) + + settings = openmc.Settings() + settings.particles = 1000 + settings.inactive = 10 + settings.batches = 50 + + return openmc.Model(geometry, materials, settings) + + +def test_from_model(model): + ref_xs = MicroXS.from_csv('test_reference.csv') + test_xs = MicroXS.from_model(model, model.materials[0]) + + assert ref_xs._units == test_xs._units + np.testing.assert_allclose(test_xs, ref_xs, rtol=1e-11) + diff --git a/tests/regression_tests/microxs/test_reference.csv b/tests/regression_tests/microxs/test_reference.csv new file mode 100644 index 000000000..0c47409b0 --- /dev/null +++ b/tests/regression_tests/microxs/test_reference.csv @@ -0,0 +1,7 @@ +nuclide,"(n,gamma)","(n,2n)","(n,p)","(n,a)","(n,3n)","(n,4n)",fission +U234,23.518634203050674,0.0008255330840536984,0.0,0.0,9.404554397521455e-07,0.0,0.49531930067650964 +U235,10.621118186344795,0.004359401013759254,0.0,0.0,7.2974901306692395e-06,0.0,49.10955932965902 +U238,0.8652742788116055,0.005661917442209096,0.0,0.0,4.92273921631416e-05,0.0,0.10579281644765708 +U236,9.095623870006163,0.0024373322002926834,0.0,0.0,1.966889146690413e-05,0.0,0.3231539233923791 +O16,7.511380881289377e-05,0.0,1.3764104470470622e-05,0.002862620940027927,0.0,0.0,0.0 +O17,0.00041221042693945085,1.9826700699084533e-05,7.764409141772239e-06,0.05938517754333328,0.0,0.0,0.0 diff --git a/tests/unit_tests/test_deplete_flux_operator.py b/tests/unit_tests/test_deplete_flux_operator.py deleted file mode 100644 index 8f17538ed..000000000 --- a/tests/unit_tests/test_deplete_flux_operator.py +++ /dev/null @@ -1,83 +0,0 @@ -"""Basic unit tests for openmc.deplete.FluxDepletionOperator instantiation - -Modifies and resets environment variable OPENMC_CROSS_SECTIONS -to a custom file with new depletion_chain node -""" - -from pathlib import Path - -import pytest -from openmc.deplete.flux_operator import FluxDepletionOperator -from openmc import Material, Materials -import pandas as pd -import numpy as np - -CHAIN_PATH = Path(__file__).parents[1] / "chain_simple.xml" -ONE_GROUP_XS = Path(__file__).parents[1] / "micro_xs_simple.csv" - - -def test_create_micro_xs_from_data_array(): - nuclides = [ - 'U234', - 'U235', - 'U238', - 'U236', - 'O16', - 'O17', - 'I135', - 'Xe135', - 'Xe136', - 'Cs135', - 'Gd157', - 'Gd156'] - reactions = ['fission', '(n,gamma)'] - # These values are placeholders and are not at all - # physically meaningful. - data = np.array([[0.1, 0.], - [0.1, 0.], - [0.9, 0.], - [0.4, 0.], - [0., 0.], - [0., 0.], - [0., 0.1], - [0., 0.9], - [0., 0.], - [0., 0.], - [0., 0.1], - [0., 0.1]]) - - FluxDepletionOperator.create_micro_xs_from_data_array( - nuclides, reactions, data) - with pytest.raises(ValueError, match=r'Nuclides list of length \d* and ' - r'reactions array of length \d* do not ' - r'match dimensions of data array of shape \(\d*\,d*\)'): - FluxDepletionOperator.create_micro_xs_from_data_array( - nuclides, reactions, data[:, 0]) - - -def test_create_micro_xs_from_csv(): - FluxDepletionOperator.create_micro_xs_from_csv(ONE_GROUP_XS) - - -def test_operator_init(): - """The test uses a temporary dummy chain. This file will be removed - at the end of the test, and only contains a depletion_chain node.""" - volume = 1 - nuclides = {'U234': 8.922411359424315e+18, - 'U235': 9.98240191860822e+20, - 'U238': 2.2192386373095893e+22, - 'U236': 4.5724195495061115e+18, - 'O16': 4.639065406771322e+22, - 'O17': 1.7588724018066158e+19} - micro_xs = FluxDepletionOperator.create_micro_xs_from_csv(ONE_GROUP_XS) - nuclide_flux_operator = FluxDepletionOperator.from_nuclides( - volume, nuclides, 'atom/cm3', micro_xs, CHAIN_PATH) - - fuel = Material(name="uo2") - fuel.add_element("U", 1, percent_type="ao", enrichment=4.25) - fuel.add_element("O", 2) - fuel.set_density("g/cc", 10.4) - fuel.depletable=True - fuel.volume = 1 - materials = Materials([fuel]) - nuclide_flux_operator = FluxDepletionOperator(materials, micro_xs, CHAIN_PATH) diff --git a/tests/unit_tests/test_deplete_independent_operator.py b/tests/unit_tests/test_deplete_independent_operator.py new file mode 100644 index 000000000..f6cc7e200 --- /dev/null +++ b/tests/unit_tests/test_deplete_independent_operator.py @@ -0,0 +1,36 @@ +"""Basic unit tests for openmc.deplete.IndependentOperator instantiation + +""" + +from pathlib import Path + +import pytest +from openmc.deplete import IndependentOperator, MicroXS +from openmc import Material, Materials +import numpy as np + +CHAIN_PATH = Path(__file__).parents[1] / "chain_simple.xml" +ONE_GROUP_XS = Path(__file__).parents[1] / "micro_xs_simple.csv" + +def test_operator_init(): + """The test uses a temporary dummy chain. This file will be removed + at the end of the test, and only contains a depletion_chain node.""" + volume = 1 + nuclides = {'U234': 8.922411359424315e+18, + 'U235': 9.98240191860822e+20, + 'U238': 2.2192386373095893e+22, + 'U236': 4.5724195495061115e+18, + 'O16': 4.639065406771322e+22, + 'O17': 1.7588724018066158e+19} + micro_xs = MicroXS.from_csv(ONE_GROUP_XS) + IndependentOperator.from_nuclides( + volume, nuclides, micro_xs, CHAIN_PATH, nuc_units='atom/cm3') + + fuel = Material(name="uo2") + fuel.add_element("U", 1, percent_type="ao", enrichment=4.25) + fuel.add_element("O", 2) + fuel.set_density("g/cc", 10.4) + fuel.depletable=True + fuel.volume = 1 + materials = Materials([fuel]) + IndependentOperator(materials, micro_xs, CHAIN_PATH) diff --git a/tests/unit_tests/test_deplete_microxs.py b/tests/unit_tests/test_deplete_microxs.py new file mode 100644 index 000000000..17d9cb38f --- /dev/null +++ b/tests/unit_tests/test_deplete_microxs.py @@ -0,0 +1,60 @@ +"""Basic unit tests for openmc.deplete.IndependentOperator instantiation + +Modifies and resets environment variable OPENMC_CROSS_SECTIONS +to a custom file with new depletion_chain node +""" + +from os import remove +from pathlib import Path + +import pytest +from openmc.deplete import MicroXS +import numpy as np + +ONE_GROUP_XS = Path(__file__).parents[1] / "micro_xs_simple.csv" + + +def test_from_array(): + nuclides = [ + 'U234', + 'U235', + 'U238', + 'U236', + 'O16', + 'O17', + 'I135', + 'Xe135', + 'Xe136', + 'Cs135', + 'Gd157', + 'Gd156'] + reactions = ['fission', '(n,gamma)'] + # These values are placeholders and are not at all + # physically meaningful. + data = np.array([[0.1, 0.], + [0.1, 0.], + [0.9, 0.], + [0.4, 0.], + [0., 0.], + [0., 0.], + [0., 0.1], + [0., 0.9], + [0., 0.], + [0., 0.], + [0., 0.1], + [0., 0.1]]) + + MicroXS.from_array(nuclides, reactions, data) + with pytest.raises(ValueError, match=r'Nuclides list of length \d* and ' + r'reactions array of length \d* do not ' + r'match dimensions of data array of shape \(\d*\,d*\)'): + MicroXS.from_array(nuclides, reactions, data[:, 0]) + +def test_csv(): + ref_xs = MicroXS.from_csv(ONE_GROUP_XS) + ref_xs.to_csv('temp_xs.csv') + temp_xs = MicroXS.from_csv('temp_xs.csv') + assert ref_xs._units == temp_xs._units + assert np.all(ref_xs == temp_xs) + remove('temp_xs.csv') + From ea1e2d02c42465413341eb841f9ab5e3fa559260 Mon Sep 17 00:00:00 2001 From: yardasol Date: Fri, 29 Jul 2022 22:59:47 -0500 Subject: [PATCH 100/131] Adjust docpages to accomodate new transport-independent depletion scheme. --- docs/source/methods/depletion.rst | 81 ++++---- docs/source/pythonapi/deplete.rst | 27 +-- docs/source/usersguide/depletion.rst | 284 ++++++++++++++++++--------- 3 files changed, 248 insertions(+), 144 deletions(-) diff --git a/docs/source/methods/depletion.rst b/docs/source/methods/depletion.rst index dce1f2503..0e06b3a50 100644 --- a/docs/source/methods/depletion.rst +++ b/docs/source/methods/depletion.rst @@ -103,16 +103,16 @@ integrate over the entire timestep. Our aim here is not to exhaustively describe all integration methods but rather to give a few examples that elucidate the main considerations one must take into account when choosing a method. Generally, there is a tradeoff between the -accuracy of the method and its computational expense. The expense is driven -almost entirely by the time to compute a transport solution, i.e., to evaluate -:math:`\mathbf{A}` for a given :math:`\mathbf{n}`. Thus, the cost of a method -scales with the number of :math:`\mathbf{A}` evaluations that are performed per -timestep. On the other hand, methods that require more evaluations generally -achieve higher accuracy. The predictor method only requires one evaluation and -its error converges as :math:`\mathcal{O}(h)`. The CE/CM method requires two -evaluations and is thus twice as expensive as the predictor method, but achieves -an error of :math:`\mathcal{O}(h^2)`. An exhaustive description of time -integration methods and their merits can be found in the `thesis of Colin Josey +accuracy of the method and its computational expense. In the case of +transport-coupled depletion, the expense is driven almost entirely by the time +to compute a transport solution, i.e., to evaluate :math:`\mathbf{A}` for a +given :math:`\mathbf{n}`. Thus, the cost of a method scales with the number of +:math:`\mathbf{A}` evaluations that are performed per timestep. On the other +hand, methods that require more evaluations generally achieve higher accuracy. The predictor method only requires one evaluation and its error converges as +:math:`\mathcal{O}(h)`. The CE/CM method requires two evaluations and is thus +twice as expensive as the predictor method, but achieves an error of +:math:`\mathcal{O}(h^2)`. An exhaustive description of time integration methods +and their merits can be found in the `thesis of Colin Josey `_. OpenMC does not rely on a single time integration method but rather has several @@ -169,12 +169,14 @@ Data Considerations In principle, solving Eq. :eq:`depletion-matrix` using CRAM is fairly simple: just construct the burnup matrix at various times and solve a set of sparse -linear systems. However, constructing the burnup matrix itself involves not only -solving the transport equation to estimate transmutation reaction rates but also -a series of choices about what data to include. In OpenMC, the burnup matrix is -constructed based on data inside of a *depletion chain* file, which includes -fundamental data gathered from ENDF incident neutron, decay, and fission product -yield sublibraries. For each nuclide, this file includes: +linear systems. However, constructing the burnup matrix itself involves not +only solving the transport equation to estimate transmutation reaction rates +(in the case of transport-coupled depletion) or to obtain microscopic cross +sections (in the case of transport-independent depletion), but also a series of +choices about what data to include. In OpenMC, the burnup matrix is constructed +based on data inside of a *depletion chain* file, which includes fundamental +data gathered from ENDF incident neutron, decay, and fission product yield +sublibraries. For each nuclide, this file includes: - What transmutation reactions are possible, their Q values, and their products; - If a nuclide is not stable, what decay modes are possible, their branching @@ -185,9 +187,12 @@ yield sublibraries. For each nuclide, this file includes: Transmutation Reactions ----------------------- -OpenMC will setup tallies in a problem based on what transmutation reactions are -available in a depletion chain file, so any arbitrary number of transmutation -reactions can be tracked. The pregenerated chain files that are available on +In transport-coupled depletion, OpenMC will setup tallies in a problem based on +what transmutation reactions are available in a depletion chain file, so any +arbitrary number of transmutation reactions can be tracked. In +transport-independent depletion, OpenMC will calculate reaction rates for every +reaction that is present in both the available cross sections and the depletion +chain file. The pregenerated chain files that are available on https://openmc.org include the following transmutation reactions: fission, (n,\ :math:`\gamma`\ ), (n,2n), (n,3n), (n,4n), (n,p), and (n,\ :math:`\alpha`\ ). @@ -202,11 +207,12 @@ accurately model the branching of the capture reaction in Am241. This is complicated by the fact that the branching ratio may depend on the incident neutron energy causing capture. -OpenMC does not currently allow energy-dependent capture branching ratios. -However, the depletion chain file does allow a transmutation reaction to be -listed multiple times with different branching ratios resulting in different -products. Spectrum-averaged capture branching ratios have been computed in LWR -and SFR spectra and are available at https://openmc.org/depletion-chains. +OpenMC's transport solver does not currently allow energy-dependent capture +branching ratios. However, the depletion chain file does allow a transmutation +reaction to be listed multiple times with different branching ratios resulting +in different products. Spectrum-averaged capture branching ratios have been +computed in LWR and SFR spectra and are available at +https://openmc.org/depletion-chains. Fission Product Yields ---------------------- @@ -217,26 +223,31 @@ energies. It is an open question as to what the best way to handle this energy dependence is. OpenMC includes three methods for treating the energy dependence of FPY: -1. Use FPY data corresponding to a specified energy. +1. Use FPY data corresponding to a specified energy. This is used by default in + both transport-coupled and transport-independent depletion. 2. Tally fission rates above and below a specified cutoff energy. Assume that all fissions below the cutoff energy correspond to thermal FPY data and all - fission above the cutoff energy correspond to fast FPY data. + fission above the cutoff energy correspond to fast FPY data. Only applicable + to transport-coupled depletion. 3. Compute the average energy at which fission events occur and use an effective FPY by linearly interpolating between FPY provided at neighboring energies. + Only applicable to transport-coupled depletion -The method can be selected through the ``fission_yield_mode`` argument to the -:class:`openmc.deplete.Operator` constructor. +The method for transport-coupled depletion can be selected through the +``fission_yield_mode`` argument to the :class:`openmc.deplete.Operator` +constructor. Power Normalization ------------------- -The reaction rates provided OpenMC are given in units of reactions per source -particle. For depletion, it is necessary to compute an absolute reaction rate in -reactions per second. To do so, the reaction rates are normalized based on a -specified power. A complete description of how this normalization can be -performed is described in :ref:`usersguide_tally_normalization`. Here, we simply -note that the main depletion class, :class:`openmc.deplete.Operator`, allows the -user to choose one of two methods for estimating the heating rate, including: +In transport-coupled depletion, the reaction rates provided OpenMC are given in +units of reactions per source particle. For depletion, it is necessary to +compute an absolute reaction rate in reactions per second. To do so, the +reaction rates are normalized based on a specified power. A complete +description of how this normalization can be performed is described in +:ref:`usersguide_tally_normalization`. Here, we simply note that the main +depletion class, :class:`openmc.deplete.Operator`, allows the user to choose +one of two methods for estimating the heating rate, including: 1. Using fixed Q values from a depletion chain file (useful for comparisons to other codes that use fixed Q values), or diff --git a/docs/source/pythonapi/deplete.rst b/docs/source/pythonapi/deplete.rst index 246cea59f..cc36a17c7 100644 --- a/docs/source/pythonapi/deplete.rst +++ b/docs/source/pythonapi/deplete.rst @@ -12,19 +12,20 @@ Primary API The two primary requirements to perform depletion with :mod:`openmc.deplete` are: - 1) A transport operator + 1) A reaction rate operator 2) A time-integration scheme -The former is responsible for executing a transport code, like OpenMC, -and retaining important information required for depletion. The most common examples -are reaction rates and power normalization data. The latter is responsible for -projecting reaction rates and compositions forward in calendar time across -some step size :math:`\Delta t`, and obtaining new compositions given a power -or power density. The :class:`Operator` is provided to handle communicating with -OpenMC. Several classes are provided that implement different time-integration -algorithms for depletion calculations, which are described in detail in Colin -Josey's thesis, `Development and analysis of high order neutron -transport-depletion coupling algorithms `_. +The former is responsible for obtaining transmuation reaction rates. The latter +is responsible for projecting reaction rates and compositions forward in +calendar time across some step size :math:`\Delta t`, and obtaining new +compositions given a power or power density. The :class:`Operator` class is +provided to obtain reaction rates via tallies through OpenMC's transport +solver, and the :class:`IndependentOperator` class is provided to obtain +reaction rates from cross-section data. Several classes are provided that +implement different time-integration algorithms for depletion calculations, +which are described in detail in Colin Josey's thesis, `Development and +analysis of high order neutron transport-depletion coupling algorithms +`_. .. autosummary:: :toctree: generated @@ -40,8 +41,8 @@ transport-depletion coupling algorithms `_. SICELIIntegrator SILEQIIntegrator -Each of these classes expects a "transport operator" to be passed. Operators -specific to OpenMC are available using the following classes: +Each of these classes expects a "reaction rate operator" to be passed. Operators +provided by OpenMC are available using the following classes: .. autosummary:: :toctree: generated diff --git a/docs/source/usersguide/depletion.rst b/docs/source/usersguide/depletion.rst index 0bc2a5af9..7409c6610 100644 --- a/docs/source/usersguide/depletion.rst +++ b/docs/source/usersguide/depletion.rst @@ -4,23 +4,55 @@ Depletion and Transmutation =========================== -OpenMC supports coupled depletion, or burnup, calculations through the -:mod:`openmc.deplete` Python module. OpenMC solves the transport equation to -obtain transmutation reaction rates, and then the reaction rates are used to -solve a set of transmutation equations that determine the evolution of nuclide -densities within a material. The nuclide densities predicted as some future time -are then used to determine updated reaction rates, and the process is repeated -for as many timesteps as are requested. +OpenMC supports transport-coupled and transport-independent depletion, or +burnup, calculations through the :mod:`openmc.deplete` Python module. OpenMC +uses transmutation reaction rates to solve a set of transmutation equations +that determine the evolution of nuclide densities within a material. The +nuclide densities predicted as some future time are then used to determine +updated reaction rates, and the process is repeated for as many timesteps as +are requested. -The depletion module is designed such that the flux/reaction rate solution (the -transport "operator") is completely isolated from the solution of the -transmutation equations and the method used for advancing time. At present, the -:mod:`openmc.deplete` module offers a single transport operator, -:class:`openmc.deplete.Operator` (which uses the OpenMC transport solver), but -in principle additional operator classes based on other transport codes could be -implemented and no changes to the depletion solver itself would be needed. The -operator class requires a :class:`~openmc.Model` instance containing -material, geometry, and settings information:: +The depletion module is designed such that the reaction rate solution (the +"operator") is completely isolated from the solution of the transmutation +equations and the method used for advancing time. + +:mod:`openmc.deplete` supports multiple time-integration methods for determining +material compositions over time. Each method appears as a different class. +For example, :class:`openmc.deplete.CECMIntegrator` runs a depletion calculation +using the CE/CM algorithm (deplete over a timestep using the middle-of-step +reaction rates). An instance of :class:`~openmc.deplete.abc.TransportOperator` +is passed to one of these functions along with the timesteps and power level:: + + power = 1200.0e6 # watts + timesteps = [10.0, 10.0, 10.0] # days + openmc.deplete.CECMIntegrator(op, timesteps, power, timestep_units='d').integrate() + +The depletion problem is executed, and once it is done a +``depletion_results.h5`` file is written. The results can be analyzed using the +:class:`openmc.deplete.Results` class. This class has methods that allow for +easy retrieval of k-effective, nuclide concentrations, and reaction rates over +time:: + + results = openmc.deplete.Results("depletion_results.h5") + time, keff = results.get_keff() + +Note that the coupling between the reaction rate solver and the transmutation +solver happens in-memory rather than by reading/writing files on disk. OpenMC has two categories of +operators for obtaining transmutation reaction rates. + +.. _coupled-depletion: + +Transport-coupled depletion +=========================== + +This category of operator solves the transport equation to obtain transmutation +reaction rates. At present, the :mod:`openmc.deplete` module offers a single +transport-coupled operator, :class:`openmc.deplete.Operator` (which uses the +OpenMC transport solver), but in principle additional transport-coupled operator +classes based on other transport codes could be implemented and no changes to +the depletion solver itself would be needed. The +:class:`openmc.deplete.Operator` class requires a :class:`~openmc.Model` +instance containing material, geometry, and settings information:: model = openmc.Model() ... @@ -30,36 +62,14 @@ material, geometry, and settings information:: Any material that contains a fissionable nuclide is depleted by default, but this can behavior can be changed with the :attr:`Material.depletable` attribute. -.. important:: The volume must be specified for each material that is depleted by - setting the :attr:`Material.volume` attribute. This is necessary - in order to calculate the proper normalization of tally results - based on the source rate. - -:mod:`openmc.deplete` supports multiple time-integration methods for determining -material compositions over time. Each method appears as a different class. -For example, :class:`openmc.deplete.CECMIntegrator` runs a depletion calculation -using the CE/CM algorithm (deplete over a timestep using the middle-of-step -reaction rates). An instance of :class:`openmc.deplete.Operator` is passed to -one of these functions along with the timesteps and power level:: - - power = 1200.0e6 # watts - timesteps = [10.0, 10.0, 10.0] # days - openmc.deplete.CECMIntegrator(op, timesteps, power, timestep_units='d').integrate() - -The coupled transport-depletion problem is executed, and once it is done a -``depletion_results.h5`` file is written. The results can be analyzed using the -:class:`openmc.deplete.Results` class. This class has methods that allow for -easy retrieval of k-effective, nuclide concentrations, and reaction rates over -time:: - - results = openmc.deplete.Results("depletion_results.h5") - time, keff = results.get_keff() - -Note that the coupling between the transport solver and the transmutation solver -happens in-memory rather than by reading/writing files on disk. +.. important:: + + The volume must be specified for each material that is depleted by setting + the :attr:`Material.volume` attribute. This is necessary in order to + calculate the proper normalization of tally results based on the source rate. Fixed-Source Transmutation -========================== +-------------------------- When the ``power`` or ``power_density`` argument is used for one of the Integrator classes, it is assumed that OpenMC is running in k-eigenvalue mode, @@ -91,10 +101,12 @@ timestep in the calculation. A zero source rate for a given timestep will result in a decay-only step, where all reaction rates are zero. Caveats -======= +------- + +.. _energy-deposition: Energy Deposition ------------------ +~~~~~~~~~~~~~~~~~ The default energy deposition mode, ``"fission-q"``, instructs the :class:`~openmc.deplete.Operator` to normalize reaction rates using the product @@ -126,7 +138,7 @@ should be, including indirect components. Some examples are provided below:: A more complete way to model the energy deposition is to use the modified -heating reactions described in :ref:`methods_heating`. These values can be used +heating reactions described in :ref:`methods_heating`. These values can be used to normalize reaction rates instead of using the fission reaction rates with:: op = openmc.deplete.Operator(model, "chain.xml", @@ -137,7 +149,7 @@ of :meth:`openmc.data.IncidentNeutron.from_njoy()`, and will eventually be bundl into the distributed libraries. Local Spectra and Repeated Materials ------------------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ It is not uncommon to explicitly create a single burnable material across many locations. From a pure transport perspective, there is nothing wrong with @@ -184,42 +196,81 @@ Transport-independent depletion .. note:: - This feature is still under heavy development. API changes are - possible and likely in the near future. + This feature is still under heavy development and has yet to be verifed + code-to-code . API changes and feature additions are possible and likely in + the near future. -OpenMC supports running depletion calculations independent of the OpenMC -transport solver using the :class:`~openmc.deplete.IndependentOperator` class. -This class supports both constant-flux (``source-rate`` normalization) and -constant-power depletion (``fission-q`` normalization). +This category of operator uses pre-calculated one-group microscopic cross +sections to obtain transmutation reaction rates. OpenMC provides the +:class:`~openmc.deplete.IndependentOperator` for this method of calculation. +While the one-group microscopic cross sections can be calculated using a +transport solver, :class:`~openmc.deplete.IndependentOperator` is not directly +coupled to any transport solver. The +:class:`~openmc.deplete.IndependentOperator` class requires a +:class:`openmc.Materials` object, a :class:`~openmc.deplete.MicroXS` object, +and a path to a depletion chain file:: -.. important:: + # load in the microscopic cross sections + materials = openmc.Materials() + ... - Make sure you set the correct parameter in the :class:`openmc.abc.Integrator` - class. Use the ``source_rates`` parameter when - ``normalization_mode == source-rate``, and use ``power`` or ``power_density`` - when ``normalization_mode == fission-q``. + micro_xs = openmc.deplete.MicroXS() + ... -.. warning:: + op = IndependentOperator(materials, micro_xs, chain_file) - The accuracy of results when using ``fission-q`` is entirely dependent on - your depletion chain. Make sure it has sufficient data to resolve the - dynamics of your particular scenario. +.. note:: -:class:`~openmc.deplete.IndependentOperator` class uses one-group microscopic cross sections to calculate reaction -rates. Users can generate one-group microscopic cross sections using the + The same statements from :ref:`coupled-depletion` about which + materials are depleted and the requirement for depletable materials to have + a specified volume also apply here. + +An alternate constructor, +:meth:`~openmc.deplete.IndependentOperator.from_nuclides`, accepts a volume and +dictionary of nuclide concentrations in place of the :class:`openmc.Materials` +object:: + + nuclides = {'U234': 8.92e18, + 'U235': 9.98e20, + 'U238': 2.22e22, + 'U236': 4.57e18, + 'O16': 4.64e22, + 'O17': 1.76e19} + volume = 0.5 + op = openmc.deplete.IndependentOperator.from_nuclides(volume, + nuclides, + micro_xs, + chain_file, + nuc_units='atom/cm3') + +A user can then define an integrator class as they would for a coupled +transport-depletion calculation and follow the same steps from there. + +.. note:: + + Ideally, one-group cross section data should be available for every + reaction in the depletion chain. If a nuclide that has a reaction + associated with it in the depletion chain is present in the `nuclides` + parameter but not the cross section data, that reaction will not be + simulated. + +Generating Microscopic Cross Sections +------------------------------------- + +Users can generate the one-group microscopic cross sections needed by +:class:`~openmc.deplete.IndependentOperator` using the :class:`~openmc.deplete.MicroXS` class:: import openmc - from openmc.deplete import MicroXS model = openmc.Model.from_xml() - micro_xs = MicroXS.from_model(model, model.materials[0]) + micro_xs = openmc.deplete.MicroXS.from_model(model, model.materials[0]) - micro_xs.to_csv(micro_xs_path) - -:class:`~openmc.deplete.MicroXS` also includes functions to read in cross -section data directly from a ``.csv`` file or from data arrays:: +The :meth:`~openmc.deplete.MicroXS.from_model()` method will produce a +:class:`~openmc.deplete.MicroXS` object with microscopic cross section data in +units of ``b``, which is what :class:`~openmc.deplete.IndependentOperator` +expects the units to be. The :class:`~openmc.deplete.MicroXS` class also includes functions to read in cross section data directly from a ``.csv`` file or from data arrays:: micro_xs = MicroXS.from_csv(micro_xs_path) @@ -230,32 +281,73 @@ section data directly from a ``.csv`` file or from data arrays:: [0.01, 0.5]]) micro_xs = MicroXS.from_array(nuclides, reactions, data) -:class:`~openmc.deplete.IndependentOperator` has two ways to initialize it: -the default constructor accepts an :class:`openmc.Materials` object and -one-group microscopic cross sections as a :class:`~openmc.deplete.MicroXS` -object, while the :meth:`~openmc.deplete.IndependentOperator.from_nuclides` -method accepts a volume and dictionary of nuclide concentrations in place of the -:class:`openmc.Materials` object in addition to the other parameters:: +.. important :: - # load in the microscopic cross sections - op = IndependentOperator(materials, micro_xs, chain_file) + Both :meth:`~openmc.deplete.MicroXS.from_csv()` and + :meth:`~openmc.deplete.MicroXS.from_array()` assume the cross section values + provided are in barns by defualt, but have no way of verifying this. Make + sure your cross sections are in the correct units before passing to a + :class:`~openmc.deplete.IndependentOperator` object. - # alternate construtor - nuclides = {'U234': 8.92e18, - 'U235': 9.98e20, - 'U238': 2.22e22, - 'U236': 4.57e18, - 'O16': 4.64e22, - 'O17': 1.76e19} - volume = 0.5 - op = IndependentOperator.from_nuclides(volume, nuclides, micro_xs, - chain_file, nuc_units='atom/cm3') +Caveats +------- -A user can then define an integrator class as they would for a coupled -transport-depletion calculation and follow the same steps from there. +Reaction Rate Normalization +~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. note:: Ideally, one-group cross section data should be available for every - reaction in the depletion chain. If a nuclide that has a reaction - associated with it in the depletion chain is present in the `nuclides` - parameter but not the cross section data, that reaction will not be - simulated. +The :class:`~openmc.deplete.IndependentOperator` class supports two methods for +normalizing reaction rates: + +.. important:: + + Make sure you set the correct parameter in the :class:`openmc.abc.Integrator` + class. Use the ``source_rates`` parameter when + ``normalization_mode == source-rate``, and use ``power`` or ``power_density`` + when ``normalization_mode == fission-q``. + +1. ``soure-rate`` normalization, which assumes the ``source-rate`` provided by + the time integrator is a flux, and obtains the reaction rates by multiplying + the cross-sections by the ``source-rate``. +2. ``fission-q`` normalization, which assumes the ``source-rate`` provided by + the time integrator is a power, and obtains the reaction rates by computing a + value for the flux based on this power. The general equation for the flux is + + .. math:: + + \phi = \frac{P}{V \cdot \sum_i (Q_i \cdot \Sigma^f_i \cdot \rho_i)} + + where :math:`\sum_i` is the sum over all nuclides :math:`i`. This equation + makes the same assumptions and issues as discussed in + :ref:`energy-deposition`. Unfortunately, the proposed solution in that + section does not apply here since we are decoupled from transport code. + However, there is a method to converge to a more accurate value for flux by + using substeps during time integration. + `This paper `_ provides a + good discussion of this method. Hopefully such a method will be implemented + in OpenMC in the near future. + +.. warning:: + + The accuracy of results when using ``fission-q`` is entirely dependent on + your depletion chain. Make sure it has sufficient data to resolve the + dynamics of your particular scenario. + +Multiple Materials +~~~~~~~~~~~~~~~~~~ + +Running a depletion simulation with multiple materials using the +``source-rate`` normalization method treats each material as completely +separate with respect to reaction rates. This can be useful for running many +different cases of a particular scenario. However, running a depletion +simulation with multiple materials using the ``fission-q`` normalization method +treats each material as part of the same "reactor" due to how ``fission-q`` +normalization conglomerates energy values from each material to a single value. +This behavior may change in the future. + +Time integration +~~~~~~~~~~~~~~~~ + +The one-group microscopic cross sections passed to +:class:`openmc.deplete.IndependentOperator` are fixed values for the entire +depletion simulation. This implicit assumption may produce inaccurate results +for certain scenarios. From 511464b53b590549b053b386ac336b47ad3ca1a6 Mon Sep 17 00:00:00 2001 From: yardasol Date: Sat, 30 Jul 2022 15:39:47 -0500 Subject: [PATCH 101/131] adjust top-level module descriptions --- openmc/deplete/abc.py | 9 ++++----- openmc/deplete/helpers.py | 2 +- openmc/deplete/independent_operator.py | 2 +- openmc/deplete/operator.py | 10 +++++----- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index cdbd1361a..5cb3b254a 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -1,7 +1,6 @@ -"""function module. +"""abc module. -This module contains the Operator class, which is then passed to an integrator -to run a full depletion simulation. +This module contains Abstract Base Classes for implementing operator, integrator, depletion system solver, and operator helper classes """ from abc import ABC, abstractmethod @@ -864,8 +863,8 @@ class SIIntegrator(Integrator): initial heavy metal inventory to get total power if ``power`` is not specified. source_rates : float or iterable of float, optional - Source rate in [neutron/sec] or neutron flux in [neutron/s-cm^2] for each - interval in :attr:`timesteps` + Source rate in [neutron/sec] or neutron flux in [neutron/s-cm^2] for + each interval in :attr:`timesteps` .. versionadded:: 0.12.1 timestep_units : {'s', 'min', 'h', 'd', 'MWd/kg'} diff --git a/openmc/deplete/helpers.py b/openmc/deplete/helpers.py index 7b863b69a..4fcde4f6b 100644 --- a/openmc/deplete/helpers.py +++ b/openmc/deplete/helpers.py @@ -1,5 +1,5 @@ """ -Class for normalizing fission energy deposition +Classes for collecting and calculating quantities for reaction rate operators """ import bisect from abc import abstractmethod diff --git a/openmc/deplete/independent_operator.py b/openmc/deplete/independent_operator.py index 98ef6656d..a7eb54f57 100644 --- a/openmc/deplete/independent_operator.py +++ b/openmc/deplete/independent_operator.py @@ -1,4 +1,4 @@ -"""Independent depletion operator +"""Transport-independent operator for depletion. This module implements a depletion operator that runs independently of any transport solver by using user-provided one-group cross sections. diff --git a/openmc/deplete/operator.py b/openmc/deplete/operator.py index 53a1db3e9..2f8dcfba2 100644 --- a/openmc/deplete/operator.py +++ b/openmc/deplete/operator.py @@ -1,9 +1,9 @@ -"""OpenMC transport operator +"""Transport-coupled operator for depletion. -This module implements a transport operator for OpenMC so that it can be used by -depletion integrators. The implementation makes use of the Python bindings to -OpenMC's C API so that reading tally results and updating material number -densities is all done in-memory instead of through the filesystem. +This module implements an operator coupled to OpenMC's transport sovler so that +it can be used by depletion integrators. The implementation makes use of the Python bindings to OpenMC's C API so that reading tally results and updating +material number densities is all done in-memory instead of through the +filesystem. """ From 773757246bbbd6a680a6bc1648c1ae7d2cbc88ed Mon Sep 17 00:00:00 2001 From: yardasol Date: Sat, 30 Jul 2022 16:46:31 -0500 Subject: [PATCH 102/131] Operator->CoupledOperator - update references in docstrings and docpages - minor adjustments to related docs in deplete module files - retain backwards compatiblity by exporint Operator alias --- docs/source/methods/depletion.rst | 8 ++--- docs/source/pythonapi/deplete.rst | 16 +++++----- docs/source/usersguide/depletion.rst | 43 +++++++++++++------------- openmc/deplete/abc.py | 46 +++++++++++++++------------- openmc/deplete/chain.py | 3 +- openmc/deplete/helpers.py | 25 ++++++++------- openmc/deplete/operator.py | 27 ++++++++-------- openmc/deplete/results.py | 10 +++--- 8 files changed, 96 insertions(+), 82 deletions(-) diff --git a/docs/source/methods/depletion.rst b/docs/source/methods/depletion.rst index 0e06b3a50..66ee50397 100644 --- a/docs/source/methods/depletion.rst +++ b/docs/source/methods/depletion.rst @@ -234,7 +234,7 @@ of FPY: Only applicable to transport-coupled depletion The method for transport-coupled depletion can be selected through the -``fission_yield_mode`` argument to the :class:`openmc.deplete.Operator` +``fission_yield_mode`` argument to the :class:`openmc.deplete.CoupledOperator` constructor. Power Normalization @@ -246,8 +246,8 @@ compute an absolute reaction rate in reactions per second. To do so, the reaction rates are normalized based on a specified power. A complete description of how this normalization can be performed is described in :ref:`usersguide_tally_normalization`. Here, we simply note that the main -depletion class, :class:`openmc.deplete.Operator`, allows the user to choose -one of two methods for estimating the heating rate, including: +depletion class, :class:`openmc.deplete.CoupledOperator`, allows the user to +choose one of two methods for estimating the heating rate, including: 1. Using fixed Q values from a depletion chain file (useful for comparisons to other codes that use fixed Q values), or @@ -255,4 +255,4 @@ one of two methods for estimating the heating rate, including: energy-dependent estimate of the true heating rate. The method for normalization can be chosen through the ``normalization_mode`` -argument to the :class:`openmc.deplete.Operator` class. +argument to the :class:`openmc.deplete.CoupledOperator` class. diff --git a/docs/source/pythonapi/deplete.rst b/docs/source/pythonapi/deplete.rst index cc36a17c7..bc07ed1b0 100644 --- a/docs/source/pythonapi/deplete.rst +++ b/docs/source/pythonapi/deplete.rst @@ -18,8 +18,8 @@ are: The former is responsible for obtaining transmuation reaction rates. The latter is responsible for projecting reaction rates and compositions forward in calendar time across some step size :math:`\Delta t`, and obtaining new -compositions given a power or power density. The :class:`Operator` class is -provided to obtain reaction rates via tallies through OpenMC's transport +compositions given a power or power density. The :class:`CoupledOperator` class +is provided to obtain reaction rates via tallies through OpenMC's transport solver, and the :class:`IndependentOperator` class is provided to obtain reaction rates from cross-section data. Several classes are provided that implement different time-integration algorithms for depletion calculations, @@ -49,11 +49,11 @@ provided by OpenMC are available using the following classes: :nosignatures: :template: mycallable.rst - Operator + CoupledOperator IndependentOperator -The :class:`Operator` and :class:`IndependentOperator` classes must also have -some knowledge of how nuclides transmute and decay. This is handled by the +The :class:`CoupledOperator` and :class:`IndependentOperator` classes must also +have some knowledge of how nuclides transmute and decay. This is handled by the :class:`Chain`. Minimal Example @@ -71,7 +71,7 @@ A minimal example for performing depletion would be: # Representation of a depletion chain >>> chain_file = "chain_casl.xml" - >>> operator = openmc.deplete.Operator( + >>> operator = openmc.deplete.CoupledOperator( ... model, chain_file) # Set up 5 time steps of one day each @@ -177,7 +177,7 @@ with :func:`cram.CRAM48` being the default. :class:`multiprocessing.pool.Pool` class. If set to ``None`` (default), the number returned by :func:`os.cpu_count` is used. -The following classes are used to help the :class:`openmc.deplete.Operator` +The following classes are used to help the :class:`openmc.deplete.CoupledOperator` compute quantities like effective fission yields, reaction rates, and total system energy. @@ -194,6 +194,8 @@ total system energy. helpers.FissionYieldCutoffHelper helpers.FluxCollapseHelper +The :class:`openmc.deplete.IndependentOperator` uses inner class subclassed from +those listed to perform similar calculations. Intermediate Classes -------------------- diff --git a/docs/source/usersguide/depletion.rst b/docs/source/usersguide/depletion.rst index 7409c6610..990031c40 100644 --- a/docs/source/usersguide/depletion.rst +++ b/docs/source/usersguide/depletion.rst @@ -47,17 +47,17 @@ Transport-coupled depletion This category of operator solves the transport equation to obtain transmutation reaction rates. At present, the :mod:`openmc.deplete` module offers a single -transport-coupled operator, :class:`openmc.deplete.Operator` (which uses the -OpenMC transport solver), but in principle additional transport-coupled operator -classes based on other transport codes could be implemented and no changes to -the depletion solver itself would be needed. The -:class:`openmc.deplete.Operator` class requires a :class:`~openmc.Model` +transport-coupled operator, :class:`openmc.deplete.CoupledOperator` (which uses +the OpenMC transport solver), but in principle additional transport-coupled +operator classes based on other transport codes could be implemented and no +changes to the depletion solver itself would be needed. The +:class:`openmc.deplete.CoupledOperator` class requires a :class:`~openmc.Model` instance containing material, geometry, and settings information:: model = openmc.Model() ... - op = openmc.deplete.Operator(model) + op = openmc.deplete.CoupledOperator(model) Any material that contains a fissionable nuclide is depleted by default, but this can behavior can be changed with the :attr:`Material.depletable` attribute. @@ -86,11 +86,11 @@ using the :attr:`Material.depletable` attribute:: mat = openmc.Material() mat.depletable = True -When constructing the :class:`~openmc.deplete.Operator`, you should indicate -that normalization of tally results will be done based on the source rate rather -than a power or power density:: +When constructing the :class:`~openmc.deplete.CoupledOperator`, you should +indicate that normalization of tally results will be done based on the source +rate rather than a power or power density:: - op = openmc.deplete.Operator(model, normalization_mode='source-rate') + op = openmc.deplete.CoupledOperator(model, normalization_mode='source-rate') Finally, when creating a depletion integrator, use the ``source_rates`` argument:: @@ -109,13 +109,14 @@ Energy Deposition ~~~~~~~~~~~~~~~~~ The default energy deposition mode, ``"fission-q"``, instructs the -:class:`~openmc.deplete.Operator` to normalize reaction rates using the product -of fission reaction rates and fission Q values taken from the depletion chain. -This approach does not consider indirect contributions to energy deposition, -such as neutron heating and energy from secondary photons. In doing this, the -energy deposited during a transport calculation will be lower than expected. -This causes the reaction rates to be over-adjusted to hit the user-specific -power, or power density, leading to an over-depletion of burnable materials. +:class:`~openmc.deplete.CoupledOperator` to normalize reaction rates using the +product of fission reaction rates and fission Q values taken from the depletion +chain. This approach does not consider indirect contributions to energy +deposition, such as neutron heating and energy from secondary photons. In doing +this, the energy deposited during a transport calculation will be lower than +expected. This causes the reaction rates to be over-adjusted to hit the +user-specific power, or power density, leading to an over-depletion of burnable +materials. There are some remedies. First, the fission Q values can be directly set in a variety of ways. This requires knowing what the total fission energy release @@ -130,10 +131,10 @@ should be, including indirect components. Some examples are provided below:: # create a modified chain and write it to a new file chain = openmc.deplete.Chain.from_xml("chain.xml", fission_q) chain.export_to_xml("chain_mod_q.xml") - op = openmc.deplete.Operator(model, "chain_mod_q.xml") + op = openmc.deplete.CoupledOperator(model, "chain_mod_q.xml") # alternatively, pass the modified fission Q directly to the operator - op = openmc.deplete.Operator(model, "chain.xml", + op = openmc.deplete.CoupledOperator(model, "chain.xml", fission_q=fission_q) @@ -141,7 +142,7 @@ A more complete way to model the energy deposition is to use the modified heating reactions described in :ref:`methods_heating`. These values can be used to normalize reaction rates instead of using the fission reaction rates with:: - op = openmc.deplete.Operator(model, "chain.xml", + op = openmc.deplete.CoupledOperator(model, "chain.xml", normalization_mode="energy-deposition") These modified heating libraries can be generated by running the latest version @@ -174,7 +175,7 @@ the next transport step. This can be countered by instructing the operator to treat repeated instances of the same material as a unique material definition with:: - op = openmc.deplete.Operator(model, chain_file, + op = openmc.deplete.CoupledOperator(model, chain_file, diff_burnable_mats=True) For our example problem, this would deplete fuel on the outer region of the diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 5cb3b254a..dd1fd5b04 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -82,7 +82,8 @@ class TransportOperator(ABC): operator that takes a vector of material compositions and returns an eigenvalue and reaction rates. This abstract class sets the requirements for such a transport operator. Users should instantiate - :class:`openmc.deplete.Operator` rather than this class. + :class:`openmc.deplete.CoupledOperator` or + :class:`openmc.deplete.IndependentOperator` rather than this class. Parameters ---------- @@ -220,9 +221,11 @@ class ReactionRateHelper(ABC): Parameters ---------- n_nucs : int - Number of burnable nuclides tracked by :class:`openmc.deplete.Operator` + Number of burnable nuclides tracked by + :class:`openmc.deplete.abc.TransportOperator` n_react : int - Number of reactions tracked by :class:`openmc.deplete.Operator` + Number of reactions tracked by + :class:`openmc.deplete.abc.TransportOperator` Attributes ---------- @@ -291,9 +294,9 @@ class NormalizationHelper(ABC): """Abstract class for obtaining normalization factor on tallies This helper class determines how reaction rates calculated by an instance of - :class:`openmc.deplete.Operator` should be normalized for the purpose of - constructing a burnup matrix. Based on the method chosen, the power or - source rate provided by the user, and reaction rates from a + :class:`openmc.deplete.abc.TransportOperator` should be normalized for the + purpose of constructing a burnup matrix. Based on the method chosen, the + power or source rate provided by the user, and reaction rates from a :class:`ReactionRateHelper`, this class will scale reaction rates to the correct values. @@ -301,7 +304,7 @@ class NormalizationHelper(ABC): ---------- nuclides : list of str All nuclides with desired reaction rates. Ordered to be - consistent with :class:`openmc.deplete.Operator` + consistent with :class:`openmc.deplete.abc.TransportOperator` """ @@ -315,9 +318,9 @@ class NormalizationHelper(ABC): def prepare(self, chain_nucs, rate_index): """Perform work needed to obtain energy produced - This method is called prior to the transport simulations - in :meth:`openmc.deplete.Operator.initial_condition`. Only used for - energy-based normalization. + This method is called prior to calculating the reaction rates + in :meth:`openmc.deplete.abc.TransportOperator.initial_condition`. Only + used for energy-based normalization. Parameters ---------- @@ -430,7 +433,7 @@ class FissionYieldHelper(ABC): def unpack(): """Unpack tally data prior to compute fission yields. - Called after a :meth:`openmc.deplete.Operator.__call__` + Called after a :meth:`openmc.deplete.abc.TransportOperator.__call__` routine during the normalization of reaction rates. Not necessary for all subclasses to implement, unless tallies @@ -451,7 +454,7 @@ class FissionYieldHelper(ABC): mat_indexes : iterable of int Indices of tallied materials that will have their fission yields computed by this helper. Necessary as the - :class:`openmc.deplete.Operator` that uses this helper + :class:`openmc.deplete.CoupledOperator` that uses this helper may only burn a subset of all materials when running in parallel mode. """ @@ -463,14 +466,15 @@ class FissionYieldHelper(ABC): ---------- nuclides : iterable of str Nuclides with non-zero densities from the - :class:`openmc.deplete.Operator` + :class:`openmc.deplete.abc.TransportOperator` Returns ------- nuclides : list of str - Union of nuclides that the :class:`openmc.deplete.Operator` - says have non-zero densities at this stage and those that - have yield data. Sorted by nuclide name + Union of nuclides that the + :class:`openmc.deplete.abc.TransportOperator` says have non-zero + densities at this stage and those that have yield data. Sorted by + nuclide name """ return sorted(self._chain_set & set(nuclides)) @@ -484,7 +488,7 @@ class FissionYieldHelper(ABC): Parameters ---------- - operator : openmc.deplete.TransportOperator + operator : openmc.deplete.abc.TransportOperator Operator with a depletion chain kwargs: optional Additional keyword arguments to be used in constuction @@ -505,7 +509,7 @@ class Integrator(ABC): _params = r""" Parameters ---------- - operator : openmc.deplete.TransportOperator + operator : openmc.deplete.abc.TransportOperator Operator to perform transport simulations timesteps : iterable of float or iterable of tuple Array of timesteps. Note that values are not cumulative. The units are @@ -548,7 +552,7 @@ class Integrator(ABC): Attributes ---------- - operator : openmc.deplete.TransportOperator + operator : openmc.deplete.abc.TransportOperator Operator to perform transport simulations chain : openmc.deplete.Chain Depletion chain @@ -843,7 +847,7 @@ class SIIntegrator(Integrator): _params = r""" Parameters ---------- - operator : openmc.deplete.TransportOperator + operator : openmc.deplete.abc.TransportOperator The operator object to simulate on. timesteps : iterable of float or iterable of tuple Array of timesteps. Note that values are not cumulative. The units are @@ -889,7 +893,7 @@ class SIIntegrator(Integrator): Attributes ---------- - operator : openmc.deplete.TransportOperator + operator : openmc.deplete.abc.TransportOperator Operator to perform transport simulations chain : openmc.deplete.Chain Depletion chain diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index 0c1c34cf7..d63bc6bfb 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -264,7 +264,8 @@ class Chain: requires a list of ENDF incident neutron, decay, and neutron fission product yield sublibrary files. The depletion chain used during a depletion simulation is indicated by either an argument to - :class:`openmc.deplete.Operator` or through the + :class:`openmc.deplete.CoupledOperator` or + :class:`openmc.deplete.IndependentOperator`, or through the ``depletion_chain`` item in the :envvar:`OPENMC_CROSS_SECTIONS` environment variable. diff --git a/openmc/deplete/helpers.py b/openmc/deplete/helpers.py index 4fcde4f6b..262e61b35 100644 --- a/openmc/deplete/helpers.py +++ b/openmc/deplete/helpers.py @@ -68,7 +68,7 @@ class TalliedFissionYieldHelper(FissionYieldHelper): mat_indexes : iterable of int Indices of tallied materials that will have their fission yields computed by this helper. Necessary as the - :class:`openmc.deplete.Operator` that uses this helper + :class:`openmc.deplete.CoupledOperator` that uses this helper may only burn a subset of all materials when running in parallel mode. """ @@ -133,9 +133,11 @@ class DirectReactionRateHelper(ReactionRateHelper): Parameters ---------- n_nucs : int - Number of burnable nuclides tracked by :class:`openmc.deplete.Operator` + Number of burnable nuclides tracked by + :class:`openmc.deplete.CoupledOperator` n_react : int - Number of reactions tracked by :class:`openmc.deplete.Operator` + Number of reactions tracked by an instance of + :class:`openmc.deplete.CoupledOperator` Attributes ---------- @@ -217,9 +219,10 @@ class FluxCollapseHelper(ReactionRateHelper): Parameters ---------- n_nucs : int - Number of burnable nuclides tracked by :class:`openmc.deplete.Operator` + Number of burnable nuclides tracked by + :class:`openmc.deplete.CoupledOperator` n_react : int - Number of reactions tracked by :class:`openmc.deplete.Operator` + Number of reactions tracked by :class:`openmc.deplete.CoupledOperator` energies : iterable of float Energy group boundaries for flux spectrum in [eV] reactions : iterable of str @@ -385,7 +388,7 @@ class ChainFissionHelper(EnergyNormalizationHelper): ---------- nuclides : list of str All nuclides with desired reaction rates. Ordered to be - consistent with :class:`openmc.deplete.Operator` + consistent with :class:`openmc.deplete.CoupledOperator` energy : float Total energy [J/s/source neutron] produced in a transport simulation. Updated in the material iteration with :meth:`update`. @@ -637,7 +640,7 @@ class FissionYieldCutoffHelper(TalliedFissionYieldHelper): that occurred below ``cutoff``. The number of materials in the first axis corresponds to the number of materials burned by the - :class:`openmc.deplete.Operator` + :class:`openmc.deplete.CoupledOperator` """ def __init__(self, chain_nuclides, n_bmats, cutoff=112.0, @@ -694,7 +697,7 @@ class FissionYieldCutoffHelper(TalliedFissionYieldHelper): Parameters ---------- - operator : openmc.deplete.Operator + operator : openmc.deplete.CoupledOperator Operator with a chain and burnable materials kwargs: Additional keyword arguments to be used in construction @@ -720,7 +723,7 @@ class FissionYieldCutoffHelper(TalliedFissionYieldHelper): mat_indexes : iterable of int Indices of tallied materials that will have their fission yields computed by this helper. Necessary as the - :class:`openmc.deplete.Operator` that uses this helper + :class:`openmc.deplete.CoupledOperator` that uses this helper may only burn a subset of all materials when running in parallel mode. """ @@ -845,7 +848,7 @@ class AveragedFissionYieldHelper(TalliedFissionYieldHelper): mat_indexes : iterable of int Indices of tallied materials that will have their fission yields computed by this helper. Necessary as the - :class:`openmc.deplete.Operator` that uses this helper + :class:`openmc.deplete.CoupledOperator` that uses this helper may only burn a subset of all materials when running in parallel mode. """ @@ -958,7 +961,7 @@ class AveragedFissionYieldHelper(TalliedFissionYieldHelper): Parameters ---------- - operator : openmc.deplete.TransportOperator + operator : openmc.deplete.CoupledOperator Operator with a depletion chain kwargs : Additional keyword arguments to be used in construction diff --git a/openmc/deplete/operator.py b/openmc/deplete/operator.py index 2f8dcfba2..8bf031453 100644 --- a/openmc/deplete/operator.py +++ b/openmc/deplete/operator.py @@ -30,7 +30,7 @@ from .helpers import ( SourceRateHelper, FluxCollapseHelper) -__all__ = ["Operator", "OperatorResult"] +__all__ = ["CoupledOperator", "Operator", "OperatorResult"] def _find_cross_sections(model): @@ -56,13 +56,13 @@ def _find_cross_sections(model): return cross_sections -class Operator(OpenMCOperator): - """OpenMC transport operator for depletion. +class CoupledOperator(OpenMCOperator): + """Transport-coupled operator for depletion. - Instances of this class can be used to perform depletion using OpenMC as the - transport operator. Normally, a user needn't call methods of this class - directly. Instead, an instance of this class is passed to an integrator - class, such as :class:`openmc.deplete.CECMIntegrator`. + Instances of this class can be used to perform transport-coupled depletion + using OpenMC's transport solver. Normally, a user needn't call methods of + this class directly. Instead, an instance of this class is passed to an + integrator class, such as :class:`openmc.deplete.CECMIntegrator`. .. versionchanged:: 0.13.0 The geometry and settings parameters have been replaced with a @@ -193,11 +193,11 @@ class Operator(OpenMCOperator): reduce_chain=False, reduce_chain_level=None): # check for old call to constructor if isinstance(model, openmc.Geometry): - msg = "As of version 0.13.0 openmc.deplete.Operator requires an " \ - "openmc.Model object rather than the openmc.Geometry and " \ - "openmc.Settings parameters. Please use the geometry and " \ - "settings objects passed here to create a model with which " \ - "to generate the depletion Operator." + msg = "As of version 0.13.0 openmc.deplete.CoupledOperator " \ + "requires an openmc.Model object rather than the " \ + "openmc.Geometry and openmc.Settings parameters. Please use " \ + "the geometry and settings objects passed here to create a " \ + " model with which to generate the depletion Operator." raise TypeError(msg) # Determine cross sections / depletion chain @@ -516,3 +516,6 @@ class Operator(OpenMCOperator): """Finalize a depletion simulation and release resources.""" if self.cleanup_when_done: openmc.lib.finalize() + +# Retain deprecated name for the time being +Operator = CoupledOperator diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py index c238002fe..1c5535601 100644 --- a/openmc/deplete/results.py +++ b/openmc/deplete/results.py @@ -81,9 +81,9 @@ class Results(list): .. note:: Initial values for some isotopes that do not appear in initial concentrations may be non-zero, depending on the - value of the :attr:`openmc.deplete.Operator.dilute_initial` - attribute. The :class:`openmc.deplete.Operator` class adds isotopes - according to this setting, which can be set to zero. + value of the :attr:`openmc.deplete.CoupledOperator.dilute_initial` + attribute. The :class:`openmc.deplete.CoupledOperator` class adds + isotopes according to this setting, which can be set to zero. Parameters ---------- @@ -145,8 +145,8 @@ class Results(list): Initial values for some isotopes that do not appear in initial concentrations may be non-zero, depending on the - value of :class:`openmc.deplete.Operator` ``dilute_initial`` - The :class:`openmc.deplete.Operator` adds isotopes according + value of :class:`openmc.deplete.CoupledOperator` ``dilute_initial`` + The :class:`openmc.deplete.CoupledOperator` adds isotopes according to this setting, which can be set to zero. Parameters From 467ae6b9fbe2e043510b0fb9587def567bd07dcd Mon Sep 17 00:00:00 2001 From: yardasol Date: Sat, 30 Jul 2022 16:55:36 -0500 Subject: [PATCH 103/131] operator.py -> coupled_operator.py --- openmc/deplete/__init__.py | 2 +- openmc/deplete/{operator.py => coupled_operator.py} | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) rename openmc/deplete/{operator.py => coupled_operator.py} (98%) diff --git a/openmc/deplete/__init__.py b/openmc/deplete/__init__.py index 12e4abe87..329ee8b52 100644 --- a/openmc/deplete/__init__.py +++ b/openmc/deplete/__init__.py @@ -8,7 +8,7 @@ A depletion front-end tool. from .nuclide import * from .chain import * from .openmc_operator import * -from .operator import * +from .coupled_operator import * from .independent_operator import * from .microxs import * from .reaction_rates import * diff --git a/openmc/deplete/operator.py b/openmc/deplete/coupled_operator.py similarity index 98% rename from openmc/deplete/operator.py rename to openmc/deplete/coupled_operator.py index 8bf031453..35b63e9cf 100644 --- a/openmc/deplete/operator.py +++ b/openmc/deplete/coupled_operator.py @@ -191,6 +191,13 @@ class CoupledOperator(OpenMCOperator): fission_yield_mode="constant", fission_yield_opts=None, reaction_rate_mode="direct", reaction_rate_opts=None, reduce_chain=False, reduce_chain_level=None): + # warn of name change + warn( + "The Operator(...) class has been renamed and will " + "be removed in a future version of OpenMC. Use " + "CoupledOperator(...) instead.", + FutureWarning + ) # check for old call to constructor if isinstance(model, openmc.Geometry): msg = "As of version 0.13.0 openmc.deplete.CoupledOperator " \ From 3e81a86915881b952dbe26449c0101cc9c1104f5 Mon Sep 17 00:00:00 2001 From: yardasol Date: Sat, 30 Jul 2022 18:37:51 -0500 Subject: [PATCH 104/131] TransportOperator->DepletionOperator --- docs/source/pythonapi/deplete.rst | 42 +++++++++++---------- docs/source/usersguide/depletion.rst | 8 ++-- openmc/deplete/abc.py | 46 +++++++++++------------ openmc/deplete/coupled_operator.py | 7 ++-- openmc/deplete/helpers.py | 2 +- openmc/deplete/independent_operator.py | 9 +++-- openmc/deplete/openmc_operator.py | 11 +++--- openmc/deplete/stepresult.py | 2 +- tests/dummy_operator.py | 4 +- tests/unit_tests/test_deplete_operator.py | 4 +- 10 files changed, 70 insertions(+), 65 deletions(-) diff --git a/docs/source/pythonapi/deplete.rst b/docs/source/pythonapi/deplete.rst index bc07ed1b0..9419244fe 100644 --- a/docs/source/pythonapi/deplete.rst +++ b/docs/source/pythonapi/deplete.rst @@ -12,20 +12,20 @@ Primary API The two primary requirements to perform depletion with :mod:`openmc.deplete` are: - 1) A reaction rate operator + 1) A depletion operator 2) A time-integration scheme -The former is responsible for obtaining transmuation reaction rates. The latter -is responsible for projecting reaction rates and compositions forward in -calendar time across some step size :math:`\Delta t`, and obtaining new -compositions given a power or power density. The :class:`CoupledOperator` class -is provided to obtain reaction rates via tallies through OpenMC's transport -solver, and the :class:`IndependentOperator` class is provided to obtain -reaction rates from cross-section data. Several classes are provided that -implement different time-integration algorithms for depletion calculations, -which are described in detail in Colin Josey's thesis, `Development and -analysis of high order neutron transport-depletion coupling algorithms -`_. +The former is responsible for calcuating retaining important information required for depletion. The most common examples are reaction rates and power +normalization data. The latter is responsible for projecting reaction rates and +compositions forward in calendar time across some step size :math:`\Delta t`, +and obtaining new compositions given a power or power density. The +:class:`CoupledOperator` class is provided to obtain reaction rates via tallies +through OpenMC's transport solver, and the :class:`IndependentOperator` class is +provided to obtain reaction rates from cross-section data. Several classes are +provided that implement different time-integration algorithms for depletion +calculations, which are described in detail in Colin Josey's thesis, +`Development and analysis of high order neutron transport-depletion coupling +algorithms `_. .. autosummary:: :toctree: generated @@ -41,8 +41,8 @@ analysis of high order neutron transport-depletion coupling algorithms SICELIIntegrator SILEQIIntegrator -Each of these classes expects a "reaction rate operator" to be passed. Operators -provided by OpenMC are available using the following classes: +Each of these classes expects a "depletion operator" to be passed. OpenMC +provides The following classes implementing depletion operators: .. autosummary:: :toctree: generated @@ -214,7 +214,7 @@ are stored in :class:`helpers.TalliedFissionYieldHelper` helpers.TalliedFissionYieldHelper -Methods common to OpenMC-specific implementations of :class:`TransportOperator` +Methods common to OpenMC-specific implementations of :class:`DepletionOperator` are stored in :class:`openmc_operator.OpenMCOperator` .. autosummary:: @@ -230,19 +230,21 @@ Abstract Base Classes A good starting point for extending capabilities in :mod:`openmc.deplete` is to examine the following abstract base classes. Custom classes can -inherit from :class:`abc.TransportOperator` to implement alternative -schemes for collecting reaction rates and other data from a transport code -prior to depleting materials +inherit from :class:`abc.DepletionOperator` to implement alternative +schemes for collecting reaction rates and other data prior to depleting +materials .. autosummary:: :toctree: generated :nosignatures: :template: mycallable.rst - abc.TransportOperator + abc.DepletionOperator The following classes are abstract classes used to pass information from -OpenMC simulations back on to the :class:`abc.TransportOperator` +transport simulations (in the case of transport-coupled depletion) or to +simply calculate these quantities directly (in the case of +transport-independent depletion) back on to the :class:`abc.DepletionOperator` .. autosummary:: :toctree: generated diff --git a/docs/source/usersguide/depletion.rst b/docs/source/usersguide/depletion.rst index 990031c40..cc59efa6c 100644 --- a/docs/source/usersguide/depletion.rst +++ b/docs/source/usersguide/depletion.rst @@ -13,14 +13,14 @@ updated reaction rates, and the process is repeated for as many timesteps as are requested. The depletion module is designed such that the reaction rate solution (the -"operator") is completely isolated from the solution of the transmutation +depletion "operator") is completely isolated from the solution of the transmutation equations and the method used for advancing time. :mod:`openmc.deplete` supports multiple time-integration methods for determining material compositions over time. Each method appears as a different class. For example, :class:`openmc.deplete.CECMIntegrator` runs a depletion calculation using the CE/CM algorithm (deplete over a timestep using the middle-of-step -reaction rates). An instance of :class:`~openmc.deplete.abc.TransportOperator` +reaction rates). An instance of :class:`~openmc.deplete.abc.DepletionOperator` is passed to one of these functions along with the timesteps and power level:: power = 1200.0e6 # watts @@ -37,8 +37,8 @@ time:: time, keff = results.get_keff() Note that the coupling between the reaction rate solver and the transmutation -solver happens in-memory rather than by reading/writing files on disk. OpenMC has two categories of -operators for obtaining transmutation reaction rates. +solver happens in-memory rather than by reading/writing files on disk. OpenMC has two categories of depletion operators for obtaining transmutation reaction +rates. .. _coupled-depletion: diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index dd1fd5b04..e874479c0 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -28,8 +28,8 @@ from .pool import deplete __all__ = [ - "OperatorResult", "TransportOperator", "ReactionRateHelper", - "NormalizationHelper", "FissionYieldHelper", + "OperatorResult", "DepletionOperator", + "ReactionRateHelper", "NormalizationHelper", "FissionYieldHelper", "Integrator", "SIIntegrator", "DepSystemSolver", "add_params"] @@ -75,13 +75,13 @@ def change_directory(output_dir): os.chdir(orig_dir) -class TransportOperator(ABC): - """Abstract class defining a transport operator +class DepletionOperator(ABC): + """Abstract class defining a depletion operator - Each depletion integrator is written to work with a generic transport + Each depletion integrator is written to work with a generic depletion operator that takes a vector of material compositions and returns an eigenvalue and reaction rates. This abstract class sets the requirements - for such a transport operator. Users should instantiate + for such a depletion operator. Users should instantiate :class:`openmc.deplete.CoupledOperator` or :class:`openmc.deplete.IndependentOperator` rather than this class. @@ -222,10 +222,10 @@ class ReactionRateHelper(ABC): ---------- n_nucs : int Number of burnable nuclides tracked by - :class:`openmc.deplete.abc.TransportOperator` + :class:`openmc.deplete.abc.DepletionOperator` n_react : int Number of reactions tracked by - :class:`openmc.deplete.abc.TransportOperator` + :class:`openmc.deplete.abc.DepletionOperator` Attributes ---------- @@ -294,7 +294,7 @@ class NormalizationHelper(ABC): """Abstract class for obtaining normalization factor on tallies This helper class determines how reaction rates calculated by an instance of - :class:`openmc.deplete.abc.TransportOperator` should be normalized for the + :class:`openmc.deplete.abc.DepletionOperator` should be normalized for the purpose of constructing a burnup matrix. Based on the method chosen, the power or source rate provided by the user, and reaction rates from a :class:`ReactionRateHelper`, this class will scale reaction rates to the @@ -304,7 +304,7 @@ class NormalizationHelper(ABC): ---------- nuclides : list of str All nuclides with desired reaction rates. Ordered to be - consistent with :class:`openmc.deplete.abc.TransportOperator` + consistent with :class:`openmc.deplete.abc.DepletionOperator` """ @@ -319,7 +319,7 @@ class NormalizationHelper(ABC): """Perform work needed to obtain energy produced This method is called prior to calculating the reaction rates - in :meth:`openmc.deplete.abc.TransportOperator.initial_condition`. Only + in :meth:`openmc.deplete.abc.DepletionOperator.initial_condition`. Only used for energy-based normalization. Parameters @@ -433,7 +433,7 @@ class FissionYieldHelper(ABC): def unpack(): """Unpack tally data prior to compute fission yields. - Called after a :meth:`openmc.deplete.abc.TransportOperator.__call__` + Called after a :meth:`openmc.deplete.abc.DepletionOperator.__call__` routine during the normalization of reaction rates. Not necessary for all subclasses to implement, unless tallies @@ -466,13 +466,13 @@ class FissionYieldHelper(ABC): ---------- nuclides : iterable of str Nuclides with non-zero densities from the - :class:`openmc.deplete.abc.TransportOperator` + :class:`openmc.deplete.abc.DepletionOperator` Returns ------- nuclides : list of str Union of nuclides that the - :class:`openmc.deplete.abc.TransportOperator` says have non-zero + :class:`openmc.deplete.abc.DepletionOperator` says have non-zero densities at this stage and those that have yield data. Sorted by nuclide name @@ -488,7 +488,7 @@ class FissionYieldHelper(ABC): Parameters ---------- - operator : openmc.deplete.abc.TransportOperator + operator : openmc.deplete.abc.DepletionOperator Operator with a depletion chain kwargs: optional Additional keyword arguments to be used in constuction @@ -509,8 +509,8 @@ class Integrator(ABC): _params = r""" Parameters ---------- - operator : openmc.deplete.abc.TransportOperator - Operator to perform transport simulations + operator : openmc.deplete.abc.DepletionOperator + Operator that calculates reaction rates timesteps : iterable of float or iterable of tuple Array of timesteps. Note that values are not cumulative. The units are specified by the `timestep_units` argument when `timesteps` is an @@ -552,8 +552,8 @@ class Integrator(ABC): Attributes ---------- - operator : openmc.deplete.abc.TransportOperator - Operator to perform transport simulations + operator : openmc.deplete.abc.DepletionOperator + Operator that calculates reaction rates chain : openmc.deplete.Chain Depletion chain timesteps : iterable of float @@ -847,8 +847,8 @@ class SIIntegrator(Integrator): _params = r""" Parameters ---------- - operator : openmc.deplete.abc.TransportOperator - The operator object to simulate on. + operator : openmc.deplete.abc.DepletionOperator + Operator that calculates reaction rates timesteps : iterable of float or iterable of tuple Array of timesteps. Note that values are not cumulative. The units are specified by the `timestep_units` argument when `timesteps` is an @@ -893,8 +893,8 @@ class SIIntegrator(Integrator): Attributes ---------- - operator : openmc.deplete.abc.TransportOperator - Operator to perform transport simulations + operator : openmc.deplete.abc.DepletionOperator + Operator that calculates reaction rates chain : openmc.deplete.Chain Depletion chain timesteps : iterable of float diff --git a/openmc/deplete/coupled_operator.py b/openmc/deplete/coupled_operator.py index 35b63e9cf..39e46c882 100644 --- a/openmc/deplete/coupled_operator.py +++ b/openmc/deplete/coupled_operator.py @@ -1,7 +1,8 @@ """Transport-coupled operator for depletion. -This module implements an operator coupled to OpenMC's transport sovler so that -it can be used by depletion integrators. The implementation makes use of the Python bindings to OpenMC's C API so that reading tally results and updating +This module implements a depletion operator coupled to OpenMC's transport solver +so that it can be used by depletion integrators. The implementation makes use of +the Python bindings to OpenMC's C API so that reading tally results and updating material number densities is all done in-memory instead of through the filesystem. @@ -57,7 +58,7 @@ def _find_cross_sections(model): class CoupledOperator(OpenMCOperator): - """Transport-coupled operator for depletion. + """Transport-coupled depletion operator. Instances of this class can be used to perform transport-coupled depletion using OpenMC's transport solver. Normally, a user needn't call methods of diff --git a/openmc/deplete/helpers.py b/openmc/deplete/helpers.py index 262e61b35..77f3da201 100644 --- a/openmc/deplete/helpers.py +++ b/openmc/deplete/helpers.py @@ -557,7 +557,7 @@ class ConstantFissionYieldHelper(FissionYieldHelper): Parameters ---------- - operator : openmc.deplete.TransportOperator + operator : openmc.deplete.abc.DepletionOperator operator with a depletion chain kwargs: Additional keyword arguments to be used in construction diff --git a/openmc/deplete/independent_operator.py b/openmc/deplete/independent_operator.py index a7eb54f57..631117403 100644 --- a/openmc/deplete/independent_operator.py +++ b/openmc/deplete/independent_operator.py @@ -1,4 +1,4 @@ -"""Transport-independent operator for depletion. +"""Transport-independent depletion operator. This module implements a depletion operator that runs independently of any transport solver by using user-provided one-group cross sections. @@ -23,8 +23,8 @@ from .results import Results from .helpers import ChainFissionHelper, ConstantFissionYieldHelper, SourceRateHelper class IndependentOperator(OpenMCOperator): - """Depletion operator that uses one-group cross sections to calculate - reaction rates. + """Transport-independent depletion operator that uses one-group cross + sections to calculate reaction rates. Instances of this class can be used to perform depletion using one-group cross sections and constant flux or constant power. Normally, a user needn't @@ -189,7 +189,8 @@ class IndependentOperator(OpenMCOperator): if ``reduce_chain`` evaluates to true. The default value of ``None`` implies no limit on the depth. fission_yield_opts : dict of str to option, optional - Optional arguments to pass to the `FissionYieldHelper`. Will be + Optional arguments to pass to the + :class:`openmc.deplete.helpers.FissionYieldHelper` class. Will be passed directly on to the helper. Passing a value of None will use the defaults for the associated helper. diff --git a/openmc/deplete/openmc_operator.py b/openmc/deplete/openmc_operator.py index 7235ab9bf..bcf73a010 100644 --- a/openmc/deplete/openmc_operator.py +++ b/openmc/deplete/openmc_operator.py @@ -1,6 +1,7 @@ """OpenMC transport operator -This module implements functions shared by both OpenMC transport operators as well as indepenedent depletion operators. +This module implements functions shared by both OpenMC transport-coupled and +transport-independent depletion operators. """ @@ -11,7 +12,7 @@ import numpy as np import openmc from openmc.mpi import comm -from .abc import TransportOperator, OperatorResult +from .abc import DepletionOperator, OperatorResult from .atom_number import AtomNumber from .reaction_rates import ReactionRates @@ -41,12 +42,12 @@ def _distribute(items): j += chunk_size -class OpenMCOperator(TransportOperator): +class OpenMCOperator(DepletionOperator): """Abstract class holding OpenMC-specific functions for running depletion calculations. - Specific classes for running coupled transport-depletion calculations or - depletion-only calculations are implemented as subclasses of OpenMCOperator + Specific classes for running transport-coupled or transport-independent + depletion calculations are implemented as subclasses of OpenMCOperator. Parameters ---------- diff --git a/openmc/deplete/stepresult.py b/openmc/deplete/stepresult.py index 80a664c6f..3082cf304 100644 --- a/openmc/deplete/stepresult.py +++ b/openmc/deplete/stepresult.py @@ -462,7 +462,7 @@ class StepResult: Parameters ---------- - op : openmc.deplete.TransportOperator + op : openmc.deplete.abc.DepletionOperator The operator used to generate these results. x : list of list of numpy.array The prior x vectors. Indexed [i][cell] using the above equation. diff --git a/tests/dummy_operator.py b/tests/dummy_operator.py index 1bb00129d..c54269cc0 100644 --- a/tests/dummy_operator.py +++ b/tests/dummy_operator.py @@ -6,7 +6,7 @@ import scipy.sparse as sp from uncertainties import ufloat from openmc.deplete.reaction_rates import ReactionRates -from openmc.deplete.abc import TransportOperator, OperatorResult +from openmc.deplete.abc import DepletionOperator, OperatorResult from openmc.deplete import ( CECMIntegrator, PredictorIntegrator, CELIIntegrator, LEQIIntegrator, EPCRK4Integrator, CF4Integrator, SICELIIntegrator, SILEQIIntegrator @@ -117,7 +117,7 @@ class TestChain: return sp.csr_matrix(np.array([[a11, a12], [a21, a22]])) -class DummyOperator(TransportOperator): +class DummyOperator(DepletionOperator): """This is a dummy operator class with no statistical uncertainty. y_1' = sin(y_2) y_1 + cos(y_1) y_2 diff --git a/tests/unit_tests/test_deplete_operator.py b/tests/unit_tests/test_deplete_operator.py index 5fe8715ac..1910112b8 100644 --- a/tests/unit_tests/test_deplete_operator.py +++ b/tests/unit_tests/test_deplete_operator.py @@ -7,7 +7,7 @@ to a custom file with new depletion_chain node from pathlib import Path import pytest -from openmc.deplete.abc import TransportOperator +from openmc.deplete.abc import DepletionOperator from openmc.deplete.chain import Chain, _find_chain_file BARE_XS_FILE = "bare_cross_sections.xml" @@ -32,7 +32,7 @@ def bare_xs(run_in_tmpdir): yield BARE_XS_FILE -class BareDepleteOperator(TransportOperator): +class BareDepleteOperator(DepletionOperator): """Very basic class for testing the initialization.""" @staticmethod From eac4092362545db994f0653eaea134afcf864dbf Mon Sep 17 00:00:00 2001 From: yardasol Date: Mon, 1 Aug 2022 09:35:30 -0500 Subject: [PATCH 105/131] address @paulromano's fourth round of comments - syntax fixes - remove `units` argument from MicroXS.from_csv and MicroXS.from_array - make Operator warning display only when users call Operator --- openmc/deplete/abc.py | 4 ++-- openmc/deplete/coupled_operator.py | 18 ++++++++++-------- openmc/deplete/microxs.py | 29 ++++++++++++----------------- 3 files changed, 24 insertions(+), 27 deletions(-) diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index e874479c0..8439fab34 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -529,8 +529,8 @@ class Integrator(ABC): initial heavy metal inventory to get total power if ``power`` is not specified. source_rates : float or iterable of float, optional - Source rate in [neutron/sec] or neutron flux in [neut/cm^2-s] for each - interval in :attr:`timesteps` + Source rate in [neutron/sec] or neutron flux in [neutron/s-cm^2] for + each interval in :attr:`timesteps` .. versionadded:: 0.12.1 timestep_units : {'s', 'min', 'h', 'd', 'MWd/kg'} diff --git a/openmc/deplete/coupled_operator.py b/openmc/deplete/coupled_operator.py index 39e46c882..2e457f4d7 100644 --- a/openmc/deplete/coupled_operator.py +++ b/openmc/deplete/coupled_operator.py @@ -192,13 +192,7 @@ class CoupledOperator(OpenMCOperator): fission_yield_mode="constant", fission_yield_opts=None, reaction_rate_mode="direct", reaction_rate_opts=None, reduce_chain=False, reduce_chain_level=None): - # warn of name change - warn( - "The Operator(...) class has been renamed and will " - "be removed in a future version of OpenMC. Use " - "CoupledOperator(...) instead.", - FutureWarning - ) + # check for old call to constructor if isinstance(model, openmc.Geometry): msg = "As of version 0.13.0 openmc.deplete.CoupledOperator " \ @@ -526,4 +520,12 @@ class CoupledOperator(OpenMCOperator): openmc.lib.finalize() # Retain deprecated name for the time being -Operator = CoupledOperator +def Operator(**args, **kwargs): + # warn of name change + warn( + "The Operator(...) class has been renamed and will " + "be removed in a future version of OpenMC. Use " + "CoupledOperator(...) instead.", + FutureWarning + ) + return CoupledOperator(*args, **kwargs) diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py index 6abf8836f..68255d18a 100644 --- a/openmc/deplete/microxs.py +++ b/openmc/deplete/microxs.py @@ -6,7 +6,6 @@ nuclides names as row indices and reaction names as column indices. import tempfile from pathlib import Path -from os import chdir from pandas import DataFrame, read_csv, concat from numpy import ndarray @@ -54,7 +53,8 @@ class MicroXS(DataFrame): Returns ------- - MicroXS, cross section data in [b] + MicroXS + Cross section data in [b] """ groups = EnergyGroups(energy_bounds) @@ -73,15 +73,12 @@ class MicroXS(DataFrame): model.tallies = tallies # create temporary run - original_dir = Path.cwd() - temp_dir = tempfile.TemporaryDirectory() - chdir(tempfile.gettempdir()) - statepoint_path = model.run() - chdir(original_dir) + with tempfile.TemporaryDirectory() as temp_dir: + statepoint_path = model.run(cwd=temp_dir) - with StatePoint(statepoint_path) as sp: - for rxn in xs: - xs[rxn].load_from_statepoint(sp) + with StatePoint(statepoint_path) as sp: + for rxn in xs: + xs[rxn].load_from_statepoint(sp) # Build the DataFrame micro_xs = cls() @@ -89,7 +86,7 @@ class MicroXS(DataFrame): df = xs[rxn].get_pandas_dataframe(xs_type='micro') df.index = df['nuclide'] df.drop(['nuclide', xs[rxn].domain_type, 'group in', 'std. dev.'], axis=1, inplace=True) - df.rename({'mean':rxn}, axis=1, inplace=True) + df.rename({'mean': rxn}, axis=1, inplace=True) micro_xs = concat([micro_xs, df], axis=1) micro_xs._units = 'b' @@ -100,7 +97,7 @@ class MicroXS(DataFrame): return micro_xs @classmethod - def from_array(cls, nuclides, reactions, data, units='b'): + def from_array(cls, nuclides, reactions, data): """ Creates a ``MicroXS`` object from arrays. @@ -115,8 +112,6 @@ class MicroXS(DataFrame): data : ndarray of floats Array containing one-group microscopic cross section values for each nuclide and reaction - units : {'b', 'cm^2'} - Units of the cross section values in ``data`` Returns ------- @@ -133,12 +128,12 @@ class MicroXS(DataFrame): cls._validate_micro_xs_inputs( nuclides, reactions, data) micro_xs = cls(index=nuclides, columns=reactions, data=data) - micro_xs._units = units + micro_xs._units = 'b' return micro_xs @classmethod - def from_csv(cls, csv_file, units='b', **kwargs): + def from_csv(cls, csv_file, **kwargs): """ Load a ``MicroXS`` object from a ``.csv`` file. @@ -165,7 +160,7 @@ class MicroXS(DataFrame): cls._validate_micro_xs_inputs(list(micro_xs.index), list(micro_xs.columns), micro_xs.to_numpy()) - micro_xs._units = units + micro_xs._units = 'b' return micro_xs From 51b1350f4a413f4c7a533d3f45a9ae2f3801d316 Mon Sep 17 00:00:00 2001 From: yardasol Date: Mon, 1 Aug 2022 09:49:20 -0500 Subject: [PATCH 106/131] add basic unit test for initalizing CoupledOperator --- .../test_deplete_coupled_operator.py | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 tests/unit_tests/test_deplete_coupled_operator.py diff --git a/tests/unit_tests/test_deplete_coupled_operator.py b/tests/unit_tests/test_deplete_coupled_operator.py new file mode 100644 index 000000000..6de0342b0 --- /dev/null +++ b/tests/unit_tests/test_deplete_coupled_operator.py @@ -0,0 +1,55 @@ +"""Basic unit tests for openmc.deplete.CoupledOperator instantiation + +""" + +from pathlib import Path + +import pytest +from openmc.deplete import CoupledOperator +import openmc +import numpy as np + +CHAIN_PATH = Path(__file__).parents[1] / "chain_simple.xml" + +@pytest.fixture(scope="module") +def model(): + fuel = openmc.Material(name="uo2") + fuel.add_element("U", 1, percent_type="ao", enrichment=4.25) + fuel.add_element("O", 2) + fuel.set_density("g/cc", 10.4) + + clad = openmc.Material(name="clad") + clad.add_element("Zr", 1) + clad.set_density("g/cc", 6) + + water = openmc.Material(name="water") + water.add_element("O", 1) + water.add_element("H", 2) + water.set_density("g/cc", 1.0) + water.add_s_alpha_beta("c_H_in_H2O") + + radii = [0.42, 0.45] + fuel.volume = np.pi * radii[0] ** 2 + clad.volume = np.pi * (radii[1]**2 - radii[0]**2) + water.volume = 1.24**2 - (np.pi * radii[1]**2) + + materials = openmc.Materials([fuel, clad, water]) + + pin_surfaces = [openmc.ZCylinder(r=r) for r in radii] + pin_univ = openmc.model.pin(pin_surfaces, materials) + bound_box = openmc.rectangular_prism(1.24, 1.24, boundary_type="reflective") + root_cell = openmc.Cell(fill=pin_univ, region=bound_box) + geometry = openmc.Geometry([root_cell]) + + settings = openmc.Settings() + settings.particles = 1000 + settings.inactive = 10 + settings.batches = 50 + + return openmc.Model(geometry, materials, settings) + +def test_operator_init(model): + """The test uses a temporary dummy chain. This file will be removed + at the end of the test, and only contains a depletion_chain node.""" + + CoupledOperator(model, CHAIN_PATH) From 2f985a808609319ea763944285293b60c11dd6f3 Mon Sep 17 00:00:00 2001 From: yardasol Date: Mon, 1 Aug 2022 09:58:52 -0500 Subject: [PATCH 107/131] syntax fix in CoupledOperator --- openmc/deplete/coupled_operator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openmc/deplete/coupled_operator.py b/openmc/deplete/coupled_operator.py index 2e457f4d7..448952b62 100644 --- a/openmc/deplete/coupled_operator.py +++ b/openmc/deplete/coupled_operator.py @@ -192,7 +192,7 @@ class CoupledOperator(OpenMCOperator): fission_yield_mode="constant", fission_yield_opts=None, reaction_rate_mode="direct", reaction_rate_opts=None, reduce_chain=False, reduce_chain_level=None): - + # check for old call to constructor if isinstance(model, openmc.Geometry): msg = "As of version 0.13.0 openmc.deplete.CoupledOperator " \ @@ -520,7 +520,7 @@ class CoupledOperator(OpenMCOperator): openmc.lib.finalize() # Retain deprecated name for the time being -def Operator(**args, **kwargs): +def Operator(*args, **kwargs): # warn of name change warn( "The Operator(...) class has been renamed and will " From 4bab1b5611a929ec7a0166711dd61f64c1f62735 Mon Sep 17 00:00:00 2001 From: yardasol Date: Mon, 1 Aug 2022 12:09:21 -0500 Subject: [PATCH 108/131] remove attribute from MicroXS; specify xs values to be in barns --- openmc/deplete/independent_operator.py | 1 - openmc/deplete/microxs.py | 10 +++------- tests/unit_tests/test_deplete_microxs.py | 1 - 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/openmc/deplete/independent_operator.py b/openmc/deplete/independent_operator.py index 631117403..cd5a4d454 100644 --- a/openmc/deplete/independent_operator.py +++ b/openmc/deplete/independent_operator.py @@ -129,7 +129,6 @@ class IndependentOperator(OpenMCOperator): 'fission_yield_opts': fission_yield_opts} cross_sections = micro_xs * 1e-24 - cross_sections._units = 'cm^2' super().__init__( materials, cross_sections, diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py index 68255d18a..aef3c26c2 100644 --- a/openmc/deplete/microxs.py +++ b/openmc/deplete/microxs.py @@ -111,7 +111,8 @@ class MicroXS(DataFrame): :data:`openmc.deplete.chain.REACTIONS` data : ndarray of floats Array containing one-group microscopic cross section values for - each nuclide and reaction + each nuclide and reaction. Cross section values are assumed to be + in [b]. Returns ------- @@ -128,7 +129,6 @@ class MicroXS(DataFrame): cls._validate_micro_xs_inputs( nuclides, reactions, data) micro_xs = cls(index=nuclides, columns=reactions, data=data) - micro_xs._units = 'b' return micro_xs @@ -141,9 +141,7 @@ class MicroXS(DataFrame): ---------- csv_file : str Relative path to csv-file containing microscopic cross section - data. - units : {'b', 'cm^2'} - Units of the cross section values in the ``.csv`` file + data. Cross section values are assumed to be in [b] **kwargs : dict Keyword arguments to pass to :func:`pandas.read_csv()`. @@ -160,8 +158,6 @@ class MicroXS(DataFrame): cls._validate_micro_xs_inputs(list(micro_xs.index), list(micro_xs.columns), micro_xs.to_numpy()) - micro_xs._units = 'b' - return micro_xs @staticmethod diff --git a/tests/unit_tests/test_deplete_microxs.py b/tests/unit_tests/test_deplete_microxs.py index 17d9cb38f..557082eda 100644 --- a/tests/unit_tests/test_deplete_microxs.py +++ b/tests/unit_tests/test_deplete_microxs.py @@ -54,7 +54,6 @@ def test_csv(): ref_xs = MicroXS.from_csv(ONE_GROUP_XS) ref_xs.to_csv('temp_xs.csv') temp_xs = MicroXS.from_csv('temp_xs.csv') - assert ref_xs._units == temp_xs._units assert np.all(ref_xs == temp_xs) remove('temp_xs.csv') From 78e4e0aedf43082409be060eee9a559525f76669 Mon Sep 17 00:00:00 2001 From: yardasol Date: Mon, 1 Aug 2022 12:20:46 -0500 Subject: [PATCH 109/131] doc adjustments from @paulromano's comments --- docs/source/methods/depletion.rst | 2 +- docs/source/pythonapi/deplete.rst | 6 +++--- docs/source/usersguide/depletion.rst | 21 ++++++++++----------- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/docs/source/methods/depletion.rst b/docs/source/methods/depletion.rst index 66ee50397..308a62e19 100644 --- a/docs/source/methods/depletion.rst +++ b/docs/source/methods/depletion.rst @@ -231,7 +231,7 @@ of FPY: to transport-coupled depletion. 3. Compute the average energy at which fission events occur and use an effective FPY by linearly interpolating between FPY provided at neighboring energies. - Only applicable to transport-coupled depletion + Only applicable to transport-coupled depletion. The method for transport-coupled depletion can be selected through the ``fission_yield_mode`` argument to the :class:`openmc.deplete.CoupledOperator` diff --git a/docs/source/pythonapi/deplete.rst b/docs/source/pythonapi/deplete.rst index 9419244fe..a57ec915d 100644 --- a/docs/source/pythonapi/deplete.rst +++ b/docs/source/pythonapi/deplete.rst @@ -15,7 +15,7 @@ are: 1) A depletion operator 2) A time-integration scheme -The former is responsible for calcuating retaining important information required for depletion. The most common examples are reaction rates and power +The former is responsible for calcuating and retaining important information required for depletion. The most common examples are reaction rates and power normalization data. The latter is responsible for projecting reaction rates and compositions forward in calendar time across some step size :math:`\Delta t`, and obtaining new compositions given a power or power density. The @@ -194,8 +194,8 @@ total system energy. helpers.FissionYieldCutoffHelper helpers.FluxCollapseHelper -The :class:`openmc.deplete.IndependentOperator` uses inner class subclassed from -those listed to perform similar calculations. +The :class:`openmc.deplete.IndependentOperator` uses inner classes subclassed +from those listed above to perform similar calculations. Intermediate Classes -------------------- diff --git a/docs/source/usersguide/depletion.rst b/docs/source/usersguide/depletion.rst index cc59efa6c..5052f7263 100644 --- a/docs/source/usersguide/depletion.rst +++ b/docs/source/usersguide/depletion.rst @@ -8,7 +8,7 @@ OpenMC supports transport-coupled and transport-independent depletion, or burnup, calculations through the :mod:`openmc.deplete` Python module. OpenMC uses transmutation reaction rates to solve a set of transmutation equations that determine the evolution of nuclide densities within a material. The -nuclide densities predicted as some future time are then used to determine +nuclide densities predicted at some future time are then used to determine updated reaction rates, and the process is repeated for as many timesteps as are requested. @@ -197,8 +197,8 @@ Transport-independent depletion .. note:: - This feature is still under heavy development and has yet to be verifed - code-to-code . API changes and feature additions are possible and likely in + This feature is still under heavy development and has yet to be rigorously + verified. API changes and feature additions are possible and likely in the near future. This category of operator uses pre-calculated one-group microscopic cross @@ -282,7 +282,7 @@ expects the units to be. The :class:`~openmc.deplete.MicroXS` class also include [0.01, 0.5]]) micro_xs = MicroXS.from_array(nuclides, reactions, data) -.. important :: +.. important:: Both :meth:`~openmc.deplete.MicroXS.from_csv()` and :meth:`~openmc.deplete.MicroXS.from_array()` assume the cross section values @@ -306,12 +306,12 @@ normalizing reaction rates: ``normalization_mode == source-rate``, and use ``power`` or ``power_density`` when ``normalization_mode == fission-q``. -1. ``soure-rate`` normalization, which assumes the ``source-rate`` provided by +1. ``source-rate`` normalization, which assumes the ``source_rate`` provided by the time integrator is a flux, and obtains the reaction rates by multiplying the cross-sections by the ``source-rate``. -2. ``fission-q`` normalization, which assumes the ``source-rate`` provided by - the time integrator is a power, and obtains the reaction rates by computing a - value for the flux based on this power. The general equation for the flux is +2. ``fission-q`` normalization, which uses the ``power`` or ``power_density`` + provided by the time integrator to obtain reaction rates by computing a value + for the flux based on this power. The general equation for the flux is .. math:: @@ -324,8 +324,7 @@ normalizing reaction rates: However, there is a method to converge to a more accurate value for flux by using substeps during time integration. `This paper `_ provides a - good discussion of this method. Hopefully such a method will be implemented - in OpenMC in the near future. + good discussion of this method. .. warning:: @@ -342,7 +341,7 @@ separate with respect to reaction rates. This can be useful for running many different cases of a particular scenario. However, running a depletion simulation with multiple materials using the ``fission-q`` normalization method treats each material as part of the same "reactor" due to how ``fission-q`` -normalization conglomerates energy values from each material to a single value. +normalization accumulates energy values from each material to a single value. This behavior may change in the future. Time integration From 89d79310bb527d4e6cc171693f4584c513643aee Mon Sep 17 00:00:00 2001 From: yardasol Date: Mon, 1 Aug 2022 12:44:28 -0500 Subject: [PATCH 110/131] revert DepletionOperator to TransportOperator; doc updates --- docs/source/pythonapi/deplete.rst | 14 ++++---- docs/source/usersguide/depletion.rst | 8 ++--- openmc/deplete/abc.py | 42 +++++++++++------------ openmc/deplete/coupled_operator.py | 8 ++--- openmc/deplete/helpers.py | 2 +- openmc/deplete/independent_operator.py | 6 ++-- openmc/deplete/openmc_operator.py | 6 ++-- openmc/deplete/stepresult.py | 2 +- tests/dummy_operator.py | 4 +-- tests/unit_tests/test_deplete_operator.py | 4 +-- 10 files changed, 48 insertions(+), 48 deletions(-) diff --git a/docs/source/pythonapi/deplete.rst b/docs/source/pythonapi/deplete.rst index a57ec915d..020251bd2 100644 --- a/docs/source/pythonapi/deplete.rst +++ b/docs/source/pythonapi/deplete.rst @@ -12,7 +12,7 @@ Primary API The two primary requirements to perform depletion with :mod:`openmc.deplete` are: - 1) A depletion operator + 1) A transpor operator 2) A time-integration scheme The former is responsible for calcuating and retaining important information required for depletion. The most common examples are reaction rates and power @@ -41,8 +41,8 @@ algorithms `_. SICELIIntegrator SILEQIIntegrator -Each of these classes expects a "depletion operator" to be passed. OpenMC -provides The following classes implementing depletion operators: +Each of these classes expects a "transport operator" to be passed. OpenMC +provides The following classes implementing transpor operators: .. autosummary:: :toctree: generated @@ -214,7 +214,7 @@ are stored in :class:`helpers.TalliedFissionYieldHelper` helpers.TalliedFissionYieldHelper -Methods common to OpenMC-specific implementations of :class:`DepletionOperator` +Methods common to OpenMC-specific implementations of :class:`TransportOperator` are stored in :class:`openmc_operator.OpenMCOperator` .. autosummary:: @@ -230,7 +230,7 @@ Abstract Base Classes A good starting point for extending capabilities in :mod:`openmc.deplete` is to examine the following abstract base classes. Custom classes can -inherit from :class:`abc.DepletionOperator` to implement alternative +inherit from :class:`abc.TransportOperator` to implement alternative schemes for collecting reaction rates and other data prior to depleting materials @@ -239,12 +239,12 @@ materials :nosignatures: :template: mycallable.rst - abc.DepletionOperator + abc.TransportOperator The following classes are abstract classes used to pass information from transport simulations (in the case of transport-coupled depletion) or to simply calculate these quantities directly (in the case of -transport-independent depletion) back on to the :class:`abc.DepletionOperator` +transport-independent depletion) back on to the :class:`abc.TransportOperator` .. autosummary:: :toctree: generated diff --git a/docs/source/usersguide/depletion.rst b/docs/source/usersguide/depletion.rst index 5052f7263..d39ebb83e 100644 --- a/docs/source/usersguide/depletion.rst +++ b/docs/source/usersguide/depletion.rst @@ -13,14 +13,14 @@ updated reaction rates, and the process is repeated for as many timesteps as are requested. The depletion module is designed such that the reaction rate solution (the -depletion "operator") is completely isolated from the solution of the transmutation -equations and the method used for advancing time. +transport "operator") is completely isolated from the solution of the +transmutation equations and the method used for advancing time. :mod:`openmc.deplete` supports multiple time-integration methods for determining material compositions over time. Each method appears as a different class. For example, :class:`openmc.deplete.CECMIntegrator` runs a depletion calculation using the CE/CM algorithm (deplete over a timestep using the middle-of-step -reaction rates). An instance of :class:`~openmc.deplete.abc.DepletionOperator` +reaction rates). An instance of :class:`~openmc.deplete.abc.TransportOperator` is passed to one of these functions along with the timesteps and power level:: power = 1200.0e6 # watts @@ -37,7 +37,7 @@ time:: time, keff = results.get_keff() Note that the coupling between the reaction rate solver and the transmutation -solver happens in-memory rather than by reading/writing files on disk. OpenMC has two categories of depletion operators for obtaining transmutation reaction +solver happens in-memory rather than by reading/writing files on disk. OpenMC has two categories of transport operators for obtaining transmutation reaction rates. .. _coupled-depletion: diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 8439fab34..8cf0eceb2 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -28,7 +28,7 @@ from .pool import deplete __all__ = [ - "OperatorResult", "DepletionOperator", + "OperatorResult", "TransportOperator", "ReactionRateHelper", "NormalizationHelper", "FissionYieldHelper", "Integrator", "SIIntegrator", "DepSystemSolver", "add_params"] @@ -75,13 +75,13 @@ def change_directory(output_dir): os.chdir(orig_dir) -class DepletionOperator(ABC): - """Abstract class defining a depletion operator +class TransportOperator(ABC): + """Abstract class defining a transport operator Each depletion integrator is written to work with a generic depletion operator that takes a vector of material compositions and returns an eigenvalue and reaction rates. This abstract class sets the requirements - for such a depletion operator. Users should instantiate + for such a transport operator. Users should instantiate :class:`openmc.deplete.CoupledOperator` or :class:`openmc.deplete.IndependentOperator` rather than this class. @@ -222,10 +222,10 @@ class ReactionRateHelper(ABC): ---------- n_nucs : int Number of burnable nuclides tracked by - :class:`openmc.deplete.abc.DepletionOperator` + :class:`openmc.deplete.abc.TransportOperator` n_react : int Number of reactions tracked by - :class:`openmc.deplete.abc.DepletionOperator` + :class:`openmc.deplete.abc.TransportOperator` Attributes ---------- @@ -294,7 +294,7 @@ class NormalizationHelper(ABC): """Abstract class for obtaining normalization factor on tallies This helper class determines how reaction rates calculated by an instance of - :class:`openmc.deplete.abc.DepletionOperator` should be normalized for the + :class:`openmc.deplete.abc.TransportOperator` should be normalized for the purpose of constructing a burnup matrix. Based on the method chosen, the power or source rate provided by the user, and reaction rates from a :class:`ReactionRateHelper`, this class will scale reaction rates to the @@ -304,7 +304,7 @@ class NormalizationHelper(ABC): ---------- nuclides : list of str All nuclides with desired reaction rates. Ordered to be - consistent with :class:`openmc.deplete.abc.DepletionOperator` + consistent with :class:`openmc.deplete.abc.TransportOperator` """ @@ -319,7 +319,7 @@ class NormalizationHelper(ABC): """Perform work needed to obtain energy produced This method is called prior to calculating the reaction rates - in :meth:`openmc.deplete.abc.DepletionOperator.initial_condition`. Only + in :meth:`openmc.deplete.abc.TransportOperator.initial_condition`. Only used for energy-based normalization. Parameters @@ -433,7 +433,7 @@ class FissionYieldHelper(ABC): def unpack(): """Unpack tally data prior to compute fission yields. - Called after a :meth:`openmc.deplete.abc.DepletionOperator.__call__` + Called after a :meth:`openmc.deplete.abc.TransportOperator.__call__` routine during the normalization of reaction rates. Not necessary for all subclasses to implement, unless tallies @@ -466,13 +466,13 @@ class FissionYieldHelper(ABC): ---------- nuclides : iterable of str Nuclides with non-zero densities from the - :class:`openmc.deplete.abc.DepletionOperator` + :class:`openmc.deplete.abc.TransportOperator` Returns ------- nuclides : list of str Union of nuclides that the - :class:`openmc.deplete.abc.DepletionOperator` says have non-zero + :class:`openmc.deplete.abc.TransportOperator` says have non-zero densities at this stage and those that have yield data. Sorted by nuclide name @@ -488,7 +488,7 @@ class FissionYieldHelper(ABC): Parameters ---------- - operator : openmc.deplete.abc.DepletionOperator + operator : openmc.deplete.abc.TransportOperator Operator with a depletion chain kwargs: optional Additional keyword arguments to be used in constuction @@ -509,8 +509,8 @@ class Integrator(ABC): _params = r""" Parameters ---------- - operator : openmc.deplete.abc.DepletionOperator - Operator that calculates reaction rates + operator : openmc.deplete.abc.TransportOperator + Operator to perform transport simulations timesteps : iterable of float or iterable of tuple Array of timesteps. Note that values are not cumulative. The units are specified by the `timestep_units` argument when `timesteps` is an @@ -552,8 +552,8 @@ class Integrator(ABC): Attributes ---------- - operator : openmc.deplete.abc.DepletionOperator - Operator that calculates reaction rates + operator : openmc.deplete.abc.TransportOperator + Operator to perform transport simulations chain : openmc.deplete.Chain Depletion chain timesteps : iterable of float @@ -847,8 +847,8 @@ class SIIntegrator(Integrator): _params = r""" Parameters ---------- - operator : openmc.deplete.abc.DepletionOperator - Operator that calculates reaction rates + operator : openmc.deplete.abc.TransportOperator + Operator to perform transport simulations timesteps : iterable of float or iterable of tuple Array of timesteps. Note that values are not cumulative. The units are specified by the `timestep_units` argument when `timesteps` is an @@ -893,8 +893,8 @@ class SIIntegrator(Integrator): Attributes ---------- - operator : openmc.deplete.abc.DepletionOperator - Operator that calculates reaction rates + operator : openmc.deplete.abc.TransportOperator + Operator to perform transport simulations chain : openmc.deplete.Chain Depletion chain timesteps : iterable of float diff --git a/openmc/deplete/coupled_operator.py b/openmc/deplete/coupled_operator.py index 448952b62..d5bb5b4bd 100644 --- a/openmc/deplete/coupled_operator.py +++ b/openmc/deplete/coupled_operator.py @@ -1,6 +1,6 @@ -"""Transport-coupled operator for depletion. +"""Transport-coupled transport operator for depletion. -This module implements a depletion operator coupled to OpenMC's transport solver +This module implements a transport operator coupled to OpenMC's transport solver so that it can be used by depletion integrators. The implementation makes use of the Python bindings to OpenMC's C API so that reading tally results and updating material number densities is all done in-memory instead of through the @@ -58,7 +58,7 @@ def _find_cross_sections(model): class CoupledOperator(OpenMCOperator): - """Transport-coupled depletion operator. + """Transport-coupled transport operator. Instances of this class can be used to perform transport-coupled depletion using OpenMC's transport solver. Normally, a user needn't call methods of @@ -199,7 +199,7 @@ class CoupledOperator(OpenMCOperator): "requires an openmc.Model object rather than the " \ "openmc.Geometry and openmc.Settings parameters. Please use " \ "the geometry and settings objects passed here to create a " \ - " model with which to generate the depletion Operator." + " model with which to generate the transport Operator." raise TypeError(msg) # Determine cross sections / depletion chain diff --git a/openmc/deplete/helpers.py b/openmc/deplete/helpers.py index 77f3da201..7b2d370ce 100644 --- a/openmc/deplete/helpers.py +++ b/openmc/deplete/helpers.py @@ -557,7 +557,7 @@ class ConstantFissionYieldHelper(FissionYieldHelper): Parameters ---------- - operator : openmc.deplete.abc.DepletionOperator + operator : openmc.deplete.abc.TransportOperator operator with a depletion chain kwargs: Additional keyword arguments to be used in construction diff --git a/openmc/deplete/independent_operator.py b/openmc/deplete/independent_operator.py index cd5a4d454..d2a9f04d0 100644 --- a/openmc/deplete/independent_operator.py +++ b/openmc/deplete/independent_operator.py @@ -1,6 +1,6 @@ -"""Transport-independent depletion operator. +"""Transport-independent transport operator for depletion. -This module implements a depletion operator that runs independently of any +This module implements a transport operator that runs independently of any transport solver by using user-provided one-group cross sections. """ @@ -23,7 +23,7 @@ from .results import Results from .helpers import ChainFissionHelper, ConstantFissionYieldHelper, SourceRateHelper class IndependentOperator(OpenMCOperator): - """Transport-independent depletion operator that uses one-group cross + """Transport-independent transport operator that uses one-group cross sections to calculate reaction rates. Instances of this class can be used to perform depletion using one-group diff --git a/openmc/deplete/openmc_operator.py b/openmc/deplete/openmc_operator.py index bcf73a010..6e6f8dd8d 100644 --- a/openmc/deplete/openmc_operator.py +++ b/openmc/deplete/openmc_operator.py @@ -1,7 +1,7 @@ """OpenMC transport operator This module implements functions shared by both OpenMC transport-coupled and -transport-independent depletion operators. +transport-independent transport operators. """ @@ -12,7 +12,7 @@ import numpy as np import openmc from openmc.mpi import comm -from .abc import DepletionOperator, OperatorResult +from .abc import TransportOperator, OperatorResult from .atom_number import AtomNumber from .reaction_rates import ReactionRates @@ -42,7 +42,7 @@ def _distribute(items): j += chunk_size -class OpenMCOperator(DepletionOperator): +class OpenMCOperator(TransportOperator): """Abstract class holding OpenMC-specific functions for running depletion calculations. diff --git a/openmc/deplete/stepresult.py b/openmc/deplete/stepresult.py index 3082cf304..c54f9edcf 100644 --- a/openmc/deplete/stepresult.py +++ b/openmc/deplete/stepresult.py @@ -462,7 +462,7 @@ class StepResult: Parameters ---------- - op : openmc.deplete.abc.DepletionOperator + op : openmc.deplete.abc.TransportOperator The operator used to generate these results. x : list of list of numpy.array The prior x vectors. Indexed [i][cell] using the above equation. diff --git a/tests/dummy_operator.py b/tests/dummy_operator.py index c54269cc0..1bb00129d 100644 --- a/tests/dummy_operator.py +++ b/tests/dummy_operator.py @@ -6,7 +6,7 @@ import scipy.sparse as sp from uncertainties import ufloat from openmc.deplete.reaction_rates import ReactionRates -from openmc.deplete.abc import DepletionOperator, OperatorResult +from openmc.deplete.abc import TransportOperator, OperatorResult from openmc.deplete import ( CECMIntegrator, PredictorIntegrator, CELIIntegrator, LEQIIntegrator, EPCRK4Integrator, CF4Integrator, SICELIIntegrator, SILEQIIntegrator @@ -117,7 +117,7 @@ class TestChain: return sp.csr_matrix(np.array([[a11, a12], [a21, a22]])) -class DummyOperator(DepletionOperator): +class DummyOperator(TransportOperator): """This is a dummy operator class with no statistical uncertainty. y_1' = sin(y_2) y_1 + cos(y_1) y_2 diff --git a/tests/unit_tests/test_deplete_operator.py b/tests/unit_tests/test_deplete_operator.py index 1910112b8..5fe8715ac 100644 --- a/tests/unit_tests/test_deplete_operator.py +++ b/tests/unit_tests/test_deplete_operator.py @@ -7,7 +7,7 @@ to a custom file with new depletion_chain node from pathlib import Path import pytest -from openmc.deplete.abc import DepletionOperator +from openmc.deplete.abc import TransportOperator from openmc.deplete.chain import Chain, _find_chain_file BARE_XS_FILE = "bare_cross_sections.xml" @@ -32,7 +32,7 @@ def bare_xs(run_in_tmpdir): yield BARE_XS_FILE -class BareDepleteOperator(DepletionOperator): +class BareDepleteOperator(TransportOperator): """Very basic class for testing the initialization.""" @staticmethod From 1c6fb99947ab1c451749e3c9be75398a59af6b6d Mon Sep 17 00:00:00 2001 From: yardasol Date: Mon, 1 Aug 2022 13:36:51 -0500 Subject: [PATCH 111/131] fix regression test microxs --- tests/regression_tests/microxs/test.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/regression_tests/microxs/test.py b/tests/regression_tests/microxs/test.py index bebe1c302..89fdfcaeb 100644 --- a/tests/regression_tests/microxs/test.py +++ b/tests/regression_tests/microxs/test.py @@ -47,6 +47,4 @@ def test_from_model(model): ref_xs = MicroXS.from_csv('test_reference.csv') test_xs = MicroXS.from_model(model, model.materials[0]) - assert ref_xs._units == test_xs._units np.testing.assert_allclose(test_xs, ref_xs, rtol=1e-11) - From 6865264abdca31302384bbd80d8eabfacdd3fe5b Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Tue, 2 Aug 2022 15:43:21 +0100 Subject: [PATCH 112/131] added option for year as time units --- openmc/deplete/results.py | 16 +++++++++------- tests/unit_tests/test_deplete_resultslist.py | 6 ++++-- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py index c238002fe..f56aee3ce 100644 --- a/openmc/deplete/results.py +++ b/openmc/deplete/results.py @@ -16,6 +16,8 @@ __all__ = ["Results", "ResultsList"] def _get_time_as(seconds, units): + if units == "y": + return seconds / (60 * 60 * 24 * 365.25) # 365.25 due to the leap year if units == "d": return seconds / (60 * 60 * 24) elif units == "h": @@ -95,7 +97,7 @@ class Results(list): Units for the returned concentration. Default is ``"atoms"`` .. versionadded:: 0.12 - time_units : {"s", "min", "h", "d"}, optional + time_units : {"s", "min", "h", "d", "y"}, optional Units for the returned time array. Default is ``"s"`` to return the value in seconds. @@ -109,7 +111,7 @@ class Results(list): Concentration of specified nuclide in units of ``nuc_units`` """ - cv.check_value("time_units", time_units, {"s", "d", "min", "h"}) + cv.check_value("time_units", time_units, {"s", "d", "min", "h", "y"}) cv.check_value("nuc_units", nuc_units, {"atoms", "atom/b-cm", "atom/cm3"}) @@ -190,7 +192,7 @@ class Results(list): Parameters ---------- - time_units : {"s", "d", "h", "min"}, optional + time_units : {"s", "d", "min", "h", "y"}, optional Desired units for the times array Returns @@ -203,7 +205,7 @@ class Results(list): 1 contains the associated uncertainty """ - cv.check_value("time_units", time_units, {"s", "d", "min", "h"}) + cv.check_value("time_units", time_units, {"s", "d", "min", "h", "y"}) times = np.empty_like(self, dtype=float) eigenvalues = np.empty((len(self), 2), dtype=float) @@ -257,7 +259,7 @@ class Results(list): Parameters ---------- - time_units : {"s", "d", "h", "min"}, optional + time_units : {"s", "d", "min", "h", "y"}, optional Return the vector in these units. Default is to convert to days @@ -267,7 +269,7 @@ class Results(list): 1-D vector of time points """ - cv.check_value("time_units", time_units, {"s", "d", "min", "h"}) + cv.check_value("time_units", time_units, {"s", "d", "min", "h", "y"}) times = np.fromiter( (r.time[0] for r in self), @@ -296,7 +298,7 @@ class Results(list): ---------- time : float Desired point in time - time_units : {"s", "d", "min", "h"}, optional + time_units : {"s", "d", "min", "h", "y"}, optional Units on ``time``. Default: days atol : float, optional Absolute tolerance (in ``time_units``) if ``time`` is not diff --git a/tests/unit_tests/test_deplete_resultslist.py b/tests/unit_tests/test_deplete_resultslist.py index 6ce15601f..86bf4ebc5 100644 --- a/tests/unit_tests/test_deplete_resultslist.py +++ b/tests/unit_tests/test_deplete_resultslist.py @@ -70,14 +70,16 @@ def test_get_keff(res): np.testing.assert_allclose(k[:, 1], u_ref) -@pytest.mark.parametrize("unit", ("s", "d", "min", "h")) +@pytest.mark.parametrize("unit", ("s", "d", "min", "h", "y")) def test_get_steps(unit): # Make a Results full of near-empty Result instances # Just fill out a time schedule results = openmc.deplete.Results() # Time in units of unit times = np.linspace(0, 100, num=5) - if unit == "d": + if unit == "y": + conversion_to_seconds = 60 * 60 * 24 * 365.25 + elif unit == "d": conversion_to_seconds = 60 * 60 * 24 elif unit == "h": conversion_to_seconds = 60 * 60 From 4a06d14b156b2e0d666f5f5faa293e9bc1356c33 Mon Sep 17 00:00:00 2001 From: Olek <45364492+yardasol@users.noreply.github.com> Date: Tue, 2 Aug 2022 12:28:00 -0500 Subject: [PATCH 113/131] Apply suggestions from code review Co-authored-by: Paul Romano --- docs/source/methods/depletion.rst | 3 ++- docs/source/pythonapi/deplete.rst | 5 +++-- docs/source/usersguide/depletion.rst | 13 ++++++++----- openmc/deplete/abc.py | 4 ++-- openmc/deplete/helpers.py | 1 - openmc/deplete/microxs.py | 2 -- 6 files changed, 15 insertions(+), 13 deletions(-) diff --git a/docs/source/methods/depletion.rst b/docs/source/methods/depletion.rst index 308a62e19..1b131bed5 100644 --- a/docs/source/methods/depletion.rst +++ b/docs/source/methods/depletion.rst @@ -108,7 +108,8 @@ transport-coupled depletion, the expense is driven almost entirely by the time to compute a transport solution, i.e., to evaluate :math:`\mathbf{A}` for a given :math:`\mathbf{n}`. Thus, the cost of a method scales with the number of :math:`\mathbf{A}` evaluations that are performed per timestep. On the other -hand, methods that require more evaluations generally achieve higher accuracy. The predictor method only requires one evaluation and its error converges as +hand, methods that require more evaluations generally achieve higher accuracy. +The predictor method only requires one evaluation and its error converges as :math:`\mathcal{O}(h)`. The CE/CM method requires two evaluations and is thus twice as expensive as the predictor method, but achieves an error of :math:`\mathcal{O}(h^2)`. An exhaustive description of time integration methods diff --git a/docs/source/pythonapi/deplete.rst b/docs/source/pythonapi/deplete.rst index 020251bd2..0b7faceb7 100644 --- a/docs/source/pythonapi/deplete.rst +++ b/docs/source/pythonapi/deplete.rst @@ -12,10 +12,11 @@ Primary API The two primary requirements to perform depletion with :mod:`openmc.deplete` are: - 1) A transpor operator + 1) A transport operator 2) A time-integration scheme -The former is responsible for calcuating and retaining important information required for depletion. The most common examples are reaction rates and power +The former is responsible for calculating and retaining important information +required for depletion. The most common examples are reaction rates and power normalization data. The latter is responsible for projecting reaction rates and compositions forward in calendar time across some step size :math:`\Delta t`, and obtaining new compositions given a power or power density. The diff --git a/docs/source/usersguide/depletion.rst b/docs/source/usersguide/depletion.rst index d39ebb83e..29bbcd1ed 100644 --- a/docs/source/usersguide/depletion.rst +++ b/docs/source/usersguide/depletion.rst @@ -37,7 +37,8 @@ time:: time, keff = results.get_keff() Note that the coupling between the reaction rate solver and the transmutation -solver happens in-memory rather than by reading/writing files on disk. OpenMC has two categories of transport operators for obtaining transmutation reaction +solver happens in-memory rather than by reading/writing files on disk. OpenMC +has two categories of transport operators for obtaining transmutation reaction rates. .. _coupled-depletion: @@ -195,7 +196,7 @@ across all material instances. Transport-independent depletion =============================== -.. note:: +.. warning:: This feature is still under heavy development and has yet to be rigorously verified. API changes and feature additions are possible and likely in @@ -218,7 +219,7 @@ and a path to a depletion chain file:: micro_xs = openmc.deplete.MicroXS() ... - op = IndependentOperator(materials, micro_xs, chain_file) + op = openmc.deplete.IndependentOperator(materials, micro_xs, chain_file) .. note:: @@ -270,8 +271,10 @@ Users can generate the one-group microscopic cross sections needed by The :meth:`~openmc.deplete.MicroXS.from_model()` method will produce a :class:`~openmc.deplete.MicroXS` object with microscopic cross section data in -units of ``b``, which is what :class:`~openmc.deplete.IndependentOperator` -expects the units to be. The :class:`~openmc.deplete.MicroXS` class also includes functions to read in cross section data directly from a ``.csv`` file or from data arrays:: +units of barns, which is what :class:`~openmc.deplete.IndependentOperator` +expects the units to be. The :class:`~openmc.deplete.MicroXS` class also +includes functions to read in cross section data directly from a ``.csv`` file +or from data arrays:: micro_xs = MicroXS.from_csv(micro_xs_path) diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 8cf0eceb2..e3587f052 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -78,7 +78,7 @@ def change_directory(output_dir): class TransportOperator(ABC): """Abstract class defining a transport operator - Each depletion integrator is written to work with a generic depletion + Each depletion integrator is written to work with a generic transport operator that takes a vector of material compositions and returns an eigenvalue and reaction rates. This abstract class sets the requirements for such a transport operator. Users should instantiate @@ -604,7 +604,7 @@ class Integrator(ABC): else: source_rates = [p*operator.heavy_metal for p in power_density] elif source_rates is None: - raise ValueError("Either power, power_density, source_rates must be set") + raise ValueError("Either power, power_density, or source_rates must be set") if not isinstance(source_rates, Iterable): # Ensure that rate is single value if that is the case diff --git a/openmc/deplete/helpers.py b/openmc/deplete/helpers.py index 7b2d370ce..1e382576e 100644 --- a/openmc/deplete/helpers.py +++ b/openmc/deplete/helpers.py @@ -615,7 +615,6 @@ class FissionYieldCutoffHelper(TalliedFissionYieldHelper): Default: 0.0253 [eV] fast_energy : float, optional Energy of yield data corresponding to fast yields. - Default: 500 [KeV] Attributes ---------- diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py index aef3c26c2..89ad5e03c 100644 --- a/openmc/deplete/microxs.py +++ b/openmc/deplete/microxs.py @@ -89,8 +89,6 @@ class MicroXS(DataFrame): df.rename({'mean': rxn}, axis=1, inplace=True) micro_xs = concat([micro_xs, df], axis=1) - micro_xs._units = 'b' - # Revert to the original tallies model.tallies = original_tallies From aa278ada262e88e6c470d44477c704010ccb9832 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Tue, 2 Aug 2022 20:13:14 +0100 Subject: [PATCH 114/131] [skip ci] @paulromano review suggestion change "y" to "a" --- openmc/deplete/results.py | 16 ++++++++-------- tests/unit_tests/test_deplete_resultslist.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py index f56aee3ce..6feac89b6 100644 --- a/openmc/deplete/results.py +++ b/openmc/deplete/results.py @@ -16,7 +16,7 @@ __all__ = ["Results", "ResultsList"] def _get_time_as(seconds, units): - if units == "y": + if units == "a": return seconds / (60 * 60 * 24 * 365.25) # 365.25 due to the leap year if units == "d": return seconds / (60 * 60 * 24) @@ -97,7 +97,7 @@ class Results(list): Units for the returned concentration. Default is ``"atoms"`` .. versionadded:: 0.12 - time_units : {"s", "min", "h", "d", "y"}, optional + time_units : {"s", "min", "h", "d", "a"}, optional Units for the returned time array. Default is ``"s"`` to return the value in seconds. @@ -111,7 +111,7 @@ class Results(list): Concentration of specified nuclide in units of ``nuc_units`` """ - cv.check_value("time_units", time_units, {"s", "d", "min", "h", "y"}) + cv.check_value("time_units", time_units, {"s", "d", "min", "h", "a"}) cv.check_value("nuc_units", nuc_units, {"atoms", "atom/b-cm", "atom/cm3"}) @@ -192,7 +192,7 @@ class Results(list): Parameters ---------- - time_units : {"s", "d", "min", "h", "y"}, optional + time_units : {"s", "d", "min", "h", "a"}, optional Desired units for the times array Returns @@ -205,7 +205,7 @@ class Results(list): 1 contains the associated uncertainty """ - cv.check_value("time_units", time_units, {"s", "d", "min", "h", "y"}) + cv.check_value("time_units", time_units, {"s", "d", "min", "h", "a"}) times = np.empty_like(self, dtype=float) eigenvalues = np.empty((len(self), 2), dtype=float) @@ -259,7 +259,7 @@ class Results(list): Parameters ---------- - time_units : {"s", "d", "min", "h", "y"}, optional + time_units : {"s", "d", "min", "h", "a"}, optional Return the vector in these units. Default is to convert to days @@ -269,7 +269,7 @@ class Results(list): 1-D vector of time points """ - cv.check_value("time_units", time_units, {"s", "d", "min", "h", "y"}) + cv.check_value("time_units", time_units, {"s", "d", "min", "h", "a"}) times = np.fromiter( (r.time[0] for r in self), @@ -298,7 +298,7 @@ class Results(list): ---------- time : float Desired point in time - time_units : {"s", "d", "min", "h", "y"}, optional + time_units : {"s", "d", "min", "h", "a"}, optional Units on ``time``. Default: days atol : float, optional Absolute tolerance (in ``time_units``) if ``time`` is not diff --git a/tests/unit_tests/test_deplete_resultslist.py b/tests/unit_tests/test_deplete_resultslist.py index 86bf4ebc5..175f75b21 100644 --- a/tests/unit_tests/test_deplete_resultslist.py +++ b/tests/unit_tests/test_deplete_resultslist.py @@ -70,7 +70,7 @@ def test_get_keff(res): np.testing.assert_allclose(k[:, 1], u_ref) -@pytest.mark.parametrize("unit", ("s", "d", "min", "h", "y")) +@pytest.mark.parametrize("unit", ("s", "d", "min", "h", "a")) def test_get_steps(unit): # Make a Results full of near-empty Result instances # Just fill out a time schedule From 3c22a405fee0ebed2600a5ccacb1bd8f73939e7e Mon Sep 17 00:00:00 2001 From: shimwell Date: Tue, 2 Aug 2022 20:29:19 +0100 Subject: [PATCH 115/131] added doc strings for time_units --- openmc/deplete/results.py | 23 ++++++++++++++++---- tests/unit_tests/test_deplete_resultslist.py | 2 +- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py index 6feac89b6..017c9c668 100644 --- a/openmc/deplete/results.py +++ b/openmc/deplete/results.py @@ -16,6 +16,17 @@ __all__ = ["Results", "ResultsList"] def _get_time_as(seconds, units): + """Converts the time in seconds to time in different units + + Parameters + ---------- + seconds : float + The time to convert expressed in seconds + units : {"s", "min", "h", "d", "a"} + The units to convert time into. Available options are seconds ``"s"``, + minutes ``"min"``, hours ``"hours"`` days ``"d"``, years ``"a"`` + + """ if units == "a": return seconds / (60 * 60 * 24 * 365.25) # 365.25 due to the leap year if units == "d": @@ -99,7 +110,8 @@ class Results(list): .. versionadded:: 0.12 time_units : {"s", "min", "h", "d", "a"}, optional Units for the returned time array. Default is ``"s"`` to - return the value in seconds. + return the value in seconds. Other options are minutes ``"min"``, + hours ``"hours"``, days ``"d"``, years ``"a"`` .. versionadded:: 0.12 @@ -193,7 +205,8 @@ class Results(list): Parameters ---------- time_units : {"s", "d", "min", "h", "a"}, optional - Desired units for the times array + Desired units for the times array. Options are seconds ``"s"`` + minutes ``"min"``, hours ``"hours"``, days ``"d"``, years ``"a"`` Returns ------- @@ -261,7 +274,8 @@ class Results(list): ---------- time_units : {"s", "d", "min", "h", "a"}, optional Return the vector in these units. Default is to - convert to days + convert to days ``"d"``. Other options are seconds ``"s"``, minutes + ``"min"``, hours ``"hours"``, years ``"a"`` Returns ------- @@ -299,7 +313,8 @@ class Results(list): time : float Desired point in time time_units : {"s", "d", "min", "h", "a"}, optional - Units on ``time``. Default: days + Units on ``time``. Default: days ``"d"``. Other options are seconds + ``"s"``, minutes ``"min"``, hours ``"hours"`` and years ``"a"`` atol : float, optional Absolute tolerance (in ``time_units``) if ``time`` is not found. diff --git a/tests/unit_tests/test_deplete_resultslist.py b/tests/unit_tests/test_deplete_resultslist.py index 175f75b21..4d8808c2f 100644 --- a/tests/unit_tests/test_deplete_resultslist.py +++ b/tests/unit_tests/test_deplete_resultslist.py @@ -77,7 +77,7 @@ def test_get_steps(unit): results = openmc.deplete.Results() # Time in units of unit times = np.linspace(0, 100, num=5) - if unit == "y": + if unit == "a": conversion_to_seconds = 60 * 60 * 24 * 365.25 elif unit == "d": conversion_to_seconds = 60 * 60 * 24 From ea926ef84a2b5c89a661c04f957269aecfc3f6bd Mon Sep 17 00:00:00 2001 From: shimwell Date: Tue, 2 Aug 2022 20:36:42 +0100 Subject: [PATCH 116/131] specified that year is Julian in docstring --- openmc/deplete/results.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py index 017c9c668..9d4913300 100644 --- a/openmc/deplete/results.py +++ b/openmc/deplete/results.py @@ -111,7 +111,8 @@ class Results(list): time_units : {"s", "min", "h", "d", "a"}, optional Units for the returned time array. Default is ``"s"`` to return the value in seconds. Other options are minutes ``"min"``, - hours ``"hours"``, days ``"d"``, years ``"a"`` + hours ``"hours"``, days ``"d"``, years ``"a"`` (Julian year 365.25 + days). .. versionadded:: 0.12 @@ -207,6 +208,7 @@ class Results(list): time_units : {"s", "d", "min", "h", "a"}, optional Desired units for the times array. Options are seconds ``"s"`` minutes ``"min"``, hours ``"hours"``, days ``"d"``, years ``"a"`` + (Julian year 365.25 days). Returns ------- @@ -275,7 +277,8 @@ class Results(list): time_units : {"s", "d", "min", "h", "a"}, optional Return the vector in these units. Default is to convert to days ``"d"``. Other options are seconds ``"s"``, minutes - ``"min"``, hours ``"hours"``, years ``"a"`` + ``"min"``, hours ``"hours"``, years ``"a"`` (Julian year 365.25 + days). Returns ------- @@ -315,6 +318,7 @@ class Results(list): time_units : {"s", "d", "min", "h", "a"}, optional Units on ``time``. Default: days ``"d"``. Other options are seconds ``"s"``, minutes ``"min"``, hours ``"hours"`` and years ``"a"`` + (Julian year 365.25 days) atol : float, optional Absolute tolerance (in ``time_units``) if ``time`` is not found. From 8965dd1e66f53db42f607e165c504ba354d65c64 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 2 Aug 2022 22:11:37 -0500 Subject: [PATCH 117/131] Check for macroscopic data in materials when running Universe.plot --- openmc/universe.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openmc/universe.py b/openmc/universe.py index 78f75bbfc..7dee19313 100644 --- a/openmc/universe.py +++ b/openmc/universe.py @@ -334,6 +334,13 @@ class Universe(UniverseBase): if seed is not None: model.settings.seed = seed + # Determine whether any materials contains macroscopic data and if + # so, set energy mode accordingly + for mat in self.get_all_materials().values(): + if mat._macroscopic is not None: + model.settings.energy_mode = 'multi-group' + break + # Create plot object matching passed arguments plot = openmc.Plot() plot.origin = origin From 17482d4fbe6a61aafb5369693549ef86bd04ec93 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 3 Aug 2022 06:45:38 -0500 Subject: [PATCH 118/131] Change ordering of halfspaces for RectangularParallelepiped -/+ --- openmc/model/surface_composite.py | 4 ++-- tests/regression_tests/torus/inputs_true.dat | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openmc/model/surface_composite.py b/openmc/model/surface_composite.py index 6271154fb..51019b0d8 100644 --- a/openmc/model/surface_composite.py +++ b/openmc/model/surface_composite.py @@ -451,10 +451,10 @@ class RectangularParallelepiped(CompositeSurface): self.zmax = openmc.ZPlane(z0=zmax, **kwargs) def __neg__(self): - return +self.xmin & -self.xmax & +self.ymin & -self.ymax & +self.zmin & -self.zmax + return -self.xmax & +self.xmin & -self.ymax & +self.ymin & -self.zmax & +self.zmin def __pos__(self): - return -self.xmin | +self.xmax | -self.ymin | +self.ymax | -self.zmin | +self.zmax + return +self.xmax | -self.xmin | +self.ymax | -self.ymin | +self.zmax | -self.zmin class XConeOneSided(CompositeSurface): diff --git a/tests/regression_tests/torus/inputs_true.dat b/tests/regression_tests/torus/inputs_true.dat index 0045d7e46..0ccbda400 100644 --- a/tests/regression_tests/torus/inputs_true.dat +++ b/tests/regression_tests/torus/inputs_true.dat @@ -3,7 +3,7 @@ - + From aa5526aab6e8ea75af389cf63daf9a43f4fff196 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Wed, 3 Aug 2022 13:49:30 +0100 Subject: [PATCH 119/131] Review suggestions from @paulromano Co-authored-by: Paul Romano --- openmc/deplete/results.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py index 9d4913300..7e1c6431e 100644 --- a/openmc/deplete/results.py +++ b/openmc/deplete/results.py @@ -24,7 +24,7 @@ def _get_time_as(seconds, units): The time to convert expressed in seconds units : {"s", "min", "h", "d", "a"} The units to convert time into. Available options are seconds ``"s"``, - minutes ``"min"``, hours ``"hours"`` days ``"d"``, years ``"a"`` + minutes ``"min"``, hours ``"h"`` days ``"d"``, Julian years ``"a"`` """ if units == "a": @@ -111,8 +111,7 @@ class Results(list): time_units : {"s", "min", "h", "d", "a"}, optional Units for the returned time array. Default is ``"s"`` to return the value in seconds. Other options are minutes ``"min"``, - hours ``"hours"``, days ``"d"``, years ``"a"`` (Julian year 365.25 - days). + hours ``"h"``, days ``"d"``, and Julian years ``"a"``. .. versionadded:: 0.12 @@ -206,9 +205,9 @@ class Results(list): Parameters ---------- time_units : {"s", "d", "min", "h", "a"}, optional - Desired units for the times array. Options are seconds ``"s"`` - minutes ``"min"``, hours ``"hours"``, days ``"d"``, years ``"a"`` - (Julian year 365.25 days). + Desired units for the times array. Options are seconds ``"s"``, + minutes ``"min"``, hours ``"h"``, days ``"d"``, and Julian years + ``"a"``. Returns ------- @@ -277,8 +276,7 @@ class Results(list): time_units : {"s", "d", "min", "h", "a"}, optional Return the vector in these units. Default is to convert to days ``"d"``. Other options are seconds ``"s"``, minutes - ``"min"``, hours ``"hours"``, years ``"a"`` (Julian year 365.25 - days). + ``"min"``, hours ``"h"``, days ``"d"``, and Julian years ``"a"``. Returns ------- @@ -317,8 +315,7 @@ class Results(list): Desired point in time time_units : {"s", "d", "min", "h", "a"}, optional Units on ``time``. Default: days ``"d"``. Other options are seconds - ``"s"``, minutes ``"min"``, hours ``"hours"`` and years ``"a"`` - (Julian year 365.25 days) + ``"s"``, minutes ``"min"``, hours ``"h"`` and Julian years ``"a"``. atol : float, optional Absolute tolerance (in ``time_units``) if ``time`` is not found. From 65624df58da7a2cf63cc4dcc6299ef0add974e15 Mon Sep 17 00:00:00 2001 From: yardasol Date: Wed, 3 Aug 2022 12:30:02 -0500 Subject: [PATCH 120/131] add chain_file, dilute_initial parameters to MicroXS.from_model --- docs/source/usersguide/depletion.rst | 6 +- openmc/deplete/microxs.py | 84 +++++++++++++++--- tests/micro_xs_simple.csv | 18 ++-- .../test_reference_fission_q.h5 | Bin 35688 -> 36312 bytes .../test_reference_source_rate.h5 | Bin 35688 -> 36312 bytes tests/regression_tests/microxs/test.py | 8 +- .../microxs/test_reference.csv | 20 +++-- 7 files changed, 107 insertions(+), 29 deletions(-) diff --git a/docs/source/usersguide/depletion.rst b/docs/source/usersguide/depletion.rst index 29bbcd1ed..821ff338b 100644 --- a/docs/source/usersguide/depletion.rst +++ b/docs/source/usersguide/depletion.rst @@ -216,7 +216,7 @@ and a path to a depletion chain file:: materials = openmc.Materials() ... - micro_xs = openmc.deplete.MicroXS() + micro_xs = openmc.deplete.MicroXS ... op = openmc.deplete.IndependentOperator(materials, micro_xs, chain_file) @@ -267,7 +267,9 @@ Users can generate the one-group microscopic cross sections needed by model = openmc.Model.from_xml() - micro_xs = openmc.deplete.MicroXS.from_model(model, model.materials[0]) + micro_xs = openmc.deplete.MicroXS.from_model(model, + model.materials[0], + chain_file) The :meth:`~openmc.deplete.MicroXS.from_model()` method will produce a :class:`~openmc.deplete.MicroXS` object with microscopic cross section data in diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py index 89ad5e03c..09de090bf 100644 --- a/openmc/deplete/microxs.py +++ b/openmc/deplete/microxs.py @@ -8,13 +8,16 @@ import tempfile from pathlib import Path from pandas import DataFrame, read_csv, concat -from numpy import ndarray +import numpy as np from openmc.checkvalue import check_type, check_value, check_iterable_type from openmc.mgxs import EnergyGroups, ArbitraryXS, FissionXS -from openmc import Tallies, StatePoint +from openmc.data import DataLibrary +from openmc import Tallies, StatePoint, Materials -from .chain import REACTIONS + +from .chain import Chain, REACTIONS +from .coupled_operator import _find_cross_sections _valid_rxns = list(REACTIONS) _valid_rxns.append('fission') @@ -29,13 +32,8 @@ class MicroXS(DataFrame): def from_model(cls, model, reaction_domain, - reactions=['(n,gamma)', - '(n,2n)', - '(n,p)', - '(n,a)', - '(n,3n)', - '(n,4n)', - 'fission'], + chain_file, + dilute_initial=1.0e3, energy_bounds=(0, 20e6)): """Generate a one-group cross-section dataframe using OpenMC. Note that the ``openmc`` executable must be compiled. @@ -46,6 +44,14 @@ class MicroXS(DataFrame): OpenMC model object. Must contain geometry, materials, and settings. reaction_domain : openmc.Material or openmc.Cell or openmc.Universe or openmc.RegularMesh Domain in which to tally reaction rates. + chain_file : str + Path to the depletion chain XML file that will be used in depletion + simulation. Used to determine cross sections for materials not + present in the inital composition. + dilute_initial : float + Initial atom density [atoms/cm^3] to add for nuclides that + are zero in initial condition to ensure they exist in the cross + section data. Only done for nuclides with reaction rates. reactions : list of str, optional Reaction names to tally energy_bound : 2-tuple of float, optional @@ -63,6 +69,12 @@ class MicroXS(DataFrame): original_tallies = model.tallies tallies = Tallies() xs = {} + reactions, diluted_materials = cls._add_dilute_nuclides(chain_file, + model, + dilute_initial) + diluted_materials.export_to_xml('diluted_materials.xml') + model.materials = diluted_materials + for rxn in reactions: if rxn == 'fission': xs[rxn] = FissionXS(domain=reaction_domain, groups=groups, by_nuclide=True) @@ -94,6 +106,56 @@ class MicroXS(DataFrame): return micro_xs + @classmethod + def _add_dilute_nuclides(cls, chain_file, model, dilute_initial): + chain = Chain.from_xml(chain_file) + reactions = chain.reactions + cross_sections = _find_cross_sections(model) + nuclides_with_data = cls._get_nuclides_with_data(cross_sections) + burnable_nucs = [nuc.name for nuc in chain.nuclides + if nuc.name in nuclides_with_data] + diluted_materials = Materials() + for material in model.materials: + if material.depletable: + nuc_densities = material.get_nuclide_atom_densities() + dilute_density = 1.E-24 * dilute_initial + material.set_density('sum') + for nuc, density in nuc_densities.items(): + material.remove_nuclide(nuc) + material.add_nuclide(nuc, density) + for burn_nuc in burnable_nucs: + if burn_nuc not in nuc_densities: + material.add_nuclide(burn_nuc, + dilute_density) + diluted_materials.append(material) + return reactions, diluted_materials + + @staticmethod + def _get_nuclides_with_data(cross_sections): + """Loads cross_sections.xml file to find nuclides with neutron data + + Parameters + ---------- + cross_sections : str + Path to cross_sections.xml file + + Returns + ------- + nuclides : set of str + Set of nuclide names that have cross secton data + + """ + nuclides = set() + data_lib = DataLibrary.from_xml(cross_sections) + for library in data_lib.libraries: + if library['type'] != 'neutron': + continue + for name in library['materials']: + if name not in nuclides: + nuclides.add(name) + + return nuclides + @classmethod def from_array(cls, nuclides, reactions, data): """ @@ -162,6 +224,6 @@ class MicroXS(DataFrame): def _validate_micro_xs_inputs(nuclides, reactions, data): check_iterable_type('nuclides', nuclides, str) check_iterable_type('reactions', reactions, str) - check_type('data', data, ndarray, expected_iter_type=float) + check_type('data', data, np.ndarray, expected_iter_type=float) for reaction in reactions: check_value('reactions', reaction, _valid_rxns) diff --git a/tests/micro_xs_simple.csv b/tests/micro_xs_simple.csv index bc43600ba..787ce74c3 100644 --- a/tests/micro_xs_simple.csv +++ b/tests/micro_xs_simple.csv @@ -1,7 +1,13 @@ nuclide,"(n,gamma)",fission -U234,23.518634203050645,0.49531930067650976 -U235,10.621118186344665,49.10955932965905 -U238,0.8652742788116043,0.10579281644765708 -U236,9.095623870006154,0.32315392339237936 -O16,0.0029535689693132015,0.0 -O17,0.05980775766572907,0.0 +U234,22.231989822002465,0.49620744663749855 +U235,10.479008971197121,48.41787337164604 +U238,0.8673334105437324,0.10467880588762352 +U236,8.65171044607122,0.31948392400019293 +O16,7.497851000107524e-05,0.0 +O17,0.0004079227797153371,0.0 +I135,6.842395323713927,0.0 +Xe135,227463.86426990604,0.0 +Xe136,0.02317896034753588,0.0 +Cs135,2.1721665580713623,0.0 +Gd157,12786.09939237018,0.0 +Gd156,3.4006085445846983,0.0 diff --git a/tests/regression_tests/deplete_no_transport/test_reference_fission_q.h5 b/tests/regression_tests/deplete_no_transport/test_reference_fission_q.h5 index 6c1b3de849e1a8792b9ce9398c7e2e41ae410b5e..82bf11567363a56b84f87a204df7ea79d6f7e124 100644 GIT binary patch delta 1568 zcmY+EVN6?96vz860`E2(Ey0yX4du<%wGIXaYoXLa%OkFCuoaDFCMp%TRf&t$;DR_E zq953xjP@khxEK*-VGs@E4XukIG=!y@2`L7(!Kg5^SjkMfFQfayY~Q_Y*Cx&F`Jdl8 z_kZ5Ix3Mu4y@W>fDC+ec7(uK6KU&|MiwrOjFH+(`8ToLqY3~&Iyg(jG*nJ@lcG5&R znW*p=9!Cj_FEl2`3zhkJqlP3MbD?dXER}(y1 zp2kz-qW*H|GrMud5M0~$G}v~@L9T6r2yej7y*Ye>PxY4vHAIc_Qo$haUcU+$i;$ew$hEq|!hZ8B0y3xvXgd&=Vu;rv}qp)-ZKK z`z6SD7Fin1S|x;of}Se!Bh3Y99IW<*J_G}gaYm3H$)b*xPQap$14A;ctS9xHSj%oR z;H={iZxp-h#t;gc;70KYIQ68uuGdJYfQiy%k%j48>D}WrRwnP%UEuRrdA1U+nag+uxon9x4EM`moGss;uiZ=T~3ozc| zK;a?SYB8b6F!XioPx%m?!t8El)EBD9%3*>huUJPt;=CDTsw1Mn<20*;J1sgi6GKN>RisHV*7(eIZxP2X5w< zr8m~hC+^?wx+HX3R~9aJ9cx-L`=|en{kdjrQm}i6{(1kr`TBDwReXLNE?zizuBT_d z`Q6fX)e{X>0$ct2*TI-km_Xat-M8WUz(jOM%D%4`P~V-G zztOD`%&*O~xBWHMX=>ZJ6i5Zx~VS5pn^ydEcM22uIF2!uMFvO2rc*L_xNh zEQ%Yts2y&R(r=zT4~L?yveC1E5PMkZ{$wR(~O*Vcg6s)J+RugZOaN(QCgfHU3OD0d0Y9F>^}40uhz z3ta90ulEpbfCou1VUb!kNTX9yR)lacgt#~?1&^RRaj>5_^yM?DCmENN_F^$kU>7`i z!Hd|0;%#Eo^7AKttip5Dr-hG#R>UXdlc)FY!^y`~6AC5d_-LgDr>M#*tO-XFv;?@I zHprV7a~Q8a1{jc-kndk^Vx7mN3jW;}@A_6ImUd=;==b;aJ$j%CuKDD%j`82_c{(G! zWAfX&X`OU0>{N zX85-El>g?nOVukAYrdz)+jf{=Je{x2t$jT*GVS}-vUBf>bIY%^1)t35;MNsxPri=- E0CJB zS-`}>=(6t7r4c@q7?x!i!?+V=nCN^nK z|IhP#p7Ve1J*S09ls}5bohYA3ZW=?Z0H1fiHk&)Zz)GD7kLbvUgN4vek=Lj6s2EC0 zHXNY|SxGca)J9R!my{;Om0HuC_^OK}!?V&&X2QrKw+jqLo8hX57PZ6W1{;n(L}VNm zY8^0aWSNXu&jt|4^{Ia6YLaAVNrdXSnFKf*Pd8R`GS!7OB+gTgTU*Tqsg7Bb(iFrS zV!+yiAU9ZGnK$DC-Gvxp#vx?hgUc3R*8HT37tI8ZSxEDYD76Ika|w^ zus0cqv~h@U7yH{L5z2VsoBEev_hw7mU^}5wUP_ZiC)2sK-B+kKDR=4*;PtGbQVHk0 z@8S_3sf?=^2DyFed#IGba_65Y(hG}SCt9*3@+d6#cv0>M?CslF@*zfqQ+F}rNvQ*yju2eAVtpNmOO%o6zMK)KY1RbSdmLyw zA+ z{yQjM*zS)$JsSDsd%geX9WR``w0WEVm!Z=i9pCbXe_-b8(}m~i{a3ykXrGf{>~QP; z^z&x}hi)wY_!C5h8*jfgd-T{YVe0&~^TwL9!UwBYUb%EIEA0GX=tFY!@8~eUPbdeJONFoT}zq2 zKKr6u)2^wp?f1Sjx1n6{aWrt{LEmp>J#&FuTtuF-*5-TmKlAVy}SMqDrHdGg(gz6`zneJ!I{A`w+rvFP;N9CXBaXj_RH!C#bL-AP$VR) z?Zzl|@Bz)5Ylxx9K3UBu|7Iv^4(x;YWI1)M3456nPs*xthVHd z$(XcXWi0d&Apu_rT`=9mu?fY@4I;>!vy;W`#90Zf46d2XE(@7!n^}on2Fx~D#UWbz0eGsvB9mvwKWjp+1c^1kY94vN( zASW2-8norvt&p?wc%hSQzN{Z0igxO;rVgTdJK>j|SD`Rq1Y5HcJGwT(d8ZD~>?G=f zQ%_WMagcL`VA{t)!DoWJz~ii&Ahd;$TMuMhM0L1fb=L?;yUE$C?AZh-x?$CPLN0J1 z>{-5)pj}npTD?mWM86=Wr?ftyVlCA{69y zkwtYwFSVoHvic4<44kp9z&_uIc1B(-?Il6H;^VX%(aWPmk7Dc1AjIv}x{rg!Z|lj^ zTT$wzFwMyOH)jk{Tk4n9zd*8KTnmIr;Z3~WPk8z=ru*UTwo|wk1oHzAQ0@R++y5zR zfT2K__BW%T8(PvYETsvSjAO7d)@5ie4e(^xx>rDzFo z&R|uF7fKi}KL%Kllu|a{cXFQlzhy7vPQMt8UtIe0$`6KMI&-u8ZhAO4cI5hxqjz2l z#&?`g9FLlVEf42A$NqqiXFZqB-}!#<@ar3W z{Pyb=(eruR+U3bUG4t6{{Db&&Vzl+*ckLZ(qJOr$ZS-D$_;_@9<@dXn!~d6FqUpzJ X`lt2u#emio+?X}PHy`p_Ds}t|vOiXw diff --git a/tests/regression_tests/microxs/test.py b/tests/regression_tests/microxs/test.py index 89fdfcaeb..dbfe3357b 100644 --- a/tests/regression_tests/microxs/test.py +++ b/tests/regression_tests/microxs/test.py @@ -1,10 +1,14 @@ """Test one-group cross section generation""" +from pathlib import Path + import numpy as np import pytest import openmc from openmc.deplete import MicroXS +CHAIN_FILE = Path(__file__).parents[2] / "chain_simple.xml" + @pytest.fixture(scope="module") def model(): fuel = openmc.Material(name="uo2") @@ -24,8 +28,6 @@ def model(): radii = [0.42, 0.45] fuel.volume = np.pi * radii[0] ** 2 - clad.volume = np.pi * (radii[1]**2 - radii[0]**2) - water.volume = 1.24**2 - (np.pi * radii[1]**2) materials = openmc.Materials([fuel, clad, water]) @@ -45,6 +47,6 @@ def model(): def test_from_model(model): ref_xs = MicroXS.from_csv('test_reference.csv') - test_xs = MicroXS.from_model(model, model.materials[0]) + test_xs = MicroXS.from_model(model, model.materials[0], CHAIN_FILE) np.testing.assert_allclose(test_xs, ref_xs, rtol=1e-11) diff --git a/tests/regression_tests/microxs/test_reference.csv b/tests/regression_tests/microxs/test_reference.csv index 0c47409b0..787ce74c3 100644 --- a/tests/regression_tests/microxs/test_reference.csv +++ b/tests/regression_tests/microxs/test_reference.csv @@ -1,7 +1,13 @@ -nuclide,"(n,gamma)","(n,2n)","(n,p)","(n,a)","(n,3n)","(n,4n)",fission -U234,23.518634203050674,0.0008255330840536984,0.0,0.0,9.404554397521455e-07,0.0,0.49531930067650964 -U235,10.621118186344795,0.004359401013759254,0.0,0.0,7.2974901306692395e-06,0.0,49.10955932965902 -U238,0.8652742788116055,0.005661917442209096,0.0,0.0,4.92273921631416e-05,0.0,0.10579281644765708 -U236,9.095623870006163,0.0024373322002926834,0.0,0.0,1.966889146690413e-05,0.0,0.3231539233923791 -O16,7.511380881289377e-05,0.0,1.3764104470470622e-05,0.002862620940027927,0.0,0.0,0.0 -O17,0.00041221042693945085,1.9826700699084533e-05,7.764409141772239e-06,0.05938517754333328,0.0,0.0,0.0 +nuclide,"(n,gamma)",fission +U234,22.231989822002465,0.49620744663749855 +U235,10.479008971197121,48.41787337164604 +U238,0.8673334105437324,0.10467880588762352 +U236,8.65171044607122,0.31948392400019293 +O16,7.497851000107524e-05,0.0 +O17,0.0004079227797153371,0.0 +I135,6.842395323713927,0.0 +Xe135,227463.86426990604,0.0 +Xe136,0.02317896034753588,0.0 +Cs135,2.1721665580713623,0.0 +Gd157,12786.09939237018,0.0 +Gd156,3.4006085445846983,0.0 From cbb0ee9964b081d8a4686fceaa265a550036d693 Mon Sep 17 00:00:00 2001 From: Olek <45364492+yardasol@users.noreply.github.com> Date: Wed, 3 Aug 2022 16:45:25 -0500 Subject: [PATCH 121/131] Apply suggestions from code review Co-authored-by: Paul Romano --- openmc/deplete/microxs.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py index 09de090bf..75058fce0 100644 --- a/openmc/deplete/microxs.py +++ b/openmc/deplete/microxs.py @@ -72,7 +72,6 @@ class MicroXS(DataFrame): reactions, diluted_materials = cls._add_dilute_nuclides(chain_file, model, dilute_initial) - diluted_materials.export_to_xml('diluted_materials.xml') model.materials = diluted_materials for rxn in reactions: @@ -113,12 +112,12 @@ class MicroXS(DataFrame): cross_sections = _find_cross_sections(model) nuclides_with_data = cls._get_nuclides_with_data(cross_sections) burnable_nucs = [nuc.name for nuc in chain.nuclides - if nuc.name in nuclides_with_data] + if nuc.name in nuclides_with_data] diluted_materials = Materials() for material in model.materials: if material.depletable: nuc_densities = material.get_nuclide_atom_densities() - dilute_density = 1.E-24 * dilute_initial + dilute_density = 1.e-24 * dilute_initial material.set_density('sum') for nuc, density in nuc_densities.items(): material.remove_nuclide(nuc) From 15f66ed50af45b140cceb7d20070d8e9968a3a20 Mon Sep 17 00:00:00 2001 From: Olek <45364492+yardasol@users.noreply.github.com> Date: Wed, 3 Aug 2022 17:46:26 -0500 Subject: [PATCH 122/131] Minor styling fix in `Material.py` change `E` to `e` for scientific notation. --- openmc/material.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openmc/material.py b/openmc/material.py index 6c2641680..913d53d9d 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -861,7 +861,7 @@ class Material(IDManagerMixin): elif self.density_units == 'atom/b-cm': density = self.density elif self.density_units == 'atom/cm3' or self.density_units == 'atom/cc': - density = 1.E-24 * self.density + density = 1.e-24 * self.density # For ease of processing split out nuc, nuc_density, # and nuc_density_type into separate arrays @@ -897,7 +897,7 @@ class Material(IDManagerMixin): # Convert the mass density to an atom density if not density_in_atom: - density = -density / self.average_molar_mass * 1.E-24 \ + density = -density / self.average_molar_mass * 1.e-24 \ * openmc.data.AVOGADRO nuc_densities = density * nuc_densities From 13c83a1c005eab8e2ea38cdce46c2f798cee59d1 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 4 Aug 2022 08:54:20 -0500 Subject: [PATCH 123/131] Bump up number of samples in test_combine_distributions --- tests/unit_tests/test_stats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit_tests/test_stats.py b/tests/unit_tests/test_stats.py index 1cfcf00c3..b8bb94f37 100644 --- a/tests/unit_tests/test_stats.py +++ b/tests/unit_tests/test_stats.py @@ -436,5 +436,5 @@ def test_combine_distributions(): # Sample the combined distribution and make sure the sample mean is within # uncertainty of the expected value - samples = combined.sample(10) + samples = combined.sample(1000) assert_sample_mean(samples, 0.25) From c3921b735fa7bc4ea4fc923467f542bf472a2427 Mon Sep 17 00:00:00 2001 From: yardasol Date: Wed, 3 Aug 2022 17:43:48 -0500 Subject: [PATCH 124/131] Changes from @paulromano's 6th round of comments - add more detail to `MicroXS` example in User's Guide - Cleanup the `from_model` method - Reduce repeated code between `MicroXS` and `CoupledOperator` --- docs/source/usersguide/depletion.rst | 4 +- openmc/deplete/coupled_operator.py | 35 ++++++++++----- openmc/deplete/microxs.py | 67 +++++++++++++++------------- 3 files changed, 62 insertions(+), 44 deletions(-) diff --git a/docs/source/usersguide/depletion.rst b/docs/source/usersguide/depletion.rst index 821ff338b..29c8114d9 100644 --- a/docs/source/usersguide/depletion.rst +++ b/docs/source/usersguide/depletion.rst @@ -216,9 +216,7 @@ and a path to a depletion chain file:: materials = openmc.Materials() ... - micro_xs = openmc.deplete.MicroXS - ... - + micro_xs = openmc.deplete.MicroXS.from_csv(micro_xs_path) op = openmc.deplete.IndependentOperator(materials, micro_xs, chain_file) .. note:: diff --git a/openmc/deplete/coupled_operator.py b/openmc/deplete/coupled_operator.py index d5bb5b4bd..eba90f60d 100644 --- a/openmc/deplete/coupled_operator.py +++ b/openmc/deplete/coupled_operator.py @@ -56,6 +56,30 @@ def _find_cross_sections(model): ) return cross_sections +def _get_nuclides_with_data(cross_sections): + """Loads cross_sections.xml file to find nuclides with neutron data + + Parameters + ---------- + cross_sections : str + Path to cross_sections.xml file + + Returns + ------- + nuclides : set of str + Set of nuclide names that have cross secton data + + """ + nuclides = set() + data_lib = DataLibrary.from_xml(cross_sections) + for library in data_lib.libraries: + if library['type'] != 'neutron': + continue + for name in library['materials']: + if name not in nuclides: + nuclides.add(name) + + return nuclides class CoupledOperator(OpenMCOperator): """Transport-coupled transport operator. @@ -310,16 +334,7 @@ class CoupledOperator(OpenMCOperator): Set of nuclide names that have cross secton data """ - nuclides = set() - data_lib = DataLibrary.from_xml(cross_sections) - for library in data_lib.libraries: - if library['type'] != 'neutron': - continue - for name in library['materials']: - if name not in nuclides: - nuclides.add(name) - - return nuclides + return _get_nuclides_with_data(cross_sections) def _get_helper_classes(self, helper_kwargs): """Create the ``_rate_helper``, ``_normalization_helper``, and diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py index 75058fce0..32e0518ff 100644 --- a/openmc/deplete/microxs.py +++ b/openmc/deplete/microxs.py @@ -6,6 +6,7 @@ nuclides names as row indices and reaction names as column indices. import tempfile from pathlib import Path +from copy import deepcopy from pandas import DataFrame, read_csv, concat import numpy as np @@ -13,11 +14,11 @@ import numpy as np from openmc.checkvalue import check_type, check_value, check_iterable_type from openmc.mgxs import EnergyGroups, ArbitraryXS, FissionXS from openmc.data import DataLibrary -from openmc import Tallies, StatePoint, Materials +from openmc import Tallies, StatePoint, Materials, Material from .chain import Chain, REACTIONS -from .coupled_operator import _find_cross_sections +from .coupled_operator import _find_cross_sections, _get_nuclides_with_data _valid_rxns = list(REACTIONS) _valid_rxns.append('fission') @@ -67,6 +68,7 @@ class MicroXS(DataFrame): # Set up the reaction tallies original_tallies = model.tallies + original_materials = deepcopy(model.materials) tallies = Tallies() xs = {} reactions, diluted_materials = cls._add_dilute_nuclides(chain_file, @@ -100,17 +102,45 @@ class MicroXS(DataFrame): df.rename({'mean': rxn}, axis=1, inplace=True) micro_xs = concat([micro_xs, df], axis=1) - # Revert to the original tallies + # Revert to the original tallies and materials model.tallies = original_tallies + model.materials = original_materials return micro_xs @classmethod def _add_dilute_nuclides(cls, chain_file, model, dilute_initial): + """ + Add nuclides not present in burnable materials that have neutron data + and are present in the depletion chain to those materials. This allows + us to tally those specific nuclides for reactions to create one-group + cross sections. + + Parameters + ---------- + chain_file : str + Path to the depletion chain XML file that will be used in depletion + simulation. Used to determine cross sections for materials not + present in the inital composition. + model : openmc.Model + Model object + dilute_initial : float + Initial atom density [atoms/cm^3] to add for nuclides that + are zero in initial condition to ensure they exist in the cross + section data. Only done for nuclides with reaction rates. + + Returns + ------- + reactions : list of str + List of reaction names + diluted_materials : openmc.Materials + :class:`openmc.Materials` object with nuclides added to burnable + materials. + """ chain = Chain.from_xml(chain_file) reactions = chain.reactions cross_sections = _find_cross_sections(model) - nuclides_with_data = cls._get_nuclides_with_data(cross_sections) + nuclides_with_data = _get_nuclides_with_data(cross_sections) burnable_nucs = [nuc.name for nuc in chain.nuclides if nuc.name in nuclides_with_data] diluted_materials = Materials() @@ -125,36 +155,11 @@ class MicroXS(DataFrame): for burn_nuc in burnable_nucs: if burn_nuc not in nuc_densities: material.add_nuclide(burn_nuc, - dilute_density) + dilute_density) diluted_materials.append(material) + return reactions, diluted_materials - @staticmethod - def _get_nuclides_with_data(cross_sections): - """Loads cross_sections.xml file to find nuclides with neutron data - - Parameters - ---------- - cross_sections : str - Path to cross_sections.xml file - - Returns - ------- - nuclides : set of str - Set of nuclide names that have cross secton data - - """ - nuclides = set() - data_lib = DataLibrary.from_xml(cross_sections) - for library in data_lib.libraries: - if library['type'] != 'neutron': - continue - for name in library['materials']: - if name not in nuclides: - nuclides.add(name) - - return nuclides - @classmethod def from_array(cls, nuclides, reactions, data): """ From d5a8e29b7c526a252bf611dcba6978640554c09d Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 4 Aug 2022 12:12:52 -0500 Subject: [PATCH 125/131] fix IndependentOperator._load_prev_results() --- openmc/deplete/independent_operator.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openmc/deplete/independent_operator.py b/openmc/deplete/independent_operator.py index d2a9f04d0..b7956205a 100644 --- a/openmc/deplete/independent_operator.py +++ b/openmc/deplete/independent_operator.py @@ -232,7 +232,10 @@ class IndependentOperator(OpenMCOperator): def _load_previous_results(self): """Load results from a previous depletion simulation""" # Reload volumes into geometry - self.prev_res[-1].transfer_volumes(self.materials) + model = openmc.Model(materials=self.materials) + self.prev_res[-1].transfer_volumes(model) + self.materials = model.materials + # Store previous results in operator # Distribute reaction rates according to those tracked From 3ef15da62f4a24cab65c330acabcd34593270cf5 Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 4 Aug 2022 12:13:07 -0500 Subject: [PATCH 126/131] fix MicroXS.from_model() --- openmc/deplete/microxs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py index 32e0518ff..29ddc7268 100644 --- a/openmc/deplete/microxs.py +++ b/openmc/deplete/microxs.py @@ -106,7 +106,7 @@ class MicroXS(DataFrame): model.tallies = original_tallies model.materials = original_materials - return micro_xs + return cls(micro_xs) @classmethod def _add_dilute_nuclides(cls, chain_file, model, dilute_initial): From fc3d94b781b26e9ad4a885b77f847d329040f856 Mon Sep 17 00:00:00 2001 From: Joffrey Dorville Date: Thu, 4 Aug 2022 11:24:46 -0600 Subject: [PATCH 127/131] Apply @paulromano suggestions --- openmc/data/njoy.py | 4 ++-- openmc/data/reaction.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openmc/data/njoy.py b/openmc/data/njoy.py index 70351a772..d8ecaae18 100644 --- a/openmc/data/njoy.py +++ b/openmc/data/njoy.py @@ -127,7 +127,7 @@ acer / %%%%%%%%%%%%%%%%%%%%%%%% Write out in ACE format %%%%%%%%%%%%%%%%%%%%%%%% 1 0 1 .{ext} / '{library}: {zsymam} at {temperature}'/ {mat} {temperature} -1 1 {ismoothing}/ +1 1 {ismooth}/ / """ @@ -383,7 +383,7 @@ def make_ace(filename, temperatures=None, acer=True, xsdir=None, # acer if acer: - ismoothing = int(smoothing) + ismooth = int(smoothing) nacer_in = nlast for i, temperature in enumerate(temperatures): # Extend input with an ACER run for each temperature diff --git a/openmc/data/reaction.py b/openmc/data/reaction.py index 42fc576da..a2431cf1c 100644 --- a/openmc/data/reaction.py +++ b/openmc/data/reaction.py @@ -82,7 +82,7 @@ def _get_products(ev, mt): Raises ------ - IOError: + IOError When the Kalbach-Mann systematics is used, but the product is not defined in the 'center-of-mass' system. The breakup logic is not implemented which can lead to this error being raised while From c75524cfa0288103ec37e1538ff30472f5a83b9d Mon Sep 17 00:00:00 2001 From: Olek <45364492+yardasol@users.noreply.github.com> Date: Thu, 4 Aug 2022 16:11:11 -0500 Subject: [PATCH 128/131] simplify `from_model` Co-authored-by: Paul Romano --- openmc/deplete/microxs.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py index 29ddc7268..c4094970f 100644 --- a/openmc/deplete/microxs.py +++ b/openmc/deplete/microxs.py @@ -94,19 +94,16 @@ class MicroXS(DataFrame): xs[rxn].load_from_statepoint(sp) # Build the DataFrame - micro_xs = cls() - for rxn in xs: - df = xs[rxn].get_pandas_dataframe(xs_type='micro') - df.index = df['nuclide'] - df.drop(['nuclide', xs[rxn].domain_type, 'group in', 'std. dev.'], axis=1, inplace=True) - df.rename({'mean': rxn}, axis=1, inplace=True) - micro_xs = concat([micro_xs, df], axis=1) + series = {} + for rx in xs: + df = xs[rx].get_pandas_dataframe(xs_type='micro') + series[rx] = df.set_index('nuclide')['mean'] # Revert to the original tallies and materials model.tallies = original_tallies model.materials = original_materials - return cls(micro_xs) + return cls(series) @classmethod def _add_dilute_nuclides(cls, chain_file, model, dilute_initial): From c943128587e846d1e477ba9b37cdfa49d05d18df Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Fri, 25 Mar 2022 13:25:55 -0500 Subject: [PATCH 129/131] Add pytest request path to location of cleanup files in cpp driver test --- tests/regression_tests/cpp_driver/test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/regression_tests/cpp_driver/test.py b/tests/regression_tests/cpp_driver/test.py index 95912b3f7..e8b1c62ac 100644 --- a/tests/regression_tests/cpp_driver/test.py +++ b/tests/regression_tests/cpp_driver/test.py @@ -48,8 +48,8 @@ def cpp_driver(request): finally: # Remove local build directory when test is complete - shutil.rmtree('build') - os.remove('CMakeLists.txt') + shutil.rmtree(request.node.path.parent / 'build') + os.remove(request.node.path.parent / 'CMakeLists.txt') @pytest.fixture From d3535a521e06958f5d9f16c00c5238f60f825300 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Mon, 11 Apr 2022 12:23:23 -0500 Subject: [PATCH 130/131] Add Python pickle files to ignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 88e59895d..3b2a24a4d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ *.o *.log *.out +*.pkl # Compiler python objects *.pyc From 98f5da737f7607cc82d7d8b51fa76ff697c0f579 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Sat, 6 Aug 2022 06:20:11 -0500 Subject: [PATCH 131/131] rm space --- openmc/universe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openmc/universe.py b/openmc/universe.py index 78f75bbfc..4e9cd91f3 100644 --- a/openmc/universe.py +++ b/openmc/universe.py @@ -785,7 +785,7 @@ class DAGMCUniverse(UniverseBase): Universe instance """ - bounding_cell = openmc.Cell(fill=self, cell_id =bounding_cell_id, region=self.bounding_region(**kwargs)) + bounding_cell = openmc.Cell(fill=self, cell_id=bounding_cell_id, region=self.bounding_region(**kwargs)) return openmc.Universe(cells=[bounding_cell]) @classmethod