mirror of
https://github.com/openmc-dev/openmc.git
synced 2026-07-28 14:15:42 -04:00
adding back files to be reviewed
This commit is contained in:
parent
ae28233110
commit
bc09d1ef55
1244 changed files with 301904 additions and 0 deletions
28521
openmc/data/BREMX.DAT
Normal file
28521
openmc/data/BREMX.DAT
Normal file
File diff suppressed because it is too large
Load diff
35
openmc/data/__init__.py
Normal file
35
openmc/data/__init__.py
Normal 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
8
openmc/data/_endf.pyx
Normal 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
460
openmc/data/ace.py
Normal 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)
|
||||
310
openmc/data/angle_distribution.py
Normal file
310
openmc/data/angle_distribution.py
Normal 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
117
openmc/data/angle_energy.py
Normal 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
|
||||
BIN
openmc/data/compton_profiles.h5
Normal file
BIN
openmc/data/compton_profiles.h5
Normal file
Binary file not shown.
462
openmc/data/correlated.py
Normal file
462
openmc/data/correlated.py
Normal 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
385
openmc/data/data.py
Normal 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
484
openmc/data/decay.py
Normal 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)
|
||||
BIN
openmc/data/density_effect.h5
Normal file
BIN
openmc/data/density_effect.h5
Normal file
Binary file not shown.
39
openmc/data/endf.c
Normal file
39
openmc/data/endf.c
Normal 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
550
openmc/data/endf.py
Normal 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
|
||||
1277
openmc/data/energy_distribution.py
Normal file
1277
openmc/data/energy_distribution.py
Normal file
File diff suppressed because it is too large
Load diff
380
openmc/data/fission_energy.py
Normal file
380
openmc/data/fission_energy.py
Normal 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
716
openmc/data/function.py
Normal 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
114
openmc/data/grid.py
Normal 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
402
openmc/data/kalbach_mann.py
Normal 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
142
openmc/data/laboratory.py
Normal 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
173
openmc/data/library.py
Normal 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
3475
openmc/data/mass16.txt
Normal file
File diff suppressed because it is too large
Load diff
535
openmc/data/multipole.py
Normal file
535
openmc/data/multipole.py
Normal 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
165
openmc/data/nbody.py
Normal 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
926
openmc/data/neutron.py
Normal 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
593
openmc/data/njoy.py
Normal 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
1306
openmc/data/photon.py
Normal file
File diff suppressed because it is too large
Load diff
187
openmc/data/product.py
Normal file
187
openmc/data/product.py
Normal 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
1189
openmc/data/reaction.py
Normal file
File diff suppressed because it is too large
Load diff
522
openmc/data/reconstruct.pyx
Normal file
522
openmc/data/reconstruct.pyx
Normal 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
1071
openmc/data/resonance.py
Normal file
File diff suppressed because it is too large
Load diff
708
openmc/data/resonance_covariance.py
Normal file
708
openmc/data/resonance_covariance.py
Normal 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
927
openmc/data/thermal.py
Normal 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
|
||||
212
openmc/data/thermal_angle_energy.py
Normal file
212
openmc/data/thermal_angle_energy.py
Normal 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'
|
||||
95
openmc/data/uncorrelated.py
Normal file
95
openmc/data/uncorrelated.py
Normal 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
215
openmc/data/urr.py
Normal 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue