adding back files to be reviewed

This commit is contained in:
Paul Romano 2019-10-28 11:55:45 -05:00
parent ae28233110
commit bc09d1ef55
1244 changed files with 301904 additions and 0 deletions

28521
openmc/data/BREMX.DAT Normal file

File diff suppressed because it is too large Load diff

35
openmc/data/__init__.py Normal file
View file

@ -0,0 +1,35 @@
# Version of HDF5 nuclear data format
HDF5_VERSION_MAJOR = 3
HDF5_VERSION_MINOR = 0
HDF5_VERSION = (HDF5_VERSION_MAJOR, HDF5_VERSION_MINOR)
# Version of WMP nuclear data format
WMP_VERSION_MAJOR = 1
WMP_VERSION_MINOR = 1
WMP_VERSION = (WMP_VERSION_MAJOR, WMP_VERSION_MINOR)
from .data import *
from .neutron import *
from .photon import *
from .decay import *
from .reaction import *
from . import ace
from .angle_distribution import *
from . import endf
from .energy_distribution import *
from .product import *
from .angle_energy import *
from .uncorrelated import *
from .correlated import *
from .kalbach_mann import *
from .nbody import *
from .thermal import *
from .urr import *
from .library import *
from .fission_energy import *
from .resonance import *
from .resonance_covariance import *
from .multipole import *
from .grid import *
from .function import *

8
openmc/data/_endf.pyx Normal file
View file

@ -0,0 +1,8 @@
# cython: c_string_type=str, c_string_encoding=ascii
cdef extern from "endf.c":
double cfloat_endf(const char* buffer, int n)
def float_endf(s):
cdef const char* c_string = s
return cfloat_endf(c_string, len(s))

460
openmc/data/ace.py Normal file
View file

@ -0,0 +1,460 @@
"""This module is for reading ACE-format cross sections. ACE stands for "A
Compact ENDF" format and originated from work on MCNP_. It is used in a number
of other Monte Carlo particle transport codes.
ACE-format cross sections are typically generated from ENDF_ files through a
cross section processing program like NJOY_. The ENDF data consists of tabulated
thermal data, ENDF/B resonance parameters, distribution parameters in the
unresolved resonance region, and tabulated data in the fast region. After the
ENDF data has been reconstructed and Doppler-broadened, the ACER module
generates ACE-format cross sections.
.. _MCNP: https://laws.lanl.gov/vhosts/mcnp.lanl.gov/
.. _NJOY: http://t2.lanl.gov/codes.shtml
.. _ENDF: http://www.nndc.bnl.gov/endf
"""
from pathlib import PurePath
import struct
import sys
import numpy as np
from openmc.mixin import EqualityMixin
import openmc.checkvalue as cv
from .data import ATOMIC_SYMBOL, gnd_name
from .endf import ENDF_FLOAT_RE
def get_metadata(zaid, metastable_scheme='nndc'):
"""Return basic identifying data for a nuclide with a given ZAID.
Parameters
----------
zaid : int
ZAID (1000*Z + A) obtained from a library
metastable_scheme : {'nndc', 'mcnp'}
Determine how ZAID identifiers are to be interpreted in the case of
a metastable nuclide. Because the normal ZAID (=1000*Z + A) does not
encode metastable information, different conventions are used among
different libraries. In MCNP libraries, the convention is to add 400
for a metastable nuclide except for Am242m, for which 95242 is
metastable and 95642 (or 1095242 in newer libraries) is the ground
state. For NNDC libraries, ZAID is given as 1000*Z + A + 100*m.
Returns
-------
name : str
Name of the table
element : str
The atomic symbol of the isotope in the table; e.g., Zr.
Z : int
Number of protons in the nucleus
mass_number : int
Number of nucleons in the nucleus
metastable : int
Metastable state of the nucleus. A value of zero indicates ground state.
"""
cv.check_type('zaid', zaid, int)
cv.check_value('metastable_scheme', metastable_scheme, ['nndc', 'mcnp'])
Z = zaid // 1000
mass_number = zaid % 1000
if metastable_scheme == 'mcnp':
if zaid > 1000000:
# New SZA format
Z = Z % 1000
if zaid == 1095242:
metastable = 0
else:
metastable = zaid // 1000000
else:
if zaid == 95242:
metastable = 1
elif zaid == 95642:
metastable = 0
else:
metastable = 1 if mass_number > 300 else 0
elif metastable_scheme == 'nndc':
metastable = 1 if mass_number > 300 else 0
while mass_number > 3 * Z:
mass_number -= 100
# Determine name
element = ATOMIC_SYMBOL[Z]
name = gnd_name(Z, mass_number, metastable)
return (name, element, Z, mass_number, metastable)
def ascii_to_binary(ascii_file, binary_file):
"""Convert an ACE file in ASCII format (type 1) to binary format (type 2).
Parameters
----------
ascii_file : str
Filename of ASCII ACE file
binary_file : str
Filename of binary ACE file to be written
"""
# Open ASCII file
ascii = open(str(ascii_file), 'r')
# Set default record length
record_length = 4096
# Read data from ASCII file
lines = ascii.readlines()
ascii.close()
# Open binary file
binary = open(str(binary_file), 'wb')
idx = 0
while idx < len(lines):
# check if it's a > 2.0.0 version header
if lines[idx].split()[0][1] == '.':
if lines[idx + 1].split()[3] == '3':
idx = idx + 3
else:
raise NotImplementedError('Only backwards compatible ACE'
'headers currently supported')
# Read/write header block
hz = lines[idx][:10].encode('UTF-8')
aw0 = float(lines[idx][10:22])
tz = float(lines[idx][22:34])
hd = lines[idx][35:45].encode('UTF-8')
hk = lines[idx + 1][:70].encode('UTF-8')
hm = lines[idx + 1][70:80].encode('UTF-8')
binary.write(struct.pack(str('=10sdd10s70s10s'), hz, aw0, tz, hd, hk, hm))
# Read/write IZ/AW pairs
data = ' '.join(lines[idx + 2:idx + 6]).split()
iz = list(map(int, data[::2]))
aw = list(map(float, data[1::2]))
izaw = [item for sublist in zip(iz, aw) for item in sublist]
binary.write(struct.pack(str('=' + 16*'id'), *izaw))
# Read/write NXS and JXS arrays. Null bytes are added at the end so
# that XSS will start at the second record
nxs = list(map(int, ' '.join(lines[idx + 6:idx + 8]).split()))
jxs = list(map(int, ' '.join(lines[idx + 8:idx + 12]).split()))
binary.write(struct.pack(str('=16i32i{0}x'.format(record_length - 500)),
*(nxs + jxs)))
# Read/write XSS array. Null bytes are added to form a complete record
# at the end of the file
n_lines = (nxs[0] + 3)//4
xss = list(map(float, ' '.join(lines[
idx + 12:idx + 12 + n_lines]).split()))
extra_bytes = record_length - ((len(xss)*8 - 1) % record_length + 1)
binary.write(struct.pack(str('={0}d{1}x'.format(nxs[0], extra_bytes)),
*xss))
# Advance to next table in file
idx += 12 + n_lines
# Close binary file
binary.close()
def get_table(filename, name=None):
"""Read a single table from an ACE file
Parameters
----------
filename : str
Path of the ACE library to load table from
name : str, optional
Name of table to load, e.g. '92235.71c'
Returns
-------
openmc.data.ace.Table
ACE table with specified name. If no name is specified, the first table
in the file is returned.
"""
if name is None:
return Library(filename).tables[0]
else:
lib = Library(filename, name)
if lib.tables:
return lib.tables[0]
else:
raise ValueError('Could not find ACE table with name: {}'
.format(name))
class Library(EqualityMixin):
"""A Library objects represents an ACE-formatted file which may contain
multiple tables with data.
Parameters
----------
filename : str
Path of the ACE library file to load.
table_names : None, str, or iterable, optional
Tables from the file to read in. If None, reads in all of the
tables. If str, reads in only the single table of a matching name.
verbose : bool, optional
Determines whether output is printed to the stdout when reading a
Library
Attributes
----------
tables : list
List of :class:`Table` instances
"""
def __init__(self, filename, table_names=None, verbose=False):
if isinstance(table_names, str):
table_names = [table_names]
if table_names is not None:
table_names = set(table_names)
self.tables = []
# Determine whether file is ASCII or binary
filename = str(filename)
try:
fh = open(filename, 'rb')
# Grab 10 lines of the library
sb = b''.join([fh.readline() for i in range(10)])
# Try to decode it with ascii
sb.decode('ascii')
# No exception so proceed with ASCII - reopen in non-binary
fh.close()
with open(filename, 'r') as fh:
self._read_ascii(fh, table_names, verbose)
except UnicodeDecodeError:
fh.close()
with open(filename, 'rb') as fh:
self._read_binary(fh, table_names, verbose)
def _read_binary(self, ace_file, table_names, verbose=False,
recl_length=4096, entries=512):
"""Read a binary (Type 2) ACE table.
Parameters
----------
ace_file : file
Open ACE file
table_names : None, str, or iterable
Tables from the file to read in. If None, reads in all of the
tables. If str, reads in only the single table of a matching name.
verbose : str, optional
Whether to display what tables are being read. Defaults to False.
recl_length : int, optional
Fortran record length in binary file. Default value is 4096 bytes.
entries : int, optional
Number of entries per record. The default is 512 corresponding to a
record length of 4096 bytes with double precision data.
"""
while True:
start_position = ace_file.tell()
# Check for end-of-file
if len(ace_file.read(1)) == 0:
return
ace_file.seek(start_position)
# Read name, atomic mass ratio, temperature, date, comment, and
# material
name, atomic_weight_ratio, temperature, date, comment, mat = \
struct.unpack(str('=10sdd10s70s10s'), ace_file.read(116))
name = name.decode().strip()
# Read ZAID/awr combinations
data = struct.unpack(str('=' + 16*'id'), ace_file.read(192))
pairs = list(zip(data[::2], data[1::2]))
# Read NXS
nxs = list(struct.unpack(str('=16i'), ace_file.read(64)))
# Determine length of XSS and number of records
length = nxs[0]
n_records = (length + entries - 1)//entries
# verify that we are supposed to read this table in
if (table_names is not None) and (name not in table_names):
ace_file.seek(start_position + recl_length*(n_records + 1))
continue
if verbose:
kelvin = round(temperature * 1e6 / 8.617342e-5)
print("Loading nuclide {0} at {1} K".format(name, kelvin))
# Read JXS
jxs = list(struct.unpack(str('=32i'), ace_file.read(128)))
# Read XSS
ace_file.seek(start_position + recl_length)
xss = list(struct.unpack(str('={0}d'.format(length)),
ace_file.read(length*8)))
# Insert zeros at beginning of NXS, JXS, and XSS arrays so that the
# indexing will be the same as Fortran. This makes it easier to
# follow the ACE format specification.
nxs.insert(0, 0)
nxs = np.array(nxs, dtype=int)
jxs.insert(0, 0)
jxs = np.array(jxs, dtype=int)
xss.insert(0, 0.0)
xss = np.array(xss)
# Create ACE table with data read in
table = Table(name, atomic_weight_ratio, temperature, pairs,
nxs, jxs, xss)
self.tables.append(table)
# Advance to next record
ace_file.seek(start_position + recl_length*(n_records + 1))
def _read_ascii(self, ace_file, table_names, verbose=False):
"""Read an ASCII (Type 1) ACE table.
Parameters
----------
ace_file : file
Open ACE file
table_names : None, str, or iterable
Tables from the file to read in. If None, reads in all of the
tables. If str, reads in only the single table of a matching name.
verbose : str, optional
Whether to display what tables are being read. Defaults to False.
"""
tables_seen = set()
lines = [ace_file.readline() for i in range(13)]
while len(lines) != 0 and lines[0].strip() != '':
# Read name of table, atomic mass ratio, and temperature. If first
# line is empty, we are at end of file
# check if it's a 2.0 style header
if lines[0].split()[0][1] == '.':
words = lines[0].split()
name = words[1]
words = lines[1].split()
atomic_weight_ratio = float(words[0])
temperature = float(words[1])
commentlines = int(words[3])
for i in range(commentlines):
lines.pop(0)
lines.append(ace_file.readline())
else:
words = lines[0].split()
name = words[0]
atomic_weight_ratio = float(words[1])
temperature = float(words[2])
datastr = ' '.join(lines[2:6]).split()
pairs = list(zip(map(int, datastr[::2]),
map(float, datastr[1::2])))
datastr = '0 ' + ' '.join(lines[6:8])
nxs = np.fromstring(datastr, sep=' ', dtype=int)
n_lines = (nxs[1] + 3)//4
# Ensure that we have more tables to read in
if (table_names is not None) and (table_names <= tables_seen):
break
tables_seen.add(name)
# verify that we are supposed to read this table in
if (table_names is not None) and (name not in table_names):
for i in range(n_lines - 1):
ace_file.readline()
lines = [ace_file.readline() for i in range(13)]
continue
# Read lines corresponding to this table
lines += [ace_file.readline() for i in range(n_lines - 1)]
if verbose:
kelvin = round(temperature * 1e6 / 8.617342e-5)
print("Loading nuclide {0} at {1} K".format(name, kelvin))
# Insert zeros at beginning of NXS, JXS, and XSS arrays so that the
# indexing will be the same as Fortran. This makes it easier to
# follow the ACE format specification.
datastr = '0 ' + ' '.join(lines[8:12])
jxs = np.fromstring(datastr, dtype=int, sep=' ')
datastr = '0.0 ' + ''.join(lines[12:12+n_lines])
xss = np.fromstring(datastr, sep=' ')
# When NJOY writes an ACE file, any values less than 1e-100 actually
# get written without the 'e'. Thus, what we do here is check
# whether the xss array is of the right size (if a number like
# 1.0-120 is encountered, np.fromstring won't capture any numbers
# after it). If it's too short, then we apply the ENDF float regular
# expression. We don't do this by default because it's expensive!
if xss.size != nxs[1] + 1:
datastr = ENDF_FLOAT_RE.sub(r'\1e\2\3', datastr)
xss = np.fromstring(datastr, sep=' ')
assert xss.size == nxs[1] + 1
table = Table(name, atomic_weight_ratio, temperature, pairs,
nxs, jxs, xss)
self.tables.append(table)
# Read all data blocks
lines = [ace_file.readline() for i in range(13)]
class Table(EqualityMixin):
"""ACE cross section table
Parameters
----------
name : str
ZAID identifier of the table, e.g. '92235.70c'.
atomic_weight_ratio : float
Atomic mass ratio of the target nuclide.
temperature : float
Temperature of the target nuclide in MeV.
pairs : list of tuple
16 pairs of ZAIDs and atomic weight ratios. Used for thermal scattering
tables to indicate what isotopes scattering is applied to.
nxs : numpy.ndarray
Array that defines various lengths with in the table
jxs : numpy.ndarray
Array that gives locations in the ``xss`` array for various blocks of
data
xss : numpy.ndarray
Raw data for the ACE table
"""
def __init__(self, name, atomic_weight_ratio, temperature, pairs,
nxs, jxs, xss):
self.name = name
self.atomic_weight_ratio = atomic_weight_ratio
self.temperature = temperature
self.pairs = pairs
self.nxs = nxs
self.jxs = jxs
self.xss = xss
def __repr__(self):
return "<ACE Table: {}>".format(self.name)

View file

@ -0,0 +1,310 @@
from collections.abc import Iterable
from io import StringIO
from numbers import Real
from warnings import warn
import numpy as np
import openmc.checkvalue as cv
from openmc.mixin import EqualityMixin
from openmc.stats import Univariate, Tabular, Uniform, Legendre
from .function import INTERPOLATION_SCHEME
from .data import EV_PER_MEV
from .endf import get_head_record, get_cont_record, get_tab1_record, \
get_list_record, get_tab2_record
class AngleDistribution(EqualityMixin):
"""Angle distribution as a function of incoming energy
Parameters
----------
energy : Iterable of float
Incoming energies in eV at which distributions exist
mu : Iterable of openmc.stats.Univariate
Distribution of scattering cosines corresponding to each incoming energy
Attributes
----------
energy : Iterable of float
Incoming energies in eV at which distributions exist
mu : Iterable of openmc.stats.Univariate
Distribution of scattering cosines corresponding to each incoming energy
"""
def __init__(self, energy, mu):
super().__init__()
self.energy = energy
self.mu = mu
@property
def energy(self):
return self._energy
@property
def mu(self):
return self._mu
@energy.setter
def energy(self, energy):
cv.check_type('angle distribution incoming energy', energy,
Iterable, Real)
self._energy = energy
@mu.setter
def mu(self, mu):
cv.check_type('angle distribution scattering cosines', mu,
Iterable, Univariate)
self._mu = mu
def to_hdf5(self, group):
"""Write angle distribution to an HDF5 group
Parameters
----------
group : h5py.Group
HDF5 group to write to
"""
dset = group.create_dataset('energy', data=self.energy)
# Make sure all data is tabular
mu_tabular = [mu_i if isinstance(mu_i, Tabular) else
mu_i.to_tabular() for mu_i in self.mu]
# Determine total number of (mu,p) pairs and create array
n_pairs = sum([len(mu_i.x) for mu_i in mu_tabular])
pairs = np.empty((3, n_pairs))
# Create array for offsets
offsets = np.empty(len(mu_tabular), dtype=int)
interpolation = np.empty(len(mu_tabular), dtype=int)
j = 0
# Populate offsets and pairs array
for i, mu_i in enumerate(mu_tabular):
n = len(mu_i.x)
offsets[i] = j
interpolation[i] = 1 if mu_i.interpolation == 'histogram' else 2
pairs[0, j:j+n] = mu_i.x
pairs[1, j:j+n] = mu_i.p
pairs[2, j:j+n] = mu_i.c
j += n
# Create dataset for distributions
dset = group.create_dataset('mu', data=pairs)
# Write interpolation as attribute
dset.attrs['offsets'] = offsets
dset.attrs['interpolation'] = interpolation
@classmethod
def from_hdf5(cls, group):
"""Generate angular distribution from HDF5 data
Parameters
----------
group : h5py.Group
HDF5 group to read from
Returns
-------
openmc.data.AngleDistribution
Angular distribution
"""
energy = group['energy'][()]
data = group['mu']
offsets = data.attrs['offsets']
interpolation = data.attrs['interpolation']
mu = []
n_energy = len(energy)
for i in range(n_energy):
# Determine length of outgoing energy distribution and number of
# discrete lines
j = offsets[i]
if i < n_energy - 1:
n = offsets[i+1] - j
else:
n = data.shape[1] - j
interp = INTERPOLATION_SCHEME[interpolation[i]]
mu_i = Tabular(data[0, j:j+n], data[1, j:j+n], interp)
mu_i.c = data[2, j:j+n]
mu.append(mu_i)
return cls(energy, mu)
@classmethod
def from_ace(cls, ace, location_dist, location_start):
"""Generate an angular distribution from ACE data
Parameters
----------
ace : openmc.data.ace.Table
ACE table to read from
location_dist : int
Index in the XSS array corresponding to the start of a block,
e.g. JXS(9).
location_start : int
Index in the XSS array corresponding to the start of an angle
distribution array
Returns
-------
openmc.data.AngleDistribution
Angular distribution
"""
# Set starting index for angle distribution
idx = location_dist + location_start - 1
# Number of energies at which angular distributions are tabulated
n_energies = int(ace.xss[idx])
idx += 1
# Incoming energy grid
energy = ace.xss[idx:idx + n_energies]*EV_PER_MEV
idx += n_energies
# Read locations for angular distributions
lc = ace.xss[idx:idx + n_energies].astype(int)
idx += n_energies
mu = []
for i in range(n_energies):
if lc[i] > 0:
# Equiprobable 32 bin distribution
idx = location_dist + abs(lc[i]) - 1
cos = ace.xss[idx:idx + 33]
pdf = np.zeros(33)
pdf[:32] = 1.0/(32.0*np.diff(cos))
cdf = np.linspace(0.0, 1.0, 33)
mu_i = Tabular(cos, pdf, 'histogram', ignore_negative=True)
mu_i.c = cdf
elif lc[i] < 0:
# Tabular angular distribution
idx = location_dist + abs(lc[i]) - 1
intt = int(ace.xss[idx])
n_points = int(ace.xss[idx + 1])
data = ace.xss[idx + 2:idx + 2 + 3*n_points]
data.shape = (3, n_points)
mu_i = Tabular(data[0], data[1], INTERPOLATION_SCHEME[intt])
mu_i.c = data[2]
else:
# Isotropic angular distribution
mu_i = Uniform(-1., 1.)
mu.append(mu_i)
return cls(energy, mu)
@classmethod
def from_endf(cls, ev, mt):
"""Generate an angular distribution from an ENDF evaluation
Parameters
----------
ev : openmc.data.endf.Evaluation
ENDF evaluation
mt : int
The MT value of the reaction to get angular distributions for
Returns
-------
openmc.data.AngleDistribution
Angular distribution
"""
file_obj = StringIO(ev.section[4, mt])
# Read HEAD record
items = get_head_record(file_obj)
lvt = items[2]
ltt = items[3]
# Read CONT record
items = get_cont_record(file_obj)
li = items[2]
nk = items[4]
center_of_mass = (items[3] == 2)
# Check for obsolete energy transformation matrix. If present, just skip
# it and keep reading
if lvt > 0:
warn('Obsolete energy transformation matrix in MF=4 angular '
'distribution.')
for _ in range((nk + 5)//6):
file_obj.readline()
if ltt == 0 and li == 1:
# Purely isotropic
energy = np.array([0., ev.info['energy_max']])
mu = [Uniform(-1., 1.), Uniform(-1., 1.)]
elif ltt == 1 and li == 0:
# Legendre polynomial coefficients
params, tab2 = get_tab2_record(file_obj)
n_energy = params[5]
energy = np.zeros(n_energy)
mu = []
for i in range(n_energy):
items, al = get_list_record(file_obj)
temperature = items[0]
energy[i] = items[1]
coefficients = np.asarray([1.0] + al)
mu.append(Legendre(coefficients))
elif ltt == 2 and li == 0:
# Tabulated probability distribution
params, tab2 = get_tab2_record(file_obj)
n_energy = params[5]
energy = np.zeros(n_energy)
mu = []
for i in range(n_energy):
params, f = get_tab1_record(file_obj)
temperature = params[0]
energy[i] = params[1]
if f.n_regions > 1:
raise NotImplementedError('Angular distribution with multiple '
'interpolation regions not supported.')
mu.append(Tabular(f.x, f.y, INTERPOLATION_SCHEME[f.interpolation[0]]))
elif ltt == 3 and li == 0:
# Legendre for low energies / tabulated for high energies
params, tab2 = get_tab2_record(file_obj)
n_energy_legendre = params[5]
energy_legendre = np.zeros(n_energy_legendre)
mu = []
for i in range(n_energy_legendre):
items, al = get_list_record(file_obj)
temperature = items[0]
energy_legendre[i] = items[1]
coefficients = np.asarray([1.0] + al)
mu.append(Legendre(coefficients))
params, tab2 = get_tab2_record(file_obj)
n_energy_tabulated = params[5]
energy_tabulated = np.zeros(n_energy_tabulated)
for i in range(n_energy_tabulated):
params, f = get_tab1_record(file_obj)
temperature = params[0]
energy_tabulated[i] = params[1]
if f.n_regions > 1:
raise NotImplementedError('Angular distribution with multiple '
'interpolation regions not supported.')
mu.append(Tabular(f.x, f.y, INTERPOLATION_SCHEME[f.interpolation[0]]))
energy = np.concatenate((energy_legendre, energy_tabulated))
return AngleDistribution(energy, mu)

117
openmc/data/angle_energy.py Normal file
View file

@ -0,0 +1,117 @@
from abc import ABCMeta, abstractmethod
from io import StringIO
import openmc.data
from openmc.mixin import EqualityMixin
class AngleEnergy(EqualityMixin, metaclass=ABCMeta):
"""Distribution in angle and energy of a secondary particle."""
@abstractmethod
def to_hdf5(self, group):
pass
@staticmethod
def from_hdf5(group):
"""Generate angle-energy distribution from HDF5 data
Parameters
----------
group : h5py.Group
HDF5 group to read from
Returns
-------
openmc.data.AngleEnergy
Angle-energy distribution
"""
dist_type = group.attrs['type'].decode()
if dist_type == 'uncorrelated':
return openmc.data.UncorrelatedAngleEnergy.from_hdf5(group)
elif dist_type == 'correlated':
return openmc.data.CorrelatedAngleEnergy.from_hdf5(group)
elif dist_type == 'kalbach-mann':
return openmc.data.KalbachMann.from_hdf5(group)
elif dist_type == 'nbody':
return openmc.data.NBodyPhaseSpace.from_hdf5(group)
elif dist_type == 'coherent_elastic':
return openmc.data.CoherentElasticAE.from_hdf5(group)
elif dist_type == 'incoherent_elastic':
return openmc.data.IncoherentElasticAE.from_hdf5(group)
elif dist_type == 'incoherent_elastic_discrete':
return openmc.data.IncoherentElasticAEDiscrete.from_hdf5(group)
elif dist_type == 'incoherent_inelastic_discrete':
return openmc.data.IncoherentInelasticAEDiscrete.from_hdf5(group)
elif dist_type == 'incoherent_inelastic':
return openmc.data.IncoherentInelasticAE.from_hdf5(group)
@staticmethod
def from_ace(ace, location_dist, location_start, rx=None):
"""Generate an angle-energy distribution from ACE data
Parameters
----------
ace : openmc.data.ace.Table
ACE table to read from
location_dist : int
Index in the XSS array corresponding to the start of a block,
e.g. JXS(11) for the the DLW block.
location_start : int
Index in the XSS array corresponding to the start of an energy
distribution array
rx : Reaction
Reaction this energy distribution will be associated with
Returns
-------
distribution : openmc.data.AngleEnergy
Secondary angle-energy distribution
"""
# Set starting index for energy distribution
idx = location_dist + location_start - 1
law = int(ace.xss[idx + 1])
location_data = int(ace.xss[idx + 2])
# Position index for reading law data
idx = location_dist + location_data - 1
# Parse energy distribution data
if law == 2:
distribution = openmc.data.UncorrelatedAngleEnergy()
distribution.energy = openmc.data.DiscretePhoton.from_ace(ace, idx)
elif law in (3, 33):
distribution = openmc.data.UncorrelatedAngleEnergy()
distribution.energy = openmc.data.LevelInelastic.from_ace(ace, idx)
elif law == 4:
distribution = openmc.data.UncorrelatedAngleEnergy()
distribution.energy = openmc.data.ContinuousTabular.from_ace(
ace, idx, location_dist)
elif law == 5:
distribution = openmc.data.UncorrelatedAngleEnergy()
distribution.energy = openmc.data.GeneralEvaporation.from_ace(ace, idx)
elif law == 7:
distribution = openmc.data.UncorrelatedAngleEnergy()
distribution.energy = openmc.data.MaxwellEnergy.from_ace(ace, idx)
elif law == 9:
distribution = openmc.data.UncorrelatedAngleEnergy()
distribution.energy = openmc.data.Evaporation.from_ace(ace, idx)
elif law == 11:
distribution = openmc.data.UncorrelatedAngleEnergy()
distribution.energy = openmc.data.WattEnergy.from_ace(ace, idx)
elif law == 44:
distribution = openmc.data.KalbachMann.from_ace(
ace, idx, location_dist)
elif law == 61:
distribution = openmc.data.CorrelatedAngleEnergy.from_ace(
ace, idx, location_dist)
elif law == 66:
distribution = openmc.data.NBodyPhaseSpace.from_ace(
ace, idx, rx.q_value)
else:
raise ValueError("Unsupported ACE secondary energy "
"distribution law {}".format(law))
return distribution

Binary file not shown.

462
openmc/data/correlated.py Normal file
View file

@ -0,0 +1,462 @@
from collections.abc import Iterable
from numbers import Real, Integral
from warnings import warn
import numpy as np
import openmc.checkvalue as cv
from openmc.stats import Tabular, Univariate, Discrete, Mixture, \
Uniform, Legendre
from .function import INTERPOLATION_SCHEME
from .angle_energy import AngleEnergy
from .data import EV_PER_MEV
from .endf import get_list_record, get_tab2_record
class CorrelatedAngleEnergy(AngleEnergy):
"""Correlated angle-energy distribution
Parameters
----------
breakpoints : Iterable of int
Breakpoints defining interpolation regions
interpolation : Iterable of int
Interpolation codes
energy : Iterable of float
Incoming energies at which distributions exist
energy_out : Iterable of openmc.stats.Univariate
Distribution of outgoing energies corresponding to each incoming energy
mu : Iterable of Iterable of openmc.stats.Univariate
Distribution of scattering cosine for each incoming/outgoing energy
Attributes
----------
breakpoints : Iterable of int
Breakpoints defining interpolation regions
interpolation : Iterable of int
Interpolation codes
energy : Iterable of float
Incoming energies at which distributions exist
energy_out : Iterable of openmc.stats.Univariate
Distribution of outgoing energies corresponding to each incoming energy
mu : Iterable of Iterable of openmc.stats.Univariate
Distribution of scattering cosine for each incoming/outgoing energy
"""
_name = 'correlated'
def __init__(self, breakpoints, interpolation, energy, energy_out, mu):
super().__init__()
self.breakpoints = breakpoints
self.interpolation = interpolation
self.energy = energy
self.energy_out = energy_out
self.mu = mu
@property
def breakpoints(self):
return self._breakpoints
@property
def interpolation(self):
return self._interpolation
@property
def energy(self):
return self._energy
@property
def energy_out(self):
return self._energy_out
@property
def mu(self):
return self._mu
@breakpoints.setter
def breakpoints(self, breakpoints):
cv.check_type('correlated angle-energy breakpoints', breakpoints,
Iterable, Integral)
self._breakpoints = breakpoints
@interpolation.setter
def interpolation(self, interpolation):
cv.check_type('correlated angle-energy interpolation', interpolation,
Iterable, Integral)
self._interpolation = interpolation
@energy.setter
def energy(self, energy):
cv.check_type('correlated angle-energy incoming energy', energy,
Iterable, Real)
self._energy = energy
@energy_out.setter
def energy_out(self, energy_out):
cv.check_type('correlated angle-energy outgoing energy', energy_out,
Iterable, Univariate)
self._energy_out = energy_out
@mu.setter
def mu(self, mu):
cv.check_iterable_type('correlated angle-energy outgoing cosine',
mu, Univariate, 2, 2)
self._mu = mu
def to_hdf5(self, group):
"""Write distribution to an HDF5 group
Parameters
----------
group : h5py.Group
HDF5 group to write to
"""
group.attrs['type'] = np.string_(self._name)
dset = group.create_dataset('energy', data=self.energy)
dset.attrs['interpolation'] = np.vstack((self.breakpoints,
self.interpolation))
# Determine total number of (E,p) pairs and create array
n_tuple = sum(len(d.x) for d in self.energy_out)
eout = np.empty((5, n_tuple))
# Make sure all mu data is tabular
mu_tabular = []
for i, mu_i in enumerate(self.mu):
mu_tabular.append([mu_ij if isinstance(mu_ij, (Tabular, Discrete)) else
mu_ij.to_tabular() for mu_ij in mu_i])
# Determine total number of (mu,p) points and create array
n_tuple = sum(sum(len(mu_ij.x) for mu_ij in mu_i)
for mu_i in mu_tabular)
mu = np.empty((3, n_tuple))
# Create array for offsets
offsets = np.empty(len(self.energy_out), dtype=int)
interpolation = np.empty(len(self.energy_out), dtype=int)
n_discrete_lines = np.empty(len(self.energy_out), dtype=int)
offset_e = 0
offset_mu = 0
# Populate offsets and eout array
for i, d in enumerate(self.energy_out):
n = len(d)
offsets[i] = offset_e
if isinstance(d, Mixture):
discrete, continuous = d.distribution
n_discrete_lines[i] = m = len(discrete)
interpolation[i] = 1 if continuous.interpolation == 'histogram' else 2
eout[0, offset_e:offset_e+m] = discrete.x
eout[1, offset_e:offset_e+m] = discrete.p
eout[2, offset_e:offset_e+m] = discrete.c
eout[0, offset_e+m:offset_e+n] = continuous.x
eout[1, offset_e+m:offset_e+n] = continuous.p
eout[2, offset_e+m:offset_e+n] = continuous.c
else:
if isinstance(d, Tabular):
n_discrete_lines[i] = 0
interpolation[i] = 1 if d.interpolation == 'histogram' else 2
elif isinstance(d, Discrete):
n_discrete_lines[i] = n
interpolation[i] = 1
eout[0, offset_e:offset_e+n] = d.x
eout[1, offset_e:offset_e+n] = d.p
eout[2, offset_e:offset_e+n] = d.c
for j, mu_ij in enumerate(mu_tabular[i]):
if isinstance(mu_ij, Discrete):
eout[3, offset_e+j] = 0
else:
eout[3, offset_e+j] = 1 if mu_ij.interpolation == 'histogram' else 2
eout[4, offset_e+j] = offset_mu
n_mu = len(mu_ij)
mu[0, offset_mu:offset_mu+n_mu] = mu_ij.x
mu[1, offset_mu:offset_mu+n_mu] = mu_ij.p
mu[2, offset_mu:offset_mu+n_mu] = mu_ij.c
offset_mu += n_mu
offset_e += n
# Create dataset for outgoing energy distributions
dset = group.create_dataset('energy_out', data=eout)
# Write interpolation on outgoing energy as attribute
dset.attrs['offsets'] = offsets
dset.attrs['interpolation'] = interpolation
dset.attrs['n_discrete_lines'] = n_discrete_lines
# Create dataset for outgoing angle distributions
group.create_dataset('mu', data=mu)
@classmethod
def from_hdf5(cls, group):
"""Generate correlated angle-energy distribution from HDF5 data
Parameters
----------
group : h5py.Group
HDF5 group to read from
Returns
-------
openmc.data.CorrelatedAngleEnergy
Correlated angle-energy distribution
"""
interp_data = group['energy'].attrs['interpolation']
energy_breakpoints = interp_data[0, :]
energy_interpolation = interp_data[1, :]
energy = group['energy'][()]
offsets = group['energy_out'].attrs['offsets']
interpolation = group['energy_out'].attrs['interpolation']
n_discrete_lines = group['energy_out'].attrs['n_discrete_lines']
dset_eout = group['energy_out'][()]
energy_out = []
dset_mu = group['mu'][()]
mu = []
n_energy = len(energy)
for i in range(n_energy):
# Determine length of outgoing energy distribution and number of
# discrete lines
offset_e = offsets[i]
if i < n_energy - 1:
n = offsets[i+1] - offset_e
else:
n = dset_eout.shape[1] - offset_e
m = n_discrete_lines[i]
# Create discrete distribution if lines are present
if m > 0:
x = dset_eout[0, offset_e:offset_e+m]
p = dset_eout[1, offset_e:offset_e+m]
eout_discrete = Discrete(x, p)
eout_discrete.c = dset_eout[2, offset_e:offset_e+m]
p_discrete = eout_discrete.c[-1]
# Create continuous distribution
if m < n:
interp = INTERPOLATION_SCHEME[interpolation[i]]
x = dset_eout[0, offset_e+m:offset_e+n]
p = dset_eout[1, offset_e+m:offset_e+n]
eout_continuous = Tabular(x, p, interp, ignore_negative=True)
eout_continuous.c = dset_eout[2, offset_e+m:offset_e+n]
# If both continuous and discrete are present, create a mixture
# distribution
if m == 0:
eout_i = eout_continuous
elif m == n:
eout_i = eout_discrete
else:
eout_i = Mixture([p_discrete, 1. - p_discrete],
[eout_discrete, eout_continuous])
# Read angular distributions
mu_i = []
for j in range(n):
# Determine interpolation scheme
interp_code = int(dset_eout[3, offsets[i] + j])
# Determine offset and length
offset_mu = int(dset_eout[4, offsets[i] + j])
if offsets[i] + j < dset_eout.shape[1] - 1:
n_mu = int(dset_eout[4, offsets[i] + j + 1]) - offset_mu
else:
n_mu = dset_mu.shape[1] - offset_mu
# Get data
x = dset_mu[0, offset_mu:offset_mu+n_mu]
p = dset_mu[1, offset_mu:offset_mu+n_mu]
c = dset_mu[2, offset_mu:offset_mu+n_mu]
if interp_code == 0:
mu_ij = Discrete(x, p)
else:
mu_ij = Tabular(x, p, INTERPOLATION_SCHEME[interp_code],
ignore_negative=True)
mu_ij.c = c
mu_i.append(mu_ij)
offset_mu += n_mu
energy_out.append(eout_i)
mu.append(mu_i)
return cls(energy_breakpoints, energy_interpolation,
energy, energy_out, mu)
@classmethod
def from_ace(cls, ace, idx, ldis):
"""Generate correlated angle-energy distribution from ACE data
Parameters
----------
ace : openmc.data.ace.Table
ACE table to read from
idx : int
Index in XSS array of the start of the energy distribution data
(LDIS + LOCC - 1)
ldis : int
Index in XSS array of the start of the energy distribution block
(e.g. JXS[11])
Returns
-------
openmc.data.CorrelatedAngleEnergy
Correlated angle-energy distribution
"""
# Read number of interpolation regions and incoming energies
n_regions = int(ace.xss[idx])
n_energy_in = int(ace.xss[idx + 1 + 2*n_regions])
# Get interpolation information
idx += 1
if n_regions > 0:
breakpoints = ace.xss[idx:idx + n_regions].astype(int)
interpolation = ace.xss[idx + n_regions:idx + 2*n_regions].astype(int)
else:
breakpoints = np.array([n_energy_in])
interpolation = np.array([2])
# Incoming energies at which distributions exist
idx += 2*n_regions + 1
energy = ace.xss[idx:idx + n_energy_in]*EV_PER_MEV
# Location of distributions
idx += n_energy_in
loc_dist = ace.xss[idx:idx + n_energy_in].astype(int)
# Initialize list of distributions
energy_out = []
mu = []
# Read each outgoing energy distribution
for i in range(n_energy_in):
idx = ldis + loc_dist[i] - 1
# intt = interpolation scheme (1=hist, 2=lin-lin)
INTTp = int(ace.xss[idx])
intt = INTTp % 10
n_discrete_lines = (INTTp - intt)//10
if intt not in (1, 2):
warn("Interpolation scheme for continuous tabular distribution "
"is not histogram or linear-linear.")
intt = 2
# Secondary energy distribution
n_energy_out = int(ace.xss[idx + 1])
data = ace.xss[idx + 2:idx + 2 + 4*n_energy_out].copy()
data.shape = (4, n_energy_out)
data[0,:] *= EV_PER_MEV
# Create continuous distribution
eout_continuous = Tabular(data[0][n_discrete_lines:],
data[1][n_discrete_lines:]/EV_PER_MEV,
INTERPOLATION_SCHEME[intt],
ignore_negative=True)
eout_continuous.c = data[2][n_discrete_lines:]
if np.any(data[1][n_discrete_lines:] < 0.0):
warn("Correlated angle-energy distribution has negative "
"probabilities.")
# If discrete lines are present, create a mixture distribution
if n_discrete_lines > 0:
eout_discrete = Discrete(data[0][:n_discrete_lines],
data[1][:n_discrete_lines])
eout_discrete.c = data[2][:n_discrete_lines]
if n_discrete_lines == n_energy_out:
eout_i = eout_discrete
else:
p_discrete = min(sum(eout_discrete.p), 1.0)
eout_i = Mixture([p_discrete, 1. - p_discrete],
[eout_discrete, eout_continuous])
else:
eout_i = eout_continuous
energy_out.append(eout_i)
lc = data[3].astype(int)
# Secondary angular distributions
mu_i = []
for j in range(n_energy_out):
if lc[j] > 0:
idx = ldis + abs(lc[j]) - 1
intt = int(ace.xss[idx])
n_cosine = int(ace.xss[idx + 1])
data = ace.xss[idx + 2:idx + 2 + 3*n_cosine]
data.shape = (3, n_cosine)
mu_ij = Tabular(data[0], data[1], INTERPOLATION_SCHEME[intt])
mu_ij.c = data[2]
else:
# Isotropic distribution
mu_ij = Uniform(-1., 1.)
mu_i.append(mu_ij)
# Add cosine distributions for this incoming energy to list
mu.append(mu_i)
return cls(breakpoints, interpolation, energy, energy_out, mu)
@classmethod
def from_endf(cls, file_obj):
"""Generate correlated angle-energy distribution from an ENDF evaluation
Parameters
----------
file_obj : file-like object
ENDF file positioned at the start of a section for a correlated
angle-energy distribution
Returns
-------
openmc.data.CorrelatedAngleEnergy
Correlated angle-energy distribution
"""
params, tab2 = get_tab2_record(file_obj)
lep = params[3]
ne = params[5]
energy = np.zeros(ne)
n_discrete_energies = np.zeros(ne, dtype=int)
energy_out = []
mu = []
for i in range(ne):
items, values = get_list_record(file_obj)
energy[i] = items[1]
n_discrete_energies[i] = items[2]
# TODO: separate out discrete lines
n_angle = items[3]
n_energy_out = items[5]
values = np.asarray(values)
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]
energy_out_i = Tabular(eout_i, eout_p_i, INTERPOLATION_SCHEME[lep],
ignore_negative=True)
energy_out.append(energy_out_i)
# Legendre coefficients used for angular distributions
mu_i = []
for j in range(n_energy_out):
mu_i.append(Legendre(values[j,1:]))
mu.append(mu_i)
return cls(tab2.breakpoints, tab2.interpolation, energy,
energy_out, mu)

385
openmc/data/data.py Normal file
View file

@ -0,0 +1,385 @@
import itertools
from math import sqrt
import os
import re
from warnings import warn
# Isotopic abundances from Meija J, Coplen T B, et al, "Isotopic compositions
# of the elements 2013 (IUPAC Technical Report)", Pure. Appl. Chem. 88 (3),
# pp. 293-306 (2013). The "representative isotopic abundance" values from
# column 9 are used except where an interval is given, in which case the
# "best measurement" is used.
NATURAL_ABUNDANCE = {
'H1': 0.99984426, 'H2': 0.00015574, 'He3': 0.000002,
'He4': 0.999998, 'Li6': 0.07589, 'Li7': 0.92411,
'Be9': 1.0, 'B10': 0.1982, 'B11': 0.8018,
'C12': 0.988922, 'C13': 0.011078, 'N14': 0.996337,
'N15': 0.003663, 'O16': 0.9976206, 'O17': 0.000379,
'O18': 0.0020004, 'F19': 1.0, 'Ne20': 0.9048,
'Ne21': 0.0027, 'Ne22': 0.0925, 'Na23': 1.0,
'Mg24': 0.78951, 'Mg25': 0.1002, 'Mg26': 0.11029,
'Al27': 1.0, 'Si28': 0.9222968, 'Si29': 0.0468316,
'Si30': 0.0308716, 'P31': 1.0, 'S32': 0.9504074,
'S33': 0.0074869, 'S34': 0.0419599, 'S36': 0.0001458,
'Cl35': 0.757647, 'Cl37': 0.242353, 'Ar36': 0.003336,
'Ar38': 0.000629, 'Ar40': 0.996035, 'K39': 0.932581,
'K40': 0.000117, 'K41': 0.067302, 'Ca40': 0.96941,
'Ca42': 0.00647, 'Ca43': 0.00135, 'Ca44': 0.02086,
'Ca46': 0.00004, 'Ca48': 0.00187, 'Sc45': 1.0,
'Ti46': 0.0825, 'Ti47': 0.0744, 'Ti48': 0.7372,
'Ti49': 0.0541, 'Ti50': 0.0518, 'V50': 0.0025,
'V51': 0.9975, 'Cr50': 0.04345, 'Cr52': 0.83789,
'Cr53': 0.09501, 'Cr54': 0.02365, 'Mn55': 1.0,
'Fe54': 0.05845, 'Fe56': 0.91754, 'Fe57': 0.02119,
'Fe58': 0.00282, 'Co59': 1.0, 'Ni58': 0.680769,
'Ni60': 0.262231, 'Ni61': 0.011399, 'Ni62': 0.036345,
'Ni64': 0.009256, 'Cu63': 0.6915, 'Cu65': 0.3085,
'Zn64': 0.4917, 'Zn66': 0.2773, 'Zn67': 0.0404,
'Zn68': 0.1845, 'Zn70': 0.0061, 'Ga69': 0.60108,
'Ga71': 0.39892, 'Ge70': 0.2052, 'Ge72': 0.2745,
'Ge73': 0.0776, 'Ge74': 0.3652, 'Ge76': 0.0775,
'As75': 1.0, 'Se74': 0.0086, 'Se76': 0.0923,
'Se77': 0.076, 'Se78': 0.2369, 'Se80': 0.498,
'Se82': 0.0882, 'Br79': 0.50686, 'Br81': 0.49314,
'Kr78': 0.00355, 'Kr80': 0.02286, 'Kr82': 0.11593,
'Kr83': 0.115, 'Kr84': 0.56987, 'Kr86': 0.17279,
'Rb85': 0.7217, 'Rb87': 0.2783, 'Sr84': 0.0056,
'Sr86': 0.0986, 'Sr87': 0.07, 'Sr88': 0.8258,
'Y89': 1.0, 'Zr90': 0.5145, 'Zr91': 0.1122,
'Zr92': 0.1715, 'Zr94': 0.1738, 'Zr96': 0.028,
'Nb93': 1.0, 'Mo92': 0.14649, 'Mo94': 0.09187,
'Mo95': 0.15873, 'Mo96': 0.16673, 'Mo97': 0.09582,
'Mo98': 0.24292, 'Mo100': 0.09744, 'Ru96': 0.0554,
'Ru98': 0.0187, 'Ru99': 0.1276, 'Ru100': 0.126,
'Ru101': 0.1706, 'Ru102': 0.3155, 'Ru104': 0.1862,
'Rh103': 1.0, 'Pd102': 0.0102, 'Pd104': 0.1114,
'Pd105': 0.2233, 'Pd106': 0.2733, 'Pd108': 0.2646,
'Pd110': 0.1172, 'Ag107': 0.51839, 'Ag109': 0.48161,
'Cd106': 0.01245, 'Cd108': 0.00888, 'Cd110': 0.1247,
'Cd111': 0.12795, 'Cd112': 0.24109, 'Cd113': 0.12227,
'Cd114': 0.28754, 'Cd116': 0.07512, 'In113': 0.04281,
'In115': 0.95719, 'Sn112': 0.0097, 'Sn114': 0.0066,
'Sn115': 0.0034, 'Sn116': 0.1454, 'Sn117': 0.0768,
'Sn118': 0.2422, 'Sn119': 0.0859, 'Sn120': 0.3258,
'Sn122': 0.0463, 'Sn124': 0.0579, 'Sb121': 0.5721,
'Sb123': 0.4279, 'Te120': 0.0009, 'Te122': 0.0255,
'Te123': 0.0089, 'Te124': 0.0474, 'Te125': 0.0707,
'Te126': 0.1884, 'Te128': 0.3174, 'Te130': 0.3408,
'I127': 1.0, 'Xe124': 0.00095, 'Xe126': 0.00089,
'Xe128': 0.0191, 'Xe129': 0.26401, 'Xe130': 0.04071,
'Xe131': 0.21232, 'Xe132': 0.26909, 'Xe134': 0.10436,
'Xe136': 0.08857, 'Cs133': 1.0, 'Ba130': 0.0011,
'Ba132': 0.001, 'Ba134': 0.0242, 'Ba135': 0.0659,
'Ba136': 0.0785, 'Ba137': 0.1123, 'Ba138': 0.717,
'La138': 0.0008881, 'La139': 0.9991119, 'Ce136': 0.00186,
'Ce138': 0.00251, 'Ce140': 0.88449, 'Ce142': 0.11114,
'Pr141': 1.0, 'Nd142': 0.27153, 'Nd143': 0.12173,
'Nd144': 0.23798, 'Nd145': 0.08293, 'Nd146': 0.17189,
'Nd148': 0.05756, 'Nd150': 0.05638, 'Sm144': 0.0308,
'Sm147': 0.15, 'Sm148': 0.1125, 'Sm149': 0.1382,
'Sm150': 0.0737, 'Sm152': 0.2674, 'Sm154': 0.2274,
'Eu151': 0.4781, 'Eu153': 0.5219, 'Gd152': 0.002,
'Gd154': 0.0218, 'Gd155': 0.148, 'Gd156': 0.2047,
'Gd157': 0.1565, 'Gd158': 0.2484, 'Gd160': 0.2186,
'Tb159': 1.0, 'Dy156': 0.00056, 'Dy158': 0.00095,
'Dy160': 0.02329, 'Dy161': 0.18889, 'Dy162': 0.25475,
'Dy163': 0.24896, 'Dy164': 0.2826, 'Ho165': 1.0,
'Er162': 0.00139, 'Er164': 0.01601, 'Er166': 0.33503,
'Er167': 0.22869, 'Er168': 0.26978, 'Er170': 0.1491,
'Tm169': 1.0, 'Yb168': 0.00123, 'Yb170': 0.02982,
'Yb171': 0.14086, 'Yb172': 0.21686, 'Yb173': 0.16103,
'Yb174': 0.32025, 'Yb176': 0.12995, 'Lu175': 0.97401,
'Lu176': 0.02599, 'Hf174': 0.0016, 'Hf176': 0.0526,
'Hf177': 0.186, 'Hf178': 0.2728, 'Hf179': 0.1362,
'Hf180': 0.3508, 'Ta180': 0.0001201, 'Ta181': 0.9998799,
'W180': 0.0012, 'W182': 0.265, 'W183': 0.1431,
'W184': 0.3064, 'W186': 0.2843, 'Re185': 0.374,
'Re187': 0.626, 'Os184': 0.0002, 'Os186': 0.0159,
'Os187': 0.0196, 'Os188': 0.1324, 'Os189': 0.1615,
'Os190': 0.2626, 'Os192': 0.4078, 'Ir191': 0.373,
'Ir193': 0.627, 'Pt190': 0.00012, 'Pt192': 0.00782,
'Pt194': 0.32864, 'Pt195': 0.33775, 'Pt196': 0.25211,
'Pt198': 0.07356, 'Au197': 1.0, 'Hg196': 0.0015,
'Hg198': 0.1004, 'Hg199': 0.1694, 'Hg200': 0.2314,
'Hg201': 0.1317, 'Hg202': 0.2974, 'Hg204': 0.0682,
'Tl203': 0.29524, 'Tl205': 0.70476, 'Pb204': 0.014,
'Pb206': 0.241, 'Pb207': 0.221, 'Pb208': 0.524,
'Bi209': 1.0, 'Th230': 0.0002, 'Th232': 0.9998,
'Pa231': 1.0, 'U234': 0.000054, 'U235': 0.007204,
'U238': 0.992742
}
ATOMIC_SYMBOL = {0: 'n', 1: 'H', 2: 'He', 3: 'Li', 4: 'Be', 5: 'B', 6: 'C',
7: 'N', 8: 'O', 9: 'F', 10: 'Ne', 11: 'Na', 12: 'Mg', 13: 'Al',
14: 'Si', 15: 'P', 16: 'S', 17: 'Cl', 18: 'Ar', 19: 'K',
20: 'Ca', 21: 'Sc', 22: 'Ti', 23: 'V', 24: 'Cr', 25: 'Mn',
26: 'Fe', 27: 'Co', 28: 'Ni', 29: 'Cu', 30: 'Zn', 31: 'Ga',
32: 'Ge', 33: 'As', 34: 'Se', 35: 'Br', 36: 'Kr', 37: 'Rb',
38: 'Sr', 39: 'Y', 40: 'Zr', 41: 'Nb', 42: 'Mo', 43: 'Tc',
44: 'Ru', 45: 'Rh', 46: 'Pd', 47: 'Ag', 48: 'Cd', 49: 'In',
50: 'Sn', 51: 'Sb', 52: 'Te', 53: 'I', 54: 'Xe', 55: 'Cs',
56: 'Ba', 57: 'La', 58: 'Ce', 59: 'Pr', 60: 'Nd', 61: 'Pm',
62: 'Sm', 63: 'Eu', 64: 'Gd', 65: 'Tb', 66: 'Dy', 67: 'Ho',
68: 'Er', 69: 'Tm', 70: 'Yb', 71: 'Lu', 72: 'Hf', 73: 'Ta',
74: 'W', 75: 'Re', 76: 'Os', 77: 'Ir', 78: 'Pt', 79: 'Au',
80: 'Hg', 81: 'Tl', 82: 'Pb', 83: 'Bi', 84: 'Po', 85: 'At',
86: 'Rn', 87: 'Fr', 88: 'Ra', 89: 'Ac', 90: 'Th', 91: 'Pa',
92: 'U', 93: 'Np', 94: 'Pu', 95: 'Am', 96: 'Cm', 97: 'Bk',
98: 'Cf', 99: 'Es', 100: 'Fm', 101: 'Md', 102: 'No',
103: 'Lr', 104: 'Rf', 105: 'Db', 106: 'Sg', 107: 'Bh',
108: 'Hs', 109: 'Mt', 110: 'Ds', 111: 'Rg', 112: 'Cn',
113: 'Nh', 114: 'Fl', 115: 'Mc', 116: 'Lv', 117: 'Ts',
118: 'Og'}
ATOMIC_NUMBER = {value: key for key, value in ATOMIC_SYMBOL.items()}
_ATOMIC_MASS = {}
_GND_NAME_RE = re.compile(r'([A-Zn][a-z]*)(\d+)((?:_[em]\d+)?)')
def atomic_mass(isotope):
"""Return atomic mass of isotope in atomic mass units.
Atomic mass data comes from the `Atomic Mass Evaluation 2016
<https://www-nds.iaea.org/amdc/ame2016/AME2016-a.pdf>`_.
Parameters
----------
isotope : str
Name of isotope, e.g., 'Pu239'
Returns
-------
float
Atomic mass of isotope in [amu]
"""
if not _ATOMIC_MASS:
# Load data from AME2016 file
mass_file = os.path.join(os.path.dirname(__file__), 'mass16.txt')
with open(mass_file, 'r') as ame:
# Read lines in file starting at line 40
for line in itertools.islice(ame, 39, None):
name = '{}{}'.format(line[20:22].strip(), int(line[16:19]))
mass = float(line[96:99]) + 1e-6*float(
line[100:106] + '.' + line[107:112])
_ATOMIC_MASS[name.lower()] = mass
# For isotopes found in some libraries that represent all natural
# isotopes of their element (e.g. C0), calculate the atomic mass as
# the sum of the atomic mass times the natural abudance of the isotopes
# that make up the element.
for element in ['C', 'Zn', 'Pt', 'Os', 'Tl']:
isotope_zero = element.lower() + '0'
_ATOMIC_MASS[isotope_zero] = 0.
for iso, abundance in NATURAL_ABUNDANCE.items():
if re.match(r'{}\d+'.format(element), iso):
_ATOMIC_MASS[isotope_zero] += abundance * \
_ATOMIC_MASS[iso.lower()]
# Get rid of metastable information
if '_' in isotope:
isotope = isotope[:isotope.find('_')]
return _ATOMIC_MASS[isotope.lower()]
def atomic_weight(element):
"""Return atomic weight of an element in atomic mass units.
Computes an average of the atomic mass of each of element's naturally
occurring isotopes weighted by their relative abundance.
Parameters
----------
element : str
Name of element, e.g. 'H', 'U'
Returns
-------
float
Atomic weight of element in [amu]
"""
weight = 0.
for nuclide, abundance in NATURAL_ABUNDANCE.items():
if re.match(r'{}\d+'.format(element), nuclide):
weight += atomic_mass(nuclide) * abundance
if weight > 0.:
return weight
else:
raise ValueError("No naturally-occurring isotopes for element '{}'."
.format(element))
def water_density(temperature, pressure=0.1013):
"""Return the density of liquid water at a given temperature and pressure.
The density is calculated from a polynomial fit using equations and values
from the 2012 version of the IAPWS-IF97 formulation. Only the equations
for region 1 are implemented here. Region 1 is limited to liquid water
below 100 [MPa] with a temperature above 273.15 [K], below 623.15 [K], and
below saturation.
Reference: International Association for the Properties of Water and Steam,
"Revised Release on the IAPWS Industrial Formulation 1997 for the
Thermodynamic Properties of Water and Steam", IAPWS R7-97(2012).
Parameters
----------
temperature : float
Water temperature in units of [K]
pressure : float
Water pressure in units of [MPa]
Returns
-------
float
Water density in units of [g/cm^3]
"""
# Make sure the temperature and pressure are inside the min/max region 1
# bounds. (Relax the 273.15 bound to 273 in case a user wants 0 deg C data
# but they only use 3 digits for their conversion to K.)
if pressure > 100.0:
warn("Results are not valid for pressures above 100 MPa.")
if pressure < 0.0:
warn("Results are not valid for pressures below zero.")
if temperature < 273:
warn("Results are not valid for temperatures below 273.15 K.")
if temperature > 623.15:
warn("Results are not valid for temperatures above 623.15 K.")
# IAPWS region 4 parameters
n4 = [0.11670521452767e4, -0.72421316703206e6, -0.17073846940092e2,
0.12020824702470e5, -0.32325550322333e7, 0.14915108613530e2,
-0.48232657361591e4, 0.40511340542057e6, -0.23855557567849,
0.65017534844798e3]
# Compute the saturation temperature at the given pressure.
beta = pressure**(0.25)
E = beta**2 + n4[2] * beta + n4[5]
F = n4[0] * beta**2 + n4[3] * beta + n4[6]
G = n4[1] * beta**2 + n4[4] * beta + n4[7]
D = 2.0 * G / (-F - sqrt(F**2 - 4 * E * G))
T_sat = 0.5 * (n4[9] + D
- sqrt((n4[9] + D)**2 - 4.0 * (n4[8] + n4[9] * D)))
# Make sure we aren't above saturation. (Relax this bound by .2 degrees
# for deg C to K conversions.)
if temperature > T_sat + 0.2:
warn("Results are not valid for temperatures above saturation "
"(above the boiling point).")
# IAPWS region 1 parameters
R_GAS_CONSTANT = 0.461526 # kJ / kg / K
ref_p = 16.53 # MPa
ref_T = 1386 # K
n1f = [0.14632971213167, -0.84548187169114, -0.37563603672040e1,
0.33855169168385e1, -0.95791963387872, 0.15772038513228,
-0.16616417199501e-1, 0.81214629983568e-3, 0.28319080123804e-3,
-0.60706301565874e-3, -0.18990068218419e-1, -0.32529748770505e-1,
-0.21841717175414e-1, -0.52838357969930e-4, -0.47184321073267e-3,
-0.30001780793026e-3, 0.47661393906987e-4, -0.44141845330846e-5,
-0.72694996297594e-15, -0.31679644845054e-4, -0.28270797985312e-5,
-0.85205128120103e-9, -0.22425281908000e-5, -0.65171222895601e-6,
-0.14341729937924e-12, -0.40516996860117e-6, -0.12734301741641e-8,
-0.17424871230634e-9, -0.68762131295531e-18, 0.14478307828521e-19,
0.26335781662795e-22, -0.11947622640071e-22, 0.18228094581404e-23,
-0.93537087292458e-25]
I1f = [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 4,
4, 4, 5, 8, 8, 21, 23, 29, 30, 31, 32]
J1f = [-2, -1, 0, 1, 2, 3, 4, 5, -9, -7, -1, 0, 1, 3, -3, 0, 1, 3, 17, -4,
0, 6, -5, -2, 10, -8, -11, -6, -29, -31, -38, -39, -40, -41]
# Nondimensionalize the pressure and temperature.
pi = pressure / ref_p
tau = ref_T / temperature
# Compute the derivative of gamma (dimensionless Gibbs free energy) with
# respect to pi.
gamma1_pi = 0.0
for n, I, J in zip(n1f, I1f, J1f):
gamma1_pi -= n * I * (7.1 - pi)**(I - 1) * (tau - 1.222)**J
# Compute the leading coefficient. This sets the units at
# 1 [MPa] * [kg K / kJ] * [1 / K]
# = 1e6 [N / m^2] * 1e-3 [kg K / N / m] * [1 / K]
# = 1e3 [kg / m^3]
# = 1 [g / cm^3]
coeff = pressure / R_GAS_CONSTANT / temperature
# Compute and return the density.
return coeff / pi / gamma1_pi
def gnd_name(Z, A, m=0):
"""Return nuclide name using GND convention
Parameters
----------
Z : int
Atomic number
A : int
Mass number
m : int, optional
Metastable state
Returns
-------
str
Nuclide name in GND convention, e.g., 'Am242_m1'
"""
if m > 0:
return '{}{}_m{}'.format(ATOMIC_SYMBOL[Z], A, m)
else:
return '{}{}'.format(ATOMIC_SYMBOL[Z], A)
def zam(name):
"""Return tuple of (atomic number, mass number, metastable state)
Parameters
----------
name : str
Name of nuclide using GND convention, e.g., 'Am242_m1'
Returns
-------
3-tuple of int
Atomic number, mass number, and metastable state
"""
try:
symbol, A, state = _GND_NAME_RE.match(name).groups()
except AttributeError:
raise ValueError("'{}' does not appear to be a nuclide name in GND "
"format".format(name))
if symbol not in ATOMIC_NUMBER:
raise ValueError("'{}' is not a recognized element symbol"
.format(symbol))
metastable = int(state[2:]) if state else 0
return (ATOMIC_NUMBER[symbol], int(A), metastable)
# Values here are from the Committee on Data for Science and Technology
# (CODATA) 2014 recommendation (doi:10.1103/RevModPhys.88.035009).
# The value of the Boltzman constant in units of eV / K
K_BOLTZMANN = 8.6173303e-5
# Unit conversions
EV_PER_MEV = 1.0e6
JOULE_PER_EV = 1.6021766208e-19
# Avogadro's constant
AVOGADRO = 6.022140857e23
# Neutron mass in units of amu
NEUTRON_MASS = 1.00866491588

484
openmc/data/decay.py Normal file
View file

@ -0,0 +1,484 @@
from collections import namedtuple
from collections.abc import Iterable
from io import StringIO
from math import log
from numbers import Real
import re
from warnings import warn
import numpy as np
from uncertainties import ufloat, unumpy, UFloat
import openmc.checkvalue as cv
from openmc.mixin import EqualityMixin
from .data import ATOMIC_SYMBOL, ATOMIC_NUMBER
from .endf import Evaluation, get_head_record, get_list_record, get_tab1_record
# Gives name and (change in A, change in Z) resulting from decay
_DECAY_MODES = {
0: ('gamma', (0, 0)),
1: ('beta-', (0, 1)),
2: ('ec/beta+', (0, -1)),
3: ('IT', (0, 0)),
4: ('alpha', (-4, -2)),
5: ('n', (-1, 0)),
6: ('sf', None),
7: ('p', (-1, -1)),
8: ('e-', (0, 0)),
9: ('xray', (0, 0)),
10: ('unknown', None)
}
_RADIATION_TYPES = {
0: 'gamma',
1: 'beta-',
2: 'ec/beta+',
4: 'alpha',
5: 'n',
6: 'sf',
7: 'p',
8: 'e-',
9: 'xray',
10: 'anti-neutrino',
11: 'neutrino'
}
def get_decay_modes(value):
"""Return sequence of decay modes given an ENDF RTYP value.
Parameters
----------
value : float
ENDF definition of sequence of decay modes
Returns
-------
list of str
List of successive decays, e.g. ('beta-', 'neutron')
"""
return [_DECAY_MODES[int(x)][0] for x in
str(value).strip('0').replace('.', '')]
class FissionProductYields(EqualityMixin):
"""Independent and cumulative fission product yields.
Parameters
----------
ev_or_filename : str of openmc.data.endf.Evaluation
ENDF fission product yield evaluation to read from. If given as a
string, it is assumed to be the filename for the ENDF file.
Attributes
----------
cumulative : list of dict
Cumulative yields for each tabulated energy. Each item in the list is a
dictionary whose keys are nuclide names and values are cumulative
yields. The i-th dictionary corresponds to the i-th incident neutron
energy.
energies : Iterable of float or None
Energies at which fission product yields are tabulated.
independent : list of dict
Independent yields for each tabulated energy. Each item in the list is a
dictionary whose keys are nuclide names and values are independent
yields. The i-th dictionary corresponds to the i-th incident neutron
energy.
nuclide : dict
Properties of the fissioning nuclide.
Notes
-----
Neutron fission yields are typically not measured with a monoenergetic
source of neutrons. As such, if the fission yields are given at, e.g.,
0.0253 eV, one should interpret this as meaning that they are derived from a
typical thermal reactor flux spectrum as opposed to a monoenergetic source
at 0.0253 eV.
"""
def __init__(self, ev_or_filename):
# Define function that can be used to read both independent and
# cumulative yields
def get_yields(file_obj):
# Determine number of energies
n_energy = get_head_record(file_obj)[2]
energies = np.zeros(n_energy)
data = []
for i in range(n_energy):
# Determine i-th energy and number of products
items, values = get_list_record(file_obj)
energies[i] = items[0]
n_products = items[5]
# Get yields for i-th energy
yields = {}
for j in range(n_products):
Z, A = divmod(int(values[4*j]), 1000)
isomeric_state = int(values[4*j + 1])
name = ATOMIC_SYMBOL[Z] + str(A)
if isomeric_state > 0:
name += '_m{}'.format(isomeric_state)
yield_j = ufloat(values[4*j + 2], values[4*j + 3])
yields[name] = yield_j
data.append(yields)
return energies, data
# Get evaluation if str is passed
if isinstance(ev_or_filename, Evaluation):
ev = ev_or_filename
else:
ev = Evaluation(ev_or_filename)
# Assign basic nuclide properties
self.nuclide = {
'name': ev.gnd_name,
'atomic_number': ev.target['atomic_number'],
'mass_number': ev.target['mass_number'],
'isomeric_state': ev.target['isomeric_state']
}
# Read independent yields
if (8, 454) in ev.section:
file_obj = StringIO(ev.section[8, 454])
self.energies, self.independent = get_yields(file_obj)
# Read cumulative yields
if (8, 459) in ev.section:
file_obj = StringIO(ev.section[8, 459])
energies, self.cumulative = get_yields(file_obj)
assert np.all(energies == self.energies)
@classmethod
def from_endf(cls, ev_or_filename):
"""Generate fission product yield data from an ENDF evaluation
Parameters
----------
ev_or_filename : str or openmc.data.endf.Evaluation
ENDF fission product yield evaluation to read from. If given as a
string, it is assumed to be the filename for the ENDF file.
Returns
-------
openmc.data.FissionProductYields
Fission product yield data
"""
return cls(ev_or_filename)
class DecayMode(EqualityMixin):
"""Radioactive decay mode.
Parameters
----------
parent : str
Parent decaying nuclide
modes : list of str
Successive decay modes
daughter_state : int
Metastable state of the daughter nuclide
energy : uncertainties.UFloat
Total decay energy in eV available in the decay process.
branching_ratio : uncertainties.UFloat
Fraction of the decay of the parent nuclide which proceeds by this mode.
Attributes
----------
branching_ratio : uncertainties.UFloat
Fraction of the decay of the parent nuclide which proceeds by this mode.
daughter : str
Name of daughter nuclide produced from decay
energy : uncertainties.UFloat
Total decay energy in eV available in the decay process.
modes : list of str
Successive decay modes
parent : str
Parent decaying nuclide
"""
def __init__(self, parent, modes, daughter_state, energy,
branching_ratio):
self._daughter_state = daughter_state
self.parent = parent
self.modes = modes
self.energy = energy
self.branching_ratio = branching_ratio
def __repr__(self):
return ('<DecayMode: ({}), {} -> {}, {}>'.format(
','.join(self.modes), self.parent, self.daughter,
self.branching_ratio))
@property
def branching_ratio(self):
return self._branching_ratio
@property
def daughter(self):
# Determine atomic number and mass number of parent
symbol, A = re.match(r'([A-Zn][a-z]*)(\d+)', self.parent).groups()
A = int(A)
Z = ATOMIC_NUMBER[symbol]
# Process changes
for mode in self.modes:
for name, changes in _DECAY_MODES.values():
if name == mode:
if changes is not None:
delta_A, delta_Z = changes
A += delta_A
Z += delta_Z
if self._daughter_state > 0:
return '{}{}_m{}'.format(ATOMIC_SYMBOL[Z], A, self._daughter_state)
else:
return '{}{}'.format(ATOMIC_SYMBOL[Z], A)
@property
def energy(self):
return self._energy
@property
def modes(self):
return self._modes
@property
def parent(self):
return self._parent
@branching_ratio.setter
def branching_ratio(self, branching_ratio):
cv.check_type('branching ratio', branching_ratio, UFloat)
cv.check_greater_than('branching ratio',
branching_ratio.nominal_value, 0.0, True)
if branching_ratio.nominal_value == 0.0:
warn('Decay mode {} of parent {} has a zero branching ratio.'
.format(self.modes, self.parent))
cv.check_greater_than('branching ratio uncertainty',
branching_ratio.std_dev, 0.0, True)
self._branching_ratio = branching_ratio
@energy.setter
def energy(self, energy):
cv.check_type('decay energy', energy, UFloat)
cv.check_greater_than('decay energy', energy.nominal_value, 0.0, True)
cv.check_greater_than('decay energy uncertainty',
energy.std_dev, 0.0, True)
self._energy = energy
@modes.setter
def modes(self, modes):
cv.check_type('decay modes', modes, Iterable, str)
self._modes = modes
@parent.setter
def parent(self, parent):
cv.check_type('parent nuclide', parent, str)
self._parent = parent
class Decay(EqualityMixin):
"""Radioactive decay data.
Parameters
----------
ev_or_filename : str of openmc.data.endf.Evaluation
ENDF radioactive decay data evaluation to read from. If given as a
string, it is assumed to be the filename for the ENDF file.
Attributes
----------
average_energies : dict
Average decay energies in eV of each type of radiation for decay heat
applications.
decay_constant : uncertainties.UFloat
Decay constant in inverse seconds.
half_life : uncertainties.UFloat
Half-life of the decay in seconds.
modes : list
Decay mode information for each mode of decay.
nuclide : dict
Dictionary describing decaying nuclide with keys 'name',
'excited_state', 'mass', 'stable', 'spin', and 'parity'.
spectra : dict
Resulting radiation spectra for each radiation type.
"""
def __init__(self, ev_or_filename):
# Get evaluation if str is passed
if isinstance(ev_or_filename, Evaluation):
ev = ev_or_filename
else:
ev = Evaluation(ev_or_filename)
file_obj = StringIO(ev.section[8, 457])
self.nuclide = {}
self.modes = []
self.spectra = {}
self.average_energies = {}
# Get head record
items = get_head_record(file_obj)
Z, A = divmod(items[0], 1000)
metastable = items[3]
self.nuclide['atomic_number'] = Z
self.nuclide['mass_number'] = A
self.nuclide['isomeric_state'] = metastable
if metastable > 0:
self.nuclide['name'] = '{}{}_m{}'.format(ATOMIC_SYMBOL[Z], A,
metastable)
else:
self.nuclide['name'] = '{}{}'.format(ATOMIC_SYMBOL[Z], A)
self.nuclide['mass'] = items[1] # AWR
self.nuclide['excited_state'] = items[2] # State of the original nuclide
self.nuclide['stable'] = (items[4] == 1) # Nucleus stability flag
# Determine if radioactive/stable
if not self.nuclide['stable']:
NSP = items[5] # Number of radiation types
# Half-life and decay energies
items, values = get_list_record(file_obj)
self.half_life = ufloat(items[0], items[1])
NC = items[4]//2
pairs = [x for x in zip(values[::2], values[1::2])]
ex = self.average_energies
ex['light'] = ufloat(*pairs[0])
ex['electromagnetic'] = ufloat(*pairs[1])
ex['heavy'] = ufloat(*pairs[2])
if NC == 17:
ex['beta-'] = ufloat(*pairs[3])
ex['beta+'] = ufloat(*pairs[4])
ex['auger'] = ufloat(*pairs[5])
ex['conversion'] = ufloat(*pairs[6])
ex['gamma'] = ufloat(*pairs[7])
ex['xray'] = ufloat(*pairs[8])
ex['Bremsstrahlung'] = ufloat(*pairs[9])
ex['annihilation'] = ufloat(*pairs[10])
ex['alpha'] = ufloat(*pairs[11])
ex['recoil'] = ufloat(*pairs[12])
ex['SF'] = ufloat(*pairs[13])
ex['neutron'] = ufloat(*pairs[14])
ex['proton'] = ufloat(*pairs[15])
ex['neutrino'] = ufloat(*pairs[16])
items, values = get_list_record(file_obj)
spin = items[0]
if spin == -77.777:
self.nuclide['spin'] = None
else:
self.nuclide['spin'] = spin
self.nuclide['parity'] = items[1] # Parity of the nuclide
# Decay mode information
n_modes = items[5] # Number of decay modes
for i in range(n_modes):
decay_type = get_decay_modes(values[6*i])
isomeric_state = int(values[6*i + 1])
energy = ufloat(*values[6*i + 2:6*i + 4])
branching_ratio = ufloat(*values[6*i + 4:6*(i + 1)])
mode = DecayMode(self.nuclide['name'], decay_type, isomeric_state,
energy, branching_ratio)
self.modes.append(mode)
discrete_type = {0.0: None, 1.0: 'allowed', 2.0: 'first-forbidden',
3.0: 'second-forbidden', 4.0: 'third-forbidden',
5.0: 'fourth-forbidden', 6.0: 'fifth-forbidden'}
# Read spectra
for i in range(NSP):
spectrum = {}
items, values = get_list_record(file_obj)
# Decay radiation type
spectrum['type'] = _RADIATION_TYPES[items[1]]
# Continuous spectrum flag
spectrum['continuous_flag'] = {0: 'discrete', 1: 'continuous',
2: 'both'}[items[2]]
spectrum['discrete_normalization'] = ufloat(*values[0:2])
spectrum['energy_average'] = ufloat(*values[2:4])
spectrum['continuous_normalization'] = ufloat(*values[4:6])
NER = items[5] # Number of tabulated discrete energies
if not spectrum['continuous_flag'] == 'continuous':
# Information about discrete spectrum
spectrum['discrete'] = []
for j in range(NER):
items, values = get_list_record(file_obj)
di = {}
di['energy'] = ufloat(*items[0:2])
di['from_mode'] = get_decay_modes(values[0])
di['type'] = discrete_type[values[1]]
di['intensity'] = ufloat(*values[2:4])
if spectrum['type'] == 'ec/beta+':
di['positron_intensity'] = ufloat(*values[4:6])
elif spectrum['type'] == 'gamma':
if len(values) >= 6:
di['internal_pair'] = ufloat(*values[4:6])
if len(values) >= 8:
di['total_internal_conversion'] = ufloat(*values[6:8])
if len(values) == 12:
di['k_shell_conversion'] = ufloat(*values[8:10])
di['l_shell_conversion'] = ufloat(*values[10:12])
spectrum['discrete'].append(di)
if not spectrum['continuous_flag'] == 'discrete':
# Read continuous spectrum
ci = {}
params, ci['probability'] = get_tab1_record(file_obj)
ci['type'] = get_decay_modes(params[0])
# Read covariance (Ek, Fk) table
LCOV = params[3]
if LCOV != 0:
items, values = get_list_record(file_obj)
ci['covariance_lb'] = items[3]
ci['covariance'] = zip(values[0::2], values[1::2])
spectrum['continuous'] = ci
# Add spectrum to dictionary
self.spectra[spectrum['type']] = spectrum
else:
items, values = get_list_record(file_obj)
items, values = get_list_record(file_obj)
self.nuclide['spin'] = items[0]
self.nuclide['parity'] = items[1]
self.half_life = ufloat(float('inf'), float('inf'))
@property
def decay_constant(self):
if hasattr(self.half_life, 'n'):
return log(2.)/self.half_life
else:
mu, sigma = self.half_life
return ufloat(log(2.)/mu, log(2.)/mu**2*sigma)
@classmethod
def from_endf(cls, ev_or_filename):
"""Generate radioactive decay data from an ENDF evaluation
Parameters
----------
ev_or_filename : str or openmc.data.endf.Evaluation
ENDF radioactive decay data evaluation to read from. If given as a
string, it is assumed to be the filename for the ENDF file.
Returns
-------
openmc.data.Decay
Radioactive decay data
"""
return cls(ev_or_filename)

Binary file not shown.

39
openmc/data/endf.c Normal file
View file

@ -0,0 +1,39 @@
#include <stdlib.h>
double cfloat_endf(const char* buffer, int n)
{
char arr[12]; // 11 characters plus a null terminator
int j = 0; // current position in arr
int found_significand = 0;
int found_exponent = 0;
for (int i = 0; i < n; ++i) {
// Skip whitespace characters
char c = buffer[i];
if (c == ' ') continue;
if (found_significand) {
if (!found_exponent) {
if (c == '+' || c == '-') {
// In the case that we encounter +/- and we haven't yet encountered
// e/E, we manually add it
arr[j++] = 'e';
found_exponent = 1;
} else if (c == 'e' || c == 'E' || c == 'd' || c == 'D') {
arr[j++] = 'e';
found_exponent = 1;
continue;
}
}
} else if (c == '.' || (c >= '0' && c <= '9')) {
found_significand = 1;
}
// Copy character
arr[j++] = c;
}
// Done copying. Add null terminator and convert to double
arr[j] = '\0';
return atof(arr);
}

550
openmc/data/endf.py Normal file
View file

@ -0,0 +1,550 @@
"""Module for parsing and manipulating data from ENDF evaluations.
All the classes and functions in this module are based on document
ENDF-102 titled "Data Formats and Procedures for the Evaluated Nuclear
Data File ENDF-6". The latest version from June 2009 can be found at
http://www-nds.iaea.org/ndspub/documents/endf/endf102/endf102.pdf
"""
import io
import re
import os
from math import pi
from pathlib import PurePath
from collections import OrderedDict
from collections.abc import Iterable
import numpy as np
from numpy.polynomial.polynomial import Polynomial
from .data import ATOMIC_SYMBOL, gnd_name
from .function import Tabulated1D, INTERPOLATION_SCHEME
from openmc.stats.univariate import Uniform, Tabular, Legendre
try:
from ._endf import float_endf
_CYTHON = True
except ImportError:
_CYTHON = False
_LIBRARY = {0: 'ENDF/B', 1: 'ENDF/A', 2: 'JEFF', 3: 'EFF',
4: 'ENDF/B High Energy', 5: 'CENDL', 6: 'JENDL',
17: 'TENDL', 18: 'ROSFOND', 21: 'SG-21', 31: 'INDL/V',
32: 'INDL/A', 33: 'FENDL', 34: 'IRDF', 35: 'BROND',
36: 'INGDB-90', 37: 'FENDL/A', 41: 'BROND'}
_SUBLIBRARY = {
0: 'Photo-nuclear data',
1: 'Photo-induced fission product yields',
3: 'Photo-atomic data',
4: 'Radioactive decay data',
5: 'Spontaneous fission product yields',
6: 'Atomic relaxation data',
10: 'Incident-neutron data',
11: 'Neutron-induced fission product yields',
12: 'Thermal neutron scattering data',
19: 'Neutron standards',
113: 'Electro-atomic data',
10010: 'Incident-proton data',
10011: 'Proton-induced fission product yields',
10020: 'Incident-deuteron data',
10030: 'Incident-triton data',
20030: 'Incident-helion (3He) data',
20040: 'Incident-alpha data'
}
SUM_RULES = {1: [2, 3],
3: [4, 5, 11, 16, 17, 22, 23, 24, 25, 27, 28, 29, 30, 32, 33, 34, 35,
36, 37, 41, 42, 44, 45, 152, 153, 154, 156, 157, 158, 159, 160,
161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172,
173, 174, 175, 176, 177, 178, 179, 180, 181, 183, 184, 185,
186, 187, 188, 189, 190, 194, 195, 196, 198, 199, 200],
4: list(range(50, 92)),
16: list(range(875, 892)),
18: [19, 20, 21, 38],
27: [18, 101],
101: [102, 103, 104, 105, 106, 107, 108, 109, 111, 112, 113, 114,
115, 116, 117, 155, 182, 191, 192, 193, 197],
103: list(range(600, 650)),
104: list(range(650, 700)),
105: list(range(700, 750)),
106: list(range(750, 800)),
107: list(range(800, 850))}
ENDF_FLOAT_RE = re.compile(r'([\s\-\+]?\d*\.\d+)([\+\-]) ?(\d+)')
def py_float_endf(s):
"""Convert string of floating point number in ENDF to float.
The ENDF-6 format uses an 'e-less' floating point number format,
e.g. -1.23481+10. Trying to convert using the float built-in won't work
because of the lack of an 'e'. This function allows such strings to be
converted while still allowing numbers that are not in exponential notation
to be converted as well.
Parameters
----------
s : str
Floating-point number from an ENDF file
Returns
-------
float
The number
"""
return float(ENDF_FLOAT_RE.sub(r'\1e\2\3', s))
if not _CYTHON:
float_endf = py_float_endf
def int_endf(s):
"""Convert string of integer number in ENDF to int.
The ENDF-6 format technically allows integers to be represented by a field
of all blanks. This function acts like int(s) except when s is a string of
all whitespace, in which case zero is returned.
Parameters
----------
s : str
Integer or spaces
Returns
-------
integer
The number or 0
"""
return 0 if s.isspace() else int(s)
def get_text_record(file_obj):
"""Return data from a TEXT record in an ENDF-6 file.
Parameters
----------
file_obj : file-like object
ENDF-6 file to read from
Returns
-------
str
Text within the TEXT record
"""
return file_obj.readline()[:66]
def get_cont_record(file_obj, skip_c=False):
"""Return data from a CONT record in an ENDF-6 file.
Parameters
----------
file_obj : file-like object
ENDF-6 file to read from
skip_c : bool
Determine whether to skip the first two quantities (C1, C2) of the CONT
record.
Returns
-------
tuple
The six items within the CONT record
"""
line = file_obj.readline()
if skip_c:
C1 = None
C2 = None
else:
C1 = float_endf(line[:11])
C2 = float_endf(line[11:22])
L1 = int_endf(line[22:33])
L2 = int_endf(line[33:44])
N1 = int_endf(line[44:55])
N2 = int_endf(line[55:66])
return (C1, C2, L1, L2, N1, N2)
def get_head_record(file_obj):
"""Return data from a HEAD record in an ENDF-6 file.
Parameters
----------
file_obj : file-like object
ENDF-6 file to read from
Returns
-------
tuple
The six items within the HEAD record
"""
line = file_obj.readline()
ZA = int(float_endf(line[:11]))
AWR = float_endf(line[11:22])
L1 = int_endf(line[22:33])
L2 = int_endf(line[33:44])
N1 = int_endf(line[44:55])
N2 = int_endf(line[55:66])
return (ZA, AWR, L1, L2, N1, N2)
def get_list_record(file_obj):
"""Return data from a LIST record in an ENDF-6 file.
Parameters
----------
file_obj : file-like object
ENDF-6 file to read from
Returns
-------
list
The six items within the header
list
The values within the list
"""
# determine how many items are in list
items = get_cont_record(file_obj)
NPL = items[4]
# read items
b = []
for i in range((NPL - 1)//6 + 1):
line = file_obj.readline()
n = min(6, NPL - 6*i)
for j in range(n):
b.append(float_endf(line[11*j:11*(j + 1)]))
return (items, b)
def get_tab1_record(file_obj):
"""Return data from a TAB1 record in an ENDF-6 file.
Parameters
----------
file_obj : file-like object
ENDF-6 file to read from
Returns
-------
list
The six items within the header
openmc.data.Tabulated1D
The tabulated function
"""
# Determine how many interpolation regions and total points there are
line = file_obj.readline()
C1 = float_endf(line[:11])
C2 = float_endf(line[11:22])
L1 = int_endf(line[22:33])
L2 = int_endf(line[33:44])
n_regions = int_endf(line[44:55])
n_pairs = int_endf(line[55:66])
params = [C1, C2, L1, L2]
# Read the interpolation region data, namely NBT and INT
breakpoints = np.zeros(n_regions, dtype=int)
interpolation = np.zeros(n_regions, dtype=int)
m = 0
for i in range((n_regions - 1)//3 + 1):
line = file_obj.readline()
to_read = min(3, n_regions - m)
for j in range(to_read):
breakpoints[m] = int_endf(line[0:11])
interpolation[m] = int_endf(line[11:22])
line = line[22:]
m += 1
# Read tabulated pairs x(n) and y(n)
x = np.zeros(n_pairs)
y = np.zeros(n_pairs)
m = 0
for i in range((n_pairs - 1)//3 + 1):
line = file_obj.readline()
to_read = min(3, n_pairs - m)
for j in range(to_read):
x[m] = float_endf(line[:11])
y[m] = float_endf(line[11:22])
line = line[22:]
m += 1
return params, Tabulated1D(x, y, breakpoints, interpolation)
def get_tab2_record(file_obj):
# Determine how many interpolation regions and total points there are
params = get_cont_record(file_obj)
n_regions = params[4]
# Read the interpolation region data, namely NBT and INT
breakpoints = np.zeros(n_regions, dtype=int)
interpolation = np.zeros(n_regions, dtype=int)
m = 0
for i in range((n_regions - 1)//3 + 1):
line = file_obj.readline()
to_read = min(3, n_regions - m)
for j in range(to_read):
breakpoints[m] = int(line[0:11])
interpolation[m] = int(line[11:22])
line = line[22:]
m += 1
return params, Tabulated2D(breakpoints, interpolation)
def get_intg_record(file_obj):
"""
Return data from an INTG record in an ENDF-6 file. Used to store the
covariance matrix in a compact format.
Parameters
----------
file_obj : file-like object
ENDF-6 file to read from
Returns
-------
numpy.ndarray
The correlation matrix described in the INTG record
"""
# determine how many items are in list and NDIGIT
items = get_cont_record(file_obj)
ndigit = items[2]
npar = items[3] # Number of parameters
nlines = items[4] # Lines to read
NROW_RULES = {2: 18, 3: 12, 4: 11, 5: 9, 6: 8}
nrow = NROW_RULES[ndigit]
# read lines and build correlation matrix
corr = np.identity(npar)
for i in range(nlines):
line = file_obj.readline()
ii = int_endf(line[:5]) - 1 # -1 to account for 0 indexing
jj = int_endf(line[5:10]) - 1
factor = 10**ndigit
for j in range(nrow):
if jj+j >= ii:
break
element = int_endf(line[11+(ndigit+1)*j:11+(ndigit+1)*(j+1)])
if element > 0:
corr[ii, jj] = (element+0.5)/factor
elif element < 0:
corr[ii, jj] = (element-0.5)/factor
# Symmetrize the correlation matrix
corr = corr + corr.T - np.diag(corr.diagonal())
return corr
def get_evaluations(filename):
"""Return a list of all evaluations within an ENDF file.
Parameters
----------
filename : str
Path to ENDF-6 formatted file
Returns
-------
list
A list of :class:`openmc.data.endf.Evaluation` instances.
"""
evaluations = []
with open(str(filename), 'r') as fh:
while True:
pos = fh.tell()
line = fh.readline()
if line[66:70] == ' -1':
break
fh.seek(pos)
evaluations.append(Evaluation(fh))
return evaluations
class Evaluation(object):
"""ENDF material evaluation with multiple files/sections
Parameters
----------
filename_or_obj : str or file-like
Path to ENDF file to read or an open file positioned at the start of an
ENDF material
Attributes
----------
info : dict
Miscellaneous information about the evaluation.
target : dict
Information about the target material, such as its mass, isomeric state,
whether it's stable, and whether it's fissionable.
projectile : dict
Information about the projectile such as its mass.
reaction_list : list of 4-tuples
List of sections in the evaluation. The entries of the tuples are the
file (MF), section (MT), number of records (NC), and modification
indicator (MOD).
"""
def __init__(self, filename_or_obj):
if isinstance(filename_or_obj, (str, PurePath)):
fh = open(str(filename_or_obj), 'r')
else:
fh = filename_or_obj
self.section = {}
self.info = {}
self.target = {}
self.projectile = {}
self.reaction_list = []
# Skip TPID record. Evaluators sometimes put in TPID records that are
# ill-formated because they lack MF/MT values or put them in the wrong
# columns.
if fh.tell() == 0:
fh.readline()
MF = 0
# Determine MAT number for this evaluation
while MF == 0:
position = fh.tell()
line = fh.readline()
MF = int(line[70:72])
self.material = int(line[66:70])
fh.seek(position)
while True:
# Find next section
while True:
position = fh.tell()
line = fh.readline()
MAT = int(line[66:70])
MF = int(line[70:72])
MT = int(line[72:75])
if MT > 0 or MAT == 0:
fh.seek(position)
break
# If end of material reached, exit loop
if MAT == 0:
fh.readline()
break
section_data = ''
while True:
line = fh.readline()
if line[72:75] == ' 0':
break
else:
section_data += line
self.section[MF, MT] = section_data
self._read_header()
def __repr__(self):
if 'zsymam' in self.target:
name = self.target['zsymam'].replace(' ', '')
else:
name = 'Unknown'
return '<{} for {} {}>'.format(self.info['sublibrary'], name,
self.info['library'])
def _read_header(self):
file_obj = io.StringIO(self.section[1, 451])
# Information about target/projectile
items = get_head_record(file_obj)
Z, A = divmod(items[0], 1000)
self.target['atomic_number'] = Z
self.target['mass_number'] = A
self.target['mass'] = items[1]
self._LRP = items[2]
self.target['fissionable'] = (items[3] == 1)
try:
library = _LIBRARY[items[4]]
except KeyError:
library = 'Unknown'
self.info['modification'] = items[5]
# Control record 1
items = get_cont_record(file_obj)
self.target['excitation_energy'] = items[0]
self.target['stable'] = (int(items[1]) == 0)
self.target['state'] = items[2]
self.target['isomeric_state'] = m = items[3]
self.info['format'] = items[5]
assert self.info['format'] == 6
# Set correct excited state for Am242_m1, which is wrong in ENDF/B-VII.1
if Z == 95 and A == 242 and m == 1:
self.target['state'] = 2
# Control record 2
items = get_cont_record(file_obj)
self.projectile['mass'] = items[0]
self.info['energy_max'] = items[1]
library_release = items[2]
self.info['sublibrary'] = _SUBLIBRARY[items[4]]
library_version = items[5]
self.info['library'] = (library, library_version, library_release)
# Control record 3
items = get_cont_record(file_obj)
self.target['temperature'] = items[0]
self.info['derived'] = (items[2] > 0)
NWD = items[4]
NXC = items[5]
# Text records
text = [get_text_record(file_obj) for i in range(NWD)]
if len(text) >= 5:
self.target['zsymam'] = text[0][0:11]
self.info['laboratory'] = text[0][11:22]
self.info['date'] = text[0][22:32]
self.info['author'] = text[0][32:66]
self.info['reference'] = text[1][1:22]
self.info['date_distribution'] = text[1][22:32]
self.info['date_release'] = text[1][33:43]
self.info['date_entry'] = text[1][55:63]
self.info['identifier'] = text[2:5]
self.info['description'] = text[5:]
# File numbers, reaction designations, and number of records
for i in range(NXC):
_, _, mf, mt, nc, mod = get_cont_record(file_obj, skip_c=True)
self.reaction_list.append((mf, mt, nc, mod))
@property
def gnd_name(self):
return gnd_name(self.target['atomic_number'],
self.target['mass_number'],
self.target['isomeric_state'])
class Tabulated2D(object):
"""Metadata for a two-dimensional function.
This is a dummy class that is not really used other than to store the
interpolation information for a two-dimensional function. Once we refactor
to adopt GND-like data containers, this will probably be removed or
extended.
Parameters
----------
breakpoints : Iterable of int
Breakpoints for interpolation regions
interpolation : Iterable of int
Interpolation scheme identification number, e.g., 3 means y is linear in
ln(x).
"""
def __init__(self, breakpoints, interpolation):
self.breakpoints = breakpoints
self.interpolation = interpolation

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,380 @@
from collections.abc import Callable
from copy import deepcopy
from io import StringIO
import sys
import h5py
import numpy as np
from .data import EV_PER_MEV
from .endf import get_cont_record, get_list_record, get_tab1_record, Evaluation
from .function import Function1D, Tabulated1D, Polynomial, sum_functions
import openmc.checkvalue as cv
from openmc.mixin import EqualityMixin
_NAMES = (
'fragments', 'prompt_neutrons', 'delayed_neutrons',
'prompt_photons', 'delayed_photons', 'betas',
'neutrinos', 'recoverable', 'total'
)
class FissionEnergyRelease(EqualityMixin):
"""Energy relased by fission reactions.
Energy is carried away from fission reactions by many different particles.
The attributes of this class specify how much energy is released in the form
of fission fragments, neutrons, photons, etc. Each component is also (in
general) a function of the incident neutron energy.
Following a fission reaction, most of the energy release is carried by the
daughter nuclei fragments. These fragments accelerate apart from the
Coulomb force on the time scale of ~10^-20 s [1]. Those fragments emit
prompt neutrons between ~10^-18 and ~10^-13 s after scission (although some
prompt neutrons may come directly from the scission point) [1]. Prompt
photons follow with a time scale of ~10^-14 to ~10^-7 s [1]. The fission
products then emit delayed neutrons with half lives between 0.1 and 100 s.
The remaining fission energy comes from beta decays of the fission products
which release beta particles, photons, and neutrinos (that escape the
reactor and do not produce usable heat).
Use the class methods to instantiate this class from an HDF5 or ENDF
dataset. The :meth:`FissionEnergyRelease.from_hdf5` method builds this
class from the usual OpenMC HDF5 data files.
:meth:`FissionEnergyRelease.from_endf` uses ENDF-formatted data.
References
----------
[1] D. G. Madland, "Total prompt energy release in the neutron-induced
fission of ^235U, ^238U, and ^239Pu", Nuclear Physics A 772:113--137 (2006).
<http://dx.doi.org/10.1016/j.nuclphysa.2006.03.013>
Attributes
----------
fragments : Callable
Function that accepts incident neutron energy value(s) and returns the
kinetic energy of the fission daughter nuclides (after prompt neutron
emission).
prompt_neutrons : Callable
Function of energy that returns the kinetic energy of prompt fission
neutrons.
delayed_neutrons : Callable
Function of energy that returns the kinetic energy of delayed neutrons
emitted from fission products.
prompt_photons : Callable
Function of energy that returns the kinetic energy of prompt fission
photons.
delayed_photons : Callable
Function of energy that returns the kinetic energy of delayed photons.
betas : Callable
Function of energy that returns the kinetic energy of delayed beta
particles.
neutrinos : Callable
Function of energy that returns the kinetic energy of neutrinos.
recoverable : Callable
Function of energy that returns the kinetic energy of all products that
can be absorbed in the reactor (all of the energy except for the
neutrinos).
total : Callable
Function of energy that returns the kinetic energy of all products.
q_prompt : Callable
Function of energy that returns the prompt fission Q-value (fragments +
prompt neutrons + prompt photons - incident neutron energy).
q_recoverable : Callable
Function of energy that returns the recoverable fission Q-value
(total release - neutrinos - incident neutron energy). This value is
sometimes referred to as the pseudo-Q-value.
q_total : Callable
Function of energy that returns the total fission Q-value (total release
- incident neutron energy).
"""
def __init__(self, fragments, prompt_neutrons, delayed_neutrons,
prompt_photons, delayed_photons, betas, neutrinos):
self.fragments = fragments
self.prompt_neutrons = prompt_neutrons
self.delayed_neutrons = delayed_neutrons
self.prompt_photons = prompt_photons
self.delayed_photons = delayed_photons
self.betas = betas
self.neutrinos = neutrinos
@property
def fragments(self):
return self._fragments
@property
def prompt_neutrons(self):
return self._prompt_neutrons
@property
def delayed_neutrons(self):
return self._delayed_neutrons
@property
def prompt_photons(self):
return self._prompt_photons
@property
def delayed_photons(self):
return self._delayed_photons
@property
def betas(self):
return self._betas
@property
def neutrinos(self):
return self._neutrinos
@property
def recoverable(self):
components = ['fragments', 'prompt_neutrons', 'delayed_neutrons',
'prompt_photons', 'delayed_photons', 'betas']
return sum_functions(getattr(self, c) for c in components)
@property
def total(self):
components = ['fragments', 'prompt_neutrons', 'delayed_neutrons',
'prompt_photons', 'delayed_photons', 'betas',
'neutrinos']
return sum_functions(getattr(self, c) for c in components)
@property
def q_prompt(self):
# Use a polynomial to subtract incident energy.
funcs = [self.fragments, self.prompt_neutrons, self.prompt_photons,
Polynomial((0.0, -1.0))]
return sum_functions(funcs)
@property
def q_recoverable(self):
# Use a polynomial to subtract incident energy.
return sum_functions([self.recoverable, Polynomial((0.0, -1.0))])
@property
def q_total(self):
# Use a polynomial to subtract incident energy.
return sum_functions([self.total, Polynomial((0.0, -1.0))])
@fragments.setter
def fragments(self, energy_release):
cv.check_type('fragments', energy_release, Callable)
self._fragments = energy_release
@prompt_neutrons.setter
def prompt_neutrons(self, energy_release):
cv.check_type('prompt_neutrons', energy_release, Callable)
self._prompt_neutrons = energy_release
@delayed_neutrons.setter
def delayed_neutrons(self, energy_release):
cv.check_type('delayed_neutrons', energy_release, Callable)
self._delayed_neutrons = energy_release
@prompt_photons.setter
def prompt_photons(self, energy_release):
cv.check_type('prompt_photons', energy_release, Callable)
self._prompt_photons = energy_release
@delayed_photons.setter
def delayed_photons(self, energy_release):
cv.check_type('delayed_photons', energy_release, Callable)
self._delayed_photons = energy_release
@betas.setter
def betas(self, energy_release):
cv.check_type('betas', energy_release, Callable)
self._betas = energy_release
@neutrinos.setter
def neutrinos(self, energy_release):
cv.check_type('neutrinos', energy_release, Callable)
self._neutrinos = energy_release
@classmethod
def from_endf(cls, ev, incident_neutron):
"""Generate fission energy release data from an ENDF file.
Parameters
----------
ev : openmc.data.endf.Evaluation
ENDF evaluation
incident_neutron : openmc.data.IncidentNeutron
Corresponding incident neutron dataset
Returns
-------
openmc.data.FissionEnergyRelease
Fission energy release data
"""
cv.check_type('evaluation', ev, Evaluation)
# Check to make sure this ENDF file matches the expected isomer.
if ev.target['atomic_number'] != incident_neutron.atomic_number:
raise ValueError('The atomic number of the ENDF evaluation does '
'not match the given IncidentNeutron.')
if ev.target['mass_number'] != incident_neutron.mass_number:
raise ValueError('The atomic mass of the ENDF evaluation does '
'not match the given IncidentNeutron.')
if ev.target['isomeric_state'] != incident_neutron.metastable:
raise ValueError('The metastable state of the ENDF evaluation '
'does not match the given IncidentNeutron.')
if not ev.target['fissionable']:
raise ValueError('The ENDF evaluation is not fissionable.')
if (1, 458) not in ev.section:
raise ValueError('ENDF evaluation does not have MF=1, MT=458.')
file_obj = StringIO(ev.section[1, 458])
# Read first record and check whether any components appear as
# tabulated functions
items = get_cont_record(file_obj)
lfc = items[3]
nfc = items[5]
# Parse the ENDF LIST into an array.
items, data = get_list_record(file_obj)
npoly = items[3]
# Associate each set of values and uncertainties with its label.
functions = {}
for i, name in enumerate(_NAMES):
coeffs = data[2*i::18]
# Ignore recoverable and total since we recalculate those directly
if name in ('recoverable', 'total'):
continue
# In ENDF/B-VII.1, data for 2nd-order coefficients were mistakenly
# not converted from MeV to eV. Check for this error and fix it if
# present.
if npoly == 2: # Only check 2nd-order data.
# If a 5 MeV neutron causes a change of more than 100 MeV, we
# know something is wrong.
second_order = coeffs[2]
if abs(second_order) * (5e6)**2 > 1e8:
# If we found the error, reduce 2nd-order coeff by 10**6.
coeffs[2] /= EV_PER_MEV
# If multiple coefficients were given, we can create the polynomial
# and move on to the next component
if npoly > 0:
functions[name] = Polynomial(coeffs)
continue
# If a single coefficient was given, we need to use the Sher-Beck
# formula for energy dependence
zeroth_order = coeffs[0]
if name in ('delayed_photons', 'betas'):
func = Polynomial((zeroth_order, -0.075))
elif name == 'neutrinos':
func = Polynomial((zeroth_order, -0.105))
elif name == 'prompt_neutrons':
# Prompt neutrons require nu-data. It is not clear from
# ENDF-102 whether prompt or total nu value should be used, but
# the delayed neutron fraction is so small that the difference
# is negligible. MT=18 (n, fission) might not be available so
# try MT=19 (n, f) as well.
if 18 in incident_neutron and not incident_neutron[18].redundant:
nu = [p.yield_ for p in incident_neutron[18].products
if p.particle == 'neutron'
and p.emission_mode in ('prompt', 'total')]
elif 19 in incident_neutron:
nu = [p.yield_ for p in incident_neutron[19].products
if p.particle == 'neutron'
and p.emission_mode in ('prompt', 'total')]
else:
raise ValueError('IncidentNeutron data has no fission '
'reaction.')
if len(nu) == 0:
raise ValueError(
'Nu data is needed to compute fission energy '
'release with the Sher-Beck format.'
)
if len(nu) > 1:
raise ValueError('Ambiguous prompt/total nu value.')
nu = nu[0]
if isinstance(nu, Tabulated1D):
# Evaluate Sher-Beck polynomial form at each tabulated value
func = deepcopy(nu)
func.y = (zeroth_order + 1.307*nu.x - 8.07e6*(nu.y - nu.y[0]))
elif isinstance(nu, Polynomial):
# Combine polynomials
if len(nu) == 1:
func = Polynomial([zeroth_order, 1.307])
else:
func = Polynomial(
[zeroth_order, 1.307 - 8.07e6*nu.coef[1]]
+ [-8.07e6*c for c in nu.coef[2:]])
else:
func = Polynomial(coeffs)
functions[name] = func
# Check for tabulated data
if lfc == 1:
for _ in range(nfc):
# Get tabulated function
items, eifc = get_tab1_record(file_obj)
# Determine which component it is
ifc = items[3]
name = _NAMES[ifc - 1]
# Replace value in dictionary
functions[name] = eifc
# Build the object
return cls(**functions)
@classmethod
def from_hdf5(cls, group):
"""Generate fission energy release data from an HDF5 group.
Parameters
----------
group : h5py.Group
HDF5 group to read from
Returns
-------
openmc.data.FissionEnergyRelease
Fission energy release data
"""
fragments = Function1D.from_hdf5(group['fragments'])
prompt_neutrons = Function1D.from_hdf5(group['prompt_neutrons'])
delayed_neutrons = Function1D.from_hdf5(group['delayed_neutrons'])
prompt_photons = Function1D.from_hdf5(group['prompt_photons'])
delayed_photons = Function1D.from_hdf5(group['delayed_photons'])
betas = Function1D.from_hdf5(group['betas'])
neutrinos = Function1D.from_hdf5(group['neutrinos'])
return cls(fragments, prompt_neutrons, delayed_neutrons, prompt_photons,
delayed_photons, betas, neutrinos)
def to_hdf5(self, group):
"""Write energy release data to an HDF5 group
Parameters
----------
group : h5py.Group
HDF5 group to write to
"""
self.fragments.to_hdf5(group, 'fragments')
self.prompt_neutrons.to_hdf5(group, 'prompt_neutrons')
self.delayed_neutrons.to_hdf5(group, 'delayed_neutrons')
self.prompt_photons.to_hdf5(group, 'prompt_photons')
self.delayed_photons.to_hdf5(group, 'delayed_photons')
self.betas.to_hdf5(group, 'betas')
self.neutrinos.to_hdf5(group, 'neutrinos')
self.q_prompt.to_hdf5(group, 'q_prompt')
self.q_recoverable.to_hdf5(group, 'q_recoverable')

716
openmc/data/function.py Normal file
View file

@ -0,0 +1,716 @@
from abc import ABCMeta, abstractmethod
from collections.abc import Iterable, Callable
from functools import reduce
from itertools import zip_longest
from numbers import Real, Integral
from math import exp, log
import numpy as np
import openmc.data
import openmc.checkvalue as cv
from openmc.mixin import EqualityMixin
from .data import EV_PER_MEV
INTERPOLATION_SCHEME = {1: 'histogram', 2: 'linear-linear', 3: 'linear-log',
4: 'log-linear', 5: 'log-log'}
def sum_functions(funcs):
"""Add tabulated/polynomials functions together
Parameters
----------
funcs : list of Function1D
Functions to add
Returns
-------
Function1D
Sum of polynomial/tabulated functions
"""
# Copy so we can iterate multiple times
funcs = list(funcs)
# Get x values for all tabulated components
xs = []
for f in funcs:
if isinstance(f, Tabulated1D):
xs.append(f.x)
if not np.all(f.interpolation == 2):
raise ValueError('Only linear-linear tabulated functions '
'can be combined')
if xs:
# Take the union of all energies (sorted)
x = reduce(np.union1d, xs)
# Evaluate each function and add together
y = sum(f(x) for f in funcs)
return Tabulated1D(x, y)
else:
# If no tabulated functions are present, we need to combine the
# polynomials by adding their coefficients
coeffs = [sum(x) for x in zip_longest(*funcs, fillvalue=0.0)]
return Polynomial(coeffs)
class Function1D(EqualityMixin, metaclass=ABCMeta):
"""A function of one independent variable with HDF5 support."""
@abstractmethod
def __call__(self): pass
@abstractmethod
def to_hdf5(self, group, name='xy'):
"""Write function to an HDF5 group
Parameters
----------
group : h5py.Group
HDF5 group to write to
name : str
Name of the dataset to create
"""
pass
@classmethod
def from_hdf5(cls, dataset):
"""Generate function from an HDF5 dataset
Parameters
----------
dataset : h5py.Dataset
Dataset to read from
Returns
-------
openmc.data.Function1D
Function read from dataset
"""
for subclass in cls.__subclasses__():
if dataset.attrs['type'].decode() == subclass.__name__:
return subclass.from_hdf5(dataset)
raise ValueError("Unrecognized Function1D class: '"
+ dataset.attrs['type'].decode() + "'")
class Tabulated1D(Function1D):
"""A one-dimensional tabulated function.
This class mirrors the TAB1 type from the ENDF-6 format. A tabulated
function is specified by tabulated (x,y) pairs along with interpolation
rules that determine the values between tabulated pairs.
Once an object has been created, it can be used as though it were an actual
function, e.g.:
>>> f = Tabulated1D([0, 10], [4, 5])
>>> [f(xi) for xi in numpy.linspace(0, 10, 5)]
[4.0, 4.25, 4.5, 4.75, 5.0]
Parameters
----------
x : Iterable of float
Independent variable
y : Iterable of float
Dependent variable
breakpoints : Iterable of int
Breakpoints for interpolation regions
interpolation : Iterable of int
Interpolation scheme identification number, e.g., 3 means y is linear in
ln(x).
Attributes
----------
x : Iterable of float
Independent variable
y : Iterable of float
Dependent variable
breakpoints : Iterable of int
Breakpoints for interpolation regions
interpolation : Iterable of int
Interpolation scheme identification number, e.g., 3 means y is linear in
ln(x).
n_regions : int
Number of interpolation regions
n_pairs : int
Number of tabulated (x,y) pairs
"""
def __init__(self, x, y, breakpoints=None, interpolation=None):
if breakpoints is None or interpolation is None:
# Single linear-linear interpolation region by default
self.breakpoints = np.array([len(x)])
self.interpolation = np.array([2])
else:
self.breakpoints = np.asarray(breakpoints, dtype=int)
self.interpolation = np.asarray(interpolation, dtype=int)
self.x = np.asarray(x)
self.y = np.asarray(y)
def __call__(self, x):
# Check if input is scalar
if not isinstance(x, Iterable):
return self._interpolate_scalar(x)
x = np.array(x)
# Create output array
y = np.zeros_like(x)
# Get indices for interpolation
idx = np.searchsorted(self.x, x, side='right') - 1
# Loop over interpolation regions
for k in range(len(self.breakpoints)):
# Get indices for the begining and ending of this region
i_begin = self.breakpoints[k-1] - 1 if k > 0 else 0
i_end = self.breakpoints[k] - 1
# Figure out which idx values lie within this region
contained = (idx >= i_begin) & (idx < i_end)
xk = x[contained] # x values in this region
xi = self.x[idx[contained]] # low edge of corresponding bins
xi1 = self.x[idx[contained] + 1] # high edge of corresponding bins
yi = self.y[idx[contained]]
yi1 = self.y[idx[contained] + 1]
if self.interpolation[k] == 1:
# Histogram
y[contained] = yi
elif self.interpolation[k] == 2:
# Linear-linear
y[contained] = yi + (xk - xi)/(xi1 - xi)*(yi1 - yi)
elif self.interpolation[k] == 3:
# Linear-log
y[contained] = yi + np.log(xk/xi)/np.log(xi1/xi)*(yi1 - yi)
elif self.interpolation[k] == 4:
# Log-linear
y[contained] = yi*np.exp((xk - xi)/(xi1 - xi)*np.log(yi1/yi))
elif self.interpolation[k] == 5:
# Log-log
y[contained] = (yi*np.exp(np.log(xk/xi)/np.log(xi1/xi)
*np.log(yi1/yi)))
# In some cases, x values might be outside the tabulated region due only
# to precision, so we check if they're close and set them equal if so.
y[np.isclose(x, self.x[0], atol=1e-14)] = self.y[0]
y[np.isclose(x, self.x[-1], atol=1e-14)] = self.y[-1]
return y
def _interpolate_scalar(self, x):
if x <= self._x[0]:
return self._y[0]
elif x >= self._x[-1]:
return self._y[-1]
# Get the index for interpolation
idx = np.searchsorted(self._x, x, side='right') - 1
# Loop over interpolation regions
for b, p in zip(self.breakpoints, self.interpolation):
if idx < b - 1:
break
xi = self._x[idx] # low edge of the corresponding bin
xi1 = self._x[idx + 1] # high edge of the corresponding bin
yi = self._y[idx]
yi1 = self._y[idx + 1]
if p == 1:
# Histogram
return yi
elif p == 2:
# Linear-linear
return yi + (x - xi)/(xi1 - xi)*(yi1 - yi)
elif p == 3:
# Linear-log
return yi + log(x/xi)/log(xi1/xi)*(yi1 - yi)
elif p == 4:
# Log-linear
return yi*exp((x - xi)/(xi1 - xi)*log(yi1/yi))
elif p == 5:
# Log-log
return yi*exp(log(x/xi)/log(xi1/xi)*log(yi1/yi))
def __len__(self):
return len(self.x)
@property
def x(self):
return self._x
@property
def y(self):
return self._y
@property
def breakpoints(self):
return self._breakpoints
@property
def interpolation(self):
return self._interpolation
@property
def n_pairs(self):
return len(self.x)
@property
def n_regions(self):
return len(self.breakpoints)
@x.setter
def x(self, x):
cv.check_type('x values', x, Iterable, Real)
self._x = x
@y.setter
def y(self, y):
cv.check_type('y values', y, Iterable, Real)
self._y = y
@breakpoints.setter
def breakpoints(self, breakpoints):
cv.check_type('breakpoints', breakpoints, Iterable, Integral)
self._breakpoints = breakpoints
@interpolation.setter
def interpolation(self, interpolation):
cv.check_type('interpolation', interpolation, Iterable, Integral)
self._interpolation = interpolation
def integral(self):
"""Integral of the tabulated function over its tabulated range.
Returns
-------
numpy.ndarray
Array of same length as the tabulated data that represents partial
integrals from the bottom of the range to each tabulated point.
"""
# Create output array
partial_sum = np.zeros(len(self.x) - 1)
i_low = 0
for k in range(len(self.breakpoints)):
# Determine which x values are within this interpolation range
i_high = self.breakpoints[k] - 1
# Get x values and bounding (x,y) pairs
x0 = self.x[i_low:i_high]
x1 = self.x[i_low + 1:i_high + 1]
y0 = self.y[i_low:i_high]
y1 = self.y[i_low + 1:i_high + 1]
if self.interpolation[k] == 1:
# Histogram
partial_sum[i_low:i_high] = y0*(x1 - x0)
elif self.interpolation[k] == 2:
# Linear-linear
m = (y1 - y0)/(x1 - x0)
partial_sum[i_low:i_high] = (y0 - m*x0)*(x1 - x0) + \
m*(x1**2 - x0**2)/2
elif self.interpolation[k] == 3:
# Linear-log
logx = np.log(x1/x0)
m = (y1 - y0)/logx
partial_sum[i_low:i_high] = y0 + m*(x1*(logx - 1) + x0)
elif self.interpolation[k] == 4:
# Log-linear
m = np.log(y1/y0)/(x1 - x0)
partial_sum[i_low:i_high] = y0/m*(np.exp(m*(x1 - x0)) - 1)
elif self.interpolation[k] == 5:
# Log-log
m = np.log(y1/y0)/np.log(x1/x0)
partial_sum[i_low:i_high] = y0/((m + 1)*x0**m)*(
x1**(m + 1) - x0**(m + 1))
i_low = i_high
return np.concatenate(([0.], np.cumsum(partial_sum)))
def to_hdf5(self, group, name='xy'):
"""Write tabulated function to an HDF5 group
Parameters
----------
group : h5py.Group
HDF5 group to write to
name : str
Name of the dataset to create
"""
dataset = group.create_dataset(name, data=np.vstack(
[self.x, self.y]))
dataset.attrs['type'] = np.string_(type(self).__name__)
dataset.attrs['breakpoints'] = self.breakpoints
dataset.attrs['interpolation'] = self.interpolation
@classmethod
def from_hdf5(cls, dataset):
"""Generate tabulated function from an HDF5 dataset
Parameters
----------
dataset : h5py.Dataset
Dataset to read from
Returns
-------
openmc.data.Tabulated1D
Function read from dataset
"""
if dataset.attrs['type'].decode() != cls.__name__:
raise ValueError("Expected an HDF5 attribute 'type' equal to '"
+ cls.__name__ + "'")
x = dataset[0, :]
y = dataset[1, :]
breakpoints = dataset.attrs['breakpoints']
interpolation = dataset.attrs['interpolation']
return cls(x, y, breakpoints, interpolation)
@classmethod
def from_ace(cls, ace, idx=0, convert_units=True):
"""Create a Tabulated1D object from an ACE table.
Parameters
----------
ace : openmc.data.ace.Table
An ACE table
idx : int
Offset to read from in XSS array (default of zero)
convert_units : bool
If the abscissa represents energy, indicate whether to convert MeV
to eV.
Returns
-------
openmc.data.Tabulated1D
Tabulated data object
"""
# Get number of regions and pairs
n_regions = int(ace.xss[idx])
n_pairs = int(ace.xss[idx + 1 + 2*n_regions])
# Get interpolation information
idx += 1
if n_regions > 0:
breakpoints = ace.xss[idx:idx + n_regions].astype(int)
interpolation = ace.xss[idx + n_regions:idx + 2*n_regions].astype(int)
else:
# 0 regions implies linear-linear interpolation by default
breakpoints = np.array([n_pairs])
interpolation = np.array([2])
# Get (x,y) pairs
idx += 2*n_regions + 1
x = ace.xss[idx:idx + n_pairs].copy()
y = ace.xss[idx + n_pairs:idx + 2*n_pairs].copy()
if convert_units:
x *= EV_PER_MEV
return Tabulated1D(x, y, breakpoints, interpolation)
class Polynomial(np.polynomial.Polynomial, Function1D):
"""A power series class.
Parameters
----------
coef : Iterable of float
Polynomial coefficients in order of increasing degree
"""
def to_hdf5(self, group, name='xy'):
"""Write polynomial function to an HDF5 group
Parameters
----------
group : h5py.Group
HDF5 group to write to
name : str
Name of the dataset to create
"""
dataset = group.create_dataset(name, data=self.coef)
dataset.attrs['type'] = np.string_(type(self).__name__)
@classmethod
def from_hdf5(cls, dataset):
"""Generate function from an HDF5 dataset
Parameters
----------
dataset : h5py.Dataset
Dataset to read from
Returns
-------
openmc.data.Function1D
Function read from dataset
"""
if dataset.attrs['type'].decode() != cls.__name__:
raise ValueError("Expected an HDF5 attribute 'type' equal to '"
+ cls.__name__ + "'")
return cls(dataset[()])
class Combination(EqualityMixin):
"""Combination of multiple functions with a user-defined operator
This class allows you to create a callable object which represents the
combination of other callable objects by way of a series of user-defined
operators connecting each of the callable objects.
Parameters
----------
functions : Iterable of Callable
Functions to combine according to operations
operations : Iterable of numpy.ufunc
Operations to perform between functions; note that the standard order
of operations will not be followed, but can be simulated by
combinations of Combination objects. The operations parameter must have
a length one less than the number of functions.
Attributes
----------
functions : Iterable of Callable
Functions to combine according to operations
operations : Iterable of numpy.ufunc
Operations to perform between functions; note that the standard order
of operations will not be followed, but can be simulated by
combinations of Combination objects. The operations parameter must have
a length one less than the number of functions.
"""
def __init__(self, functions, operations):
self.functions = functions
self.operations = operations
def __call__(self, x):
ans = self.functions[0](x)
for i, operation in enumerate(self.operations):
ans = operation(ans, self.functions[i + 1](x))
return ans
@property
def functions(self):
return self._functions
@functions.setter
def functions(self, functions):
cv.check_type('functions', functions, Iterable, Callable)
self._functions = functions
@property
def operations(self):
return self._operations
@operations.setter
def operations(self, operations):
cv.check_type('operations', operations, Iterable, np.ufunc)
length = len(self.functions) - 1
cv.check_length('operations', operations, length, length_max=length)
self._operations = operations
class Sum(EqualityMixin):
"""Sum of multiple functions.
This class allows you to create a callable object which represents the sum
of other callable objects. This is used for redundant reactions whereby the
cross section is defined as the sum of other cross sections.
Parameters
----------
functions : Iterable of Callable
Functions which are to be added together
Attributes
----------
functions : Iterable of Callable
Functions which are to be added together
"""
def __init__(self, functions):
self.functions = list(functions)
def __call__(self, x):
return sum(f(x) for f in self.functions)
@property
def functions(self):
return self._functions
@functions.setter
def functions(self, functions):
cv.check_type('functions', functions, Iterable, Callable)
self._functions = functions
class Regions1D(EqualityMixin):
"""Piecewise composition of multiple functions.
This class allows you to create a callable object which is composed
of multiple other callable objects, each applying to a specific interval
Parameters
----------
functions : Iterable of Callable
Functions which are to be combined in a piecewise fashion
breakpoints : Iterable of float
The values of the dependent variable that define the domain of
each function. The `i`\ th and `(i+1)`\ th values are the limits of the
domain of the `i`\ th function. Values must be monotonically increasing.
Attributes
----------
functions : Iterable of Callable
Functions which are to be combined in a piecewise fashion
breakpoints : Iterable of float
The breakpoints between each function
"""
def __init__(self, functions, breakpoints):
self.functions = functions
self.breakpoints = breakpoints
def __call__(self, x):
i = np.searchsorted(self.breakpoints, x)
if isinstance(x, Iterable):
ans = np.empty_like(x)
for j in range(len(i)):
ans[j] = self.functions[i[j]](x[j])
return ans
else:
return self.functions[i](x)
@property
def functions(self):
return self._functions
@property
def breakpoints(self):
return self._breakpoints
@functions.setter
def functions(self, functions):
cv.check_type('functions', functions, Iterable, Callable)
self._functions = functions
@breakpoints.setter
def breakpoints(self, breakpoints):
cv.check_iterable_type('breakpoints', breakpoints, Real)
self._breakpoints = breakpoints
class ResonancesWithBackground(EqualityMixin):
"""Cross section in resolved resonance region.
Parameters
----------
resonances : openmc.data.Resonances
Resolved resonance parameter data
background : Callable
Background cross section as a function of energy
mt : int
MT value of the reaction
Attributes
----------
resonances : openmc.data.Resonances
Resolved resonance parameter data
background : Callable
Background cross section as a function of energy
mt : int
MT value of the reaction
"""
def __init__(self, resonances, background, mt):
self.resonances = resonances
self.background = background
self.mt = mt
def __call__(self, x):
# Get background cross section
xs = self.background(x)
for r in self.resonances:
if not isinstance(r, openmc.data.resonance._RESOLVED):
continue
if isinstance(x, Iterable):
# Determine which energies are within resolved resonance range
within = (r.energy_min <= x) & (x <= r.energy_max)
# Get resonance cross sections and add to background
resonant_xs = r.reconstruct(x[within])
xs[within] += resonant_xs[self.mt]
else:
if r.energy_min <= x <= r.energy_max:
resonant_xs = r.reconstruct(x)
xs += resonant_xs[self.mt]
return xs
@property
def background(self):
return self._background
@property
def mt(self):
return self._mt
@property
def resonances(self):
return self._resonances
@background.setter
def background(self, background):
cv.check_type('background cross section', background, Callable)
self._background = background
@mt.setter
def mt(self, mt):
cv.check_type('MT value', mt, Integral)
self._mt = mt
@resonances.setter
def resonances(self, resonances):
cv.check_type('resolved resonance parameters', resonances,
openmc.data.Resonances)
self._resonances = resonances

114
openmc/data/grid.py Normal file
View file

@ -0,0 +1,114 @@
import numpy as np
def linearize(x, f, tolerance=0.001):
"""Return a tabulated representation of a function of one variable.
Parameters
----------
x : Iterable of float
Initial x values at which the function should be evaluated
f : Callable
Function of a single variable
tolerance : float
Tolerance on the interpolation error
Returns
-------
numpy.ndarray
Tabulated values of the independent variable
numpy.ndarray
Tabulated values of the dependent variable
"""
# Make sure x is a numpy array
x = np.asarray(x)
# Initialize output arrays
x_out = []
y_out = []
# Initialize stack
x_stack = [x[0]]
y_stack = [f(x[0])]
for i in range(x.shape[0] - 1):
x_stack.insert(0, x[i + 1])
y_stack.insert(0, f(x[i + 1]))
while True:
x_high, x_low = x_stack[-2:]
y_high, y_low = y_stack[-2:]
x_mid = 0.5*(x_low + x_high)
y_mid = f(x_mid)
y_interp = y_low + (y_high - y_low)/(x_high - x_low)*(x_mid - x_low)
error = abs((y_interp - y_mid)/y_mid)
if error > tolerance:
x_stack.insert(-1, x_mid)
y_stack.insert(-1, y_mid)
else:
x_out.append(x_stack.pop())
y_out.append(y_stack.pop())
if len(x_stack) == 1:
break
x_out.append(x_stack.pop())
y_out.append(y_stack.pop())
return np.array(x_out), np.array(y_out)
def thin(x, y, tolerance=0.001):
"""Check for (x,y) points that can be removed.
Parameters
----------
x : numpy.ndarray
Independent variable
y : numpy.ndarray
Dependent variable
tolerance : float
Tolerance on interpolation error
Returns
-------
numpy.ndarray
Tabulated values of the independent variable
numpy.ndarray
Tabulated values of the dependent variable
"""
# Initialize output arrays
x_out = x.copy()
y_out = y.copy()
N = x.shape[0]
i_left = 0
i_right = 2
while i_left < N - 2 and i_right < N:
m = (y[i_right] - y[i_left])/(x[i_right] - x[i_left])
for i in range(i_left + 1, i_right):
# Determine error in interpolated point
y_interp = y[i_left] + m*(x[i] - x[i_left])
if abs(y[i]) > 0.:
error = abs((y_interp - y[i])/y[i])
else:
error = 2*tolerance
if error > tolerance:
for i_remove in range(i_left + 1, i_right - 1):
x_out[i_remove] = np.nan
y_out[i_remove] = np.nan
i_left = i_right - 1
i_right = i_left + 1
break
i_right += 1
for i_remove in range(i_left + 1, i_right - 1):
x_out[i_remove] = np.nan
y_out[i_remove] = np.nan
return x_out[np.isfinite(x_out)], y_out[np.isfinite(y_out)]

402
openmc/data/kalbach_mann.py Normal file
View file

@ -0,0 +1,402 @@
from collections.abc import Iterable
from numbers import Real, Integral
from warnings import warn
import numpy as np
import openmc.checkvalue as cv
from openmc.stats import Tabular, Univariate, Discrete, Mixture
from .function import Tabulated1D, INTERPOLATION_SCHEME
from .angle_energy import AngleEnergy
from .data import EV_PER_MEV
from .endf import get_list_record, get_tab2_record
class KalbachMann(AngleEnergy):
"""Kalbach-Mann distribution
Parameters
----------
breakpoints : Iterable of int
Breakpoints defining interpolation regions
interpolation : Iterable of int
Interpolation codes
energy : Iterable of float
Incoming energies at which distributions exist
energy_out : Iterable of openmc.stats.Univariate
Distribution of outgoing energies corresponding to each incoming energy
precompound : Iterable of openmc.data.Tabulated1D
Precompound factor 'r' as a function of outgoing energy for each
incoming energy
slope : Iterable of openmc.data.Tabulated1D
Kalbach-Chadwick angular distribution slope value 'a' as a function of
outgoing energy for each incoming energy
Attributes
----------
breakpoints : Iterable of int
Breakpoints defining interpolation regions
interpolation : Iterable of int
Interpolation codes
energy : Iterable of float
Incoming energies at which distributions exist
energy_out : Iterable of openmc.stats.Univariate
Distribution of outgoing energies corresponding to each incoming energy
precompound : Iterable of openmc.data.Tabulated1D
Precompound factor 'r' as a function of outgoing energy for each
incoming energy
slope : Iterable of openmc.data.Tabulated1D
Kalbach-Chadwick angular distribution slope value 'a' as a function of
outgoing energy for each incoming energy
"""
def __init__(self, breakpoints, interpolation, energy, energy_out,
precompound, slope):
super().__init__()
self.breakpoints = breakpoints
self.interpolation = interpolation
self.energy = energy
self.energy_out = energy_out
self.precompound = precompound
self.slope = slope
@property
def breakpoints(self):
return self._breakpoints
@property
def interpolation(self):
return self._interpolation
@property
def energy(self):
return self._energy
@property
def energy_out(self):
return self._energy_out
@property
def precompound(self):
return self._precompound
@property
def slope(self):
return self._slope
@breakpoints.setter
def breakpoints(self, breakpoints):
cv.check_type('Kalbach-Mann breakpoints', breakpoints,
Iterable, Integral)
self._breakpoints = breakpoints
@interpolation.setter
def interpolation(self, interpolation):
cv.check_type('Kalbach-Mann interpolation', interpolation,
Iterable, Integral)
self._interpolation = interpolation
@energy.setter
def energy(self, energy):
cv.check_type('Kalbach-Mann incoming energy', energy,
Iterable, Real)
self._energy = energy
@energy_out.setter
def energy_out(self, energy_out):
cv.check_type('Kalbach-Mann distributions', energy_out,
Iterable, Univariate)
self._energy_out = energy_out
@precompound.setter
def precompound(self, precompound):
cv.check_type('Kalbach-Mann precompound factor', precompound,
Iterable, Tabulated1D)
self._precompound = precompound
@slope.setter
def slope(self, slope):
cv.check_type('Kalbach-Mann slope', slope, Iterable, Tabulated1D)
self._slope = slope
def to_hdf5(self, group):
"""Write distribution to an HDF5 group
Parameters
----------
group : h5py.Group
HDF5 group to write to
"""
group.attrs['type'] = np.string_('kalbach-mann')
dset = group.create_dataset('energy', data=self.energy)
dset.attrs['interpolation'] = np.vstack((self.breakpoints,
self.interpolation))
# Determine total number of (E,p,r,a) tuples and create array
n_tuple = sum(len(d) for d in self.energy_out)
distribution = np.empty((5, n_tuple))
# Create array for offsets
offsets = np.empty(len(self.energy_out), dtype=int)
interpolation = np.empty(len(self.energy_out), dtype=int)
n_discrete_lines = np.empty(len(self.energy_out), dtype=int)
j = 0
# Populate offsets and distribution array
for i, (eout, km_r, km_a) in enumerate(zip(
self.energy_out, self.precompound, self.slope)):
n = len(eout)
offsets[i] = j
if isinstance(eout, Mixture):
discrete, continuous = eout.distribution
n_discrete_lines[i] = m = len(discrete)
interpolation[i] = 1 if continuous.interpolation == 'histogram' else 2
distribution[0, j:j+m] = discrete.x
distribution[1, j:j+m] = discrete.p
distribution[2, j:j+m] = discrete.c
distribution[0, j+m:j+n] = continuous.x
distribution[1, j+m:j+n] = continuous.p
distribution[2, j+m:j+n] = continuous.c
else:
if isinstance(eout, Tabular):
n_discrete_lines[i] = 0
interpolation[i] = 1 if eout.interpolation == 'histogram' else 2
elif isinstance(eout, Discrete):
n_discrete_lines[i] = n
interpolation[i] = 1
distribution[0, j:j+n] = eout.x
distribution[1, j:j+n] = eout.p
distribution[2, j:j+n] = eout.c
distribution[3, j:j+n] = km_r.y
distribution[4, j:j+n] = km_a.y
j += n
# Create dataset for distributions
dset = group.create_dataset('distribution', data=distribution)
# Write interpolation as attribute
dset.attrs['offsets'] = offsets
dset.attrs['interpolation'] = interpolation
dset.attrs['n_discrete_lines'] = n_discrete_lines
@classmethod
def from_hdf5(cls, group):
"""Generate Kalbach-Mann distribution from HDF5 data
Parameters
----------
group : h5py.Group
HDF5 group to read from
Returns
-------
openmc.data.KalbachMann
Kalbach-Mann energy distribution
"""
interp_data = group['energy'].attrs['interpolation']
energy_breakpoints = interp_data[0, :]
energy_interpolation = interp_data[1, :]
energy = group['energy'][()]
data = group['distribution']
offsets = data.attrs['offsets']
interpolation = data.attrs['interpolation']
n_discrete_lines = data.attrs['n_discrete_lines']
energy_out = []
precompound = []
slope = []
n_energy = len(energy)
for i in range(n_energy):
# Determine length of outgoing energy distribution and number of
# discrete lines
j = offsets[i]
if i < n_energy - 1:
n = offsets[i+1] - j
else:
n = data.shape[1] - j
m = n_discrete_lines[i]
# Create discrete distribution if lines are present
if m > 0:
eout_discrete = Discrete(data[0, j:j+m], data[1, j:j+m])
eout_discrete.c = data[2, j:j+m]
p_discrete = eout_discrete.c[-1]
# Create continuous distribution
if m < n:
interp = INTERPOLATION_SCHEME[interpolation[i]]
eout_continuous = Tabular(data[0, j+m:j+n], data[1, j+m:j+n], interp)
eout_continuous.c = data[2, j+m:j+n]
# If both continuous and discrete are present, create a mixture
# distribution
if m == 0:
eout_i = eout_continuous
elif m == n:
eout_i = eout_discrete
else:
eout_i = Mixture([p_discrete, 1. - p_discrete],
[eout_discrete, eout_continuous])
km_r = Tabulated1D(data[0, j:j+n], data[3, j:j+n])
km_a = Tabulated1D(data[0, j:j+n], data[4, j:j+n])
energy_out.append(eout_i)
precompound.append(km_r)
slope.append(km_a)
return cls(energy_breakpoints, energy_interpolation,
energy, energy_out, precompound, slope)
@classmethod
def from_ace(cls, ace, idx, ldis):
"""Generate Kalbach-Mann energy-angle distribution from ACE data
Parameters
----------
ace : openmc.data.ace.Table
ACE table to read from
idx : int
Index in XSS array of the start of the energy distribution data
(LDIS + LOCC - 1)
ldis : int
Index in XSS array of the start of the energy distribution block
(e.g. JXS[11])
Returns
-------
openmc.data.KalbachMann
Kalbach-Mann energy-angle distribution
"""
# Read number of interpolation regions and incoming energies
n_regions = int(ace.xss[idx])
n_energy_in = int(ace.xss[idx + 1 + 2*n_regions])
# Get interpolation information
idx += 1
if n_regions > 0:
breakpoints = ace.xss[idx:idx + n_regions].astype(int)
interpolation = ace.xss[idx + n_regions:idx + 2*n_regions].astype(int)
else:
breakpoints = np.array([n_energy_in])
interpolation = np.array([2])
# Incoming energies at which distributions exist
idx += 2*n_regions + 1
energy = ace.xss[idx:idx + n_energy_in]*EV_PER_MEV
# Location of distributions
idx += n_energy_in
loc_dist = ace.xss[idx:idx + n_energy_in].astype(int)
# Initialize variables
energy_out = []
km_r = []
km_a = []
# Read each outgoing energy distribution
for i in range(n_energy_in):
idx = ldis + loc_dist[i] - 1
# intt = interpolation scheme (1=hist, 2=lin-lin)
INTTp = int(ace.xss[idx])
intt = INTTp % 10
n_discrete_lines = (INTTp - intt)//10
if intt not in (1, 2):
warn("Interpolation scheme for continuous tabular distribution "
"is not histogram or linear-linear.")
intt = 2
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
# Create continuous distribution
eout_continuous = Tabular(data[0][n_discrete_lines:],
data[1][n_discrete_lines:]/EV_PER_MEV,
INTERPOLATION_SCHEME[intt],
ignore_negative=True)
eout_continuous.c = data[2][n_discrete_lines:]
if np.any(data[1][n_discrete_lines:] < 0.0):
warn("Kalbach-Mann energy distribution has negative "
"probabilities.")
# If discrete lines are present, create a mixture distribution
if n_discrete_lines > 0:
eout_discrete = Discrete(data[0][:n_discrete_lines],
data[1][:n_discrete_lines])
eout_discrete.c = data[2][:n_discrete_lines]
if n_discrete_lines == n_energy_out:
eout_i = eout_discrete
else:
p_discrete = min(sum(eout_discrete.p), 1.0)
eout_i = Mixture([p_discrete, 1. - p_discrete],
[eout_discrete, eout_continuous])
else:
eout_i = eout_continuous
energy_out.append(eout_i)
km_r.append(Tabulated1D(data[0], data[3]))
km_a.append(Tabulated1D(data[0], data[4]))
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
Parameters
----------
file_obj : file-like object
ENDF file positioned at the start of the Kalbach-Mann distribution
Returns
-------
openmc.data.KalbachMann
Kalbach-Mann energy-angle distribution
"""
params, tab2 = get_tab2_record(file_obj)
lep = params[3]
ne = params[5]
energy = np.zeros(ne)
n_discrete_energies = np.zeros(ne, dtype=int)
energy_out = []
precompound = []
slope = []
for i in range(ne):
items, values = get_list_record(file_obj)
energy[i] = items[1]
n_discrete_energies[i] = items[2]
# TODO: split out discrete energies
n_angle = items[3]
n_energy_out = items[5]
values = np.asarray(values)
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]
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]
if n_angle == 2:
a_i = values[:,3]
else:
a_i = np.zeros_like(r_i)
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)

142
openmc/data/laboratory.py Normal file
View file

@ -0,0 +1,142 @@
from collections.abc import Iterable
from numbers import Real, Integral
import numpy as np
import openmc.checkvalue as cv
from openmc.stats import Tabular, Univariate, Discrete, Mixture
from .angle_energy import AngleEnergy
from .function import INTERPOLATION_SCHEME
from .endf import get_tab2_record, get_tab1_record
class LaboratoryAngleEnergy(AngleEnergy):
"""Laboratory angle-energy distribution
Parameters
----------
breakpoints : Iterable of int
Breakpoints defining interpolation regions
interpolation : Iterable of int
Interpolation codes
energy : Iterable of float
Incoming energies at which distributions exist
mu : Iterable of openmc.stats.Univariate
Distribution of scattering cosines for each incoming energy
energy_out : Iterable of Iterable of openmc.stats.Univariate
Distribution of outgoing energies for each incoming energy/scattering
cosine
Attributes
----------
breakpoints : Iterable of int
Breakpoints defining interpolation regions
interpolation : Iterable of int
Interpolation codes
energy : Iterable of float
Incoming energies at which distributions exist
mu : Iterable of openmc.stats.Univariate
Distribution of scattering cosines for each incoming energy
energy_out : Iterable of Iterable of openmc.stats.Univariate
Distribution of outgoing energies for each incoming energy/scattering
cosine
"""
def __init__(self, breakpoints, interpolation, energy, mu, energy_out):
super().__init__()
self.breakpoints = breakpoints
self.interpolation = interpolation
self.energy = energy
self.mu = mu
self.energy_out = energy_out
@property
def breakpoints(self):
return self._breakpoints
@property
def interpolation(self):
return self._interpolation
@property
def energy(self):
return self._energy
@property
def mu(self):
return self._mu
@property
def energy_out(self):
return self._energy_out
@breakpoints.setter
def breakpoints(self, breakpoints):
cv.check_type('laboratory angle-energy breakpoints', breakpoints,
Iterable, Integral)
self._breakpoints = breakpoints
@interpolation.setter
def interpolation(self, interpolation):
cv.check_type('laboratory angle-energy interpolation', interpolation,
Iterable, Integral)
self._interpolation = interpolation
@energy.setter
def energy(self, energy):
cv.check_type('laboratory angle-energy incoming energy', energy,
Iterable, Real)
self._energy = energy
@mu.setter
def mu(self, mu):
cv.check_type('laboratory angle-energy outgoing cosine', mu,
Iterable, Univariate)
self._mu = mu
@energy_out.setter
def energy_out(self, energy_out):
cv.check_iterable_type('laboratory angle-energy outgoing energy',
energy_out, Univariate, 2, 2)
self._energy_out = energy_out
@classmethod
def from_endf(cls, file_obj):
"""Generate laboratory angle-energy distribution from an ENDF evaluation
Parameters
----------
file_obj : file-like object
ENDF file positioned at the start of a section for a correlated
angle-energy distribution
Returns
-------
openmc.data.LaboratoryAngleEnergy
Laboratory angle-energy distribution
"""
params, tab2 = get_tab2_record(file_obj)
ne = params[5]
energy = np.zeros(ne)
mu = []
energy_out = []
for i in range(ne):
params, tab2mu = get_tab2_record(file_obj)
energy[i] = params[1]
n_mu = params[5]
mu_i = np.zeros(n_mu)
p_mu_i = np.zeros(n_mu)
energy_out_i = []
for j in range(n_mu):
params, f = get_tab1_record(file_obj)
mu_i[j] = params[1]
p_mu_i[j] = sum(f.y)
energy_out_i.append(Tabular(f.x, f.y))
mu.append(Tabular(mu_i, p_mu_i))
energy_out.append(energy_out_i)
return cls(tab2.breakpoints, tab2.interpolation, energy, mu, energy_out)
def to_hdf5(self, group):
raise NotImplementedError

173
openmc/data/library.py Normal file
View file

@ -0,0 +1,173 @@
import os
import xml.etree.ElementTree as ET
import pathlib
import h5py
from openmc.mixin import EqualityMixin
from openmc._xml import clean_indentation
from openmc.checkvalue import check_type
class DataLibrary(EqualityMixin):
"""Collection of cross section data libraries.
Attributes
----------
libraries : list of dict
List in which each item is a dictionary summarizing cross section data
from a single file. The dictionary has keys 'path', 'type', and
'materials'.
"""
def __init__(self):
self.libraries = []
def get_by_material(self, name):
"""Return the library dictionary containing a given material.
Parameters
----------
name : str
Name of material, e.g. 'Am241'
Returns
-------
library : dict or None
Dictionary summarizing cross section data from a single file;
the dictionary has keys 'path', 'type', and 'materials'.
"""
for library in self.libraries:
if name in library['materials']:
return library
return None
def register_file(self, filename):
"""Register a file with the data library.
Parameters
----------
filename : str or Path
Path to the file to be registered.
If an ``xml`` file, treat as the depletion chain file without
materials.
"""
if not isinstance(filename, pathlib.Path):
path = pathlib.Path(filename)
else:
path = filename
if path.suffix == '.xml':
filetype = 'depletion_chain'
materials = []
elif path.suffix == '.h5':
with h5py.File(path, 'r') as h5file:
filetype = h5file.attrs['filetype'].decode()[5:]
materials = list(h5file)
else:
raise ValueError(
"File type {} not supported by {}"
.format(path.name, self.__class__.__name__))
library = {'path': str(path), 'type': filetype, 'materials': materials}
self.libraries.append(library)
def export_to_xml(self, path='cross_sections.xml'):
"""Export cross section data library to an XML file.
Parameters
----------
path : str
Path to file to write. Defaults to 'cross_sections.xml'.
"""
root = ET.Element('cross_sections')
# Determine common directory for library paths
common_dir = os.path.dirname(os.path.commonprefix(
[lib['path'] for lib in self.libraries]))
if common_dir == '':
common_dir = '.'
if os.path.relpath(common_dir, os.path.dirname(str(path))) != '.':
dir_element = ET.SubElement(root, "directory")
dir_element.text = os.path.realpath(common_dir)
for library in self.libraries:
if library['type'] == "depletion_chain":
lib_element = ET.SubElement(root, "depletion_chain")
else:
lib_element = ET.SubElement(root, "library")
lib_element.set('materials', ' '.join(library['materials']))
lib_element.set('path', os.path.relpath(library['path'], common_dir))
lib_element.set('type', library['type'])
# Clean the indentation to be user-readable
clean_indentation(root)
# Write XML file
tree = ET.ElementTree(root)
tree.write(str(path), xml_declaration=True, encoding='utf-8',
method='xml')
@classmethod
def from_xml(cls, path=None):
"""Read cross section data library from an XML file.
Parameters
----------
path : str, optional
Path to XML file to read. If not provided, the
:envvar:`OPENMC_CROSS_SECTIONS` environment variable will be used.
Returns
-------
data : openmc.data.DataLibrary
Data library object initialized from the provided XML
"""
data = cls()
# If path is None, get the cross sections from the
# OPENMC_CROSS_SECTIONS environment variable
if path is None:
path = os.environ.get('OPENMC_CROSS_SECTIONS')
# Check to make sure there was an environmental variable.
if path is None:
raise ValueError("Either path or OPENMC_CROSS_SECTIONS "
"environmental variable must be set")
# Convert to string to support pathlib
# TODO: Remove when support is Python 3.6+ only
path = str(path)
tree = ET.parse(path)
root = tree.getroot()
if root.find('directory') is not None:
directory = root.find('directory').text
else:
directory = os.path.dirname(path)
for lib_element in root.findall('library'):
filename = os.path.join(directory, lib_element.attrib['path'])
filetype = lib_element.attrib['type']
materials = lib_element.attrib['materials'].split()
library = {'path': filename, 'type': filetype,
'materials': materials}
data.libraries.append(library)
# get depletion chain data
dep_node = root.find("depletion_chain")
if dep_node is not None:
filename = os.path.join(directory, dep_node.attrib['path'])
library = {'path': filename, 'type': 'depletion_chain',
'materials': []}
data.libraries.append(library)
return data

3475
openmc/data/mass16.txt Normal file

File diff suppressed because it is too large Load diff

535
openmc/data/multipole.py Normal file
View file

@ -0,0 +1,535 @@
from numbers import Integral, Real
from math import exp, erf, pi, sqrt
import h5py
import numpy as np
from . import WMP_VERSION, WMP_VERSION_MAJOR
from .data import K_BOLTZMANN
import openmc.checkvalue as cv
from openmc.mixin import EqualityMixin
# Constants that determine which value to access
_MP_EA = 0 # Pole
# Residue indices
_MP_RS = 1 # Residue scattering
_MP_RA = 2 # Residue absorption
_MP_RF = 3 # Residue fission
# Polynomial fit indices
_FIT_S = 0 # Scattering
_FIT_A = 1 # Absorption
_FIT_F = 2 # Fission
def _faddeeva(z):
r"""Evaluate the complex Faddeeva function.
Technically, the value we want is given by the equation:
.. math::
w(z) = \frac{i}{\pi} \int_{-\infty}^{\infty} \frac{1}{z - t}
\exp(-t^2) \text{d}t
as shown in Equation 63 from Hwang, R. N. "A rigorous pole
representation of multilevel cross sections and its practical
applications." Nuclear Science and Engineering 96.3 (1987): 192-209.
The :func:`scipy.special.wofz` function evaluates
:math:`w(z) = \exp(-z^2) \text{erfc}(-iz)`. These two forms of the Faddeeva
function are related by a transformation.
If we call the integral form :math:`w_\text{int}`, and the function form
:math:`w_\text{fun}`:
.. math::
w_\text{int}(z) =
\begin{cases}
w_\text{fun}(z) & \text{for } \text{Im}(z) > 0\\
-w_\text{fun}(z^*)^* & \text{for } \text{Im}(z) < 0
\end{cases}
Parameters
----------
z : complex
Argument to the Faddeeva function.
Returns
-------
complex
:math:`\frac{i}{\pi} \int_{-\infty}^{\infty} \frac{1}{z - t} \exp(-t^2)
\text{d}t`
"""
from scipy.special import wofz
if np.angle(z) > 0:
return wofz(z)
else:
return -np.conj(wofz(z.conjugate()))
def _broaden_wmp_polynomials(E, dopp, n):
r"""Evaluate Doppler-broadened windowed multipole curvefit.
The curvefit is a polynomial of the form :math:`\frac{a}{E}
+ \frac{b}{\sqrt{E}} + c + d \sqrt{E} + \ldots`
Parameters
----------
E : Real
Energy to evaluate at.
dopp : Real
sqrt(atomic weight ratio / kT) in units of eV.
n : Integral
Number of components to the polynomial.
Returns
-------
numpy.ndarray
The value of each Doppler-broadened curvefit polynomial term.
"""
sqrtE = sqrt(E)
beta = sqrtE * dopp
half_inv_dopp2 = 0.5 / dopp**2
quarter_inv_dopp4 = half_inv_dopp2**2
if beta > 6.0:
# Save time, ERF(6) is 1 to machine precision.
# beta/sqrtpi*exp(-beta**2) is also approximately 1 machine epsilon.
erf_beta = 1.0
exp_m_beta2 = 0.0
else:
erf_beta = erf(beta)
exp_m_beta2 = exp(-beta**2)
# Assume that, for sure, we'll use a second order (1/E, 1/V, const)
# fit, and no less.
factors = np.zeros(n)
factors[0] = erf_beta / E
factors[1] = 1.0 / sqrtE
factors[2] = (factors[0] * (half_inv_dopp2 + E)
+ exp_m_beta2 / (beta * sqrt(pi)))
# Perform recursive broadening of high order components. range(1, n-2)
# replaces a do i = 1, n-3. All indices are reduced by one due to the
# 1-based vs. 0-based indexing.
for i in range(1, n-2):
if i != 1:
factors[i+2] = (-factors[i-2] * (i - 1.0) * i * quarter_inv_dopp4
+ factors[i] * (E + (1.0 + 2.0 * i) * half_inv_dopp2))
else:
factors[i+2] = factors[i]*(E + (1.0 + 2.0 * i) * half_inv_dopp2)
return factors
class WindowedMultipole(EqualityMixin):
"""Resonant cross sections represented in the windowed multipole format.
Parameters
----------
name : str
Name of the nuclide using the GND naming convention
Attributes
----------
fit_order : Integral
Order of the windowed curvefit.
fissionable : bool
Whether or not the target nuclide has fission data.
spacing : Real
The width of each window in sqrt(E)-space. For example, the frst window
will end at (sqrt(E_min) + spacing)**2 and the second window at
(sqrt(E_min) + 2*spacing)**2.
sqrtAWR : Real
Square root of the atomic weight ratio of the target nuclide.
E_min : Real
Lowest energy in eV the library is valid for.
E_max : Real
Highest energy in eV the library is valid for.
data : np.ndarray
A 2D array of complex poles and residues. data[i, 0] gives the energy
at which pole i is located. data[i, 1:] gives the residues associated
with the i-th pole. There are 3 residues, one each for the scattering,
absorption, and fission channels.
windows : np.ndarray
A 2D array of Integral values. windows[i, 0] - 1 is the index of the
first pole in window i. windows[i, 1] - 1 is the index of the last pole
in window i.
broaden_poly : np.ndarray
A 1D array of boolean values indicating whether or not the polynomial
curvefit in that window should be Doppler broadened.
curvefit : np.ndarray
A 3D array of Real curvefit polynomial coefficients. curvefit[i, 0, :]
gives coefficients for the scattering cross section in window i.
curvefit[i, 1, :] gives absorption coefficients and curvefit[i, 2, :]
gives fission coefficients. The polynomial terms are increasing powers
of sqrt(E) starting with 1/E e.g:
a/E + b/sqrt(E) + c + d sqrt(E) + ...
"""
def __init__(self, name):
self.name = name
self.spacing = None
self.sqrtAWR = None
self.E_min = None
self.E_max = None
self.data = None
self.windows = None
self.broaden_poly = None
self.curvefit = None
@property
def name(self):
return self._name
@property
def fit_order(self):
return self.curvefit.shape[1] - 1
@property
def fissionable(self):
return self.data.shape[1] == 4
@property
def spacing(self):
return self._spacing
@property
def sqrtAWR(self):
return self._sqrtAWR
@property
def E_min(self):
return self._E_min
@property
def E_max(self):
return self._E_max
@property
def data(self):
return self._data
@property
def windows(self):
return self._windows
@property
def broaden_poly(self):
return self._broaden_poly
@property
def curvefit(self):
return self._curvefit
@name.setter
def name(self, name):
cv.check_type('name', name, str)
self._name = name
@spacing.setter
def spacing(self, spacing):
if spacing is not None:
cv.check_type('spacing', spacing, Real)
cv.check_greater_than('spacing', spacing, 0.0, equality=False)
self._spacing = spacing
@sqrtAWR.setter
def sqrtAWR(self, sqrtAWR):
if sqrtAWR is not None:
cv.check_type('sqrtAWR', sqrtAWR, Real)
cv.check_greater_than('sqrtAWR', sqrtAWR, 0.0, equality=False)
self._sqrtAWR = sqrtAWR
@E_min.setter
def E_min(self, E_min):
if E_min is not None:
cv.check_type('E_min', E_min, Real)
cv.check_greater_than('E_min', E_min, 0.0, equality=True)
self._E_min = E_min
@E_max.setter
def E_max(self, E_max):
if E_max is not None:
cv.check_type('E_max', E_max, Real)
cv.check_greater_than('E_max', E_max, 0.0, equality=False)
self._E_max = E_max
@data.setter
def data(self, data):
if data is not None:
cv.check_type('data', data, np.ndarray)
if len(data.shape) != 2:
raise ValueError('Multipole data arrays must be 2D')
if data.shape[1] not in (3, 4):
raise ValueError(
'data.shape[1] must be 3 or 4. One value for the pole.'
' One each for the scattering and absorption residues. '
'Possibly one more for a fission residue.')
if not np.issubdtype(data.dtype, np.complexfloating):
raise TypeError('Multipole data arrays must be complex dtype')
self._data = data
@windows.setter
def windows(self, windows):
if windows is not None:
cv.check_type('windows', windows, np.ndarray)
if len(windows.shape) != 2:
raise ValueError('Multipole windows arrays must be 2D')
if not np.issubdtype(windows.dtype, np.integer):
raise TypeError('Multipole windows arrays must be integer'
' dtype')
self._windows = windows
@broaden_poly.setter
def broaden_poly(self, broaden_poly):
if broaden_poly is not None:
cv.check_type('broaden_poly', broaden_poly, np.ndarray)
if len(broaden_poly.shape) != 1:
raise ValueError('Multipole broaden_poly arrays must be 1D')
if not np.issubdtype(broaden_poly.dtype, np.bool_):
raise TypeError('Multipole broaden_poly arrays must be boolean'
' dtype')
self._broaden_poly = broaden_poly
@curvefit.setter
def curvefit(self, curvefit):
if curvefit is not None:
cv.check_type('curvefit', curvefit, np.ndarray)
if len(curvefit.shape) != 3:
raise ValueError('Multipole curvefit arrays must be 3D')
if curvefit.shape[2] not in (2, 3): # sig_s, sig_a (maybe sig_f)
raise ValueError('The third dimension of multipole curvefit'
' arrays must have a length of 2 or 3')
if not np.issubdtype(curvefit.dtype, np.floating):
raise TypeError('Multipole curvefit arrays must be float dtype')
self._curvefit = curvefit
@classmethod
def from_hdf5(cls, group_or_filename):
"""Construct a WindowedMultipole object from an HDF5 group or file.
Parameters
----------
group_or_filename : h5py.Group or str
HDF5 group containing multipole data. If given as a string, it is
assumed to be the filename for the HDF5 file, and the first group is
used to read from.
Returns
-------
openmc.data.WindowedMultipole
Resonant cross sections represented in the windowed multipole
format.
"""
if isinstance(group_or_filename, h5py.Group):
group = group_or_filename
else:
h5file = h5py.File(str(group_or_filename), 'r')
# Make sure version matches
if 'version' in h5file.attrs:
major, minor = h5file.attrs['version']
if major != WMP_VERSION_MAJOR:
raise IOError(
'WMP data format uses version {}. {} whereas your '
'installation of the OpenMC Python API expects version '
'{}.x.'.format(major, minor, WMP_VERSION_MAJOR))
else:
raise IOError(
'WMP data does not indicate a version. Your installation of '
'the OpenMC Python API expects version {}.x data.'
.format(WMP_VERSION_MAJOR))
group = list(h5file.values())[0]
name = group.name[1:]
out = cls(name)
# Read scalars.
out.spacing = group['spacing'][()]
out.sqrtAWR = group['sqrtAWR'][()]
out.E_min = group['E_min'][()]
out.E_max = group['E_max'][()]
# Read arrays.
err = "WMP '{}' array shape is not consistent with the '{}' array shape"
out.data = group['data'][()]
out.windows = group['windows'][()]
out.broaden_poly = group['broaden_poly'][...].astype(np.bool)
if out.broaden_poly.shape[0] != out.windows.shape[0]:
raise ValueError(err.format('broaden_poly', 'windows'))
out.curvefit = group['curvefit'][()]
if out.curvefit.shape[0] != out.windows.shape[0]:
raise ValueError(err.format('curvefit', 'windows'))
# _broaden_wmp_polynomials assumes the curve fit has at least 3 terms.
if out.fit_order < 2:
raise ValueError("Windowed multipole is only supported for "
"curvefits with 3 or more terms.")
return out
def _evaluate(self, E, T):
"""Compute scattering, absorption, and fission cross sections.
Parameters
----------
E : Real
Energy of the incident neutron in eV.
T : Real
Temperature of the target in K.
Returns
-------
3-tuple of Real
Total, absorption, and fission microscopic cross sections at the
given energy and temperature.
"""
if E < self.E_min: return (0, 0, 0)
if E > self.E_max: return (0, 0, 0)
# ======================================================================
# Bookkeeping
# Define some frequently used variables.
sqrtkT = sqrt(K_BOLTZMANN * T)
sqrtE = sqrt(E)
invE = 1.0 / E
# Locate us. The i_window calc omits a + 1 present in F90 because of
# the 1-based vs. 0-based indexing. Similarly startw needs to be
# decreased by 1. endw does not need to be decreased because
# range(startw, endw) does not include endw.
i_window = int(np.floor((sqrtE - sqrt(self.E_min)) / self.spacing))
startw = self.windows[i_window, 0] - 1
endw = self.windows[i_window, 1]
# Initialize the ouptut cross sections.
sig_s = 0.0
sig_a = 0.0
sig_f = 0.0
# ======================================================================
# Add the contribution from the curvefit polynomial.
if sqrtkT != 0 and self.broaden_poly[i_window]:
# Broaden the curvefit.
dopp = self.sqrtAWR / sqrtkT
broadened_polynomials = _broaden_wmp_polynomials(E, dopp,
self.fit_order + 1)
for i_poly in range(self.fit_order+1):
sig_s += (self.curvefit[i_window, i_poly, _FIT_S]
* broadened_polynomials[i_poly])
sig_a += (self.curvefit[i_window, i_poly, _FIT_A]
* broadened_polynomials[i_poly])
if self.fissionable:
sig_f += (self.curvefit[i_window, i_poly, _FIT_F]
* broadened_polynomials[i_poly])
else:
temp = invE
for i_poly in range(self.fit_order+1):
sig_s += self.curvefit[i_window, i_poly, _FIT_S] * temp
sig_a += self.curvefit[i_window, i_poly, _FIT_A] * temp
if self.fissionable:
sig_f += self.curvefit[i_window, i_poly, _FIT_F] * temp
temp *= sqrtE
# ======================================================================
# Add the contribution from the poles in this window.
if sqrtkT == 0.0:
# If at 0K, use asymptotic form.
for i_pole in range(startw, endw):
psi_chi = -1j / (self.data[i_pole, _MP_EA] - sqrtE)
c_temp = psi_chi / E
sig_s += (self.data[i_pole, _MP_RS] * c_temp).real
sig_a += (self.data[i_pole, _MP_RA] * c_temp).real
if self.fissionable:
sig_f += (self.data[i_pole, _MP_RF] * c_temp).real
else:
# At temperature, use Faddeeva function-based form.
dopp = self.sqrtAWR / sqrtkT
for i_pole in range(startw, endw):
Z = (sqrtE - self.data[i_pole, _MP_EA]) * dopp
w_val = _faddeeva(Z) * dopp * invE * sqrt(pi)
sig_s += (self.data[i_pole, _MP_RS] * w_val).real
sig_a += (self.data[i_pole, _MP_RA] * w_val).real
if self.fissionable:
sig_f += (self.data[i_pole, _MP_RF] * w_val).real
return sig_s, sig_a, sig_f
def __call__(self, E, T):
"""Compute scattering, absorption, and fission cross sections.
Parameters
----------
E : Real or Iterable of Real
Energy of the incident neutron in eV.
T : Real
Temperature of the target in K.
Returns
-------
3-tuple of Real or 3-tuple of numpy.ndarray
Total, absorption, and fission microscopic cross sections at the
given energy and temperature.
"""
fun = np.vectorize(lambda x: self._evaluate(x, T))
return fun(E)
def export_to_hdf5(self, path, mode='a', libver='earliest'):
"""Export windowed multipole data to an HDF5 file.
Parameters
----------
path : str
Path to write HDF5 file to
mode : {'r', r+', 'w', 'x', 'a'}
Mode that is used to open the HDF5 file. This is the second argument
to the :class:`h5py.File` constructor.
libver : {'earliest', 'latest'}
Compatibility mode for the HDF5 file. 'latest' will produce files
that are less backwards compatible but have performance benefits.
"""
# Open file and write version.
with h5py.File(str(path), mode, libver=libver) as f:
f.attrs['filetype'] = np.string_('data_wmp')
f.attrs['version'] = np.array(WMP_VERSION)
g = f.create_group(self.name)
# Write scalars.
g.create_dataset('spacing', data=np.array(self.spacing))
g.create_dataset('sqrtAWR', data=np.array(self.sqrtAWR))
g.create_dataset('E_min', data=np.array(self.E_min))
g.create_dataset('E_max', data=np.array(self.E_max))
# Write arrays.
g.create_dataset('data', data=self.data)
g.create_dataset('windows', data=self.windows)
g.create_dataset('broaden_poly',
data=self.broaden_poly.astype(np.int8))
g.create_dataset('curvefit', data=self.curvefit)

165
openmc/data/nbody.py Normal file
View file

@ -0,0 +1,165 @@
from numbers import Real, Integral
import numpy as np
import openmc.checkvalue as cv
from .angle_energy import AngleEnergy
from .endf import get_cont_record
class NBodyPhaseSpace(AngleEnergy):
"""N-body phase space distribution
Parameters
----------
total_mass : float
Total mass of product particles
n_particles : int
Number of product particles
atomic_weight_ratio : float
Atomic weight ratio of target nuclide
q_value : float
Q value for reaction in eV
Attributes
----------
total_mass : float
Total mass of product particles
n_particles : int
Number of product particles
atomic_weight_ratio : float
Atomic weight ratio of target nuclide
q_value : float
Q value for reaction in eV
"""
def __init__(self, total_mass, n_particles, atomic_weight_ratio, q_value):
self.total_mass = total_mass
self.n_particles = n_particles
self.atomic_weight_ratio = atomic_weight_ratio
self.q_value = q_value
@property
def total_mass(self):
return self._total_mass
@property
def n_particles(self):
return self._n_particles
@property
def atomic_weight_ratio(self):
return self._atomic_weight_ratio
@property
def q_value(self):
return self._q_value
@total_mass.setter
def total_mass(self, total_mass):
name = 'N-body phase space total mass'
cv.check_type(name, total_mass, Real)
cv.check_greater_than(name, total_mass, 0.)
self._total_mass = total_mass
@n_particles.setter
def n_particles(self, n_particles):
name = 'N-body phase space number of particles'
cv.check_type(name, n_particles, Integral)
cv.check_greater_than(name, n_particles, 0)
self._n_particles = n_particles
@atomic_weight_ratio.setter
def atomic_weight_ratio(self, atomic_weight_ratio):
name = 'N-body phase space atomic weight ratio'
cv.check_type(name, atomic_weight_ratio, Real)
cv.check_greater_than(name, atomic_weight_ratio, 0.0)
self._atomic_weight_ratio = atomic_weight_ratio
@q_value.setter
def q_value(self, q_value):
name = 'N-body phase space Q value'
cv.check_type(name, q_value, Real)
self._q_value = q_value
def to_hdf5(self, group):
"""Write distribution to an HDF5 group
Parameters
----------
group : h5py.Group
HDF5 group to write to
"""
group.attrs['type'] = np.string_('nbody')
group.attrs['total_mass'] = self.total_mass
group.attrs['n_particles'] = self.n_particles
group.attrs['atomic_weight_ratio'] = self.atomic_weight_ratio
group.attrs['q_value'] = self.q_value
@classmethod
def from_hdf5(cls, group):
"""Generate N-body phase space distribution from HDF5 data
Parameters
----------
group : h5py.Group
HDF5 group to read from
Returns
-------
openmc.data.NBodyPhaseSpace
N-body phase space distribution
"""
total_mass = group.attrs['total_mass']
n_particles = group.attrs['n_particles']
awr = group.attrs['atomic_weight_ratio']
q_value = group.attrs['q_value']
return cls(total_mass, n_particles, awr, q_value)
@classmethod
def from_ace(cls, ace, idx, q_value):
"""Generate N-body phase space distribution from ACE data
Parameters
----------
ace : openmc.data.ace.Table
ACE table to read from
idx : int
Index in XSS array of the start of the energy distribution data
(LDIS + LOCC - 1)
q_value : float
Q-value for reaction in eV
Returns
-------
openmc.data.NBodyPhaseSpace
N-body phase space distribution
"""
n_particles = int(ace.xss[idx])
total_mass = ace.xss[idx + 1]
return cls(total_mass, n_particles, ace.atomic_weight_ratio, q_value)
@classmethod
def from_endf(cls, file_obj):
"""Generate N-body phase space distribution from an ENDF evaluation
Parameters
----------
file_obj : file-like object
ENDF file positions at the start of the N-body phase space
distribution
Returns
-------
openmc.data.NBodyPhaseSpace
N-body phase space distribution
"""
items = get_cont_record(file_obj)
total_mass = items[0]
n_particles = items[5]
# TODO: get awr and Q value
return cls(total_mass, n_particles, 1.0, 0.0)

926
openmc/data/neutron.py Normal file
View file

@ -0,0 +1,926 @@
from collections import OrderedDict
from collections.abc import Mapping, MutableMapping
from io import StringIO
from math import log10
from numbers import Integral, Real
import os
import tempfile
from warnings import warn
import numpy as np
import h5py
from . import HDF5_VERSION, HDF5_VERSION_MAJOR
from .ace import Library, Table, get_table, get_metadata
from .data import ATOMIC_SYMBOL, K_BOLTZMANN, EV_PER_MEV
from .endf import (
Evaluation, SUM_RULES, get_head_record, get_tab1_record, get_evaluations)
from .fission_energy import FissionEnergyRelease
from .function import Tabulated1D, Sum, ResonancesWithBackground
from .grid import linearize, thin
from .njoy import make_ace
from .product import Product
from .reaction import Reaction, _get_photon_products_ace
from . import resonance as res
from . import resonance_covariance as res_cov
from .urr import ProbabilityTables
import openmc.checkvalue as cv
from openmc.mixin import EqualityMixin
# Fractions of resonance widths used for reconstructing resonances
_RESONANCE_ENERGY_GRID = np.logspace(-3, 3, 61)
class IncidentNeutron(EqualityMixin):
"""Continuous-energy neutron interaction data.
This class stores data derived from an ENDF-6 format neutron interaction
sublibrary. Instances of this class are not normally instantiated by the
user but rather created using the factory methods
:meth:`IncidentNeutron.from_hdf5`, :meth:`IncidentNeutron.from_ace`, and
:meth:`IncidentNeutron.from_endf`.
Parameters
----------
name : str
Name of the nuclide using the GND naming convention
atomic_number : int
Number of protons in the target nucleus
mass_number : int
Number of nucleons in the target nucleus
metastable : int
Metastable state of the target nucleus. A value of zero indicates ground
state.
atomic_weight_ratio : float
Atomic mass ratio of the target nuclide.
kTs : Iterable of float
List of temperatures of the target nuclide in the data set.
The temperatures have units of eV.
Attributes
----------
atomic_number : int
Number of protons in the target nucleus
atomic_symbol : str
Atomic symbol of the nuclide, e.g., 'Zr'
atomic_weight_ratio : float
Atomic weight ratio of the target nuclide.
fission_energy : None or openmc.data.FissionEnergyRelease
The energy released by fission, tabulated by component (e.g. prompt
neutrons or beta particles) and dependent on incident neutron energy
mass_number : int
Number of nucleons in the target nucleus
metastable : int
Metastable state of the target nucleus. A value of zero indicates ground
state.
name : str
Name of the nuclide using the GND naming convention
reactions : collections.OrderedDict
Contains the cross sections, secondary angle and energy distributions,
and other associated data for each reaction. The keys are the MT values
and the values are Reaction objects.
resonances : openmc.data.Resonances or None
Resonance parameters
resonance_covariance : openmc.data.ResonanceCovariance or None
Covariance for resonance parameters
temperatures : list of str
List of string representations the temperatures of the target nuclide
in the data set. The temperatures are strings of the temperature,
rounded to the nearest integer; e.g., '294K'
kTs : Iterable of float
List of temperatures of the target nuclide in the data set.
The temperatures have units of eV.
urr : dict
Dictionary whose keys are temperatures (e.g., '294K') and values are
unresolved resonance region probability tables.
"""
def __init__(self, name, atomic_number, mass_number, metastable,
atomic_weight_ratio, kTs):
self.name = name
self.atomic_number = atomic_number
self.mass_number = mass_number
self.metastable = metastable
self.atomic_weight_ratio = atomic_weight_ratio
self.kTs = kTs
self.energy = {}
self._fission_energy = None
self.reactions = OrderedDict()
self._urr = {}
self._resonances = None
def __contains__(self, mt):
return mt in self.reactions
def __getitem__(self, mt):
if mt in self.reactions:
return self.reactions[mt]
else:
raise KeyError('No reaction with MT={}.'.format(mt))
def __repr__(self):
return "<IncidentNeutron: {}>".format(self.name)
def __iter__(self):
return iter(self.reactions.values())
@property
def name(self):
return self._name
@property
def atomic_number(self):
return self._atomic_number
@property
def mass_number(self):
return self._mass_number
@property
def metastable(self):
return self._metastable
@property
def atomic_weight_ratio(self):
return self._atomic_weight_ratio
@property
def fission_energy(self):
return self._fission_energy
@property
def reactions(self):
return self._reactions
@property
def resonances(self):
return self._resonances
@property
def resonance_covariance(self):
return self._resonance_covariance
@property
def urr(self):
return self._urr
@property
def temperatures(self):
return ["{}K".format(int(round(kT / K_BOLTZMANN))) for kT in self.kTs]
@name.setter
def name(self, name):
cv.check_type('name', name, str)
self._name = name
@property
def atomic_symbol(self):
return ATOMIC_SYMBOL[self.atomic_number]
@atomic_number.setter
def atomic_number(self, atomic_number):
cv.check_type('atomic number', atomic_number, Integral)
cv.check_greater_than('atomic number', atomic_number, 0, True)
self._atomic_number = atomic_number
@mass_number.setter
def mass_number(self, mass_number):
cv.check_type('mass number', mass_number, Integral)
cv.check_greater_than('mass number', mass_number, 0, True)
self._mass_number = mass_number
@metastable.setter
def metastable(self, metastable):
cv.check_type('metastable', metastable, Integral)
cv.check_greater_than('metastable', metastable, 0, True)
self._metastable = metastable
@atomic_weight_ratio.setter
def atomic_weight_ratio(self, atomic_weight_ratio):
cv.check_type('atomic weight ratio', atomic_weight_ratio, Real)
cv.check_greater_than('atomic weight ratio', atomic_weight_ratio, 0.0)
self._atomic_weight_ratio = atomic_weight_ratio
@fission_energy.setter
def fission_energy(self, fission_energy):
cv.check_type('fission energy release', fission_energy,
FissionEnergyRelease)
self._fission_energy = fission_energy
@reactions.setter
def reactions(self, reactions):
cv.check_type('reactions', reactions, Mapping)
self._reactions = reactions
@resonances.setter
def resonances(self, resonances):
cv.check_type('resonances', resonances, res.Resonances)
self._resonances = resonances
@resonance_covariance.setter
def resonance_covariance(self, resonance_covariance):
cv.check_type('resonance covariance', resonance_covariance,
res_cov.ResonanceCovariances)
self._resonance_covariance = resonance_covariance
@urr.setter
def urr(self, urr):
cv.check_type('probability table dictionary', urr, MutableMapping)
for key, value in urr:
cv.check_type('probability table temperature', key, str)
cv.check_type('probability tables', value, ProbabilityTables)
self._urr = urr
def add_temperature_from_ace(self, ace_or_filename, metastable_scheme='nndc'):
"""Append data from an ACE file at a different temperature.
Parameters
----------
ace_or_filename : openmc.data.ace.Table or str
ACE table to read from. If given as a string, it is assumed to be
the filename for the ACE file.
metastable_scheme : {'nndc', 'mcnp'}
Determine how ZAID identifiers are to be interpreted in the case of
a metastable nuclide. Because the normal ZAID (=1000*Z + A) does not
encode metastable information, different conventions are used among
different libraries. In MCNP libraries, the convention is to add 400
for a metastable nuclide except for Am242m, for which 95242 is
metastable and 95642 (or 1095242 in newer libraries) is the ground
state. For NNDC libraries, ZAID is given as 1000*Z + A + 100*m.
"""
data = IncidentNeutron.from_ace(ace_or_filename, metastable_scheme)
# Check if temprature already exists
strT = data.temperatures[0]
if strT in self.temperatures:
warn('Cross sections at T={} already exist.'.format(strT))
return
# Check that name matches
if data.name != self.name:
raise ValueError('Data provided for an incorrect nuclide.')
# Add temperature
self.kTs += data.kTs
# Add energy grid
self.energy[strT] = data.energy[strT]
# Add normal and redundant reactions
for mt in data.reactions:
if mt in self:
self[mt].xs[strT] = data[mt].xs[strT]
else:
warn("Tried to add cross sections for MT={} at T={} but this "
"reaction doesn't exist.".format(mt, strT))
# Add probability tables
if strT in data.urr:
self.urr[strT] = data.urr[strT]
def add_elastic_0K_from_endf(self, filename, overwrite=False):
"""Append 0K elastic scattering cross section from an ENDF file.
Parameters
----------
filename : str
Path to ENDF file
overwrite : bool
If existing 0 K data is present, this flag can be used to indicate
that it should be overwritten. Otherwise, an exception will be
thrown.
Raises
------
ValueError
If 0 K data is already present and the `overwrite` parameter is
False.
"""
# Check for existing data
if '0K' in self.energy and not overwrite:
raise ValueError('0 K data already exists for this nuclide.')
data = type(self).from_endf(filename)
if data.resonances is not None:
x = []
y = []
for rr in data.resonances:
if isinstance(rr, res.RMatrixLimited):
raise TypeError('R-Matrix Limited not supported.')
elif isinstance(rr, res.Unresolved):
continue
# Get energies/widths for resonances
e_peak = rr.parameters['energy'].values
if isinstance(rr, res.MultiLevelBreitWigner):
gamma = rr.parameters['totalWidth'].values
elif isinstance(rr, res.ReichMoore):
df = rr.parameters
gamma = (df['neutronWidth'] +
df['captureWidth'] +
abs(df['fissionWidthA']) +
abs(df['fissionWidthB'])).values
# Determine peak energies and widths
e_min, e_max = rr.energy_min, rr.energy_max
in_range = (e_peak > e_min) & (e_peak < e_max)
e_peak = e_peak[in_range]
gamma = gamma[in_range]
# Get midpoints between resonances (use min/max energy of
# resolved region as absolute lower/upper bound)
e_mid = np.concatenate(
([e_min], (e_peak[1:] + e_peak[:-1])/2, [e_max]))
# Add grid around each resonance that includes the peak +/- the
# width times each value in _RESONANCE_ENERGY_GRID. Values are
# constrained so that points around one resonance don't overlap
# with points around another. This algorithm is from Fudge.
energies = []
for e, g, e_lower, e_upper in zip(e_peak, gamma, e_mid[:-1],
e_mid[1:]):
e_left = e - g*_RESONANCE_ENERGY_GRID
energies.append(e_left[e_left > e_lower][::-1])
e_right = e + g*_RESONANCE_ENERGY_GRID[1:]
energies.append(e_right[e_right < e_upper])
# Concatenate all points
energies = np.concatenate(energies)
# Create 1000 equal log-spaced energies over RRR, combine with
# resonance peaks and half-height energies
e_log = np.logspace(log10(e_min), log10(e_max), 1000)
energies = np.union1d(e_log, energies)
# Linearize and thin cross section
xi, yi = linearize(energies, data[2].xs['0K'])
xi, yi = thin(xi, yi)
# If there are multiple resolved resonance ranges (e.g. Pu239 in
# ENDF/B-VII.1), combine them
x = np.concatenate((x, xi))
y = np.concatenate((y, yi))
else:
energies = data[2].xs['0K'].x
x, y = linearize(energies, data[2].xs['0K'])
x, y = thin(x, y)
# Set 0K energy grid and elastic scattering cross section
self.energy['0K'] = x
self[2].xs['0K'] = Tabulated1D(x, y)
def get_reaction_components(self, mt):
"""Determine what reactions make up redundant reaction.
Parameters
----------
mt : int
ENDF MT number of the reaction to find components of.
Returns
-------
mts : list of int
ENDF MT numbers of reactions that make up the redundant reaction and
have cross sections provided.
"""
mts = []
if mt in SUM_RULES:
for mt_i in SUM_RULES[mt]:
mts += self.get_reaction_components(mt_i)
if mts:
return mts
else:
return [mt] if mt in self else []
def export_to_hdf5(self, path, mode='a', libver='earliest'):
"""Export incident neutron data to an HDF5 file.
Parameters
----------
path : str
Path to write HDF5 file to
mode : {'r', r+', 'w', 'x', 'a'}
Mode that is used to open the HDF5 file. This is the second argument
to the :class:`h5py.File` constructor.
libver : {'earliest', 'latest'}
Compatibility mode for the HDF5 file. 'latest' will produce files
that are less backwards compatible but have performance benefits.
"""
# If data come from ENDF, don't allow exporting to HDF5
if hasattr(self, '_evaluation'):
raise NotImplementedError('Cannot export incident neutron data that '
'originated from an ENDF file.')
# Open file and write version
f = h5py.File(str(path), mode, libver=libver)
f.attrs['filetype'] = np.string_('data_neutron')
f.attrs['version'] = np.array(HDF5_VERSION)
# Write basic data
g = f.create_group(self.name)
g.attrs['Z'] = self.atomic_number
g.attrs['A'] = self.mass_number
g.attrs['metastable'] = self.metastable
g.attrs['atomic_weight_ratio'] = self.atomic_weight_ratio
ktg = g.create_group('kTs')
for i, temperature in enumerate(self.temperatures):
ktg.create_dataset(temperature, data=self.kTs[i])
# Write energy grid
eg = g.create_group('energy')
for temperature in self.temperatures:
eg.create_dataset(temperature, data=self.energy[temperature])
# Write 0K energy grid if needed
if '0K' in self.energy and '0K' not in eg:
eg.create_dataset('0K', data=self.energy['0K'])
# Write reaction data
rxs_group = g.create_group('reactions')
for rx in self.reactions.values():
# Skip writing redundant reaction if it doesn't have photon
# production or is a summed transmutation reaction. MT=4 is also
# sometimes needed for probability tables. Also write gas
# production, heating, and damage energy production.
if rx.redundant:
photon_rx = any(p.particle == 'photon' for p in rx.products)
keep_mts = (4, 16, 103, 104, 105, 106, 107,
203, 204, 205, 206, 207, 301, 444, 901)
if not (photon_rx or rx.mt in keep_mts):
continue
rx_group = rxs_group.create_group('reaction_{:03}'.format(rx.mt))
rx.to_hdf5(rx_group)
# Write total nu data if available
if len(rx.derived_products) > 0 and 'total_nu' not in g:
tgroup = g.create_group('total_nu')
rx.derived_products[0].to_hdf5(tgroup)
# Write unresolved resonance probability tables
if self.urr:
urr_group = g.create_group('urr')
for temperature, urr in self.urr.items():
tgroup = urr_group.create_group(temperature)
urr.to_hdf5(tgroup)
# Write fission energy release data
if self.fission_energy is not None:
fer_group = g.create_group('fission_energy_release')
self.fission_energy.to_hdf5(fer_group)
f.close()
@classmethod
def from_hdf5(cls, group_or_filename):
"""Generate continuous-energy neutron interaction data from HDF5 group
Parameters
----------
group_or_filename : h5py.Group or str
HDF5 group containing interaction data. If given as a string, it is
assumed to be the filename for the HDF5 file, and the first group is
used to read from.
Returns
-------
openmc.data.IncidentNeutron
Continuous-energy neutron interaction data
"""
if isinstance(group_or_filename, h5py.Group):
group = group_or_filename
else:
h5file = h5py.File(str(group_or_filename), 'r')
# Make sure version matches
if 'version' in h5file.attrs:
major, minor = h5file.attrs['version']
# For now all versions of HDF5 data can be read
else:
raise IOError(
'HDF5 data does not indicate a version. Your installation of '
'the OpenMC Python API expects version {}.x data.'
.format(HDF5_VERSION_MAJOR))
group = list(h5file.values())[0]
name = group.name[1:]
atomic_number = group.attrs['Z']
mass_number = group.attrs['A']
metastable = group.attrs['metastable']
atomic_weight_ratio = group.attrs['atomic_weight_ratio']
kTg = group['kTs']
kTs = []
for temp in kTg:
kTs.append(kTg[temp][()])
data = cls(name, atomic_number, mass_number, metastable,
atomic_weight_ratio, kTs)
# Read energy grid
e_group = group['energy']
for temperature, dset in e_group.items():
data.energy[temperature] = dset[()]
# Read reaction data
rxs_group = group['reactions']
for name, obj in sorted(rxs_group.items()):
if name.startswith('reaction_'):
rx = Reaction.from_hdf5(obj, data.energy)
data.reactions[rx.mt] = rx
# Read total nu data if available
if rx.mt in (18, 19, 20, 21, 38) and 'total_nu' in group:
tgroup = group['total_nu']
rx.derived_products.append(Product.from_hdf5(tgroup))
# Read unresolved resonance probability tables
if 'urr' in group:
urr_group = group['urr']
for temperature, tgroup in urr_group.items():
data.urr[temperature] = ProbabilityTables.from_hdf5(tgroup)
# Read fission energy release data
if 'fission_energy_release' in group:
fer_group = group['fission_energy_release']
data.fission_energy = FissionEnergyRelease.from_hdf5(fer_group)
return data
@classmethod
def from_ace(cls, ace_or_filename, metastable_scheme='nndc'):
"""Generate incident neutron continuous-energy data from an ACE table
Parameters
----------
ace_or_filename : openmc.data.ace.Table or str
ACE table to read from. If the value is a string, it is assumed to
be the filename for the ACE file.
metastable_scheme : {'nndc', 'mcnp'}
Determine how ZAID identifiers are to be interpreted in the case of
a metastable nuclide. Because the normal ZAID (=1000*Z + A) does not
encode metastable information, different conventions are used among
different libraries. In MCNP libraries, the convention is to add 400
for a metastable nuclide except for Am242m, for which 95242 is
metastable and 95642 (or 1095242 in newer libraries) is the ground
state. For NNDC libraries, ZAID is given as 1000*Z + A + 100*m.
Returns
-------
openmc.data.IncidentNeutron
Incident neutron continuous-energy data
"""
# First obtain the data for the first provided ACE table/file
if isinstance(ace_or_filename, Table):
ace = ace_or_filename
else:
ace = get_table(ace_or_filename)
# If mass number hasn't been specified, make an educated guess
zaid, xs = ace.name.split('.')
if not xs.endswith('c'):
raise TypeError(
"{} is not a continuous-energy neutron ACE table.".format(ace))
name, element, Z, mass_number, metastable = \
get_metadata(int(zaid), metastable_scheme)
# Assign temperature to the running list
kTs = [ace.temperature*EV_PER_MEV]
data = cls(name, Z, mass_number, metastable,
ace.atomic_weight_ratio, kTs)
# Get string of temperature to use as a dictionary key
strT = data.temperatures[0]
# Read energy grid
n_energy = ace.nxs[3]
i = ace.jxs[1]
energy = ace.xss[i : i + n_energy]*EV_PER_MEV
data.energy[strT] = energy
total_xs = ace.xss[i + n_energy : i + 2*n_energy]
absorption_xs = ace.xss[i + 2*n_energy : i + 3*n_energy]
heating_number = ace.xss[i + 4*n_energy : i + 5*n_energy]*EV_PER_MEV
# Create redundant reactions (total, absorption, and heating)
total = Reaction(1)
total.xs[strT] = Tabulated1D(energy, total_xs)
total.redundant = True
data.reactions[1] = total
if np.count_nonzero(absorption_xs) > 0:
absorption = Reaction(101)
absorption.xs[strT] = Tabulated1D(energy, absorption_xs)
absorption.redundant = True
data.reactions[101] = absorption
heating = Reaction(301)
heating.xs[strT] = Tabulated1D(energy, heating_number*total_xs)
heating.redundant = True
data.reactions[301] = heating
# Read each reaction
n_reaction = ace.nxs[4] + 1
for i in range(n_reaction):
rx = Reaction.from_ace(ace, i)
data.reactions[rx.mt] = rx
# Some photon production reactions may be assigned to MTs that don't
# exist, usually MT=4. In this case, we create a new reaction and add
# them
n_photon_reactions = ace.nxs[6]
photon_mts = ace.xss[ace.jxs[13]:ace.jxs[13] +
n_photon_reactions].astype(int)
for mt in np.unique(photon_mts // 1000):
if mt not in data:
if mt not in SUM_RULES:
warn('Photon production is present for MT={} but no '
'cross section is given.'.format(mt))
continue
# Create redundant reaction with appropriate cross section
mts = data.get_reaction_components(mt)
if len(mts) == 0:
warn('Photon production is present for MT={} but no '
'reaction components exist.'.format(mt))
continue
# Determine redundant cross section
rx = data._get_redundant_reaction(mt, mts)
rx.products += _get_photon_products_ace(ace, rx)
data.reactions[mt] = rx
# For transmutation reactions, sometimes only individual levels are
# present in an ACE file, e.g. MT=600-649 instead of the summation
# MT=103. In this case, if a user wants to tally (n,p), OpenMC doesn't
# know about the total cross section. Here, we explicitly create a
# redundant reaction for this purpose.
for mt in (16, 103, 104, 105, 106, 107):
if mt not in data:
# Determine if any individual levels are present
mts = data.get_reaction_components(mt)
if len(mts) == 0:
continue
# Determine redundant cross section
rx = data._get_redundant_reaction(mt, mts)
data.reactions[mt] = rx
# Make sure redundant cross sections that are present in an ACE file get
# marked as such
for rx in data:
mts = data.get_reaction_components(rx.mt)
if mts != [rx.mt]:
rx.redundant = True
if rx.mt in (203, 204, 205, 206, 207, 444):
rx.redundant = True
# Read unresolved resonance probability tables
urr = ProbabilityTables.from_ace(ace)
if urr is not None:
data.urr[strT] = urr
return data
@classmethod
def from_endf(cls, ev_or_filename, covariance=False):
"""Generate incident neutron continuous-energy data from an ENDF evaluation
Parameters
----------
ev_or_filename : openmc.data.endf.Evaluation or str
ENDF evaluation to read from. If given as a string, it is assumed to
be the filename for the ENDF file.
covariance : bool
Flag to indicate whether or not covariance data from File 32 should be
retrieved
Returns
-------
openmc.data.IncidentNeutron
Incident neutron continuous-energy data
"""
if isinstance(ev_or_filename, Evaluation):
ev = ev_or_filename
else:
ev = Evaluation(ev_or_filename)
atomic_number = ev.target['atomic_number']
mass_number = ev.target['mass_number']
metastable = ev.target['isomeric_state']
atomic_weight_ratio = ev.target['mass']
temperature = ev.target['temperature']
# Determine name
element = ATOMIC_SYMBOL[atomic_number]
if metastable > 0:
name = '{}{}_m{}'.format(element, mass_number, metastable)
else:
name = '{}{}'.format(element, mass_number)
# Instantiate incident neutron data
data = cls(name, atomic_number, mass_number, metastable,
atomic_weight_ratio, [temperature])
if (2, 151) in ev.section:
data.resonances = res.Resonances.from_endf(ev)
if (32, 151) in ev.section and covariance:
data.resonance_covariance = (
res_cov.ResonanceCovariances.from_endf(ev, data.resonances)
)
# Read each reaction
for mf, mt, nc, mod in ev.reaction_list:
if mf == 3:
data.reactions[mt] = Reaction.from_endf(ev, mt)
# Replace cross sections for elastic, capture, fission
try:
if any(isinstance(r, res._RESOLVED) for r in data.resonances):
for mt in (2, 102, 18):
if mt in data.reactions:
rx = data.reactions[mt]
rx.xs['0K'] = ResonancesWithBackground(
data.resonances, rx.xs['0K'], mt)
except ValueError:
# Thrown if multiple resolved ranges (e.g. Pu239 in ENDF/B-VII.1)
pass
# If first-chance, second-chance, etc. fission are present, check
# whether energy distributions were specified in MF=5. If not, copy the
# energy distribution from MT=18.
for mt, rx in data.reactions.items():
if mt in (19, 20, 21, 38):
if (5, mt) not in ev.section:
if rx.products:
neutron = data.reactions[18].products[0]
rx.products[0].applicability = neutron.applicability
rx.products[0].distribution = neutron.distribution
# Read fission energy release (requires that we already know nu for
# fission)
if (1, 458) in ev.section:
data.fission_energy = FissionEnergyRelease.from_endf(ev, data)
data._evaluation = ev
return data
@classmethod
def from_njoy(cls, filename, temperatures=None, evaluation=None, **kwargs):
"""Generate incident neutron data by running NJOY.
Parameters
----------
filename : str
Path to ENDF file
temperatures : iterable of float
Temperatures in Kelvin to produce data at. If omitted, data is
produced at room temperature (293.6 K)
evaluation : openmc.data.endf.Evaluation, optional
If the ENDF file contains multiple material evaluations, this
argument indicates which evaluation to use.
**kwargs
Keyword arguments passed to :func:`openmc.data.njoy.make_ace`
Returns
-------
data : openmc.data.IncidentNeutron
Incident neutron continuous-energy data
"""
with tempfile.TemporaryDirectory() as tmpdir:
# Run NJOY to create an ACE library
kwargs.setdefault("output_dir", tmpdir)
for key in ("acer", "pendf", "heatr", "broadr", "gaspr", "purr"):
kwargs.setdefault(key, os.path.join(kwargs["output_dir"], key))
kwargs['evaluation'] = evaluation
make_ace(filename, temperatures, **kwargs)
# Create instance from ACE tables within library
lib = Library(kwargs['acer'])
data = cls.from_ace(lib.tables[0])
for table in lib.tables[1:]:
data.add_temperature_from_ace(table)
# Add 0K elastic scattering cross section
if '0K' not in data.energy:
pendf = Evaluation(kwargs['pendf'])
file_obj = StringIO(pendf.section[3, 2])
get_head_record(file_obj)
params, xs = get_tab1_record(file_obj)
data.energy['0K'] = xs.x
data[2].xs['0K'] = xs
# Add fission energy release data
ev = evaluation if evaluation is not None else Evaluation(filename)
if (1, 458) in ev.section:
data.fission_energy = f = FissionEnergyRelease.from_endf(ev, data)
else:
f = None
# For energy deposition, we want to store two different KERMAs:
# one calculated assuming outgoing photons deposit their energy
# locally, and one calculated assuming they carry their energy
# away. This requires two HEATR runs (which make_ace does by
# default). Here, we just need to correct for the fact that NJOY
# uses a fission heating number of h = EFR, whereas we want:
#
# 1) h = EFR + EGP + EGD + EB (for local case)
# 2) h = EFR + EB (for non-local case)
#
# The best way to handle this is to subtract off the fission
# KERMA that NJOY calculates and add back exactly what we want.
# If NJOY is not run with HEATR at all, skip everything below
if not kwargs["heatr"]:
return data
# Helper function to get a cross section from an ENDF file on a
# given energy grid
def get_file3_xs(ev, mt, E):
file_obj = StringIO(ev.section[3, mt])
get_head_record(file_obj)
_, xs = get_tab1_record(file_obj)
return xs(E)
heating_local = Reaction(901)
heating_local.redundant = True
heatr_evals = get_evaluations(kwargs["heatr"])
heatr_local_evals = get_evaluations(kwargs["heatr"] + "_local")
for ev, ev_local in zip(heatr_evals, heatr_local_evals):
temp = "{}K".format(round(ev.target["temperature"]))
# Get total KERMA (originally from ACE file) and energy grid
kerma = data.reactions[301].xs[temp]
E = kerma.x
if f is not None:
# Replace fission KERMA with (EFR + EB)*sigma_f
fission = data.reactions[18].xs[temp]
kerma_fission = get_file3_xs(ev, 318, E)
kerma.y = kerma.y - kerma_fission + (
f.fragments(E) + f.betas(E)) * fission(E)
# For local KERMA, we first need to get the values from the
# HEATR run with photon energy deposited locally and put
# them on the same energy grid
kerma_local = get_file3_xs(ev_local, 301, E)
if f is not None:
# When photons deposit their energy locally, we replace the
# fission KERMA with (EFR + EGP + EGD + EB)*sigma_f
kerma_fission_local = get_file3_xs(ev_local, 318, E)
kerma_local = kerma_local - kerma_fission_local + (
f.fragments(E) + f.prompt_photons(E)
+ f.delayed_photons(E) + f.betas(E))*fission(E)
heating_local.xs[temp] = Tabulated1D(E, kerma_local)
data.reactions[901] = heating_local
return data
def _get_redundant_reaction(self, mt, mts):
"""Create redundant reaction from its components
Parameters
----------
mt : int
MT value of the desired reaction
mts : iterable of int
MT values of its components
Returns
-------
openmc.Reaction
Redundant reaction
"""
# Get energy grid
strT = self.temperatures[0]
energy = self.energy[strT]
rx = Reaction(mt)
xss = [self.reactions[mt_i].xs[strT] for mt_i in mts]
idx = min([xs._threshold_idx if hasattr(xs, '_threshold_idx')
else 0 for xs in xss])
rx.xs[strT] = Tabulated1D(energy[idx:], Sum(xss)(energy[idx:]))
rx.xs[strT]._threshold_idx = idx
rx.redundant = True
return rx

593
openmc/data/njoy.py Normal file
View file

@ -0,0 +1,593 @@
from collections import namedtuple
from io import StringIO
import os
import shutil
from subprocess import Popen, PIPE, STDOUT, CalledProcessError
import tempfile
from pathlib import Path
from . import endf
# For a given MAT number, give a name for the ACE table and a list of ZAID
# identifiers. This is based on Appendix C in the ENDF manual.
ThermalTuple = namedtuple('ThermalTuple', ['name', 'zaids', 'nmix'])
_THERMAL_DATA = {
1: ThermalTuple('hh2o', [1001], 1),
2: ThermalTuple('parah', [1001], 1),
3: ThermalTuple('orthoh', [1001], 1),
5: ThermalTuple('hyh2', [1001], 1),
7: ThermalTuple('hzrh', [1001], 1),
8: ThermalTuple('hcah2', [1001], 1),
10: ThermalTuple('hice', [1001], 1),
11: ThermalTuple('dd2o', [1002], 1),
12: ThermalTuple('parad', [1002], 1),
13: ThermalTuple('orthod', [1002], 1),
14: ThermalTuple('dice', [1002], 1),
26: ThermalTuple('be', [4009], 1),
27: ThermalTuple('bebeo', [4009], 1),
28: ThermalTuple('bebe2c', [4009], 1),
30: ThermalTuple('graph', [6000, 6012, 6013], 1),
31: ThermalTuple('grph10', [6000, 6012, 6013], 1),
32: ThermalTuple('grph30', [6000, 6012, 6013], 1),
33: ThermalTuple('lch4', [1001], 1),
34: ThermalTuple('sch4', [1001], 1),
35: ThermalTuple('sch4p2', [1001], 1),
37: ThermalTuple('hch2', [1001], 1),
38: ThermalTuple('mesi00', [1001], 1),
39: ThermalTuple('lucite', [1001], 1),
40: ThermalTuple('benz', [1001, 6000, 6012], 2),
42: ThermalTuple('tol00', [1001], 1),
43: ThermalTuple('sisic', [14028, 14029, 14030], 1),
44: ThermalTuple('csic', [6000, 6012, 6013], 1),
45: ThermalTuple('ouo2', [8016, 8017, 8018], 1),
46: ThermalTuple('obeo', [8016, 8017, 8018], 1),
47: ThermalTuple('sio2-a', [8016, 8017, 8018, 14028, 14029, 14030], 3),
48: ThermalTuple('osap00', [92238], 1),
49: ThermalTuple('sio2-b', [8016, 8017, 8018, 14028, 14029, 14030], 3),
50: ThermalTuple('oice', [8016, 8017, 8018], 1),
51: ThermalTuple('od2o', [8016, 8017, 8018], 1),
52: ThermalTuple('mg24', [12024], 1),
53: ThermalTuple('al27', [13027], 1),
55: ThermalTuple('yyh2', [39089], 1),
56: ThermalTuple('fe56', [26056], 1),
58: ThermalTuple('zrzrh', [40000, 40090, 40091, 40092, 40094, 40096], 1),
59: ThermalTuple('si00', [14028], 1),
60: ThermalTuple('asap00', [13027], 1),
71: ThermalTuple('n-un', [7014, 7015], 1),
72: ThermalTuple('u-un', [92238], 1),
75: ThermalTuple('uuo2', [8016, 8017, 8018], 1),
}
def _get_thermal_data(ev, mat):
"""Return appropriate ThermalTuple, accounting for bugs."""
# JEFF assigns MAT=59 to Ca in CaH2 (which is supposed to be silicon).
if ev.info['library'][0] == 'JEFF':
if ev.material == 59:
if 'CaH2' in ''.join(ev.info['description']):
zaids = [20040, 20042, 20043, 20044, 20046, 20048]
return ThermalTuple('cacah2', zaids, 1)
# Before ENDF/B-VIII.0, crystalline graphite was MAT=31
if ev.info['library'] != ('ENDF/B', 8, 0):
if ev.material == 31:
return _THERMAL_DATA[30]
# ENDF/B incorrectly assigns MAT numbers for UO2
#
# Material | ENDF Manual | VII.0 | VII.1 | VIII.0
# ---------|-------------|-------|-------|-------
# O in UO2 | 45 | 75 | 75 | 75
# U in UO2 | 75 | 76 | 48 | 48
if ev.info['library'][0] == 'ENDF/B':
if ev.material == 75:
return _THERMAL_DATA[45]
version = ev.info['library'][1:]
if version in ((7, 1), (8, 0)) and ev.material == 48:
return _THERMAL_DATA[75]
if version == (7, 0) and ev.material == 76:
return _THERMAL_DATA[75]
# If not a problematic material, use the dictionary as is
return _THERMAL_DATA[mat]
_TEMPLATE_RECONR = """
reconr / %%%%%%%%%%%%%%%%%%% Reconstruct XS for neutrons %%%%%%%%%%%%%%%%%%%%%%%
{nendf} {npendf}
'{library} PENDF for {zsymam}'/
{mat} 2/
{error}/ err
'{library}: {zsymam}'/
'Processed by NJOY'/
0/
"""
_TEMPLATE_BROADR = """
broadr / %%%%%%%%%%%%%%%%%%%%%%% Doppler broaden XS %%%%%%%%%%%%%%%%%%%%%%%%%%%%
{nendf} {npendf} {nbroadr}
{mat} {num_temp} 0 0 0. /
{error}/ errthn
{temps}
0/
"""
_TEMPLATE_HEATR = """
heatr / %%%%%%%%%%%%%%%%%%%%%%%%% Add heating kerma %%%%%%%%%%%%%%%%%%%%%%%%%%%%
{nendf} {nheatr_in} {nheatr} /
{mat} 4 0 0 0 /
302 318 402 444 /
"""
_TEMPLATE_HEATR_LOCAL = """
heatr / %%%%%%%%%%%%%%%%% Add heating kerma (local photons) %%%%%%%%%%%%%%%%%%%%
{nendf} {nheatr_in} {nheatr_local} /
{mat} 4 0 0 1 /
302 318 402 444 /
"""
_TEMPLATE_GASPR = """
gaspr / %%%%%%%%%%%%%%%%%%%%%%%%% Add gas production %%%%%%%%%%%%%%%%%%%%%%%%%%%
{nendf} {ngaspr_in} {ngaspr} /
"""
_TEMPLATE_PURR = """
purr / %%%%%%%%%%%%%%%%%%%%%%%% Add probability tables %%%%%%%%%%%%%%%%%%%%%%%%%
{nendf} {npurr_in} {npurr} /
{mat} {num_temp} 1 20 64 /
{temps}
1.e10
0/
"""
_TEMPLATE_ACER = """
acer / %%%%%%%%%%%%%%%%%%%%%%%% Write out in ACE format %%%%%%%%%%%%%%%%%%%%%%%%
{nendf} {nacer_in} 0 {nace} {ndir}
1 0 1 .{ext} /
'{library}: {zsymam} at {temperature}'/
{mat} {temperature}
1 1/
/
"""
_THERMAL_TEMPLATE_THERMR = """
thermr / %%%%%%%%%%%%%%%% Add thermal scattering data (free gas) %%%%%%%%%%%%%%%
0 {nthermr1_in} {nthermr1}
0 {mat} 12 {num_temp} 1 0 {iform} 1 221 1/
{temps}
{error} {energy_max}
thermr / %%%%%%%%%%%%%%%% Add thermal scattering data (bound) %%%%%%%%%%%%%%%%%%
{nthermal_endf} {nthermr2_in} {nthermr2}
{mat_thermal} {mat} 16 {num_temp} {inelastic} {elastic} {iform} {natom} 222 1/
{temps}
{error} {energy_max}
"""
_THERMAL_TEMPLATE_ACER = """
acer / %%%%%%%%%%%%%%%%%%%%%%%% Write out in ACE format %%%%%%%%%%%%%%%%%%%%%%%%
{nendf} {nthermal_acer_in} 0 {nace} {ndir}
2 0 1 .{ext}/
'{library}: {zsymam_thermal} processed by NJOY'/
{mat} {temperature} '{data.name}' /
{zaids} /
222 64 {mt_elastic} {elastic_type} {data.nmix} {energy_max} {iwt}/
"""
def run(commands, tapein, tapeout, input_filename=None, stdout=False,
njoy_exec='njoy'):
"""Run NJOY with given commands
Parameters
----------
commands : str
Input commands for NJOY
tapein : dict
Dictionary mapping tape numbers to paths for any input files
tapeout : dict
Dictionary mapping tape numbers to paths for any output files
input_filename : str, optional
File name to write out NJOY input commands
stdout : bool, optional
Whether to display output when running NJOY
njoy_exec : str, optional
Path to NJOY executable
Raises
------
subprocess.CalledProcessError
If the NJOY process returns with a non-zero status
"""
if input_filename is not None:
with open(str(input_filename), 'w') as f:
f.write(commands)
with tempfile.TemporaryDirectory() as tmpdir:
# Copy evaluations to appropriates 'tapes'
for tape_num, filename in tapein.items():
tmpfilename = os.path.join(tmpdir, 'tape{}'.format(tape_num))
shutil.copy(str(filename), tmpfilename)
# Start up NJOY process
njoy = Popen([njoy_exec], cwd=tmpdir, stdin=PIPE, stdout=PIPE,
stderr=STDOUT, universal_newlines=True)
njoy.stdin.write(commands)
njoy.stdin.flush()
lines = []
while True:
# If process is finished, break loop
line = njoy.stdout.readline()
if not line and njoy.poll() is not None:
break
lines.append(line)
if stdout:
# If user requested output, print to screen
print(line, end='')
# Check for error
if njoy.returncode != 0:
raise CalledProcessError(njoy.returncode, njoy_exec,
''.join(lines))
# Copy output files back to original directory
for tape_num, filename in tapeout.items():
tmpfilename = os.path.join(tmpdir, 'tape{}'.format(tape_num))
if os.path.isfile(tmpfilename):
shutil.move(tmpfilename, str(filename))
def make_pendf(filename, pendf='pendf', error=0.001, stdout=False):
"""Generate pointwise ENDF file from an ENDF file
Parameters
----------
filename : str
Path to ENDF file
pendf : str, optional
Path of pointwise ENDF file to write
error : float, optional
Fractional error tolerance for NJOY processing
stdout : bool
Whether to display NJOY standard output
Raises
------
subprocess.CalledProcessError
If the NJOY process returns with a non-zero status
"""
make_ace(filename, pendf=pendf, error=error, broadr=False,
heatr=False, purr=False, acer=False, stdout=stdout)
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):
"""Generate incident neutron ACE file from an ENDF file
File names can be passed to
``[acer, xsdir, pendf, broadr, heatr, gaspr, purr]``
to specify the exact output for the given module.
Otherwise, the files will be writen to the current directory
or directory specified by ``output_dir``. Default file
names mirror the variable names, e.g. ``heatr`` output
will be written to a file named ``heatr`` unless otherwise
specified.
Parameters
----------
filename : str
Path to ENDF file
temperatures : iterable of float, optional
Temperatures in Kelvin to produce ACE files at. If omitted, data is
produced at room temperature (293.6 K).
acer : bool or str, optional
Flag indicating if acer should be run. If a string is give, write the
resulting ``ace`` file to this location. Path of ACE file to write.
Defaults to ``"ace"``
xsdir : str, optional
Path of xsdir file to write. Defaults to ``"xsdir"`` in the same
directory as ``acer``
output_dir : str, optional
Directory to write output for requested modules. If not provided
and at least one of ``[pendf, broadr, heatr, gaspr, purr, acer]``
is ``True``, then write output files to current directory. If given,
must be a path to a directory.
pendf : str, optional
Path of pendf file to write. If omitted, the pendf file is not saved.
error : float, optional
Fractional error tolerance for NJOY processing
broadr : bool or str, optional
Indicating whether to Doppler broaden XS when running NJOY. If string,
write the output tape to this file.
heatr : bool or str, optional
Indicating whether to add heating kerma when running NJOY. If string,
write the output tape to this file.
gaspr : bool or str, optional
Indicating whether to add gas production data when running NJOY.
If string, write the output tape to this file.
purr : bool or str, optional
Indicating whether to add probability table when running NJOY.
If string, write the output tape to this file.
evaluation : openmc.data.endf.Evaluation, optional
If the ENDF file contains multiple material evaluations, this argument
indicates which evaluation should be used.
**kwargs
Keyword arguments passed to :func:`openmc.data.njoy.run`
Raises
------
subprocess.CalledProcessError
If the NJOY process returns with a non-zero status
IOError
If ``output_dir`` does not point to a directory
"""
if output_dir is None:
output_dir = Path()
else:
output_dir = Path(output_dir)
if not output_dir.is_dir():
raise IOError("{} is not a directory".format(output_dir))
ev = evaluation if evaluation is not None else endf.Evaluation(filename)
mat = ev.material
zsymam = ev.target['zsymam']
# Determine name of library
library = '{}-{}.{}'.format(*ev.info['library'])
if temperatures is None:
temperatures = [293.6]
num_temp = len(temperatures)
temps = ' '.join(str(i) for i in temperatures)
# Create njoy commands by modules
commands = ""
nendf, npendf = 20, 21
tapein = {nendf: filename}
tapeout = {}
if pendf:
tapeout[npendf] = (output_dir / "pendf") if pendf is True else pendf
# reconr
commands += _TEMPLATE_RECONR
nlast = npendf
# broadr
if broadr:
nbroadr = nlast + 1
tapeout[nbroadr] = (output_dir / "broadr") if broadr is True else broadr
commands += _TEMPLATE_BROADR
nlast = nbroadr
# heatr
if heatr:
nheatr_in = nlast
nheatr_local = nheatr_in + 1
tapeout[nheatr_local] = (output_dir / "heatr_local") if heatr is True \
else heatr + '_local'
commands += _TEMPLATE_HEATR_LOCAL
nheatr = nheatr_local + 1
tapeout[nheatr] = (output_dir / "heatr") if heatr is True else heatr
commands += _TEMPLATE_HEATR
nlast = nheatr
# gaspr
if gaspr:
ngaspr_in = nlast
ngaspr = ngaspr_in + 1
tapeout[ngaspr] = (output_dir / "gaspr") if gaspr is True else gaspr
commands += _TEMPLATE_GASPR
nlast = ngaspr
# purr
if purr:
npurr_in = nlast
npurr = npurr_in + 1
tapeout[npurr] = (output_dir / "purr") if purr is True else purr
commands += _TEMPLATE_PURR
nlast = npurr
commands = commands.format(**locals())
# acer
if acer:
nacer_in = nlast
fname = '{}_{:.1f}'
for i, temperature in enumerate(temperatures):
# Extend input with an ACER run for each temperature
nace = nacer_in + 1 + 2*i
ndir = nace + 1
ext = '{:02}'.format(i + 1)
commands += _TEMPLATE_ACER.format(**locals())
# Indicate tapes to save for each ACER run
tapeout[nace] = fname.format("ace", temperature)
tapeout[ndir] = fname.format("xsdir", temperature)
commands += 'stop\n'
run(commands, tapein, tapeout, **kwargs)
if acer:
ace = (output_dir / "ace") if acer is True else Path(acer)
xsdir = (ace.parent / "xsdir") if xsdir is None else xsdir
with ace.open('w') as ace_file, xsdir.open('w') as xsdir_file:
for temperature in temperatures:
# Get contents of ACE file
text = open(fname.format("ace", temperature), 'r').read()
# If the target is metastable, make sure that ZAID in the ACE
# file reflects this by adding 400
if ev.target['isomeric_state'] > 0:
mass_first_digit = int(text[3])
if mass_first_digit <= 2:
text = text[:3] + str(mass_first_digit + 4) + text[4:]
# Concatenate into destination ACE file
ace_file.write(text)
# Concatenate into destination xsdir file
text = open(fname.format("xsdir", temperature), 'r').read()
xsdir_file.write(text)
# Remove ACE/xsdir files for each temperature
for temperature in temperatures:
os.remove(fname.format("ace", temperature))
os.remove(fname.format("xsdir", temperature))
def make_ace_thermal(filename, filename_thermal, temperatures=None,
ace='ace', xsdir='xsdir', error=0.001, iwt=2,
evaluation=None, evaluation_thermal=None, **kwargs):
"""Generate thermal scattering ACE file from ENDF files
Parameters
----------
filename : str
Path to ENDF neutron sublibrary file
filename_thermal : str
Path to ENDF thermal scattering sublibrary file
temperatures : iterable of float, optional
Temperatures in Kelvin to produce data at. If omitted, data is produced
at all temperatures given in the ENDF thermal scattering sublibrary.
ace : str, optional
Path of ACE file to write
xsdir : str, optional
Path of xsdir file to write
error : float, optional
Fractional error tolerance for NJOY processing
iwt : int
`iwt` parameter used in NJOR/ACER card 9
evaluation : openmc.data.endf.Evaluation, optional
If the ENDF neutron sublibrary file contains multiple material
evaluations, this argument indicates which evaluation to use.
evaluation_thermal : openmc.data.endf.Evaluation, optional
If the ENDF thermal scattering sublibrary file contains multiple
material evaluations, this argument indicates which evaluation to use.
**kwargs
Keyword arguments passed to :func:`openmc.data.njoy.run`
Raises
------
subprocess.CalledProcessError
If the NJOY process returns with a non-zero status
"""
ev = evaluation if evaluation is not None else endf.Evaluation(filename)
mat = ev.material
zsymam = ev.target['zsymam']
ev_thermal = (evaluation_thermal if evaluation_thermal is not None
else endf.Evaluation(filename_thermal))
mat_thermal = ev_thermal.material
zsymam_thermal = ev_thermal.target['zsymam']
# Determine name, isotopes based on MAT number
data = _get_thermal_data(ev_thermal, mat_thermal)
zaids = ' '.join(str(zaid) for zaid in data.zaids[:3])
# Determine name of library
library = '{}-{}.{}'.format(*ev_thermal.info['library'])
# Determine if thermal elastic is present
if (7, 2) in ev_thermal.section:
elastic = 1
mt_elastic = 223
# Determine whether elastic is incoherent (0) or coherent (1)
file_obj = StringIO(ev_thermal.section[7, 2])
elastic_type = endf.get_head_record(file_obj)[2] - 1
else:
elastic = 0
mt_elastic = 0
elastic_type = 0
# Determine number of principal atoms
file_obj = StringIO(ev_thermal.section[7, 4])
items = endf.get_head_record(file_obj)
items, values = endf.get_list_record(file_obj)
energy_max = values[3]
natom = int(values[5])
# Note that the 'iform' parameter is omitted in NJOY 99. We assume that the
# user is using NJOY 2012 or later.
iform = 0
inelastic = 2
# Determine temperatures from MF=7, MT=4 if none were specified
if temperatures is None:
file_obj = StringIO(ev_thermal.section[7, 4])
endf.get_head_record(file_obj)
endf.get_list_record(file_obj)
endf.get_tab2_record(file_obj)
params = endf.get_tab1_record(file_obj)[0]
temperatures = [params[0]]
for i in range(params[2]):
temperatures.append(endf.get_list_record(file_obj)[0][0])
num_temp = len(temperatures)
temps = ' '.join(str(i) for i in temperatures)
# Create njoy commands by modules
commands = ""
nendf, nthermal_endf, npendf = 20, 21, 22
tapein = {nendf: filename, nthermal_endf: filename_thermal}
tapeout = {}
# reconr
commands += _TEMPLATE_RECONR
nlast = npendf
# broadr
nbroadr = nlast + 1
commands += _TEMPLATE_BROADR
nlast = nbroadr
# thermr
nthermr1_in = nlast
nthermr1 = nthermr1_in + 1
nthermr2_in = nthermr1
nthermr2 = nthermr2_in + 1
commands += _THERMAL_TEMPLATE_THERMR
nlast = nthermr2
commands = commands.format(**locals())
# acer
nthermal_acer_in = nlast
fname = '{}_{:.1f}'
for i, temperature in enumerate(temperatures):
# Extend input with an ACER run for each temperature
nace = nthermal_acer_in + 1 + 2*i
ndir = nace + 1
ext = '{:02}'.format(i + 1)
commands += _THERMAL_TEMPLATE_ACER.format(**locals())
# Indicate tapes to save for each ACER run
tapeout[nace] = fname.format(ace, temperature)
tapeout[ndir] = fname.format(xsdir, temperature)
commands += 'stop\n'
run(commands, tapein, tapeout, **kwargs)
with open(ace, 'w') as ace_file, open(xsdir, 'w') as xsdir_file:
# Concatenate ACE and xsdir files together
for temperature in temperatures:
text = open(fname.format(ace, temperature), 'r').read()
ace_file.write(text)
text = open(fname.format(xsdir, temperature), 'r').read()
xsdir_file.write(text)
# Remove ACE/xsdir files for each temperature
for temperature in temperatures:
os.remove(fname.format(ace, temperature))
os.remove(fname.format(xsdir, temperature))

1306
openmc/data/photon.py Normal file

File diff suppressed because it is too large Load diff

187
openmc/data/product.py Normal file
View file

@ -0,0 +1,187 @@
from collections.abc import Iterable
from io import StringIO
from numbers import Real
import sys
import numpy as np
import openmc.checkvalue as cv
from openmc.mixin import EqualityMixin
from .angle_energy import AngleEnergy
from .function import Tabulated1D, Polynomial, Function1D
class Product(EqualityMixin):
"""Secondary particle emitted in a nuclear reaction
Parameters
----------
particle : str, optional
What particle the reaction product is. Defaults to 'neutron'.
Attributes
----------
applicability : Iterable of openmc.data.Tabulated1D
Probability of sampling a given distribution for this product.
decay_rate : float
Decay rate in inverse seconds
distribution : Iterable of openmc.data.AngleEnergy
Distributions of energy and angle of product.
emission_mode : {'prompt', 'delayed', 'total'}
Indicate whether the particle is emitted immediately or whether it
results from the decay of reaction product (e.g., neutron emitted from a
delayed neutron precursor). A special value of 'total' is used when the
yield represents particles from prompt and delayed sources.
particle : str
What particle the reaction product is.
yield_ : openmc.data.Function1D
Yield of secondary particle in the reaction.
"""
def __init__(self, particle='neutron'):
self.particle = particle
self.decay_rate = 0.0
self.emission_mode = 'prompt'
self.distribution = []
self.applicability = []
self.yield_ = Polynomial((1,)) # 0-order polynomial i.e. a constant
def __repr__(self):
if isinstance(self.yield_, Real):
return "<Product: {}, emission={}, yield={}>".format(
self.particle, self.emission_mode, self.yield_)
elif isinstance(self.yield_, Tabulated1D):
if np.all(self.yield_.y == self.yield_.y[0]):
return "<Product: {}, emission={}, yield={}>".format(
self.particle, self.emission_mode, self.yield_.y[0])
else:
return "<Product: {}, emission={}, yield=tabulated>".format(
self.particle, self.emission_mode)
else:
return "<Product: {}, emission={}, yield=polynomial>".format(
self.particle, self.emission_mode)
@property
def applicability(self):
return self._applicability
@property
def decay_rate(self):
return self._decay_rate
@property
def distribution(self):
return self._distribution
@property
def emission_mode(self):
return self._emission_mode
@property
def particle(self):
return self._particle
@property
def yield_(self):
return self._yield
@applicability.setter
def applicability(self, applicability):
cv.check_type('product distribution applicability', applicability,
Iterable, Tabulated1D)
self._applicability = applicability
@decay_rate.setter
def decay_rate(self, decay_rate):
cv.check_type('product decay rate', decay_rate, Real)
cv.check_greater_than('product decay rate', decay_rate, 0.0, True)
self._decay_rate = decay_rate
@distribution.setter
def distribution(self, distribution):
cv.check_type('product angle-energy distribution', distribution,
Iterable, AngleEnergy)
self._distribution = distribution
@emission_mode.setter
def emission_mode(self, emission_mode):
cv.check_value('product emission mode', emission_mode,
('prompt', 'delayed', 'total'))
self._emission_mode = emission_mode
@particle.setter
def particle(self, particle):
cv.check_type('product particle type', particle, str)
self._particle = particle
@yield_.setter
def yield_(self, yield_):
cv.check_type('product yield', yield_, Function1D)
self._yield = yield_
def to_hdf5(self, group):
"""Write product to an HDF5 group
Parameters
----------
group : h5py.Group
HDF5 group to write to
"""
group.attrs['particle'] = np.string_(self.particle)
group.attrs['emission_mode'] = np.string_(self.emission_mode)
if self.decay_rate > 0.0:
group.attrs['decay_rate'] = self.decay_rate
# Write yield
self.yield_.to_hdf5(group, 'yield')
# Write applicability/distribution
group.attrs['n_distribution'] = len(self.distribution)
for i, d in enumerate(self.distribution):
dgroup = group.create_group('distribution_{}'.format(i))
if self.applicability:
self.applicability[i].to_hdf5(dgroup, 'applicability')
d.to_hdf5(dgroup)
@classmethod
def from_hdf5(cls, group):
"""Generate reaction product from HDF5 data
Parameters
----------
group : h5py.Group
HDF5 group to read from
Returns
-------
openmc.data.Product
Reaction product
"""
particle = group.attrs['particle'].decode()
p = cls(particle)
p.emission_mode = group.attrs['emission_mode'].decode()
if 'decay_rate' in group.attrs:
p.decay_rate = group.attrs['decay_rate']
# Read yield
p.yield_ = Function1D.from_hdf5(group['yield'])
# Read applicability/distribution
n_distribution = group.attrs['n_distribution']
distribution = []
applicability = []
for i in range(n_distribution):
dgroup = group['distribution_{}'.format(i)]
if 'applicability' in dgroup:
applicability.append(Tabulated1D.from_hdf5(
dgroup['applicability']))
distribution.append(AngleEnergy.from_hdf5(dgroup))
p.distribution = distribution
p.applicability = applicability
return p

1189
openmc/data/reaction.py Normal file

File diff suppressed because it is too large Load diff

522
openmc/data/reconstruct.pyx Normal file
View file

@ -0,0 +1,522 @@
from libc.stdlib cimport malloc, calloc, free
from libc.math cimport cos, sin, sqrt, atan, M_PI
cimport numpy as np
import numpy as np
from numpy.linalg import inv
cimport cython
cdef extern from "complex.h":
double cabs(double complex)
double complex conj(double complex)
double creal(complex double)
double cimag(complex double)
double complex cexp(double complex)
# Physical constants are from CODATA 2014
cdef double NEUTRON_MASS_ENERGY = 939.5654133e6 # eV/c^2
cdef double HBAR_C = 197.3269788e5 # eV-b^0.5
@cython.cdivision(True)
def wave_number(double A, double E):
r"""Neutron wave number in center-of-mass system.
ENDF-102 defines the neutron wave number in the center-of-mass system in
Equation D.10 as
.. math::
k = \frac{2m_n}{\hbar} \frac{A}{A + 1} \sqrt{|E|}
Parameters
----------
A : double
Ratio of target mass to neutron mass
E : double
Energy in eV
Returns
-------
double
Neutron wave number in b^-0.5
"""
return A/(A + 1)*sqrt(2*NEUTRON_MASS_ENERGY*abs(E))/HBAR_C
@cython.cdivision(True)
cdef double _wave_number(double A, double E):
return A/(A + 1)*sqrt(2*NEUTRON_MASS_ENERGY*abs(E))/HBAR_C
@cython.cdivision(True)
cdef double phaseshift(int l, double rho):
"""Calculate hardsphere phase shift as given in ENDF-102, Equation D.13
Parameters
----------
l : int
Angular momentum quantum number
rho : float
Product of the wave number and the channel radius
Returns
-------
double
Hardsphere phase shift
"""
if l == 0:
return rho
elif l == 1:
return rho - atan(rho)
elif l == 2:
return rho - atan(3*rho/(3 - rho**2))
elif l == 3:
return rho - atan((15*rho - rho**3)/(15 - 6*rho**2))
elif l == 4:
return rho - atan((105*rho - 10*rho**3)/(105 - 45*rho**2 + rho**4))
@cython.cdivision(True)
def penetration_shift(int l, double rho):
r"""Calculate shift and penetration factors as given in ENDF-102, Equations D.11
and D.12.
Parameters
----------
l : int
Angular momentum quantum number
rho : float
Product of the wave number and the channel radius
Returns
-------
double
Penetration factor for given :math:`l`
double
Shift factor for given :math:`l`
"""
cdef double den
if l == 0:
return rho, 0.
elif l == 1:
den = 1 + rho**2
return rho**3/den, -1/den
elif l == 2:
den = 9 + 3*rho**2 + rho**4
return rho**5/den, -(18 + 3*rho**2)/den
elif l == 3:
den = 225 + 45*rho**2 + 6*rho**4 + rho**6
return rho**7/den, -(675 + 90*rho**2 + 6*rho**4)/den
elif l == 4:
den = 11025 + 1575*rho**2 + 135*rho**4 + 10*rho**6 + rho**8
return rho**9/den, -(44100 + 4725*rho**2 + 270*rho**4 + 10*rho**6)/den
@cython.boundscheck(False)
@cython.wraparound(False)
@cython.cdivision(True)
def reconstruct_mlbw(mlbw, double E):
"""Evaluate cross section using MLBW data.
Parameters
----------
mlbw : openmc.data.MultiLevelBreitWigner
Multi-level Breit-Wigner resonance parameters
E : double
Energy in eV at which to evaluate the cross section
Returns
-------
elastic : double
Elastic scattering cross section in barns
capture : double
Radiative capture cross section in barns
fission : double
Fission cross section in barns
"""
cdef int i, nJ, ij, l, n_res, i_res
cdef double elastic, capture, fission
cdef double A, k, rho, rhohat, I
cdef double P, S, phi, cos2phi, sin2phi
cdef double Ex, Q, rhoc, rhochat, P_c, S_c
cdef double jmin, jmax, j, Dl
cdef double E_r, gt, gn, gg, gf, gx, P_r, S_r, P_rx
cdef double gnE, gtE, Eprime, x, f
cdef double *g
cdef double (*s)[2]
cdef double [:,:] params
I = mlbw.target_spin
A = mlbw.atomic_weight_ratio
k = _wave_number(A, E)
elastic = 0.
capture = 0.
fission = 0.
for i, l in enumerate(mlbw._l_values):
params = mlbw._parameter_matrix[l]
rho = k*mlbw.channel_radius[l](E)
rhohat = k*mlbw.scattering_radius[l](E)
P, S = penetration_shift(l, rho)
phi = phaseshift(l, rhohat)
cos2phi = cos(2*phi)
sin2phi = sin(2*phi)
# Determine shift and penetration at modified energy
if mlbw._competitive[i]:
Ex = E + mlbw.q_value[l]*(A + 1)/A
rhoc = mlbw.channel_radius[l](Ex)
rhochat = mlbw.scattering_radius[l](Ex)
P_c, S_c = penetration_shift(l, rhoc)
if Ex < 0:
P_c = 0
# Determine range of total angular momentum values based on equation
# 41 in LA-UR-12-27079
jmin = abs(abs(I - l) - 0.5)
jmax = I + l + 0.5
nJ = int(jmax - jmin + 1)
# Determine Dl factor using Equation 43 in LA-UR-12-27079
Dl = 2*l + 1
g = <double *> malloc(nJ*sizeof(double))
for ij in range(nJ):
j = jmin + ij
g[ij] = (2*j + 1)/(4*I + 2)
Dl -= g[ij]
s = <double (*)[2]> calloc(2*nJ, sizeof(double))
for i_res in range(params.shape[0]):
# Copy resonance parameters
E_r = params[i_res, 0]
j = params[i_res, 2]
ij = int(j - jmin)
gt = params[i_res, 3]
gn = params[i_res, 4]
gg = params[i_res, 5]
gf = params[i_res, 6]
gx = params[i_res, 7]
P_r = params[i_res, 8]
S_r = params[i_res, 9]
P_rx = params[i_res, 10]
# Calculate neutron and total width at energy E
gnE = P*gn/P_r # ENDF-102, Equation D.7
gtE = gnE + gg + gf
if gx > 0:
gtE += gx*P_c/P_rx
Eprime = E_r + (S_r - S)/(2*P_r)*gn # ENDF-102, Equation D.9
x = 2*(E - Eprime)/gtE # LA-UR-12-27079, Equation 26
f = 2*gnE/(gtE*(1 + x*x)) # Common factor in Equation 40
s[ij][0] += f # First sum in Equation 40
s[ij][1] += f*x # Second sum in Equation 40
capture += f*g[ij]*gg/gtE
if gf > 0:
fission += f*g[ij]*gf/gtE
for ij in range(nJ):
# Add all but last term of LA-UR-12-27079, Equation 40
elastic += g[ij]*((1 - cos2phi - s[ij][0])**2 +
(sin2phi + s[ij][1])**2)
# Add final term with Dl from Equation 40
elastic += 2*Dl*(1 - cos2phi)
# Free memory
free(g)
free(s)
capture *= 2*M_PI/(k*k)
fission *= 2*M_PI/(k*k)
elastic *= M_PI/(k*k)
return (elastic, capture, fission)
@cython.boundscheck(False)
@cython.wraparound(False)
@cython.cdivision(True)
def reconstruct_slbw(slbw, double E):
"""Evaluate cross section using SLBW data.
Parameters
----------
slbw : openmc.data.SingleLevelBreitWigner
Single-level Breit-Wigner resonance parameters
E : double
Energy in eV at which to evaluate the cross section
Returns
-------
elastic : double
Elastic scattering cross section in barns
capture : double
Radiative capture cross section in barns
fission : double
Fission cross section in barns
"""
cdef int i, l, i_res
cdef double elastic, capture, fission
cdef double A, k, rho, rhohat, I
cdef double P, S, phi, cos2phi, sin2phi, sinphi2
cdef double Ex, rhoc, rhochat, P_c, S_c
cdef double E_r, J, gt, gn, gg, gf, gx, P_r, S_r, P_rx
cdef double gnE, gtE, Eprime, f
cdef double x, theta, psi, chi
cdef double [:,:] params
I = slbw.target_spin
A = slbw.atomic_weight_ratio
k = _wave_number(A, E)
elastic = 0.
capture = 0.
fission = 0.
for i, l in enumerate(slbw._l_values):
params = slbw._parameter_matrix[l]
rho = k*slbw.channel_radius[l](E)
rhohat = k*slbw.scattering_radius[l](E)
P, S = penetration_shift(l, rho)
phi = phaseshift(l, rhohat)
cos2phi = cos(2*phi)
sin2phi = sin(2*phi)
sinphi2 = sin(phi)**2
# Add potential scattering -- first term in ENDF-102, Equation D.2
elastic += 4*M_PI/(k*k)*(2*l + 1)*sinphi2
# Determine shift and penetration at modified energy
if slbw._competitive[i]:
Ex = E + slbw.q_value[l]*(A + 1)/A
rhoc = slbw.channel_radius[l](Ex)
rhochat = slbw.scattering_radius[l](Ex)
P_c, S_c = penetration_shift(l, rhoc)
if Ex < 0:
P_c = 0
for i_res in range(params.shape[0]):
# Copy resonance parameters
E_r = params[i_res, 0]
J = params[i_res, 2]
gt = params[i_res, 3]
gn = params[i_res, 4]
gg = params[i_res, 5]
gf = params[i_res, 6]
gx = params[i_res, 7]
P_r = params[i_res, 8]
S_r = params[i_res, 9]
P_rx = params[i_res, 10]
# Calculate neutron and total width at energy E
gnE = P*gn/P_r # Equation D.7
gtE = gnE + gg + gf
if gx > 0:
gtE += gx*P_c/P_rx
Eprime = E_r + (S_r - S)/(2*P_r)*gn # Equation D.9
gJ = (2*J + 1)/(4*I + 2) # Mentioned in section D.1.1.4
# Calculate common factor for elastic, capture, and fission
# cross sections
f = M_PI/(k*k)*gJ*gnE/((E - Eprime)**2 + gtE**2/4)
# Add contribution to elastic per Equation D.2
elastic += f*(gnE*cos2phi - 2*(gg + gf)*sinphi2
+ 2*(E - Eprime)*sin2phi)
# Add contribution to capture per Equation D.3
capture += f*gg
# Add contribution to fission per Equation D.6
if gf > 0:
fission += f*gf
return (elastic, capture, fission)
@cython.boundscheck(False)
@cython.wraparound(False)
@cython.cdivision(True)
def reconstruct_rm(rm, double E):
"""Evaluate cross section using Reich-Moore data.
Parameters
----------
rm : openmc.data.ReichMoore
Reich-Moore resonance parameters
E : double
Energy in eV at which to evaluate the cross section
Returns
-------
elastic : double
Elastic scattering cross section in barns
capture : double
Radiative capture cross section in barns
fission : double
Fission cross section in barns
"""
cdef int i, l, m, n, i_res
cdef int i_s, num_s, i_J, num_J
cdef double elastic, capture, fission, total
cdef double A, k, rho, rhohat, I
cdef double P, S, phi
cdef double smin, smax, s, Jmin, Jmax, J, j
cdef double E_r, gn, gg, gfa, gfb, P_r
cdef double E_diff, abs_value, gJ
cdef double Kr, Ki, x
cdef double complex Ubar, U_, factor
cdef bint hasfission
cdef np.ndarray[double, ndim=2] one
cdef np.ndarray[double complex, ndim=2] K, Imat, U
cdef double [:,:] params
# Get nuclear spin
I = rm.target_spin
elastic = 0.
fission = 0.
total = 0.
A = rm.atomic_weight_ratio
k = _wave_number(A, E)
one = np.eye(3)
K = np.zeros((3,3), dtype=complex)
for i, l in enumerate(rm._l_values):
# Check for l-dependent scattering radius
rho = k*rm.channel_radius[l](E)
rhohat = k*rm.scattering_radius[l](E)
# Calculate shift and penetrability
P, S = penetration_shift(l, rho)
# Calculate phase shift
phi = phaseshift(l, rhohat)
# Calculate common factor on collision matrix terms (term outside curly
# braces in ENDF-102, Eq. D.27)
Ubar = cexp(-2j*phi)
# The channel spin is the vector sum of the target spin, I, and the
# neutron spin, 1/2, so can take on values of |I - 1/2| < s < I + 1/2
smin = abs(I - 0.5)
smax = I + 0.5
num_s = int(smax - smin + 1)
for i_s in range(num_s):
s = i_s + smin
# Total angular momentum is the vector sum of l and s and can assume
# values between |l - s| < J < l + s
Jmin = abs(l - s)
Jmax = l + s
num_J = int(Jmax - Jmin + 1)
for i_J in range(num_J):
J = i_J + Jmin
# Initialize K matrix
for m in range(3):
for n in range(3):
K[m,n] = 0.0
hasfission = False
if (l, J) in rm._parameter_matrix:
params = rm._parameter_matrix[l, J]
for i_res in range(params.shape[0]):
# Sometimes, the same (l, J) quantum numbers can occur
# for different values of the channel spin, s. In this
# case, the sign of the channel spin indicates which
# spin is to be used. If the spin is negative assume
# this resonance comes from the I - 1/2 channel and vice
# versa.
j = params[i_res, 2]
if l > 0:
if (j < 0 and s != smin) or (j > 0 and s != smax):
continue
# Copy resonance parameters
E_r = params[i_res, 0]
gn = params[i_res, 3]
gg = params[i_res, 4]
gfa = params[i_res, 5]
gfb = params[i_res, 6]
P_r = params[i_res, 7]
# Calculate neutron width at energy E
gn = sqrt(P*gn/P_r)
# Calculate j/2 * inverse of denominator of K matrix terms
factor = 0.5j/(E_r - E - 0.5j*gg)
# Upper triangular portion of K matrix -- see ENDF-102,
# Equation D.28
K[0,0] = K[0,0] + gn*gn*factor
if gfa != 0.0 or gfb != 0.0:
# Negate fission widths if necessary
gfa = (-1 if gfa < 0 else 1)*sqrt(abs(gfa))
gfb = (-1 if gfb < 0 else 1)*sqrt(abs(gfb))
K[0,1] = K[0,1] + gn*gfa*factor
K[0,2] = K[0,2] + gn*gfb*factor
K[1,1] = K[1,1] + gfa*gfa*factor
K[1,2] = K[1,2] + gfa*gfb*factor
K[2,2] = K[2,2] + gfb*gfb*factor
hasfission = True
# Get collision matrix
gJ = (2*J + 1)/(4*I + 2)
if hasfission:
# Copy upper triangular portion of K to lower triangular
K[1,0] = K[0,1]
K[2,0] = K[0,2]
K[2,1] = K[1,2]
Imat = inv(one - K)
U = Ubar*(2*Imat - one) # ENDF-102, Eq. D.27
elastic += gJ*cabs(1 - U[0,0])**2 # ENDF-102, Eq. D.24
total += 2*gJ*(1 - creal(U[0,0])) # ENDF-102, Eq. D.23
# Calculate fission from ENDF-102, Eq. D.26
fission += 4*gJ*(cabs(Imat[1,0])**2 + cabs(Imat[2,0])**2)
else:
U_ = Ubar*(2/(1 - K[0,0]) - 1)
if abs(creal(K[0,0])) < 3e-4 and abs(phi) < 3e-4:
# If K and phi are both very small, the calculated cross
# sections can lose precision because the real part of U
# ends up very close to unity. To get around this, we
# use Euler's formula to express Ubar by real and
# imaginary parts, expand cos(2phi) = 1 - 2phi^2 +
# O(phi^4), and then simplify
Kr = creal(K[0,0])
Ki = cimag(K[0,0])
x = 2*(-Kr + (Kr*Kr + Ki*Ki)*(1 - phi*phi) + phi*phi -
sin(2*phi)*Ki)/((1 - Kr)*(1 - Kr) + Ki*Ki)
total += 2*gJ*x
elastic += gJ*(x*x + cimag(U_)**2)
else:
total += 2*gJ*(1 - creal(U_)) # ENDF-102, Eq. D.23
elastic += gJ*cabs(1 - U_)**2 # ENDF-102, Eq. D.24
# Calculate capture as difference of other cross sections as per ENDF-102,
# Equation D.25
capture = total - elastic - fission
elastic *= M_PI/(k*k)
capture *= M_PI/(k*k)
fission *= M_PI/(k*k)
return (elastic, capture, fission)

1071
openmc/data/resonance.py Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,708 @@
from collections.abc import MutableSequence
import warnings
import io
import copy
import numpy as np
import pandas as pd
from . import endf
import openmc.checkvalue as cv
from .resonance import Resonances
def _add_file2_contributions(file32params, file2params):
"""Function for aiding in adding resonance parameters from File 2 that are
not always present in File 32. Uses already imported resonance data.
Paramaters
----------
file32params : pandas.Dataframe
Incomplete set of resonance parameters contained in File 32.
file2params : pandas.Dataframe
Resonance parameters from File 2. Ordered by energy.
Returns
-------
parameters : pandas.Dataframe
Complete set of parameters ordered by L-values and then energy
"""
# Use l-values and competitiveWidth from File 2 data
# Re-sort File 2 by energy to match File 32
file2params = file2params.sort_values(by=['energy'])
file2params.reset_index(drop=True, inplace=True)
# Sort File 32 parameters by energy as well (maintaining index)
file32params.sort_values(by=['energy'], inplace=True)
# Add in values (.values converts to array first to ignore index)
file32params['L'] = file2params['L'].values
if 'competitiveWidth' in file2params.columns:
file32params['competitiveWidth'] = file2params['competitiveWidth'].values
# Resort to File 32 order (by L then by E) for use with covariance
file32params.sort_index(inplace=True)
return file32params
class ResonanceCovariances(Resonances):
"""Resolved resonance covariance data
Parameters
----------
ranges : list of openmc.data.ResonanceCovarianceRange
Distinct energy ranges for resonance data
Attributes
----------
ranges : list of openmc.data.ResonanceCovarianceRange
Distinct energy ranges for resonance data
"""
@property
def ranges(self):
return self._ranges
@ranges.setter
def ranges(self, ranges):
cv.check_type('resonance ranges', ranges, MutableSequence)
self._ranges = cv.CheckedList(ResonanceCovarianceRange,
'resonance range', ranges)
@classmethod
def from_endf(cls, ev, resonances):
"""Generate resonance covariance data from an ENDF evaluation.
Parameters
----------
ev : openmc.data.endf.Evaluation
ENDF evaluation
resonances : openmc.data.Resonance object
openmc.data.Resonanance object generated from the same evaluation
used to import values not contained in File 32
Returns
-------
openmc.data.ResonanceCovariances
Resonance covariance data
"""
file_obj = io.StringIO(ev.section[32, 151])
# Determine whether discrete or continuous representation
items = endf.get_head_record(file_obj)
n_isotope = items[4] # Number of isotopes
ranges = []
for iso in range(n_isotope):
items = endf.get_cont_record(file_obj)
abundance = items[1]
fission_widths = (items[3] == 1) # Flag for fission widths
n_ranges = items[4] # Number of resonance energy ranges
for j in range(n_ranges):
items = endf.get_cont_record(file_obj)
# Unresolved flags - 0: only scattering radius given
# 1: resolved parameters given
# 2: unresolved parameters given
unresolved_flag = items[2]
formalism = items[3] # resonance formalism
# Throw error for unsupported formalisms
if formalism in [0, 7]:
error = 'LRF='+str(formalism)+' covariance not supported '\
'for this formalism'
raise NotImplementedError(error)
if unresolved_flag in (0, 1):
# Resolved resonance region
resonance = resonances.ranges[j]
erange = _FORMALISMS[formalism].from_endf(ev, file_obj,
items, resonance)
ranges.append(erange)
elif unresolved_flag == 2:
warn = 'Unresolved resonance not supported. Covariance '\
'values for the unresolved region not imported.'
warnings.warn(warn)
return cls(ranges)
class ResonanceCovarianceRange:
"""Resonace covariance range. Base class for different formalisms.
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
Attributes
----------
energy_min : float
Minimum energy of the resolved resonance range in eV
energy_max : float
Maximum energy of the resolved resonance range in eV
parameters : pandas.DataFrame
Resonance parameters
covariance : numpy.array
The covariance matrix contained within the ENDF evaluation
lcomp : int
Flag indicating format of the covariance matrix within the ENDF file
file2res : openmc.data.ResonanceRange object
Corresponding resonance range with File 2 data.
mpar : int
Number of parameters in covariance matrix for each individual resonance
formalism : str
String descriptor of formalism
"""
def __init__(self, energy_min, energy_max):
self.energy_min = energy_min
self.energy_max = energy_max
def subset(self, parameter_str, bounds):
"""Produce a subset of resonance parameters and the corresponding
covariance matrix to an IncidentNeutron object.
Parameters
----------
parameter_str : str
parameter to be discriminated
(i.e. 'energy', 'captureWidth', 'fissionWidthA'...)
bounds : np.array
[low numerical bound, high numerical bound]
Returns
-------
res_cov_range : openmc.data.ResonanceCovarianceRange
ResonanceCovarianceRange object that contains a subset of the
covariance matrix (upper triangular) as well as a subset parameters
within self.file2params
"""
# Copy range and prevent change of original
res_cov_range = copy.deepcopy(self)
parameters = self.file2res.parameters
cov = res_cov_range.covariance
mpar = res_cov_range.mpar
# Create mask
mask1 = parameters[parameter_str] >= bounds[0]
mask2 = parameters[parameter_str] <= bounds[1]
mask = mask1 & mask2
res_cov_range.parameters = parameters[mask]
indices = res_cov_range.parameters.index.values
# Build subset of covariance
sub_cov_dim = len(indices)*mpar
cov_subset_vals = []
for index1 in indices:
for i in range(mpar):
for index2 in indices:
for j in range(mpar):
if index2*mpar+j >= index1*mpar+i:
cov_subset_vals.append(cov[index1*mpar+i,
index2*mpar+j])
cov_subset = np.zeros([sub_cov_dim, sub_cov_dim])
tri_indices = np.triu_indices(sub_cov_dim)
cov_subset[tri_indices] = cov_subset_vals
res_cov_range.file2res.parameters = parameters[mask]
res_cov_range.covariance = cov_subset
return res_cov_range
def sample(self, n_samples):
"""Sample resonance parameters based on the covariances provided
within an ENDF evaluation.
Parameters
----------
n_samples : int
The number of samples to produce
Returns
-------
samples : list of openmc.data.ResonanceCovarianceRange objects
List of samples size `n_samples`
"""
warn_str = 'Sampling routine does not guarantee positive values for '\
'parameters. This can lead to undefined behavior in the '\
'reconstruction routine.'
warnings.warn(warn_str)
parameters = self.parameters
cov = self.covariance
# Symmetrizing covariance matrix
cov = cov + cov.T - np.diag(cov.diagonal())
formalism = self.formalism
mpar = self.mpar
samples = []
# Handling MLBW/SLBW sampling
if formalism == 'mlbw' or formalism == 'slbw':
params = ['energy', 'neutronWidth', 'captureWidth', 'fissionWidth',
'competitiveWidth']
param_list = params[:mpar]
mean_array = parameters[param_list].values
mean = mean_array.flatten()
par_samples = np.random.multivariate_normal(mean, cov,
size=n_samples)
spin = parameters['J'].values
l_value = parameters['L'].values
for sample in par_samples:
energy = sample[0::mpar]
gn = sample[1::mpar]
gg = sample[2::mpar]
gf = sample[3::mpar] if mpar > 3 else parameters['fissionWidth'].values
gx = sample[4::mpar] if mpar > 4 else parameters['competitiveWidth'].values
gt = gn + gg + gf + gx
records = []
for j, E in enumerate(energy):
records.append([energy[j], l_value[j], spin[j], gt[j],
gn[j], gg[j], gf[j], gx[j]])
columns = ['energy', 'L', 'J', 'totalWidth', 'neutronWidth',
'captureWidth', 'fissionWidth', 'competitiveWidth']
sample_params = pd.DataFrame.from_records(records,
columns=columns)
# Copy ResonanceRange object
res_range = copy.copy(self.file2res)
res_range.parameters = sample_params
samples.append(res_range)
# Handling RM sampling
elif formalism == 'rm':
params = ['energy', 'neutronWidth', 'captureWidth',
'fissionWidthA', 'fissionWidthB']
param_list = params[:mpar]
mean_array = parameters[param_list].values
mean = mean_array.flatten()
par_samples = np.random.multivariate_normal(mean, cov,
size=n_samples)
spin = parameters['J'].values
l_value = parameters['L'].values
for sample in par_samples:
energy = sample[0::mpar]
gn = sample[1::mpar]
gg = sample[2::mpar]
gfa = sample[3::mpar] if mpar > 3 else parameters['fissionWidthA'].values
gfb = sample[4::mpar] if mpar > 3 else parameters['fissionWidthB'].values
records = []
for j, E in enumerate(energy):
records.append([energy[j], l_value[j], spin[j], gn[j],
gg[j], gfa[j], gfb[j]])
columns = ['energy', 'L', 'J', 'neutronWidth',
'captureWidth', 'fissionWidthA', 'fissionWidthB']
sample_params = pd.DataFrame.from_records(records,
columns=columns)
# Copy ResonanceRange object
res_range = copy.copy(self.file2res)
res_range.parameters = sample_params
samples.append(res_range)
return samples
class MultiLevelBreitWignerCovariance(ResonanceCovarianceRange):
"""Multi-level Breit-Wigner resolved resonance formalism covariance data.
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
Attributes
----------
energy_min : float
Minimum energy of the resolved resonance range in eV
energy_max : float
Maximum energy of the resolved resonance range in eV
parameters : pandas.DataFrame
Resonance parameters
covariance : numpy.array
The covariance matrix contained within the ENDF evaluation
mpar : int
Number of parameters in covariance matrix for each individual resonance
lcomp : int
Flag indicating format of the covariance matrix within the ENDF file
file2res : openmc.data.ResonanceRange object
Corresponding resonance range with File 2 data.
formalism : str
String descriptor of formalism
"""
def __init__(self, energy_min, energy_max, parameters, covariance, mpar,
lcomp, file2res):
super().__init__(energy_min, energy_max)
self.parameters = parameters
self.covariance = covariance
self.mpar = mpar
self.lcomp = lcomp
self.file2res = copy.copy(file2res)
self.formalism = 'mlbw'
@classmethod
def from_endf(cls, ev, file_obj, items, resonance):
"""Create MLBW covariance 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=32, MT=151
items : list
Items from the CONT record at the start of the resonance range
subsection
resonance : openmc.data.ResonanceRange object
Corresponding resonance range with File 2 data.
Returns
-------
openmc.data.MultiLevelBreitWignerCovariance
Multi-level Breit-Wigner resonance covariance 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 = endf.get_tab1_record(file_obj)
# Other scatter radius parameters
items = endf.get_cont_record(file_obj)
target_spin = items[0]
lcomp = items[3] # Flag for compatibility 0, 1, 2 - 2 is compact form
nls = items[4] # number of l-values
# Build covariance matrix for General Resolved Resonance Formats
if lcomp == 1:
items = endf.get_cont_record(file_obj)
# Number of short range type resonance covariances
num_short_range = items[4]
# Number of long range type resonance covariances
num_long_range = items[5]
# Read resonance widths, J values, etc
records = []
for i in range(num_short_range):
items, values = endf.get_list_record(file_obj)
mpar = items[2]
num_res = items[5]
num_par_vals = num_res*6
res_values = values[:num_par_vals]
cov_values = values[num_par_vals:]
energy = res_values[0::6]
spin = res_values[1::6]
gt = res_values[2::6]
gn = res_values[3::6]
gg = res_values[4::6]
gf = res_values[5::6]
for i, E in enumerate(energy):
records.append([energy[i], spin[i], gt[i], gn[i],
gg[i], gf[i]])
# Build the upper-triangular covariance matrix
cov_dim = mpar*num_res
cov = np.zeros([cov_dim, cov_dim])
indices = np.triu_indices(cov_dim)
cov[indices] = cov_values
# Compact format - Resonances and individual uncertainties followed by
# compact correlations
elif lcomp == 2:
items, values = endf.get_list_record(file_obj)
mean = items
num_res = items[5]
energy = values[0::12]
spin = values[1::12]
gt = values[2::12]
gn = values[3::12]
gg = values[4::12]
gf = values[5::12]
par_unc = []
for i in range(num_res):
res_unc = values[i*12+6 : i*12+12]
# Delete 0 values (not provided, no fission width)
# DAJ/DGT always zero, DGF sometimes nonzero [1, 2, 5]
res_unc_nonzero = []
for j in range(6):
if j in [1, 2, 5] and res_unc[j] != 0.0:
res_unc_nonzero.append(res_unc[j])
elif j in [0, 3, 4]:
res_unc_nonzero.append(res_unc[j])
par_unc.extend(res_unc_nonzero)
records = []
for i, E in enumerate(energy):
records.append([energy[i], spin[i], gt[i], gn[i],
gg[i], gf[i]])
corr = endf.get_intg_record(file_obj)
cov = np.diag(par_unc).dot(corr).dot(np.diag(par_unc))
# Compatible resolved resonance format
elif lcomp == 0:
cov = np.zeros([4, 4])
records = []
cov_index = 0
for i in range(nls):
items, values = endf.get_list_record(file_obj)
num_res = items[5]
for j in range(num_res):
one_res = values[18*j:18*(j+1)]
res_values = one_res[:6]
cov_values = one_res[6:]
records.append(list(res_values))
# Populate the coviariance matrix for this resonance
# There are no covariances between resonances in lcomp=0
cov[cov_index, cov_index] = cov_values[0]
cov[cov_index+1, cov_index+1 : cov_index+2] = cov_values[1:2]
cov[cov_index+1, cov_index+3] = cov_values[4]
cov[cov_index+2, cov_index+2] = cov_values[3]
cov[cov_index+2, cov_index+3] = cov_values[5]
cov[cov_index+3, cov_index+3] = cov_values[6]
cov_index += 4
if j < num_res-1: # Pad matrix for additional values
cov = np.pad(cov, ((0, 4), (0, 4)), 'constant',
constant_values=0)
# Create pandas DataFrame with resonance data, currently
# redundant with data.IncidentNeutron.resonance
columns = ['energy', 'J', 'totalWidth', 'neutronWidth',
'captureWidth', 'fissionWidth']
parameters = pd.DataFrame.from_records(records, columns=columns)
# Determine mpar (number of parameters for each resonance in
# covariance matrix)
nparams, params = parameters.shape
covsize = cov.shape[0]
mpar = int(covsize/nparams)
# Add parameters from File 2
parameters = _add_file2_contributions(parameters,
resonance.parameters)
# Create instance of class
mlbw = cls(energy_min, energy_max, parameters, cov, mpar, lcomp,
resonance)
return mlbw
class SingleLevelBreitWignerCovariance(MultiLevelBreitWignerCovariance):
"""Single-level Breit-Wigner resolved resonance formalism covariance data.
Single-level Breit-Wigner resolved resonance data is is identified by LRF=1
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
Attributes
----------
energy_min : float
Minimum energy of the resolved resonance range in eV
energy_max : float
Maximum energy of the resolved resonance range in eV
parameters : pandas.DataFrame
Resonance parameters
covariance : numpy.array
The covariance matrix contained within the ENDF evaluation
mpar : int
Number of parameters in covariance matrix for each individual resonance
formalism : str
String descriptor of formalism
lcomp : int
Flag indicating format of the covariance matrix within the ENDF file
file2res : openmc.data.ResonanceRange object
Corresponding resonance range with File 2 data.
"""
def __init__(self, energy_min, energy_max, parameters, covariance, mpar,
lcomp, file2res):
super().__init__(energy_min, energy_max, parameters, covariance, mpar,
lcomp, file2res)
self.formalism = 'slbw'
class ReichMooreCovariance(ResonanceCovarianceRange):
"""Reich-Moore resolved resonance formalism covariance data.
Reich-Moore resolved resonance data is identified by LRF=3 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
Attributes
----------
energy_min : float
Minimum energy of the resolved resonance range in eV
energy_max : float
Maximum energy of the resolved resonance range in eV
parameters : pandas.DataFrame
Resonance parameters
covariance : numpy.array
The covariance matrix contained within the ENDF evaluation
lcomp : int
Flag indicating format of the covariance matrix within the ENDF file
mpar : int
Number of parameters in covariance matrix for each individual resonance
file2res : openmc.data.ResonanceRange object
Corresponding resonance range with File 2 data.
formalism : str
String descriptor of formalism
"""
def __init__(self, energy_min, energy_max, parameters, covariance, mpar,
lcomp, file2res):
super().__init__(energy_min, energy_max)
self.parameters = parameters
self.covariance = covariance
self.mpar = mpar
self.lcomp = lcomp
self.file2res = copy.copy(file2res)
self.formalism = 'rm'
@classmethod
def from_endf(cls, ev, file_obj, items, resonance):
"""Create Reich-Moore resonance covariance data from an ENDF
evaluation. Includes the resonance parameters contained separately in
File 32.
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
resonance : openmc.data.Resonance object
openmc.data.Resonanance object generated from the same evaluation
used to import values not contained in File 32
Returns
-------
openmc.data.ReichMooreCovariance
Reich-Moore resonance covariance 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 = endf.get_tab1_record(file_obj)
# Other scatter radius parameters
items = endf.get_cont_record(file_obj)
target_spin = items[0]
lcomp = items[3] # Flag for compatibility 0, 1, 2 - 2 is compact form
nls = items[4] # Number of l-values
# Build covariance matrix for General Resolved Resonance Formats
if lcomp == 1:
items = endf.get_cont_record(file_obj)
# Number of short range type resonance covariances
num_short_range = items[4]
# Number of long range type resonance covariances
num_long_range = items[5]
# Read resonance widths, J values, etc
channel_radius = {}
scattering_radius = {}
records = []
for i in range(num_short_range):
items, values = endf.get_list_record(file_obj)
mpar = items[2]
num_res = items[5]
num_par_vals = num_res*6
res_values = values[:num_par_vals]
cov_values = values[num_par_vals:]
energy = res_values[0::6]
spin = res_values[1::6]
gn = res_values[2::6]
gg = res_values[3::6]
gfa = res_values[4::6]
gfb = res_values[5::6]
for i, E in enumerate(energy):
records.append([energy[i], spin[i], gn[i], gg[i],
gfa[i], gfb[i]])
# Build the upper-triangular covariance matrix
cov_dim = mpar*num_res
cov = np.zeros([cov_dim, cov_dim])
indices = np.triu_indices(cov_dim)
cov[indices] = cov_values
# Compact format - Resonances and individual uncertainties followed by
# compact correlations
elif lcomp == 2:
items, values = endf.get_list_record(file_obj)
num_res = items[5]
energy = values[0::12]
spin = values[1::12]
gn = values[2::12]
gg = values[3::12]
gfa = values[4::12]
gfb = values[5::12]
par_unc = []
for i in range(num_res):
res_unc = values[i*12+6 : i*12+12]
# Delete 0 values (not provided in evaluation)
res_unc = [x for x in res_unc if x != 0.0]
par_unc.extend(res_unc)
records = []
for i, E in enumerate(energy):
records.append([energy[i], spin[i], gn[i], gg[i],
gfa[i], gfb[i]])
corr = endf.get_intg_record(file_obj)
cov = np.diag(par_unc).dot(corr).dot(np.diag(par_unc))
# Create pandas DataFrame with resonacne data
columns = ['energy', 'J', 'neutronWidth', 'captureWidth',
'fissionWidthA', 'fissionWidthB']
parameters = pd.DataFrame.from_records(records, columns=columns)
# Determine mpar (number of parameters for each resonance in
# covariance matrix)
nparams, params = parameters.shape
covsize = cov.shape[0]
mpar = int(covsize/nparams)
# Add parameters from File 2
parameters = _add_file2_contributions(parameters,
resonance.parameters)
# Create instance of ReichMooreCovariance
rmc = cls(energy_min, energy_max, parameters, cov, mpar, lcomp,
resonance)
return rmc
_FORMALISMS = {
0: ResonanceCovarianceRange,
1: SingleLevelBreitWignerCovariance,
2: MultiLevelBreitWignerCovariance,
3: ReichMooreCovariance
# 7: RMatrixLimitedCovariance
}

927
openmc/data/thermal.py Normal file
View file

@ -0,0 +1,927 @@
from collections.abc import Iterable
from collections import namedtuple
from difflib import get_close_matches
from numbers import Real
from io import StringIO
import itertools
import os
import re
import tempfile
from warnings import warn
import numpy as np
import h5py
import openmc.checkvalue as cv
from openmc.mixin import EqualityMixin
from openmc.stats import Discrete, Tabular
from . import HDF5_VERSION, HDF5_VERSION_MAJOR, endf
from .data import K_BOLTZMANN, ATOMIC_SYMBOL, EV_PER_MEV, NATURAL_ABUNDANCE
from .ace import Table, get_table, Library
from .angle_energy import AngleEnergy
from .function import Tabulated1D, Function1D
from .njoy import make_ace_thermal
from .thermal_angle_energy import (CoherentElasticAE, IncoherentElasticAE,
IncoherentElasticAEDiscrete,
IncoherentInelasticAEDiscrete,
IncoherentInelasticAE)
_THERMAL_NAMES = {
'c_Al27': ('al', 'al27', 'al-27'),
'c_Al_in_Sapphire': ('asap00',),
'c_Be': ('be', 'be-metal', 'be-met', 'be00'),
'c_BeO': ('beo',),
'c_Be_in_BeO': ('bebeo', 'be-beo', 'be-o', 'be/o', 'bbeo00'),
'c_Be_in_Be2C': ('bebe2c',),
'c_C6H6': ('benz', 'c6h6'),
'c_C_in_SiC': ('csic', 'c-sic'),
'c_Ca_in_CaH2': ('cah', 'cah00'),
'c_D_in_D2O': ('dd2o', 'd-d2o', 'hwtr', 'hw', 'dhw00'),
'c_D_in_D2O_ice': ('dice',),
'c_Fe56': ('fe', 'fe56', 'fe-56'),
'c_Graphite': ('graph', 'grph', 'gr', 'gr00'),
'c_Graphite_10p': ('grph10',),
'c_Graphite_30p': ('grph30',),
'c_H_in_CaH2': ('hcah2', 'hca00'),
'c_H_in_CH2': ('hch2', 'poly', 'pol', 'h-poly', 'pol00'),
'c_H_in_CH4_liquid': ('lch4', 'lmeth'),
'c_H_in_CH4_solid': ('sch4', 'smeth'),
'c_H_in_CH4_solid_phase_II': ('sch4p2',),
'c_H_in_H2O': ('hh2o', 'h-h2o', 'lwtr', 'lw', 'lw00'),
'c_H_in_H2O_solid': ('hice', 'h-ice', 'ice00'),
'c_H_in_C5O2H8': ('lucite', 'c5o2h8', 'h-luci'),
'c_H_in_Mesitylene': ('mesi00',),
'c_H_in_Toluene': ('tol00',),
'c_H_in_YH2': ('hyh2', 'h-yh2'),
'c_H_in_ZrH': ('hzrh', 'h-zrh', 'h-zr', 'h/zr', 'hzr', 'hzr00'),
'c_Mg24': ('mg', 'mg24', 'mg00'),
'c_O_in_Sapphire': ('osap00',),
'c_O_in_BeO': ('obeo', 'o-beo', 'o-be', 'o/be', 'obeo00'),
'c_O_in_D2O': ('od2o', 'o-d2o', 'ohw00'),
'c_O_in_H2O_ice': ('oice', 'o-ice'),
'c_O_in_UO2': ('ouo2', 'o-uo2', 'o2-u', 'o2/u', 'ouo200'),
'c_N_in_UN': ('n-un',),
'c_ortho_D': ('orthod', 'orthoD', 'dortho', 'od200'),
'c_ortho_H': ('orthoh', 'orthoH', 'hortho', 'oh200'),
'c_Si28': ('si00',),
'c_Si_in_SiC': ('sisic', 'si-sic'),
'c_SiO2_alpha': ('sio2', 'sio2a'),
'c_SiO2_beta': ('sio2b',),
'c_para_D': ('parad', 'paraD', 'dpara', 'pd200'),
'c_para_H': ('parah', 'paraH', 'hpara', 'ph200'),
'c_U_in_UN': ('u-un',),
'c_U_in_UO2': ('uuo2', 'u-uo2', 'u-o2', 'u/o2', 'uuo200'),
'c_Y_in_YH2': ('yyh2', 'y-yh2'),
'c_Zr_in_ZrH': ('zrzrh', 'zr-zrh', 'zr-h', 'zr/h')
}
def _temperature_str(T):
# round() normally returns an int when called with a single argument, but
# numpy floats overload rounding to return another float
return "{}K".format(int(round(T)))
def get_thermal_name(name):
"""Get proper S(a,b) table name, e.g. 'HH2O' -> 'c_H_in_H2O'
Parameters
----------
name : str
Name of an ACE thermal scattering table
Returns
-------
str
GND-format thermal scattering name
"""
if name in _THERMAL_NAMES:
return name
else:
for proper_name, names in _THERMAL_NAMES.items():
if name.lower() in names:
return proper_name
# Make an educated guess?? This actually works well for
# JEFF-3.2 which stupidly uses names like lw00.32t,
# lw01.32t, etc. for different temperatures
# First, construct a list of all the values/keys in the names
# dictionary
all_names = itertools.chain(_THERMAL_NAMES.keys(),
*_THERMAL_NAMES.values())
matches = get_close_matches(name, all_names, cutoff=0.5)
if matches:
# Figure out the key for the corresponding match
match = matches[0]
if match not in _THERMAL_NAMES:
for key, value_list in _THERMAL_NAMES.items():
if match in value_list:
match = key
break
warn('Thermal scattering material "{}" is not recognized. '
'Assigning a name of {}.'.format(name, match))
return match
else:
# OK, we give up. Just use the ACE name.
warn('Thermal scattering material "{0}" is not recognized. '
'Assigning a name of c_{0}.'.format(name))
return 'c_' + name
class CoherentElastic(Function1D):
r"""Coherent elastic scattering data from a crystalline material
The integrated cross section for coherent elastic scattering from a
powdered crystalline material may be represented as:
.. math::
\sigma(E,T) = \frac{1}{E} \sum\limits_{i=1}^{E_i < E} s_i(T)
where :math:`s_i(T)` is proportional the structure factor in [eV-b] at
the moderator temperature :math:`T` in Kelvin.
Parameters
----------
bragg_edges : Iterable of float
Bragg edge energies in eV
factors : Iterable of float
Partial sum of structure factors, :math:`\sum\limits_{i=1}^{E_i<E} s_i`
Attributes
----------
bragg_edges : Iterable of float
Bragg edge energies in eV
factors : Iterable of float
Partial sum of structure factors, :math:`\sum\limits_{i=1}^{E_i<E} s_i`
"""
def __init__(self, bragg_edges, factors):
self.bragg_edges = bragg_edges
self.factors = factors
def __call__(self, E):
idx = np.searchsorted(self.bragg_edges, E) - 1
if isinstance(E, Iterable):
E = np.asarray(E)
nonzero = idx >= 0
xs = np.zeros_like(E)
xs[nonzero] = self.factors[idx[nonzero]] / E[nonzero]
return xs
else:
return self.factors[idx] / E if idx >= 0 else 0.0
def __len__(self):
return len(self.bragg_edges)
@property
def bragg_edges(self):
return self._bragg_edges
@property
def factors(self):
return self._factors
@bragg_edges.setter
def bragg_edges(self, bragg_edges):
cv.check_type('Bragg edges', bragg_edges, Iterable, Real)
self._bragg_edges = np.asarray(bragg_edges)
@factors.setter
def factors(self, factors):
cv.check_type('structure factor cumulative sums', factors,
Iterable, Real)
self._factors = np.asarray(factors)
def to_hdf5(self, group, name):
"""Write coherent elastic scattering to an HDF5 group
Parameters
----------
group : h5py.Group
HDF5 group to write to
name : str
Name of the dataset to create
"""
dataset = group.create_dataset(name, data=np.vstack(
[self.bragg_edges, self.factors]))
dataset.attrs['type'] = np.string_(type(self).__name__)
@classmethod
def from_hdf5(cls, dataset):
"""Read coherent elastic scattering from an HDF5 dataset
Parameters
----------
dataset : h5py.Dataset
HDF5 dataset to read from
Returns
-------
openmc.data.CoherentElastic
Coherent elastic scattering cross section
"""
bragg_edges = dataset[0, :]
factors = dataset[1, :]
return cls(bragg_edges, factors)
class IncoherentElastic(Function1D):
r"""Incoherent elastic scattering cross section
Elastic scattering can be treated in the incoherent approximation for
partially ordered systems such as ZrHx and polyethylene. The integrated
cross section can be obtained as:
.. math::
\sigma(E,T) = \frac{\sigma_b}{2} \left ( \frac{1 - e^{-4EW'(T)}}
{2EW'(T)} \right )
where :math:`\sigma_b` is the characteristic bound cross section, and
:math:`W'(T)` is the Debye-Waller integral divided by the atomic mass
in [eV\ :math:`^{-1}`].
Parameters
----------
bound_xs : float
Characteristic bound cross section in [b]
debye_waller : float
Debye-Waller integral in [eV\ :math:`^{-1}`]
Attributes
----------
bound_xs : float
Characteristic bound cross section in [b]
debye_waller : float
Debye-Waller integral in [eV\ :math:`^{-1}`]
"""
def __init__(self, bound_xs, debye_waller):
self.bound_xs = bound_xs
self.debye_waller = debye_waller
def __call__(self, E):
W = self.debye_waller
return self.bound_xs / 2.0 * (1 - np.exp(-4*E*W)) / (2*E*W)
def to_hdf5(self, group, name):
"""Write incoherent elastic scattering to an HDF5 group
Parameters
----------
group : h5py.Group
HDF5 group to write to
name : str
Name of the dataset to create
"""
data = np.array([self.bound_xs, self.debye_waller])
dataset = group.create_dataset(name, data=data)
dataset.attrs['type'] = np.string_(type(self).__name__)
@classmethod
def from_hdf5(cls, dataset):
"""Read incoherent elastic scattering from an HDF5 dataset
Parameters
----------
dataset : h5py.Dataset
HDF5 dataset to read from
Returns
-------
openmc.data.IncoherentElastic
Incoherent elastic scattering cross section
"""
bound_xs, debye_waller = dataset[()]
return cls(bound_xs, debye_waller)
class ThermalScatteringReaction(EqualityMixin):
r"""Thermal scattering reaction
This class is used to hold the integral and differential cross sections
for either elastic or inelastic thermal scattering.
Parameters
----------
xs : dict of str to Function1D
Integral cross section at each temperature
distribution : dict of str to AngleEnergy
Secondary angle-energy distribution at each temperature
Attributes
----------
xs : dict of str to Function1D
Integral cross section at each temperature
distribution : dict of str to AngleEnergy
Secondary angle-energy distribution at each temperature
"""
def __init__(self, xs, distribution):
self.xs = xs
self.distribution = distribution
def to_hdf5(self, group, name):
"""Write thermal scattering reaction to HDF5
Parameters
----------
group : h5py.Group
HDF5 group to write to
name : {'elastic', 'inelastic'}
Name of reaction to write
"""
for T, xs in self.xs.items():
Tgroup = group.require_group(T)
rx_group = Tgroup.create_group(name)
xs.to_hdf5(rx_group, 'xs')
dgroup = rx_group.create_group('distribution')
self.distribution[T].to_hdf5(dgroup)
@classmethod
def from_hdf5(cls, group, name, temperatures):
"""Generate thermal scattering reaction data from HDF5
Parameters
----------
group : h5py.Group
HDF5 group to read from
name : {'elastic', 'inelastic'}
Name of the reaction to read
temperatures : Iterable of str
Temperatures to read
Returns
-------
openmc.data.ThermalScatteringReaction
Thermal scattering reaction data
"""
xs = {}
distribution = {}
for T in temperatures:
rx_group = group[T][name]
xs[T] = Function1D.from_hdf5(rx_group['xs'])
if isinstance(xs[T], CoherentElastic):
distribution[T] = CoherentElasticAE(xs[T])
else:
distribution[T] = AngleEnergy.from_hdf5(rx_group['distribution'])
return cls(xs, distribution)
class ThermalScattering(EqualityMixin):
"""A ThermalScattering object contains thermal scattering data as represented by
an S(alpha, beta) table.
Parameters
----------
name : str
Name of the material using GND convention, e.g. c_H_in_H2O
atomic_weight_ratio : float
Atomic mass ratio of the target nuclide.
kTs : Iterable of float
List of temperatures of the target nuclide in the data set.
The temperatures have units of eV.
Attributes
----------
atomic_weight_ratio : float
Atomic mass ratio of the target nuclide.
energy_max : float
Maximum energy for thermal scattering data in [eV]
elastic : openmc.data.ThermalScatteringReaction or None
Elastic scattering derived in the coherent or incoherent approximation
inelastic : openmc.data.ThermalScatteringReaction
Inelastic scattering cross section derived in the incoherent
approximation
name : str
Name of the material using GND convention, e.g. c_H_in_H2O
temperatures : Iterable of str
List of string representations the temperatures of the target nuclide
in the data set. The temperatures are strings of the temperature,
rounded to the nearest integer; e.g., '294K'
kTs : Iterable of float
List of temperatures of the target nuclide in the data set.
The temperatures have units of eV.
nuclides : Iterable of str
Nuclide names that the thermal scattering data applies to
"""
def __init__(self, name, atomic_weight_ratio, energy_max, kTs):
self.name = name
self.atomic_weight_ratio = atomic_weight_ratio
self.energy_max = energy_max
self.kTs = kTs
self.elastic = None
self.inelastic = None
self.nuclides = []
def __repr__(self):
if hasattr(self, 'name'):
return "<Thermal Scattering Data: {}>".format(self.name)
else:
return "<Thermal Scattering Data>"
@property
def temperatures(self):
return [_temperature_str(kT / K_BOLTZMANN) for kT in self.kTs]
def export_to_hdf5(self, path, mode='a', libver='earliest'):
"""Export table to an HDF5 file.
Parameters
----------
path : str
Path to write HDF5 file to
mode : {'r', r+', 'w', 'x', 'a'}
Mode that is used to open the HDF5 file. This is the second argument
to the :class:`h5py.File` constructor.
libver : {'earliest', 'latest'}
Compatibility mode for the HDF5 file. 'latest' will produce files
that are less backwards compatible but have performance benefits.
"""
# Open file and write version
with h5py.File(str(path), mode, libver=libver) as f:
f.attrs['filetype'] = np.string_('data_thermal')
f.attrs['version'] = np.array(HDF5_VERSION)
# Write basic data
g = f.create_group(self.name)
g.attrs['atomic_weight_ratio'] = self.atomic_weight_ratio
g.attrs['energy_max'] = self.energy_max
g.attrs['nuclides'] = np.array(self.nuclides, dtype='S')
ktg = g.create_group('kTs')
for i, temperature in enumerate(self.temperatures):
ktg.create_dataset(temperature, data=self.kTs[i])
# Write elastic/inelastic reaction data
if self.elastic is not None:
self.elastic.to_hdf5(g, 'elastic')
self.inelastic.to_hdf5(g, 'inelastic')
def add_temperature_from_ace(self, ace_or_filename, name=None):
"""Add data to the ThermalScattering object from an ACE file at a
different temperature.
Parameters
----------
ace_or_filename : openmc.data.ace.Table or str
ACE table to read from. If given as a string, it is assumed to be
the filename for the ACE file.
name : str
GND-conforming name of the material, e.g. c_H_in_H2O. If none is
passed, the appropriate name is guessed based on the name of the ACE
table.
Returns
-------
openmc.data.ThermalScattering
Thermal scattering data
"""
data = ThermalScattering.from_ace(ace_or_filename, name)
# Check if temprature already exists
strT = data.temperatures[0]
if strT in self.temperatures:
warn('S(a,b) data at T={} already exists.'.format(strT))
return
# Check that name matches
if data.name != self.name:
raise ValueError('Data provided for an incorrect material.')
# Add temperature
self.kTs += data.kTs
# Add inelastic cross section and distributions
if data.inelastic is not None:
self.inelastic.xs.update(data.inelastic.xs)
self.inelastic.distribution.update(data.inelastic.distribution)
# Add elastic cross sectoin and angular distribution
if data.elastic is not None:
self.elastic.xs.update(data.elastic.xs)
self.elastic.distribution.update(data.elastic.distribution)
@classmethod
def from_hdf5(cls, group_or_filename):
"""Generate thermal scattering data from HDF5 group
Parameters
----------
group_or_filename : h5py.Group or str
HDF5 group containing interaction data. If given as a string, it is
assumed to be the filename for the HDF5 file, and the first group
is used to read from.
Returns
-------
openmc.data.ThermalScattering
Neutron thermal scattering data
"""
if isinstance(group_or_filename, h5py.Group):
group = group_or_filename
else:
h5file = h5py.File(str(group_or_filename), 'r')
# Make sure version matches
if 'version' in h5file.attrs:
major, minor = h5file.attrs['version']
if major != HDF5_VERSION_MAJOR:
raise IOError(
'HDF5 data format uses version {}.{} whereas your '
'installation of the OpenMC Python API expects version '
'{}.x.'.format(major, minor, HDF5_VERSION_MAJOR))
else:
raise IOError(
'HDF5 data does not indicate a version. Your installation of '
'the OpenMC Python API expects version {}.x data.'
.format(HDF5_VERSION_MAJOR))
group = list(h5file.values())[0]
name = group.name[1:]
atomic_weight_ratio = group.attrs['atomic_weight_ratio']
energy_max = group.attrs['energy_max']
kTg = group['kTs']
kTs = [dataset[()] for dataset in kTg.values()]
table = cls(name, atomic_weight_ratio, energy_max, kTs)
table.nuclides = [nuc.decode() for nuc in group.attrs['nuclides']]
# Read thermal elastic scattering
if 'elastic' in group[table.temperatures[0]]:
table.elastic = ThermalScatteringReaction.from_hdf5(
group, 'elastic', table.temperatures
)
# Read thermal inelastic scattering
table.inelastic = ThermalScatteringReaction.from_hdf5(
group, 'inelastic', table.temperatures
)
return table
@classmethod
def from_ace(cls, ace_or_filename, name=None):
"""Generate thermal scattering data from an ACE table
Parameters
----------
ace_or_filename : openmc.data.ace.Table or str
ACE table to read from. If given as a string, it is assumed to be
the filename for the ACE file.
name : str
GND-conforming name of the material, e.g. c_H_in_H2O. If none is
passed, the appropriate name is guessed based on the name of the ACE
table.
Returns
-------
openmc.data.ThermalScattering
Thermal scattering data
"""
if isinstance(ace_or_filename, Table):
ace = ace_or_filename
else:
ace = get_table(ace_or_filename)
# Get new name that is GND-consistent
ace_name, xs = ace.name.split('.')
if not xs.endswith('t'):
raise TypeError("{} is not a thermal scattering ACE table.".format(ace))
name = get_thermal_name(ace_name)
# Assign temperature to the running list
kTs = [ace.temperature*EV_PER_MEV]
# Incoherent inelastic scattering cross section
idx = ace.jxs[1]
n_energy = int(ace.xss[idx])
energy = ace.xss[idx+1 : idx+1+n_energy]*EV_PER_MEV
xs = ace.xss[idx+1+n_energy : idx+1+2*n_energy]
inelastic_xs = Tabulated1D(energy, xs)
energy_max = energy[-1]
# Incoherent inelastic angle-energy distribution
continuous = (ace.nxs[7] == 2)
n_energy_out = ace.nxs[4]
if not continuous:
n_mu = ace.nxs[3]
idx = ace.jxs[3]
energy_out = ace.xss[idx:idx + n_energy * n_energy_out *
(n_mu + 2): n_mu + 2]*EV_PER_MEV
energy_out.shape = (n_energy, n_energy_out)
mu_out = ace.xss[idx:idx + n_energy * n_energy_out * (n_mu + 2)]
mu_out.shape = (n_energy, n_energy_out, n_mu+2)
mu_out = mu_out[:, :, 1:]
skewed = (ace.nxs[7] == 1)
distribution = IncoherentInelasticAEDiscrete(energy_out, mu_out, skewed)
else:
n_mu = ace.nxs[3] - 1
idx = ace.jxs[3]
locc = ace.xss[idx:idx + n_energy].astype(int)
n_energy_out = \
ace.xss[idx + n_energy:idx + 2 * n_energy].astype(int)
energy_out = []
mu_out = []
for i in range(n_energy):
idx = locc[i]
# Outgoing energy distribution for incoming energy i
e = ace.xss[idx + 1:idx + 1 + n_energy_out[i]*(n_mu + 3):
n_mu + 3]*EV_PER_MEV
p = ace.xss[idx + 2:idx + 2 + n_energy_out[i]*(n_mu + 3):
n_mu + 3]/EV_PER_MEV
c = ace.xss[idx + 3:idx + 3 + n_energy_out[i]*(n_mu + 3):
n_mu + 3]
eout_i = Tabular(e, p, 'linear-linear', ignore_negative=True)
eout_i.c = c
# Outgoing angle distribution for each
# (incoming, outgoing) energy pair
mu_i = []
for j in range(n_energy_out[i]):
mu = ace.xss[idx + 4:idx + 4 + n_mu]
p_mu = 1. / n_mu * np.ones(n_mu)
mu_ij = Discrete(mu, p_mu)
mu_ij.c = np.cumsum(p_mu)
mu_i.append(mu_ij)
idx += 3 + n_mu
energy_out.append(eout_i)
mu_out.append(mu_i)
# Create correlated angle-energy distribution
breakpoints = [n_energy]
interpolation = [2]
energy = inelastic_xs.x
distribution = IncoherentInelasticAE(
breakpoints, interpolation, energy, energy_out, mu_out)
table = cls(name, ace.atomic_weight_ratio, energy_max, kTs)
T = table.temperatures[0]
table.inelastic = ThermalScatteringReaction(
{T: inelastic_xs}, {T: distribution}
)
# Incoherent/coherent elastic scattering cross section
idx = ace.jxs[4]
n_mu = ace.nxs[6] + 1
if idx != 0:
n_energy = int(ace.xss[idx])
energy = ace.xss[idx + 1: idx + 1 + n_energy]*EV_PER_MEV
P = ace.xss[idx + 1 + n_energy: idx + 1 + 2 * n_energy]
if ace.nxs[5] == 4:
# Coherent elastic
xs = CoherentElastic(energy, P*EV_PER_MEV)
distribution = CoherentElasticAE(xs)
# Coherent elastic shouldn't have angular distributions listed
assert n_mu == 0
else:
# Incoherent elastic
xs = Tabulated1D(energy, P)
# Angular distribution
assert n_mu > 0
idx = ace.jxs[6]
mu_out = ace.xss[idx:idx + n_energy * n_mu]
mu_out.shape = (n_energy, n_mu)
distribution = IncoherentElasticAEDiscrete(mu_out)
table.elastic = ThermalScatteringReaction({T: xs}, {T: distribution})
# Get relevant nuclides -- NJOY only allows one to specify three
# nuclides that the S(a,b) table applies to. Thus, for all elements
# other than H and Fe, we automatically add all the naturally-occurring
# isotopes.
for zaid, awr in ace.pairs:
if zaid > 0:
Z, A = divmod(zaid, 1000)
element = ATOMIC_SYMBOL[Z]
if element in ['H', 'Fe']:
table.nuclides.append(element + str(A))
else:
if element + '0' not in table.nuclides:
table.nuclides.append(element + '0')
for isotope in sorted(NATURAL_ABUNDANCE):
if re.match(r'{}\d+'.format(element), isotope):
if isotope not in table.nuclides:
table.nuclides.append(isotope)
return table
@classmethod
def from_njoy(cls, filename, filename_thermal, temperatures=None,
evaluation=None, evaluation_thermal=None,
use_endf_data=True, **kwargs):
"""Generate thermal scattering data by running NJOY.
Parameters
----------
filename : str
Path to ENDF neutron sublibrary file
filename_thermal : str
Path to ENDF thermal scattering sublibrary file
temperatures : iterable of float
Temperatures in Kelvin to produce data at. If omitted, data is
produced at all temperatures in the ENDF thermal scattering
sublibrary.
evaluation : openmc.data.endf.Evaluation, optional
If the ENDF neutron sublibrary file contains multiple material
evaluations, this argument indicates which evaluation to use.
evaluation_thermal : openmc.data.endf.Evaluation, optional
If the ENDF thermal scattering sublibrary file contains multiple
material evaluations, this argument indicates which evaluation to
use.
use_endf_data : bool
If the material has incoherent elastic scattering, the ENDF data
will be used rather than the ACE data.
**kwargs
Keyword arguments passed to :func:`openmc.data.njoy.make_ace_thermal`
Returns
-------
data : openmc.data.ThermalScattering
Thermal scattering data
"""
with tempfile.TemporaryDirectory() as tmpdir:
# Run NJOY to create an ACE library
kwargs.setdefault('ace', os.path.join(tmpdir, 'ace'))
kwargs.setdefault('xsdir', os.path.join(tmpdir, 'xsdir'))
kwargs['evaluation'] = evaluation
kwargs['evaluation_thermal'] = evaluation_thermal
make_ace_thermal(filename, filename_thermal, temperatures, **kwargs)
# Create instance from ACE tables within library
lib = Library(kwargs['ace'])
data = cls.from_ace(lib.tables[0])
for table in lib.tables[1:]:
data.add_temperature_from_ace(table)
# Load ENDF data to replace incoherent elastic
if use_endf_data:
data_endf = cls.from_endf(filename_thermal)
if data_endf.elastic is not None:
# Get appropriate temperatures
if temperatures is None:
temperatures = data_endf.temperatures
else:
temperatures = [_temperature_str(t) for t in temperatures]
# Replace ACE data with ENDF data
rx, rx_endf = data.elastic, data_endf.elastic
for t in temperatures:
if isinstance(rx_endf.xs[t], IncoherentElastic):
rx.xs[t] = rx_endf.xs[t]
rx.distribution[t] = rx_endf.distribution[t]
return data
@classmethod
def from_endf(cls, ev_or_filename):
"""Generate thermal scattering data from an ENDF file
Parameters
----------
ev_or_filename : openmc.data.endf.Evaluation or str
ENDF evaluation to read from. If given as a string, it is assumed to
be the filename for the ENDF file.
Returns
-------
openmc.data.ThermalScattering
Thermal scattering data
"""
if isinstance(ev_or_filename, endf.Evaluation):
ev = ev_or_filename
else:
ev = endf.Evaluation(ev_or_filename)
# Read coherent/incoherent elastic data
elastic = None
if (7, 2) in ev.section:
xs = {}
distribution = {}
file_obj = StringIO(ev.section[7, 2])
lhtr = endf.get_head_record(file_obj)[2]
if lhtr == 1:
# coherent elastic
# Get structure factor at first temperature
params, S = endf.get_tab1_record(file_obj)
strT = _temperature_str(params[0])
n_temps = params[2]
bragg_edges = S.x
xs[strT] = CoherentElastic(bragg_edges, S.y)
distribution = {strT: CoherentElasticAE(xs[strT])}
# Get structure factor for subsequent temperatures
for _ in range(n_temps):
params, S = endf.get_list_record(file_obj)
strT = _temperature_str(params[0])
xs[strT] = CoherentElastic(bragg_edges, S)
distribution[strT] = CoherentElasticAE(xs[strT])
elif lhtr == 2:
# incoherent elastic
params, W = endf.get_tab1_record(file_obj)
bound_xs = params[0]
for T, debye_waller in zip(W.x, W.y):
strT = _temperature_str(T)
xs[strT] = IncoherentElastic(bound_xs, debye_waller)
distribution[strT] = IncoherentElasticAE(debye_waller)
elastic = ThermalScatteringReaction(xs, distribution)
# Read incoherent inelastic data
assert (7, 4) in ev.section, 'No MF=7, MT=4 found in thermal scattering'
file_obj = StringIO(ev.section[7, 4])
params = endf.get_head_record(file_obj)
data = {'symmetric': params[4] == 0}
# Get information about principal atom
params, B = endf.get_list_record(file_obj)
data['log'] = bool(params[2])
data['free_atom_xs'] = B[0]
data['epsilon'] = B[1]
data['A0'] = awr = B[2]
data['e_max'] = energy_max = B[3]
data['M0'] = B[5]
# Get information about non-principal atoms
n_non_principal = params[5]
data['non_principal'] = []
NonPrincipal = namedtuple('NonPrincipal', ['func', 'xs', 'A', 'M'])
for i in range(1, n_non_principal + 1):
func = {0.0: 'SCT', 1.0: 'free gas', 2.0: 'diffusive'}[B[6*i]]
xs = B[6*i + 1]
A = B[6*i + 2]
M = B[6*i + 5]
data['non_principal'].append(NonPrincipal(func, xs, A, M))
# Get S(alpha,beta,T)
kTs = []
if data['free_atom_xs'] > 0.0:
params, _ = endf.get_tab2_record(file_obj)
n_beta = params[5]
sab = {'beta': np.empty(n_beta)}
for i in range(n_beta):
params, S = endf.get_tab1_record(file_obj)
T0, beta, lt = params[:3]
if i == 0:
sab['alpha'] = alpha = S.x
sab[T0] = np.empty((alpha.size, n_beta))
kTs.append(K_BOLTZMANN * T0)
sab['beta'][i] = beta
sab[T0][:, i] = S.y
for _ in range(lt):
params, S = endf.get_list_record(file_obj)
T = params[0]
if i == 0:
sab[T] = np.empty((alpha.size, n_beta))
kTs.append(K_BOLTZMANN * T)
sab[T][:, i] = S
data['sab'] = sab
# Get effective temperature for each atom
_, Teff = endf.get_tab1_record(file_obj)
data['effective_temperature'] = [Teff]
for atom in data['non_principal']:
if atom.func == 'SCT':
_, Teff = endf.get_tab1_record(file_obj)
data['effective_temperature'].append(Teff)
name = ev.target['zsymam'].strip()
instance = cls(name, awr, energy_max, kTs)
if elastic is not None:
instance.elastic = elastic
# Currently we don't have a proper cross section or distribution for
# incoherent inelastic, so we just create an empty object and attach
# all the data as a dictionary
instance.inelastic = ThermalScatteringReaction(None, None)
instance.inelastic.data = data
return instance

View file

@ -0,0 +1,212 @@
import numpy as np
from .angle_energy import AngleEnergy
from .correlated import CorrelatedAngleEnergy
class CoherentElasticAE(AngleEnergy):
r"""Differential cross section for coherent elastic scattering
The differential cross section for coherent elastic scattering from a
powdered crystalline material may be represented as:
.. math::
\frac{d^2\sigma}{dE'd\Omega} (E\rightarrow E',\mu,T) = \frac{1}{E} \sum
\limits_{i=1}^{E_i < E} s_i(T) \delta(\mu - \mu_i) \delta (E - E')
/(2\pi)
where :math:`E_i` are the energies of the Bragg edges in [eV], :math:`s_i(T)`
is the structure factor in [eV-b] at the moderator temperature :math:`T`
in [K], and :math:`\mu_i = 1 - 2E_i/E`.
Parameters
----------
coherent_xs : openmc.data.CoherentElastic
Coherent elastic scattering cross section
Attributes
----------
coherent_xs : openmc.data.CoherentElastic
Coherent elastic scattering cross section
"""
def __init__(self, coherent_xs):
self.coherent_xs = coherent_xs
def to_hdf5(self, group):
"""Write coherent elastic distribution to an HDF5 group
Parameters
----------
group : h5py.Group
HDF5 group to write to
"""
group.attrs['type'] = np.string_('coherent_elastic')
group['coherent_xs'] = group.parent['xs']
class IncoherentElasticAE(AngleEnergy):
r"""Differential cross section for incoherent elastic scattering
The differential cross section for incoherent elastic scattering may be
represented as:
.. math::
\frac{d^2\sigma}{dE'd\Omega} (E\rightarrow E',\mu,T) = \frac{\sigma_b}
{4\pi} e^{-2EW'(T)(1-\mu)} \delta(E - E')
where :math:`\sigma_b` is the characteristic cross section in [b] and
:math:`W'(T)` is the Debye-Waller integral divided by the atomic mass in
[eV\ :math:`^{-1}`].
Parameters
----------
debye_waller : float
Debye-Waller integral in [eV\ :math:`^{-1}`]
Attributes
----------
debye_waller : float
Debye-Waller integral in [eV\ :math:`^{-1}`]
"""
def __init__(self, debye_waller):
self.debye_waller = debye_waller
def to_hdf5(self, group):
"""Write incoherent elastic distribution to an HDF5 group
Parameters
----------
group : h5py.Group
HDF5 group to write to
"""
group.attrs['type'] = np.string_('incoherent_elastic')
group.create_dataset('debye_waller', data=self.debye_waller)
@classmethod
def from_hdf5(cls, group):
"""Generate incoherent elastic distribution from HDF5 data
Parameters
----------
group : h5py.Group
HDF5 group to read from
Returns
-------
openmc.data.IncoherentElasticAE
Incoherent elastic distribution
"""
return cls(group['debye_waller'])
class IncoherentElasticAEDiscrete(AngleEnergy):
"""Discrete angle representation of incoherent elastic scattering
Parameters
----------
mu_out : numpy.ndarray
Equi-probable discrete angles at each incoming energy
"""
def __init__(self, mu_out):
self.mu_out = mu_out
def to_hdf5(self, group):
"""Write discrete incoherent elastic distribution to an HDF5 group
Parameters
----------
group : h5py.Group
HDF5 group to write to
"""
group.attrs['type'] = np.string_('incoherent_elastic_discrete')
group.create_dataset('mu_out', data=self.mu_out)
@classmethod
def from_hdf5(cls, group):
"""Generate discrete incoherent elastic distribution from HDF5 data
Parameters
----------
group : h5py.Group
HDF5 group to read from
Returns
-------
openmc.data.IncoherentElasticAEDiscrete
Discrete incoherent elastic distribution
"""
return cls(group['mu_out'][()])
class IncoherentInelasticAEDiscrete(AngleEnergy):
"""Discrete angle representation of incoherent inelastic scattering
Parameters
----------
energy_out : numpy.ndarray
Outgoing energies for each incoming energy
mu_out : numpy.ndarray
Discrete angles for each incoming/outgoing energy
skewed : bool
Whether discrete angles are equi-probable or have a skewed distribution
Attributes
----------
energy_out : numpy.ndarray
Outgoing energies for each incoming energy
mu_out : numpy.ndarray
Discrete angles for each incoming/outgoing energy
skewed : bool
Whether discrete angles are equi-probable or have a skewed distribution
"""
def __init__(self, energy_out, mu_out, skewed=False):
self.energy_out = energy_out
self.mu_out = mu_out
self.skewed = skewed
def to_hdf5(self, group):
"""Write discrete incoherent inelastic distribution to an HDF5 group
Parameters
----------
group : h5py.Group
HDF5 group to write to
"""
group.attrs['type'] = np.string_('incoherent_inelastic_discrete')
group.create_dataset('energy_out', data=self.energy_out)
group.create_dataset('mu_out', data=self.mu_out)
group.create_dataset('skewed', data=self.skewed)
@classmethod
def from_hdf5(cls, group):
"""Generate discrete incoherent inelastic distribution from HDF5 data
Parameters
----------
group : h5py.Group
HDF5 group to read from
Returns
-------
openmc.data.IncoherentInelasticAEDiscrete
Discrete incoherent inelastic distribution
"""
energy_out = group['energy_out'][()]
mu_out = group['mu_out'][()]
skewed = bool(group['skewed'])
return cls(energy_out, mu_out, skewed)
class IncoherentInelasticAE(CorrelatedAngleEnergy):
_name = 'incoherent_inelastic'

View file

@ -0,0 +1,95 @@
import numpy as np
import openmc.checkvalue as cv
from .angle_energy import AngleEnergy
from .energy_distribution import EnergyDistribution
from .angle_distribution import AngleDistribution
class UncorrelatedAngleEnergy(AngleEnergy):
"""Uncorrelated angle-energy distribution
Parameters
----------
angle : openmc.data.AngleDistribution
Distribution of outgoing angles represented as scattering cosines
energy : openmc.data.EnergyDistribution
Distribution of outgoing energies
Attributes
----------
angle : openmc.data.AngleDistribution
Distribution of outgoing angles represented as scattering cosines
energy : openmc.data.EnergyDistribution
Distribution of outgoing energies
"""
def __init__(self, angle=None, energy=None):
self._angle = None
self._energy = None
if angle is not None:
self.angle = angle
if energy is not None:
self.energy = energy
@property
def angle(self):
return self._angle
@property
def energy(self):
return self._energy
@angle.setter
def angle(self, angle):
cv.check_type('uncorrelated angle distribution', angle,
AngleDistribution)
self._angle = angle
@energy.setter
def energy(self, energy):
cv.check_type('uncorrelated energy distribution', energy,
EnergyDistribution)
self._energy = energy
def to_hdf5(self, group):
"""Write distribution to an HDF5 group
Parameters
----------
group : h5py.Group
HDF5 group to write to
"""
group.attrs['type'] = np.string_('uncorrelated')
if self.angle is not None:
angle_group = group.create_group('angle')
self.angle.to_hdf5(angle_group)
if self.energy is not None:
energy_group = group.create_group('energy')
self.energy.to_hdf5(energy_group)
@classmethod
def from_hdf5(cls, group):
"""Generate uncorrelated angle-energy distribution from HDF5 data
Parameters
----------
group : h5py.Group
HDF5 group to read from
Returns
-------
openmc.data.UncorrelatedAngleEnergy
Uncorrelated angle-energy distribution
"""
dist = cls()
if 'angle' in group:
dist.angle = AngleDistribution.from_hdf5(group['angle'])
if 'energy' in group:
dist.energy = EnergyDistribution.from_hdf5(group['energy'])
return dist

215
openmc/data/urr.py Normal file
View file

@ -0,0 +1,215 @@
from collections.abc import Iterable
from numbers import Integral, Real
import numpy as np
import openmc.checkvalue as cv
from openmc.mixin import EqualityMixin
from .data import EV_PER_MEV
class ProbabilityTables(EqualityMixin):
r"""Unresolved resonance region probability tables.
Parameters
----------
energy : Iterable of float
Energies in eV at which probability tables exist
table : numpy.ndarray
Probability tables for each energy. This array is of shape (N, 6, M)
where N is the number of energies and M is the number of bands. The
second dimension indicates whether the value is for the cumulative
probability (0), total (1), elastic (2), fission (3), :math:`(n,\gamma)`
(4), or heating number (5).
interpolation : {2, 5}
Interpolation scheme between tables
inelastic_flag : int
A value less than zero indicates that the inelastic cross section is
zero within the unresolved energy range. A value greater than zero
indicates the MT number for a reaction whose cross section is to be used
in the unresolved range.
absorption_flag : int
A value less than zero indicates that the "other absorption" cross
section is zero within the unresolved energy range. A value greater than
zero indicates the MT number for a reaction whose cross section is to be
used in the unresolved range.
multiply_smooth : bool
Indicate whether probability table values are cross sections (False) or
whether they must be multiply by the corresponding "smooth" cross
sections (True).
Attributes
----------
energy : Iterable of float
Energies in eV at which probability tables exist
table : numpy.ndarray
Probability tables for each energy. This array is of shape (N, 6, M)
where N is the number of energies and M is the number of bands. The
second dimension indicates whether the value is for the cumulative
probability (0), total (1), elastic (2), fission (3), :math:`(n,\gamma)`
(4), or heating number (5).
interpolation : {2, 5}
Interpolation scheme between tables
inelastic_flag : int
A value less than zero indicates that the inelastic cross section is
zero within the unresolved energy range. A value greater than zero
indicates the MT number for a reaction whose cross section is to be used
in the unresolved range.
absorption_flag : int
A value less than zero indicates that the "other absorption" cross
section is zero within the unresolved energy range. A value greater than
zero indicates the MT number for a reaction whose cross section is to be
used in the unresolved range.
multiply_smooth : bool
Indicate whether probability table values are cross sections (False) or
whether they must be multiply by the corresponding "smooth" cross
sections (True).
"""
def __init__(self, energy, table, interpolation, inelastic_flag=-1,
absorption_flag=-1, multiply_smooth=False):
self.energy = energy
self.table = table
self.interpolation = interpolation
self.inelastic_flag = inelastic_flag
self.absorption_flag = absorption_flag
self.multiply_smooth = multiply_smooth
@property
def absorption_flag(self):
return self._absorption_flag
@property
def energy(self):
return self._energy
@property
def inelastic_flag(self):
return self._inelastic_flag
@property
def interpolation(self):
return self._interpolation
@property
def multiply_smooth(self):
return self._multiply_smooth
@property
def table(self):
return self._table
@absorption_flag.setter
def absorption_flag(self, absorption_flag):
cv.check_type('absorption flag', absorption_flag, Integral)
self._absorption_flag = absorption_flag
@energy.setter
def energy(self, energy):
cv.check_type('probability table energies', energy, Iterable, Real)
self._energy = energy
@inelastic_flag.setter
def inelastic_flag(self, inelastic_flag):
cv.check_type('inelastic flag', inelastic_flag, Integral)
self._inelastic_flag = inelastic_flag
@interpolation.setter
def interpolation(self, interpolation):
cv.check_value('interpolation', interpolation, [2, 5])
self._interpolation = interpolation
@multiply_smooth.setter
def multiply_smooth(self, multiply_smooth):
cv.check_type('multiply by smooth', multiply_smooth, bool)
self._multiply_smooth = multiply_smooth
@table.setter
def table(self, table):
cv.check_type('probability tables', table, np.ndarray)
self._table = table
def to_hdf5(self, group):
"""Write probability tables to an HDF5 group
Parameters
----------
group : h5py.Group
HDF5 group to write to
"""
group.attrs['interpolation'] = self.interpolation
group.attrs['inelastic'] = self.inelastic_flag
group.attrs['absorption'] = self.absorption_flag
group.attrs['multiply_smooth'] = int(self.multiply_smooth)
group.create_dataset('energy', data=self.energy)
group.create_dataset('table', data=self.table)
@classmethod
def from_hdf5(cls, group):
"""Generate probability tables from HDF5 data
Parameters
----------
group : h5py.Group
HDF5 group to read from
Returns
-------
openmc.data.ProbabilityTables
Probability tables
"""
interpolation = group.attrs['interpolation']
inelastic_flag = group.attrs['inelastic']
absorption_flag = group.attrs['absorption']
multiply_smooth = bool(group.attrs['multiply_smooth'])
energy = group['energy'][()]
table = group['table'][()]
return cls(energy, table, interpolation, inelastic_flag,
absorption_flag, multiply_smooth)
@classmethod
def from_ace(cls, ace):
"""Generate probability tables from an ACE table
Parameters
----------
ace : openmc.data.ace.Table
ACE table to read from
Returns
-------
openmc.data.ProbabilityTables
Unresolved resonance region probability tables
"""
# Check if URR probability tables are present
idx = ace.jxs[23]
if idx == 0:
return None
N = int(ace.xss[idx]) # Number of incident energies
M = int(ace.xss[idx+1]) # Length of probability table
interpolation = int(ace.xss[idx+2])
inelastic_flag = int(ace.xss[idx+3])
absorption_flag = int(ace.xss[idx+4])
multiply_smooth = (int(ace.xss[idx+5]) == 1)
idx += 6
# Get energies at which tables exist
energy = ace.xss[idx : idx+N]*EV_PER_MEV
idx += N
# Get probability tables
table = ace.xss[idx : idx+N*6*M].copy()
table.shape = (N, 6, M)
# Convert units on heating numbers
table[:,5,:] *= EV_PER_MEV
return cls(energy, table, interpolation, inelastic_flag,
absorption_flag, multiply_smooth)