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

openmc-dev / openmc / 17324196295

29 Aug 2025 12:46PM UTC coverage: 85.218% (+0.01%) from 85.207%
17324196295

Pull #3439

github

web-flow
Merge f5682d62d into 5529731e2
Pull Request #3439: Photonuclear Physics part 1

457 of 546 new or added lines in 8 files covered. (83.7%)

8 existing lines in 1 file now uncovered.

53400 of 62663 relevant lines covered (85.22%)

37300144.43 hits per line

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

80.25
/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
    NotImplementedError
91
        When the projectile is not a neutron and not a photon.
92

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

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

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

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

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

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

126
        p.yield_ = yield_
11✔
127

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

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

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

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

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

167
                zat = ev.target["atomic_number"] * 1000 + ev.target["mass_number"]
11✔
168
                if ev.projectile['mass'] == 0.0:
11✔
NEW
169
                    projectile_za = 0
×
170
                elif np.isclose(ev.projectile['mass'], 1.0, atol=1.0e-12, rtol=0.):
11✔
171
                    projectile_za = 1
11✔
172
                else:
NEW
173
                    raise NotImplementedError('Unknown projectile')
×
174
                p.distribution = [KalbachMann.from_endf(file_obj,
11✔
175
                                                        za,
176
                                                        zat,
177
                                                        projectile_za)]
178

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

197
            angle_dist = AngleDistribution(energy, mu)
11✔
198
            dist = UncorrelatedAngleEnergy(angle_dist)
11✔
199
            p.distribution = [dist]
11✔
200
            # TODO: Add level-inelastic info?
201

202
        elif law == 3:
11✔
203
            # Isotropic discrete emission
204
            p.distribution = [UncorrelatedAngleEnergy()]
11✔
205
            # TODO: Add level-inelastic info?
206

207
        elif law == 4:
11✔
208
            # Discrete two-body recoil
209
            pass
11✔
210

211
        elif law == 5:
11✔
212
            # Charged particle elastic scattering
213
            pass
×
214

215
        elif law == 6:
11✔
216
            # N-body phase-space distribution
217
            p.distribution = [NBodyPhaseSpace.from_endf(file_obj)]
×
218

219
        elif law == 7:
11✔
220
            # Laboratory energy-angle distribution
221
            p.distribution = [LaboratoryAngleEnergy.from_endf(file_obj)]
11✔
222

223
        products.append(p)
11✔
224

225
    return products
11✔
226

227

228
def _get_fission_products_ace(ace):
11✔
229
    """Generate fission products from an ACE table
230

231
    Parameters
232
    ----------
233
    ace : openmc.data.ace.Table
234
        ACE table to read from
235

236
    Returns
237
    -------
238
    products : list of openmc.data.Product
239
        Prompt and delayed fission neutrons
240
    derived_products : list of openmc.data.Product
241
        "Total" fission neutron
242

243
    """
244
    # No NU block
245
    if ace.jxs[2] == 0:
11✔
246
        return None, None
×
247

248
    products = []
11✔
249
    derived_products = []
11✔
250

251
    # Either prompt nu or total nu is given
252
    if ace.xss[ace.jxs[2]] > 0:
11✔
253
        whichnu = 'prompt' if ace.jxs[24] > 0 else 'total'
×
254

255
        neutron = Product('neutron')
×
256
        neutron.emission_mode = whichnu
×
257

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

271
        products.append(neutron)
×
272

273
    # Both prompt nu and total nu
274
    elif ace.xss[ace.jxs[2]] < 0:
11✔
275
        # Read prompt neutron yield
276
        prompt_neutron = Product('neutron')
11✔
277
        prompt_neutron.emission_mode = 'prompt'
11✔
278

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

292
        # Read total neutron yield
293
        total_neutron = Product('neutron')
11✔
294
        total_neutron.emission_mode = 'total'
11✔
295

296
        idx = ace.jxs[2] + int(abs(ace.xss[ace.jxs[2]])) + 1
11✔
297
        LNU = int(ace.xss[idx])
11✔
298

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

310
        products.append(prompt_neutron)
11✔
311
        derived_products.append(total_neutron)
11✔
312

313
    # Check for delayed nu data
314
    if ace.jxs[24] > 0:
11✔
315
        yield_delayed = Tabulated1D.from_ace(ace, ace.jxs[24] + 1)
×
316

317
        # Delayed neutron precursor distribution
318
        idx = ace.jxs[25]
×
319
        n_group = ace.nxs[8]
×
320
        total_group_probability = 0.
×
321
        for group in range(n_group):
×
322
            delayed_neutron = Product('neutron')
×
323
            delayed_neutron.emission_mode = 'delayed'
×
324

325
            # Convert units of inverse shakes to inverse seconds
326
            delayed_neutron.decay_rate = ace.xss[idx] * 1.e8
×
327

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

340
                # Calculate group yield
341
                group_yield = yield_delayed(energy) * group_probability(energy)
×
342
                delayed_neutron.yield_ = Tabulated1D(energy, group_yield)
×
343

344
            # Advance position
345
            nr = int(ace.xss[idx + 1])
×
346
            ne = int(ace.xss[idx + 2 + 2*nr])
×
347
            idx += 3 + 2*nr + 2*ne
×
348

349
            # Energy distribution for delayed fission neutrons
350
            location_start = int(ace.xss[ace.jxs[26] + group])
×
351
            delayed_neutron.distribution.append(
×
352
                AngleEnergy.from_ace(ace, ace.jxs[27], location_start))
353

354
            products.append(delayed_neutron)
×
355

356
        # Renormalize delayed neutron yields to reflect fact that in ACE
357
        # file, the sum of the group probabilities is not exactly one
358
        for product in products[1:]:
×
359
            if total_group_probability > 0.:
×
360
                product.yield_.y /= total_group_probability
×
361

362
    return products, derived_products
11✔
363

364

365
def _get_fission_products_endf(ev):
11✔
366
    """Generate fission products from an ENDF evaluation
367

368
    Parameters
369
    ----------
370
    ev : openmc.data.endf.Evaluation
371

372
    Returns
373
    -------
374
    products : list of openmc.data.Product
375
        Prompt and delayed fission neutrons
376
    derived_products : list of openmc.data.Product
377
        "Total" fission neutron
378

379
    """
380
    products = []
11✔
381
    derived_products = []
11✔
382

383
    if (1, 456) in ev.section:
11✔
384
        prompt_neutron = Product('neutron')
11✔
385
        prompt_neutron.emission_mode = 'prompt'
11✔
386

387
        # Prompt nu values
388
        file_obj = StringIO(ev.section[1, 456])
11✔
389
        lnu = get_head_record(file_obj)[3]
11✔
390
        if lnu == 1:
11✔
391
            # Polynomial representation
392
            items, coefficients = get_list_record(file_obj)
×
393
            prompt_neutron.yield_ = Polynomial(coefficients)
×
394
        elif lnu == 2:
11✔
395
            # Tabulated representation
396
            params, prompt_neutron.yield_ = get_tab1_record(file_obj)
11✔
397

398
        products.append(prompt_neutron)
11✔
399

400
    if (1, 452) in ev.section:
11✔
401
        total_neutron = Product('neutron')
11✔
402
        total_neutron.emission_mode = 'total'
11✔
403

404
        # Total nu values
405
        file_obj = StringIO(ev.section[1, 452])
11✔
406
        lnu = get_head_record(file_obj)[3]
11✔
407
        if lnu == 1:
11✔
408
            # Polynomial representation
409
            items, coefficients = get_list_record(file_obj)
×
410
            total_neutron.yield_ = Polynomial(coefficients)
×
411
        elif lnu == 2:
11✔
412
            # Tabulated representation
413
            params, total_neutron.yield_ = get_tab1_record(file_obj)
11✔
414

415
        if (1, 456) in ev.section:
11✔
416
            derived_products.append(total_neutron)
11✔
417
        else:
418
            products.append(total_neutron)
×
419

420
    if (1, 455) in ev.section:
11✔
421
        file_obj = StringIO(ev.section[1, 455])
11✔
422

423
        # Determine representation of delayed nu data
424
        items = get_head_record(file_obj)
11✔
425
        ldg = items[2]
11✔
426
        lnu = items[3]
11✔
427

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

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

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

476
                delayed_neutron = products[1 + i]
11✔
477
                yield_ = delayed_neutron.yield_
11✔
478

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

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

507
                delayed_neutron.distribution.append(dist)
11✔
508

509
    return products, derived_products
11✔
510

511

512
def _get_activation_products(ev, rx):
11✔
513
    """Generate activation products from an ENDF evaluation
514

515
    Parameters
516
    ----------
517
    ev : openmc.data.endf.Evaluation
518
        The ENDF evaluation
519
    rx : openmc.data.Reaction
520
        Reaction which generates activation products
521

522
    Returns
523
    -------
524
    products : list of openmc.data.Product
525
        Activation products
526

527
    """
528
    file_obj = StringIO(ev.section[8, rx.mt])
11✔
529

530
    # Determine total number of states and whether decay chain is given in a
531
    # decay sublibrary
532
    items = get_head_record(file_obj)
11✔
533
    n_states = items[4]
11✔
534
    decay_sublib = (items[5] == 1)
11✔
535

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

549
    products = []
11✔
550

551
    for mf in (9, 10):
11✔
552
        if not present[mf]:
11✔
553
            continue
11✔
554

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

564
            # Get GNDS name for product
565
            symbol = ATOMIC_SYMBOL[Z]
11✔
566
            if excited_state > 0:
11✔
567
                name = f'{symbol}{A}_e{excited_state}'
11✔
568
            else:
569
                name = f'{symbol}{A}'
11✔
570

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

582
                # Calculate yield as ratio
583
                yield_ = np.zeros_like(energy)
×
584
                yield_[idx] = prod_xs[idx] / neutron_xs[idx]
×
585
                p.yield_ = Tabulated1D(energy, yield_)
×
586

587
            # Check if product already exists from MF=6 and if it does, just
588
            # overwrite the existing yield.
589
            for product in rx.products:
11✔
590
                if name == product.particle:
11✔
591
                    product.yield_ = p.yield_
×
592
                    break
×
593
            else:
594
                products.append(p)
11✔
595

596
    return products
11✔
597

598

599
def _get_photon_products_ace(ace, rx):
11✔
600
    """Generate photon products from an ACE table
601

602
    Parameters
603
    ----------
604
    ace : openmc.data.ace.Table
605
        ACE table to read from
606
    rx : openmc.data.Reaction
607
        Reaction that generates photons
608

609
    Returns
610
    -------
611
    photons : list of openmc.Products
612
        Photons produced from reaction with given MT
613

614
    """
615
    n_photon_reactions = ace.nxs[6]
11✔
616
    photon_mts = ace.xss[ace.jxs[13]:ace.jxs[13] +
11✔
617
                         n_photon_reactions].astype(int)
618

619
    photons = []
11✔
620
    for i in range(n_photon_reactions):
11✔
621
        # Determine corresponding reaction
622
        neutron_mt = photon_mts[i] // 1000
11✔
623

624
        if neutron_mt != rx.mt:
11✔
625
            continue
11✔
626

627
        # Create photon product and assign to reactions
628
        photon = Product('photon')
11✔
629

630
        # ==================================================================
631
        # Photon yield / production cross section
632

633
        loca = int(ace.xss[ace.jxs[14] + i])
11✔
634
        idx = ace.jxs[15] + loca - 1
11✔
635
        mftype = int(ace.xss[idx])
11✔
636
        idx += 1
11✔
637

638
        if mftype in (12, 16):
11✔
639
            # Yield data taken from ENDF File 12 or 6
640
            mtmult = int(ace.xss[idx])
11✔
641
            assert mtmult == neutron_mt
11✔
642

643
            # Read photon yield as function of energy
644
            photon.yield_ = Tabulated1D.from_ace(ace, idx + 1)
11✔
645

646
        elif mftype == 13:
×
647
            # Cross section data from ENDF File 13
648

649
            # Energy grid index at which data starts
650
            threshold_idx = int(ace.xss[idx]) - 1
×
651
            n_energy = int(ace.xss[idx + 1])
×
652
            energy = ace.xss[ace.jxs[1] + threshold_idx:
×
653
                             ace.jxs[1] + threshold_idx + n_energy]*EV_PER_MEV
654

655
            # Get photon production cross section
656
            photon_prod_xs = ace.xss[idx + 2:idx + 2 + n_energy]
×
657
            neutron_xs = list(rx.xs.values())[0](energy)
×
658
            idx = np.where(neutron_xs > 0.)
×
659

660
            # Calculate photon yield
661
            yield_ = np.zeros_like(photon_prod_xs)
×
662
            yield_[idx] = photon_prod_xs[idx] / neutron_xs[idx]
×
663
            photon.yield_ = Tabulated1D(energy, yield_)
×
664

665
        else:
666
            raise ValueError(f"MFTYPE must be 12, 13, 16. Got {mftype}")
×
667

668
        # ==================================================================
669
        # Photon energy distribution
670

671
        location_start = int(ace.xss[ace.jxs[18] + i])
11✔
672
        distribution = AngleEnergy.from_ace(ace, ace.jxs[19], location_start)
11✔
673
        assert isinstance(distribution, UncorrelatedAngleEnergy)
11✔
674

675
        # ==================================================================
676
        # Photon angular distribution
677
        loc = int(ace.xss[ace.jxs[16] + i])
11✔
678

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

689
        # Add to list of distributions
690
        photon.distribution.append(distribution)
11✔
691
        photons.append(photon)
11✔
692

693
    return photons
11✔
694

695

696
def _get_photon_products_endf(ev, rx):
11✔
697
    """Generate photon products from an ENDF evaluation
698

699
    Parameters
700
    ----------
701
    ev : openmc.data.endf.Evaluation
702
        ENDF evaluation to read from
703
    rx : openmc.data.Reaction
704
        Reaction that generates photons
705

706
    Returns
707
    -------
708
    products : list of openmc.Products
709
        Photons produced from reaction with given MT
710

711
    """
712
    products = []
11✔
713

714
    if (12, rx.mt) in ev.section:
11✔
715
        file_obj = StringIO(ev.section[12, rx.mt])
11✔
716

717
        items = get_head_record(file_obj)
11✔
718
        option = items[2]
11✔
719

720
        if option == 1:
11✔
721
            # Multiplicities given
722
            n_discrete_photon = items[4]
11✔
723
            if n_discrete_photon > 1:
11✔
724
                items, total_yield = get_tab1_record(file_obj)
11✔
725
            for k in range(n_discrete_photon):
11✔
726
                photon = Product('photon')
11✔
727

728
                # Get photon yield
729
                items, photon.yield_ = get_tab1_record(file_obj)
11✔
730

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

743
                photon.distribution.append(dist)
11✔
744
                products.append(photon)
11✔
745

746
        elif option == 2:
11✔
747
            # Transition probability arrays given
748
            ppyield = {}
11✔
749
            ppyield['type'] = 'transition'
11✔
750
            ppyield['transition'] = transition = {}
11✔
751

752
            # Determine whether simple (LG=1) or complex (LG=2) transitions
753
            lg = items[3]
11✔
754

755
            # Get transition data
756
            items, values = get_list_record(file_obj)
11✔
757
            transition['energy_start'] = items[0]
11✔
758
            transition['energies'] = np.array(values[::lg + 1])
11✔
759
            transition['direct_probability'] = np.array(values[1::lg + 1])
11✔
760
            if lg == 2:
11✔
761
                # Complex case
762
                transition['conditional_probability'] = np.array(
11✔
763
                    values[2::lg + 1])
764

765
    elif (13, rx.mt) in ev.section:
11✔
766
        file_obj = StringIO(ev.section[13, rx.mt])
11✔
767

768
        # Determine option
769
        items = get_head_record(file_obj)
11✔
770
        n_discrete_photon = items[4]
11✔
771
        if n_discrete_photon > 1:
11✔
772
            items, total_xs = get_tab1_record(file_obj)
11✔
773
        for k in range(n_discrete_photon):
11✔
774
            photon = Product('photon')
11✔
775
            items, xs = get_tab1_record(file_obj)
11✔
776

777
            # Re-interpolate photon production cross section and neutron cross
778
            # section to union energy grid
779
            energy = np.union1d(xs.x, rx.xs['0K'].x)
11✔
780
            photon_prod_xs = xs(energy)
11✔
781
            neutron_xs = rx.xs['0K'](energy)
11✔
782
            idx = np.where(neutron_xs > 0)
11✔
783

784
            # Calculate yield as ratio
785
            yield_ = np.zeros_like(energy)
11✔
786
            yield_[idx] = photon_prod_xs[idx] / neutron_xs[idx]
11✔
787
            photon.yield_ = Tabulated1D(energy, yield_)
11✔
788

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

801
            photon.distribution.append(dist)
11✔
802
            products.append(photon)
11✔
803

804
    return products
11✔
805

806

807
class Reaction(EqualityMixin):
11✔
808
    """A nuclear reaction
809

810
    A Reaction object represents a single reaction channel for a nuclide with
811
    an associated cross section and, if present, a secondary angle and energy
812
    distribution.
813

814
    Parameters
815
    ----------
816
    mt : int
817
        The ENDF MT number for this reaction.
818

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

841
    """
842

843
    def __init__(self, mt):
11✔
844
        self._center_of_mass = True
11✔
845
        self._redundant = False
11✔
846
        self._q_value = 0.
11✔
847
        self._xs = {}
11✔
848
        self._products = []
11✔
849
        self._derived_products = []
11✔
850

851
        self.mt = mt
11✔
852

853
    def __repr__(self):
11✔
854
        if self.mt in REACTION_NAME:
×
855
            return f"<Reaction: MT={self.mt} {REACTION_NAME[self.mt]}>"
×
856
        else:
857
            return f"<Reaction: MT={self.mt}>"
×
858

859
    @property
11✔
860
    def center_of_mass(self):
11✔
861
        return self._center_of_mass
11✔
862

863
    @center_of_mass.setter
11✔
864
    def center_of_mass(self, center_of_mass):
11✔
865
        cv.check_type('center of mass', center_of_mass, (bool, np.bool_))
11✔
866
        self._center_of_mass = center_of_mass
11✔
867

868
    @property
11✔
869
    def redundant(self):
11✔
870
        return self._redundant
11✔
871

872
    @redundant.setter
11✔
873
    def redundant(self, redundant):
11✔
874
        cv.check_type('redundant', redundant, (bool, np.bool_))
11✔
875
        self._redundant = redundant
11✔
876

877
    @property
11✔
878
    def q_value(self):
11✔
879
        return self._q_value
11✔
880

881
    @q_value.setter
11✔
882
    def q_value(self, q_value):
11✔
883
        cv.check_type('Q value', q_value, Real)
11✔
884
        self._q_value = q_value
11✔
885

886
    @property
11✔
887
    def products(self):
11✔
888
        return self._products
11✔
889

890
    @products.setter
11✔
891
    def products(self, products):
11✔
892
        cv.check_type('reaction products', products, Iterable, Product)
11✔
893
        self._products = products
11✔
894

895
    @property
11✔
896
    def derived_products(self):
11✔
897
        return self._derived_products
11✔
898

899
    @derived_products.setter
11✔
900
    def derived_products(self, derived_products):
11✔
901
        cv.check_type('reaction derived products', derived_products,
11✔
902
                      Iterable, Product)
903
        self._derived_products = derived_products
11✔
904

905
    @property
11✔
906
    def xs(self):
11✔
907
        return self._xs
11✔
908

909
    @xs.setter
11✔
910
    def xs(self, xs):
11✔
911
        cv.check_type('reaction cross section dictionary', xs, MutableMapping)
×
912
        for key, value in xs.items():
×
913
            cv.check_type('reaction cross section temperature', key, str)
×
914
            cv.check_type('reaction cross section', value, Callable)
×
915
        self._xs = xs
×
916

917
    def to_hdf5(self, group):
11✔
918
        """Write reaction to an HDF5 group
919

920
        Parameters
921
        ----------
922
        group : h5py.Group
923
            HDF5 group to write to
924

925
        """
926

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

945
    @classmethod
11✔
946
    def from_hdf5(cls, group, energy):
11✔
947
        """Generate reaction from an HDF5 group
948

949
        Parameters
950
        ----------
951
        group : h5py.Group
952
            HDF5 group to read from
953
        energy : dict
954
            Dictionary whose keys are temperatures (e.g., '300K') and values are
955
            arrays of energies at which cross sections are tabulated at.
956

957
        Returns
958
        -------
959
        openmc.data.Reaction
960
            Reaction data
961

962
        """
963

964
        mt = group.attrs['mt']
11✔
965
        rx = cls(mt)
11✔
966
        rx.q_value = group.attrs['Q_value']
11✔
967
        rx.center_of_mass = bool(group.attrs['center_of_mass'])
11✔
968
        rx.redundant = bool(group.attrs.get('redundant', False))
11✔
969

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

986
        # Determine number of products
987
        n_product = 0
11✔
988
        for name in group:
11✔
989
            if name.startswith('product_'):
11✔
990
                n_product += 1
11✔
991

992
        # Read reaction products
993
        for i in range(n_product):
11✔
994
            pgroup = group[f'product_{i}']
11✔
995
            rx.products.append(Product.from_hdf5(pgroup))
11✔
996

997
        return rx
11✔
998

999
    @classmethod
11✔
1000
    def from_ace(cls, ace, i_reaction):
11✔
1001
        # Get nuclide energy grid
1002
        n_grid = ace.nxs[3]
11✔
1003
        grid = ace.xss[ace.jxs[1]:ace.jxs[1] + n_grid]*EV_PER_MEV
11✔
1004

1005
        # Convert data temperature to a "300.0K" number for indexing
1006
        # temperature data
1007
        strT = str(int(round(ace.temperature*EV_PER_MEV / K_BOLTZMANN))) + "K"
11✔
1008

1009
        if i_reaction > 0:
11✔
1010
            mt = int(ace.xss[ace.jxs[3] + i_reaction - 1])
11✔
1011
            rx = cls(mt)
11✔
1012

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

1016
            # ==================================================================
1017
            # CROSS SECTION
1018

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

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

1025
            # Determine number of energies in reaction
1026
            n_energy = int(ace.xss[ace.jxs[7] + loc])
11✔
1027
            energy = grid[threshold_idx:threshold_idx + n_energy]
11✔
1028

1029
            # Read reaction cross section
1030
            xs = ace.xss[ace.jxs[7] + loc + 1:ace.jxs[7] + loc + 1 + n_energy]
11✔
1031

1032
            # For damage energy production, convert to eV
1033
            if mt == 444:
11✔
1034
                xs *= EV_PER_MEV
11✔
1035

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

1042
            tabulated_xs = Tabulated1D(energy, xs)
11✔
1043
            tabulated_xs._threshold_idx = threshold_idx
11✔
1044
            rx.xs[strT] = tabulated_xs
11✔
1045

1046
            # ==================================================================
1047
            # YIELD AND ANGLE-ENERGY DISTRIBUTION
1048

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

1062
                    neutron = Product('neutron')
11✔
1063
                    neutron.yield_ = yield_
11✔
1064
                    rx.products.append(neutron)
11✔
1065
                else:
1066
                    assert mt in FISSION_MTS
11✔
1067
                    rx.products, rx.derived_products = _get_fission_products_ace(ace)
11✔
1068

1069
                    for p in rx.products:
11✔
1070
                        if p.emission_mode in ('prompt', 'total'):
11✔
1071
                            neutron = p
11✔
1072
                            break
11✔
1073
                    else:
1074
                        raise Exception("Couldn't find prompt/total fission neutron")
×
1075

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

1083
                    # Read energy distribution data
1084
                    neutron.distribution.append(AngleEnergy.from_ace(
11✔
1085
                        ace, ace.jxs[11], lnw, rx))
1086

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

1089
        else:
1090
            # Elastic scattering
1091
            mt = 2
11✔
1092
            rx = cls(mt)
11✔
1093

1094
            # Get elastic cross section values
1095
            elastic_xs = ace.xss[ace.jxs[1] + 3*n_grid:ace.jxs[1] + 4*n_grid]
11✔
1096

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

1103
            tabulated_xs = Tabulated1D(grid, elastic_xs)
11✔
1104
            tabulated_xs._threshold_idx = 0
11✔
1105
            rx.xs[strT] = tabulated_xs
11✔
1106

1107
            # No energy distribution for elastic scattering
1108
            neutron = Product('neutron')
11✔
1109
            neutron.distribution.append(UncorrelatedAngleEnergy())
11✔
1110
            rx.products.append(neutron)
11✔
1111

1112
        # ======================================================================
1113
        # ANGLE DISTRIBUTION (FOR UNCORRELATED)
1114

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

1130
            # Apply angular distribution to each uncorrelated angle-energy
1131
            # distribution
1132
            if angle_dist is not None:
11✔
1133
                for d in neutron.distribution:
11✔
1134
                    d.angle = angle_dist
11✔
1135

1136
        # ======================================================================
1137
        # PHOTON PRODUCTION
1138

1139
        rx.products += _get_photon_products_ace(ace, rx)
11✔
1140

1141
        return rx
11✔
1142

1143
    @classmethod
11✔
1144
    def from_endf(cls, ev, mt):
11✔
1145
        """Generate a reaction from an ENDF evaluation
1146

1147
        Parameters
1148
        ----------
1149
        ev : openmc.data.endf.Evaluation
1150
            ENDF evaluation
1151
        mt : int
1152
            The MT value of the reaction to get data for
1153

1154
        Returns
1155
        -------
1156
        rx : openmc.data.Reaction
1157
            Reaction data
1158

1159
        """
1160
        rx = Reaction(mt)
11✔
1161

1162
        # Integrated cross section
1163
        if (3, mt) in ev.section:
11✔
1164
            file_obj = StringIO(ev.section[3, mt])
11✔
1165
            get_head_record(file_obj)
11✔
1166
            params, rx.xs['0K'] = get_tab1_record(file_obj)
11✔
1167
            rx.q_value = params[1]
11✔
1168

1169
        # Get fission product yields (nu) as well as delayed neutron energy
1170
        # distributions
1171
        if mt in FISSION_MTS:
11✔
1172
            rx.products, rx.derived_products = _get_fission_products_endf(ev)
11✔
1173

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

1183
        elif (4, mt) in ev.section or (5, mt) in ev.section:
11✔
1184
            # Uncorrelated angle-energy distribution
1185
            neutron = Product('neutron')
11✔
1186

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

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

1211
                A = ev.target['mass']
11✔
1212
                threshold = (A + 1.)/A*abs(rx.q_value)
11✔
1213
                mass_ratio = (A/(A + 1.))**2
11✔
1214
                dist.energy = LevelInelastic(threshold, mass_ratio)
11✔
1215

1216
                neutron.distribution.append(dist)
11✔
1217

1218
            if (4, mt) in ev.section:
11✔
1219
                for dist in neutron.distribution:
11✔
1220
                    dist.angle = AngleDistribution.from_endf(ev, mt)
11✔
1221

1222
            if mt in FISSION_MTS and (5, mt) in ev.section:
11✔
1223
                # For fission reactions,
1224
                rx.products[0].applicability = neutron.applicability
11✔
1225
                rx.products[0].distribution = neutron.distribution
11✔
1226
            else:
1227
                rx.products.append(neutron)
11✔
1228

1229
        if (8, mt) in ev.section:
11✔
1230
            rx.products += _get_activation_products(ev, rx)
11✔
1231

1232
        if (12, mt) in ev.section or (13, mt) in ev.section:
11✔
1233
            rx.products += _get_photon_products_endf(ev, rx)
11✔
1234

1235
        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