• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

openmc-dev / openmc / 26783873914

01 Jun 2026 09:43PM UTC coverage: 81.362% (+0.03%) from 81.333%
26783873914

Pull #3948

github

web-flow
Merge 6314ea576 into 111eb7706
Pull Request #3948: Fix get_index_in_direction for regular meshes

18027 of 26121 branches covered (69.01%)

Branch coverage included in aggregate %.

43 of 45 new or added lines in 9 files covered. (95.56%)

443 existing lines in 12 files now uncovered.

59175 of 68766 relevant lines covered (86.05%)

48551677.44 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

74.15
/openmc/data/photon.py
1
from collections.abc import Mapping, Callable
11✔
2
from copy import deepcopy
11✔
3
from io import StringIO
11✔
4
from math import pi
11✔
5
from numbers import Integral, Real
11✔
6
import os
11✔
7

8
import h5py
11✔
9
import numpy as np
11✔
10
import pandas as pd
11✔
11
from scipy.interpolate import CubicSpline
11✔
12

13
import openmc.checkvalue as cv
11✔
14
from openmc.mixin import EqualityMixin
11✔
15
from . import HDF5_VERSION, HDF5_VERSION_MAJOR
11✔
16
from .ace import Table, get_metadata, get_table
11✔
17
from .data import ATOMIC_SYMBOL, EV_PER_MEV
11✔
18
from .endf import (
11✔
19
    as_evaluation, get_head_record, get_tab1_record, get_list_record)
20
from .function import Tabulated1D
11✔
21

22

23
# Constants
24
MASS_ELECTRON_EV = 0.5109989461e6  # Electron mass energy
11✔
25
PLANCK_C = 1.2398419739062977e4  # Planck's constant times c in eV-Angstroms
11✔
26
FINE_STRUCTURE = 137.035999139  # Inverse fine structure constant
11✔
27
CM_PER_ANGSTROM = 1.0e-8
11✔
28
# classical electron radius in cm
29
R0 = CM_PER_ANGSTROM * PLANCK_C / (2.0 * pi * FINE_STRUCTURE * MASS_ELECTRON_EV)
11✔
30

31
# Electron subshell labels
32
_SUBSHELLS = (None, 'K', 'L1', 'L2', 'L3', 'M1', 'M2', 'M3', 'M4', 'M5',
11✔
33
              'N1', 'N2', 'N3', 'N4', 'N5', 'N6', 'N7', 'O1', 'O2', 'O3',
34
              'O4', 'O5', 'O6', 'O7', 'O8', 'O9', 'P1', 'P2', 'P3', 'P4',
35
              'P5', 'P6', 'P7', 'P8', 'P9', 'P10', 'P11', 'Q1', 'Q2', 'Q3')
36

37
_REACTION_NAME = {
11✔
38
    501: ('Total photon interaction', 'total'),
39
    502: ('Photon coherent scattering', 'coherent'),
40
    504: ('Photon incoherent scattering', 'incoherent'),
41
    515: ('Pair production, electron field', 'pair_production_electron'),
42
    516: ('Total pair production', 'pair_production_total'),
43
    517: ('Pair production, nuclear field', 'pair_production_nuclear'),
44
    522: ('Photoelectric absorption', 'photoelectric'),
45
    525: ('Heating', 'heating'),
46
    526: ('Electro-atomic scattering', 'electro_atomic_scat'),
47
    527: ('Electro-atomic bremsstrahlung', 'electro_atomic_brem'),
48
    528: ('Electro-atomic excitation', 'electro_atomic_excit'),
49
    534: ('K (1s1/2) subshell photoelectric', 'K'),
50
    535: ('L1 (2s1/2) subshell photoelectric', 'L1'),
51
    536: ('L2 (2p1/2) subshell photoelectric', 'L2'),
52
    537: ('L3 (2p3/2) subshell photoelectric', 'L3'),
53
    538: ('M1 (3s1/2) subshell photoelectric', 'M1'),
54
    539: ('M2 (3p1/2) subshell photoelectric', 'M2'),
55
    540: ('M3 (3p3/2) subshell photoelectric', 'M3'),
56
    541: ('M4 (3d3/2) subshell photoelectric', 'M4'),
57
    542: ('M5 (3d5/2) subshell photoelectric', 'M5'),
58
    543: ('N1 (4s1/2) subshell photoelectric', 'N1'),
59
    544: ('N2 (4p1/2) subshell photoelectric', 'N2'),
60
    545: ('N3 (4p3/2) subshell photoelectric', 'N3'),
61
    546: ('N4 (4d3/2) subshell photoelectric', 'N4'),
62
    547: ('N5 (4d5/2) subshell photoelectric', 'N5'),
63
    548: ('N6 (4f5/2) subshell photoelectric', 'N6'),
64
    549: ('N7 (4f7/2) subshell photoelectric', 'N7'),
65
    550: ('O1 (5s1/2) subshell photoelectric', 'O1'),
66
    551: ('O2 (5p1/2) subshell photoelectric', 'O2'),
67
    552: ('O3 (5p3/2) subshell photoelectric', 'O3'),
68
    553: ('O4 (5d3/2) subshell photoelectric', 'O4'),
69
    554: ('O5 (5d5/2) subshell photoelectric', 'O5'),
70
    555: ('O6 (5f5/2) subshell photoelectric', 'O6'),
71
    556: ('O7 (5f7/2) subshell photoelectric', 'O7'),
72
    557: ('O8 (5g7/2) subshell photoelectric', 'O8'),
73
    558: ('O9 (5g9/2) subshell photoelectric', 'O9'),
74
    559: ('P1 (6s1/2) subshell photoelectric', 'P1'),
75
    560: ('P2 (6p1/2) subshell photoelectric', 'P2'),
76
    561: ('P3 (6p3/2) subshell photoelectric', 'P3'),
77
    562: ('P4 (6d3/2) subshell photoelectric', 'P4'),
78
    563: ('P5 (6d5/2) subshell photoelectric', 'P5'),
79
    564: ('P6 (6f5/2) subshell photoelectric', 'P6'),
80
    565: ('P7 (6f7/2) subshell photoelectric', 'P7'),
81
    566: ('P8 (6g7/2) subshell photoelectric', 'P8'),
82
    567: ('P9 (6g9/2) subshell photoelectric', 'P9'),
83
    568: ('P10 (6h9/2) subshell photoelectric', 'P10'),
84
    569: ('P11 (6h11/2) subshell photoelectric', 'P11'),
85
    570: ('Q1 (7s1/2) subshell photoelectric', 'Q1'),
86
    571: ('Q2 (7p1/2) subshell photoelectric', 'Q2'),
87
    572: ('Q3 (7p3/2) subshell photoelectric', 'Q3')
88
}
89

90
# Compton profiles are read from a pre-generated HDF5 file when they are first
91
# needed. The dictionary stores an array of electron momentum values (at which
92
# the profiles are tabulated) with the key 'pz' and the profile for each element
93
# is a 2D array with shape (n_shells, n_momentum_values) stored on the key Z
94
_COMPTON_PROFILES = {}
11✔
95

96
# Scaled bremsstrahlung DCSs are read from a data file provided by Selzter and
97
# Berger when they are first needed. The dictionary stores an array of n
98
# incident electron kinetic energies with key 'electron_energies', an array of
99
# k reduced photon energies with key 'photon_energies', and the cross sections
100
# for each element are in a 2D array with shape (n, k) stored on the key 'Z'.
101
# It also stores data used for calculating the density effect correction and
102
# stopping power, namely, the mean excitation energy with the key 'I', number
103
# of electrons per subshell with the key 'num_electrons', and binding energies
104
# with the key 'ionization_energy'.
105
_BREMSSTRAHLUNG = {}
11✔
106

107

108
class AtomicRelaxation(EqualityMixin):
11✔
109
    """Atomic relaxation data.
110

111
    This class stores the binding energy, number of electrons, and electron
112
    transitions possible from ioniziation for each electron subshell of an
113
    atom. All of the data originates from an ENDF-6 atomic relaxation
114
    sub-library (NSUB=6). Instances of this class are not normally instantiated
115
    directly but rather created using the factory method
116
    :math:`AtomicRelaxation.from_endf`.
117

118
    Parameters
119
    ----------
120
    binding_energy : dict
121
        Dictionary indicating the binding energy in eV (values) for given
122
        subshells (keys). The subshells should be given as strings, e.g., 'K',
123
        'L1', 'L2', etc.
124
    num_electrons : dict
125
        Dictionary indicating the number of electrons in a subshell when neutral
126
        (values) for given subshells (keys). The subshells should be given as
127
        strings, e.g., 'K', 'L1', 'L2', etc.
128
    transitions : dict of str to pandas.DataFrame
129
        Dictionary indicating allowed transitions and their probabilities
130
        (values) for given subshells (keys). The subshells should be given as
131
        strings, e.g., 'K', 'L1', 'L2', etc. The transitions are represented as
132
        a DataFrame with columns indicating the secondary and tertiary subshell,
133
        the energy of the transition in eV, and the fractional probability of
134
        the transition.
135

136
    Attributes
137
    ----------
138
    binding_energy : dict
139
        Dictionary indicating the binding energy in eV (values) for given
140
        subshells (keys). The subshells should be given as strings, e.g., 'K',
141
        'L1', 'L2', etc.
142
    num_electrons : dict
143
        Dictionary indicating the number of electrons in a subshell when neutral
144
        (values) for given subshells (keys). The subshells should be given as
145
        strings, e.g., 'K', 'L1', 'L2', etc.
146
    subshells : list
147
        List of subshells as strings, e.g. ``['K', 'L1', ...]``
148
    transitions : pandas.DataFrame
149
        Dictionary indicating allowed transitions and their probabilities
150
        (values) for given subshells (keys). The subshells should be given as
151
        strings, e.g., 'K', 'L1', 'L2', etc. The transitions are represented as
152
        a DataFrame with columns indicating the secondary and tertiary subshell,
153
        the energy of the transition in eV, and the fractional probability of
154
        the transition.
155

156
    See Also
157
    --------
158
    IncidentPhoton
159

160
    """
161
    def __init__(self, binding_energy, num_electrons, transitions):
11✔
162
        self.binding_energy = binding_energy
11✔
163
        self.num_electrons = num_electrons
11✔
164
        self.transitions = transitions
11✔
165
        self._e_fluorescence = {}
11✔
166

167
    @property
11✔
168
    def binding_energy(self):
11✔
169
        return self._binding_energy
11✔
170

171
    @binding_energy.setter
11✔
172
    def binding_energy(self, binding_energy):
11✔
173
        cv.check_type('binding energies', binding_energy, Mapping)
11✔
174
        for subshell, energy in binding_energy.items():
11✔
175
            cv.check_value('subshell', subshell, _SUBSHELLS)
11✔
176
            cv.check_type('binding energy', energy, Real)
11✔
177
            cv.check_greater_than('binding energy', energy, 0.0, True)
11✔
178
        self._binding_energy = binding_energy
11✔
179

180
    @property
11✔
181
    def num_electrons(self):
11✔
182
        return self._num_electrons
11✔
183

184
    @num_electrons.setter
11✔
185
    def num_electrons(self, num_electrons):
11✔
186
        cv.check_type('number of electrons', num_electrons, Mapping)
11✔
187
        for subshell, num in num_electrons.items():
11✔
188
            cv.check_value('subshell', subshell, _SUBSHELLS)
11✔
189
            cv.check_type('number of electrons', num, Real)
11✔
190
            cv.check_greater_than('number of electrons', num, 0.0, True)
11✔
191
        self._num_electrons = num_electrons
11✔
192

193
    @property
11✔
194
    def subshells(self):
11✔
195
        return list(sorted(self.binding_energy.keys()))
11✔
196

197
    @property
11✔
198
    def transitions(self):
11✔
199
        return self._transitions
11✔
200

201
    @transitions.setter
11✔
202
    def transitions(self, transitions):
11✔
203
        cv.check_type('transitions', transitions, Mapping)
11✔
204
        for subshell, df in transitions.items():
11✔
205
            cv.check_value('subshell', subshell, _SUBSHELLS)
11✔
206
            cv.check_type('transitions', df, pd.DataFrame)
11✔
207
        self._transitions = {
11✔
208
            subshell: df.convert_dtypes() for subshell, df in transitions.items()
209
        }
210

211
    @classmethod
11✔
212
    def from_ace(cls, ace):
11✔
213
        """Generate atomic relaxation data from an ACE file
214

215
        Parameters
216
        ----------
217
        ace : openmc.data.ace.Table
218
            ACE table to read from
219

220
        Returns
221
        -------
222
        openmc.data.AtomicRelaxation
223
            Atomic relaxation data
224

225
        """
226
        # Create data dictionaries
227
        binding_energy = {}
×
228
        num_electrons = {}
×
UNCOV
229
        transitions = {}
×
230

231
        # Get shell designators
232
        n = ace.nxs[7]
×
233
        idx = ace.jxs[11]
×
UNCOV
234
        shells = [_SUBSHELLS[int(i)] for i in ace.xss[idx : idx+n]]
×
235

236
        # Get number of electrons for each shell
237
        idx = ace.jxs[12]
×
238
        for shell, num in zip(shells, ace.xss[idx : idx+n]):
×
UNCOV
239
            num_electrons[shell] = num
×
240

241
        # Get binding energy for each shell
242
        idx = ace.jxs[13]
×
243
        for shell, e in zip(shells, ace.xss[idx : idx+n]):
×
UNCOV
244
            binding_energy[shell] = e*EV_PER_MEV
×
245

246
        # Get transition table
247
        columns = ['secondary', 'tertiary', 'energy (eV)', 'probability']
×
248
        idx = ace.jxs[18]
×
249
        for i, subi in enumerate(shells):
×
250
            n_transitions = int(ace.xss[ace.jxs[15] + i])
×
251
            if n_transitions > 0:
×
252
                records = []
×
253
                for j in range(n_transitions):
×
254
                    subj = _SUBSHELLS[int(ace.xss[idx])]
×
255
                    subk = _SUBSHELLS[int(ace.xss[idx + 1])]
×
256
                    etr = ace.xss[idx + 2]*EV_PER_MEV
×
257
                    if j == 0:
×
UNCOV
258
                        ftr = ace.xss[idx + 3]
×
259
                    else:
260
                        ftr = ace.xss[idx + 3] - ace.xss[idx - 1]
×
261
                    records.append((subj, subk, etr, ftr))
×
UNCOV
262
                    idx += 4
×
263

264
                # Create dataframe for transitions
UNCOV
265
                transitions[subi] = pd.DataFrame.from_records(
×
266
                    records, columns=columns)
267

UNCOV
268
        return cls(binding_energy, num_electrons, transitions)
×
269

270
    @classmethod
11✔
271
    def from_endf(cls, ev_or_filename):
11✔
272
        """Generate atomic relaxation data from an ENDF evaluation
273

274
        Parameters
275
        ----------
276
        ev_or_filename : str, openmc.data.endf.Evaluation, or endf.Material
277
            ENDF atomic relaxation evaluation to read from. If given as a
278
            string, it is assumed to be the filename for the ENDF file.
279

280
        Returns
281
        -------
282
        openmc.data.AtomicRelaxation
283
            Atomic relaxation data
284

285
        """
286
        ev = as_evaluation(ev_or_filename)
11✔
287

288
        # Atomic relaxation data is always MF=28, MT=533
289
        if (28, 533) not in ev.section:
11✔
UNCOV
290
            raise IOError('{} does not appear to be an atomic relaxation '
×
291
                          'sublibrary.'.format(ev))
292

293
        # Determine number of subshells
294
        file_obj = StringIO(ev.section[28, 533])
11✔
295
        params = get_head_record(file_obj)
11✔
296
        n_subshells = params[4]
11✔
297

298
        # Create data dictionaries
299
        binding_energy = {}
11✔
300
        num_electrons = {}
11✔
301
        transitions = {}
11✔
302
        columns = ['secondary', 'tertiary', 'energy (eV)', 'probability']
11✔
303

304
        # Read data for each subshell
305
        for i in range(n_subshells):
11✔
306
            params, list_items = get_list_record(file_obj)
11✔
307
            subi = _SUBSHELLS[int(params[0])]
11✔
308
            n_transitions = int(params[5])
11✔
309
            binding_energy[subi] = list_items[0]
11✔
310
            num_electrons[subi] = list_items[1]
11✔
311

312
            if n_transitions > 0:
11✔
313
                # Read transition data
314
                records = []
11✔
315
                for j in range(n_transitions):
11✔
316
                    subj = _SUBSHELLS[int(list_items[6*(j+1)])]
11✔
317
                    subk = _SUBSHELLS[int(list_items[6*(j+1) + 1])]
11✔
318
                    etr = list_items[6*(j+1) + 2]
11✔
319
                    ftr = list_items[6*(j+1) + 3]
11✔
320
                    records.append((subj, subk, etr, ftr))
11✔
321

322
                # Create dataframe for transitions
323
                transitions[subi] = pd.DataFrame.from_records(
11✔
324
                    records, columns=columns)
325

326
        # Return instance of class
327
        return cls(binding_energy, num_electrons, transitions)
11✔
328

329
    @classmethod
11✔
330
    def from_hdf5(cls, group):
11✔
331
        """Generate atomic relaxation data from an HDF5 group
332

333
        Parameters
334
        ----------
335
        group : h5py.Group
336
            HDF5 group to read from
337

338
        Returns
339
        -------
340
        openmc.data.AtomicRelaxation
341
            Atomic relaxation data
342

343
        """
344
        # Create data dictionaries
345
        binding_energy = {}
11✔
346
        num_electrons = {}
11✔
347
        transitions = {}
11✔
348

349
        designators = [s.decode() for s in group.attrs['designators']]
11✔
350
        columns = ['secondary', 'tertiary', 'energy (eV)', 'probability']
11✔
351
        for shell in designators:
11✔
352
            # Shell group
353
            sub_group = group[shell]
11✔
354

355
            # Read subshell binding energy and number of electrons
356
            if 'binding_energy' in sub_group.attrs:
11✔
357
                binding_energy[shell] = sub_group.attrs['binding_energy']
11✔
358
            if 'num_electrons' in sub_group.attrs:
11✔
359
                num_electrons[shell] = sub_group.attrs['num_electrons']
11✔
360

361
            # Read transition data
362
            if 'transitions' in sub_group:
11✔
363
                df = pd.DataFrame(sub_group['transitions'][()],
11✔
364
                                  columns=columns)
365
                # Replace float indexes back to subshell strings
366
                with pd.option_context('future.no_silent_downcasting', True):
11✔
367
                    df[columns[:2]] = df[columns[:2]].replace(
11✔
368
                                np.arange(float(len(_SUBSHELLS))), _SUBSHELLS)
369
                transitions[shell] = df
11✔
370

371
        return cls(binding_energy, num_electrons, transitions)
11✔
372

373
    def to_hdf5(self, group, shell):
11✔
374
        """Write atomic relaxation data to an HDF5 group
375

376
        Parameters
377
        ----------
378
        group : h5py.Group
379
            HDF5 group to write to
380
        shell : str
381
            The subshell to write data for
382

383
        """
384

385
        # Write subshell binding energy and number of electrons
386
        group.attrs['binding_energy'] = self.binding_energy[shell]
11✔
387
        group.attrs['num_electrons'] = self.num_electrons[shell]
11✔
388

389
        # Write transition data with replacements
390
        if shell in self.transitions:
11✔
391
            with pd.option_context('future.no_silent_downcasting', True):
11✔
392
                df = self.transitions[shell].replace(
11✔
393
                    _SUBSHELLS, range(len(_SUBSHELLS)))
394
            group.create_dataset('transitions', data=df.values.astype(float))
11✔
395

396

397
class IncidentPhoton(EqualityMixin):
11✔
398
    r"""Photon interaction data.
399

400
    This class stores photo-atomic, photo-nuclear, atomic relaxation,
401
    Compton profile, stopping power, and bremsstrahlung data assembled from
402
    different sources. To create an instance, the factory method
403
    :meth:`IncidentPhoton.from_endf` can be used. To add atomic relaxation or
404
    Compton profile data, set the :attr:`IncidentPhoton.atomic_relaxation` and
405
    :attr:`IncidentPhoton.compton_profiles` attributes directly.
406

407
    Parameters
408
    ----------
409
    atomic_number : int
410
        Number of protons in the target nucleus
411

412
    Attributes
413
    ----------
414
    atomic_number : int
415
        Number of protons in the target nucleus
416
    atomic_relaxation : openmc.data.AtomicRelaxation or None
417
        Atomic relaxation data
418
    bremsstrahlung : dict
419
        Dictionary of bremsstrahlung data with keys 'I' (mean excitation energy
420
        in [eV]), 'num_electrons' (number of electrons in each subshell),
421
        'ionization_energy' (ionization potential of each subshell),
422
        'electron_energy' (incident electron kinetic energy values in [eV]),
423
        'photon_energy' (ratio of the energy of the emitted photon to the
424
        incident electron kinetic energy), and 'dcs' (cross section values in
425
        [b]). The cross sections are in scaled form: :math:`(\beta^2/Z^2) E_k
426
        (d\sigma/dE_k)`, where :math:`E_k` is the energy of the emitted photon.
427
        A negative number of electrons in a subshell indicates conduction
428
        electrons.
429
    compton_profiles : dict
430
        Dictionary of Compton profile data with keys 'num_electrons' (number of
431
        electrons in each subshell), 'binding_energy' (ionization potential of
432
        each subshell), and 'J' (Hartree-Fock Compton profile as a function of
433
        the projection of the electron momentum on the scattering vector,
434
        :math:`p_z` for each subshell). Note that subshell occupancies may not
435
        match the atomic relaxation data.
436
    reactions : dict
437
        Contains the cross sections for each photon reaction. The keys are MT
438
        values and the values are instances of :class:`PhotonReaction`.
439

440
    """
441

442
    def __init__(self, atomic_number):
11✔
443
        self.atomic_number = atomic_number
11✔
444
        self._atomic_relaxation = None
11✔
445
        self.reactions = {}
11✔
446
        self.compton_profiles = {}
11✔
447
        self.bremsstrahlung = {}
11✔
448

449
    def __contains__(self, mt):
11✔
450
        return mt in self.reactions
11✔
451

452
    def __getitem__(self, mt):
11✔
453
        if mt in self.reactions:
11✔
454
            return self.reactions[mt]
11✔
455
        else:
UNCOV
456
            raise KeyError(f'No reaction with MT={mt}.')
×
457

458
    def __repr__(self):
11✔
UNCOV
459
        return f"<IncidentPhoton: {self.name}>"
×
460

461
    def __iter__(self):
11✔
462
        return iter(self.reactions.values())
11✔
463

464
    @property
11✔
465
    def atomic_number(self):
11✔
466
        return self._atomic_number
11✔
467

468
    @atomic_number.setter
11✔
469
    def atomic_number(self, atomic_number):
11✔
470
        cv.check_type('atomic number', atomic_number, Integral)
11✔
471
        cv.check_greater_than('atomic number', atomic_number, 0, True)
11✔
472
        self._atomic_number = atomic_number
11✔
473

474
    @property
11✔
475
    def atomic_relaxation(self):
11✔
476
        return self._atomic_relaxation
11✔
477

478
    @atomic_relaxation.setter
11✔
479
    def atomic_relaxation(self, atomic_relaxation):
11✔
480
        cv.check_type('atomic relaxation data', atomic_relaxation,
11✔
481
                      AtomicRelaxation)
482
        self._atomic_relaxation = atomic_relaxation
11✔
483

484
    @property
11✔
485
    def name(self):
11✔
486
        return ATOMIC_SYMBOL[self.atomic_number]
11✔
487

488
    @classmethod
11✔
489
    def from_ace(cls, ace_or_filename):
11✔
490
        """Generate incident photon data from an ACE table
491

492
        Parameters
493
        ----------
494
        ace_or_filename : str or openmc.data.ace.Table
495
            ACE table to read from. If given as a string, it is assumed to be
496
            the filename for the ACE file.
497

498
        Returns
499
        -------
500
        openmc.data.IncidentPhoton
501
            Photon interaction data
502

503
        """
504
        # First obtain the data for the first provided ACE table/file
UNCOV
505
        if isinstance(ace_or_filename, Table):
×
UNCOV
506
            ace = ace_or_filename
×
507
        else:
508
            ace = get_table(ace_or_filename)
×
509

510
        # Get atomic number based on name of ACE table
UNCOV
511
        zaid, xs = ace.name.split('.')
×
UNCOV
512
        if not xs.endswith('p'):
×
513
            raise TypeError(f"{ace} is not a photoatomic transport ACE table.")
×
514
        Z = get_metadata(int(zaid))[2]
×
515

516
        # Read each reaction
UNCOV
517
        data = cls(Z)
×
UNCOV
518
        for mt in (502, 504, 517, 522, 525):
×
519
            data.reactions[mt] = PhotonReaction.from_ace(ace, mt)
×
520

521
        # Get heating cross sections [eV-barn] from factors [eV per collision]
522
        # by multiplying with total xs
UNCOV
523
        data.reactions[525].xs.y *= sum([data.reactions[mt].xs.y for mt in
×
524
                                         (502, 504, 517, 522)])
525

526
        # Compton profiles
UNCOV
527
        n_shell = ace.nxs[5]
×
UNCOV
528
        if n_shell != 0:
×
529
            # Get number of electrons in each shell
530
            idx = ace.jxs[6]
×
UNCOV
531
            data.compton_profiles['num_electrons'] = ace.xss[idx : idx+n_shell]
×
532

533
            # Get binding energy for each shell
UNCOV
534
            idx = ace.jxs[7]
×
UNCOV
535
            e = ace.xss[idx : idx+n_shell]*EV_PER_MEV
×
536
            data.compton_profiles['binding_energy'] = e
×
537

538
            # Create Compton profile for each electron shell
UNCOV
539
            profiles = []
×
UNCOV
540
            for k in range(n_shell):
×
541
                # Get number of momentum values and interpolation scheme
542
                loca = int(ace.xss[ace.jxs[9] + k])
×
UNCOV
543
                jj = int(ace.xss[ace.jxs[10] + loca - 1])
×
544
                m = int(ace.xss[ace.jxs[10] + loca])
×
545

546
                # Read momentum and PDF
UNCOV
547
                idx = ace.jxs[10] + loca + 1
×
UNCOV
548
                pz = ace.xss[idx : idx+m]
×
549
                pdf = ace.xss[idx+m : idx+2*m]
×
550

551
                # Create proflie function
UNCOV
552
                J_k = Tabulated1D(pz, pdf, [m], [jj])
×
UNCOV
553
                profiles.append(J_k)
×
554
            data.compton_profiles['J'] = profiles
×
555

556
        # Subshell photoelectric xs and atomic relaxation data
UNCOV
557
        if ace.nxs[7] > 0:
×
UNCOV
558
            data.atomic_relaxation = AtomicRelaxation.from_ace(ace)
×
559

560
            # Get subshell designators
UNCOV
561
            n_subshells = ace.nxs[7]
×
UNCOV
562
            idx = ace.jxs[11]
×
563
            designators = [int(i) for i in ace.xss[idx : idx+n_subshells]]
×
564

565
            # Get energy grid for subshell photoionization
UNCOV
566
            n_energy = ace.nxs[3]
×
UNCOV
567
            idx = ace.jxs[1]
×
568
            energy = np.exp(ace.xss[idx : idx+n_energy])*EV_PER_MEV
×
569

570
            # Get cross section for each subshell
UNCOV
571
            idx = ace.jxs[16]
×
UNCOV
572
            for d in designators:
×
573
                # Create photon reaction
574
                mt = 533 + d
×
UNCOV
575
                rx = PhotonReaction(mt)
×
576
                data.reactions[mt] = rx
×
577

578
                # Store cross section, determining threshold
UNCOV
579
                xs = ace.xss[idx : idx+n_energy].copy()
×
UNCOV
580
                nonzero = (xs != 0.0)
×
581
                xs[nonzero] = np.exp(xs[nonzero])
×
582
                threshold = np.where(xs > 0.0)[0][0]
×
583
                rx.xs = Tabulated1D(energy[threshold:], xs[threshold:],
×
584
                                    [n_energy - threshold], [5])
585
                idx += n_energy
×
586

587
                # Copy binding energy
UNCOV
588
                shell = _SUBSHELLS[d]
×
UNCOV
589
                e = data.atomic_relaxation.binding_energy[shell]
×
590
                rx.subshell_binding_energy = e
×
591
        else:
592
            raise ValueError("ACE table {} does not have subshell data. Only "
×
593
                             "newer ACE photoatomic libraries are supported "
594
                             "(e.g., eprdata14).".format(ace.name))
595

596
        # Add bremsstrahlung DCS data
UNCOV
597
        data._add_bremsstrahlung()
×
598

599
        return data
×
600

601
    @classmethod
11✔
602
    def from_endf(cls, photoatomic, relaxation=None):
11✔
603
        """Generate incident photon data from an ENDF evaluation
604

605
        Parameters
606
        ----------
607
        photoatomic : str, openmc.data.endf.Evaluation, or endf.Material
608
            ENDF photoatomic data evaluation to read from. If given as a string,
609
            it is assumed to be the filename for the ENDF file.
610
        relaxation : str, openmc.data.endf.Evaluation, or endf.Material, optional
611
            ENDF atomic relaxation data evaluation to read from. If given as a
612
            string, it is assumed to be the filename for the ENDF file.
613

614
        Returns
615
        -------
616
        openmc.data.IncidentPhoton
617
            Photon interaction data
618

619
        """
620
        ev = as_evaluation(photoatomic)
11✔
621

622
        Z = ev.target['atomic_number']
11✔
623
        data = cls(Z)
11✔
624

625
        # Read each reaction
626
        for mf, mt, nc, mod in ev.reaction_list:
11✔
627
            if mf == 23:
11✔
628
                data.reactions[mt] = PhotonReaction.from_endf(ev, mt)
11✔
629

630
        # Add atomic relaxation data if it hasn't been added already
631
        if relaxation is not None:
11✔
632
            data.atomic_relaxation = AtomicRelaxation.from_endf(relaxation)
11✔
633

634
        # If Compton profile data hasn't been loaded, do so
635
        if not _COMPTON_PROFILES:
11✔
636
            filename = os.path.join(os.path.dirname(__file__), 'compton_profiles.h5')
11✔
637
            with h5py.File(filename, 'r') as f:
11✔
638
                _COMPTON_PROFILES['pz'] = f['pz'][()]
11✔
639
                for i in range(1, 101):
11✔
640
                    group = f[f'{i:03}']
11✔
641
                    num_electrons = group['num_electrons'][()]
11✔
642
                    binding_energy = group['binding_energy'][()]*EV_PER_MEV
11✔
643
                    J = group['J'][()]
11✔
644
                    _COMPTON_PROFILES[i] = {'num_electrons': num_electrons,
11✔
645
                                            'binding_energy': binding_energy,
646
                                            'J': J}
647

648
        # Add Compton profile data
649
        pz = _COMPTON_PROFILES['pz']
11✔
650
        profile = _COMPTON_PROFILES[Z]
11✔
651
        data.compton_profiles['num_electrons'] = profile['num_electrons']
11✔
652
        data.compton_profiles['binding_energy'] = profile['binding_energy']
11✔
653
        data.compton_profiles['J'] = [Tabulated1D(pz, J_k) for J_k in profile['J']]
11✔
654

655
        # Add bremsstrahlung DCS data
656
        data._add_bremsstrahlung()
11✔
657

658
        return data
11✔
659

660
    @classmethod
11✔
661
    def from_hdf5(cls, group_or_filename):
11✔
662
        """Generate photon reaction from an HDF5 group
663

664
        Parameters
665
        ----------
666
        group_or_filename : h5py.Group or str
667
            HDF5 group containing interaction data. If given as a string, it is
668
            assumed to be the filename for the HDF5 file, and the first group is
669
            used to read from.
670

671
        Returns
672
        -------
673
        openmc.data.IncidentPhoton
674
            Photon interaction data
675

676
        """
677
        if isinstance(group_or_filename, h5py.Group):
11✔
UNCOV
678
            group = group_or_filename
×
UNCOV
679
            need_to_close = False
×
680
        else:
681
            h5file = h5py.File(str(group_or_filename), 'r')
11✔
682
            need_to_close = True
11✔
683

684
            # Make sure version matches
685
            if 'version' in h5file.attrs:
11✔
686
                major, minor = h5file.attrs['version']
11✔
687
                # For now all versions of HDF5 data can be read
688
            else:
UNCOV
689
                raise IOError(
×
690
                    'HDF5 data does not indicate a version. Your installation '
691
                    'of the OpenMC Python API expects version {}.x data.'
692
                    .format(HDF5_VERSION_MAJOR))
693

694
            group = list(h5file.values())[0]
11✔
695

696
        Z = group.attrs['Z']
11✔
697
        data = cls(Z)
11✔
698

699
        # Read energy grid
700
        energy = group['energy'][()]
11✔
701

702
        # Read cross section data
703
        for mt, (name, key) in _REACTION_NAME.items():
11✔
704
            if key in group:
11✔
705
                rgroup = group[key]
11✔
706
            elif key in group['subshells']:
11✔
707
                rgroup = group['subshells'][key]
11✔
708
            else:
709
                continue
11✔
710

711
            data.reactions[mt] = PhotonReaction.from_hdf5(rgroup, mt, energy)
11✔
712

713
        # Check for necessary reactions
714
        for mt in (502, 504, 522):
11✔
715
            assert mt in data, f"Reaction {mt} not found"
11✔
716

717
        # Read atomic relaxation
718
        data.atomic_relaxation = AtomicRelaxation.from_hdf5(group['subshells'])
11✔
719

720
        # Read Compton profiles
721
        if 'compton_profiles' in group:
11✔
722
            rgroup = group['compton_profiles']
11✔
723
            profile = data.compton_profiles
11✔
724
            profile['num_electrons'] = rgroup['num_electrons'][()]
11✔
725
            profile['binding_energy'] = rgroup['binding_energy'][()]
11✔
726

727
            # Get electron momentum values
728
            pz = rgroup['pz'][()]
11✔
729
            J = rgroup['J'][()]
11✔
730
            if pz.size != J.shape[1]:
11✔
UNCOV
731
                raise ValueError("'J' array shape is not consistent with the "
×
732
                                 "'pz' array shape")
733
            profile['J'] = [Tabulated1D(pz, Jk) for Jk in J]
11✔
734

735
        # Read bremsstrahlung
736
        if 'bremsstrahlung' in group:
11✔
737
            rgroup = group['bremsstrahlung']
11✔
738
            data.bremsstrahlung['I'] = rgroup.attrs['I']
11✔
739
            for key in ('dcs', 'electron_energy', 'ionization_energy',
11✔
740
                        'num_electrons', 'photon_energy'):
741
                data.bremsstrahlung[key] = rgroup[key][()]
11✔
742

743
        # If HDF5 file was opened here, make sure it gets closed
744
        if need_to_close:
11✔
745
            h5file.close()
11✔
746

747
        return data
11✔
748

749
    def export_to_hdf5(self, path, mode='a', libver='earliest'):
11✔
750
        """Export incident photon data to an HDF5 file.
751

752
        Parameters
753
        ----------
754
        path : str
755
            Path to write HDF5 file to
756
        mode : {'r+', 'w', 'x', 'a'}
757
            Mode that is used to open the HDF5 file. This is the second argument
758
            to the :class:`h5py.File` constructor.
759
        libver : {'earliest', 'latest'}
760
            Compatibility mode for the HDF5 file. 'latest' will produce files
761
            that are less backwards compatible but have performance benefits.
762

763
        """
764
        with h5py.File(str(path), mode, libver=libver) as f:
11✔
765
            # Write filetype and version
766
            f.attrs['filetype'] = np.bytes_('data_photon')
11✔
767
            if 'version' not in f.attrs:
11✔
768
                f.attrs['version'] = np.array(HDF5_VERSION)
11✔
769

770
            group = f.create_group(self.name)
11✔
771
            group.attrs['Z'] = Z = self.atomic_number
11✔
772

773
            # Determine union energy grid
774
            union_grid = np.array([])
11✔
775
            for rx in self:
11✔
776
                union_grid = np.union1d(union_grid, rx.xs.x)
11✔
777
            group.create_dataset('energy', data=union_grid)
11✔
778

779
            # Write cross sections
780
            shell_group = group.create_group('subshells')
11✔
781
            designators = []
11✔
782
            for mt, rx in self.reactions.items():
11✔
783
                name, key = _REACTION_NAME[mt]
11✔
784
                if mt in (502, 504, 515, 517, 522, 525):
11✔
785
                    sub_group = group.create_group(key)
11✔
786
                elif mt >= 534 and mt <= 572:
11✔
787
                    # Subshell
788
                    designators.append(key)
11✔
789
                    sub_group = shell_group.create_group(key)
11✔
790

791
                    # Write atomic relaxation
792
                    if self.atomic_relaxation is not None:
11✔
793
                        if key in self.atomic_relaxation.subshells:
11✔
794
                            self.atomic_relaxation.to_hdf5(sub_group, key)
11✔
795
                else:
796
                    continue
11✔
797

798
                rx.to_hdf5(sub_group, union_grid, Z)
11✔
799

800
            shell_group.attrs['designators'] = np.array(designators, dtype='S')
11✔
801

802
            # Write Compton profiles
803
            if self.compton_profiles:
11✔
804
                compton_group = group.create_group('compton_profiles')
11✔
805

806
                profile = self.compton_profiles
11✔
807
                compton_group.create_dataset('num_electrons',
11✔
808
                                            data=profile['num_electrons'])
809
                compton_group.create_dataset('binding_energy',
11✔
810
                                            data=profile['binding_energy'])
811

812
                # Get electron momentum values
813
                compton_group.create_dataset('pz', data=profile['J'][0].x)
11✔
814

815
                # Create/write 2D array of profiles
816
                J = np.array([Jk.y for Jk in profile['J']])
11✔
817
                compton_group.create_dataset('J', data=J)
11✔
818

819
            # Write bremsstrahlung
820
            if self.bremsstrahlung:
11✔
821
                brem_group = group.create_group('bremsstrahlung')
11✔
822
                for key, value in self.bremsstrahlung.items():
11✔
823
                    if key == 'I':
11✔
824
                        brem_group.attrs[key] = value
11✔
825
                    else:
826
                        brem_group.create_dataset(key, data=value)
11✔
827

828
    def _add_bremsstrahlung(self):
11✔
829
        """Add the data used in the thick-target bremsstrahlung approximation
830

831
        """
832
        # Load bremsstrahlung data if it has not yet been loaded
833
        if not _BREMSSTRAHLUNG:
11✔
834
            # Add data used for density effect correction
835
            filename = os.path.join(os.path.dirname(__file__), 'density_effect.h5')
11✔
836
            with h5py.File(filename, 'r') as f:
11✔
837
                for i in range(1, 101):
11✔
838
                    group = f[f'{i:03}']
11✔
839
                    _BREMSSTRAHLUNG[i] = {
11✔
840
                        'I': group.attrs['I'],
841
                        'num_electrons': group['num_electrons'][()],
842
                        'ionization_energy': group['ionization_energy'][()]
843
                    }
844

845
            filename = os.path.join(os.path.dirname(__file__), 'BREMX.DAT')
11✔
846
            with open(filename, 'r') as fh:
11✔
847
                brem = fh.read().split()
11✔
848

849
            # Incident electron kinetic energy grid in eV
850
            _BREMSSTRAHLUNG['electron_energy'] = np.logspace(3, 9, 200)
11✔
851
            log_energy = np.log(_BREMSSTRAHLUNG['electron_energy'])
11✔
852

853
            # Get number of tabulated electron and photon energy values
854
            n = int(brem[37])
11✔
855
            k = int(brem[38])
11✔
856

857
            # Index in data
858
            p = 39
11✔
859

860
            # Get log of incident electron kinetic energy values, used for
861
            # cubic spline interpolation in log energy. Units are in MeV, so
862
            # convert to eV.
863
            logx = np.log(np.fromiter(brem[p:p+n], float, n)*EV_PER_MEV)
11✔
864
            p += n
11✔
865

866
            # Get reduced photon energy values
867
            _BREMSSTRAHLUNG['photon_energy'] = np.fromiter(brem[p:p+k], float, k)
11✔
868
            p += k
11✔
869

870
            for i in range(1, 101):
11✔
871
                dcs = np.empty([len(log_energy), k])
11✔
872

873
                # Get the scaled cross section values for each electron energy
874
                # and reduced photon energy for this Z. Units are in mb, so
875
                # convert to b.
876
                y = np.reshape(np.fromiter(brem[p:p+n*k], float, n*k), (n, k))*1.0e-3
11✔
877
                p += k*n
11✔
878

879
                for j in range(k):
11✔
880
                    # Cubic spline interpolation in log energy and linear DCS
881
                    cs = CubicSpline(logx, y[:, j])
11✔
882

883
                    # Get scaled DCS values (barns) on new energy grid
884
                    dcs[:, j] = cs(log_energy)
11✔
885

886
                _BREMSSTRAHLUNG[i]['dcs'] = dcs
11✔
887

888
        # Add bremsstrahlung DCS data
889
        self.bremsstrahlung['electron_energy'] = _BREMSSTRAHLUNG['electron_energy']
11✔
890
        self.bremsstrahlung['photon_energy'] = _BREMSSTRAHLUNG['photon_energy']
11✔
891
        self.bremsstrahlung.update(_BREMSSTRAHLUNG[self.atomic_number])
11✔
892

893

894
class PhotonReaction(EqualityMixin):
11✔
895
    """Photon-induced reaction
896

897
    Parameters
898
    ----------
899
    mt : int
900
        The ENDF MT number for this reaction.
901

902
    Attributes
903
    ----------
904
    anomalous_real : openmc.data.Tabulated1D
905
        Real part of the anomalous scattering factor
906
    anomlaous_imag : openmc.data.Tabulated1D
907
        Imaginary part of the anomalous scatttering factor
908
    mt : int
909
        The ENDF MT number for this reaction.
910
    scattering_factor : openmc.data.Tabulated1D
911
        Coherent or incoherent form factor.
912
    xs : Callable
913
        Cross section as a function of incident photon energy
914

915
    """
916

917
    def __init__(self, mt):
11✔
918
        self.mt = mt
11✔
919
        self._xs = None
11✔
920
        self._scattering_factor = None
11✔
921
        self._anomalous_real = None
11✔
922
        self._anomalous_imag = None
11✔
923

924
    def __repr__(self):
11✔
UNCOV
925
        if self.mt in _REACTION_NAME:
×
UNCOV
926
            return f"<Photon Reaction: MT={self.mt} {_REACTION_NAME[self.mt][0]}>"
×
927
        else:
UNCOV
928
            return f"<Photon Reaction: MT={self.mt}>"
×
929

930
    @property
11✔
931
    def anomalous_real(self):
11✔
932
        return self._anomalous_real
11✔
933

934
    @anomalous_real.setter
11✔
935
    def anomalous_real(self, anomalous_real):
11✔
936
        cv.check_type('real part of anomalous scattering factor',
11✔
937
                      anomalous_real, Callable)
938
        self._anomalous_real = anomalous_real
11✔
939

940
    @property
11✔
941
    def anomalous_imag(self):
11✔
942
        return self._anomalous_imag
11✔
943

944
    @anomalous_imag.setter
11✔
945
    def anomalous_imag(self, anomalous_imag):
11✔
946
        cv.check_type('imaginary part of anomalous scattering factor',
11✔
947
                      anomalous_imag, Callable)
948
        self._anomalous_imag = anomalous_imag
11✔
949

950
    @property
11✔
951
    def scattering_factor(self):
11✔
952
        return self._scattering_factor
11✔
953

954
    @scattering_factor.setter
11✔
955
    def scattering_factor(self, scattering_factor):
11✔
956
        cv.check_type('scattering factor', scattering_factor, Callable)
11✔
957
        self._scattering_factor = scattering_factor
11✔
958

959
    @property
11✔
960
    def xs(self):
11✔
961
        return self._xs
11✔
962

963
    @xs.setter
11✔
964
    def xs(self, xs):
11✔
965
        cv.check_type('reaction cross section', xs, Callable)
11✔
966
        self._xs = xs
11✔
967

968
    @classmethod
11✔
969
    def from_ace(cls, ace, mt):
11✔
970
        """Generate photon reaction from an ACE table
971

972
        Parameters
973
        ----------
974
        ace : openmc.data.ace.Table
975
            ACE table to read from
976
        mt : int
977
            The MT value of the reaction to get data for
978

979
        Returns
980
        -------
981
        openmc.data.PhotonReaction
982
            Photon reaction data
983

984
        """
985
        # Create instance
UNCOV
986
        rx = cls(mt)
×
987

988
        # Get energy grid (stored as logarithms)
UNCOV
989
        n = ace.nxs[3]
×
UNCOV
990
        idx = ace.jxs[1]
×
991
        energy = np.exp(ace.xss[idx : idx+n])*EV_PER_MEV
×
992

993
        # Get index for appropriate reaction
994
        if mt == 502:
×
995
            # Coherent scattering
996
            idx = ace.jxs[1] + 2*n
×
UNCOV
997
        elif mt == 504:
×
998
            # Incoherent scattering
999
            idx = ace.jxs[1] + n
×
UNCOV
1000
        elif mt == 517:
×
1001
            # Pair production
1002
            idx = ace.jxs[1] + 4*n
×
UNCOV
1003
        elif mt == 522:
×
1004
            # Photoelectric
1005
            idx = ace.jxs[1] + 3*n
×
UNCOV
1006
        elif mt == 525:
×
1007
            # Heating
1008
            idx = ace.jxs[5]
×
1009
        else:
1010
            raise ValueError('ACE photoatomic cross sections do not have '
×
1011
                             'data for MT={}.'.format(mt))
1012

1013
        # Store cross section
UNCOV
1014
        xs = ace.xss[idx : idx+n].copy()
×
1015
        if mt == 525:
×
1016
            # Get heating factors in [eV per collision]
UNCOV
1017
            xs *= EV_PER_MEV
×
1018
        else:
1019
            nonzero = (xs != 0.0)
×
1020
            xs[nonzero] = np.exp(xs[nonzero])
×
1021

1022
            # Replace zero elements to small non-zero to enable log-log
UNCOV
1023
            xs[~nonzero] = np.exp(-500.0)
×
1024
        rx.xs = Tabulated1D(energy, xs, [n], [5])
×
1025

1026
        # Get form factors for incoherent/coherent scattering
UNCOV
1027
        new_format = (ace.nxs[6] > 0)
×
1028
        if mt == 502:
×
1029
            idx = ace.jxs[3]
×
UNCOV
1030
            if new_format:
×
UNCOV
1031
                n = (ace.jxs[4] - ace.jxs[3]) // 3
×
1032
                x = ace.xss[idx : idx+n]
×
1033
                idx += n
×
1034
            else:
1035
                x = np.array([
×
1036
                    0.0, 0.01, 0.02, 0.03, 0.04, 0.05, 0.06, 0.08, 0.1, 0.12,
1037
                    0.15, 0.18, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5, 0.55,
1038
                    0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6,
1039
                    1.7, 1.8, 1.9, 2.0, 2.2, 2.4, 2.6, 2.8, 3.0, 3.2, 3.4,
1040
                    3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2, 5.4, 5.6,
1041
                    5.8, 6.0])
UNCOV
1042
                n = x.size
×
UNCOV
1043
            ff = ace.xss[idx+n : idx+2*n]
×
UNCOV
1044
            rx.scattering_factor = Tabulated1D(x, ff)
×
1045

UNCOV
1046
        elif mt == 504:
×
1047
            idx = ace.jxs[2]
×
1048
            if new_format:
×
1049
                n = (ace.jxs[3] - ace.jxs[2]) // 2
×
UNCOV
1050
                x = ace.xss[idx : idx+n]
×
1051
                idx += n
×
1052
            else:
1053
                x = np.array([
×
1054
                    0.0, 0.005, 0.01, 0.05, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.6,
1055
                    0.7, 0.8, 0.9, 1.0, 1.5, 2.0, 3.0, 4.0, 5.0, 8.0
1056
                ])
UNCOV
1057
                n = x.size
×
1058
            ff = ace.xss[idx : idx+n]
×
UNCOV
1059
            rx.scattering_factor = Tabulated1D(x, ff)
×
1060

UNCOV
1061
        return rx
×
1062

1063
    @classmethod
11✔
1064
    def from_endf(cls, ev, mt):
11✔
1065
        """Generate photon reaction from an ENDF evaluation
1066

1067
        Parameters
1068
        ----------
1069
        ev : openmc.data.endf.Evaluation or endf.Material
1070
            ENDF photo-atomic interaction data evaluation
1071
        mt : int
1072
            The MT value of the reaction to get data for
1073

1074
        Returns
1075
        -------
1076
        openmc.data.PhotonReaction
1077
            Photon reaction data
1078

1079
        """
1080
        ev = as_evaluation(ev)
11✔
1081
        rx = cls(mt)
11✔
1082

1083
        # Read photon cross section
1084
        if (23, mt) in ev.section:
11✔
1085
            file_obj = StringIO(ev.section[23, mt])
11✔
1086
            get_head_record(file_obj)
11✔
1087
            params, rx.xs = get_tab1_record(file_obj)
11✔
1088

1089
            # Set subshell binding energy and/or fluorescence yield
1090
            if mt >= 534 and mt <= 599:
11✔
1091
                rx.subshell_binding_energy = params[0]
11✔
1092
            if mt >= 534 and mt <= 572:
11✔
1093
                rx.fluorescence_yield = params[1]
11✔
1094

1095
        # Read form factors / scattering functions
1096
        if (27, mt) in ev.section:
11✔
1097
            file_obj = StringIO(ev.section[27, mt])
11✔
1098
            get_head_record(file_obj)
11✔
1099
            params, rx.scattering_factor = get_tab1_record(file_obj)
11✔
1100

1101
        # Check for anomalous scattering factor
1102
        if mt == 502:
11✔
1103
            if (27, 506) in ev.section:
11✔
1104
                file_obj = StringIO(ev.section[27, 506])
11✔
1105
                get_head_record(file_obj)
11✔
1106
                params, rx.anomalous_real = get_tab1_record(file_obj)
11✔
1107

1108
            if (27, 505) in ev.section:
11✔
1109
                file_obj = StringIO(ev.section[27, 505])
11✔
1110
                get_head_record(file_obj)
11✔
1111
                params, rx.anomalous_imag = get_tab1_record(file_obj)
11✔
1112

1113
        return rx
11✔
1114

1115
    @classmethod
11✔
1116
    def from_hdf5(cls, group, mt, energy):
11✔
1117
        """Generate photon reaction from an HDF5 group
1118

1119
        Parameters
1120
        ----------
1121
        group : h5py.Group
1122
            HDF5 group to read from
1123
        mt : int
1124
            The MT value of the reaction to get data for
1125
        energy : Iterable of float
1126
            arrays of energies at which cross sections are tabulated at
1127

1128
        Returns
1129
        -------
1130
        openmc.data.PhotonReaction
1131
            Photon reaction data
1132

1133
        """
1134
        # Create instance
1135
        rx = cls(mt)
11✔
1136

1137
        # Cross sections
1138
        xs = group['xs'][()]
11✔
1139
        # Replace zero elements to small non-zero to enable log-log
1140
        xs[xs == 0.0] = np.exp(-500.0)
11✔
1141

1142
        # Threshold
1143
        threshold_idx = 0
11✔
1144
        if 'threshold_idx' in group['xs'].attrs:
11✔
1145
            threshold_idx = group['xs'].attrs['threshold_idx']
11✔
1146

1147
        # Store cross section
1148
        rx.xs = Tabulated1D(energy[threshold_idx:], xs, [len(xs)], [5])
11✔
1149

1150
        # Check for anomalous scattering factor
1151
        if 'anomalous_real' in group:
11✔
1152
            rx.anomalous_real = Tabulated1D.from_hdf5(group['anomalous_real'])
11✔
1153
        if 'anomalous_imag' in group:
11✔
1154
            rx.anomalous_imag = Tabulated1D.from_hdf5(group['anomalous_imag'])
11✔
1155

1156
        # Check for factors / scattering functions
1157
        if 'scattering_factor' in group:
11✔
1158
            rx.scattering_factor = Tabulated1D.from_hdf5(group['scattering_factor'])
11✔
1159

1160
        return rx
11✔
1161

1162
    def to_hdf5(self, group, energy, Z):
11✔
1163
        """Write photon reaction to an HDF5 group
1164

1165
        Parameters
1166
        ----------
1167
        group : h5py.Group
1168
            HDF5 group to write to
1169
        energy : Iterable of float
1170
            arrays of energies at which cross sections are tabulated at
1171
        Z : int
1172
            atomic number
1173

1174
        """
1175

1176
        # Write cross sections
1177
        if self.mt >= 534 and self.mt <= 572:
11✔
1178
            # Determine threshold
1179
            threshold = self.xs.x[0]
11✔
1180
            idx = np.searchsorted(energy, threshold, side='right') - 1
11✔
1181

1182
            # Interpolate cross section onto union grid and write
1183
            photoionization = self.xs(energy[idx:])
11✔
1184
            group.create_dataset('xs', data=photoionization)
11✔
1185
            assert len(energy) == len(photoionization) + idx
11✔
1186
            group['xs'].attrs['threshold_idx'] = idx
11✔
1187
        else:
1188
            group.create_dataset('xs', data=self.xs(energy))
11✔
1189

1190
        # Write scattering factor
1191
        if self.scattering_factor is not None:
11✔
1192
            if self.mt == 502:
11✔
1193
                # Create integrated form factor
1194
                ff = deepcopy(self.scattering_factor)
11✔
1195
                ff.x *= ff.x
11✔
1196
                ff.y *= ff.y/Z**2
11✔
1197
                int_ff = Tabulated1D(ff.x, ff.integral())
11✔
1198
                int_ff.to_hdf5(group, 'integrated_scattering_factor')
11✔
1199
            self.scattering_factor.to_hdf5(group, 'scattering_factor')
11✔
1200
        if self.anomalous_real is not None:
11✔
1201
            self.anomalous_real.to_hdf5(group, 'anomalous_real')
11✔
1202
        if self.anomalous_imag is not None:
11✔
1203
            self.anomalous_imag.to_hdf5(group, 'anomalous_imag')
11✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc