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

openmc-dev / openmc / 22694846817

04 Mar 2026 11:42PM UTC coverage: 81.537% (-0.2%) from 81.69%
22694846817

Pull #3808

github

web-flow
Merge 41317cfa2 into 2bd06660c
Pull Request #3808: Add properties to settings w/ documentation, c++ loading of filename, and python round-trip test

17549 of 25285 branches covered (69.4%)

Branch coverage included in aggregate %.

82 of 92 new or added lines in 8 files covered. (89.13%)

1498 existing lines in 55 files now uncovered.

57990 of 67359 relevant lines covered (86.09%)

44766605.57 hits per line

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

80.63
/openmc/data/reaction.py
1
from collections.abc import Iterable, Callable, MutableMapping
11✔
2
from copy import deepcopy
11✔
3
from io import StringIO
11✔
4
from numbers import Real
11✔
5
from warnings import warn
11✔
6

7
import numpy as np
11✔
8

9
import openmc.checkvalue as cv
11✔
10
from openmc.mixin import EqualityMixin
11✔
11
from openmc.stats import Uniform, Tabular, Legendre
11✔
12
from .angle_distribution import AngleDistribution
11✔
13
from .angle_energy import AngleEnergy
11✔
14
from .correlated import CorrelatedAngleEnergy
11✔
15
from .data import ATOMIC_SYMBOL, K_BOLTZMANN, EV_PER_MEV
11✔
16
from .endf import get_head_record, get_tab1_record, get_list_record, \
11✔
17
    get_tab2_record, get_cont_record
18
from .energy_distribution import EnergyDistribution, LevelInelastic, \
11✔
19
    DiscretePhoton
20
from .function import Tabulated1D, Polynomial
11✔
21
from .kalbach_mann import KalbachMann
11✔
22
from .laboratory import LaboratoryAngleEnergy
11✔
23
from .nbody import NBodyPhaseSpace
11✔
24
from .photon import _SUBSHELLS
11✔
25
from .product import Product
11✔
26
from .uncorrelated import UncorrelatedAngleEnergy
11✔
27

28

29
REACTION_NAME = {1: '(n,total)', 2: '(n,elastic)', 3: "(n,nonelastic)",
11✔
30
                 4: '(n,level)', 5: '(n,misc)', 11: '(n,2nd)', 16: '(n,2n)',
31
                 17: '(n,3n)', 18: '(n,fission)', 19: '(n,f)', 20: '(n,nf)',
32
                 21: '(n,2nf)', 22: '(n,na)', 23: '(n,n3a)', 24: '(n,2na)',
33
                 25: '(n,3na)', 27: '(n,absorption)', 28: '(n,np)', 29: '(n,n2a)',
34
                 30: '(n,2n2a)', 32: '(n,nd)', 33: '(n,nt)', 34: '(n,n3He)',
35
                 35: '(n,nd2a)', 36: '(n,nt2a)', 37: '(n,4n)', 38: '(n,3nf)',
36
                 41: '(n,2np)', 42: '(n,3np)', 44: '(n,n2p)', 45: '(n,npa)',
37
                 91: '(n,nc)', 101: '(n,disappear)', 102: '(n,gamma)',
38
                 103: '(n,p)', 104: '(n,d)', 105: '(n,t)', 106: '(n,3He)',
39
                 107: '(n,a)', 108: '(n,2a)', 109: '(n,3a)', 111: '(n,2p)',
40
                 112: '(n,pa)', 113: '(n,t2a)', 114: '(n,d2a)', 115: '(n,pd)',
41
                 116: '(n,pt)', 117: '(n,da)', 152: '(n,5n)', 153: '(n,6n)',
42
                 154: '(n,2nt)', 155: '(n,ta)', 156: '(n,4np)', 157: '(n,3nd)',
43
                 158: '(n,nda)', 159: '(n,2npa)', 160: '(n,7n)', 161: '(n,8n)',
44
                 162: '(n,5np)', 163: '(n,6np)', 164: '(n,7np)', 165: '(n,4na)',
45
                 166: '(n,5na)', 167: '(n,6na)', 168: '(n,7na)', 169: '(n,4nd)',
46
                 170: '(n,5nd)', 171: '(n,6nd)', 172: '(n,3nt)', 173: '(n,4nt)',
47
                 174: '(n,5nt)', 175: '(n,6nt)', 176: '(n,2n3He)',
48
                 177: '(n,3n3He)', 178: '(n,4n3He)', 179: '(n,3n2p)',
49
                 180: '(n,3n2a)', 181: '(n,3npa)', 182: '(n,dt)',
50
                 183: '(n,npd)', 184: '(n,npt)', 185: '(n,ndt)',
51
                 186: '(n,np3He)', 187: '(n,nd3He)', 188: '(n,nt3He)',
52
                 189: '(n,nta)', 190: '(n,2n2p)', 191: '(n,p3He)',
53
                 192: '(n,d3He)', 193: '(n,3Hea)', 194: '(n,4n2p)',
54
                 195: '(n,4n2a)', 196: '(n,4npa)', 197: '(n,3p)',
55
                 198: '(n,n3p)', 199: '(n,3n2pa)', 200: '(n,5n2p)', 203: '(n,Xp)',
56
                 204: '(n,Xd)', 205: '(n,Xt)', 206: '(n,X3He)', 207: '(n,Xa)',
57
                 301: 'heating', 444: 'damage-energy',
58
                 501: 'photon-total', 502: 'coherent-scatter',
59
                 504: 'incoherent-scatter', 515: 'pair-production-electron',
60
                 516: 'pair-production', 517: 'pair-production-nuclear',
61
                 522: 'photoelectric',
62
                 649: '(n,pc)', 699: '(n,dc)', 749: '(n,tc)', 799: '(n,3Hec)',
63
                 849: '(n,ac)', 891: '(n,2nc)', 901: 'heating-local'}
64
REACTION_NAME.update({i: f'(n,n{i - 50})' for i in range(51, 91)})
11✔
65
REACTION_NAME.update({i: f'(n,p{i - 600})' for i in range(600, 649)})
11✔
66
REACTION_NAME.update({i: f'(n,d{i - 650})' for i in range(650, 699)})
11✔
67
REACTION_NAME.update({i: f'(n,t{i - 700})' for i in range(700, 749)})
11✔
68
REACTION_NAME.update({i: f'(n,3He{i - 750})' for i in range(750, 799)})
11✔
69
REACTION_NAME.update({i: f'(n,a{i - 800})' for i in range(800, 849)})
11✔
70
REACTION_NAME.update({i: f'(n,2n{i - 875})' for i in range(875, 891)})
11✔
71
REACTION_NAME.update(
11✔
72
    {534 + i: f'photoelectric-{shell}' for i, shell in enumerate(_SUBSHELLS[1:])}
73
)
74

75
REACTION_MT = {name: mt for mt, name in REACTION_NAME.items()}
11✔
76
REACTION_MT['total'] = 1
11✔
77
REACTION_MT['elastic'] = 2
11✔
78
REACTION_MT['fission'] = 18
11✔
79
REACTION_MT['absorption'] = 27
11✔
80
REACTION_MT['capture'] = 102
11✔
81

82
FISSION_MTS = (18, 19, 20, 21, 38)
11✔
83

84

85
def _get_products(ev, mt):
11✔
86
    """Generate products from MF=6 in an ENDF evaluation
87

88
    Parameters
89
    ----------
90
    ev : openmc.data.endf.Evaluation
91
        ENDF evaluation to read from
92
    mt : int
93
        The MT value of the reaction to get products for
94

95
    Raises
96
    ------
97
    IOError
98
        When the Kalbach-Mann systematics is used, but the product
99
        is not defined in the 'center-of-mass' system. The breakup logic
100
        is not implemented which can lead to this error being raised while
101
        the definition of the product is correct.
102

103
    Returns
104
    -------
105
    products : list of openmc.data.Product
106
        Products of the reaction
107

108
    """
109
    file_obj = StringIO(ev.section[6, mt])
11✔
110

111
    # Read HEAD record
112
    items = get_head_record(file_obj)
11✔
113
    reference_frame = {1: 'laboratory', 2: 'center-of-mass',
11✔
114
                       3: 'light-heavy', 4: 'breakup'}[items[3]]
115
    n_products = items[4]
11✔
116

117
    products = []
11✔
118
    for i in range(n_products):
11✔
119
        # Get yield for this product
120
        params, yield_ = get_tab1_record(file_obj)
11✔
121

122
        za = int(params[0])
11✔
123
        awr = params[1]
11✔
124
        law = params[3]
11✔
125

126
        if za == 0:
11✔
127
            p = Product('photon')
11✔
128
        elif za == 1:
11✔
129
            p = Product('neutron')
11✔
130
        elif za == 1000:
11✔
UNCOV
131
            p = Product('electron')
×
132
        else:
133
            Z, A = divmod(za, 1000)
11✔
134
            p = Product(f'{ATOMIC_SYMBOL[Z]}{A}')
11✔
135

136
        p.yield_ = yield_
11✔
137

138
        """
11✔
139
        # Set reference frame
140
        if reference_frame == 'laboratory':
141
            p.center_of_mass = False
142
        elif reference_frame == 'center-of-mass':
143
            p.center_of_mass = True
144
        elif reference_frame == 'light-heavy':
145
            p.center_of_mass = (awr <= 4.0)
146
        """
147

148
        if law == 0:
11✔
149
            # No distribution given
150
            pass
11✔
151
        if law == 1:
11✔
152
            # Continuum energy-angle distribution
153

154
            # Peak ahead to determine type of distribution
155
            position = file_obj.tell()
11✔
156
            params = get_cont_record(file_obj)
11✔
157
            file_obj.seek(position)
11✔
158

159
            lang = params[2]
11✔
160
            if lang == 1:
11✔
161
                p.distribution = [CorrelatedAngleEnergy.from_endf(file_obj)]
11✔
162
            elif lang == 2:
11✔
163
                # Products need to be described in the center-of-mass system
164
                product_center_of_mass = False
11✔
165
                if reference_frame == 'center-of-mass':
11✔
166
                    product_center_of_mass = True
11✔
167
                elif reference_frame == 'light-heavy':
11✔
168
                    product_center_of_mass = (awr <= 4.0)
11✔
169
                # TODO: 'breakup' logic not implemented
170

171
                if product_center_of_mass is False:
11✔
UNCOV
172
                    raise IOError(
×
173
                        "Kalbach-Mann representation must be defined in the "
174
                        "'center-of-mass' system"
175
                    )
176

177
                zat = ev.target["atomic_number"] * 1000 + ev.target["mass_number"]
11✔
178
                projectile_mass = ev.projectile["mass"]
11✔
179
                p.distribution = [KalbachMann.from_endf(file_obj,
11✔
180
                                                        za,
181
                                                        zat,
182
                                                        projectile_mass)]
183

184
        elif law == 2:
11✔
185
            # Discrete two-body scattering
186
            params, tab2 = get_tab2_record(file_obj)
11✔
187
            ne = params[5]
11✔
188
            energy = np.zeros(ne)
11✔
189
            mu = []
11✔
190
            for i in range(ne):
11✔
191
                items, values = get_list_record(file_obj)
11✔
192
                energy[i] = items[1]
11✔
193
                lang = items[2]
11✔
194
                if lang == 0:
11✔
195
                    mu.append(Legendre(values))
11✔
196
                elif lang == 12:
11✔
UNCOV
197
                    mu.append(Tabular(values[::2], values[1::2]))
×
198
                elif lang == 14:
11✔
199
                    mu.append(Tabular(values[::2], values[1::2],
11✔
200
                                      'log-linear'))
201

202
            angle_dist = AngleDistribution(energy, mu)
11✔
203
            dist = UncorrelatedAngleEnergy(angle_dist)
11✔
204
            p.distribution = [dist]
11✔
205
            # TODO: Add level-inelastic info?
206

207
        elif law == 3:
11✔
208
            # Isotropic discrete emission
209
            p.distribution = [UncorrelatedAngleEnergy()]
11✔
210
            # TODO: Add level-inelastic info?
211

212
        elif law == 4:
11✔
213
            # Discrete two-body recoil
214
            pass
11✔
215

216
        elif law == 5:
11✔
217
            # Charged particle elastic scattering
UNCOV
218
            pass
×
219

220
        elif law == 6:
11✔
221
            # N-body phase-space distribution
UNCOV
222
            p.distribution = [NBodyPhaseSpace.from_endf(file_obj)]
×
223

224
        elif law == 7:
11✔
225
            # Laboratory energy-angle distribution
226
            p.distribution = [LaboratoryAngleEnergy.from_endf(file_obj)]
11✔
227

228
        products.append(p)
11✔
229

230
    return products
11✔
231

232

233
def _get_fission_products_ace(ace):
11✔
234
    """Generate fission products from an ACE table
235

236
    Parameters
237
    ----------
238
    ace : openmc.data.ace.Table
239
        ACE table to read from
240

241
    Returns
242
    -------
243
    products : list of openmc.data.Product
244
        Prompt and delayed fission neutrons
245
    derived_products : list of openmc.data.Product
246
        "Total" fission neutron
247

248
    """
249
    # No NU block
250
    if ace.jxs[2] == 0:
11✔
251
        return None, None
×
252

253
    products = []
11✔
254
    derived_products = []
11✔
255

256
    # Either prompt nu or total nu is given
257
    if ace.xss[ace.jxs[2]] > 0:
11✔
258
        whichnu = 'prompt' if ace.jxs[24] > 0 else 'total'
×
259

260
        neutron = Product('neutron')
×
UNCOV
261
        neutron.emission_mode = whichnu
×
262

UNCOV
263
        idx = ace.jxs[2]
×
264
        LNU = int(ace.xss[idx])
×
UNCOV
265
        if LNU == 1:
×
266
            # Polynomial function form of nu
UNCOV
267
            NC = int(ace.xss[idx+1])
×
UNCOV
268
            coefficients = ace.xss[idx+2 : idx+2+NC].copy()
×
UNCOV
269
            for i in range(coefficients.size):
×
UNCOV
270
                coefficients[i] *= EV_PER_MEV**(-i)
×
UNCOV
271
            neutron.yield_ = Polynomial(coefficients)
×
UNCOV
272
        elif LNU == 2:
×
273
            # Tabular data form of nu
UNCOV
274
            neutron.yield_ = Tabulated1D.from_ace(ace, idx + 1)
×
275

276
        products.append(neutron)
×
277

278
    # Both prompt nu and total nu
279
    elif ace.xss[ace.jxs[2]] < 0:
11✔
280
        # Read prompt neutron yield
281
        prompt_neutron = Product('neutron')
11✔
282
        prompt_neutron.emission_mode = 'prompt'
11✔
283

284
        idx = ace.jxs[2] + 1
11✔
285
        LNU = int(ace.xss[idx])
11✔
286
        if LNU == 1:
11✔
287
            # Polynomial function form of nu
UNCOV
288
            NC = int(ace.xss[idx+1])
×
UNCOV
289
            coefficients = ace.xss[idx+2 : idx+2+NC].copy()
×
UNCOV
290
            for i in range(coefficients.size):
×
UNCOV
291
                coefficients[i] *= EV_PER_MEV**(-i)
×
UNCOV
292
            prompt_neutron.yield_ = Polynomial(coefficients)
×
293
        elif LNU == 2:
11✔
294
            # Tabular data form of nu
295
            prompt_neutron.yield_ = Tabulated1D.from_ace(ace, idx + 1)
11✔
296

297
        # Read total neutron yield
298
        total_neutron = Product('neutron')
11✔
299
        total_neutron.emission_mode = 'total'
11✔
300

301
        idx = ace.jxs[2] + int(abs(ace.xss[ace.jxs[2]])) + 1
11✔
302
        LNU = int(ace.xss[idx])
11✔
303

304
        if LNU == 1:
11✔
305
            # Polynomial function form of nu
UNCOV
306
            NC = int(ace.xss[idx+1])
×
UNCOV
307
            coefficients = ace.xss[idx+2 : idx+2+NC].copy()
×
308
            for i in range(coefficients.size):
×
UNCOV
309
                coefficients[i] *= EV_PER_MEV**(-i)
×
UNCOV
310
            total_neutron.yield_ = Polynomial(coefficients)
×
311
        elif LNU == 2:
11✔
312
            # Tabular data form of nu
313
            total_neutron.yield_ = Tabulated1D.from_ace(ace, idx + 1)
11✔
314

315
        products.append(prompt_neutron)
11✔
316
        derived_products.append(total_neutron)
11✔
317

318
    # Check for delayed nu data
319
    if ace.jxs[24] > 0:
11✔
UNCOV
320
        yield_delayed = Tabulated1D.from_ace(ace, ace.jxs[24] + 1)
×
321

322
        # Delayed neutron precursor distribution
323
        idx = ace.jxs[25]
×
324
        n_group = ace.nxs[8]
×
325
        total_group_probability = 0.
×
UNCOV
326
        for group in range(n_group):
×
UNCOV
327
            delayed_neutron = Product('neutron')
×
UNCOV
328
            delayed_neutron.emission_mode = 'delayed'
×
329

330
            # Convert units of inverse shakes to inverse seconds
331
            delayed_neutron.decay_rate = ace.xss[idx] * 1.e8
×
332

UNCOV
333
            group_probability = Tabulated1D.from_ace(ace, idx + 1)
×
334
            if np.all(group_probability.y == group_probability.y[0]):
×
335
                delayed_neutron.yield_ = deepcopy(yield_delayed)
×
UNCOV
336
                delayed_neutron.yield_.y *= group_probability.y[0]
×
UNCOV
337
                total_group_probability += group_probability.y[0]
×
338
            else:
339
                # Get union energy grid and ensure energies are within
340
                # interpolable range of both functions
UNCOV
341
                max_energy = min(yield_delayed.x[-1], group_probability.x[-1])
×
UNCOV
342
                energy = np.union1d(yield_delayed.x, group_probability.x)
×
343
                energy = energy[energy <= max_energy]
×
344

345
                # Calculate group yield
UNCOV
346
                group_yield = yield_delayed(energy) * group_probability(energy)
×
347
                delayed_neutron.yield_ = Tabulated1D(energy, group_yield)
×
348

349
            # Advance position
UNCOV
350
            nr = int(ace.xss[idx + 1])
×
351
            ne = int(ace.xss[idx + 2 + 2*nr])
×
352
            idx += 3 + 2*nr + 2*ne
×
353

354
            # Energy distribution for delayed fission neutrons
UNCOV
355
            location_start = int(ace.xss[ace.jxs[26] + group])
×
UNCOV
356
            delayed_neutron.distribution.append(
×
357
                AngleEnergy.from_ace(ace, ace.jxs[27], location_start))
358

UNCOV
359
            products.append(delayed_neutron)
×
360

361
        # Renormalize delayed neutron yields to reflect fact that in ACE
362
        # file, the sum of the group probabilities is not exactly one
UNCOV
363
        for product in products[1:]:
×
UNCOV
364
            if total_group_probability > 0.:
×
UNCOV
365
                product.yield_.y /= total_group_probability
×
366

367
    return products, derived_products
11✔
368

369

370
def _get_fission_products_endf(ev):
11✔
371
    """Generate fission products from an ENDF evaluation
372

373
    Parameters
374
    ----------
375
    ev : openmc.data.endf.Evaluation
376

377
    Returns
378
    -------
379
    products : list of openmc.data.Product
380
        Prompt and delayed fission neutrons
381
    derived_products : list of openmc.data.Product
382
        "Total" fission neutron
383

384
    """
385
    products = []
11✔
386
    derived_products = []
11✔
387

388
    if (1, 456) in ev.section:
11✔
389
        prompt_neutron = Product('neutron')
11✔
390
        prompt_neutron.emission_mode = 'prompt'
11✔
391

392
        # Prompt nu values
393
        file_obj = StringIO(ev.section[1, 456])
11✔
394
        lnu = get_head_record(file_obj)[3]
11✔
395
        if lnu == 1:
11✔
396
            # Polynomial representation
UNCOV
397
            items, coefficients = get_list_record(file_obj)
×
UNCOV
398
            prompt_neutron.yield_ = Polynomial(coefficients)
×
399
        elif lnu == 2:
11✔
400
            # Tabulated representation
401
            params, prompt_neutron.yield_ = get_tab1_record(file_obj)
11✔
402

403
        products.append(prompt_neutron)
11✔
404

405
    if (1, 452) in ev.section:
11✔
406
        total_neutron = Product('neutron')
11✔
407
        total_neutron.emission_mode = 'total'
11✔
408

409
        # Total nu values
410
        file_obj = StringIO(ev.section[1, 452])
11✔
411
        lnu = get_head_record(file_obj)[3]
11✔
412
        if lnu == 1:
11✔
413
            # Polynomial representation
UNCOV
414
            items, coefficients = get_list_record(file_obj)
×
UNCOV
415
            total_neutron.yield_ = Polynomial(coefficients)
×
416
        elif lnu == 2:
11✔
417
            # Tabulated representation
418
            params, total_neutron.yield_ = get_tab1_record(file_obj)
11✔
419

420
        if (1, 456) in ev.section:
11✔
421
            derived_products.append(total_neutron)
11✔
422
        else:
UNCOV
423
            products.append(total_neutron)
×
424

425
    if (1, 455) in ev.section:
11✔
426
        file_obj = StringIO(ev.section[1, 455])
11✔
427

428
        # Determine representation of delayed nu data
429
        items = get_head_record(file_obj)
11✔
430
        ldg = items[2]
11✔
431
        lnu = items[3]
11✔
432

433
        if ldg == 0:
11✔
434
            # Delayed-group constants energy independent
435
            items, decay_constants = get_list_record(file_obj)
11✔
436
            for constant in decay_constants:
11✔
437
                delayed_neutron = Product('neutron')
11✔
438
                delayed_neutron.emission_mode = 'delayed'
11✔
439
                delayed_neutron.decay_rate = constant
11✔
440
                products.append(delayed_neutron)
11✔
441
        elif ldg == 1:
×
442
            # Delayed-group constants energy dependent
443
            raise NotImplementedError('Delayed neutron with energy-dependent '
×
444
                                      'group constants.')
445

446
        # In MF=1, MT=455, the delayed-group abundances are actually not
447
        # specified if the group constants are energy-independent. In this case,
448
        # the abundances must be inferred from MF=5, MT=455 where multiple
449
        # energy distributions are given.
450
        if lnu == 1:
11✔
451
            # Nu represented as polynomial
UNCOV
452
            items, coefficients = get_list_record(file_obj)
×
UNCOV
453
            yield_ = Polynomial(coefficients)
×
UNCOV
454
            for neutron in products[-6:]:
×
UNCOV
455
                neutron.yield_ = deepcopy(yield_)
×
456
        elif lnu == 2:
11✔
457
            # Nu represented by tabulation
458
            params, yield_ = get_tab1_record(file_obj)
11✔
459
            for neutron in products[-6:]:
11✔
460
                neutron.yield_ = deepcopy(yield_)
11✔
461

462
        if (5, 455) in ev.section:
11✔
463
            file_obj = StringIO(ev.section[5, 455])
11✔
464
            items = get_head_record(file_obj)
11✔
465
            nk = items[4]
11✔
466
            if nk > 1 and len(decay_constants) == 1:
11✔
467
                # If only one precursor group is listed in MF=1, MT=455, use the
468
                # energy spectra from MF=5 to split them into different groups
UNCOV
469
                for _ in range(nk - 1):
×
UNCOV
470
                    products.append(deepcopy(products[1]))
×
471
            elif nk != len(decay_constants):
11✔
UNCOV
472
                raise ValueError(
×
473
                    'Number of delayed neutron fission spectra ({}) does not '
474
                    'match number of delayed neutron precursors ({}).'.format(
475
                        nk, len(decay_constants)))
476
            for i in range(nk):
11✔
477
                params, applicability = get_tab1_record(file_obj)
11✔
478
                dist = UncorrelatedAngleEnergy()
11✔
479
                dist.energy = EnergyDistribution.from_endf(file_obj, params)
11✔
480

481
                delayed_neutron = products[1 + i]
11✔
482
                yield_ = delayed_neutron.yield_
11✔
483

484
                # Here we handle the fact that the delayed neutron yield is the
485
                # product of the total delayed neutron yield and the
486
                # "applicability" of the energy distribution law in file 5.
487
                if isinstance(yield_, Tabulated1D):
11✔
488
                    if np.all(applicability.y == applicability.y[0]):
11✔
489
                        yield_.y *= applicability.y[0]
11✔
490
                    else:
491
                        # Get union energy grid and ensure energies are within
492
                        # interpolable range of both functions
493
                        max_energy = min(yield_.x[-1], applicability.x[-1])
×
494
                        energy = np.union1d(yield_.x, applicability.x)
×
UNCOV
495
                        energy = energy[energy <= max_energy]
×
496

497
                        # Calculate group yield
UNCOV
498
                        group_yield = yield_(energy) * applicability(energy)
×
UNCOV
499
                        delayed_neutron.yield_ = Tabulated1D(energy, group_yield)
×
UNCOV
500
                elif isinstance(yield_, Polynomial):
×
UNCOV
501
                    if len(yield_) == 1:
×
UNCOV
502
                        delayed_neutron.yield_ = deepcopy(applicability)
×
UNCOV
503
                        delayed_neutron.yield_.y *= yield_.coef[0]
×
504
                    else:
UNCOV
505
                        if np.all(applicability.y == applicability.y[0]):
×
UNCOV
506
                            yield_.coef[0] *= applicability.y[0]
×
507
                        else:
UNCOV
508
                            raise NotImplementedError(
×
509
                                'Total delayed neutron yield and delayed group '
510
                                'probability are both energy-dependent.')
511

512
                delayed_neutron.distribution.append(dist)
11✔
513

514
    return products, derived_products
11✔
515

516

517
def _get_activation_products(ev, rx):
11✔
518
    """Generate activation products from an ENDF evaluation
519

520
    Parameters
521
    ----------
522
    ev : openmc.data.endf.Evaluation
523
        The ENDF evaluation
524
    rx : openmc.data.Reaction
525
        Reaction which generates activation products
526

527
    Returns
528
    -------
529
    products : list of openmc.data.Product
530
        Activation products
531

532
    """
533
    file_obj = StringIO(ev.section[8, rx.mt])
11✔
534

535
    # Determine total number of states and whether decay chain is given in a
536
    # decay sublibrary
537
    items = get_head_record(file_obj)
11✔
538
    n_states = items[4]
11✔
539
    decay_sublib = (items[5] == 1)
11✔
540

541
    # Determine if file 9/10 are present
542
    present = {9: False, 10: False}
11✔
543
    for _ in range(n_states):
11✔
544
        if decay_sublib:
11✔
545
            items = get_cont_record(file_obj)
11✔
546
        else:
547
            items, values = get_list_record(file_obj)
11✔
548
        lmf = items[2]
11✔
549
        if lmf == 9:
11✔
550
            present[9] = True
11✔
551
        elif lmf == 10:
11✔
UNCOV
552
            present[10] = True
×
553

554
    products = []
11✔
555

556
    for mf in (9, 10):
11✔
557
        if not present[mf]:
11✔
558
            continue
11✔
559

560
        file_obj = StringIO(ev.section[mf, rx.mt])
11✔
561
        items = get_head_record(file_obj)
11✔
562
        n_states = items[4]
11✔
563
        for i in range(n_states):
11✔
564
            # Determine what the product is
565
            items, xs = get_tab1_record(file_obj)
11✔
566
            Z, A = divmod(items[2], 1000)
11✔
567
            excited_state = items[3]
11✔
568

569
            # Get GNDS name for product
570
            symbol = ATOMIC_SYMBOL[Z]
11✔
571
            if excited_state > 0:
11✔
572
                name = f'{symbol}{A}_e{excited_state}'
11✔
573
            else:
574
                name = f'{symbol}{A}'
11✔
575

576
            p = Product(name)
11✔
577
            if mf == 9:
11✔
578
                p.yield_ = xs
11✔
579
            else:
580
                # Re-interpolate production cross section and neutron cross
581
                # section to union energy grid
UNCOV
582
                energy = np.union1d(xs.x, rx.xs['0K'].x)
×
UNCOV
583
                prod_xs = xs(energy)
×
584
                neutron_xs = rx.xs['0K'](energy)
×
585
                idx = np.where(neutron_xs > 0)
×
586

587
                # Calculate yield as ratio
UNCOV
588
                yield_ = np.zeros_like(energy)
×
UNCOV
589
                yield_[idx] = prod_xs[idx] / neutron_xs[idx]
×
UNCOV
590
                p.yield_ = Tabulated1D(energy, yield_)
×
591

592
            # Check if product already exists from MF=6 and if it does, just
593
            # overwrite the existing yield.
594
            for product in rx.products:
11✔
595
                if name == product.particle:
11✔
UNCOV
596
                    product.yield_ = p.yield_
×
UNCOV
597
                    break
×
598
            else:
599
                products.append(p)
11✔
600

601
    return products
11✔
602

603

604
def _get_photon_products_ace(ace, rx):
11✔
605
    """Generate photon products from an ACE table
606

607
    Parameters
608
    ----------
609
    ace : openmc.data.ace.Table
610
        ACE table to read from
611
    rx : openmc.data.Reaction
612
        Reaction that generates photons
613

614
    Returns
615
    -------
616
    photons : list of openmc.Products
617
        Photons produced from reaction with given MT
618

619
    """
620
    n_photon_reactions = ace.nxs[6]
11✔
621
    photon_mts = ace.xss[ace.jxs[13]:ace.jxs[13] +
11✔
622
                         n_photon_reactions].astype(int)
623

624
    photons = []
11✔
625
    for i in range(n_photon_reactions):
11✔
626
        # Determine corresponding reaction
627
        neutron_mt = photon_mts[i] // 1000
11✔
628

629
        if neutron_mt != rx.mt:
11✔
630
            continue
11✔
631

632
        # Create photon product and assign to reactions
633
        photon = Product('photon')
11✔
634

635
        # ==================================================================
636
        # Photon yield / production cross section
637

638
        loca = int(ace.xss[ace.jxs[14] + i])
11✔
639
        idx = ace.jxs[15] + loca - 1
11✔
640
        mftype = int(ace.xss[idx])
11✔
641
        idx += 1
11✔
642

643
        if mftype in (12, 16):
11✔
644
            # Yield data taken from ENDF File 12 or 6
645
            mtmult = int(ace.xss[idx])
11✔
646
            assert mtmult == neutron_mt
11✔
647

648
            # Read photon yield as function of energy
649
            photon.yield_ = Tabulated1D.from_ace(ace, idx + 1)
11✔
650

651
        elif mftype == 13:
×
652
            # Cross section data from ENDF File 13
653

654
            # Energy grid index at which data starts
655
            threshold_idx = int(ace.xss[idx]) - 1
×
656
            n_energy = int(ace.xss[idx + 1])
×
UNCOV
657
            energy = ace.xss[ace.jxs[1] + threshold_idx:
×
658
                             ace.jxs[1] + threshold_idx + n_energy]*EV_PER_MEV
659

660
            # Get photon production cross section
UNCOV
661
            photon_prod_xs = ace.xss[idx + 2:idx + 2 + n_energy]
×
UNCOV
662
            neutron_xs = list(rx.xs.values())[0](energy)
×
UNCOV
663
            idx = np.where(neutron_xs > 0.)
×
664

665
            # Calculate photon yield
UNCOV
666
            yield_ = np.zeros_like(photon_prod_xs)
×
UNCOV
667
            yield_[idx] = photon_prod_xs[idx] / neutron_xs[idx]
×
UNCOV
668
            photon.yield_ = Tabulated1D(energy, yield_)
×
669

670
        else:
UNCOV
671
            raise ValueError(f"MFTYPE must be 12, 13, 16. Got {mftype}")
×
672

673
        # ==================================================================
674
        # Photon energy distribution
675

676
        location_start = int(ace.xss[ace.jxs[18] + i])
11✔
677
        distribution = AngleEnergy.from_ace(ace, ace.jxs[19], location_start)
11✔
678
        assert isinstance(distribution, UncorrelatedAngleEnergy)
11✔
679

680
        # ==================================================================
681
        # Photon angular distribution
682
        loc = int(ace.xss[ace.jxs[16] + i])
11✔
683

684
        if loc == 0:
11✔
685
            # No angular distribution data are given for this reaction,
686
            # isotropic scattering is asssumed in LAB
687
            energy = np.array([photon.yield_.x[0], photon.yield_.x[-1]])
11✔
688
            mu_isotropic = Uniform(-1., 1.)
11✔
689
            distribution.angle = AngleDistribution(
11✔
690
                energy, [mu_isotropic, mu_isotropic])
691
        else:
UNCOV
692
            distribution.angle = AngleDistribution.from_ace(ace, ace.jxs[17], loc)
×
693

694
        # Add to list of distributions
695
        photon.distribution.append(distribution)
11✔
696
        photons.append(photon)
11✔
697

698
    return photons
11✔
699

700

701
def _get_photon_products_endf(ev, rx):
11✔
702
    """Generate photon products from an ENDF evaluation
703

704
    Parameters
705
    ----------
706
    ev : openmc.data.endf.Evaluation
707
        ENDF evaluation to read from
708
    rx : openmc.data.Reaction
709
        Reaction that generates photons
710

711
    Returns
712
    -------
713
    products : list of openmc.Products
714
        Photons produced from reaction with given MT
715

716
    """
717
    products = []
11✔
718

719
    if (12, rx.mt) in ev.section:
11✔
720
        file_obj = StringIO(ev.section[12, rx.mt])
11✔
721

722
        items = get_head_record(file_obj)
11✔
723
        option = items[2]
11✔
724

725
        if option == 1:
11✔
726
            # Multiplicities given
727
            n_discrete_photon = items[4]
11✔
728
            if n_discrete_photon > 1:
11✔
729
                items, total_yield = get_tab1_record(file_obj)
11✔
730
            for k in range(n_discrete_photon):
11✔
731
                photon = Product('photon')
11✔
732

733
                # Get photon yield
734
                items, photon.yield_ = get_tab1_record(file_obj)
11✔
735

736
                # Get photon energy distribution
737
                law = items[3]
11✔
738
                dist = UncorrelatedAngleEnergy()
11✔
739
                if law == 1:
11✔
740
                    # TODO: Get file 15 distribution
741
                    pass
11✔
742
                elif law == 2:
11✔
743
                    energy = items[0]
11✔
744
                    primary_flag = items[2]
11✔
745
                    dist.energy = DiscretePhoton(primary_flag, energy,
11✔
746
                                                 ev.target['mass'])
747

748
                photon.distribution.append(dist)
11✔
749
                products.append(photon)
11✔
750

751
        elif option == 2:
11✔
752
            # Transition probability arrays given
753
            ppyield = {}
11✔
754
            ppyield['type'] = 'transition'
11✔
755
            ppyield['transition'] = transition = {}
11✔
756

757
            # Determine whether simple (LG=1) or complex (LG=2) transitions
758
            lg = items[3]
11✔
759

760
            # Get transition data
761
            items, values = get_list_record(file_obj)
11✔
762
            transition['energy_start'] = items[0]
11✔
763
            transition['energies'] = np.array(values[::lg + 1])
11✔
764
            transition['direct_probability'] = np.array(values[1::lg + 1])
11✔
765
            if lg == 2:
11✔
766
                # Complex case
767
                transition['conditional_probability'] = np.array(
11✔
768
                    values[2::lg + 1])
769

770
    elif (13, rx.mt) in ev.section:
11✔
771
        file_obj = StringIO(ev.section[13, rx.mt])
11✔
772

773
        # Determine option
774
        items = get_head_record(file_obj)
11✔
775
        n_discrete_photon = items[4]
11✔
776
        if n_discrete_photon > 1:
11✔
777
            items, total_xs = get_tab1_record(file_obj)
11✔
778
        for k in range(n_discrete_photon):
11✔
779
            photon = Product('photon')
11✔
780
            items, xs = get_tab1_record(file_obj)
11✔
781

782
            # Re-interpolate photon production cross section and neutron cross
783
            # section to union energy grid
784
            energy = np.union1d(xs.x, rx.xs['0K'].x)
11✔
785
            photon_prod_xs = xs(energy)
11✔
786
            neutron_xs = rx.xs['0K'](energy)
11✔
787
            idx = np.where(neutron_xs > 0)
11✔
788

789
            # Calculate yield as ratio
790
            yield_ = np.zeros_like(energy)
11✔
791
            yield_[idx] = photon_prod_xs[idx] / neutron_xs[idx]
11✔
792
            photon.yield_ = Tabulated1D(energy, yield_)
11✔
793

794
            # Get photon energy distribution
795
            law = items[3]
11✔
796
            dist = UncorrelatedAngleEnergy()
11✔
797
            if law == 1:
11✔
798
                # TODO: Get file 15 distribution
799
                pass
11✔
800
            elif law == 2:
11✔
801
                energy = items[1]
11✔
802
                primary_flag = items[2]
11✔
803
                dist.energy = DiscretePhoton(primary_flag, energy,
11✔
804
                                             ev.target['mass'])
805

806
            photon.distribution.append(dist)
11✔
807
            products.append(photon)
11✔
808

809
    return products
11✔
810

811

812
class Reaction(EqualityMixin):
11✔
813
    """A nuclear reaction
814

815
    A Reaction object represents a single reaction channel for a nuclide with
816
    an associated cross section and, if present, a secondary angle and energy
817
    distribution.
818

819
    Parameters
820
    ----------
821
    mt : int
822
        The ENDF MT number for this reaction.
823

824
    Attributes
825
    ----------
826
    center_of_mass : bool
827
        Indicates whether scattering kinematics should be performed in the
828
        center-of-mass or laboratory reference frame.
829
        grid above the threshold value in barns.
830
    redundant : bool
831
        Indicates whether or not this is a redundant reaction
832
    mt : int
833
        The ENDF MT number for this reaction.
834
    q_value : float
835
        The Q-value of this reaction in eV.
836
    xs : dict of str to openmc.data.Function1D
837
        Microscopic cross section for this reaction as a function of incident
838
        energy; these cross sections are provided in a dictionary where the key
839
        is the temperature of the cross section set.
840
    products : Iterable of openmc.data.Product
841
        Reaction products
842
    derived_products : Iterable of openmc.data.Product
843
        Derived reaction products. Used for 'total' fission neutron data when
844
        prompt/delayed data also exists.
845

846
    """
847

848
    def __init__(self, mt):
11✔
849
        self._center_of_mass = True
11✔
850
        self._redundant = False
11✔
851
        self._q_value = 0.
11✔
852
        self._xs = {}
11✔
853
        self._products = []
11✔
854
        self._derived_products = []
11✔
855

856
        self.mt = mt
11✔
857

858
    def __repr__(self):
11✔
UNCOV
859
        if self.mt in REACTION_NAME:
×
UNCOV
860
            return f"<Reaction: MT={self.mt} {REACTION_NAME[self.mt]}>"
×
861
        else:
UNCOV
862
            return f"<Reaction: MT={self.mt}>"
×
863

864
    @property
11✔
865
    def center_of_mass(self):
11✔
866
        return self._center_of_mass
11✔
867

868
    @center_of_mass.setter
11✔
869
    def center_of_mass(self, center_of_mass):
11✔
870
        cv.check_type('center of mass', center_of_mass, (bool, np.bool_))
11✔
871
        self._center_of_mass = center_of_mass
11✔
872

873
    @property
11✔
874
    def redundant(self):
11✔
875
        return self._redundant
11✔
876

877
    @redundant.setter
11✔
878
    def redundant(self, redundant):
11✔
879
        cv.check_type('redundant', redundant, (bool, np.bool_))
11✔
880
        self._redundant = redundant
11✔
881

882
    @property
11✔
883
    def q_value(self):
11✔
884
        return self._q_value
11✔
885

886
    @q_value.setter
11✔
887
    def q_value(self, q_value):
11✔
888
        cv.check_type('Q value', q_value, Real)
11✔
889
        self._q_value = q_value
11✔
890

891
    @property
11✔
892
    def products(self):
11✔
893
        return self._products
11✔
894

895
    @products.setter
11✔
896
    def products(self, products):
11✔
897
        cv.check_type('reaction products', products, Iterable, Product)
11✔
898
        self._products = products
11✔
899

900
    @property
11✔
901
    def derived_products(self):
11✔
902
        return self._derived_products
11✔
903

904
    @derived_products.setter
11✔
905
    def derived_products(self, derived_products):
11✔
906
        cv.check_type('reaction derived products', derived_products,
11✔
907
                      Iterable, Product)
908
        self._derived_products = derived_products
11✔
909

910
    @property
11✔
911
    def xs(self):
11✔
912
        return self._xs
11✔
913

914
    @xs.setter
11✔
915
    def xs(self, xs):
11✔
UNCOV
916
        cv.check_type('reaction cross section dictionary', xs, MutableMapping)
×
UNCOV
917
        for key, value in xs.items():
×
UNCOV
918
            cv.check_type('reaction cross section temperature', key, str)
×
UNCOV
919
            cv.check_type('reaction cross section', value, Callable)
×
UNCOV
920
        self._xs = xs
×
921

922
    def to_hdf5(self, group):
11✔
923
        """Write reaction to an HDF5 group
924

925
        Parameters
926
        ----------
927
        group : h5py.Group
928
            HDF5 group to write to
929

930
        """
931

932
        group.attrs['mt'] = self.mt
11✔
933
        if self.mt in REACTION_NAME:
11✔
934
            group.attrs['label'] = np.bytes_(REACTION_NAME[self.mt])
11✔
935
        else:
UNCOV
936
            group.attrs['label'] = np.bytes_(self.mt)
×
937
        group.attrs['Q_value'] = self.q_value
11✔
938
        group.attrs['center_of_mass'] = 1 if self.center_of_mass else 0
11✔
939
        group.attrs['redundant'] = 1 if self.redundant else 0
11✔
940
        for T in self.xs:
11✔
941
            Tgroup = group.create_group(T)
11✔
942
            if self.xs[T] is not None:
11✔
943
                dset = Tgroup.create_dataset('xs', data=self.xs[T].y)
11✔
944
                threshold_idx = getattr(self.xs[T], '_threshold_idx', 0)
11✔
945
                dset.attrs['threshold_idx'] = threshold_idx
11✔
946
        for i, p in enumerate(self.products):
11✔
947
            pgroup = group.create_group(f'product_{i}')
11✔
948
            p.to_hdf5(pgroup)
11✔
949

950
    @classmethod
11✔
951
    def from_hdf5(cls, group, energy):
11✔
952
        """Generate reaction from an HDF5 group
953

954
        Parameters
955
        ----------
956
        group : h5py.Group
957
            HDF5 group to read from
958
        energy : dict
959
            Dictionary whose keys are temperatures (e.g., '300K') and values are
960
            arrays of energies at which cross sections are tabulated at.
961

962
        Returns
963
        -------
964
        openmc.data.Reaction
965
            Reaction data
966

967
        """
968

969
        mt = group.attrs['mt']
11✔
970
        rx = cls(mt)
11✔
971
        rx.q_value = group.attrs['Q_value']
11✔
972
        rx.center_of_mass = bool(group.attrs['center_of_mass'])
11✔
973
        rx.redundant = bool(group.attrs.get('redundant', False))
11✔
974

975
        # Read cross section at each temperature
976
        for T, Tgroup in group.items():
11✔
977
            if T.endswith('K'):
11✔
978
                if 'xs' in Tgroup:
11✔
979
                    # Make sure temperature has associated energy grid
980
                    if T not in energy:
11✔
UNCOV
981
                        raise ValueError(
×
982
                            'Could not create reaction cross section for MT={} '
983
                            'at T={} because no corresponding energy grid '
984
                            'exists.'.format(mt, T))
985
                    xs = Tgroup['xs'][()]
11✔
986
                    threshold_idx = Tgroup['xs'].attrs['threshold_idx']
11✔
987
                    tabulated_xs = Tabulated1D(energy[T][threshold_idx:], xs)
11✔
988
                    tabulated_xs._threshold_idx = threshold_idx
11✔
989
                    rx.xs[T] = tabulated_xs
11✔
990

991
        # Determine number of products
992
        n_product = 0
11✔
993
        for name in group:
11✔
994
            if name.startswith('product_'):
11✔
995
                n_product += 1
11✔
996

997
        # Read reaction products
998
        for i in range(n_product):
11✔
999
            pgroup = group[f'product_{i}']
11✔
1000
            rx.products.append(Product.from_hdf5(pgroup))
11✔
1001

1002
        return rx
11✔
1003

1004
    @classmethod
11✔
1005
    def from_ace(cls, ace, i_reaction):
11✔
1006
        # Get nuclide energy grid
1007
        n_grid = ace.nxs[3]
11✔
1008
        grid = ace.xss[ace.jxs[1]:ace.jxs[1] + n_grid]*EV_PER_MEV
11✔
1009

1010
        # Convert data temperature to a "300.0K" number for indexing
1011
        # temperature data
1012
        strT = str(int(round(ace.temperature*EV_PER_MEV / K_BOLTZMANN))) + "K"
11✔
1013

1014
        if i_reaction > 0:
11✔
1015
            mt = int(ace.xss[ace.jxs[3] + i_reaction - 1])
11✔
1016
            rx = cls(mt)
11✔
1017

1018
            # Get Q-value of reaction
1019
            rx.q_value = ace.xss[ace.jxs[4] + i_reaction - 1]*EV_PER_MEV
11✔
1020

1021
            # ==================================================================
1022
            # CROSS SECTION
1023

1024
            # Get locator for cross-section data
1025
            loc = int(ace.xss[ace.jxs[6] + i_reaction - 1])
11✔
1026

1027
            # Determine starting index on energy grid
1028
            threshold_idx = int(ace.xss[ace.jxs[7] + loc - 1]) - 1
11✔
1029

1030
            # Determine number of energies in reaction
1031
            n_energy = int(ace.xss[ace.jxs[7] + loc])
11✔
1032
            energy = grid[threshold_idx:threshold_idx + n_energy]
11✔
1033

1034
            # Read reaction cross section
1035
            xs = ace.xss[ace.jxs[7] + loc + 1:ace.jxs[7] + loc + 1 + n_energy]
11✔
1036

1037
            # For damage energy production, convert to eV
1038
            if mt == 444:
11✔
1039
                xs *= EV_PER_MEV
11✔
1040

1041
            # Fix negatives -- known issue for Y89 in JEFF 3.2
1042
            if np.any(xs < 0.0):
11✔
UNCOV
1043
                warn("Negative cross sections found for MT={} in {}. Setting "
×
1044
                     "to zero.".format(rx.mt, ace.name))
UNCOV
1045
                xs[xs < 0.0] = 0.0
×
1046

1047
            tabulated_xs = Tabulated1D(energy, xs)
11✔
1048
            tabulated_xs._threshold_idx = threshold_idx
11✔
1049
            rx.xs[strT] = tabulated_xs
11✔
1050

1051
            # ==================================================================
1052
            # YIELD AND ANGLE-ENERGY DISTRIBUTION
1053

1054
            # Determine multiplicity
1055
            ty = int(ace.xss[ace.jxs[5] + i_reaction - 1])
11✔
1056
            rx.center_of_mass = (ty < 0)
11✔
1057
            if i_reaction < ace.nxs[5] + 1:
11✔
1058
                if ty != 19:
11✔
1059
                    if abs(ty) > 100:
11✔
1060
                        # Energy-dependent neutron yield
1061
                        idx = ace.jxs[11] + abs(ty) - 101
11✔
1062
                        yield_ = Tabulated1D.from_ace(ace, idx)
11✔
1063
                    else:
1064
                        # 0-order polynomial i.e. a constant
1065
                        yield_ = Polynomial((abs(ty),))
11✔
1066

1067
                    neutron = Product('neutron')
11✔
1068
                    neutron.yield_ = yield_
11✔
1069
                    rx.products.append(neutron)
11✔
1070
                else:
1071
                    assert mt in FISSION_MTS
11✔
1072
                    rx.products, rx.derived_products = _get_fission_products_ace(ace)
11✔
1073

1074
                    for p in rx.products:
11✔
1075
                        if p.emission_mode in ('prompt', 'total'):
11✔
1076
                            neutron = p
11✔
1077
                            break
11✔
1078
                    else:
UNCOV
1079
                        raise Exception("Couldn't find prompt/total fission neutron")
×
1080

1081
                # Determine locator for ith energy distribution
1082
                lnw = int(ace.xss[ace.jxs[10] + i_reaction - 1])
11✔
1083
                while lnw > 0:
11✔
1084
                    # Applicability of this distribution
1085
                    neutron.applicability.append(Tabulated1D.from_ace(
11✔
1086
                        ace, ace.jxs[11] + lnw + 2))
1087

1088
                    # Read energy distribution data
1089
                    neutron.distribution.append(AngleEnergy.from_ace(
11✔
1090
                        ace, ace.jxs[11], lnw, rx))
1091

1092
                    lnw = int(ace.xss[ace.jxs[11] + lnw - 1])
11✔
1093

1094
        else:
1095
            # Elastic scattering
1096
            mt = 2
11✔
1097
            rx = cls(mt)
11✔
1098

1099
            # Get elastic cross section values
1100
            elastic_xs = ace.xss[ace.jxs[1] + 3*n_grid:ace.jxs[1] + 4*n_grid]
11✔
1101

1102
            # Fix negatives -- known issue for Ti46,49,50 in JEFF 3.2
1103
            if np.any(elastic_xs < 0.0):
11✔
UNCOV
1104
                warn("Negative elastic scattering cross section found for {}. "
×
1105
                     "Setting to zero.".format(ace.name))
UNCOV
1106
                elastic_xs[elastic_xs < 0.0] = 0.0
×
1107

1108
            tabulated_xs = Tabulated1D(grid, elastic_xs)
11✔
1109
            tabulated_xs._threshold_idx = 0
11✔
1110
            rx.xs[strT] = tabulated_xs
11✔
1111

1112
            # No energy distribution for elastic scattering
1113
            neutron = Product('neutron')
11✔
1114
            neutron.distribution.append(UncorrelatedAngleEnergy())
11✔
1115
            rx.products.append(neutron)
11✔
1116

1117
        # ======================================================================
1118
        # ANGLE DISTRIBUTION (FOR UNCORRELATED)
1119

1120
        if i_reaction < ace.nxs[5] + 1:
11✔
1121
            # Check if angular distribution data exist
1122
            loc = int(ace.xss[ace.jxs[8] + i_reaction])
11✔
1123
            if loc < 0:
11✔
1124
                # Angular distribution is given as part of a product
1125
                # angle-energy distribution
1126
                angle_dist = None
11✔
1127
            elif loc == 0:
11✔
1128
                # Angular distribution is isotropic
UNCOV
1129
                energy = [0.0, grid[-1]]
×
UNCOV
1130
                mu = Uniform(-1., 1.)
×
UNCOV
1131
                angle_dist = AngleDistribution(energy, [mu, mu])
×
1132
            else:
1133
                angle_dist = AngleDistribution.from_ace(ace, ace.jxs[9], loc)
11✔
1134

1135
            # Apply angular distribution to each uncorrelated angle-energy
1136
            # distribution
1137
            if angle_dist is not None:
11✔
1138
                for d in neutron.distribution:
11✔
1139
                    d.angle = angle_dist
11✔
1140

1141
        # ======================================================================
1142
        # PHOTON PRODUCTION
1143

1144
        rx.products += _get_photon_products_ace(ace, rx)
11✔
1145

1146
        return rx
11✔
1147

1148
    @classmethod
11✔
1149
    def from_endf(cls, ev, mt):
11✔
1150
        """Generate a reaction from an ENDF evaluation
1151

1152
        Parameters
1153
        ----------
1154
        ev : openmc.data.endf.Evaluation
1155
            ENDF evaluation
1156
        mt : int
1157
            The MT value of the reaction to get data for
1158

1159
        Returns
1160
        -------
1161
        rx : openmc.data.Reaction
1162
            Reaction data
1163

1164
        """
1165
        rx = Reaction(mt)
11✔
1166

1167
        # Integrated cross section
1168
        if (3, mt) in ev.section:
11✔
1169
            file_obj = StringIO(ev.section[3, mt])
11✔
1170
            get_head_record(file_obj)
11✔
1171
            params, rx.xs['0K'] = get_tab1_record(file_obj)
11✔
1172
            rx.q_value = params[1]
11✔
1173

1174
        # Get fission product yields (nu) as well as delayed neutron energy
1175
        # distributions
1176
        if mt in FISSION_MTS:
11✔
1177
            rx.products, rx.derived_products = _get_fission_products_endf(ev)
11✔
1178

1179
        if (6, mt) in ev.section:
11✔
1180
            # Product angle-energy distribution
1181
            for product in _get_products(ev, mt):
11✔
1182
                if mt in FISSION_MTS and product.particle == 'neutron':
11✔
1183
                    rx.products[0].applicability = product.applicability
11✔
1184
                    rx.products[0].distribution = product.distribution
11✔
1185
                else:
1186
                    rx.products.append(product)
11✔
1187

1188
        elif (4, mt) in ev.section or (5, mt) in ev.section:
11✔
1189
            # Uncorrelated angle-energy distribution
1190
            neutron = Product('neutron')
11✔
1191

1192
            # Note that the energy distribution for MT=455 is read in
1193
            # _get_fission_products_endf rather than here
1194
            if (5, mt) in ev.section:
11✔
1195
                file_obj = StringIO(ev.section[5, mt])
11✔
1196
                items = get_head_record(file_obj)
11✔
1197
                nk = items[4]
11✔
1198
                for i in range(nk):
11✔
1199
                    params, applicability = get_tab1_record(file_obj)
11✔
1200
                    dist = UncorrelatedAngleEnergy()
11✔
1201
                    dist.energy = EnergyDistribution.from_endf(file_obj, params)
11✔
1202

1203
                    neutron.applicability.append(applicability)
11✔
1204
                    neutron.distribution.append(dist)
11✔
1205
            elif mt == 2:
11✔
1206
                # Elastic scattering -- no energy distribution is given since it
1207
                # can be calulcated analytically
1208
                dist = UncorrelatedAngleEnergy()
11✔
1209
                neutron.distribution.append(dist)
11✔
1210
            elif mt >= 51 and mt < 91:
11✔
1211
                # Level inelastic scattering -- no energy distribution is given
1212
                # since it can be calculated analytically. Here we determine the
1213
                # necessary parameters to create a LevelInelastic object
1214
                dist = UncorrelatedAngleEnergy()
11✔
1215

1216
                A = ev.target['mass']
11✔
1217
                threshold = (A + 1.)/A*abs(rx.q_value)
11✔
1218
                mass_ratio = (A/(A + 1.))**2
11✔
1219
                dist.energy = LevelInelastic(threshold, mass_ratio)
11✔
1220

1221
                neutron.distribution.append(dist)
11✔
1222

1223
            if (4, mt) in ev.section:
11✔
1224
                for dist in neutron.distribution:
11✔
1225
                    dist.angle = AngleDistribution.from_endf(ev, mt)
11✔
1226

1227
            if mt in FISSION_MTS and (5, mt) in ev.section:
11✔
1228
                # For fission reactions,
1229
                rx.products[0].applicability = neutron.applicability
11✔
1230
                rx.products[0].distribution = neutron.distribution
11✔
1231
            else:
1232
                rx.products.append(neutron)
11✔
1233

1234
        if (8, mt) in ev.section:
11✔
1235
            rx.products += _get_activation_products(ev, rx)
11✔
1236

1237
        if (12, mt) in ev.section or (13, mt) in ev.section:
11✔
1238
            rx.products += _get_photon_products_endf(ev, rx)
11✔
1239

1240
        return rx
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