from collections import OrderedDict, Mapping, Callable from copy import deepcopy from io import StringIO from numbers import Integral, Real import os import h5py import numpy as np import pandas as pd from scipy.interpolate import CubicSpline from openmc.mixin import EqualityMixin import openmc.checkvalue as cv from . import HDF5_VERSION from .ace import Table, get_metadata, get_table from .data import ATOMIC_SYMBOL, EV_PER_MEV from .endf import Evaluation, get_head_record, get_tab1_record, get_list_record from .function import Tabulated1D _SUBSHELLS = ['K', 'L1', 'L2', 'L3', 'M1', 'M2', 'M3', 'M4', 'M5', 'N1', 'N2', 'N3', 'N4', 'N5', 'N6', 'N7', 'O1', 'O2', 'O3', 'O4', 'O5', 'O6', 'O7', 'O8', 'O9', 'P1', 'P2', 'P3', 'P4', 'P5', 'P6', 'P7', 'P8', 'P9', 'P10', 'P11', 'Q1', 'Q2', 'Q3'] # Helper function to map designator to subshell string or None def _subshell(i): if i == 0: return None else: return _SUBSHELLS[i - 1] _REACTION_NAME = { 501: 'Total photon interaction', 502: 'Photon coherent scattering', 504: 'Photon incoherent scattering', 515: 'Pair production, electron field', 516: 'Total pair production', 517: 'Pair production, nuclear field', 522: 'Photoelectric absorption', 526: 'Electro-atomic scattering', 527: 'Electro-atomic bremsstrahlung', 528: 'Electro-atomic excitation', 534: 'K (1s1/2) subshell photoelectric', 535: 'L1 (2s1/2) subshell photoelectric', 536: 'L2 (2p1/2) subshell photoelectric', 537: 'L3 (2p3/2) subshell photoelectric', 538: 'M1 (3s1/2) subshell photoelectric', 539: 'M2 (3p1/2) subshell photoelectric', 540: 'M3 (3p3/2) subshell photoelectric', 541: 'M4 (3d3/2) subshell photoelectric', 542: 'M5 (3d5/2) subshell photoelectric', 543: 'N1 (4s1/2) subshell photoelectric', 544: 'N2 (4p1/2) subshell photoelectric', 545: 'N3 (4p3/2) subshell photoelectric', 546: 'N4 (4d3/2) subshell photoelectric', 547: 'N5 (4d5/2) subshell photoelectric', 548: 'N6 (4f5/2) subshell photoelectric', 549: 'N7 (4f7/2) subshell photoelectric', 550: 'O1 (5s1/2) subshell photoelectric', 551: 'O2 (5p1/2) subshell photoelectric', 552: 'O3 (5p3/2) subshell photoelectric', 553: 'O4 (5d3/2) subshell photoelectric', 554: 'O5 (5d5/2) subshell photoelectric', 555: 'O6 (5f5/2) subshell photoelectric', 556: 'O7 (5f7/2) subshell photoelectric', 557: 'O8 (5g7/2) subshell photoelectric', 558: 'O9 (5g9/2) subshell photoelectric', 559: 'P1 (6s1/2) subshell photoelectric', 560: 'P2 (6p1/2) subshell photoelectric', 561: 'P3 (6p3/2) subshell photoelectric', 562: 'P4 (6d3/2) subshell photoelectric', 563: 'P5 (6d5/2) subshell photoelectric', 564: 'P6 (6f5/2) subshell photoelectric', 565: 'P7 (6f7/2) subshell photoelectric', 566: 'P8 (6g7/2) subshell photoelectric', 567: 'P9 (6g9/2) subshell photoelectric', 568: 'P10 (6h9/2) subshell photoelectric', 569: 'P11 (6h11/2) subshell photoelectric', 570: 'Q1 (7s1/2) subshell photoelectric', 571: 'Q2 (7p1/2) subshell photoelectric', 572: 'Q3 (7p3/2) subshell photoelectric' } # Compton profiles are read from a pre-generated HDF5 file when they are first # needed. The dictionary stores an array of electron momentum values (at which # the profiles are tabulated) with the key 'pz' and the profile for each element # is a 2D array with shape (n_shells, n_momentum_values) stored on the key Z _COMPTON_PROFILES = {} # Stopping powers are read from a pre-generated HDF5 file when they are first # needed. The dictionary stores an array of energy values at which the other # quantities are tabulated with the key 'energy' and for each element has the # mean excitation energy and arrays containing the collision stopping powers # and radiative stopping powers stored on the key 'Z'. _STOPPING_POWERS = {} # Scaled bremsstrahlung DCSs are read from a data file provided by Selzter and # Berger when they are first needed. The dictionary stores an array of n # incident electron kinetic energies with key 'electron_energies', an array of # k reduced photon energies with key 'photon_energies', and the cross sections # for each element are in a 2D array with shape (n, k) stored on the key 'Z'. _BREMSSTRAHLUNG = {} class AtomicRelaxation(EqualityMixin): """Atomic relaxation data. This class stores the binding energy, number of electrons, and electron transitions possible from ioniziation for each subshell with an atom. All of the data originates from an ENDF-6 atomic relaxation sub-library (NSUB=6). Instances of this class are not normally instantiated directly but rather created using the factory method :math:`AtomicRelaxation.from_endf`. Parameters ---------- binding_energy : dict Dictionary indicating the binding energy in eV (values) for given subshells (keys). The subshells should be given as strings, e.g., 'K', 'L1', 'L2', etc. num_electrons : dict Dictionary indicating the number of electrons in a subshell when neutral (values) for given subshells (keys). The subshells should be given as strings, e.g., 'K', 'L1', 'L2', etc. transitions : pandas.DataFrame Dictionary indicating allowed transitions and their probabilities (values) for given subshells (keys). The subshells should be given as strings, e.g., 'K', 'L1', 'L2', etc. The transitions are represented as a DataFrame with columns indicating the secondary and tertiary subshell, the energy of the transition in eV, and the fractional probability of the transition. Attributes ---------- binding_energy : dict Dictionary indicating the binding energy in eV (values) for given subshells (keys). The subshells should be given as strings, e.g., 'K', 'L1', 'L2', etc. num_electrons : dict Dictionary indicating the number of electrons in a subshell when neutral (values) for given subshells (keys). The subshells should be given as strings, e.g., 'K', 'L1', 'L2', etc. transitions : pandas.DataFrame Dictionary indicating allowed transitions and their probabilities (values) for given subshells (keys). The subshells should be given as strings, e.g., 'K', 'L1', 'L2', etc. The transitions are represented as a DataFrame with columns indicating the secondary and tertiary subshell, the energy of the transition in eV, and the fractional probability of the transition. See Also -------- IncidentPhoton """ def __init__(self, binding_energy, num_electrons, transitions): self.binding_energy = binding_energy self.num_electrons = num_electrons self.transitions = transitions @property def binding_energy(self): return self._binding_energy @property def num_electrons(self): return self._num_electrons @property def subshells(self): return list(sorted(self.binding_energy.keys())) @property def transitions(self): return self._transitions @binding_energy.setter def binding_energy(self, binding_energy): cv.check_type('binding energies', binding_energy, Mapping) for subshell, energy in binding_energy.items(): cv.check_value('subshell', subshell, _SUBSHELLS) cv.check_type('binding energy', energy, Real) cv.check_greater_than('binding energy', energy, 0.0, True) self._binding_energy = binding_energy @num_electrons.setter def num_electrons(self, num_electrons): cv.check_type('number of electrons', num_electrons, Mapping) for subshell, num in num_electrons.items(): cv.check_value('subshell', subshell, _SUBSHELLS) cv.check_type('number of electrons', num, Real) cv.check_greater_than('number of electrons', num, 0.0, True) self._num_electrons = num_electrons @transitions.setter def transitions(self, transitions): cv.check_type('transitions', transitions, Mapping) for subshell, df in transitions.items(): cv.check_value('subshell', subshell, _SUBSHELLS) cv.check_type('transitions', df, pd.DataFrame) self._transitions = transitions @classmethod def from_ace(cls, ace): """Generate atomic relaxation data from an ACE file Parameters ---------- ace : openmc.data.ace.Table ACE table to read from Returns ------- openmc.data.AtomicRelaxation Atomic relaxation data """ # Create data dictionaries binding_energy = {} num_electrons = {} transitions = {} # Get shell designators n = ace.nxs[7] idx = ace.jxs[11] shells = [_subshell(int(i)) for i in ace.xss[idx : idx+n]] # Get number of electrons for each shell idx = ace.jxs[12] for shell, num in zip(shells, ace.xss[idx : idx+n]): num_electrons[shell] = num # Get binding energy for each shell idx = ace.jxs[13] for shell, e in zip(shells, ace.xss[idx : idx+n]): binding_energy[shell] = e*EV_PER_MEV # Get transition table columns = ['secondary', 'tertiary', 'energy (eV)', 'probability'] idx = ace.jxs[18] for i, subi in enumerate(shells): n_transitions = int(ace.xss[ace.jxs[15] + i]) if n_transitions > 0: records = [] for j in range(n_transitions): subj = _subshell(int(ace.xss[idx])) subk = _subshell(int(ace.xss[idx + 1])) etr = ace.xss[idx + 2]*EV_PER_MEV if j == 0: ftr = ace.xss[idx + 3] else: ftr = ace.xss[idx + 3] - ace.xss[idx - 1] records.append((subj, subk, etr, ftr)) idx += 4 # Create dataframe for transitions transitions[subi] = pd.DataFrame.from_records( records, columns=columns) return cls(binding_energy, num_electrons, transitions) @classmethod def from_endf(cls, ev_or_filename): """Generate atomic relaxation data from an ENDF evaluation Parameters ---------- ev_or_filename : str or openmc.data.endf.Evaluation ENDF atomic relaxation evaluation to read from. If given as a string, it is assumed to be the filename for the ENDF file. Returns ------- openmc.data.AtomicRelaxation Atomic relaxation data """ if isinstance(ev_or_filename, Evaluation): ev = ev_or_filename else: ev = Evaluation(ev_or_filename) # Atomic relaxation data is always MF=28, MT=533 if (28, 533) not in ev.section: raise IOError('{} does not appear to be an atomic relaxation ' 'sublibrary.'.format(ev)) # Determine number of subshells file_obj = StringIO(ev.section[28, 533]) params = get_head_record(file_obj) n_subshells = params[4] # Create data dictionaries binding_energy = {} num_electrons = {} transitions = {} columns = ['secondary', 'tertiary', 'energy (eV)', 'probability'] # Read data for each subshell for i in range(n_subshells): params, list_items = get_list_record(file_obj) subi = _subshell(int(params[0])) n_transitions = int(params[5]) binding_energy[subi] = list_items[0] num_electrons[subi] = list_items[1] if n_transitions > 0: # Read transition data records = [] for j in range(n_transitions): subj = _subshell(int(list_items[6*(j+1)])) subk = _subshell(int(list_items[6*(j+1) + 1])) etr = list_items[6*(j+1) + 2] ftr = list_items[6*(j+1) + 3] records.append((subj, subk, etr, ftr)) # Create dataframe for transitions transitions[subi] = pd.DataFrame.from_records( records, columns=columns) # Return instance of class return cls(binding_energy, num_electrons, transitions) def to_hdf5(self, group): raise NotImplementedError class IncidentPhoton(EqualityMixin): """Photon interaction data. This class stores photo-atomic, photo-nuclear, atomic relaxation, Compton profile, stopping power, and bremsstrahlung data assembled from different sources. To create an instance, the factory method :meth:`IncidentPhoton.from_endf` can be used. To add atomic relaxation or Compton profile data, set the :attr:`IncidentPhoton.atomic_relaxation` and :attr:`IncidentPhoton.compton_profiles` attributes directly. Parameters ---------- atomic_number : int Number of protons in the target nucleus Attributes ---------- atomic_number : int Number of protons in the target nucleus atomic_relaxation : openmc.data.AtomicRelaxation or None Atomic relaxation data bremsstrahlung : dict Dictionary of bremsstrahlung DCS data with keys 'electron_energy' (incident electron kinetic energy values in eV), 'photon_energy' (ratio of the energy of the emitted photon to the incident electron kinetic energy), and 'dcs' (cross sectin values in mb). The cross sections are in scaled form: :math:`(\beta^2/Z^2) E_k (d\sigma/dE_k)`, where :math:`E_k` is the energy of the emitted photon. compton_profiles : dict Dictionary of Compton profile data with keys 'num_electrons' (number of electrons in each subshell), 'binding_energy' (ionization potential of each subshell), and 'J' (Hartree-Fock Compton profile as a function of the projection of the electron momentum on the scattering vector, :math:`p_z` for each subshell). Note that subshell occupancies may not match the atomic relaxation data. reactions : collections.OrderedDict Contains the cross sections for each photon reaction. The keys are MT values and the values are instances of :class:`PhotonReaction`. stopping_powers : dict Dictionary of stopping power data with keys 'energy' (in eV), 'I' (mean excitation energy), 's_collision' (collision stopping power in eV cm:sup:`2`/g), and 's_radiative' (radiative stopping power in eV cm:sup:`2`/g) summed_reactions : collections.OrderedDict Contains summed cross sections. The keys are MT values and the values are instances of :class:`PhotonReaction`. """ def __init__(self, atomic_number): self.atomic_number = atomic_number self._atomic_relaxation = None self.reactions = OrderedDict() self.summed_reactions = OrderedDict() self.compton_profiles = {} self.stopping_powers = {} self.bremsstrahlung = {} def __contains__(self, mt): return mt in self.reactions or mt in self.summed_reactions def __getitem__(self, mt): if mt in self.reactions: return self.reactions[mt] elif mt in self.summed_reactions: return self.summed_reactions[mt] else: raise KeyError('No reaction with MT={}.'.format(mt)) def __repr__(self): return "".format(self.name) def __iter__(self): return iter(self.reactions.values()) @property def atomic_number(self): return self._atomic_number @property def atomic_relaxation(self): return self._atomic_relaxation @property def name(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 @atomic_relaxation.setter def atomic_relaxation(self, atomic_relaxation): cv.check_type('atomic relaxation data', atomic_relaxation, AtomicRelaxation) self._atomic_relaxation = atomic_relaxation @classmethod def from_ace(cls, ace_or_filename): """Generate incident photon data from an ACE table Parameters ---------- ace_or_filename : str or openmc.data.ace.Table ACE table to read from. If given as a string, it is assumed to be the filename for the ACE file. Returns ------- openmc.data.IncidentPhoton Photon interaction 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) # Get atomic number based on name of ACE table zaid = ace.name.split('.')[0] Z = get_metadata(int(zaid))[2] # Read each reaction data = cls(Z) for mt in (502, 504, 515, 522): data.reactions[mt] = PhotonReaction.from_ace(ace, mt) # Compton profiles n_shell = ace.nxs[5] if n_shell != 0: # Get number of electrons in each shell idx = ace.jxs[6] data.compton_profiles['num_electrons'] = ace.xss[idx : idx+n_shell] # Get binding energy for each shell idx = ace.jxs[7] data.compton_profiles['binding_energy'] = ace.xss[idx : idx+n_shell] # Create Compton profile for each electron shell profiles = [] for k in range(n_shell): # Get number of momentum values and interpolation scheme loca = int(ace.xss[ace.jxs[9] + k]) jj = int(ace.xss[ace.jxs[10] + loca - 1]) m = int(ace.xss[ace.jxs[10] + loca]) # Read momentum and PDF idx = ace.jxs[10] + loca + 1 pz = ace.xss[idx : idx+m] pdf = ace.xss[idx+m : idx+2*m] # Create proflie function J_k = Tabulated1D(pz, pdf, [m], [jj]) profiles.append(J_k) data.compton_profiles['J'] = profiles # Subshell photoelectric xs and atomic relaxation data if ace.nxs[7] > 0: data.atomic_relaxation = AtomicRelaxation.from_ace(ace) # Get subshell designators n_subshells = ace.nxs[7] idx = ace.jxs[11] designators = [int(i) for i in ace.xss[idx : idx+n_subshells]] # Get energy grid for subshell photoionization n_energy = ace.nxs[3] idx = ace.jxs[1] energy = np.exp(ace.xss[idx : idx+n_energy])*EV_PER_MEV # Get cross section for each subshell idx = ace.jxs[16] for d in designators: # Create photon reaction mt = 533 + d rx = PhotonReaction(mt) data.reactions[mt] = rx # Store cross section xs = ace.xss[idx : idx+n_energy].copy() nonzero = (xs != 0.0) xs[nonzero] = np.exp(xs[nonzero]) rx.xs = Tabulated1D(energy, xs, [n_energy], [5]) idx += n_energy # Copy binding energy shell = _subshell(d) e = data.atomic_relaxation.binding_energy[shell] rx.subshell_binding_energy = e return data @classmethod def from_endf(cls, photoatomic, relaxation=None): """Generate incident photon data from an ENDF evaluation Parameters ---------- photoatomic : str or openmc.data.endf.Evaluation ENDF photoatomic data evaluation to read from. If given as a string, it is assumed to be the filename for the ENDF file. relaxation : str or openmc.data.endf.Evaluation, optional ENDF atomic relaxation data evaluation to read from. If given as a string, it is assumed to be the filename for the ENDF file. Returns ------- openmc.data.IncidentPhoton Photon interaction data """ if isinstance(photoatomic, Evaluation): ev = photoatomic else: ev = Evaluation(photoatomic) Z = ev.target['atomic_number'] data = cls(Z) # Read each reaction for mf, mt, nc, mod in ev.reaction_list: if mf == 23: data.reactions[mt] = PhotonReaction.from_endf(ev, mt) # Add atomic relaxation data if it hasn't been added already if relaxation is not None: data.atomic_relaxation = AtomicRelaxation.from_endf(relaxation) # If Compton profile data hasn't been loaded, do so if not _COMPTON_PROFILES: filename = os.path.join(os.path.dirname(__file__), 'compton_profiles.h5') with h5py.File(filename, 'r') as f: _COMPTON_PROFILES['pz'] = f['pz'].value for i in range(1, 101): group = f['{:03}'.format(i)] num_electrons = group['num_electrons'].value binding_energy = group['binding_energy'].value*EV_PER_MEV J = group['J'].value _COMPTON_PROFILES[i] = {'num_electrons': num_electrons, 'binding_energy': binding_energy, 'J': J} # Add Compton profile data pz = _COMPTON_PROFILES['pz'] profile = _COMPTON_PROFILES[Z] data.compton_profiles['num_electrons'] = profile['num_electrons'] data.compton_profiles['binding_energy'] = profile['binding_energy'] data.compton_profiles['J'] = [Tabulated1D(pz, J_k) for J_k in profile['J']] # Load stopping power data if it has not yet been loaded if not _STOPPING_POWERS: filename = os.path.join(os.path.dirname(__file__), 'stopping_powers.h5') with h5py.File(filename, 'r') as f: # Units are in MeV; convert to eV _STOPPING_POWERS['energy'] = f['energy'].value*EV_PER_MEV for i in range(1, 99): group = f['{:03}'.format(i)] _STOPPING_POWERS[i] = {'I': group.attrs['I'], 's_collision': group['s_collision'].value, 's_radiative': group['s_radiative'].value} # Units are in MeV cm^2/g; convert to eV cm^2/g _STOPPING_POWERS[i]['s_collision'] *= EV_PER_MEV _STOPPING_POWERS[i]['s_radiative'] *= EV_PER_MEV # Add stopping power data if Z < 99: data.stopping_powers['energy'] = _STOPPING_POWERS['energy'] data.stopping_powers.update(_STOPPING_POWERS[Z]) # Load bremsstrahlung data if it has not yet been loaded if not _BREMSSTRAHLUNG: filename = os.path.join(os.path.dirname(__file__), 'BREMX.DAT') brem = open(filename, 'r').read().split() # Incident electron kinetic energy grid in eV _BREMSSTRAHLUNG['electron_energy'] = np.logspace(3, 9, 200) log_energy = np.log(_BREMSSTRAHLUNG['electron_energy']) # Get number of tabulated electron and photon energy values n = int(brem[37]) k = int(brem[38]) # Index in data p = 39 # Get log of incident electron kinetic energy values, used for cubic # spline interpolation in log energy. Units are in MeV, so convert to eV. logx = np.log(np.fromiter(brem[p:p+n], float, n)*EV_PER_MEV) p += n # Get reduced photon energy values _BREMSSTRAHLUNG['photon_energy'] = np.fromiter(brem[p:p+k], float, k) p += k for i in range(1, 101): dcs = np.empty([len(log_energy), k]) # Get the scaled cross section values for each electron energy and # reduced photon energy for this Z y = np.reshape(np.fromiter(brem[p:p+n*k], float, n*k), (n, k)) p += k*n for j in range(k): # Cubic spline interpolation in log energy and linear DCS cs = CubicSpline(logx, y[:,j]) # Get scaled DCS values (millibarns) on new energy grid dcs[:,j] = cs(log_energy) _BREMSSTRAHLUNG[i] = {'dcs': dcs} # Add bremsstrahlung DCS data data.bremsstrahlung['electron_energy'] = _BREMSSTRAHLUNG['electron_energy'] data.bremsstrahlung['photon_energy'] = _BREMSSTRAHLUNG['photon_energy'] data.bremsstrahlung['dcs'] = _BREMSSTRAHLUNG[Z]['dcs'] return data def export_to_hdf5(self, path, mode='a'): """Export incident photon 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. """ # Open file and write version f = h5py.File(path, mode, libver='latest') f.attrs['filetype'] = np.string_('data_photon') if 'version' not in f.attrs: f.attrs['version'] = np.array(HDF5_VERSION) group = f.create_group(self.name) group.attrs['Z'] = Z = self.atomic_number # Determine union energy grid union_grid = np.array([]) for rx in self: union_grid = np.union1d(union_grid, rx.xs.x) group.create_dataset('energy', data=union_grid) # Write coherent scattering cross section rx = self.reactions[502] coh_group = group.create_group('coherent') coh_group.create_dataset('xs', data=rx.xs(union_grid)) if rx.scattering_factor is not None: # Create integrated form factor ff = deepcopy(rx.scattering_factor) ff.x *= ff.x ff.y *= ff.y/Z**2 int_ff = Tabulated1D(ff.x, ff.integral()) int_ff.to_hdf5(coh_group, 'integrated_scattering_factor') if rx.anomalous_real is not None: rx.anomalous_real.to_hdf5(coh_group, 'anomalous_real') if rx.anomalous_imag is not None: rx.anomalous_imag.to_hdf5(coh_group, 'anomalous_imag') # Write incoherent scattering cross section rx = self[504] incoh_group = group.create_group('incoherent') incoh_group.create_dataset('xs', data=rx.xs(union_grid)) if rx.scattering_factor is not None: rx.scattering_factor.to_hdf5(incoh_group, 'scattering_factor') # Write electron-field pair production cross section if 515 in self: pair_group = group.create_group('pair_production_electron') pair_group.create_dataset('xs', data=self[515].xs(union_grid)) # Write nuclear-field pair production cross section if 517 in self: pair_group = group.create_group('pair_production_nuclear') pair_group.create_dataset('xs', data=self[517].xs(union_grid)) # Write photoelectric cross section photoelec_group = group.create_group('photoelectric') photoelec_group.create_dataset('xs', data=self[522].xs(union_grid)) # Write photoionization cross sections shell_group = group.create_group('subshells') designators = [] for mt, rx in self.reactions.items(): if mt >= 534 and mt <= 572: # Get name of subshell shell = _SUBSHELLS[mt - 534] designators.append(shell) sub_group = shell_group.create_group(shell) if self.atomic_relaxation is not None: relax = self.atomic_relaxation # Write subshell binding energy and number of electrons sub_group.attrs['binding_energy'] = relax.binding_energy[shell] sub_group.attrs['num_electrons'] = relax.num_electrons[shell] # Write transition data with replacements if shell in relax.transitions: shell_values = _SUBSHELLS.copy() shell_values.insert(0, None) df = relax.transitions[shell].replace( shell_values, range(len(shell_values))) sub_group.create_dataset('transitions', data=df.as_matrix()) # Determine threshold threshold = rx.xs.x[0] idx = np.searchsorted(union_grid, threshold, side='right') - 1 # Interpolate cross section onto union grid and write photoionization = rx.xs(union_grid[idx:]) sub_group.create_dataset('xs', data=photoionization) assert len(union_grid) == len(photoionization) + idx sub_group['xs'].attrs['threshold_idx'] = idx shell_group.attrs['designators'] = np.array(designators, dtype='S') # Write Compton profiles if self.compton_profiles: compton_group = group.create_group('compton_profiles') profile = self.compton_profiles compton_group.create_dataset('num_electrons', data=profile['num_electrons']) compton_group.create_dataset('binding_energy', data=profile['binding_energy']) # Get electron momentum values compton_group.create_dataset('pz', data=profile['J'][0].x) # Create/write 2D array of profiles J = np.array([Jk.y for Jk in profile['J']]) compton_group.create_dataset('J', data=J) # Write stopping powers if self.stopping_powers: s_group = group.create_group('stopping_powers') for key, value in self.stopping_powers.items(): if key == 'I': s_group.attrs[key] = value else: s_group.create_dataset(key, data=value) # Write bremsstrahlung if self.bremsstrahlung: brem_group = group.create_group('bremsstrahlung') brem = self.bremsstrahlung brem_group.create_dataset('electron_energy', data=brem['electron_energy']) brem_group.create_dataset('photon_energy', data=brem['photon_energy']) brem_group.create_dataset('dcs', data=brem['dcs']) class PhotonReaction(EqualityMixin): """Photon-induced reaction Parameters ---------- mt : int The ENDF MT number for this reaction. Attributes ---------- anomalous_real : openmc.data.Tabulated1D Real part of the anomalous scattering factor anomlaous_imag : openmc.data.Tabulated1D Imaginary part of the anomalous scatttering factor mt : int The ENDF MT number for this reaction. scattering_factor : openmc.data.Tabulated1D Coherent or incoherent form factor. xs : Callable Cross section as a function of incident photon energy """ def __init__(self, mt): self.mt = mt self._xs = None self._scattering_factor = None self._anomalous_real = None self._anomalous_imag = None def __repr__(self): if self.mt in _REACTION_NAME: return "".format( self.mt, _REACTION_NAME[self.mt]) else: return "".format(self.mt) @property def anomalous_real(self): return self._anomalous_real @property def anomalous_imag(self): return self._anomalous_imag @property def scattering_factor(self): return self._scattering_factor @property def xs(self): return self._xs @anomalous_real.setter def anomalous_real(self, anomalous_real): cv.check_type('real part of anomalous scattering factor', anomalous_real, Callable) self._anomalous_real = anomalous_real @anomalous_imag.setter def anomalous_imag(self, anomalous_imag): cv.check_type('imaginary part of anomalous scattering factor', anomalous_imag, Callable) self._anomalous_imag = anomalous_imag @scattering_factor.setter def scattering_factor(self, scattering_factor): cv.check_type('scattering factor', scattering_factor, Callable) self._scattering_factor = scattering_factor @xs.setter def xs(self, xs): cv.check_type('reaction cross section', xs, Callable) self._xs = xs @classmethod def from_ace(cls, ace, mt): """Generate photon reaction from an ACE table Parameters ---------- ace : openmc.data.ace.Table ACE table to read from mt : int The MT value of the reaction to get data for Returns ------- openmc.data.PhotonReaction Photon reaction data """ # Create instance rx = cls(mt) # Get energy grid (stored as logarithms) n = ace.nxs[3] idx = ace.jxs[1] energy = np.exp(ace.xss[idx : idx+n])*EV_PER_MEV # Get index for appropriate reaction if mt == 502: # Coherent scattering idx = ace.jxs[1] + 2*n elif mt == 504: # Incoherent scattering idx = ace.jxs[1] + n elif mt == 515: # Pair production idx = ace.jxs[1] + 4*n elif mt == 522: # Photoelectric idx = ace.jxs[1] + 3*n else: raise ValueError('ACE photoatomic cross sections do not have ' 'data for MT={}.'.format(mt)) # Store cross section xs = ace.xss[idx : idx+n].copy() nonzero = (xs != 0.0) xs[nonzero] = np.exp(xs[nonzero]) rx.xs = Tabulated1D(energy, xs, [n], [5]) # Get form factors for incoherent/coherent scattering new_format = (ace.nxs[6] > 0) if mt == 502: idx = ace.jxs[3] if new_format: n = (ace.jxs[4] - ace.jxs[3]) // 3 x = ace.xss[idx : idx+n] idx += n else: x = np.array([ 0.0, 0.01, 0.02, 0.03, 0.04, 0.05, 0.06, 0.08, 0.1, 0.12, 0.15, 0.18, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0, 2.2, 2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2, 5.4, 5.6, 5.8, 6.0]) n = x.size ff = ace.xss[idx+n : idx+2*n] rx.scattering_factor = Tabulated1D(x, ff) elif mt == 504: idx = ace.jxs[2] if new_format: n = (ace.jxs[3] - ace.jxs[2]) // 2 x = ace.xss[idx : idx+n] idx += n else: x = np.array([ 0.0, 0.005, 0.01, 0.05, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.5, 2.0, 3.0, 4.0, 5.0, 8.0 ]) n = x.size ff = ace.xss[idx : idx+n] rx.scattering_factor = Tabulated1D(x, ff) return rx @classmethod def from_endf(cls, ev, mt): """Generate photon reaction from an ENDF evaluation Parameters ---------- ev : openmc.data.endf.Evaluation ENDF photo-atomic interaction data evaluation mt : int The MT value of the reaction to get data for Returns ------- openmc.data.PhotonReaction Photon reaction data """ rx = cls(mt) # Read photon cross section if (23, mt) in ev.section: file_obj = StringIO(ev.section[23, mt]) get_head_record(file_obj) params, rx.xs = get_tab1_record(file_obj) # Set subshell binding energy and/or fluorescence yield if mt >= 534 and mt <= 599: rx.subshell_binding_energy = params[0] if mt >= 534 and mt <= 572: rx.fluorescence_yield = params[1] # Read form factors / scattering functions if (27, mt) in ev.section: file_obj = StringIO(ev.section[27, mt]) get_head_record(file_obj) params, rx.scattering_factor = get_tab1_record(file_obj) # Check for anomalous scattering factor if mt == 502: if (27, 506) in ev.section: file_obj = StringIO(ev.section[27, 506]) get_head_record(file_obj) params, rx.anomalous_real = get_tab1_record(file_obj) if (27, 505) in ev.section: file_obj = StringIO(ev.section[27, 505]) get_head_record(file_obj) params, rx.anomalous_imag = get_tab1_record(file_obj) return rx