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

openmc-dev / openmc / 16414963882

21 Jul 2025 10:49AM UTC coverage: 85.127% (+0.01%) from 85.117%
16414963882

Pull #3439

github

web-flow
Merge 031b1cb6c into a649d7bc6
Pull Request #3439: Photonuclear Physics part 1

453 of 541 new or added lines in 8 files covered. (83.73%)

8 existing lines in 1 file now uncovered.

53110 of 62389 relevant lines covered (85.13%)

36084181.27 hits per line

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

82.37
/openmc/data/photonuclear.py
1
from collections import OrderedDict
11✔
2
from collections.abc import Callable, Iterable
11✔
3
from io import StringIO
11✔
4
from numbers import Integral, Real
11✔
5
from warnings import warn
11✔
6
import os
11✔
7
import tempfile
11✔
8

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

12
from openmc.mixin import EqualityMixin
11✔
13
from openmc.stats import Uniform
11✔
14
import openmc.checkvalue as cv
11✔
15
from . import HDF5_VERSION, HDF5_VERSION_MAJOR
11✔
16
from .ace import Table, get_metadata, get_table, Library
11✔
17
from .angle_distribution import AngleDistribution
11✔
18
from .angle_energy import AngleEnergy
11✔
19
from .correlated import CorrelatedAngleEnergy
11✔
20
from .data import ATOMIC_SYMBOL, EV_PER_MEV
11✔
21
from .endf import Evaluation, SUM_RULES, get_head_record, get_tab1_record,  get_cont_record
11✔
22
from .fission_energy import FissionEnergyRelease
11✔
23
from .function import Tabulated1D
11✔
24
from .njoy import make_ace_photonuclear
11✔
25
from .reaction import Reaction, REACTION_NAME as _REACTION_NAME, FISSION_MTS, _get_products, _get_fission_products_endf, _get_photon_products_endf, _get_activation_products
11✔
26
from .product import Product
11✔
27
from .energy_distribution import EnergyDistribution, LevelInelastic, \
11✔
28
    DiscretePhoton
29
from .function import Tabulated1D, Polynomial
11✔
30
from .kalbach_mann import KalbachMann
11✔
31
from .laboratory import LaboratoryAngleEnergy
11✔
32
from .nbody import NBodyPhaseSpace
11✔
33
from .product import Product
11✔
34
from .uncorrelated import UncorrelatedAngleEnergy
11✔
35

36
REACTION_NAME = {50 : '(gamma,n0)'}
11✔
37
REACTION_NAME.update({key:value.replace("(n,","(gamma,") for key,value in _REACTION_NAME.items()})
11✔
38

39
class PhotonuclearReaction(EqualityMixin):
11✔
40
    def __init__(self, mt):
11✔
41
        self._center_of_mass = True
11✔
42
        self._redundant = False
11✔
43
        self._q_value = 0.
11✔
44
        self._xs = None
11✔
45
        self._products = []
11✔
46
        self._derived_products = []
11✔
47
        self.mt = mt
11✔
48

49
    def __repr__(self):
11✔
NEW
50
        if self.mt in _REACTION_NAME:
×
NEW
51
            return f"<PhotonuclearReaction: MT={self.mt} {REACTION_NAME[self.mt]}>"
×
52
        else:
NEW
53
            return f"<PhotonuclearReaction: MT={self.mt}>"
×
54
    
55
    @property
11✔
56
    def center_of_mass(self):
11✔
57
        return self._center_of_mass
11✔
58

59
    @center_of_mass.setter
11✔
60
    def center_of_mass(self, center_of_mass):
11✔
61
        cv.check_type('center of mass', center_of_mass, (bool, np.bool_))
11✔
62
        self._center_of_mass = center_of_mass
11✔
63

64
    @property
11✔
65
    def redundant(self):
11✔
66
        return self._redundant
11✔
67

68
    @redundant.setter
11✔
69
    def redundant(self, redundant):
11✔
70
        cv.check_type('redundant', redundant, (bool, np.bool_))
11✔
71
        self._redundant = redundant
11✔
72

73
    @property
11✔
74
    def q_value(self):
11✔
75
        return self._q_value
11✔
76

77
    @q_value.setter
11✔
78
    def q_value(self, q_value):
11✔
79
        cv.check_type('Q value', q_value, Real)
11✔
80
        self._q_value = q_value
11✔
81

82
    @property
11✔
83
    def products(self):
11✔
84
        return self._products
11✔
85

86
    @products.setter
11✔
87
    def products(self, products):
11✔
NEW
88
        cv.check_type('reaction products', products, Iterable, Product)
×
NEW
89
        self._products = products
×
90

91
    @property
11✔
92
    def derived_products(self):
11✔
NEW
93
        return self._derived_products
×
94

95
    @derived_products.setter
11✔
96
    def derived_products(self, derived_products):
11✔
NEW
97
        cv.check_type('reaction derived products', derived_products,
×
98
                      Iterable, Product)
NEW
99
        self._derived_products = derived_products
×
100

101
    @property
11✔
102
    def xs(self):
11✔
103
        return self._xs
11✔
104

105
    @xs.setter
11✔
106
    def xs(self, xs):
11✔
107
        cv.check_type("reaction cross section", xs, Callable)
11✔
108
        self._xs = xs
11✔
109
        
110
    @classmethod
11✔
111
    def from_endf(cls, ev, mt):
11✔
112
        """Generate a reaction from an ENDF evaluation
113

114
        Parameters
115
        ----------
116
        ev : openmc.data.endf.Evaluation
117
            ENDF evaluation
118
        mt : int
119
            The MT value of the reaction to get data for
120

121
        Returns
122
        -------
123
        rx : openmc.data.PhotonuclearReaction
124
            Reaction data
125

126
        """
127
        rx = cls(mt)
11✔
128

129
        # Integrated cross section
130
        if (3, mt) in ev.section:
11✔
131
            file_obj = StringIO(ev.section[3, mt])
11✔
132
            get_head_record(file_obj)
11✔
133
            params, rx.xs = get_tab1_record(file_obj)
11✔
134
            rx.q_value = params[1]
11✔
135
            
136
        # Get fission product yields (nu) as well as delayed neutron energy
137
        # distributions
138
        if mt in FISSION_MTS:
11✔
NEW
139
            rx.products, rx.derived_products = _get_fission_products_endf(ev)        
×
140

141
        if (6, mt) in ev.section:
11✔
142
            # Product angle-energy distribution
143
            for product in _get_products(ev, mt):
11✔
144
                if mt in FISSION_MTS and product.particle == 'neutron':
11✔
NEW
145
                    rx.products[0].applicability = product.applicability
×
NEW
146
                    rx.products[0].distribution = product.distribution
×
147
                else:
148
                    rx.products.append(product)
11✔
149
        elif (4, mt) in ev.section or (5, mt) in ev.section:
11✔
150
            # Uncorrelated angle-energy distribution
151

152
            # Note that the energy distribution for MT=455 is read in
153
            # _get_fission_products_endf rather than here
NEW
154
            if (5, mt) in ev.section:
×
NEW
155
                product = Product('photon')
×
NEW
156
                file_obj = StringIO(ev.section[5, mt])
×
NEW
157
                items = get_head_record(file_obj)
×
NEW
158
                nk = items[4]
×
NEW
159
                for i in range(nk):
×
NEW
160
                    params, applicability = get_tab1_record(file_obj)
×
NEW
161
                    dist = UncorrelatedAngleEnergy()
×
NEW
162
                    dist.energy = EnergyDistribution.from_endf(file_obj, params)
×
163

NEW
164
                    product.applicability.append(applicability)
×
NEW
165
                    product.distribution.append(dist)
×
NEW
166
            elif mt == 2:
×
167
                # Elastic scattering -- no energy distribution is given since it
168
                # can be calulcated analytically
NEW
169
                product = Product('photon')
×
NEW
170
                dist = UncorrelatedAngleEnergy()
×
NEW
171
                product.distribution.append(dist)
×
NEW
172
            elif mt >= 50 and mt < 91:
×
173
                # Level inelastic scattering -- no energy distribution is given
174
                # since it can be calculated analytically. Here we determine the
175
                # necessary parameters to create a LevelInelastic object
NEW
176
                product = Product('neutron')
×
NEW
177
                dist = UncorrelatedAngleEnergy()
×
178

NEW
179
                A = ev.target['mass']
×
NEW
180
                threshold = abs(rx.q_value)
×
NEW
181
                mass_ratio = (A-1)/A
×
NEW
182
                dist.energy = LevelInelastic(threshold, mass_ratio)
×
183

NEW
184
                product.distribution.append(dist)
×
185

NEW
186
            if (4, mt) in ev.section:
×
NEW
187
                for dist in product.distribution:
×
NEW
188
                    dist.angle = AngleDistribution.from_endf(ev, mt)
×
189

NEW
190
            if mt in FISSION_MTS and (5, mt) in ev.section:
×
191
                # For fission reactions,
NEW
192
                rx.products[0].applicability = product.applicability
×
NEW
193
                rx.products[0].distribution = product.distribution
×
194
            else:
NEW
195
                rx.products.append(product)
×
196

197
        if (8, mt) in ev.section:
11✔
NEW
198
            rx.products += _get_activation_products(ev, rx)
×
199

200
        if (12, mt) in ev.section or (13, mt) in ev.section:
11✔
NEW
201
            rx.products += _get_photon_products_endf(ev, rx)
×
202
        return rx
11✔
203
    
204
    def to_hdf5(self, group):
11✔
205
        """Write reaction to an HDF5 group
206

207
        Parameters
208
        ----------
209
        group : h5py.Group
210
            HDF5 group to write to
211

212
        """
213

214
        group.attrs['mt'] = self.mt
11✔
215
        if self.mt in REACTION_NAME:
11✔
216
            group.attrs['label'] = np.bytes_(REACTION_NAME[self.mt])
11✔
217
        else:
NEW
218
            group.attrs['label'] = np.bytes_(self.mt)
×
219
        group.attrs['Q_value'] = self.q_value
11✔
220
        group.attrs['center_of_mass'] = 1 if self.center_of_mass else 0
11✔
221
        group.attrs['redundant'] = 1 if self.redundant else 0
11✔
222
        
223
        dset = group.create_dataset('xs', data=self.xs.y)
11✔
224
        threshold_idx = getattr(self.xs, '_threshold_idx', 0)
11✔
225
        dset.attrs['threshold_idx'] = threshold_idx
11✔
226
        
227
        for i, p in enumerate(self.products):
11✔
228
            pgroup = group.create_group(f'product_{i}')
11✔
229
            p.to_hdf5(pgroup)
11✔
230

231
    @classmethod
11✔
232
    def from_hdf5(cls, group, energy):
11✔
233
        """Generate reaction from an HDF5 group
234

235
        Parameters
236
        ----------
237
        group : h5py.Group
238
            HDF5 group to read from
239
        energy : array
240
            array of energies at which cross sections are tabulated at.
241

242
        Returns
243
        -------
244
        openmc.data.Reaction
245
            Reaction data
246

247
        """
248

249
        mt = group.attrs['mt']
11✔
250
        rx = cls(mt)
11✔
251
        rx.q_value = group.attrs['Q_value']
11✔
252
        rx.center_of_mass = bool(group.attrs['center_of_mass'])
11✔
253
        rx.redundant = bool(group.attrs.get('redundant', False))
11✔
254
        xs = group['xs'][()]
11✔
255
        threshold_idx = group['xs'].attrs['threshold_idx']
11✔
256
        tabulated_xs = Tabulated1D(energy[threshold_idx:], xs)
11✔
257
        tabulated_xs._threshold_idx = threshold_idx
11✔
258
        rx.xs = tabulated_xs
11✔
259

260
        # Determine number of products
261
        n_product = 0
11✔
262
        for name in group:
11✔
263
            if name.startswith('product_'):
11✔
264
                n_product += 1
11✔
265

266
        # Read reaction products
267
        for i in range(n_product):
11✔
268
            pgroup = group[f'product_{i}']
11✔
269
            rx.products.append(Product.from_hdf5(pgroup))
11✔
270

271
        return rx
11✔
272

273
    @classmethod
11✔
274
    def from_ace(cls, ace, i_reaction):
11✔
275
        # Get nuclide energy grid
276
        n_grid = ace.nxs[3]
11✔
277
        grid = ace.xss[ace.jxs[1] : ace.jxs[1] + n_grid] * EV_PER_MEV
11✔
278

279
        if i_reaction > 0:
11✔
280
            mt = int(ace.xss[ace.jxs[6] + i_reaction - 1])
11✔
281
            rx = cls(mt)
11✔
282
            
283
            # Get Q-value of reaction
284
            rx.q_value = ace.xss[ace.jxs[7] + i_reaction - 1]*EV_PER_MEV
11✔
285

286
            # ==================================================================
287
            # CROSS SECTION
288

289
            # Get locator for cross-section data
290
            loc = int(ace.xss[ace.jxs[8] + i_reaction - 1])
11✔
291

292
            # Determine starting index on energy grid
293
            threshold_idx = int(ace.xss[ace.jxs[9] + loc - 1]) - 1
11✔
294

295
            # Determine number of energies in reaction
296
            n_energy = int(ace.xss[ace.jxs[9] + loc])
11✔
297
            energy = grid[threshold_idx : threshold_idx + n_energy]
11✔
298

299
            # Read reaction cross section
300
            xs = ace.xss[ace.jxs[9] + loc + 1 : ace.jxs[9] + loc + 1 + n_energy]
11✔
301

302
            # Fix negatives -- known issue for Y89 in JEFF 3.2
303
            if np.any(xs < 0.0):
11✔
NEW
304
                warn(
×
305
                    "Negative cross sections found for MT={} in {}. Setting "
306
                    "to zero.".format(rx.mt, ace.name)
307
                )
NEW
308
                xs[xs < 0.0] = 0.0
×
309

310
            tabulated_xs = Tabulated1D(energy, xs)
11✔
311
            tabulated_xs._threshold_idx = threshold_idx
11✔
312
            rx.xs = tabulated_xs
11✔
313
            
314
            # ==================================================================
315
            # YIELD AND ANGLE-ENERGY DISTRIBUTION
316
            
317
            for i_typ in range(ace.nxs[5]):
11✔
318
                loc = ace.jxs[10]+i_typ*ace.nxs[7]-1
11✔
319
                mts = ace.xss[int(ace.xss[loc+5]):int(ace.xss[loc+5])+int(ace.xss[loc+2])].astype(int)
11✔
320
                try:
11✔
321
                    i_mtr = mts.tolist().index(mt)
11✔
322
                except ValueError:
11✔
323
                    #skip products for other reactions
324
                    continue
11✔
325
                match int(ace.xss[loc+1]):
11✔
326
                    case 1:
11✔
327
                        particle=Product('neutron')
11✔
328
                    case 2:
11✔
329
                        particle=Product('photon')
11✔
330
                    case _:
11✔
331
                        #TODO: support more product particles
332
                        # for now skip particle if it is not a neutron or photon
333
                        warn(f"Unsupported secondary particle type in {ace.name} for MT={mt}. "
11✔
334
                            "This product will be skipped.")
335
                        continue
11✔
336
                        
337
                # Determine reference frame        
338
                ty = int(ace.xss[int(ace.xss[loc+6])+i_mtr])
11✔
339
                rx.center_of_mass = (ty < 0)
11✔
340
                
341
                # Determine multiplicity
342
                idx = int(ace.xss[loc+8])+int(ace.xss[int(ace.xss[loc+7])+i_mtr])-1
11✔
343
                match int(ace.xss[idx]):
11✔
344
                    case 6 | 16 | 12:
11✔
345
                        assert int(ace.xss[idx+1]) == mt
11✔
346
                        yield_ = Tabulated1D.from_ace(ace, idx+2)
11✔
NEW
347
                    case _:
×
NEW
348
                        raise NotImplementedError('partial yields not implemented yet')   
×
349
                particle.yield_ = yield_
11✔
350
                
351
                   
352
                # Determine locator for energy distribution  
353
                idx = int(ace.xss[int(ace.xss[loc+11])+i_mtr])   
11✔
354
                distribution = AngleEnergy.from_ace(ace, int(ace.xss[loc+12]), idx)
11✔
355
                
356
                # Determine locator for angular distribution
357
                idx = int(ace.xss[int(ace.xss[loc+9])+i_mtr])
11✔
358
                if idx==0:
11✔
359
                    # No angular distribution data are given for this reaction,
360
                    # isotropic scattering is assumed in LAB
361
                    energy = np.array([particle.yield_.x[0], particle.yield_.x[-1]])
11✔
362
                    mu_isotropic = Uniform(-1., 1.)
11✔
363
                    distribution.angle = AngleDistribution(
11✔
364
                    energy, [mu_isotropic, mu_isotropic])
365
                elif idx==-1:
11✔
366
                    pass
11✔
367
                else:
368
                    assert idx>=0
11✔
369
                    distribution.angle = AngleDistribution.from_ace(ace, int(ace.xss[loc+10]), idx)     
11✔
370
                    
371
                particle.distribution.append(distribution)
11✔
372
                rx.products.append(particle)
11✔
373
        else:
374
            # elastic cross section
NEW
375
            mt = 2
×
NEW
376
            rx = cls(mt)
×
377

378
            # Get elastic cross section values
NEW
379
            elastic_xs = ace.xss[ace.jxs[4] : ace.jxs[4] + ace.nxs[3]]
×
380

381
            # Fix negatives -- known issue for Ti46,49,50 in JEFF 3.2
NEW
382
            if np.any(elastic_xs < 0.0):
×
NEW
383
                warn(
×
384
                    "Negative elastic scattering cross section found for {}. "
385
                    "Setting to zero.".format(ace.name)
386
                )
NEW
387
                elastic_xs[elastic_xs < 0.0] = 0.0
×
388

NEW
389
            tabulated_xs = Tabulated1D(grid, elastic_xs)
×
NEW
390
            tabulated_xs._threshold_idx = 0
×
NEW
391
            rx.xs = tabulated_xs
×
392

393
        return rx
11✔
394

395
class IncidentPhotonuclear(EqualityMixin):
11✔
396
    """photo-nuclear interaction data.
397

398
    This class stores data derived from an ENDF-6 format photo-nuclear interaction
399
    sublibrary. Instances of this class are not normally instantiated by the
400
    user but rather created using the factory methods
401
    :meth:`Photonuclear.from_hdf5`, :meth:`Photonuclear.from_ace`, and
402
    :meth:`Photonuclear.from_endf`.
403

404
    Parameters
405
    ----------
406
    name : str
407
        Name of the nuclide using the GND naming convention
408
    atomic_number : int
409
        Number of photo-nuclears in the target nucleus
410
    atomic_number : int
411
        Number of photo-nuclears in the target nucleus
412
    mass_number : int
413
        Number of nucleons in the target nucleus
414
    metastable : int
415
        Metastable state of the target nucleus. A value of zero indicates ground
416
        state.
417
    atomic_weight_ratio : float
418
        Atomic weight ratio of the target nuclide.
419

420
    Attributes
421
    ----------
422
    atomic_number : int
423
        Number of photo-nuclears in the target nucleus
424
    atomic_symbol : str
425
        Atomic symbol of the nuclide, e.g., 'Zr'
426
    atomic_weight_ratio : float
427
        Atomic weight ratio of the target nuclide.
428
    fission_energy : None or openmc.data.FissionEnergyRelease
429
        The energy released by fission, tabulated by component (e.g. prompt
430
        neutrons or beta particles) and dependent on incident neutron energy        
431
    mass_number : int
432
        Number of nucleons in the target nucleus
433
    metastable : int
434
        Metastable state of the target nucleus. A value of zero indicates ground
435
        state.
436
    name : str
437
        Name of the nuclide using the GND naming convention
438
    reactions : collections.OrderedDict
439
        Contains the cross sections, secondary angle and energy distributions,
440
        and other associated data for each reaction. The keys are the MT values
441
        and the values are Reaction objects.
442

443
    """
444

445
    def __init__(
11✔
446
        self, name, atomic_number, mass_number, metastable, atomic_weight_ratio
447
    ):
448
        self.name = name
11✔
449
        self.atomic_number = atomic_number
11✔
450
        self.mass_number = mass_number
11✔
451
        self.reactions = OrderedDict()
11✔
452
        self.energy = []
11✔
453
        self._fission_energy = None
11✔
454
        self.metastable = metastable
11✔
455
        self.atomic_weight_ratio = atomic_weight_ratio
11✔
456

457
    def __contains__(self, mt):
11✔
458
        return mt in self.reactions
11✔
459

460
    def __getitem__(self, mt):
11✔
461
        if mt in self.reactions:
11✔
462
            return self.reactions[mt]
11✔
463
        else:
464
            # Try to create a redundant cross section
NEW
465
            mts = self.get_reaction_components(mt)
×
NEW
466
            if len(mts) > 0:
×
NEW
467
                return self._get_redundant_reaction(mt, mts)
×
468
            else:
NEW
469
                raise KeyError(f'No reaction with MT={mt}.')
×
470
    
471
    def __repr__(self):
11✔
NEW
472
        return "<IncidentPhotonuclear: {}>".format(self.name)
×
473

474
    def __iter__(self):
11✔
475
        return iter(self.reactions.values())
11✔
476

477
    @property
11✔
478
    def atomic_number(self):
11✔
479
        return self._atomic_number
11✔
480

481
    @property
11✔
482
    def name(self):
11✔
483
        return self._name
11✔
484

485
    @property
11✔
486
    def mass_number(self):
11✔
487
        return self._mass_number
11✔
488

489
    @property
11✔
490
    def metastable(self):
11✔
491
        return self._metastable
11✔
492

493
    @property
11✔
494
    def atomic_weight_ratio(self):
11✔
495
        return self._atomic_weight_ratio
11✔
496

497
    @property
11✔
498
    def atomic_symbol(self):
11✔
499
        return ATOMIC_SYMBOL[self.atomic_number]
11✔
500

501
    @name.setter
11✔
502
    def name(self, name):
11✔
503
        cv.check_type("name", name, str)
11✔
504
        self._name = name
11✔
505

506
    @atomic_number.setter
11✔
507
    def atomic_number(self, atomic_number):
11✔
508
        cv.check_type("atomic number", atomic_number, Integral)
11✔
509
        cv.check_greater_than("atomic number", atomic_number, 0, True)
11✔
510
        self._atomic_number = atomic_number
11✔
511

512
    @mass_number.setter
11✔
513
    def mass_number(self, mass_number):
11✔
514
        cv.check_type("mass number", mass_number, Integral)
11✔
515
        cv.check_greater_than("mass number", mass_number, 0, True)
11✔
516
        self._mass_number = mass_number
11✔
517

518
    @metastable.setter
11✔
519
    def metastable(self, metastable):
11✔
520
        cv.check_type("metastable", metastable, Integral)
11✔
521
        cv.check_greater_than("metastable", metastable, 0, True)
11✔
522
        self._metastable = metastable
11✔
523

524
    @atomic_weight_ratio.setter
11✔
525
    def atomic_weight_ratio(self, atomic_weight_ratio):
11✔
526
        cv.check_type("atomic weight ratio", atomic_weight_ratio, Real)
11✔
527
        cv.check_greater_than("atomic weight ratio", atomic_weight_ratio, 0.0)
11✔
528
        self._atomic_weight_ratio = atomic_weight_ratio
11✔
529
        
530
    @property
11✔
531
    def fission_energy(self):
11✔
532
        return self._fission_energy
11✔
533

534
    @fission_energy.setter
11✔
535
    def fission_energy(self, fission_energy):
11✔
536
        cv.check_type('fission energy release', fission_energy,
11✔
537
                      FissionEnergyRelease)
538
        self._fission_energy = fission_energy        
11✔
539

540
    @classmethod
11✔
541
    def from_endf(cls, ev_or_filename):
11✔
542
        """Generate incident photo-nuclear data from an ENDF evaluation
543

544
        Parameters
545
        ----------
546
        ev_or_filename : openmc.data.endf.Evaluation or str
547
            ENDF evaluation to read from. If given as a string, it is assumed to
548
            be the filename for the ENDF file.
549

550

551
        Returns
552
        -------
553
        openmc.data.Photonuclear
554
            photo-nuclear interaction data
555

556
        """
557

558
        if isinstance(ev_or_filename, Evaluation):
11✔
NEW
559
            ev = ev_or_filename
×
560
        else:
561
            ev = Evaluation(ev_or_filename)
11✔
562

563
        atomic_number = ev.target["atomic_number"]
11✔
564
        mass_number = ev.target["mass_number"]
11✔
565
        metastable = ev.target["isomeric_state"]
11✔
566
        atomic_weight_ratio = ev.target["mass"]
11✔
567

568
        element = ATOMIC_SYMBOL[atomic_number]
11✔
569
        if metastable > 0:
11✔
NEW
570
            name = f'{element}{mass_number}_m{metastable}'
×
571
        else:
572
            name = f'{element}{mass_number}'
11✔
573

574
        data = cls(name, atomic_number, mass_number, metastable, atomic_weight_ratio)
11✔
575

576
        # Read each reaction
577
        for mf, mt, nc, mod in ev.reaction_list:
11✔
578
            if mf == 3:
11✔
579
                data.reactions[mt] = PhotonuclearReaction.from_endf(ev, mt)
11✔
580
                
581
        # Read fission energy release (requires that we already know nu for
582
        # fission)
583
        if (1, 458) in ev.section:
11✔
NEW
584
            data.fission_energy = FissionEnergyRelease.from_endf(ev, data)
×
585

586
        data._evaluation = ev
11✔
587
        return data
11✔
588

589
    @classmethod
11✔
590
    def from_ace(cls, ace_or_filename, metastable_scheme="nndc"):
11✔
591
        """Generate incident photo-nuclear continuous-energy data from an ACE table
592

593
        Parameters
594
        ----------
595
        ace_or_filename : openmc.data.ace.Table or str
596
            ACE table to read from. If the value is a string, it is assumed to
597
            be the filename for the ACE file.
598
        metastable_scheme : {'nndc', 'mcnp'}
599
            Determine how ZAID identifiers are to be interpreted in the case of
600
            a metastable nuclide. Because the normal ZAID (=1000*Z + A) does not
601
            encode metastable information, different conventions are used among
602
            different libraries. In MCNP libraries, the convention is to add 400
603
            for a metastable nuclide except for Am242m, for which 95242 is
604
            metastable and 95642 (or 1095242 in newer libraries) is the ground
605
            state. For NNDC libraries, ZAID is given as 1000*Z + A + 100*m.
606

607
        Returns
608
        -------
609
        openmc.data.Photonuclear
610
            Incident photo-nuclear continuous-energy data
611

612
        """
613

614
        # First obtain the data for the first provided ACE table/file
615
        if isinstance(ace_or_filename, Table):
11✔
616
            ace = ace_or_filename
11✔
617
        else:
NEW
618
            ace = get_table(ace_or_filename)
×
619

620
        # If mass number hasn't been specified, make an educated guess
621
        zaid, xs = ace.name.split(".")
11✔
622
        if not xs.endswith("u"):
11✔
NEW
623
            raise TypeError(
×
624
                "{} is not a continuous-energy photo-nuclear ACE table.".format(ace)
625
            )
626
        name, element, Z, mass_number, metastable = get_metadata(
11✔
627
            int(zaid), metastable_scheme
628
        )
629

630
        data = cls(name, Z, mass_number, metastable, ace.atomic_weight_ratio)
11✔
631

632
        # Read energy grid
633
        n_energy = ace.nxs[3]
11✔
634
        i = ace.jxs[1]
11✔
635
        energy = ace.xss[i : i + n_energy] * EV_PER_MEV
11✔
636
        data.energy = energy
11✔
637

638
        total_xs = ace.xss[ace.jxs[2] : ace.jxs[2] + n_energy]
11✔
639
        nonelastic_xs = ace.xss[ace.jxs[3] : ace.jxs[3] + n_energy]
11✔
640
        elastic_xs = total_xs-nonelastic_xs 
11✔
641
        heating_number = ace.xss[ace.jxs[5] : ace.jxs[5] +  n_energy] * EV_PER_MEV
11✔
642
        
643
        # Create redundant reaction for total (MT=1)
644
        total = PhotonuclearReaction(1)
11✔
645
        total.xs = Tabulated1D(energy, total_xs)
11✔
646
        total.redundant = True
11✔
647
        data.reactions[1] = total
11✔
648
        
649
        # Create redundant reaction for nonelastic (MT=3)
650
        nonelastic = PhotonuclearReaction(3)
11✔
651
        nonelastic.xs = Tabulated1D(energy, nonelastic_xs)
11✔
652
        nonelastic.redundant = True
11✔
653
        data.reactions[3] = nonelastic
11✔
654

655
        # Create redundant reaction for heating (MT=301)
656
        heating = PhotonuclearReaction(301)
11✔
657
        heating.xs = Tabulated1D(energy, heating_number*total_xs)
11✔
658
        heating.redundant = True
11✔
659
        data.reactions[301] = heating
11✔
660
            
661
        # Read each reaction
662
        n_reaction = ace.nxs[4] + 1
11✔
663
        for i in range(n_reaction):
11✔
664
            if i==0:
11✔
665
                if ace.jxs[4]==0:
11✔
666
                    assert np.allclose(total_xs,nonelastic_xs)
11✔
667
                else:
NEW
668
                    raise NotImplementedError('photonuclear elastic scattering is not supported.')
×
669
            else:
670
                rx = PhotonuclearReaction.from_ace(ace, i)
11✔
671
                data.reactions[rx.mt] = rx
11✔
672

673
        return data
11✔
674

675
    def export_to_hdf5(self, path, mode="a", libver="earliest"):
11✔
676
        """Export incident photo-nuclear data to an HDF5 file.
677

678
        Parameters
679
        ----------
680
        path : str
681
            Path to write HDF5 file to
682
        mode : {'r', 'r+', 'w', 'x', 'a'}
683
            Mode that is used to open the HDF5 file. This is the second argument
684
            to the :class:`h5py.File` constructor.
685
        libver : {'earliest', 'latest'}
686
            Compatibility mode for the HDF5 file. 'latest' will produce files
687
            that are less backwards compatible but have performance benefits.
688

689
        """
690

691
        # Open file and write version
692
        f = h5py.File(str(path), mode, libver=libver)
11✔
693
        f.attrs["filetype"] = np.bytes_("data_photonuclear")
11✔
694
        if "version" not in f.attrs:
11✔
695
            f.attrs["version"] = np.array(HDF5_VERSION)
11✔
696

697
        # If data come from ENDF, don't allow exporting to HDF5
698
        if hasattr(self, '_evaluation'):
11✔
699
            raise NotImplementedError('Cannot export incident photonuclear data that '
11✔
700
                                      'originated from an ENDF file.')
701
        # Write basic data
702
        g = f.create_group(self.name)
11✔
703
        g.attrs['Z'] = self.atomic_number
11✔
704
        g.attrs['A'] = self.mass_number
11✔
705
        g.attrs['metastable'] = self.metastable
11✔
706
        g.attrs['atomic_weight_ratio'] = self.atomic_weight_ratio
11✔
707

708
        # Determine union energy grid
709
        union_grid = np.array([])
11✔
710
        for rx in self:
11✔
711
            union_grid = np.union1d(union_grid, rx.xs.x)
11✔
712
            for product in rx.products:
11✔
713
                union_grid = np.union1d(union_grid, product.yield_.x)
11✔
714
        g.create_dataset("energy", data=union_grid)
11✔
715
        
716
        # Determine threshold_idx
717
        for rx in self:
11✔
718
          threshold_idx, = np.flatnonzero(union_grid == rx.xs.x[0])
11✔
719
          rx._threshold_idx = threshold_idx
11✔
720
                
721
        # Write reaction data
722
        rxs_group = g.create_group('reactions')
11✔
723
        for rx in self.reactions.values():
11✔
724
            # Skip writing redundant reaction if it doesn't have neutron
725
            # production or is a summed transmutation reaction.
726
            # Also write heating.
727
            if rx.redundant:
11✔
728
                neutron_rx = any(p.particle == 'neutron' for p in rx.products)
11✔
729
                keep_mts = (301,)
11✔
730
                if not (neutron_rx or rx.mt in keep_mts):
11✔
731
                    continue
11✔
732

733
            rx_group = rxs_group.create_group(f'reaction_{rx.mt:03}')
11✔
734
            rx.to_hdf5(rx_group)
11✔
735
        
736
        # Write fission energy release data
737
        if self.fission_energy is not None:
11✔
NEW
738
            fer_group = g.create_group('fission_energy_release')
×
NEW
739
            self.fission_energy.to_hdf5(fer_group)
×
740
        
741
        f.close()
11✔
742

743
    @classmethod
11✔
744
    def from_hdf5(cls, group_or_filename):
11✔
745
        """Generate photo-nuclear interaction data from HDF5 group
746

747
        Parameters
748
        ----------
749
        group_or_filename : h5py.Group or str
750
            HDF5 group containing interaction data. If given as a string, it is
751
            assumed to be the filename for the HDF5 file, and the first group is
752
            used to read from.
753

754
        Returns
755
        -------
756
        openmc.data.Photonuclear
757
            photo-nuclear interaction data
758

759
        """
760
        if isinstance(group_or_filename, h5py.Group):
11✔
NEW
761
            group = group_or_filename
×
762
        else:
763
            h5file = h5py.File(str(group_or_filename), "r")
11✔
764

765
            # Make sure version matches
766
            if "version" in h5file.attrs:
11✔
767
                major, minor = h5file.attrs["version"]
11✔
768
                # For now all versions of HDF5 data can be read
769
            else:
NEW
770
                raise IOError(
×
771
                    "HDF5 data does not indicate a version. Your installation of "
772
                    "the OpenMC Python API expects version {}.x data.".format(
773
                        HDF5_VERSION_MAJOR
774
                    )
775
                )
776

777
            group = list(h5file.values())[0]
11✔
778

779
        name = group.name[1:]
11✔
780
        atomic_number = group.attrs["Z"]
11✔
781
        mass_number = group.attrs["A"]
11✔
782
        metastable = group.attrs["metastable"]
11✔
783
        atomic_weight_ratio = group.attrs["atomic_weight_ratio"]
11✔
784

785
        data = cls(name, atomic_number, mass_number, metastable, atomic_weight_ratio)
11✔
786

787
        # Read energy grid
788
        data.energy = group["energy"][()]
11✔
789

790
        # Read reaction data
791
        rxs_group = group["reactions"]
11✔
792
        for name, obj in sorted(rxs_group.items()):
11✔
793
            if name.startswith("reaction_"):
11✔
794
                rx = PhotonuclearReaction.from_hdf5(obj, data.energy)
11✔
795
                data.reactions[rx.mt] = rx
11✔
796

797
        # Read fission energy release data
798
        if 'fission_energy_release' in group:
11✔
NEW
799
            fer_group = group['fission_energy_release']
×
NEW
800
            data.fission_energy = FissionEnergyRelease.from_hdf5(fer_group)
×
801

802
        # Close h5file when done if was opened
803
        if not isinstance(group_or_filename, h5py.Group):
11✔
804
            h5file.close()
11✔
805
            
806
        return data
11✔
807

808
    @classmethod
11✔
809
    def from_njoy(cls, filename, evaluation=None, **kwargs):
11✔
810
        """Generate incident photo-nuclear data by running NJOY.
811

812
        Parameters
813
        ----------
814
        filename : str
815
            Path to ENDF file
816
        evaluation : openmc.data.endf.Evaluation, optional
817
            If the ENDF file contains multiple material evaluations, this
818
            argument indicates which evaluation to use.
819
        **kwargs
820
            Keyword arguments passed to :func:`openmc.data.njoy.make_ace_photonuclear`
821

822
        Returns
823
        -------
824
        data : openmc.data.Photonuclear
825
            Incident photo-nuclear continuous-energy data
826

827
        """
828
        with tempfile.TemporaryDirectory() as tmpdir:
11✔
829
            # Run NJOY to create an ACE library
830
            kwargs.setdefault("output_dir", tmpdir)
11✔
831
            for key in ("acer", "pendf"):
11✔
832
                kwargs.setdefault(key, os.path.join(kwargs["output_dir"], key))
11✔
833
            kwargs["evaluation"] = evaluation
11✔
834
            make_ace_photonuclear(filename, **kwargs)
11✔
835

836
            # Create instance from ACE tables within library
837
            lib = Library(kwargs["acer"])
11✔
838
            data = cls.from_ace(lib.tables[0])
11✔
839
            
840
            # Add fission energy release data
841
            ev = evaluation if evaluation is not None else Evaluation(filename)
11✔
842
            if (1, 458) in ev.section:
11✔
843
                data.fission_energy = FissionEnergyRelease.from_endf(ev, data)
11✔
844

845
        return data
11✔
846

847
    def get_reaction_components(self, mt):
11✔
848
        """Determine what reactions make up redundant reaction.
849

850
        Parameters
851
        ----------
852
        mt : int
853
            ENDF MT number of the reaction to find components of.
854

855
        Returns
856
        -------
857
        mts : list of int
858
            ENDF MT numbers of reactions that make up the redundant reaction and
859
            have cross sections provided.
860

861
        """
862
        mts = []
11✔
863
        if mt in SUM_RULES:
11✔
864
            for mt_i in SUM_RULES[mt]:
11✔
865
                mts += self.get_reaction_components(mt_i)
11✔
866
        if mts:
11✔
867
            return mts
11✔
868
        else:
869
            return [mt] if mt in self else []        
11✔
870

871
    def _get_redundant_reaction(self, mt, mts):
11✔
872
        """Create redundant reaction from its components
873

874
        Parameters
875
        ----------
876
        mt : int
877
            MT value of the desired reaction
878
        mts : iterable of int
879
            MT values of its components
880

881
        Returns
882
        -------
883
        openmc.Reaction
884
            Redundant reaction
885

886
        """
887

NEW
888
        rx = PhotonuclearReaction(mt)
×
889
        # Get energy grid
NEW
890
        energy = self.energy
×
NEW
891
        xss = [self.reactions[mt_i].xs for mt_i in mts]
×
NEW
892
        idx = min([xs._threshold_idx if hasattr(xs, '_threshold_idx')
×
893
                   else 0 for xs in xss])
NEW
894
        rx.xs = Tabulated1D(energy[idx:], Sum(xss)(energy[idx:]))
×
NEW
895
        rx.xs._threshold_idx = idx
×
896

NEW
897
        rx.redundant = True
×
898

NEW
899
        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