mirror of
https://github.com/openmc-dev/openmc.git
synced 2026-07-21 14:35:27 -04:00
1102 lines
37 KiB
Python
1102 lines
37 KiB
Python
from collections.abc import MutableSequence, Iterable
|
|
import io
|
|
|
|
import numpy as np
|
|
from numpy.polynomial import Polynomial
|
|
import pandas as pd
|
|
|
|
import openmc.checkvalue as cv
|
|
from .data import NEUTRON_MASS
|
|
from .endf import (
|
|
as_evaluation, get_head_record, get_cont_record, get_tab1_record,
|
|
get_list_record)
|
|
try:
|
|
from .reconstruct import wave_number, penetration_shift, reconstruct_mlbw, \
|
|
reconstruct_slbw, reconstruct_rm
|
|
_reconstruct = True
|
|
except ImportError:
|
|
_reconstruct = False
|
|
|
|
|
|
class Resonances:
|
|
"""Resolved and unresolved resonance data
|
|
|
|
Parameters
|
|
----------
|
|
ranges : list of openmc.data.ResonanceRange
|
|
Distinct energy ranges for resonance data
|
|
|
|
Attributes
|
|
----------
|
|
ranges : list of openmc.data.ResonanceRange
|
|
Distinct energy ranges for resonance data
|
|
resolved : openmc.data.ResonanceRange or None
|
|
Resolved resonance range
|
|
unresolved : openmc.data.Unresolved or None
|
|
Unresolved resonance range
|
|
|
|
"""
|
|
|
|
def __init__(self, ranges):
|
|
self.ranges = ranges
|
|
|
|
def __iter__(self):
|
|
for r in self.ranges:
|
|
yield r
|
|
|
|
@property
|
|
def ranges(self):
|
|
return self._ranges
|
|
|
|
@ranges.setter
|
|
def ranges(self, ranges):
|
|
cv.check_type('resonance ranges', ranges, MutableSequence)
|
|
self._ranges = cv.CheckedList(ResonanceRange, 'resonance ranges',
|
|
ranges)
|
|
|
|
@property
|
|
def resolved(self):
|
|
resolved_ranges = [r for r in self.ranges
|
|
if not isinstance(r, Unresolved)]
|
|
if len(resolved_ranges) > 1:
|
|
raise ValueError('More than one resolved range present')
|
|
elif len(resolved_ranges) == 0:
|
|
return None
|
|
else:
|
|
return resolved_ranges[0]
|
|
|
|
@property
|
|
def unresolved(self):
|
|
for r in self.ranges:
|
|
if isinstance(r, Unresolved):
|
|
return r
|
|
else:
|
|
return None
|
|
|
|
@classmethod
|
|
def from_endf(cls, ev):
|
|
"""Generate resonance data from an ENDF evaluation.
|
|
|
|
Parameters
|
|
----------
|
|
ev : openmc.data.endf.Evaluation or endf.Material
|
|
ENDF evaluation
|
|
|
|
Returns
|
|
-------
|
|
openmc.data.Resonances
|
|
Resonance data
|
|
|
|
"""
|
|
ev = as_evaluation(ev)
|
|
file_obj = io.StringIO(ev.section[2, 151])
|
|
|
|
# Determine whether discrete or continuous representation
|
|
items = get_head_record(file_obj)
|
|
n_isotope = items[4] # Number of isotopes
|
|
|
|
ranges = []
|
|
for _ in range(n_isotope):
|
|
items = get_cont_record(file_obj)
|
|
fission_widths = (items[3] == 1) # fission widths are given?
|
|
n_ranges = items[4] # number of resonance energy ranges
|
|
|
|
for j in range(n_ranges):
|
|
items = get_cont_record(file_obj)
|
|
resonance_flag = items[2] # flag for resolved (1)/unresolved (2)
|
|
formalism = items[3] # resonance formalism
|
|
|
|
if resonance_flag in (0, 1):
|
|
# resolved resonance region
|
|
erange = _FORMALISMS[formalism].from_endf(ev, file_obj, items)
|
|
|
|
elif resonance_flag == 2:
|
|
# unresolved resonance region
|
|
erange = Unresolved.from_endf(file_obj, items, fission_widths)
|
|
|
|
# erange.material = self
|
|
ranges.append(erange)
|
|
|
|
return cls(ranges)
|
|
|
|
|
|
class ResonanceRange:
|
|
"""Resolved resonance range
|
|
|
|
Parameters
|
|
----------
|
|
target_spin : float
|
|
Intrinsic spin, :math:`I`, of the target nuclide
|
|
energy_min : float
|
|
Minimum energy of the resolved resonance range in eV
|
|
energy_max : float
|
|
Maximum energy of the resolved resonance range in eV
|
|
channel : dict
|
|
Dictionary whose keys are l-values and values are channel radii as a
|
|
function of energy
|
|
scattering : dict
|
|
Dictionary whose keys are l-values and values are scattering radii as a
|
|
function of energy
|
|
|
|
Attributes
|
|
----------
|
|
channel_radius : dict
|
|
Dictionary whose keys are l-values and values are channel radii as a
|
|
function of energy
|
|
energy_max : float
|
|
Maximum energy of the resolved resonance range in eV
|
|
energy_min : float
|
|
Minimum energy of the resolved resonance range in eV
|
|
scattering_radius : dict
|
|
Dictionary whose keys are l-values and values are scattering radii as a
|
|
function of energ
|
|
target_spin : float
|
|
Intrinsic spin, :math:`I`, of the target nuclide
|
|
|
|
"""
|
|
def __init__(self, target_spin, energy_min, energy_max, channel, scattering):
|
|
self.target_spin = target_spin
|
|
self.energy_min = energy_min
|
|
self.energy_max = energy_max
|
|
self.channel_radius = channel
|
|
self.scattering_radius = scattering
|
|
|
|
self._prepared = False
|
|
self._parameter_matrix = {}
|
|
|
|
def __copy__(self):
|
|
cls = type(self)
|
|
new_copy = cls.__new__(cls)
|
|
new_copy.__dict__.update(self.__dict__)
|
|
new_copy._prepared = False
|
|
return new_copy
|
|
|
|
@classmethod
|
|
def from_endf(cls, ev, file_obj, items):
|
|
"""Create resonance range from an ENDF evaluation.
|
|
|
|
This factory method is only used when LRU=0, indicating that only a
|
|
scattering radius appears in MF=2, MT=151. All subclasses of
|
|
ResonanceRange override this method with their own.
|
|
|
|
Parameters
|
|
----------
|
|
ev : openmc.data.endf.Evaluation
|
|
ENDF evaluation
|
|
file_obj : file-like object
|
|
ENDF file positioned at the second record of a resonance range
|
|
subsection in MF=2, MT=151
|
|
items : list
|
|
Items from the CONT record at the start of the resonance range
|
|
subsection
|
|
|
|
Returns
|
|
-------
|
|
openmc.data.ResonanceRange
|
|
Resonance range data
|
|
|
|
"""
|
|
energy_min, energy_max = items[0:2]
|
|
|
|
# For scattering radius-only, NRO must be zero
|
|
assert items[4] == 0
|
|
|
|
# Get energy-independent scattering radius
|
|
items = get_cont_record(file_obj)
|
|
target_spin = items[0]
|
|
ap = Polynomial((items[1],))
|
|
|
|
# Calculate channel radius from ENDF-102 equation D.14
|
|
a = Polynomial((0.123 * (NEUTRON_MASS*ev.target['mass'])**(1./3.) + 0.08,))
|
|
|
|
return cls(target_spin, energy_min, energy_max, {0: a}, {0: ap})
|
|
|
|
def reconstruct(self, energies):
|
|
"""Evaluate cross section at specified energies.
|
|
|
|
Parameters
|
|
----------
|
|
energies : float or Iterable of float
|
|
Energies at which the cross section should be evaluated
|
|
|
|
Returns
|
|
-------
|
|
3-tuple of float or numpy.ndarray
|
|
Elastic, capture, and fission cross sections at the specified
|
|
energies
|
|
|
|
"""
|
|
if not _reconstruct:
|
|
raise RuntimeError("Resonance reconstruction not available.")
|
|
|
|
# Pre-calculate penetrations and shifts for resonances
|
|
if not self._prepared:
|
|
self._prepare_resonances()
|
|
|
|
if isinstance(energies, Iterable):
|
|
elastic = np.zeros_like(energies)
|
|
capture = np.zeros_like(energies)
|
|
fission = np.zeros_like(energies)
|
|
|
|
for i, E in enumerate(energies):
|
|
xse, xsg, xsf = self._reconstruct(self, E)
|
|
elastic[i] = xse
|
|
capture[i] = xsg
|
|
fission[i] = xsf
|
|
else:
|
|
elastic, capture, fission = self._reconstruct(self, energies)
|
|
|
|
return {2: elastic, 102: capture, 18: fission}
|
|
|
|
|
|
class MultiLevelBreitWigner(ResonanceRange):
|
|
"""Multi-level Breit-Wigner resolved resonance formalism data.
|
|
|
|
Multi-level Breit-Wigner resolved resonance data is identified by LRF=2 in
|
|
the ENDF-6 format.
|
|
|
|
Parameters
|
|
----------
|
|
target_spin : float
|
|
Intrinsic spin, :math:`I`, of the target nuclide
|
|
energy_min : float
|
|
Minimum energy of the resolved resonance range in eV
|
|
energy_max : float
|
|
Maximum energy of the resolved resonance range in eV
|
|
channel : dict
|
|
Dictionary whose keys are l-values and values are channel radii as a
|
|
function of energy
|
|
scattering : dict
|
|
Dictionary whose keys are l-values and values are scattering radii as a
|
|
function of energy
|
|
|
|
Attributes
|
|
----------
|
|
atomic_weight_ratio : float
|
|
Atomic weight ratio of the target nuclide given as a function of
|
|
l-value. Note that this may be different than the value for the
|
|
evaluation as a whole.
|
|
channel_radius : dict
|
|
Dictionary whose keys are l-values and values are channel radii as a
|
|
function of energy
|
|
energy_max : float
|
|
Maximum energy of the resolved resonance range in eV
|
|
energy_min : float
|
|
Minimum energy of the resolved resonance range in eV
|
|
parameters : pandas.DataFrame
|
|
Energies, spins, and resonances widths for each resonance
|
|
q_value : dict
|
|
Q-value to be added to incident particle's center-of-mass energy to
|
|
determine the channel energy for use in the penetrability factor. The
|
|
keys of the dictionary are l-values.
|
|
scattering_radius : dict
|
|
Dictionary whose keys are l-values and values are scattering radii as a
|
|
function of energy
|
|
target_spin : float
|
|
Intrinsic spin, :math:`I`, of the target nuclide
|
|
|
|
"""
|
|
|
|
def __init__(self, target_spin, energy_min, energy_max, channel, scattering):
|
|
super().__init__(target_spin, energy_min, energy_max, channel,
|
|
scattering)
|
|
self.parameters = None
|
|
self.q_value = {}
|
|
self.atomic_weight_ratio = None
|
|
|
|
# Set resonance reconstruction function
|
|
if _reconstruct:
|
|
self._reconstruct = reconstruct_mlbw
|
|
else:
|
|
self._reconstruct = None
|
|
|
|
@classmethod
|
|
def from_endf(cls, ev, file_obj, items):
|
|
"""Create MLBW data from an ENDF evaluation.
|
|
|
|
Parameters
|
|
----------
|
|
ev : openmc.data.endf.Evaluation
|
|
ENDF evaluation
|
|
file_obj : file-like object
|
|
ENDF file positioned at the second record of a resonance range
|
|
subsection in MF=2, MT=151
|
|
items : list
|
|
Items from the CONT record at the start of the resonance range
|
|
subsection
|
|
|
|
Returns
|
|
-------
|
|
openmc.data.MultiLevelBreitWigner
|
|
Multi-level Breit-Wigner resonance parameters
|
|
|
|
"""
|
|
|
|
# Read energy-dependent scattering radius if present
|
|
energy_min, energy_max = items[0:2]
|
|
nro, naps = items[4:6]
|
|
if nro != 0:
|
|
params, ape = get_tab1_record(file_obj)
|
|
|
|
# Other scatter radius parameters
|
|
items = get_cont_record(file_obj)
|
|
target_spin = items[0]
|
|
ap = Polynomial((items[1],)) # energy-independent scattering-radius
|
|
NLS = items[4] # number of l-values
|
|
|
|
# Read resonance widths, J values, etc
|
|
channel_radius = {}
|
|
scattering_radius = {}
|
|
q_value = {}
|
|
records = []
|
|
for l in range(NLS):
|
|
items, values = get_list_record(file_obj)
|
|
l_value = items[2]
|
|
awri = items[0]
|
|
q_value[l_value] = items[1]
|
|
competitive = items[3]
|
|
|
|
# Calculate channel radius from ENDF-102 equation D.14
|
|
a = Polynomial((0.123 * (NEUTRON_MASS*awri)**(1./3.) + 0.08,))
|
|
|
|
# Construct scattering and channel radius
|
|
if nro == 0:
|
|
scattering_radius[l_value] = ap
|
|
if naps == 0:
|
|
channel_radius[l_value] = a
|
|
elif naps == 1:
|
|
channel_radius[l_value] = ap
|
|
elif nro == 1:
|
|
scattering_radius[l_value] = ape
|
|
if naps == 0:
|
|
channel_radius[l_value] = a
|
|
elif naps == 1:
|
|
channel_radius[l_value] = ape
|
|
elif naps == 2:
|
|
channel_radius[l_value] = ap
|
|
|
|
energy = values[0::6]
|
|
spin = values[1::6]
|
|
gt = np.asarray(values[2::6])
|
|
gn = np.asarray(values[3::6])
|
|
gg = np.asarray(values[4::6])
|
|
gf = np.asarray(values[5::6])
|
|
if competitive > 0:
|
|
gx = gt - (gn + gg + gf)
|
|
else:
|
|
gx = np.zeros_like(gt)
|
|
|
|
for i, E in enumerate(energy):
|
|
records.append([energy[i], l_value, spin[i], gt[i], gn[i],
|
|
gg[i], gf[i], gx[i]])
|
|
|
|
columns = ['energy', 'L', 'J', 'totalWidth', 'neutronWidth',
|
|
'captureWidth', 'fissionWidth', 'competitiveWidth']
|
|
parameters = pd.DataFrame.from_records(records, columns=columns)
|
|
|
|
# Create instance of class
|
|
mlbw = cls(target_spin, energy_min, energy_max,
|
|
channel_radius, scattering_radius)
|
|
mlbw.q_value = q_value
|
|
mlbw.atomic_weight_ratio = awri
|
|
mlbw.parameters = parameters
|
|
|
|
return mlbw
|
|
|
|
def _prepare_resonances(self):
|
|
df = self.parameters.copy()
|
|
|
|
# Penetration and shift factors
|
|
p = np.zeros(len(df))
|
|
s = np.zeros(len(df))
|
|
|
|
# Penetration and shift factors for competitive reaction
|
|
px = np.zeros(len(df))
|
|
sx = np.zeros(len(df))
|
|
|
|
l_values = []
|
|
competitive = []
|
|
|
|
A = self.atomic_weight_ratio
|
|
for i, E, l, J, gt, gn, gg, gf, gx in df.itertuples():
|
|
if l not in l_values:
|
|
l_values.append(l)
|
|
competitive.append(gx > 0)
|
|
|
|
# Determine penetration and shift corresponding to resonance energy
|
|
k = wave_number(A, E)
|
|
rho = k*self.channel_radius[l](E)
|
|
p[i], s[i] = penetration_shift(l, rho)
|
|
|
|
# Determine penetration at modified energy for competitive reaction
|
|
if gx > 0:
|
|
Ex = E + self.q_value[l]*(A + 1)/A
|
|
rho = k*self.channel_radius[l](Ex)
|
|
px[i], sx[i] = penetration_shift(l, rho)
|
|
else:
|
|
px[i] = sx[i] = 0.0
|
|
|
|
df['p'] = p
|
|
df['s'] = s
|
|
df['px'] = px
|
|
df['sx'] = sx
|
|
|
|
self._l_values = np.array(l_values)
|
|
self._competitive = np.array(competitive)
|
|
for l in l_values:
|
|
self._parameter_matrix[l] = df[df.L == l].values
|
|
|
|
self._prepared = True
|
|
|
|
|
|
class SingleLevelBreitWigner(MultiLevelBreitWigner):
|
|
"""Single-level Breit-Wigner resolved resonance formalism data.
|
|
|
|
Single-level Breit-Wigner resolved resonance data is is identified by LRF=1
|
|
in the ENDF-6 format.
|
|
|
|
Parameters
|
|
----------
|
|
target_spin : float
|
|
Intrinsic spin, :math:`I`, of the target nuclide
|
|
energy_min : float
|
|
Minimum energy of the resolved resonance range in eV
|
|
energy_max : float
|
|
Maximum energy of the resolved resonance range in eV
|
|
channel : dict
|
|
Dictionary whose keys are l-values and values are channel radii as a
|
|
function of energy
|
|
scattering : dict
|
|
Dictionary whose keys are l-values and values are scattering radii as a
|
|
function of energy
|
|
|
|
Attributes
|
|
----------
|
|
atomic_weight_ratio : float
|
|
Atomic weight ratio of the target nuclide given as a function of
|
|
l-value. Note that this may be different than the value for the
|
|
evaluation as a whole.
|
|
channel_radius : dict
|
|
Dictionary whose keys are l-values and values are channel radii as a
|
|
function of energy
|
|
energy_max : float
|
|
Maximum energy of the resolved resonance range in eV
|
|
energy_min : float
|
|
Minimum energy of the resolved resonance range in eV
|
|
parameters : pandas.DataFrame
|
|
Energies, spins, and resonances widths for each resonance
|
|
q_value : dict
|
|
Q-value to be added to incident particle's center-of-mass energy to
|
|
determine the channel energy for use in the penetrability factor. The
|
|
keys of the dictionary are l-values.
|
|
scattering_radius : dict
|
|
Dictionary whose keys are l-values and values are scattering radii as a
|
|
function of energy
|
|
target_spin : float
|
|
Intrinsic spin, :math:`I`, of the target nuclide
|
|
|
|
"""
|
|
|
|
def __init__(self, target_spin, energy_min, energy_max, channel, scattering):
|
|
super().__init__(target_spin, energy_min, energy_max, channel,
|
|
scattering)
|
|
|
|
# Set resonance reconstruction function
|
|
if _reconstruct:
|
|
self._reconstruct = reconstruct_slbw
|
|
else:
|
|
self._reconstruct = None
|
|
|
|
|
|
class ReichMoore(ResonanceRange):
|
|
"""Reich-Moore resolved resonance formalism data.
|
|
|
|
Reich-Moore resolved resonance data is identified by LRF=3 in the ENDF-6
|
|
format.
|
|
|
|
Parameters
|
|
----------
|
|
target_spin : float
|
|
Intrinsic spin, :math:`I`, of the target nuclide
|
|
energy_min : float
|
|
Minimum energy of the resolved resonance range in eV
|
|
energy_max : float
|
|
Maximum energy of the resolved resonance range in eV
|
|
channel : dict
|
|
Dictionary whose keys are l-values and values are channel radii as a
|
|
function of energy
|
|
scattering : dict
|
|
Dictionary whose keys are l-values and values are scattering radii as a
|
|
function of energy
|
|
|
|
Attributes
|
|
----------
|
|
angle_distribution : bool
|
|
Indicate whether parameters can be used to compute angular distributions
|
|
atomic_weight_ratio : float
|
|
Atomic weight ratio of the target nuclide given as a function of
|
|
l-value. Note that this may be different than the value for the
|
|
evaluation as a whole.
|
|
channel_radius : dict
|
|
Dictionary whose keys are l-values and values are channel radii as a
|
|
function of energy
|
|
energy_max : float
|
|
Maximum energy of the resolved resonance range in eV
|
|
energy_min : float
|
|
Minimum energy of the resolved resonance range in eV
|
|
num_l_convergence : int
|
|
Number of l-values which must be used to converge the calculation
|
|
scattering_radius : dict
|
|
Dictionary whose keys are l-values and values are scattering radii as a
|
|
function of energy
|
|
parameters : pandas.DataFrame
|
|
Energies, spins, and resonances widths for each resonance
|
|
target_spin : float
|
|
Intrinsic spin, :math:`I`, of the target nuclide
|
|
|
|
"""
|
|
|
|
def __init__(self, target_spin, energy_min, energy_max, channel, scattering):
|
|
super().__init__(target_spin, energy_min, energy_max, channel,
|
|
scattering)
|
|
self.parameters = None
|
|
self.angle_distribution = False
|
|
self.num_l_convergence = 0
|
|
|
|
# Set resonance reconstruction function
|
|
if _reconstruct:
|
|
self._reconstruct = reconstruct_rm
|
|
else:
|
|
self._reconstruct = None
|
|
|
|
@classmethod
|
|
def from_endf(cls, ev, file_obj, items):
|
|
"""Create Reich-Moore resonance data from an ENDF evaluation.
|
|
|
|
Parameters
|
|
----------
|
|
ev : openmc.data.endf.Evaluation
|
|
ENDF evaluation
|
|
file_obj : file-like object
|
|
ENDF file positioned at the second record of a resonance range
|
|
subsection in MF=2, MT=151
|
|
items : list
|
|
Items from the CONT record at the start of the resonance range
|
|
subsection
|
|
|
|
Returns
|
|
-------
|
|
openmc.data.ReichMoore
|
|
Reich-Moore resonance parameters
|
|
|
|
"""
|
|
# Read energy-dependent scattering radius if present
|
|
energy_min, energy_max = items[0:2]
|
|
nro, naps = items[4:6]
|
|
if nro != 0:
|
|
params, ape = get_tab1_record(file_obj)
|
|
|
|
# Other scatter radius parameters
|
|
items = get_cont_record(file_obj)
|
|
target_spin = items[0]
|
|
ap = Polynomial((items[1],))
|
|
angle_distribution = (items[3] == 1) # Flag for angular distribution
|
|
NLS = items[4] # Number of l-values
|
|
num_l_convergence = items[5] # Number of l-values for convergence
|
|
|
|
# Read resonance widths, J values, etc
|
|
channel_radius = {}
|
|
scattering_radius = {}
|
|
records = []
|
|
for i in range(NLS):
|
|
items, values = get_list_record(file_obj)
|
|
apl = Polynomial((items[1],)) if items[1] != 0.0 else ap
|
|
l_value = items[2]
|
|
awri = items[0]
|
|
|
|
# Calculate channel radius from ENDF-102 equation D.14
|
|
a = Polynomial((0.123 * (NEUTRON_MASS*awri)**(1./3.) + 0.08,))
|
|
|
|
# Construct scattering and channel radius
|
|
if nro == 0:
|
|
scattering_radius[l_value] = apl
|
|
if naps == 0:
|
|
channel_radius[l_value] = a
|
|
elif naps == 1:
|
|
channel_radius[l_value] = apl
|
|
elif nro == 1:
|
|
if naps == 0:
|
|
channel_radius[l_value] = a
|
|
scattering_radius[l_value] = ape
|
|
elif naps == 1:
|
|
channel_radius[l_value] = scattering_radius[l_value] = ape
|
|
elif naps == 2:
|
|
channel_radius[l_value] = apl
|
|
scattering_radius[l_value] = ape
|
|
|
|
energy = values[0::6]
|
|
spin = values[1::6]
|
|
gn = values[2::6]
|
|
gg = values[3::6]
|
|
gfa = values[4::6]
|
|
gfb = values[5::6]
|
|
|
|
for i, E in enumerate(energy):
|
|
records.append([energy[i], l_value, spin[i], gn[i], gg[i],
|
|
gfa[i], gfb[i]])
|
|
|
|
# Create pandas DataFrame with resonance data
|
|
columns = ['energy', 'L', 'J', 'neutronWidth', 'captureWidth',
|
|
'fissionWidthA', 'fissionWidthB']
|
|
parameters = pd.DataFrame.from_records(records, columns=columns)
|
|
|
|
# Create instance of ReichMoore
|
|
rm = cls(target_spin, energy_min, energy_max,
|
|
channel_radius, scattering_radius)
|
|
rm.parameters = parameters
|
|
rm.angle_distribution = angle_distribution
|
|
rm.num_l_convergence = num_l_convergence
|
|
rm.atomic_weight_ratio = awri
|
|
|
|
return rm
|
|
|
|
def _prepare_resonances(self):
|
|
df = self.parameters.copy()
|
|
|
|
# Penetration and shift factors
|
|
p = np.zeros(len(df))
|
|
s = np.zeros(len(df))
|
|
|
|
l_values = []
|
|
lj_values = []
|
|
|
|
A = self.atomic_weight_ratio
|
|
for i, E, l, J, gn, gg, gfa, gfb in df.itertuples():
|
|
if l not in l_values:
|
|
l_values.append(l)
|
|
if (l, abs(J)) not in lj_values:
|
|
lj_values.append((l, abs(J)))
|
|
|
|
# Determine penetration and shift corresponding to resonance energy
|
|
k = wave_number(A, E)
|
|
rho = k*self.channel_radius[l](E)
|
|
p[i], s[i] = penetration_shift(l, rho)
|
|
|
|
df['p'] = p
|
|
df['s'] = s
|
|
|
|
self._l_values = np.array(l_values)
|
|
for (l, J) in lj_values:
|
|
self._parameter_matrix[l, J] = df[(df.L == l) &
|
|
(abs(df.J) == J)].values
|
|
|
|
self._prepared = True
|
|
|
|
|
|
class RMatrixLimited(ResonanceRange):
|
|
"""R-matrix limited resolved resonance formalism data.
|
|
|
|
R-matrix limited resolved resonance data is identified by LRF=7 in the
|
|
ENDF-6 format.
|
|
|
|
Parameters
|
|
----------
|
|
energy_min : float
|
|
Minimum energy of the resolved resonance range in eV
|
|
energy_max : float
|
|
Maximum energy of the resolved resonance range in eV
|
|
particle_pairs : list of dict
|
|
List of particle pairs. Each particle pair is represented by a
|
|
dictionary that contains the mass, atomic number, spin, and parity of
|
|
each particle as well as other characteristics.
|
|
spin_groups : list of dict
|
|
List of spin groups. Each spin group is characterized by channels,
|
|
resonance energies, and resonance widths.
|
|
|
|
Attributes
|
|
----------
|
|
reduced_width : bool
|
|
Flag indicating whether channel widths in eV or reduced-width amplitudes
|
|
in eV^1/2 are given
|
|
formalism : int
|
|
Flag to specify which formulae for the R-matrix are to be used
|
|
particle_pairs : list of dict
|
|
List of particle pairs. Each particle pair is represented by a
|
|
dictionary that contains the mass, atomic number, spin, and parity of
|
|
each particle as well as other characteristics.
|
|
spin_groups : list of dict
|
|
List of spin groups. Each spin group is characterized by channels,
|
|
resonance energies, and resonance widths.
|
|
|
|
"""
|
|
|
|
def __init__(self, energy_min, energy_max, particle_pairs, spin_groups):
|
|
super().__init__(0.0, energy_min, energy_max, None, None)
|
|
self.reduced_width = False
|
|
self.formalism = 3
|
|
self.particle_pairs = particle_pairs
|
|
self.spin_groups = spin_groups
|
|
|
|
@classmethod
|
|
def from_endf(cls, ev, file_obj, items):
|
|
"""Read R-Matrix limited resonance data from an ENDF evaluation.
|
|
|
|
Parameters
|
|
----------
|
|
ev : openmc.data.endf.Evaluation
|
|
ENDF evaluation
|
|
file_obj : file-like object
|
|
ENDF file positioned at the second record of a resonance range
|
|
subsection in MF=2, MT=151
|
|
items : list
|
|
Items from the CONT record at the start of the resonance range
|
|
subsection
|
|
|
|
Returns
|
|
-------
|
|
openmc.data.RMatrixLimited
|
|
R-matrix limited resonance parameters
|
|
|
|
"""
|
|
energy_min, energy_max = items[0:2]
|
|
|
|
items = get_cont_record(file_obj)
|
|
reduced_width = (items[2] == 1) # reduced width amplitude?
|
|
formalism = items[3] # Specify which formulae are used
|
|
n_spin_groups = items[4] # Number of Jpi values (NJS)
|
|
|
|
particle_pairs = []
|
|
spin_groups = []
|
|
|
|
items, values = get_list_record(file_obj)
|
|
n_pairs = items[5]//2 # Number of particle pairs (NPP)
|
|
for i in range(n_pairs):
|
|
first = {'mass': values[12*i],
|
|
'z': int(values[12*i + 2]),
|
|
'spin': values[12*i + 4],
|
|
'parity': values[12*i + 10]}
|
|
second = {'mass': values[12*i + 1],
|
|
'z': int(values[12*i + 3]),
|
|
'spin': values[12*i + 5],
|
|
'parity': values[12*i + 11]}
|
|
|
|
q_value = values[12*i + 6]
|
|
penetrability = values[12*i + 7]
|
|
shift = values[12*i + 8]
|
|
mt = int(values[12*i + 9])
|
|
|
|
particle_pairs.append(ParticlePair(
|
|
first, second, q_value, penetrability, shift, mt))
|
|
|
|
# loop over spin groups
|
|
for i in range(n_spin_groups):
|
|
items, values = get_list_record(file_obj)
|
|
J = items[0]
|
|
if J == 0.0:
|
|
parity = '+' if items[1] == 1.0 else '-'
|
|
else:
|
|
parity = '+' if J > 0. else '-'
|
|
J = abs(J)
|
|
kbk = items[2]
|
|
kps = items[3]
|
|
n_channels = items[5]
|
|
channels = []
|
|
for j in range(n_channels):
|
|
channel = {}
|
|
channel['particle_pair'] = particle_pairs[
|
|
int(values[6*j]) - 1]
|
|
channel['l'] = values[6*j + 1]
|
|
channel['spin'] = values[6*j + 2]
|
|
channel['boundary'] = values[6*j + 3]
|
|
channel['effective_radius'] = values[6*j + 4]
|
|
channel['true_radius'] = values[6*j + 5]
|
|
channels.append(channel)
|
|
|
|
# Read resonance energies and widths
|
|
items, values = get_list_record(file_obj)
|
|
n_resonances = items[3]
|
|
records = []
|
|
m = n_channels//6 + 1
|
|
for j in range(n_resonances):
|
|
energy = values[6*m*j]
|
|
records.append([energy] + [values[6*m*j + k + 1]
|
|
for k in range(n_channels)])
|
|
|
|
# Determine column names
|
|
columns = ['energy']
|
|
for channel in channels:
|
|
mt = channel['particle_pair'].mt
|
|
if mt == 2:
|
|
columns.append('neutronWidth')
|
|
elif mt == 18:
|
|
columns.append('fissionWidth')
|
|
elif mt == 102:
|
|
columns.append('captureWidth')
|
|
else:
|
|
columns.append(f'width (MT={mt})')
|
|
|
|
# Create Pandas dataframe with resonance parameters
|
|
parameters = pd.DataFrame.from_records(records, columns=columns)
|
|
|
|
# Construct SpinGroup instance and add to list
|
|
sg = SpinGroup(J, parity, channels, parameters)
|
|
spin_groups.append(sg)
|
|
|
|
# Optional extension (Background R-Matrix)
|
|
if kbk > 0:
|
|
items, values = get_list_record(file_obj)
|
|
lbk = items[4]
|
|
if lbk == 1:
|
|
params, rbr = get_tab1_record(file_obj)
|
|
params, rbi = get_tab1_record(file_obj)
|
|
|
|
# Optional extension (Tabulated phase shifts)
|
|
if kps > 0:
|
|
items, values = get_list_record(file_obj)
|
|
lps = items[4]
|
|
if lps == 1:
|
|
params, psr = get_tab1_record(file_obj)
|
|
params, psi = get_tab1_record(file_obj)
|
|
|
|
rml = cls(energy_min, energy_max, particle_pairs, spin_groups)
|
|
rml.reduced_width = reduced_width
|
|
rml.formalism = formalism
|
|
|
|
return rml
|
|
|
|
|
|
class ParticlePair:
|
|
def __init__(self, first, second, q_value, penetrability,
|
|
shift, mt):
|
|
self.first = first
|
|
self.second = second
|
|
self.q_value = q_value
|
|
self.penetrability = penetrability
|
|
self.shift = shift
|
|
self.mt = mt
|
|
|
|
|
|
class SpinGroup:
|
|
"""Resonance spin group
|
|
|
|
Attributes
|
|
----------
|
|
spin : float
|
|
Total angular momentum (nuclear spin)
|
|
parity : {'+', '-'}
|
|
Even (+) or odd(-) parity
|
|
channels : list of openmc.data.Channel
|
|
Available channels
|
|
parameters : pandas.DataFrame
|
|
Energies/widths for each resonance/channel
|
|
|
|
"""
|
|
|
|
def __init__(self, spin, parity, channels, parameters):
|
|
self.spin = spin
|
|
self.parity = parity
|
|
self.channels = channels
|
|
self.parameters = parameters
|
|
|
|
def __repr__(self):
|
|
return f'<SpinGroup: Jpi={self.spin}{self.parity}>'
|
|
|
|
|
|
class Unresolved(ResonanceRange):
|
|
"""Unresolved resonance parameters as identified by LRU=2 in MF=2.
|
|
|
|
Parameters
|
|
----------
|
|
target_spin : float
|
|
Intrinsic spin, :math:`I`, of the target nuclide
|
|
energy_min : float
|
|
Minimum energy of the unresolved resonance range in eV
|
|
energy_max : float
|
|
Maximum energy of the unresolved resonance range in eV
|
|
channel : openmc.data.Function1D
|
|
Channel radii as a function of energy
|
|
scattering : openmc.data.Function1D
|
|
Scattering radii as a function of energy
|
|
|
|
Attributes
|
|
----------
|
|
add_to_background : bool
|
|
If True, file 3 contains partial cross sections to be added to the
|
|
average unresolved cross sections calculated from parameters.
|
|
atomic_weight_ratio : float
|
|
Atomic weight ratio of the target nuclide
|
|
channel_radius : openmc.data.Function1D
|
|
Channel radii as a function of energy
|
|
energies : Iterable of float
|
|
Energies at which parameters are tabulated
|
|
energy_max : float
|
|
Maximum energy of the unresolved resonance range in eV
|
|
energy_min : float
|
|
Minimum energy of the unresolved resonance range in eV
|
|
parameters : list of pandas.DataFrame
|
|
Average resonance parameters at each energy
|
|
scattering_radius : openmc.data.Function1D
|
|
Scattering radii as a function of energy
|
|
target_spin : float
|
|
Intrinsic spin, :math:`I`, of the target nuclide
|
|
|
|
"""
|
|
|
|
def __init__(self, target_spin, energy_min, energy_max, channel, scattering):
|
|
super().__init__(target_spin, energy_min, energy_max, channel,
|
|
scattering)
|
|
self.energies = None
|
|
self.parameters = None
|
|
self.add_to_background = False
|
|
self.atomic_weight_ratio = None
|
|
|
|
@classmethod
|
|
def from_endf(cls, file_obj, items, fission_widths):
|
|
"""Read unresolved resonance data from an ENDF evaluation.
|
|
|
|
Parameters
|
|
----------
|
|
file_obj : file-like object
|
|
ENDF file positioned at the second record of a resonance range
|
|
subsection in MF=2, MT=151
|
|
items : list
|
|
Items from the CONT record at the start of the resonance range
|
|
subsection
|
|
fission_widths : bool
|
|
Whether fission widths are given
|
|
|
|
Returns
|
|
-------
|
|
openmc.data.Unresolved
|
|
Unresolved resonance region parameters
|
|
|
|
"""
|
|
# Read energy-dependent scattering radius if present
|
|
energy_min, energy_max = items[0:2]
|
|
nro, naps = items[4:6]
|
|
if nro != 0:
|
|
params, ape = get_tab1_record(file_obj)
|
|
|
|
# Get SPI, AP, and LSSF
|
|
formalism = items[3]
|
|
if not (fission_widths and formalism == 1):
|
|
items = get_cont_record(file_obj)
|
|
target_spin = items[0]
|
|
if nro == 0:
|
|
ap = Polynomial((items[1],))
|
|
add_to_background = (items[2] == 0)
|
|
|
|
if not fission_widths and formalism == 1:
|
|
# Case A -- fission widths not given, all parameters are
|
|
# energy-independent
|
|
NLS = items[4]
|
|
columns = ['L', 'J', 'd', 'amun', 'gn0', 'gg']
|
|
records = []
|
|
for ls in range(NLS):
|
|
items, values = get_list_record(file_obj)
|
|
awri = items[0]
|
|
l = items[2]
|
|
NJS = items[5]
|
|
for j in range(NJS):
|
|
d, j, amun, gn0, gg = values[6*j:6*j + 5]
|
|
records.append([l, j, d, amun, gn0, gg])
|
|
parameters = pd.DataFrame.from_records(records, columns=columns)
|
|
energies = None
|
|
|
|
elif fission_widths and formalism == 1:
|
|
# Case B -- fission widths given, only fission widths are
|
|
# energy-dependent
|
|
items, energies = get_list_record(file_obj)
|
|
target_spin = items[0]
|
|
if nro == 0:
|
|
ap = Polynomial((items[1],))
|
|
add_to_background = (items[2] == 0)
|
|
NE, NLS = items[4:6]
|
|
records = []
|
|
columns = ['L', 'J', 'E', 'd', 'amun', 'amuf', 'gn0', 'gg', 'gf']
|
|
for ls in range(NLS):
|
|
items = get_cont_record(file_obj)
|
|
awri = items[0]
|
|
l = items[2]
|
|
NJS = items[4]
|
|
for j in range(NJS):
|
|
items, values = get_list_record(file_obj)
|
|
muf = items[3]
|
|
d = values[0]
|
|
j = values[1]
|
|
amun = values[2]
|
|
gn0 = values[3]
|
|
gg = values[4]
|
|
gfs = values[6:]
|
|
for E, gf in zip(energies, gfs):
|
|
records.append([l, j, E, d, amun, muf, gn0, gg, gf])
|
|
parameters = pd.DataFrame.from_records(records, columns=columns)
|
|
|
|
elif formalism == 2:
|
|
# Case C -- all parameters are energy-dependent
|
|
NLS = items[4]
|
|
columns = ['L', 'J', 'E', 'd', 'amux', 'amun', 'amuf', 'gx', 'gn0',
|
|
'gg', 'gf']
|
|
records = []
|
|
for ls in range(NLS):
|
|
items = get_cont_record(file_obj)
|
|
awri = items[0]
|
|
l = items[2]
|
|
NJS = items[4]
|
|
for j in range(NJS):
|
|
items, values = get_list_record(file_obj)
|
|
ne = items[5]
|
|
j = items[0]
|
|
amux = values[2]
|
|
amun = values[3]
|
|
amuf = values[5]
|
|
energies = []
|
|
for k in range(1, ne + 1):
|
|
E = values[6*k]
|
|
d = values[6*k + 1]
|
|
gx = values[6*k + 2]
|
|
gn0 = values[6*k + 3]
|
|
gg = values[6*k + 4]
|
|
gf = values[6*k + 5]
|
|
energies.append(E)
|
|
records.append([l, j, E, d, amux, amun, amuf, gx, gn0,
|
|
gg, gf])
|
|
parameters = pd.DataFrame.from_records(records, columns=columns)
|
|
|
|
# Calculate channel radius from ENDF-102 equation D.14
|
|
a = Polynomial((0.123 * (NEUTRON_MASS*awri)**(1./3.) + 0.08,))
|
|
|
|
# Determine scattering and channel radius
|
|
if nro == 0:
|
|
scattering_radius = ap
|
|
if naps == 0:
|
|
channel_radius = a
|
|
elif naps == 1:
|
|
channel_radius = ap
|
|
elif nro == 1:
|
|
scattering_radius = ape
|
|
if naps == 0:
|
|
channel_radius = a
|
|
elif naps == 1:
|
|
channel_radius = ape
|
|
elif naps == 2:
|
|
channel_radius = ap
|
|
|
|
urr = cls(target_spin, energy_min, energy_max, channel_radius,
|
|
scattering_radius)
|
|
urr.parameters = parameters
|
|
urr.add_to_background = add_to_background
|
|
urr.atomic_weight_ratio = awri
|
|
urr.energies = energies
|
|
|
|
return urr
|
|
|
|
|
|
_FORMALISMS = {0: ResonanceRange,
|
|
1: SingleLevelBreitWigner,
|
|
2: MultiLevelBreitWigner,
|
|
3: ReichMoore,
|
|
7: RMatrixLimited}
|
|
|
|
_RESOLVED = (SingleLevelBreitWigner, MultiLevelBreitWigner,
|
|
ReichMoore, RMatrixLimited)
|