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

openmc-dev / openmc / 14260484407

04 Apr 2025 07:41AM UTC coverage: 84.589% (-0.3%) from 84.851%
14260484407

Pull #3279

github

web-flow
Merge 33e03305e into 07f533461
Pull Request #3279: Hexagonal mesh model

2 of 332 new or added lines in 3 files covered. (0.6%)

2798 existing lines in 84 files now uncovered.

51799 of 61236 relevant lines covered (84.59%)

38650400.15 hits per line

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

80.45
/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 .product import Product
11✔
25
from .uncorrelated import UncorrelatedAngleEnergy
11✔
26

27

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

67
REACTION_MT = {name: mt for mt, name in REACTION_NAME.items()}
11✔
68
REACTION_MT['fission'] = 18
11✔
69

70
FISSION_MTS = (18, 19, 20, 21, 38)
11✔
71

72

73
def _get_products(ev, mt):
11✔
74
    """Generate products from MF=6 in an ENDF evaluation
75

76
    Parameters
77
    ----------
78
    ev : openmc.data.endf.Evaluation
79
        ENDF evaluation to read from
80
    mt : int
81
        The MT value of the reaction to get products for
82

83
    Raises
84
    ------
85
    IOError
86
        When the Kalbach-Mann systematics is used, but the product
87
        is not defined in the 'center-of-mass' system. The breakup logic
88
        is not implemented which can lead to this error being raised while
89
        the definition of the product is correct.
90

91
    Returns
92
    -------
93
    products : list of openmc.data.Product
94
        Products of the reaction
95

96
    """
97
    file_obj = StringIO(ev.section[6, mt])
11✔
98

99
    # Read HEAD record
100
    items = get_head_record(file_obj)
11✔
101
    reference_frame = {1: 'laboratory', 2: 'center-of-mass',
11✔
102
                       3: 'light-heavy', 4: 'breakup'}[items[3]]
103
    n_products = items[4]
11✔
104

105
    products = []
11✔
106
    for i in range(n_products):
11✔
107
        # Get yield for this product
108
        params, yield_ = get_tab1_record(file_obj)
11✔
109

110
        za = int(params[0])
11✔
111
        awr = params[1]
11✔
112
        law = params[3]
11✔
113

114
        if za == 0:
11✔
115
            p = Product('photon')
11✔
116
        elif za == 1:
11✔
117
            p = Product('neutron')
11✔
118
        elif za == 1000:
11✔
119
            p = Product('electron')
×
120
        else:
121
            Z, A = divmod(za, 1000)
11✔
122
            p = Product(f'{ATOMIC_SYMBOL[Z]}{A}')
11✔
123

124
        p.yield_ = yield_
11✔
125

126
        """
11✔
127
        # Set reference frame
128
        if reference_frame == 'laboratory':
129
            p.center_of_mass = False
130
        elif reference_frame == 'center-of-mass':
131
            p.center_of_mass = True
132
        elif reference_frame == 'light-heavy':
133
            p.center_of_mass = (awr <= 4.0)
134
        """
135

136
        if law == 0:
11✔
137
            # No distribution given
138
            pass
11✔
139
        if law == 1:
11✔
140
            # Continuum energy-angle distribution
141

142
            # Peak ahead to determine type of distribution
143
            position = file_obj.tell()
11✔
144
            params = get_cont_record(file_obj)
11✔
145
            file_obj.seek(position)
11✔
146

147
            lang = params[2]
11✔
148
            if lang == 1:
11✔
149
                p.distribution = [CorrelatedAngleEnergy.from_endf(file_obj)]
11✔
150
            elif lang == 2:
11✔
151
                # Products need to be described in the center-of-mass system
152
                product_center_of_mass = False
11✔
153
                if reference_frame == 'center-of-mass':
11✔
154
                    product_center_of_mass = True
11✔
155
                elif reference_frame == 'light-heavy':
11✔
156
                    product_center_of_mass = (awr <= 4.0)
11✔
157
                # TODO: 'breakup' logic not implemented
158

159
                if product_center_of_mass is False:
11✔
160
                    raise IOError(
×
161
                        "Kalbach-Mann representation must be defined in the "
162
                        "'center-of-mass' system"
163
                    )
164

165
                zat = ev.target["atomic_number"] * 1000 + ev.target["mass_number"]
11✔
166
                projectile_mass = ev.projectile["mass"]
11✔
167
                p.distribution = [KalbachMann.from_endf(file_obj,
11✔
168
                                                        za,
169
                                                        zat,
170
                                                        projectile_mass)]
171

172
        elif law == 2:
11✔
173
            # Discrete two-body scattering
174
            params, tab2 = get_tab2_record(file_obj)
11✔
175
            ne = params[5]
11✔
176
            energy = np.zeros(ne)
11✔
177
            mu = []
11✔
178
            for i in range(ne):
11✔
179
                items, values = get_list_record(file_obj)
11✔
180
                energy[i] = items[1]
11✔
181
                lang = items[2]
11✔
182
                if lang == 0:
11✔
183
                    mu.append(Legendre(values))
11✔
184
                elif lang == 12:
11✔
185
                    mu.append(Tabular(values[::2], values[1::2]))
×
186
                elif lang == 14:
11✔
187
                    mu.append(Tabular(values[::2], values[1::2],
11✔
188
                                      'log-linear'))
189

190
            angle_dist = AngleDistribution(energy, mu)
11✔
191
            dist = UncorrelatedAngleEnergy(angle_dist)
11✔
192
            p.distribution = [dist]
11✔
193
            # TODO: Add level-inelastic info?
194

195
        elif law == 3:
11✔
196
            # Isotropic discrete emission
197
            p.distribution = [UncorrelatedAngleEnergy()]
11✔
198
            # TODO: Add level-inelastic info?
199

200
        elif law == 4:
11✔
201
            # Discrete two-body recoil
202
            pass
11✔
203

204
        elif law == 5:
11✔
205
            # Charged particle elastic scattering
206
            pass
×
207

208
        elif law == 6:
11✔
209
            # N-body phase-space distribution
210
            p.distribution = [NBodyPhaseSpace.from_endf(file_obj)]
×
211

212
        elif law == 7:
11✔
213
            # Laboratory energy-angle distribution
214
            p.distribution = [LaboratoryAngleEnergy.from_endf(file_obj)]
11✔
215

216
        products.append(p)
11✔
217

218
    return products
11✔
219

220

221
def _get_fission_products_ace(ace):
11✔
222
    """Generate fission products from an ACE table
223

224
    Parameters
225
    ----------
226
    ace : openmc.data.ace.Table
227
        ACE table to read from
228

229
    Returns
230
    -------
231
    products : list of openmc.data.Product
232
        Prompt and delayed fission neutrons
233
    derived_products : list of openmc.data.Product
234
        "Total" fission neutron
235

236
    """
237
    # No NU block
238
    if ace.jxs[2] == 0:
11✔
239
        return None, None
×
240

241
    products = []
11✔
242
    derived_products = []
11✔
243

244
    # Either prompt nu or total nu is given
245
    if ace.xss[ace.jxs[2]] > 0:
11✔
246
        whichnu = 'prompt' if ace.jxs[24] > 0 else 'total'
×
247

248
        neutron = Product('neutron')
×
249
        neutron.emission_mode = whichnu
×
250

251
        idx = ace.jxs[2]
×
252
        LNU = int(ace.xss[idx])
×
253
        if LNU == 1:
×
254
            # Polynomial function form of nu
255
            NC = int(ace.xss[idx+1])
×
256
            coefficients = ace.xss[idx+2 : idx+2+NC].copy()
×
257
            for i in range(coefficients.size):
×
258
                coefficients[i] *= EV_PER_MEV**(-i)
×
259
            neutron.yield_ = Polynomial(coefficients)
×
260
        elif LNU == 2:
×
261
            # Tabular data form of nu
262
            neutron.yield_ = Tabulated1D.from_ace(ace, idx + 1)
×
263

264
        products.append(neutron)
×
265

266
    # Both prompt nu and total nu
267
    elif ace.xss[ace.jxs[2]] < 0:
11✔
268
        # Read prompt neutron yield
269
        prompt_neutron = Product('neutron')
11✔
270
        prompt_neutron.emission_mode = 'prompt'
11✔
271

272
        idx = ace.jxs[2] + 1
11✔
273
        LNU = int(ace.xss[idx])
11✔
274
        if LNU == 1:
11✔
275
            # Polynomial function form of nu
276
            NC = int(ace.xss[idx+1])
×
277
            coefficients = ace.xss[idx+2 : idx+2+NC].copy()
×
278
            for i in range(coefficients.size):
×
279
                coefficients[i] *= EV_PER_MEV**(-i)
×
280
            prompt_neutron.yield_ = Polynomial(coefficients)
×
281
        elif LNU == 2:
11✔
282
            # Tabular data form of nu
283
            prompt_neutron.yield_ = Tabulated1D.from_ace(ace, idx + 1)
11✔
284

285
        # Read total neutron yield
286
        total_neutron = Product('neutron')
11✔
287
        total_neutron.emission_mode = 'total'
11✔
288

289
        idx = ace.jxs[2] + int(abs(ace.xss[ace.jxs[2]])) + 1
11✔
290
        LNU = int(ace.xss[idx])
11✔
291

292
        if LNU == 1:
11✔
293
            # Polynomial function form of nu
294
            NC = int(ace.xss[idx+1])
×
295
            coefficients = ace.xss[idx+2 : idx+2+NC].copy()
×
296
            for i in range(coefficients.size):
×
297
                coefficients[i] *= EV_PER_MEV**(-i)
×
298
            total_neutron.yield_ = Polynomial(coefficients)
×
299
        elif LNU == 2:
11✔
300
            # Tabular data form of nu
301
            total_neutron.yield_ = Tabulated1D.from_ace(ace, idx + 1)
11✔
302

303
        products.append(prompt_neutron)
11✔
304
        derived_products.append(total_neutron)
11✔
305

306
    # Check for delayed nu data
307
    if ace.jxs[24] > 0:
11✔
308
        yield_delayed = Tabulated1D.from_ace(ace, ace.jxs[24] + 1)
×
309

310
        # Delayed neutron precursor distribution
311
        idx = ace.jxs[25]
×
312
        n_group = ace.nxs[8]
×
313
        total_group_probability = 0.
×
314
        for group in range(n_group):
×
315
            delayed_neutron = Product('neutron')
×
316
            delayed_neutron.emission_mode = 'delayed'
×
317

318
            # Convert units of inverse shakes to inverse seconds
319
            delayed_neutron.decay_rate = ace.xss[idx] * 1.e8
×
320

321
            group_probability = Tabulated1D.from_ace(ace, idx + 1)
×
322
            if np.all(group_probability.y == group_probability.y[0]):
×
323
                delayed_neutron.yield_ = deepcopy(yield_delayed)
×
324
                delayed_neutron.yield_.y *= group_probability.y[0]
×
325
                total_group_probability += group_probability.y[0]
×
326
            else:
327
                # Get union energy grid and ensure energies are within
328
                # interpolable range of both functions
329
                max_energy = min(yield_delayed.x[-1], group_probability.x[-1])
×
330
                energy = np.union1d(yield_delayed.x, group_probability.x)
×
331
                energy = energy[energy <= max_energy]
×
332

333
                # Calculate group yield
334
                group_yield = yield_delayed(energy) * group_probability(energy)
×
335
                delayed_neutron.yield_ = Tabulated1D(energy, group_yield)
×
336

337
            # Advance position
338
            nr = int(ace.xss[idx + 1])
×
339
            ne = int(ace.xss[idx + 2 + 2*nr])
×
340
            idx += 3 + 2*nr + 2*ne
×
341

342
            # Energy distribution for delayed fission neutrons
343
            location_start = int(ace.xss[ace.jxs[26] + group])
×
344
            delayed_neutron.distribution.append(
×
345
                AngleEnergy.from_ace(ace, ace.jxs[27], location_start))
346

347
            products.append(delayed_neutron)
×
348

349
        # Renormalize delayed neutron yields to reflect fact that in ACE
350
        # file, the sum of the group probabilities is not exactly one
351
        for product in products[1:]:
×
352
            if total_group_probability > 0.:
×
353
                product.yield_.y /= total_group_probability
×
354

355
    return products, derived_products
11✔
356

357

358
def _get_fission_products_endf(ev):
11✔
359
    """Generate fission products from an ENDF evaluation
360

361
    Parameters
362
    ----------
363
    ev : openmc.data.endf.Evaluation
364

365
    Returns
366
    -------
367
    products : list of openmc.data.Product
368
        Prompt and delayed fission neutrons
369
    derived_products : list of openmc.data.Product
370
        "Total" fission neutron
371

372
    """
373
    products = []
11✔
374
    derived_products = []
11✔
375

376
    if (1, 456) in ev.section:
11✔
377
        prompt_neutron = Product('neutron')
11✔
378
        prompt_neutron.emission_mode = 'prompt'
11✔
379

380
        # Prompt nu values
381
        file_obj = StringIO(ev.section[1, 456])
11✔
382
        lnu = get_head_record(file_obj)[3]
11✔
383
        if lnu == 1:
11✔
384
            # Polynomial representation
385
            items, coefficients = get_list_record(file_obj)
×
386
            prompt_neutron.yield_ = Polynomial(coefficients)
×
387
        elif lnu == 2:
11✔
388
            # Tabulated representation
389
            params, prompt_neutron.yield_ = get_tab1_record(file_obj)
11✔
390

391
        products.append(prompt_neutron)
11✔
392

393
    if (1, 452) in ev.section:
11✔
394
        total_neutron = Product('neutron')
11✔
395
        total_neutron.emission_mode = 'total'
11✔
396

397
        # Total nu values
398
        file_obj = StringIO(ev.section[1, 452])
11✔
399
        lnu = get_head_record(file_obj)[3]
11✔
400
        if lnu == 1:
11✔
401
            # Polynomial representation
402
            items, coefficients = get_list_record(file_obj)
×
403
            total_neutron.yield_ = Polynomial(coefficients)
×
404
        elif lnu == 2:
11✔
405
            # Tabulated representation
406
            params, total_neutron.yield_ = get_tab1_record(file_obj)
11✔
407

408
        if (1, 456) in ev.section:
11✔
409
            derived_products.append(total_neutron)
11✔
410
        else:
411
            products.append(total_neutron)
×
412

413
    if (1, 455) in ev.section:
11✔
414
        file_obj = StringIO(ev.section[1, 455])
11✔
415

416
        # Determine representation of delayed nu data
417
        items = get_head_record(file_obj)
11✔
418
        ldg = items[2]
11✔
419
        lnu = items[3]
11✔
420

421
        if ldg == 0:
11✔
422
            # Delayed-group constants energy independent
423
            items, decay_constants = get_list_record(file_obj)
11✔
424
            for constant in decay_constants:
11✔
425
                delayed_neutron = Product('neutron')
11✔
426
                delayed_neutron.emission_mode = 'delayed'
11✔
427
                delayed_neutron.decay_rate = constant
11✔
428
                products.append(delayed_neutron)
11✔
429
        elif ldg == 1:
×
430
            # Delayed-group constants energy dependent
431
            raise NotImplementedError('Delayed neutron with energy-dependent '
×
432
                                      'group constants.')
433

434
        # In MF=1, MT=455, the delayed-group abundances are actually not
435
        # specified if the group constants are energy-independent. In this case,
436
        # the abundances must be inferred from MF=5, MT=455 where multiple
437
        # energy distributions are given.
438
        if lnu == 1:
11✔
439
            # Nu represented as polynomial
440
            items, coefficients = get_list_record(file_obj)
×
441
            yield_ = Polynomial(coefficients)
×
442
            for neutron in products[-6:]:
×
443
                neutron.yield_ = deepcopy(yield_)
×
444
        elif lnu == 2:
11✔
445
            # Nu represented by tabulation
446
            params, yield_ = get_tab1_record(file_obj)
11✔
447
            for neutron in products[-6:]:
11✔
448
                neutron.yield_ = deepcopy(yield_)
11✔
449

450
        if (5, 455) in ev.section:
11✔
451
            file_obj = StringIO(ev.section[5, 455])
11✔
452
            items = get_head_record(file_obj)
11✔
453
            nk = items[4]
11✔
454
            if nk > 1 and len(decay_constants) == 1:
11✔
455
                # If only one precursor group is listed in MF=1, MT=455, use the
456
                # energy spectra from MF=5 to split them into different groups
457
                for _ in range(nk - 1):
×
458
                    products.append(deepcopy(products[1]))
×
459
            elif nk != len(decay_constants):
11✔
460
                raise ValueError(
×
461
                    'Number of delayed neutron fission spectra ({}) does not '
462
                    'match number of delayed neutron precursors ({}).'.format(
463
                        nk, len(decay_constants)))
464
            for i in range(nk):
11✔
465
                params, applicability = get_tab1_record(file_obj)
11✔
466
                dist = UncorrelatedAngleEnergy()
11✔
467
                dist.energy = EnergyDistribution.from_endf(file_obj, params)
11✔
468

469
                delayed_neutron = products[1 + i]
11✔
470
                yield_ = delayed_neutron.yield_
11✔
471

472
                # Here we handle the fact that the delayed neutron yield is the
473
                # product of the total delayed neutron yield and the
474
                # "applicability" of the energy distribution law in file 5.
475
                if isinstance(yield_, Tabulated1D):
11✔
476
                    if np.all(applicability.y == applicability.y[0]):
11✔
477
                        yield_.y *= applicability.y[0]
11✔
478
                    else:
479
                        # Get union energy grid and ensure energies are within
480
                        # interpolable range of both functions
481
                        max_energy = min(yield_.x[-1], applicability.x[-1])
×
482
                        energy = np.union1d(yield_.x, applicability.x)
×
483
                        energy = energy[energy <= max_energy]
×
484

485
                        # Calculate group yield
486
                        group_yield = yield_(energy) * applicability(energy)
×
487
                        delayed_neutron.yield_ = Tabulated1D(energy, group_yield)
×
488
                elif isinstance(yield_, Polynomial):
×
489
                    if len(yield_) == 1:
×
490
                        delayed_neutron.yield_ = deepcopy(applicability)
×
491
                        delayed_neutron.yield_.y *= yield_.coef[0]
×
492
                    else:
493
                        if np.all(applicability.y == applicability.y[0]):
×
494
                            yield_.coef[0] *= applicability.y[0]
×
495
                        else:
496
                            raise NotImplementedError(
×
497
                                'Total delayed neutron yield and delayed group '
498
                                'probability are both energy-dependent.')
499

500
                delayed_neutron.distribution.append(dist)
11✔
501

502
    return products, derived_products
11✔
503

504

505
def _get_activation_products(ev, rx):
11✔
506
    """Generate activation products from an ENDF evaluation
507

508
    Parameters
509
    ----------
510
    ev : openmc.data.endf.Evaluation
511
        The ENDF evaluation
512
    rx : openmc.data.Reaction
513
        Reaction which generates activation products
514

515
    Returns
516
    -------
517
    products : list of openmc.data.Product
518
        Activation products
519

520
    """
521
    file_obj = StringIO(ev.section[8, rx.mt])
11✔
522

523
    # Determine total number of states and whether decay chain is given in a
524
    # decay sublibrary
525
    items = get_head_record(file_obj)
11✔
526
    n_states = items[4]
11✔
527
    decay_sublib = (items[5] == 1)
11✔
528

529
    # Determine if file 9/10 are present
530
    present = {9: False, 10: False}
11✔
531
    for _ in range(n_states):
11✔
532
        if decay_sublib:
11✔
533
            items = get_cont_record(file_obj)
11✔
534
        else:
535
            items, values = get_list_record(file_obj)
11✔
536
        lmf = items[2]
11✔
537
        if lmf == 9:
11✔
538
            present[9] = True
11✔
539
        elif lmf == 10:
11✔
540
            present[10] = True
×
541

542
    products = []
11✔
543

544
    for mf in (9, 10):
11✔
545
        if not present[mf]:
11✔
546
            continue
11✔
547

548
        file_obj = StringIO(ev.section[mf, rx.mt])
11✔
549
        items = get_head_record(file_obj)
11✔
550
        n_states = items[4]
11✔
551
        for i in range(n_states):
11✔
552
            # Determine what the product is
553
            items, xs = get_tab1_record(file_obj)
11✔
554
            Z, A = divmod(items[2], 1000)
11✔
555
            excited_state = items[3]
11✔
556

557
            # Get GNDS name for product
558
            symbol = ATOMIC_SYMBOL[Z]
11✔
559
            if excited_state > 0:
11✔
560
                name = f'{symbol}{A}_e{excited_state}'
11✔
561
            else:
562
                name = f'{symbol}{A}'
11✔
563

564
            p = Product(name)
11✔
565
            if mf == 9:
11✔
566
                p.yield_ = xs
11✔
567
            else:
568
                # Re-interpolate production cross section and neutron cross
569
                # section to union energy grid
570
                energy = np.union1d(xs.x, rx.xs['0K'].x)
×
571
                prod_xs = xs(energy)
×
572
                neutron_xs = rx.xs['0K'](energy)
×
573
                idx = np.where(neutron_xs > 0)
×
574

575
                # Calculate yield as ratio
576
                yield_ = np.zeros_like(energy)
×
577
                yield_[idx] = prod_xs[idx] / neutron_xs[idx]
×
578
                p.yield_ = Tabulated1D(energy, yield_)
×
579

580
            # Check if product already exists from MF=6 and if it does, just
581
            # overwrite the existing yield.
582
            for product in rx.products:
11✔
583
                if name == product.particle:
11✔
584
                    product.yield_ = p.yield_
×
585
                    break
×
586
            else:
587
                products.append(p)
11✔
588

589
    return products
11✔
590

591

592
def _get_photon_products_ace(ace, rx):
11✔
593
    """Generate photon products from an ACE table
594

595
    Parameters
596
    ----------
597
    ace : openmc.data.ace.Table
598
        ACE table to read from
599
    rx : openmc.data.Reaction
600
        Reaction that generates photons
601

602
    Returns
603
    -------
604
    photons : list of openmc.Products
605
        Photons produced from reaction with given MT
606

607
    """
608
    n_photon_reactions = ace.nxs[6]
11✔
609
    photon_mts = ace.xss[ace.jxs[13]:ace.jxs[13] +
11✔
610
                         n_photon_reactions].astype(int)
611

612
    photons = []
11✔
613
    for i in range(n_photon_reactions):
11✔
614
        # Determine corresponding reaction
615
        neutron_mt = photon_mts[i] // 1000
11✔
616

617
        if neutron_mt != rx.mt:
11✔
618
            continue
11✔
619

620
        # Create photon product and assign to reactions
621
        photon = Product('photon')
11✔
622

623
        # ==================================================================
624
        # Photon yield / production cross section
625

626
        loca = int(ace.xss[ace.jxs[14] + i])
11✔
627
        idx = ace.jxs[15] + loca - 1
11✔
628
        mftype = int(ace.xss[idx])
11✔
629
        idx += 1
11✔
630

631
        if mftype in (12, 16):
11✔
632
            # Yield data taken from ENDF File 12 or 6
633
            mtmult = int(ace.xss[idx])
11✔
634
            assert mtmult == neutron_mt
11✔
635

636
            # Read photon yield as function of energy
637
            photon.yield_ = Tabulated1D.from_ace(ace, idx + 1)
11✔
638

639
        elif mftype == 13:
×
640
            # Cross section data from ENDF File 13
641

642
            # Energy grid index at which data starts
643
            threshold_idx = int(ace.xss[idx]) - 1
×
644
            n_energy = int(ace.xss[idx + 1])
×
645
            energy = ace.xss[ace.jxs[1] + threshold_idx:
×
646
                             ace.jxs[1] + threshold_idx + n_energy]*EV_PER_MEV
647

648
            # Get photon production cross section
649
            photon_prod_xs = ace.xss[idx + 2:idx + 2 + n_energy]
×
650
            neutron_xs = list(rx.xs.values())[0](energy)
×
651
            idx = np.where(neutron_xs > 0.)
×
652

653
            # Calculate photon yield
654
            yield_ = np.zeros_like(photon_prod_xs)
×
655
            yield_[idx] = photon_prod_xs[idx] / neutron_xs[idx]
×
656
            photon.yield_ = Tabulated1D(energy, yield_)
×
657

658
        else:
659
            raise ValueError(f"MFTYPE must be 12, 13, 16. Got {mftype}")
×
660

661
        # ==================================================================
662
        # Photon energy distribution
663

664
        location_start = int(ace.xss[ace.jxs[18] + i])
11✔
665
        distribution = AngleEnergy.from_ace(ace, ace.jxs[19], location_start)
11✔
666
        assert isinstance(distribution, UncorrelatedAngleEnergy)
11✔
667

668
        # ==================================================================
669
        # Photon angular distribution
670
        loc = int(ace.xss[ace.jxs[16] + i])
11✔
671

672
        if loc == 0:
11✔
673
            # No angular distribution data are given for this reaction,
674
            # isotropic scattering is asssumed in LAB
675
            energy = np.array([photon.yield_.x[0], photon.yield_.x[-1]])
11✔
676
            mu_isotropic = Uniform(-1., 1.)
11✔
677
            distribution.angle = AngleDistribution(
11✔
678
                energy, [mu_isotropic, mu_isotropic])
679
        else:
680
            distribution.angle = AngleDistribution.from_ace(ace, ace.jxs[17], loc)
×
681

682
        # Add to list of distributions
683
        photon.distribution.append(distribution)
11✔
684
        photons.append(photon)
11✔
685

686
    return photons
11✔
687

688

689
def _get_photon_products_endf(ev, rx):
11✔
690
    """Generate photon products from an ENDF evaluation
691

692
    Parameters
693
    ----------
694
    ev : openmc.data.endf.Evaluation
695
        ENDF evaluation to read from
696
    rx : openmc.data.Reaction
697
        Reaction that generates photons
698

699
    Returns
700
    -------
701
    products : list of openmc.Products
702
        Photons produced from reaction with given MT
703

704
    """
705
    products = []
11✔
706

707
    if (12, rx.mt) in ev.section:
11✔
708
        file_obj = StringIO(ev.section[12, rx.mt])
11✔
709

710
        items = get_head_record(file_obj)
11✔
711
        option = items[2]
11✔
712

713
        if option == 1:
11✔
714
            # Multiplicities given
715
            n_discrete_photon = items[4]
11✔
716
            if n_discrete_photon > 1:
11✔
717
                items, total_yield = get_tab1_record(file_obj)
11✔
718
            for k in range(n_discrete_photon):
11✔
719
                photon = Product('photon')
11✔
720

721
                # Get photon yield
722
                items, photon.yield_ = get_tab1_record(file_obj)
11✔
723

724
                # Get photon energy distribution
725
                law = items[3]
11✔
726
                dist = UncorrelatedAngleEnergy()
11✔
727
                if law == 1:
11✔
728
                    # TODO: Get file 15 distribution
729
                    pass
11✔
730
                elif law == 2:
11✔
731
                    energy = items[0]
11✔
732
                    primary_flag = items[2]
11✔
733
                    dist.energy = DiscretePhoton(primary_flag, energy,
11✔
734
                                                 ev.target['mass'])
735

736
                photon.distribution.append(dist)
11✔
737
                products.append(photon)
11✔
738

739
        elif option == 2:
11✔
740
            # Transition probability arrays given
741
            ppyield = {}
11✔
742
            ppyield['type'] = 'transition'
11✔
743
            ppyield['transition'] = transition = {}
11✔
744

745
            # Determine whether simple (LG=1) or complex (LG=2) transitions
746
            lg = items[3]
11✔
747

748
            # Get transition data
749
            items, values = get_list_record(file_obj)
11✔
750
            transition['energy_start'] = items[0]
11✔
751
            transition['energies'] = np.array(values[::lg + 1])
11✔
752
            transition['direct_probability'] = np.array(values[1::lg + 1])
11✔
753
            if lg == 2:
11✔
754
                # Complex case
755
                transition['conditional_probability'] = np.array(
11✔
756
                    values[2::lg + 1])
757

758
    elif (13, rx.mt) in ev.section:
11✔
759
        file_obj = StringIO(ev.section[13, rx.mt])
11✔
760

761
        # Determine option
762
        items = get_head_record(file_obj)
11✔
763
        n_discrete_photon = items[4]
11✔
764
        if n_discrete_photon > 1:
11✔
765
            items, total_xs = get_tab1_record(file_obj)
11✔
766
        for k in range(n_discrete_photon):
11✔
767
            photon = Product('photon')
11✔
768
            items, xs = get_tab1_record(file_obj)
11✔
769

770
            # Re-interpolate photon production cross section and neutron cross
771
            # section to union energy grid
772
            energy = np.union1d(xs.x, rx.xs['0K'].x)
11✔
773
            photon_prod_xs = xs(energy)
11✔
774
            neutron_xs = rx.xs['0K'](energy)
11✔
775
            idx = np.where(neutron_xs > 0)
11✔
776

777
            # Calculate yield as ratio
778
            yield_ = np.zeros_like(energy)
11✔
779
            yield_[idx] = photon_prod_xs[idx] / neutron_xs[idx]
11✔
780
            photon.yield_ = Tabulated1D(energy, yield_)
11✔
781

782
            # Get photon energy distribution
783
            law = items[3]
11✔
784
            dist = UncorrelatedAngleEnergy()
11✔
785
            if law == 1:
11✔
786
                # TODO: Get file 15 distribution
787
                pass
11✔
788
            elif law == 2:
11✔
789
                energy = items[1]
11✔
790
                primary_flag = items[2]
11✔
791
                dist.energy = DiscretePhoton(primary_flag, energy,
11✔
792
                                             ev.target['mass'])
793

794
            photon.distribution.append(dist)
11✔
795
            products.append(photon)
11✔
796

797
    return products
11✔
798

799

800
class Reaction(EqualityMixin):
11✔
801
    """A nuclear reaction
802

803
    A Reaction object represents a single reaction channel for a nuclide with
804
    an associated cross section and, if present, a secondary angle and energy
805
    distribution.
806

807
    Parameters
808
    ----------
809
    mt : int
810
        The ENDF MT number for this reaction.
811

812
    Attributes
813
    ----------
814
    center_of_mass : bool
815
        Indicates whether scattering kinematics should be performed in the
816
        center-of-mass or laboratory reference frame.
817
        grid above the threshold value in barns.
818
    redundant : bool
819
        Indicates whether or not this is a redundant reaction
820
    mt : int
821
        The ENDF MT number for this reaction.
822
    q_value : float
823
        The Q-value of this reaction in eV.
824
    xs : dict of str to openmc.data.Function1D
825
        Microscopic cross section for this reaction as a function of incident
826
        energy; these cross sections are provided in a dictionary where the key
827
        is the temperature of the cross section set.
828
    products : Iterable of openmc.data.Product
829
        Reaction products
830
    derived_products : Iterable of openmc.data.Product
831
        Derived reaction products. Used for 'total' fission neutron data when
832
        prompt/delayed data also exists.
833

834
    """
835

836
    def __init__(self, mt):
11✔
837
        self._center_of_mass = True
11✔
838
        self._redundant = False
11✔
839
        self._q_value = 0.
11✔
840
        self._xs = {}
11✔
841
        self._products = []
11✔
842
        self._derived_products = []
11✔
843

844
        self.mt = mt
11✔
845

846
    def __repr__(self):
11✔
847
        if self.mt in REACTION_NAME:
×
848
            return f"<Reaction: MT={self.mt} {REACTION_NAME[self.mt]}>"
×
849
        else:
850
            return f"<Reaction: MT={self.mt}>"
×
851

852
    @property
11✔
853
    def center_of_mass(self):
11✔
854
        return self._center_of_mass
11✔
855

856
    @center_of_mass.setter
11✔
857
    def center_of_mass(self, center_of_mass):
11✔
858
        cv.check_type('center of mass', center_of_mass, (bool, np.bool_))
11✔
859
        self._center_of_mass = center_of_mass
11✔
860

861
    @property
11✔
862
    def redundant(self):
11✔
863
        return self._redundant
11✔
864

865
    @redundant.setter
11✔
866
    def redundant(self, redundant):
11✔
867
        cv.check_type('redundant', redundant, (bool, np.bool_))
11✔
868
        self._redundant = redundant
11✔
869

870
    @property
11✔
871
    def q_value(self):
11✔
872
        return self._q_value
11✔
873

874
    @q_value.setter
11✔
875
    def q_value(self, q_value):
11✔
876
        cv.check_type('Q value', q_value, Real)
11✔
877
        self._q_value = q_value
11✔
878

879
    @property
11✔
880
    def products(self):
11✔
881
        return self._products
11✔
882

883
    @products.setter
11✔
884
    def products(self, products):
11✔
885
        cv.check_type('reaction products', products, Iterable, Product)
11✔
886
        self._products = products
11✔
887

888
    @property
11✔
889
    def derived_products(self):
11✔
890
        return self._derived_products
11✔
891

892
    @derived_products.setter
11✔
893
    def derived_products(self, derived_products):
11✔
894
        cv.check_type('reaction derived products', derived_products,
11✔
895
                      Iterable, Product)
896
        self._derived_products = derived_products
11✔
897

898
    @property
11✔
899
    def xs(self):
11✔
900
        return self._xs
11✔
901

902
    @xs.setter
11✔
903
    def xs(self, xs):
11✔
904
        cv.check_type('reaction cross section dictionary', xs, MutableMapping)
×
905
        for key, value in xs.items():
×
906
            cv.check_type('reaction cross section temperature', key, str)
×
907
            cv.check_type('reaction cross section', value, Callable)
×
908
        self._xs = xs
×
909

910
    def to_hdf5(self, group):
11✔
911
        """Write reaction to an HDF5 group
912

913
        Parameters
914
        ----------
915
        group : h5py.Group
916
            HDF5 group to write to
917

918
        """
919

920
        group.attrs['mt'] = self.mt
11✔
921
        if self.mt in REACTION_NAME:
11✔
922
            group.attrs['label'] = np.bytes_(REACTION_NAME[self.mt])
11✔
923
        else:
UNCOV
924
            group.attrs['label'] = np.bytes_(self.mt)
×
925
        group.attrs['Q_value'] = self.q_value
11✔
926
        group.attrs['center_of_mass'] = 1 if self.center_of_mass else 0
11✔
927
        group.attrs['redundant'] = 1 if self.redundant else 0
11✔
928
        for T in self.xs:
11✔
929
            Tgroup = group.create_group(T)
11✔
930
            if self.xs[T] is not None:
11✔
931
                dset = Tgroup.create_dataset('xs', data=self.xs[T].y)
11✔
932
                threshold_idx = getattr(self.xs[T], '_threshold_idx', 0)
11✔
933
                dset.attrs['threshold_idx'] = threshold_idx
11✔
934
        for i, p in enumerate(self.products):
11✔
935
            pgroup = group.create_group(f'product_{i}')
11✔
936
            p.to_hdf5(pgroup)
11✔
937

938
    @classmethod
11✔
939
    def from_hdf5(cls, group, energy):
11✔
940
        """Generate reaction from an HDF5 group
941

942
        Parameters
943
        ----------
944
        group : h5py.Group
945
            HDF5 group to read from
946
        energy : dict
947
            Dictionary whose keys are temperatures (e.g., '300K') and values are
948
            arrays of energies at which cross sections are tabulated at.
949

950
        Returns
951
        -------
952
        openmc.data.Reaction
953
            Reaction data
954

955
        """
956

957
        mt = group.attrs['mt']
11✔
958
        rx = cls(mt)
11✔
959
        rx.q_value = group.attrs['Q_value']
11✔
960
        rx.center_of_mass = bool(group.attrs['center_of_mass'])
11✔
961
        rx.redundant = bool(group.attrs.get('redundant', False))
11✔
962

963
        # Read cross section at each temperature
964
        for T, Tgroup in group.items():
11✔
965
            if T.endswith('K'):
11✔
966
                if 'xs' in Tgroup:
11✔
967
                    # Make sure temperature has associated energy grid
968
                    if T not in energy:
11✔
969
                        raise ValueError(
×
970
                            'Could not create reaction cross section for MT={} '
971
                            'at T={} because no corresponding energy grid '
972
                            'exists.'.format(mt, T))
973
                    xs = Tgroup['xs'][()]
11✔
974
                    threshold_idx = Tgroup['xs'].attrs['threshold_idx']
11✔
975
                    tabulated_xs = Tabulated1D(energy[T][threshold_idx:], xs)
11✔
976
                    tabulated_xs._threshold_idx = threshold_idx
11✔
977
                    rx.xs[T] = tabulated_xs
11✔
978

979
        # Determine number of products
980
        n_product = 0
11✔
981
        for name in group:
11✔
982
            if name.startswith('product_'):
11✔
983
                n_product += 1
11✔
984

985
        # Read reaction products
986
        for i in range(n_product):
11✔
987
            pgroup = group[f'product_{i}']
11✔
988
            rx.products.append(Product.from_hdf5(pgroup))
11✔
989

990
        return rx
11✔
991

992
    @classmethod
11✔
993
    def from_ace(cls, ace, i_reaction):
11✔
994
        # Get nuclide energy grid
995
        n_grid = ace.nxs[3]
11✔
996
        grid = ace.xss[ace.jxs[1]:ace.jxs[1] + n_grid]*EV_PER_MEV
11✔
997

998
        # Convert data temperature to a "300.0K" number for indexing
999
        # temperature data
1000
        strT = str(int(round(ace.temperature*EV_PER_MEV / K_BOLTZMANN))) + "K"
11✔
1001

1002
        if i_reaction > 0:
11✔
1003
            mt = int(ace.xss[ace.jxs[3] + i_reaction - 1])
11✔
1004
            rx = cls(mt)
11✔
1005

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

1009
            # ==================================================================
1010
            # CROSS SECTION
1011

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

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

1018
            # Determine number of energies in reaction
1019
            n_energy = int(ace.xss[ace.jxs[7] + loc])
11✔
1020
            energy = grid[threshold_idx:threshold_idx + n_energy]
11✔
1021

1022
            # Read reaction cross section
1023
            xs = ace.xss[ace.jxs[7] + loc + 1:ace.jxs[7] + loc + 1 + n_energy]
11✔
1024

1025
            # For damage energy production, convert to eV
1026
            if mt == 444:
11✔
1027
                xs *= EV_PER_MEV
11✔
1028

1029
            # Fix negatives -- known issue for Y89 in JEFF 3.2
1030
            if np.any(xs < 0.0):
11✔
1031
                warn("Negative cross sections found for MT={} in {}. Setting "
×
1032
                     "to zero.".format(rx.mt, ace.name))
1033
                xs[xs < 0.0] = 0.0
×
1034

1035
            tabulated_xs = Tabulated1D(energy, xs)
11✔
1036
            tabulated_xs._threshold_idx = threshold_idx
11✔
1037
            rx.xs[strT] = tabulated_xs
11✔
1038

1039
            # ==================================================================
1040
            # YIELD AND ANGLE-ENERGY DISTRIBUTION
1041

1042
            # Determine multiplicity
1043
            ty = int(ace.xss[ace.jxs[5] + i_reaction - 1])
11✔
1044
            rx.center_of_mass = (ty < 0)
11✔
1045
            if i_reaction < ace.nxs[5] + 1:
11✔
1046
                if ty != 19:
11✔
1047
                    if abs(ty) > 100:
11✔
1048
                        # Energy-dependent neutron yield
1049
                        idx = ace.jxs[11] + abs(ty) - 101
11✔
1050
                        yield_ = Tabulated1D.from_ace(ace, idx)
11✔
1051
                    else:
1052
                        # 0-order polynomial i.e. a constant
1053
                        yield_ = Polynomial((abs(ty),))
11✔
1054

1055
                    neutron = Product('neutron')
11✔
1056
                    neutron.yield_ = yield_
11✔
1057
                    rx.products.append(neutron)
11✔
1058
                else:
1059
                    assert mt in FISSION_MTS
11✔
1060
                    rx.products, rx.derived_products = _get_fission_products_ace(ace)
11✔
1061

1062
                    for p in rx.products:
11✔
1063
                        if p.emission_mode in ('prompt', 'total'):
11✔
1064
                            neutron = p
11✔
1065
                            break
11✔
1066
                    else:
1067
                        raise Exception("Couldn't find prompt/total fission neutron")
×
1068

1069
                # Determine locator for ith energy distribution
1070
                lnw = int(ace.xss[ace.jxs[10] + i_reaction - 1])
11✔
1071
                while lnw > 0:
11✔
1072
                    # Applicability of this distribution
1073
                    neutron.applicability.append(Tabulated1D.from_ace(
11✔
1074
                        ace, ace.jxs[11] + lnw + 2))
1075

1076
                    # Read energy distribution data
1077
                    neutron.distribution.append(AngleEnergy.from_ace(
11✔
1078
                        ace, ace.jxs[11], lnw, rx))
1079

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

1082
        else:
1083
            # Elastic scattering
1084
            mt = 2
11✔
1085
            rx = cls(mt)
11✔
1086

1087
            # Get elastic cross section values
1088
            elastic_xs = ace.xss[ace.jxs[1] + 3*n_grid:ace.jxs[1] + 4*n_grid]
11✔
1089

1090
            # Fix negatives -- known issue for Ti46,49,50 in JEFF 3.2
1091
            if np.any(elastic_xs < 0.0):
11✔
1092
                warn("Negative elastic scattering cross section found for {}. "
×
1093
                     "Setting to zero.".format(ace.name))
1094
                elastic_xs[elastic_xs < 0.0] = 0.0
×
1095

1096
            tabulated_xs = Tabulated1D(grid, elastic_xs)
11✔
1097
            tabulated_xs._threshold_idx = 0
11✔
1098
            rx.xs[strT] = tabulated_xs
11✔
1099

1100
            # No energy distribution for elastic scattering
1101
            neutron = Product('neutron')
11✔
1102
            neutron.distribution.append(UncorrelatedAngleEnergy())
11✔
1103
            rx.products.append(neutron)
11✔
1104

1105
        # ======================================================================
1106
        # ANGLE DISTRIBUTION (FOR UNCORRELATED)
1107

1108
        if i_reaction < ace.nxs[5] + 1:
11✔
1109
            # Check if angular distribution data exist
1110
            loc = int(ace.xss[ace.jxs[8] + i_reaction])
11✔
1111
            if loc < 0:
11✔
1112
                # Angular distribution is given as part of a product
1113
                # angle-energy distribution
1114
                angle_dist = None
11✔
1115
            elif loc == 0:
11✔
1116
                # Angular distribution is isotropic
1117
                energy = [0.0, grid[-1]]
×
1118
                mu = Uniform(-1., 1.)
×
1119
                angle_dist = AngleDistribution(energy, [mu, mu])
×
1120
            else:
1121
                angle_dist = AngleDistribution.from_ace(ace, ace.jxs[9], loc)
11✔
1122

1123
            # Apply angular distribution to each uncorrelated angle-energy
1124
            # distribution
1125
            if angle_dist is not None:
11✔
1126
                for d in neutron.distribution:
11✔
1127
                    d.angle = angle_dist
11✔
1128

1129
        # ======================================================================
1130
        # PHOTON PRODUCTION
1131

1132
        rx.products += _get_photon_products_ace(ace, rx)
11✔
1133

1134
        return rx
11✔
1135

1136
    @classmethod
11✔
1137
    def from_endf(cls, ev, mt):
11✔
1138
        """Generate a reaction from an ENDF evaluation
1139

1140
        Parameters
1141
        ----------
1142
        ev : openmc.data.endf.Evaluation
1143
            ENDF evaluation
1144
        mt : int
1145
            The MT value of the reaction to get data for
1146

1147
        Returns
1148
        -------
1149
        rx : openmc.data.Reaction
1150
            Reaction data
1151

1152
        """
1153
        rx = Reaction(mt)
11✔
1154

1155
        # Integrated cross section
1156
        if (3, mt) in ev.section:
11✔
1157
            file_obj = StringIO(ev.section[3, mt])
11✔
1158
            get_head_record(file_obj)
11✔
1159
            params, rx.xs['0K'] = get_tab1_record(file_obj)
11✔
1160
            rx.q_value = params[1]
11✔
1161

1162
        # Get fission product yields (nu) as well as delayed neutron energy
1163
        # distributions
1164
        if mt in FISSION_MTS:
11✔
1165
            rx.products, rx.derived_products = _get_fission_products_endf(ev)
11✔
1166

1167
        if (6, mt) in ev.section:
11✔
1168
            # Product angle-energy distribution
1169
            for product in _get_products(ev, mt):
11✔
1170
                if mt in FISSION_MTS and product.particle == 'neutron':
11✔
1171
                    rx.products[0].applicability = product.applicability
11✔
1172
                    rx.products[0].distribution = product.distribution
11✔
1173
                else:
1174
                    rx.products.append(product)
11✔
1175

1176
        elif (4, mt) in ev.section or (5, mt) in ev.section:
11✔
1177
            # Uncorrelated angle-energy distribution
1178
            neutron = Product('neutron')
11✔
1179

1180
            # Note that the energy distribution for MT=455 is read in
1181
            # _get_fission_products_endf rather than here
1182
            if (5, mt) in ev.section:
11✔
1183
                file_obj = StringIO(ev.section[5, mt])
11✔
1184
                items = get_head_record(file_obj)
11✔
1185
                nk = items[4]
11✔
1186
                for i in range(nk):
11✔
1187
                    params, applicability = get_tab1_record(file_obj)
11✔
1188
                    dist = UncorrelatedAngleEnergy()
11✔
1189
                    dist.energy = EnergyDistribution.from_endf(file_obj, params)
11✔
1190

1191
                    neutron.applicability.append(applicability)
11✔
1192
                    neutron.distribution.append(dist)
11✔
1193
            elif mt == 2:
11✔
1194
                # Elastic scattering -- no energy distribution is given since it
1195
                # can be calulcated analytically
1196
                dist = UncorrelatedAngleEnergy()
11✔
1197
                neutron.distribution.append(dist)
11✔
1198
            elif mt >= 51 and mt < 91:
11✔
1199
                # Level inelastic scattering -- no energy distribution is given
1200
                # since it can be calculated analytically. Here we determine the
1201
                # necessary parameters to create a LevelInelastic object
1202
                dist = UncorrelatedAngleEnergy()
11✔
1203

1204
                A = ev.target['mass']
11✔
1205
                threshold = (A + 1.)/A*abs(rx.q_value)
11✔
1206
                mass_ratio = (A/(A + 1.))**2
11✔
1207
                dist.energy = LevelInelastic(threshold, mass_ratio)
11✔
1208

1209
                neutron.distribution.append(dist)
11✔
1210

1211
            if (4, mt) in ev.section:
11✔
1212
                for dist in neutron.distribution:
11✔
1213
                    dist.angle = AngleDistribution.from_endf(ev, mt)
11✔
1214

1215
            if mt in FISSION_MTS and (5, mt) in ev.section:
11✔
1216
                # For fission reactions,
1217
                rx.products[0].applicability = neutron.applicability
11✔
1218
                rx.products[0].distribution = neutron.distribution
11✔
1219
            else:
1220
                rx.products.append(neutron)
11✔
1221

1222
        if (8, mt) in ev.section:
11✔
1223
            rx.products += _get_activation_products(ev, rx)
11✔
1224

1225
        if (12, mt) in ev.section or (13, mt) in ev.section:
11✔
1226
            rx.products += _get_photon_products_endf(ev, rx)
11✔
1227

1228
        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