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

openmc-dev / openmc / 21621898565

03 Feb 2026 07:57AM UTC coverage: 81.278% (-0.5%) from 81.763%
21621898565

Pull #3474

github

web-flow
Merge 7486f3ff0 into b41e22f68
Pull Request #3474: Cylindrical IndependentSource enhancements

17285 of 24269 branches covered (71.22%)

Branch coverage included in aggregate %.

56 of 64 new or added lines in 2 files covered. (87.5%)

402 existing lines in 9 files now uncovered.

55547 of 65340 relevant lines covered (85.01%)

34579169.11 hits per line

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

86.17
/openmc/data/neutron.py
1
from collections.abc import Mapping, MutableMapping
7✔
2
from io import StringIO
7✔
3
from math import log10
7✔
4
from numbers import Integral, Real
7✔
5
import os
7✔
6
import tempfile
7✔
7
from warnings import warn
7✔
8

9
import numpy as np
7✔
10
import h5py
7✔
11

12
from . import HDF5_VERSION, HDF5_VERSION_MAJOR
7✔
13
from .ace import Library, Table, get_table, get_metadata
7✔
14
from .data import ATOMIC_SYMBOL, K_BOLTZMANN, EV_PER_MEV, gnds_name
7✔
15
from .endf import (
7✔
16
    Evaluation, SUM_RULES, get_head_record, get_tab1_record, get_evaluations)
17
from .fission_energy import FissionEnergyRelease
7✔
18
from .function import Tabulated1D, Sum, ResonancesWithBackground
7✔
19
from .njoy import make_ace, make_pendf
7✔
20
from .product import Product
7✔
21
from .reaction import Reaction, _get_photon_products_ace, FISSION_MTS
7✔
22
from . import resonance as res
7✔
23
from . import resonance_covariance as res_cov
7✔
24
from .urr import ProbabilityTables
7✔
25
import openmc.checkvalue as cv
7✔
26
from openmc.mixin import EqualityMixin
7✔
27

28

29
# Fractions of resonance widths used for reconstructing resonances
30
_RESONANCE_ENERGY_GRID = np.logspace(-3, 3, 61)
7✔
31

32

33
class IncidentNeutron(EqualityMixin):
7✔
34
    """Continuous-energy neutron interaction data.
35

36
    This class stores data derived from an ENDF-6 format neutron interaction
37
    sublibrary. Instances of this class are not normally instantiated by the
38
    user but rather created using the factory methods
39
    :meth:`IncidentNeutron.from_hdf5`, :meth:`IncidentNeutron.from_ace`, and
40
    :meth:`IncidentNeutron.from_endf`.
41

42
    Parameters
43
    ----------
44
    name : str
45
        Name of the nuclide using the GNDS naming convention
46
    atomic_number : int
47
        Number of protons in the target nucleus
48
    mass_number : int
49
        Number of nucleons in the target nucleus
50
    metastable : int
51
        Metastable state of the target nucleus. A value of zero indicates ground
52
        state.
53
    atomic_weight_ratio : float
54
        Atomic mass ratio of the target nuclide.
55
    kTs : Iterable of float
56
        List of temperatures of the target nuclide in the data set.
57
        The temperatures have units of eV.
58

59
    Attributes
60
    ----------
61
    atomic_number : int
62
        Number of protons in the target nucleus
63
    atomic_symbol : str
64
        Atomic symbol of the nuclide, e.g., 'Zr'
65
    atomic_weight_ratio : float
66
        Atomic weight ratio of the target nuclide.
67
    fission_energy : None or openmc.data.FissionEnergyRelease
68
        The energy released by fission, tabulated by component (e.g. prompt
69
        neutrons or beta particles) and dependent on incident neutron energy
70
    mass_number : int
71
        Number of nucleons in the target nucleus
72
    metastable : int
73
        Metastable state of the target nucleus. A value of zero indicates ground
74
        state.
75
    name : str
76
        Name of the nuclide using the GNDS naming convention
77
    reactions : dict
78
        Contains the cross sections, secondary angle and energy distributions,
79
        and other associated data for each reaction. The keys are the MT values
80
        and the values are Reaction objects.
81
    resonances : openmc.data.Resonances or None
82
        Resonance parameters
83
    resonance_covariance : openmc.data.ResonanceCovariance or None
84
        Covariance for resonance parameters
85
    temperatures : list of str
86
        List of string representations of the temperatures of the target nuclide
87
        in the data set.  The temperatures are strings of the temperature,
88
        rounded to the nearest integer; e.g., '294K'
89
    kTs : Iterable of float
90
        List of temperatures of the target nuclide in the data set.
91
        The temperatures have units of eV.
92
    urr : dict
93
        Dictionary whose keys are temperatures (e.g., '294K') and values are
94
        unresolved resonance region probability tables.
95

96
    """
97

98
    def __init__(self, name, atomic_number, mass_number, metastable,
7✔
99
                 atomic_weight_ratio, kTs):
100
        self.name = name
7✔
101
        self.atomic_number = atomic_number
7✔
102
        self.mass_number = mass_number
7✔
103
        self.metastable = metastable
7✔
104
        self.atomic_weight_ratio = atomic_weight_ratio
7✔
105
        self.kTs = kTs
7✔
106
        self.energy = {}
7✔
107
        self._fission_energy = None
7✔
108
        self.reactions = {}
7✔
109
        self._urr = {}
7✔
110
        self._resonances = None
7✔
111

112
    def __contains__(self, mt):
7✔
113
        return mt in self.reactions
7✔
114

115
    def __getitem__(self, mt):
7✔
116
        if mt in self.reactions:
7✔
117
            return self.reactions[mt]
7✔
118
        else:
119
            # Try to create a redundant cross section
UNCOV
120
            mts = self.get_reaction_components(mt)
×
UNCOV
121
            if len(mts) > 0:
×
UNCOV
122
                return self._get_redundant_reaction(mt, mts)
×
123
            else:
UNCOV
124
                raise KeyError(f'No reaction with MT={mt}.')
×
125

126
    def __repr__(self):
7✔
127
        return f"<IncidentNeutron: {self.name}>"
×
128

129
    def __iter__(self):
7✔
130
        return iter(self.reactions.values())
7✔
131

132
    @property
7✔
133
    def name(self):
7✔
134
        return self._name
7✔
135

136
    @name.setter
7✔
137
    def name(self, name):
7✔
138
        cv.check_type('name', name, str)
7✔
139
        self._name = name
7✔
140

141
    @property
7✔
142
    def atomic_number(self):
7✔
143
        return self._atomic_number
7✔
144

145
    @atomic_number.setter
7✔
146
    def atomic_number(self, atomic_number):
7✔
147
        cv.check_type('atomic number', atomic_number, Integral)
7✔
148
        cv.check_greater_than('atomic number', atomic_number, 0, True)
7✔
149
        self._atomic_number = atomic_number
7✔
150

151
    @property
7✔
152
    def mass_number(self):
7✔
153
        return self._mass_number
7✔
154

155
    @mass_number.setter
7✔
156
    def mass_number(self, mass_number):
7✔
157
        cv.check_type('mass number', mass_number, Integral)
7✔
158
        cv.check_greater_than('mass number', mass_number, 0, True)
7✔
159
        self._mass_number = mass_number
7✔
160

161
    @property
7✔
162
    def metastable(self):
7✔
163
        return self._metastable
7✔
164

165
    @metastable.setter
7✔
166
    def metastable(self, metastable):
7✔
167
        cv.check_type('metastable', metastable, Integral)
7✔
168
        cv.check_greater_than('metastable', metastable, 0, True)
7✔
169
        self._metastable = metastable
7✔
170

171
    @property
7✔
172
    def atomic_weight_ratio(self):
7✔
173
        return self._atomic_weight_ratio
7✔
174

175
    @atomic_weight_ratio.setter
7✔
176
    def atomic_weight_ratio(self, atomic_weight_ratio):
7✔
177
        cv.check_type('atomic weight ratio', atomic_weight_ratio, Real)
7✔
178
        cv.check_greater_than('atomic weight ratio', atomic_weight_ratio, 0.0)
7✔
179
        self._atomic_weight_ratio = atomic_weight_ratio
7✔
180

181
    @property
7✔
182
    def fission_energy(self):
7✔
183
        return self._fission_energy
7✔
184

185
    @fission_energy.setter
7✔
186
    def fission_energy(self, fission_energy):
7✔
187
        cv.check_type('fission energy release', fission_energy,
7✔
188
                      FissionEnergyRelease)
189
        self._fission_energy = fission_energy
7✔
190

191
    @property
7✔
192
    def reactions(self):
7✔
193
        return self._reactions
7✔
194

195
    @reactions.setter
7✔
196
    def reactions(self, reactions):
7✔
197
        cv.check_type('reactions', reactions, Mapping)
7✔
198
        self._reactions = reactions
7✔
199

200
    @property
7✔
201
    def resonances(self):
7✔
202
        return self._resonances
7✔
203

204
    @resonances.setter
7✔
205
    def resonances(self, resonances):
7✔
206
        cv.check_type('resonances', resonances, res.Resonances)
7✔
207
        self._resonances = resonances
7✔
208

209
    @property
7✔
210
    def resonance_covariance(self):
7✔
211
        return self._resonance_covariance
7✔
212

213
    @resonance_covariance.setter
7✔
214
    def resonance_covariance(self, resonance_covariance):
7✔
215
        cv.check_type('resonance covariance', resonance_covariance,
7✔
216
                      res_cov.ResonanceCovariances)
217
        self._resonance_covariance = resonance_covariance
7✔
218

219
    @property
7✔
220
    def urr(self):
7✔
221
        return self._urr
7✔
222

223
    @urr.setter
7✔
224
    def urr(self, urr):
7✔
225
        cv.check_type('probability table dictionary', urr, MutableMapping)
×
226
        for key, value in urr:
×
227
            cv.check_type('probability table temperature', key, str)
×
228
            cv.check_type('probability tables', value, ProbabilityTables)
×
229
        self._urr = urr
×
230

231
    @property
7✔
232
    def temperatures(self):
7✔
233
        return [f"{int(round(kT / K_BOLTZMANN))}K" for kT in self.kTs]
7✔
234

235
    @property
7✔
236
    def atomic_symbol(self):
7✔
237
        return ATOMIC_SYMBOL[self.atomic_number]
7✔
238

239
    def add_temperature_from_ace(self, ace_or_filename, metastable_scheme='nndc'):
7✔
240
        """Append data from an ACE file at a different temperature.
241

242
        Parameters
243
        ----------
244
        ace_or_filename : openmc.data.ace.Table or str
245
            ACE table to read from. If given as a string, it is assumed to be
246
            the filename for the ACE file.
247
        metastable_scheme : {'nndc', 'mcnp'}
248
            Determine how ZAID identifiers are to be interpreted in the case of
249
            a metastable nuclide. Because the normal ZAID (=1000*Z + A) does not
250
            encode metastable information, different conventions are used among
251
            different libraries. In MCNP libraries, the convention is to add 400
252
            for a metastable nuclide except for Am242m, for which 95242 is
253
            metastable and 95642 (or 1095242 in newer libraries) is the ground
254
            state. For NNDC libraries, ZAID is given as 1000*Z + A + 100*m.
255

256
        """
257

258
        data = IncidentNeutron.from_ace(ace_or_filename, metastable_scheme)
7✔
259

260
        # Check if temprature already exists
261
        strT = data.temperatures[0]
7✔
262
        if strT in self.temperatures:
7✔
263
            warn(f'Cross sections at T={strT} already exist.')
×
264
            return
×
265

266
        # Check that name matches
267
        if data.name != self.name:
7✔
268
            raise ValueError('Data provided for an incorrect nuclide.')
×
269

270
        # Add temperature
271
        self.kTs += data.kTs
7✔
272

273
        # Add energy grid
274
        self.energy[strT] = data.energy[strT]
7✔
275

276
        # Add normal and redundant reactions
277
        for mt in data.reactions:
7✔
278
            if mt in self:
7✔
279
                self[mt].xs[strT] = data[mt].xs[strT]
7✔
280
            else:
281
                warn("Tried to add cross sections for MT={} at T={} but this "
×
282
                     "reaction doesn't exist.".format(mt, strT))
283

284
        # Add probability tables
285
        if strT in data.urr:
7✔
286
            self.urr[strT] = data.urr[strT]
×
287

288
    def add_elastic_0K_from_endf(self, filename, overwrite=False, **kwargs):
7✔
289
        """Append 0K elastic scattering cross section from an ENDF file.
290

291
        Parameters
292
        ----------
293
        filename : str
294
            Path to ENDF file
295
        overwrite : bool
296
            If existing 0 K data is present, this flag can be used to indicate
297
            that it should be overwritten. Otherwise, an exception will be
298
            thrown.
299
        **kwargs
300
            Keyword arguments passed to :func:`openmc.data.njoy.make_pendf`
301

302
        Raises
303
        ------
304
        ValueError
305
            If 0 K data is already present and the `overwrite` parameter is
306
            False.
307

308
        """
309
        # Check for existing data
310
        if '0K' in self.energy and not overwrite:
×
311
            raise ValueError('0 K data already exists for this nuclide.')
×
312

313
        with tempfile.TemporaryDirectory() as tmpdir:
×
314
            # Set arguments for make_pendf
315
            pendf_path = os.path.join(tmpdir, 'pendf')
×
316
            kwargs.setdefault('output_dir', tmpdir)
×
317
            kwargs.setdefault('pendf', pendf_path)
×
318

319
            # Run NJOY to create a pointwise ENDF file
320
            make_pendf(filename, **kwargs)
×
321

322
            # Add 0K elastic scattering cross section
323
            pendf = Evaluation(pendf_path)
×
324
            file_obj = StringIO(pendf.section[3, 2])
×
325
            get_head_record(file_obj)
×
326
            params, xs = get_tab1_record(file_obj)
×
327
            self.energy['0K'] = xs.x
×
328
            self[2].xs['0K'] = xs
×
329

330
    def get_reaction_components(self, mt):
7✔
331
        """Determine what reactions make up redundant reaction.
332

333
        Parameters
334
        ----------
335
        mt : int
336
            ENDF MT number of the reaction to find components of.
337

338
        Returns
339
        -------
340
        mts : list of int
341
            ENDF MT numbers of reactions that make up the redundant reaction and
342
            have cross sections provided.
343

344
        """
345
        mts = []
7✔
346
        if mt in SUM_RULES:
7✔
347
            for mt_i in SUM_RULES[mt]:
7✔
348
                mts += self.get_reaction_components(mt_i)
7✔
349
        if mts:
7✔
350
            return mts
7✔
351
        else:
352
            return [mt] if mt in self else []
7✔
353

354
    def export_to_hdf5(self, path, mode='a', libver='earliest'):
7✔
355
        """Export incident neutron data to an HDF5 file.
356

357
        Parameters
358
        ----------
359
        path : str
360
            Path to write HDF5 file to
361
        mode : {'r+', 'w', 'x', 'a'}
362
            Mode that is used to open the HDF5 file. This is the second argument
363
            to the :class:`h5py.File` constructor.
364
        libver : {'earliest', 'latest'}
365
            Compatibility mode for the HDF5 file. 'latest' will produce files
366
            that are less backwards compatible but have performance benefits.
367

368
        """
369
        # If data come from ENDF, don't allow exporting to HDF5
370
        if hasattr(self, '_evaluation'):
7✔
371
            raise NotImplementedError('Cannot export incident neutron data that '
7✔
372
                                      'originated from an ENDF file.')
373

374
        # Open file and write version
375
        with h5py.File(str(path), mode, libver=libver) as f:
7✔
376
            f.attrs['filetype'] = np.bytes_('data_neutron')
7✔
377
            f.attrs['version'] = np.array(HDF5_VERSION)
7✔
378

379
            # Write basic data
380
            g = f.create_group(self.name)
7✔
381
            g.attrs['Z'] = self.atomic_number
7✔
382
            g.attrs['A'] = self.mass_number
7✔
383
            g.attrs['metastable'] = self.metastable
7✔
384
            g.attrs['atomic_weight_ratio'] = self.atomic_weight_ratio
7✔
385
            ktg = g.create_group('kTs')
7✔
386
            for i, temperature in enumerate(self.temperatures):
7✔
387
                ktg.create_dataset(temperature, data=self.kTs[i])
7✔
388

389
            # Write energy grid
390
            eg = g.create_group('energy')
7✔
391
            for temperature in self.temperatures:
7✔
392
                eg.create_dataset(temperature, data=self.energy[temperature])
7✔
393

394
            # Write 0K energy grid if needed
395
            if '0K' in self.energy and '0K' not in eg:
7✔
396
                eg.create_dataset('0K', data=self.energy['0K'])
7✔
397

398
            # Write reaction data
399
            rxs_group = g.create_group('reactions')
7✔
400
            for rx in self.reactions.values():
7✔
401
                # Skip writing redundant reaction if it doesn't have photon
402
                # production or is a summed transmutation reaction. MT=4 is also
403
                # sometimes needed for probability tables. Also write gas
404
                # production, heating, and damage energy production.
405
                if rx.redundant:
7✔
406
                    photon_rx = any(p.particle == 'photon' for p in rx.products)
7✔
407
                    keep_mts = (4, 16, 103, 104, 105, 106, 107,
7✔
408
                                203, 204, 205, 206, 207, 301, 444, 901)
409
                    if not (photon_rx or rx.mt in keep_mts):
7✔
410
                        continue
7✔
411

412
                rx_group = rxs_group.create_group(f'reaction_{rx.mt:03}')
7✔
413
                rx.to_hdf5(rx_group)
7✔
414

415
                # Write total nu data if available
416
                if len(rx.derived_products) > 0 and 'total_nu' not in g:
7✔
417
                    tgroup = g.create_group('total_nu')
7✔
418
                    rx.derived_products[0].to_hdf5(tgroup)
7✔
419

420
            # Write unresolved resonance probability tables
421
            if self.urr:
7✔
422
                urr_group = g.create_group('urr')
7✔
423
                for temperature, urr in self.urr.items():
7✔
424
                    tgroup = urr_group.create_group(temperature)
7✔
425
                    urr.to_hdf5(tgroup)
7✔
426

427
            # Write fission energy release data
428
            if self.fission_energy is not None:
7✔
429
                fer_group = g.create_group('fission_energy_release')
7✔
430
                self.fission_energy.to_hdf5(fer_group)
7✔
431

432
    @classmethod
7✔
433
    def from_hdf5(cls, group_or_filename):
7✔
434
        """Generate continuous-energy neutron interaction data from HDF5 group
435

436
        Parameters
437
        ----------
438
        group_or_filename : h5py.Group or str
439
            HDF5 group containing interaction data. If given as a string, it is
440
            assumed to be the filename for the HDF5 file, and the first group is
441
            used to read from.
442

443
        Returns
444
        -------
445
        openmc.data.IncidentNeutron
446
            Continuous-energy neutron interaction data
447

448
        """
449
        if isinstance(group_or_filename, h5py.Group):
7✔
450
            group = group_or_filename
×
451
        else:
452
            h5file = h5py.File(str(group_or_filename), 'r')
7✔
453

454
            # Make sure version matches
455
            if 'version' in h5file.attrs:
7✔
456
                major, minor = h5file.attrs['version']
7✔
457
                # For now all versions of HDF5 data can be read
458
            else:
459
                raise IOError(
×
460
                    'HDF5 data does not indicate a version. Your installation of '
461
                    'the OpenMC Python API expects version {}.x data.'
462
                    .format(HDF5_VERSION_MAJOR))
463

464
            group = list(h5file.values())[0]
7✔
465

466
        name = group.name[1:]
7✔
467
        atomic_number = group.attrs['Z']
7✔
468
        mass_number = group.attrs['A']
7✔
469
        metastable = group.attrs['metastable']
7✔
470
        atomic_weight_ratio = group.attrs['atomic_weight_ratio']
7✔
471
        kTg = group['kTs']
7✔
472
        kTs = []
7✔
473
        for temp in kTg:
7✔
474
            kTs.append(kTg[temp][()])
7✔
475

476
        data = cls(name, atomic_number, mass_number, metastable,
7✔
477
                   atomic_weight_ratio, kTs)
478

479
        # Read energy grid
480
        e_group = group['energy']
7✔
481
        for temperature, dset in e_group.items():
7✔
482
            data.energy[temperature] = dset[()]
7✔
483

484
        # Read reaction data
485
        rxs_group = group['reactions']
7✔
486
        for name, obj in sorted(rxs_group.items()):
7✔
487
            if name.startswith('reaction_'):
7✔
488
                rx = Reaction.from_hdf5(obj, data.energy)
7✔
489
                data.reactions[rx.mt] = rx
7✔
490

491
                # Read total nu data if available
492
                if rx.mt in FISSION_MTS and 'total_nu' in group:
7✔
493
                    tgroup = group['total_nu']
7✔
494
                    rx.derived_products.append(Product.from_hdf5(tgroup))
7✔
495

496
        # Read unresolved resonance probability tables
497
        if 'urr' in group:
7✔
498
            urr_group = group['urr']
7✔
499
            for temperature, tgroup in urr_group.items():
7✔
500
                data.urr[temperature] = ProbabilityTables.from_hdf5(tgroup)
7✔
501

502
        # Read fission energy release data
503
        if 'fission_energy_release' in group:
7✔
504
            fer_group = group['fission_energy_release']
7✔
505
            data.fission_energy = FissionEnergyRelease.from_hdf5(fer_group)
7✔
506

507
        return data
7✔
508

509
    @classmethod
7✔
510
    def from_ace(cls, ace_or_filename, metastable_scheme='nndc'):
7✔
511
        """Generate incident neutron continuous-energy data from an ACE table
512

513
        Parameters
514
        ----------
515
        ace_or_filename : openmc.data.ace.Table or str
516
            ACE table to read from. If the value is a string, it is assumed to
517
            be the filename for the ACE file.
518
        metastable_scheme : {'nndc', 'mcnp'}
519
            Determine how ZAID identifiers are to be interpreted in the case of
520
            a metastable nuclide. Because the normal ZAID (=1000*Z + A) does not
521
            encode metastable information, different conventions are used among
522
            different libraries. In MCNP libraries, the convention is to add 400
523
            for a metastable nuclide except for Am242m, for which 95242 is
524
            metastable and 95642 (or 1095242 in newer libraries) is the ground
525
            state. For NNDC libraries, ZAID is given as 1000*Z + A + 100*m.
526

527
        Returns
528
        -------
529
        openmc.data.IncidentNeutron
530
            Incident neutron continuous-energy data
531

532
        """
533

534
        # First obtain the data for the first provided ACE table/file
535
        if isinstance(ace_or_filename, Table):
7✔
536
            ace = ace_or_filename
7✔
537
        else:
538
            ace = get_table(ace_or_filename)
×
539

540
        # If mass number hasn't been specified, make an educated guess
541
        zaid, xs = ace.name.split('.')
7✔
542
        if not xs.endswith('c'):
7✔
543
            raise TypeError(
×
544
                f"{ace} is not a continuous-energy neutron ACE table.")
545
        name, element, Z, mass_number, metastable = \
7✔
546
            get_metadata(int(zaid), metastable_scheme)
547

548
        # Assign temperature to the running list
549
        kTs = [ace.temperature*EV_PER_MEV]
7✔
550

551
        data = cls(name, Z, mass_number, metastable,
7✔
552
                   ace.atomic_weight_ratio, kTs)
553

554
        # Get string of temperature to use as a dictionary key
555
        strT = data.temperatures[0]
7✔
556

557
        # Read energy grid
558
        n_energy = ace.nxs[3]
7✔
559
        i = ace.jxs[1]
7✔
560
        energy = ace.xss[i : i + n_energy]*EV_PER_MEV
7✔
561
        data.energy[strT] = energy
7✔
562
        total_xs = ace.xss[i + n_energy : i + 2*n_energy]
7✔
563
        absorption_xs = ace.xss[i + 2*n_energy : i + 3*n_energy]
7✔
564
        heating_number = ace.xss[i + 4*n_energy : i + 5*n_energy]*EV_PER_MEV
7✔
565

566
        # Create redundant reaction for total (MT=1)
567
        total = Reaction(1)
7✔
568
        total.xs[strT] = Tabulated1D(energy, total_xs)
7✔
569
        total.redundant = True
7✔
570
        data.reactions[1] = total
7✔
571

572
        # Create redundant reaction for absorption (MT=101)
573
        if np.count_nonzero(absorption_xs) > 0:
7✔
574
            absorption = Reaction(101)
7✔
575
            absorption.xs[strT] = Tabulated1D(energy, absorption_xs)
7✔
576
            absorption.redundant = True
7✔
577
            data.reactions[101] = absorption
7✔
578

579
        # Create redundant reaction for heating (MT=301)
580
        heating = Reaction(301)
7✔
581
        heating.xs[strT] = Tabulated1D(energy, heating_number*total_xs)
7✔
582
        heating.redundant = True
7✔
583
        data.reactions[301] = heating
7✔
584

585
        # Read each reaction
586
        n_reaction = ace.nxs[4] + 1
7✔
587
        for i in range(n_reaction):
7✔
588
            rx = Reaction.from_ace(ace, i)
7✔
589
            data.reactions[rx.mt] = rx
7✔
590

591
        # Some photon production reactions may be assigned to MTs that don't
592
        # exist, usually MT=4. In this case, we create a new reaction and add
593
        # them
594
        n_photon_reactions = ace.nxs[6]
7✔
595
        photon_mts = ace.xss[ace.jxs[13]:ace.jxs[13] +
7✔
596
                             n_photon_reactions].astype(int)
597

598
        for mt in np.unique(photon_mts // 1000):
7✔
599
            if mt not in data:
7✔
600
                if mt not in SUM_RULES:
×
601
                    warn('Photon production is present for MT={} but no '
×
602
                         'cross section is given.'.format(mt))
603
                    continue
×
604

605
                # Create redundant reaction with appropriate cross section
606
                mts = data.get_reaction_components(mt)
×
607
                if len(mts) == 0:
×
608
                    warn('Photon production is present for MT={} but no '
×
609
                         'reaction components exist.'.format(mt))
610
                    continue
×
611

612
                # Determine redundant cross section
613
                rx = data._get_redundant_reaction(mt, mts)
×
614
                rx.products += _get_photon_products_ace(ace, rx)
×
615
                data.reactions[mt] = rx
×
616

617
        # For transmutation reactions, sometimes only individual levels are
618
        # present in an ACE file, e.g. MT=600-649 instead of the summation
619
        # MT=103. In this case, if a user wants to tally (n,p), OpenMC doesn't
620
        # know about the total cross section. Here, we explicitly create a
621
        # redundant reaction for this purpose.
622
        for mt in (16, 103, 104, 105, 106, 107):
7✔
623
            if mt not in data:
7✔
624
                # Determine if any individual levels are present
625
                mts = data.get_reaction_components(mt)
7✔
626
                if len(mts) == 0:
7✔
627
                    continue
7✔
628

629
                # Determine redundant cross section
630
                rx = data._get_redundant_reaction(mt, mts)
×
631
                data.reactions[mt] = rx
×
632

633
        # Make sure redundant cross sections that are present in an ACE file get
634
        # marked as such
635
        for rx in data:
7✔
636
            mts = data.get_reaction_components(rx.mt)
7✔
637
            if mts != [rx.mt]:
7✔
638
                rx.redundant = True
7✔
639
            if rx.mt in (203, 204, 205, 206, 207, 444):
7✔
640
                rx.redundant = True
7✔
641

642
        # Read unresolved resonance probability tables
643
        urr = ProbabilityTables.from_ace(ace)
7✔
644
        if urr is not None:
7✔
645
            data.urr[strT] = urr
×
646

647
        return data
7✔
648

649
    @classmethod
7✔
650
    def from_endf(cls, ev_or_filename, covariance=False):
7✔
651
        """Generate incident neutron continuous-energy data from an ENDF evaluation
652

653
        Parameters
654
        ----------
655
        ev_or_filename : openmc.data.endf.Evaluation or str
656
            ENDF evaluation to read from. If given as a string, it is assumed to
657
            be the filename for the ENDF file.
658

659
        covariance : bool
660
            Flag to indicate whether or not covariance data from File 32 should be
661
            retrieved
662

663
        Returns
664
        -------
665
        openmc.data.IncidentNeutron
666
            Incident neutron continuous-energy data
667

668
        """
669
        if isinstance(ev_or_filename, Evaluation):
7✔
670
            ev = ev_or_filename
×
671
        else:
672
            ev = Evaluation(ev_or_filename)
7✔
673

674
        atomic_number = ev.target['atomic_number']
7✔
675
        mass_number = ev.target['mass_number']
7✔
676
        metastable = ev.target['isomeric_state']
7✔
677
        atomic_weight_ratio = ev.target['mass']
7✔
678
        temperature = ev.target['temperature']
7✔
679

680
        # Determine name
681
        name = gnds_name(atomic_number, mass_number, metastable)
7✔
682

683
        # Instantiate incident neutron data
684
        data = cls(name, atomic_number, mass_number, metastable,
7✔
685
                   atomic_weight_ratio, [temperature])
686

687
        if (2, 151) in ev.section:
7✔
688
            data.resonances = res.Resonances.from_endf(ev)
7✔
689

690
        if (32, 151) in ev.section and covariance:
7✔
691
            data.resonance_covariance = (
7✔
692
                res_cov.ResonanceCovariances.from_endf(ev, data.resonances)
693
            )
694

695
        # Read each reaction
696
        for mf, mt, nc, mod in ev.reaction_list:
7✔
697
            if mf == 3:
7✔
698
                data.reactions[mt] = Reaction.from_endf(ev, mt)
7✔
699

700
        # Replace cross sections for elastic, capture, fission
701
        try:
7✔
702
            if any(isinstance(r, res._RESOLVED) for r in data.resonances):
7✔
703
                for mt in (2, 102, 18):
7✔
704
                    if mt in data.reactions:
7✔
705
                        rx = data.reactions[mt]
7✔
706
                        rx.xs['0K'] = ResonancesWithBackground(
7✔
707
                            data.resonances, rx.xs['0K'], mt)
708
        except ValueError:
×
709
            # Thrown if multiple resolved ranges (e.g. Pu239 in ENDF/B-VII.1)
710
            pass
×
711

712
        # If first-chance, second-chance, etc. fission are present, check
713
        # whether energy distributions were specified in MF=5. If not, copy the
714
        # energy distribution from MT=18.
715
        for mt, rx in data.reactions.items():
7✔
716
            if mt in (19, 20, 21, 38):
7✔
717
                if (5, mt) not in ev.section:
7✔
718
                    if rx.products:
7✔
719
                        neutron = data.reactions[18].products[0]
7✔
720
                        rx.products[0].applicability = neutron.applicability
7✔
721
                        rx.products[0].distribution = neutron.distribution
7✔
722

723
        # Read fission energy release (requires that we already know nu for
724
        # fission)
725
        if (1, 458) in ev.section:
7✔
726
            data.fission_energy = FissionEnergyRelease.from_endf(ev, data)
7✔
727

728
        data._evaluation = ev
7✔
729
        return data
7✔
730

731
    @classmethod
7✔
732
    def from_njoy(cls, filename, temperatures=None, evaluation=None, **kwargs):
7✔
733
        """Generate incident neutron data by running NJOY.
734

735
        Parameters
736
        ----------
737
        filename : str
738
            Path to ENDF file
739
        temperatures : iterable of float
740
            Temperatures in Kelvin to produce data at. If omitted, data is
741
            produced at room temperature (293.6 K)
742
        evaluation : openmc.data.endf.Evaluation, optional
743
            If the ENDF file contains multiple material evaluations, this
744
            argument indicates which evaluation to use.
745
        **kwargs
746
            Keyword arguments passed to :func:`openmc.data.njoy.make_ace`
747

748
        Returns
749
        -------
750
        data : openmc.data.IncidentNeutron
751
            Incident neutron continuous-energy data
752

753
        """
754
        with tempfile.TemporaryDirectory() as tmpdir:
7✔
755
            # Run NJOY to create an ACE library
756
            kwargs.setdefault("output_dir", tmpdir)
7✔
757
            for key in ("acer", "pendf", "heatr", "broadr", "gaspr", "purr"):
7✔
758
                kwargs.setdefault(key, os.path.join(kwargs["output_dir"], key))
7✔
759
            kwargs['evaluation'] = evaluation
7✔
760
            make_ace(filename, temperatures, **kwargs)
7✔
761

762
            # Create instance from ACE tables within library
763
            lib = Library(kwargs['acer'])
7✔
764
            data = cls.from_ace(lib.tables[0])
7✔
765
            for table in lib.tables[1:]:
7✔
766
                data.add_temperature_from_ace(table)
7✔
767

768
            # Use name based on ENDF evaluation. The name assigned by from_ace
769
            # may be wrong for higher metastable states (e.g., Hf178_m2)
770
            ev = evaluation if evaluation is not None else Evaluation(filename)
7✔
771
            data.name = ev.gnds_name
7✔
772

773
            # Add 0K elastic scattering cross section
774
            if '0K' not in data.energy:
7✔
775
                pendf = Evaluation(kwargs['pendf'])
7✔
776
                file_obj = StringIO(pendf.section[3, 2])
7✔
777
                get_head_record(file_obj)
7✔
778
                params, xs = get_tab1_record(file_obj)
7✔
779
                data.energy['0K'] = xs.x
7✔
780
                data[2].xs['0K'] = xs
7✔
781

782
            # Add fission energy release data
783
            if (1, 458) in ev.section:
7✔
784
                data.fission_energy = f = FissionEnergyRelease.from_endf(ev, data)
7✔
785
            else:
786
                f = None
7✔
787

788
            # For energy deposition, we want to store two different KERMAs:
789
            # one calculated assuming outgoing photons deposit their energy
790
            # locally, and one calculated assuming they carry their energy
791
            # away. This requires two HEATR runs (which make_ace does by
792
            # default). Here, we just need to correct for the fact that NJOY
793
            # uses a fission heating number of h = EFR, whereas we want:
794
            #
795
            # 1) h = EFR + EGP + EGD + EB (for local case)
796
            # 2) h = EFR + EB (for non-local case)
797
            #
798
            # The best way to handle this is to subtract off the fission
799
            # KERMA that NJOY calculates and add back exactly what we want.
800

801
            # If NJOY is not run with HEATR at all, skip everything below
802
            if not kwargs["heatr"]:
7✔
803
                return data
7✔
804

805
            # Helper function to get a cross section from an ENDF file on a
806
            # given energy grid
807
            def get_file3_xs(ev, mt, E):
7✔
808
                file_obj = StringIO(ev.section[3, mt])
7✔
809
                get_head_record(file_obj)
7✔
810
                _, xs = get_tab1_record(file_obj)
7✔
811
                return xs(E)
7✔
812

813
            heating_local = Reaction(901)
7✔
814
            heating_local.redundant = True
7✔
815

816
            heatr_evals = get_evaluations(kwargs["heatr"])
7✔
817
            heatr_local_evals = get_evaluations(kwargs["heatr"] + "_local")
7✔
818

819
            for ev, ev_local, temp in zip(heatr_evals, heatr_local_evals, data.temperatures):
7✔
820
                # Get total KERMA (originally from ACE file) and energy grid
821
                kerma = data.reactions[301].xs[temp]
7✔
822
                E = kerma.x
7✔
823

824
                if f is not None:
7✔
825
                    # Replace fission KERMA with (EFR + EB)*sigma_f
826
                    fission = data[18].xs[temp]
7✔
827
                    kerma_fission = get_file3_xs(ev, 318, E)
7✔
828
                    kerma.y = kerma.y - kerma_fission + (
7✔
829
                        f.fragments(E) + f.betas(E)) * fission(E)
830

831
                # For local KERMA, we first need to get the values from the
832
                # HEATR run with photon energy deposited locally and put
833
                # them on the same energy grid
834
                kerma_local = get_file3_xs(ev_local, 301, E)
7✔
835

836
                if f is not None:
7✔
837
                    # When photons deposit their energy locally, we replace the
838
                    # fission KERMA with (EFR + EGP + EGD + EB)*sigma_f
839
                    kerma_fission_local = get_file3_xs(ev_local, 318, E)
7✔
840
                    kerma_local = kerma_local - kerma_fission_local + (
7✔
841
                        f.fragments(E) + f.prompt_photons(E)
842
                        + f.delayed_photons(E) + f.betas(E))*fission(E)
843

844
                heating_local.xs[temp] = Tabulated1D(E, kerma_local)
7✔
845

846
            data.reactions[901] = heating_local
7✔
847

848
        return data
7✔
849

850
    def _get_redundant_reaction(self, mt, mts):
7✔
851
        """Create redundant reaction from its components
852

853
        Parameters
854
        ----------
855
        mt : int
856
            MT value of the desired reaction
857
        mts : iterable of int
858
            MT values of its components
859

860
        Returns
861
        -------
862
        openmc.Reaction
863
            Redundant reaction
864

865
        """
866

UNCOV
867
        rx = Reaction(mt)
×
868
        # Get energy grid
UNCOV
869
        for strT in self.temperatures:
×
UNCOV
870
            energy = self.energy[strT]
×
UNCOV
871
            xss = [self.reactions[mt_i].xs[strT] for mt_i in mts]
×
UNCOV
872
            idx = min([xs._threshold_idx if hasattr(xs, '_threshold_idx')
×
873
                       else 0 for xs in xss])
UNCOV
874
            rx.xs[strT] = Tabulated1D(energy[idx:], Sum(xss)(energy[idx:]))
×
UNCOV
875
            rx.xs[strT]._threshold_idx = idx
×
876

UNCOV
877
        rx.redundant = True
×
878

UNCOV
879
        return rx
×
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