mirror of
https://github.com/openmc-dev/openmc.git
synced 2026-07-21 14:35:27 -04:00
1199 lines
46 KiB
Python
1199 lines
46 KiB
Python
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 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, isotopes
|
|
from .ace import Table, get_table, Library
|
|
from .angle_energy import AngleEnergy
|
|
from .function import Tabulated1D, Function1D, Sum
|
|
from .njoy import make_ace_thermal
|
|
from .thermal_angle_energy import (CoherentElasticAE, IncoherentElasticAE,
|
|
IncoherentElasticAEDiscrete,
|
|
IncoherentInelasticAEDiscrete,
|
|
IncoherentInelasticAE, MixedElasticAE)
|
|
|
|
|
|
_THERMAL_NAMES = {
|
|
'c_Ag': ('ag',),
|
|
'c_Al27': ('al', 'al27', 'al-27', '13-al- 27'),
|
|
'c_Al_in_Al2O3': ('asap00', 'asap', 'al(al2o3)'),
|
|
'c_Al_in_Y3Al5O12': ('al(y3al5o1', 'alyag'),
|
|
'c_Au': ('au',),
|
|
'c_Be': ('be', 'be-metal', 'be-met', 'be00', 'be-metal', 'be metal', '4-be', '4-be-'),
|
|
'c_BeO': ('beo',),
|
|
'c_Be_distinct': ('besd', 'be+sd'),
|
|
'c_Be_in_BeO': ('bebeo', 'be-beo', 'be-o', 'be/o', 'bbeo00', 'be(beo)', 'be_beo'),
|
|
'c_Be_in_Be2C': ('bebe2c', 'be(be2c)'),
|
|
'c_Be_in_BeF2': ('bebef2', 'be in bef2'),
|
|
'c_Be_in_FLiBe': ('beflib', 'be(flibe)'),
|
|
'c_Bi': ('83-bi-', 'bi'),
|
|
'c_Bi_in_Ge3Bi4O12': ('bi(ge3bi4o', 'bigbo'),
|
|
'c_C6H6': ('benz', 'c6h6', 'benzine'),
|
|
'c_C_in_Be2C': ('cbe2c', 'c(be2c)'),
|
|
'c_C_in_C19H16_liquid': ('c(c19h16)l', 'c19liq'),
|
|
'c_C_in_C19H16_solid': ('c(c19h16)s', 'c19sol'),
|
|
'c_C_in_C2H6O_liquid': ('c(c2h6o)l', 'ethliq'),
|
|
'c_C_in_C2H6O_solid': ('c(c2h6o)s', 'ethsol'),
|
|
'c_C_in_C5O2H8': ('clucit', 'c(lucite)'),
|
|
'c_C_in_C6H6_liquid': ('c(c6h6)l', 'benzlq'),
|
|
'c_C_in_C6H6_solid': ('c(c6h6)s', 'benzsl'),
|
|
'c_C_in_C7H8_liquid': ('c(c7h8)l', 'tolliq'),
|
|
'c_C_in_C7H8_solid': ('c(c7h8)s', 'tolsol'),
|
|
'c_C_in_C8H8': ('cc8h8', 'c(polystyr'),
|
|
'c_C_in_C8H10_liquid': ('c(m-c8h10)l', 'xylliq'),
|
|
'c_C_in_C8H10_solid': ('c(m-c8h10)s', 'xylsol'),
|
|
'c_C_in_C9H12_liquid': ('c(c9h12)l', 'mesliq'),
|
|
'c_C_in_C9H12_solid': ('c(c9h12)s', 'messol'),
|
|
'c_C_in_CF2': ('ccf2', 'c(teflon)'),
|
|
'c_C_in_CH2': ('c(c2h4)n r', 'cch2'),
|
|
'c_C_in_CH4_liquid': ('c(ch4)l', 'cch4lq'),
|
|
'c_C_in_CH4_solid': ('c(ch4)s', 'cch4sl'),
|
|
'c_C_in_Diamond': ('c(c-diamon', 'cdiam'),
|
|
'c_C_in_SiC': ('csic', 'c-sic', 'c(3c-sic)', 'c_sic'),
|
|
'c_C_in_UC_100p': ('cuc100', 'cinuc_100p'),
|
|
'c_C_in_UC_10p': ('cuc10', 'cinuc_10p'),
|
|
'c_C_in_UC_5p': ('cuc5', 'cinuc_5p'),
|
|
'c_C_in_UC': ('cinuc', 'cinuc_nat'),
|
|
'c_C_in_UC_HALEU': ('cuchal', 'cinuc_haleu'),
|
|
'c_C_in_UC_HEU': ('cucheu', 'cinuc_heu'),
|
|
'c_C_in_ZrC': ('czrc', 'c(zrc)'),
|
|
'c_Ca': ('ca',),
|
|
'c_Ca_in_CaH2': ('cah', 'cah00', 'cacah2', 'ca(cah2)', 'ca_cah2'),
|
|
'c_Ca_in_CaO2H2': ('ca(caoh2)', 'cacaoh'),
|
|
'c_Cr': ('cr',),
|
|
'c_Cu': ('cu',),
|
|
'c_D_in_7LiD': ('dlid', 'd(7lid)'),
|
|
'c_D_in_D2O': ('dd2o', 'd-d2o', 'hwtr', 'hw', 'dhw00', 'd(d2o)'),
|
|
'c_D_in_D2O_solid': ('dice',),
|
|
'c_D_in_MgD2': ('d(mgd2)', 'dmgd2'),
|
|
'c_F_in_Be2': ('fbef2', 'f in bef2'),
|
|
'c_F_in_CF2': ('fcf2', 'f(teflon)'),
|
|
'c_F_in_FLiBe': ('fflibe', 'f(flibe)'),
|
|
'c_F_in_HF': ('f_hf',),
|
|
'c_F_in_LiF': ('f(lif)', 'flif'),
|
|
'c_F_in_MgF2': ('fmgf2', 'f in mgf2'),
|
|
'c_Fe56': ('fe', 'fe56', 'fe-56', '26-fe- 56'),
|
|
'c_Fe_in_Fe_alpha': ('fe(fe-alph', 'fealph'),
|
|
'c_Fe_in_Fe_gamma': ('fe(fe-gamm', 'fegamm'),
|
|
'c_Ga_in_GaN': ('ga(gan)', 'gagan'),
|
|
'c_Ga_in_GaSe': ('ga(gase)', 'gagase'),
|
|
'c_Ge': ('ge',),
|
|
'c_Ge_in_Ge3Bi4O12': ('ge(ge3bi4o', 'gegbo'),
|
|
'c_Ge_in_GeTe': ('ge(gete)', 'gegete'),
|
|
'c_Graphite': ('graph', 'grph', 'gr', 'gr00', 'graphite'),
|
|
'c_Graphite_10p': ('grph10', '10p graphit'),
|
|
'c_Graphite_20p': ('grph20', '20 graphite'),
|
|
'c_Graphite_30p': ('grph30', '30p graphit'),
|
|
'c_Graphite_distinct': ('grphsd', 'grph+sd'),
|
|
'c_H_in_7LiH': ('hlih', 'h(7lih)'),
|
|
'c_H_in_C5O2H8': ('lucite', 'c5o2h8', 'h-luci', 'h(lucite)'),
|
|
'c_H_in_C8H8': ('hc8h8', 'h(polystyr'),
|
|
'c_H_in_CaH2': ('hcah2', 'hca00', 'h(cah2)'),
|
|
'c_H_in_CaO2H2': ('h(caoh2)', 'hcaoh'),
|
|
'c_H1_in_CaH2': ('h1cah2', 'h1_cah2'),
|
|
'c_H2_in_CaH2': ('h2cah2', 'h2_cah2'),
|
|
'c_H_in_C19H16_liquid': ('h(c19h16)l', 'h19liq'),
|
|
'c_H_in_C19H16_solid': ('h(c19h16)s', 'h19sol'),
|
|
'c_H_in_C2H6O_liquid': ('h(c2h6o)l', 'hetliq'),
|
|
'c_H_in_C2H6O_solid': ('h(c2h6o)s', 'hetsol'),
|
|
'c_H_in_C6H6_liquid': ('h(c6h6)l', 'hbzliq'),
|
|
'c_H_in_C6H6_solid': ('h(c6h6)s', 'hbzsol'),
|
|
'c_H_in_C7H8_liquid': ('h(c7h8)l', 'htlliq'),
|
|
'c_H_in_C7H8_solid': ('h(c7h8)s', 'htlsol'),
|
|
'c_H_in_C8H10_liquid': ('h(m-c8h10)l', 'hxyliq'),
|
|
'c_H_in_C8H10_solid': ('h(m-c8h10)s', 'hxysol'),
|
|
'c_H_in_C9H12_liquid': ('h(c9h12)l', 'hmsliq'),
|
|
'c_H_in_C9H12_solid': ('h(c9h12)s', 'hmssol'),
|
|
'c_H_in_CH2': ('hch2', 'poly', 'pol', 'h-poly', 'pol00', 'h(ch2)', 'h(c2h4)n r'),
|
|
'c_H_in_CH4_liquid': ('lch4', 'lmeth', 'l-ch4', 'h(ch4)l'),
|
|
'c_H_in_CH4_solid': ('sch4', 'smeth', 's-ch4', 'h(ch4)s'),
|
|
'c_H_in_CH4_solid_phase_II': ('sch4p2',),
|
|
'c_H_in_H2O': ('hh2o', 'h-h2o', 'lwtr', 'lw', 'lw00', 'h(h2o)'),
|
|
'c_H_in_H2O_solid': ('hice', 'h-ice', 'ice00', 'h(ice-ih)', 'h(ice)'),
|
|
'c_H_in_HF': ('hhf', 'h(hf)', 'h_hf'),
|
|
'c_H_in_KOH': ('h(koh)', 'hkoh'),
|
|
'c_H_in_LiH': ('h(lih)', 'hlih2'),
|
|
'c_H_in_Mesitylene': ('mesi00', 'mesi', 'mesi-phii'),
|
|
'c_H_in_MgH2': ('h(mgh2)', 'hmgh2'),
|
|
'c_H_in_MgOH2': ('h(mgoh2)', 'hmgoh'),
|
|
'c_H_in_NaMgH3': ('h(namgh3)', 'hnamg'),
|
|
'c_H_in_NaOH': ('h(naoh)', 'hnaoh'),
|
|
'c_H_in_ParaffinicOil': ('hparaf', 'h(paraffin', 'h(paraffini'),
|
|
'c_H_in_SrH2': ('h(srh2)', 'hsrh2'),
|
|
'c_H_in_Toluene': ('tol00', 'tol', 'tolue-phii'),
|
|
'c_H_in_UH3': ('huh3', 'h(uh3)'),
|
|
'c_H_in_YH2': ('hyh2', 'h-yh2', 'h(yh2)'),
|
|
'c_H_in_ZrH': ('hzrh', 'h-zrh', 'h-zr', 'h/zr', 'hzr', 'hzr00', 'h(zrh)'),
|
|
'c_H_in_ZrH2': ('hzrh2', 'h(zrh2)', 'h(zrh2) in'),
|
|
'c_H_in_ZrHx': ('hzrhx', 'h(zrhx)', 'h(zrh15) i'),
|
|
'c_I_in_NaI': ('i(nai)', 'inai'),
|
|
'c_K': ('k',),
|
|
'c_K_in_KOH': ('k(koh)', 'kkoh'),
|
|
'c_Li_in_FLiBe': ('liflib', 'li(flibe)'),
|
|
'c_Li_in_7LiD': ('lilid', '7li(7lid)'),
|
|
'c_Li_in_7LiH': ('lilih', '7li(7lih)'),
|
|
'c_Li_in_LiF': ('li(lif)', 'lilif'),
|
|
'c_Li_in_LiH': ('li(lih)', 'lilih2'),
|
|
'c_Mg24': ('mg', 'mg24', 'mg00', '24-mg'),
|
|
'c_Mg_in_MgD2': ('mg(mgd2)', 'mgmgd2'),
|
|
'c_Mg_in_MgF2': ('mgmgf2', 'mg in mgf2'),
|
|
'c_Mg_in_MgH2': ('mg(mgh2)', 'mgmgh2'),
|
|
'c_Mg_in_MgO': ('mgmgo', 'mg in mgo'),
|
|
'c_Mg_in_MgOH2': ('mg(mgoh2)', 'mgoh2'),
|
|
'c_Mg_in_NaMgH3': ('mg(namgh3)', 'mgnamg'),
|
|
'c_Mo': ('mo',),
|
|
'c_N_in_GaN': ('n(gan)', 'ngan'),
|
|
'c_N_in_UN_100p': ('nun100', 'n-un-100p'),
|
|
'c_N_in_UN_10p': ('nun10', 'n-un-10p'),
|
|
'c_N_in_UN_5p': ('nun5', 'n-un-5p'),
|
|
'c_N_in_UN': ('n-un', 'n(un)', 'n(un) l', 'ninun'),
|
|
'c_N_in_UN_HALEU': ('nunhal', 'n-un-haleu'),
|
|
'c_N_in_UN_HEU': ('nunheu', 'n-un-heu'),
|
|
'c_Na': ('na',),
|
|
'c_Na_in_NaI': ('na(nai)', 'nanai'),
|
|
'c_Na_in_NaMgH3': ('na(namgh3)', 'nanamg'),
|
|
'c_Na_in_NaOH': ('na(naoh)', 'nanaoh'),
|
|
'c_Nb': ('nb',),
|
|
'c_Ni': ('ni',),
|
|
'c_O_in_Al2O3': ('osap00', 'osap', 'o(al2o3)'),
|
|
'c_O_in_BeO': ('obeo', 'o-beo', 'o-be', 'o/be', 'obeo00', 'o(beo)', 'o_beo'),
|
|
'c_O_in_C2H6O_liquid': ('o(c2h6o)l', 'oetliq'),
|
|
'c_O_in_C2H6O_solid': ('o(c2h6o)s', 'oetsol'),
|
|
'c_O_in_C5O2H8': ('olucit', 'o(lucite)'),
|
|
'c_O_in_CaO2H2': ('o(caoh2)', 'ocaoh'),
|
|
'c_O_in_D2O': ('od2o', 'o-d2o', 'ohw00', 'o(d2o)'),
|
|
'c_O_in_H2O': ('o(h2o)', 'oh2o'),
|
|
'c_O_in_H2O_solid': ('oice', 'o-ice', 'o(ice-ih)'),
|
|
'c_O_in_Ge3Bi4O12': ('o(ge3bi4o1', 'ogbo'),
|
|
'c_O_in_KOH': ('o(koh)', 'okoh'),
|
|
'c_O_in_MgO': ('omgo', 'o in mgo'),
|
|
'c_O_in_MgOH2': ('o(mgoh2)', 'omgoh'),
|
|
'c_O_in_NaOH': ('o(naoh)', 'onaoh'),
|
|
'c_O_in_PuO2': ('opuo2', 'o in puo2'),
|
|
'c_O_in_SiO2_alpha': ('osio2a', 'o_sio2a'),
|
|
'c_O_in_UO2_100p': ('ouo200', 'o-uo2-100p'),
|
|
'c_O_in_UO2_10p': ('ouo210', 'oinuo2-10p'),
|
|
'c_O_in_UO2_5p': ('ouo25', 'oinuo2-5p'),
|
|
'c_O_in_UO2': ('ouo2', 'o-uo2', 'o2-u', 'o2/u', 'ouo200', 'o(uo2)'),
|
|
'c_O_in_UO2_HALEU': ('ouo2hl', 'ouo2-haleu'),
|
|
'c_O_in_UO2_HEU': ('ouo2he', 'o_uo2-heu'),
|
|
'c_O_in_Y3Al5O12': ('o(y3al5o12', 'oyag'),
|
|
'c_ortho_D': ('orthod', 'orthoD', 'dortho', 'od200', 'ortod', 'ortho-d'),
|
|
'c_ortho_H': ('orthoh', 'orthoH', 'hortho', 'oh200', 'ortoh', 'ortho-h'),
|
|
'c_para_D': ('parad', 'paraD', 'dpara', 'pd200', 'para-d'),
|
|
'c_para_H': ('parah', 'paraH', 'hpara', 'ph200', 'para-h'),
|
|
'c_Pb': ('pb',),
|
|
'c_Pd': ('pd',),
|
|
'c_Pt': ('pt',),
|
|
'c_Pu_in_PuO2': ('puo2', 'pu in puo2'),
|
|
'c_Si28': ('si00', 'sili', 'si'),
|
|
'c_Si_in_SiC': ('sisic', 'si-sic', 'si(3c-sic)', 'si_sic'),
|
|
'c_Si_in_SiO2_alpha': ('si_o2a', 'si_sio2a'),
|
|
'c_SiO2_alpha': ('sio2', 'sio2a', 'sio2alpha', 'sio2-a'),
|
|
'c_SiO2_beta': ('sio2b', 'sio2beta', 'sio2-b'),
|
|
'c_S_in_ZnS': ('s(zns-spha', 'szns'),
|
|
'c_Se_in_GaSe': ('se(gase)', 'segase'),
|
|
'c_Sn': ('sn',),
|
|
'c_Sr_in_SrH2': ('sr(srh2)', 'srsrh2'),
|
|
'c_Te_in_GeTe': ('te(gete)', 'tegete'),
|
|
'c_Ti': ('ti',),
|
|
'c_U_metal_100p': ('u-100p',),
|
|
'c_U_metal_10p': ('u-10p',),
|
|
'c_U_metal_5p': ('u-5p',),
|
|
'c_U_metal': ('umetal', 'u-metal'),
|
|
'c_U_metal_HALEU': ('uhaleu', 'u-haleu'),
|
|
'c_U_metal_HEU': ('u-heu',),
|
|
'c_U_in_UC_100p': ('uc-100', 'uinuc_100p'),
|
|
'c_U_in_UC_10p': ('uc-10', 'uinuc_10p'),
|
|
'c_U_in_UC_5p': ('uc-5', 'uinuc_5p'),
|
|
'c_U_in_UC': ('uc-nat', 'uinuc_nat'),
|
|
'c_U_in_UC_HALEU': ('uc-hal', 'uinuc_haleu'),
|
|
'c_U_in_UC_HEU': ('uc-heu', 'uinuc_heu'),
|
|
'c_U_in_UN_100p': ('un-100', 'u-un-100p'),
|
|
'c_U_in_UN_10p': ('un-10', 'u-un-10p'),
|
|
'c_U_in_UN_5p': ('un-5', 'u-un-5p'),
|
|
'c_U_in_UN': ('u-un', 'u(un)', 'u(un) l', 'uinun'),
|
|
'c_U_in_UN_HALEU': ('un-hal', 'u-un-haleu'),
|
|
'c_U_in_UN_HEU': ('un-heu', 'u-un-heu'),
|
|
'c_U_in_UO2_100p': ('uo2100', 'uuo2-100p'),
|
|
'c_U_in_UO2_10p': ('uo2-10', 'uuo2-10p'),
|
|
'c_U_in_UO2_5p': ('uo2-5', 'uuo2-5p'),
|
|
'c_U_in_UO2': ('uuo2', 'u-uo2', 'u-o2', 'u/o2', 'uuo200', 'u(uo2)'),
|
|
'c_U_in_UO2_HALEU': ('uo2hal', 'uuo2-haleu'),
|
|
'c_U_in_UO2_HEU': ('uo2heu', 'u_uo2-heu'),
|
|
'c_V': ('v',),
|
|
'c_W': ('w',),
|
|
'c_Y_in_Y3Al5O12': ('y(y3al5o12', 'yyag'),
|
|
'c_Y_in_YH2': ('yyh2', 'y-yh2', 'y(yh2)'),
|
|
'c_Zn': ('zn',),
|
|
'c_Zn_in_ZnS': ('zn(zns-sph', 'znzns'),
|
|
'c_Zr': ('zr',),
|
|
'c_Zr_in_ZrC': ('zrzrc', 'zr(zrc)'),
|
|
'c_Zr_in_ZrH': ('zrzrh', 'zr-zrh', 'zr-h', 'zr/h', 'zr(zrh)'),
|
|
'c_Zr_in_ZrH2': ('zrzrh2', 'zr(zrh2)', 'zr(zrh2) i'),
|
|
'c_Zr_in_ZrHx': ('zrzrhx', 'zr(zrhx)', 'zr(zrh15)'),
|
|
}
|
|
|
|
|
|
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 f"{int(round(T))}K"
|
|
|
|
|
|
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
|
|
GNDS-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
|
|
|
|
@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)
|
|
|
|
@property
|
|
def factors(self):
|
|
return self._factors
|
|
|
|
@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.bytes_(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.bytes_(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 GNDS 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 GNDS 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 f"<Thermal Scattering Data: {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+', '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.bytes_('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
|
|
GNDS-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(f'S(a,b) data at T={strT} already exists.')
|
|
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
|
|
GNDS-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(f"{ace} is not a thermal scattering ACE table.")
|
|
if name is None:
|
|
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]
|
|
# The equiprobable angles produced by NJOY are not always
|
|
# sorted. This is problematic when the smearing algorithm
|
|
# is applied when sampling the angles. We sort the angles
|
|
# here, because they are equiprobable, so the order
|
|
# doesn't matter.
|
|
mu.sort()
|
|
|
|
# Older versions of NJOY had a bug, and the discrete
|
|
# scattering angles could sometimes be less than -1 or
|
|
# greater than 1. We check for this here, and warn users.
|
|
if mu[0] < -1. or mu[-1] > 1.:
|
|
warn('S(a,b) scattering angle for incident energy index '
|
|
f'{i} and exit energy index {j} outside of the '
|
|
'interval [-1, 1].')
|
|
|
|
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
|
|
|
|
# Check if the CDF for the outgoing energy distribution starts
|
|
# at 0. For NJOY and FRENDY evaluations, this is never the case,
|
|
# and can very rarely lead to negative energies when sampling
|
|
# the outgoing energy. From Eq. 7.6 of the ENDF manual, we can
|
|
# add an outgoing energy 0 eV that has a PDF of 0 (and of
|
|
# course, a CDF of 0 as well).
|
|
if eout_i.c[0] > 0.:
|
|
eout_i._x = np.insert(eout_i.x, 0, 0.)
|
|
eout_i._p = np.insert(eout_i.p, 0, 0.)
|
|
eout_i.c = np.insert(eout_i.c, 0, 0.)
|
|
|
|
# For this added outgoing energy (of 0 eV) we add a set of
|
|
# isotropic discrete angles.
|
|
dmu = 2. / n_mu
|
|
mu = np.linspace(-1. + 0.5*dmu, 1. - 0.5*dmu, n_mu)
|
|
p_mu = 1. / n_mu * np.ones(n_mu)
|
|
mu_0 = Discrete(mu, p_mu)
|
|
mu_0.c = np.cumsum(p_mu)
|
|
mu_i.insert(0, mu_0)
|
|
# We don't worry about renormalizing the outgoing energy PDF/CDF
|
|
# after this manipulation, because it never seems to be
|
|
# normalized to begin with (at least with NJOY).
|
|
|
|
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]
|
|
if idx != 0:
|
|
if ace.nxs[5] in (4, 5):
|
|
# Coherent elastic
|
|
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]
|
|
coherent_xs = CoherentElastic(energy, P*EV_PER_MEV)
|
|
coherent_dist = CoherentElasticAE(coherent_xs)
|
|
|
|
# Coherent elastic shouldn't have angular distributions listed
|
|
n_mu = ace.nxs[6] + 1
|
|
assert n_mu == 0
|
|
|
|
if ace.nxs[5] in (3, 5):
|
|
# Incoherent elastic scattering -- first determine if both
|
|
# incoherent and coherent are present (mixed)
|
|
mixed = (ace.nxs[5] == 5)
|
|
|
|
# Get cross section values
|
|
idx = ace.jxs[7] if mixed else ace.jxs[4]
|
|
n_energy = int(ace.xss[idx])
|
|
energy = ace.xss[idx + 1: idx + 1 + n_energy]*EV_PER_MEV
|
|
values = ace.xss[idx + 1 + n_energy: idx + 1 + 2 * n_energy]
|
|
|
|
incoherent_xs = Tabulated1D(energy, values)
|
|
|
|
# Angular distribution
|
|
n_mu = (ace.nxs[8] if mixed else ace.nxs[6]) + 1
|
|
assert n_mu > 0
|
|
idx = ace.jxs[9] if mixed else ace.jxs[6]
|
|
mu_out = ace.xss[idx:idx + n_energy * n_mu]
|
|
mu_out.shape = (n_energy, n_mu)
|
|
incoherent_dist = IncoherentElasticAEDiscrete(mu_out)
|
|
|
|
if ace.nxs[5] == 3:
|
|
xs = incoherent_xs
|
|
distribution = incoherent_dist
|
|
elif ace.nxs[5] == 4:
|
|
xs = coherent_xs
|
|
distribution = coherent_dist
|
|
else:
|
|
# Create mixed cross section -- note that coherent must come
|
|
# first due to assumption on C++ side
|
|
xs = Sum([coherent_xs, incoherent_xs])
|
|
|
|
# Create mixed distribution
|
|
distribution = MixedElasticAE(coherent_dist, incoherent_dist)
|
|
|
|
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 isotopes(element):
|
|
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, divide_incoherent_elastic=False, **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.
|
|
divide_incoherent_elastic : bool
|
|
Divide incoherent elastic cross section by number of principal
|
|
atoms. This is not part of the ENDF-6 standard but it is how it is
|
|
processed by NJOY.
|
|
**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('output_dir', tmpdir)
|
|
kwargs.setdefault('ace', os.path.join(kwargs['output_dir'], 'ace'))
|
|
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'])
|
|
name = kwargs.get('table_name')
|
|
data = cls.from_ace(lib.tables[0], name=name)
|
|
for table in lib.tables[1:]:
|
|
data.add_temperature_from_ace(table, name=name)
|
|
|
|
# Load ENDF data to replace incoherent elastic
|
|
if use_endf_data:
|
|
data_endf = cls.from_endf(filename_thermal, divide_incoherent_elastic)
|
|
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, Sum)):
|
|
rx.xs[t] = rx_endf.xs[t]
|
|
rx.distribution[t] = rx_endf.distribution[t]
|
|
|
|
return data
|
|
|
|
@classmethod
|
|
def from_endf(cls, ev_or_filename, divide_incoherent_elastic=False):
|
|
"""Generate thermal scattering data from an ENDF file
|
|
|
|
Parameters
|
|
----------
|
|
ev_or_filename : openmc.data.endf.Evaluation, endf.Material, or str
|
|
ENDF evaluation to read from. If given as a string, it is assumed to
|
|
be the filename for the ENDF file.
|
|
divide_incoherent_elastic : bool
|
|
Divide incoherent elastic cross section by number of principal
|
|
atoms. This is not part of the ENDF-6 standard but it is how it is
|
|
processed by NJOY.
|
|
|
|
Returns
|
|
-------
|
|
openmc.data.ThermalScattering
|
|
Thermal scattering data
|
|
|
|
"""
|
|
ev = endf.as_evaluation(ev_or_filename)
|
|
|
|
# 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]
|
|
free_xs = data['free_atom_xs'] / data['M0']
|
|
|
|
# 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)
|
|
|
|
# Read coherent/incoherent elastic data
|
|
elastic = None
|
|
if (7, 2) in ev.section:
|
|
# Define helper functions to avoid duplication
|
|
def get_coherent_elastic(file_obj):
|
|
# 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])
|
|
return xs, distribution
|
|
|
|
def get_incoherent_elastic(file_obj, natom):
|
|
params, W = endf.get_tab1_record(file_obj)
|
|
bound_xs = params[0]/natom
|
|
|
|
# Check whether divide_incoherent_elastic was applied correctly
|
|
if abs(free_xs - bound_xs/(1 + 1/data['A0'])**2) > 0.5:
|
|
if divide_incoherent_elastic:
|
|
msg = (
|
|
'Thermal scattering evaluation follows ENDF-6 '
|
|
'definition of bound cross section but '
|
|
'divide_incoherent_elastic=True.'
|
|
)
|
|
else:
|
|
msg = (
|
|
'Thermal scattering evaluation follows NJOY '
|
|
'definition of bound cross section but '
|
|
'divide_incoherent_elastic=False.'
|
|
)
|
|
warn(msg)
|
|
|
|
xs = {}
|
|
distribution = {}
|
|
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)
|
|
return xs, distribution
|
|
|
|
file_obj = StringIO(ev.section[7, 2])
|
|
lhtr = endf.get_head_record(file_obj)[2]
|
|
natom = data['M0'] if divide_incoherent_elastic else 1
|
|
if lhtr == 1:
|
|
# coherent elastic
|
|
xs, distribution = get_coherent_elastic(file_obj)
|
|
elif lhtr == 2:
|
|
# incoherent elastic
|
|
xs, distribution = get_incoherent_elastic(file_obj, natom)
|
|
elif lhtr == 3:
|
|
# mixed coherent / incoherent elastic
|
|
xs_c, dist_c = get_coherent_elastic(file_obj)
|
|
xs_i, dist_i = get_incoherent_elastic(file_obj, natom)
|
|
assert sorted(xs_c) == sorted(xs_i)
|
|
xs = {T: Sum([xs_c[T], xs_i[T]]) for T in xs_c}
|
|
distribution = {T: MixedElasticAE(dist_c[T], dist_i[T]) for T in dist_c}
|
|
|
|
elastic = ThermalScatteringReaction(xs, distribution)
|
|
|
|
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
|