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

openmc-dev / openmc / 21621898565

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

Pull #3474

github

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

17285 of 24269 branches covered (71.22%)

Branch coverage included in aggregate %.

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

402 existing lines in 9 files now uncovered.

55547 of 65340 relevant lines covered (85.01%)

34579169.11 hits per line

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

68.24
/openmc/data/resonance.py
1
from collections.abc import MutableSequence, Iterable
7✔
2
import io
7✔
3

4
import numpy as np
7✔
5
from numpy.polynomial import Polynomial
7✔
6
import pandas as pd
7✔
7

8
import openmc.checkvalue as cv
7✔
9
from .data import NEUTRON_MASS
7✔
10
from .endf import get_head_record, get_cont_record, get_tab1_record, get_list_record
7✔
11
try:
7✔
12
    from .reconstruct import wave_number, penetration_shift, reconstruct_mlbw, \
7✔
13
        reconstruct_slbw, reconstruct_rm
14
    _reconstruct = True
×
15
except ImportError:
7✔
16
    _reconstruct = False
7✔
17

18

19
class Resonances:
7✔
20
    """Resolved and unresolved resonance data
21

22
    Parameters
23
    ----------
24
    ranges : list of openmc.data.ResonanceRange
25
        Distinct energy ranges for resonance data
26

27
    Attributes
28
    ----------
29
    ranges : list of openmc.data.ResonanceRange
30
        Distinct energy ranges for resonance data
31
    resolved : openmc.data.ResonanceRange or None
32
        Resolved resonance range
33
    unresolved : openmc.data.Unresolved or None
34
        Unresolved resonance range
35

36
    """
37

38
    def __init__(self, ranges):
7✔
39
        self.ranges = ranges
7✔
40

41
    def __iter__(self):
7✔
42
        for r in self.ranges:
7✔
43
            yield r
7✔
44

45
    @property
7✔
46
    def ranges(self):
7✔
47
        return self._ranges
7✔
48

49
    @ranges.setter
7✔
50
    def ranges(self, ranges):
7✔
51
        cv.check_type('resonance ranges', ranges, MutableSequence)
7✔
52
        self._ranges = cv.CheckedList(ResonanceRange, 'resonance ranges',
7✔
53
                                      ranges)
54

55
    @property
7✔
56
    def resolved(self):
7✔
57
        resolved_ranges = [r for r in self.ranges
7✔
58
                           if not isinstance(r, Unresolved)]
59
        if len(resolved_ranges) > 1:
7✔
60
            raise ValueError('More than one resolved range present')
×
61
        elif len(resolved_ranges) == 0:
7✔
62
            return None
×
63
        else:
64
            return resolved_ranges[0]
7✔
65

66
    @property
7✔
67
    def unresolved(self):
7✔
68
        for r in self.ranges:
7✔
69
            if isinstance(r, Unresolved):
7✔
70
                return r
7✔
71
        else:
UNCOV
72
            return None
×
73

74
    @classmethod
7✔
75
    def from_endf(cls, ev):
7✔
76
        """Generate resonance data from an ENDF evaluation.
77

78
        Parameters
79
        ----------
80
        ev : openmc.data.endf.Evaluation
81
            ENDF evaluation
82

83
        Returns
84
        -------
85
        openmc.data.Resonances
86
            Resonance data
87

88
        """
89
        file_obj = io.StringIO(ev.section[2, 151])
7✔
90

91
        # Determine whether discrete or continuous representation
92
        items = get_head_record(file_obj)
7✔
93
        n_isotope = items[4]  # Number of isotopes
7✔
94

95
        ranges = []
7✔
96
        for _ in range(n_isotope):
7✔
97
            items = get_cont_record(file_obj)
7✔
98
            fission_widths = (items[3] == 1)  # fission widths are given?
7✔
99
            n_ranges = items[4]  # number of resonance energy ranges
7✔
100

101
            for j in range(n_ranges):
7✔
102
                items = get_cont_record(file_obj)
7✔
103
                resonance_flag = items[2]  # flag for resolved (1)/unresolved (2)
7✔
104
                formalism = items[3]  # resonance formalism
7✔
105

106
                if resonance_flag in (0, 1):
7✔
107
                    # resolved resonance region
108
                    erange = _FORMALISMS[formalism].from_endf(ev, file_obj, items)
7✔
109

110
                elif resonance_flag == 2:
7✔
111
                    # unresolved resonance region
112
                    erange = Unresolved.from_endf(file_obj, items, fission_widths)
7✔
113

114
                # erange.material = self
115
                ranges.append(erange)
7✔
116

117
        return cls(ranges)
7✔
118

119

120
class ResonanceRange:
7✔
121
    """Resolved resonance range
122

123
    Parameters
124
    ----------
125
    target_spin : float
126
        Intrinsic spin, :math:`I`, of the target nuclide
127
    energy_min : float
128
        Minimum energy of the resolved resonance range in eV
129
    energy_max : float
130
        Maximum energy of the resolved resonance range in eV
131
    channel : dict
132
        Dictionary whose keys are l-values and values are channel radii as a
133
        function of energy
134
    scattering : dict
135
        Dictionary whose keys are l-values and values are scattering radii as a
136
        function of energy
137

138
    Attributes
139
    ----------
140
    channel_radius : dict
141
        Dictionary whose keys are l-values and values are channel radii as a
142
        function of energy
143
    energy_max : float
144
        Maximum energy of the resolved resonance range in eV
145
    energy_min : float
146
        Minimum energy of the resolved resonance range in eV
147
    scattering_radius : dict
148
        Dictionary whose keys are l-values and values are scattering radii as a
149
        function of energ
150
    target_spin : float
151
        Intrinsic spin, :math:`I`, of the target nuclide
152

153
    """
154
    def __init__(self, target_spin, energy_min, energy_max, channel, scattering):
7✔
155
        self.target_spin = target_spin
7✔
156
        self.energy_min = energy_min
7✔
157
        self.energy_max = energy_max
7✔
158
        self.channel_radius = channel
7✔
159
        self.scattering_radius = scattering
7✔
160

161
        self._prepared = False
7✔
162
        self._parameter_matrix = {}
7✔
163

164
    def __copy__(self):
7✔
165
        cls = type(self)
7✔
166
        new_copy = cls.__new__(cls)
7✔
167
        new_copy.__dict__.update(self.__dict__)
7✔
168
        new_copy._prepared = False
7✔
169
        return new_copy
7✔
170

171
    @classmethod
7✔
172
    def from_endf(cls, ev, file_obj, items):
7✔
173
        """Create resonance range from an ENDF evaluation.
174

175
        This factory method is only used when LRU=0, indicating that only a
176
        scattering radius appears in MF=2, MT=151. All subclasses of
177
        ResonanceRange override this method with their own.
178

179
        Parameters
180
        ----------
181
        ev : openmc.data.endf.Evaluation
182
            ENDF evaluation
183
        file_obj : file-like object
184
            ENDF file positioned at the second record of a resonance range
185
            subsection in MF=2, MT=151
186
        items : list
187
            Items from the CONT record at the start of the resonance range
188
            subsection
189

190
        Returns
191
        -------
192
        openmc.data.ResonanceRange
193
            Resonance range data
194

195
        """
196
        energy_min, energy_max = items[0:2]
7✔
197

198
        # For scattering radius-only, NRO must be zero
199
        assert items[4] == 0
7✔
200

201
        # Get energy-independent scattering radius
202
        items = get_cont_record(file_obj)
7✔
203
        target_spin = items[0]
7✔
204
        ap = Polynomial((items[1],))
7✔
205

206
        # Calculate channel radius from ENDF-102 equation D.14
207
        a = Polynomial((0.123 * (NEUTRON_MASS*ev.target['mass'])**(1./3.) + 0.08,))
7✔
208

209
        return cls(target_spin, energy_min, energy_max, {0: a}, {0: ap})
7✔
210

211
    def reconstruct(self, energies):
7✔
212
        """Evaluate cross section at specified energies.
213

214
        Parameters
215
        ----------
216
        energies : float or Iterable of float
217
            Energies at which the cross section should be evaluated
218

219
        Returns
220
        -------
221
        3-tuple of float or numpy.ndarray
222
            Elastic, capture, and fission cross sections at the specified
223
            energies
224

225
        """
226
        if not _reconstruct:
×
227
            raise RuntimeError("Resonance reconstruction not available.")
×
228

229
        # Pre-calculate penetrations and shifts for resonances
230
        if not self._prepared:
×
231
            self._prepare_resonances()
×
232

233
        if isinstance(energies, Iterable):
×
234
            elastic = np.zeros_like(energies)
×
235
            capture = np.zeros_like(energies)
×
236
            fission = np.zeros_like(energies)
×
237

238
            for i, E in enumerate(energies):
×
239
                xse, xsg, xsf = self._reconstruct(self, E)
×
240
                elastic[i] = xse
×
241
                capture[i] = xsg
×
242
                fission[i] = xsf
×
243
        else:
244
            elastic, capture, fission = self._reconstruct(self, energies)
×
245

246
        return {2: elastic, 102: capture, 18: fission}
×
247

248

249
class MultiLevelBreitWigner(ResonanceRange):
7✔
250
    """Multi-level Breit-Wigner resolved resonance formalism data.
251

252
    Multi-level Breit-Wigner resolved resonance data is identified by LRF=2 in
253
    the ENDF-6 format.
254

255
    Parameters
256
    ----------
257
    target_spin : float
258
        Intrinsic spin, :math:`I`, of the target nuclide
259
    energy_min : float
260
        Minimum energy of the resolved resonance range in eV
261
    energy_max : float
262
        Maximum energy of the resolved resonance range in eV
263
    channel : dict
264
        Dictionary whose keys are l-values and values are channel radii as a
265
        function of energy
266
    scattering : dict
267
        Dictionary whose keys are l-values and values are scattering radii as a
268
        function of energy
269

270
    Attributes
271
    ----------
272
    atomic_weight_ratio : float
273
        Atomic weight ratio of the target nuclide given as a function of
274
        l-value. Note that this may be different than the value for the
275
        evaluation as a whole.
276
    channel_radius : dict
277
        Dictionary whose keys are l-values and values are channel radii as a
278
        function of energy
279
    energy_max : float
280
        Maximum energy of the resolved resonance range in eV
281
    energy_min : float
282
        Minimum energy of the resolved resonance range in eV
283
    parameters : pandas.DataFrame
284
        Energies, spins, and resonances widths for each resonance
285
    q_value : dict
286
        Q-value to be added to incident particle's center-of-mass energy to
287
        determine the channel energy for use in the penetrability factor. The
288
        keys of the dictionary are l-values.
289
    scattering_radius : dict
290
        Dictionary whose keys are l-values and values are scattering radii as a
291
        function of energy
292
    target_spin : float
293
        Intrinsic spin, :math:`I`, of the target nuclide
294

295
    """
296

297
    def __init__(self, target_spin, energy_min, energy_max, channel, scattering):
7✔
298
        super().__init__(target_spin, energy_min, energy_max, channel,
7✔
299
                         scattering)
300
        self.parameters = None
7✔
301
        self.q_value = {}
7✔
302
        self.atomic_weight_ratio = None
7✔
303

304
        # Set resonance reconstruction function
305
        if _reconstruct:
7✔
306
            self._reconstruct = reconstruct_mlbw
×
307
        else:
308
            self._reconstruct = None
7✔
309

310
    @classmethod
7✔
311
    def from_endf(cls, ev, file_obj, items):
7✔
312
        """Create MLBW data from an ENDF evaluation.
313

314
        Parameters
315
        ----------
316
        ev : openmc.data.endf.Evaluation
317
            ENDF evaluation
318
        file_obj : file-like object
319
            ENDF file positioned at the second record of a resonance range
320
            subsection in MF=2, MT=151
321
        items : list
322
            Items from the CONT record at the start of the resonance range
323
            subsection
324

325
        Returns
326
        -------
327
        openmc.data.MultiLevelBreitWigner
328
            Multi-level Breit-Wigner resonance parameters
329

330
        """
331

332
        # Read energy-dependent scattering radius if present
333
        energy_min, energy_max = items[0:2]
7✔
334
        nro, naps = items[4:6]
7✔
335
        if nro != 0:
7✔
336
            params, ape = get_tab1_record(file_obj)
×
337

338
        # Other scatter radius parameters
339
        items = get_cont_record(file_obj)
7✔
340
        target_spin = items[0]
7✔
341
        ap = Polynomial((items[1],))  # energy-independent scattering-radius
7✔
342
        NLS = items[4]  # number of l-values
7✔
343

344
        # Read resonance widths, J values, etc
345
        channel_radius = {}
7✔
346
        scattering_radius = {}
7✔
347
        q_value = {}
7✔
348
        records = []
7✔
349
        for l in range(NLS):
7✔
350
            items, values = get_list_record(file_obj)
7✔
351
            l_value = items[2]
7✔
352
            awri = items[0]
7✔
353
            q_value[l_value] = items[1]
7✔
354
            competitive = items[3]
7✔
355

356
            # Calculate channel radius from ENDF-102 equation D.14
357
            a = Polynomial((0.123 * (NEUTRON_MASS*awri)**(1./3.) + 0.08,))
7✔
358

359
            # Construct scattering and channel radius
360
            if nro == 0:
7✔
361
                scattering_radius[l_value] = ap
7✔
362
                if naps == 0:
7✔
363
                    channel_radius[l_value] = a
7✔
364
                elif naps == 1:
×
365
                    channel_radius[l_value] = ap
×
366
            elif nro == 1:
×
367
                scattering_radius[l_value] = ape
×
368
                if naps == 0:
×
369
                    channel_radius[l_value] = a
×
370
                elif naps == 1:
×
371
                    channel_radius[l_value] = ape
×
372
                elif naps == 2:
×
373
                    channel_radius[l_value] = ap
×
374

375
            energy = values[0::6]
7✔
376
            spin = values[1::6]
7✔
377
            gt = np.asarray(values[2::6])
7✔
378
            gn = np.asarray(values[3::6])
7✔
379
            gg = np.asarray(values[4::6])
7✔
380
            gf = np.asarray(values[5::6])
7✔
381
            if competitive > 0:
7✔
382
                gx = gt - (gn + gg + gf)
×
383
            else:
384
                gx = np.zeros_like(gt)
7✔
385

386
            for i, E in enumerate(energy):
7✔
387
                records.append([energy[i], l_value, spin[i], gt[i], gn[i],
7✔
388
                                gg[i], gf[i], gx[i]])
389

390
        columns = ['energy', 'L', 'J', 'totalWidth', 'neutronWidth',
7✔
391
                   'captureWidth', 'fissionWidth', 'competitiveWidth']
392
        parameters = pd.DataFrame.from_records(records, columns=columns)
7✔
393

394
        # Create instance of class
395
        mlbw = cls(target_spin, energy_min, energy_max,
7✔
396
                   channel_radius, scattering_radius)
397
        mlbw.q_value = q_value
7✔
398
        mlbw.atomic_weight_ratio = awri
7✔
399
        mlbw.parameters = parameters
7✔
400

401
        return mlbw
7✔
402

403
    def _prepare_resonances(self):
7✔
404
        df = self.parameters.copy()
×
405

406
        # Penetration and shift factors
407
        p = np.zeros(len(df))
×
408
        s = np.zeros(len(df))
×
409

410
        # Penetration and shift factors for competitive reaction
411
        px = np.zeros(len(df))
×
412
        sx = np.zeros(len(df))
×
413

414
        l_values = []
×
415
        competitive = []
×
416

417
        A = self.atomic_weight_ratio
×
418
        for i, E, l, J, gt, gn, gg, gf, gx in df.itertuples():
×
419
            if l not in l_values:
×
420
                l_values.append(l)
×
421
                competitive.append(gx > 0)
×
422

423
            # Determine penetration and shift corresponding to resonance energy
424
            k = wave_number(A, E)
×
425
            rho = k*self.channel_radius[l](E)
×
426
            p[i], s[i] = penetration_shift(l, rho)
×
427

428
            # Determine penetration at modified energy for competitive reaction
429
            if gx > 0:
×
430
                Ex = E + self.q_value[l]*(A + 1)/A
×
431
                rho = k*self.channel_radius[l](Ex)
×
432
                px[i], sx[i] = penetration_shift(l, rho)
×
433
            else:
434
                px[i] = sx[i] = 0.0
×
435

436
        df['p'] = p
×
437
        df['s'] = s
×
438
        df['px'] = px
×
439
        df['sx'] = sx
×
440

441
        self._l_values = np.array(l_values)
×
442
        self._competitive = np.array(competitive)
×
443
        for l in l_values:
×
444
            self._parameter_matrix[l] = df[df.L == l].values
×
445

446
        self._prepared = True
×
447

448

449
class SingleLevelBreitWigner(MultiLevelBreitWigner):
7✔
450
    """Single-level Breit-Wigner resolved resonance formalism data.
451

452
    Single-level Breit-Wigner resolved resonance data is is identified by LRF=1
453
    in the ENDF-6 format.
454

455
    Parameters
456
    ----------
457
    target_spin : float
458
        Intrinsic spin, :math:`I`, of the target nuclide
459
    energy_min : float
460
        Minimum energy of the resolved resonance range in eV
461
    energy_max : float
462
        Maximum energy of the resolved resonance range in eV
463
    channel : dict
464
        Dictionary whose keys are l-values and values are channel radii as a
465
        function of energy
466
    scattering : dict
467
        Dictionary whose keys are l-values and values are scattering radii as a
468
        function of energy
469

470
    Attributes
471
    ----------
472
    atomic_weight_ratio : float
473
        Atomic weight ratio of the target nuclide given as a function of
474
        l-value. Note that this may be different than the value for the
475
        evaluation as a whole.
476
    channel_radius : dict
477
        Dictionary whose keys are l-values and values are channel radii as a
478
        function of energy
479
    energy_max : float
480
        Maximum energy of the resolved resonance range in eV
481
    energy_min : float
482
        Minimum energy of the resolved resonance range in eV
483
    parameters : pandas.DataFrame
484
        Energies, spins, and resonances widths for each resonance
485
    q_value : dict
486
        Q-value to be added to incident particle's center-of-mass energy to
487
        determine the channel energy for use in the penetrability factor. The
488
        keys of the dictionary are l-values.
489
    scattering_radius : dict
490
        Dictionary whose keys are l-values and values are scattering radii as a
491
        function of energy
492
    target_spin : float
493
        Intrinsic spin, :math:`I`, of the target nuclide
494

495
    """
496

497
    def __init__(self, target_spin, energy_min, energy_max, channel, scattering):
7✔
498
        super().__init__(target_spin, energy_min, energy_max, channel,
7✔
499
                         scattering)
500

501
        # Set resonance reconstruction function
502
        if _reconstruct:
7✔
503
            self._reconstruct = reconstruct_slbw
×
504
        else:
505
            self._reconstruct = None
7✔
506

507

508
class ReichMoore(ResonanceRange):
7✔
509
    """Reich-Moore resolved resonance formalism data.
510

511
    Reich-Moore resolved resonance data is identified by LRF=3 in the ENDF-6
512
    format.
513

514
    Parameters
515
    ----------
516
    target_spin : float
517
        Intrinsic spin, :math:`I`, of the target nuclide
518
    energy_min : float
519
        Minimum energy of the resolved resonance range in eV
520
    energy_max : float
521
        Maximum energy of the resolved resonance range in eV
522
    channel : dict
523
        Dictionary whose keys are l-values and values are channel radii as a
524
        function of energy
525
    scattering : dict
526
        Dictionary whose keys are l-values and values are scattering radii as a
527
        function of energy
528

529
    Attributes
530
    ----------
531
    angle_distribution : bool
532
        Indicate whether parameters can be used to compute angular distributions
533
    atomic_weight_ratio : float
534
        Atomic weight ratio of the target nuclide given as a function of
535
        l-value. Note that this may be different than the value for the
536
        evaluation as a whole.
537
    channel_radius : dict
538
        Dictionary whose keys are l-values and values are channel radii as a
539
        function of energy
540
    energy_max : float
541
        Maximum energy of the resolved resonance range in eV
542
    energy_min : float
543
        Minimum energy of the resolved resonance range in eV
544
    num_l_convergence : int
545
        Number of l-values which must be used to converge the calculation
546
    scattering_radius : dict
547
        Dictionary whose keys are l-values and values are scattering radii as a
548
        function of energy
549
    parameters : pandas.DataFrame
550
        Energies, spins, and resonances widths for each resonance
551
    target_spin : float
552
        Intrinsic spin, :math:`I`, of the target nuclide
553

554
    """
555

556
    def __init__(self, target_spin, energy_min, energy_max, channel, scattering):
7✔
557
        super().__init__(target_spin, energy_min, energy_max, channel,
7✔
558
                         scattering)
559
        self.parameters = None
7✔
560
        self.angle_distribution = False
7✔
561
        self.num_l_convergence = 0
7✔
562

563
        # Set resonance reconstruction function
564
        if _reconstruct:
7✔
565
            self._reconstruct = reconstruct_rm
×
566
        else:
567
            self._reconstruct = None
7✔
568

569
    @classmethod
7✔
570
    def from_endf(cls, ev, file_obj, items):
7✔
571
        """Create Reich-Moore resonance data from an ENDF evaluation.
572

573
        Parameters
574
        ----------
575
        ev : openmc.data.endf.Evaluation
576
            ENDF evaluation
577
        file_obj : file-like object
578
            ENDF file positioned at the second record of a resonance range
579
            subsection in MF=2, MT=151
580
        items : list
581
            Items from the CONT record at the start of the resonance range
582
            subsection
583

584
        Returns
585
        -------
586
        openmc.data.ReichMoore
587
            Reich-Moore resonance parameters
588

589
        """
590
        # Read energy-dependent scattering radius if present
591
        energy_min, energy_max = items[0:2]
7✔
592
        nro, naps = items[4:6]
7✔
593
        if nro != 0:
7✔
594
            params, ape = get_tab1_record(file_obj)
×
595

596
        # Other scatter radius parameters
597
        items = get_cont_record(file_obj)
7✔
598
        target_spin = items[0]
7✔
599
        ap = Polynomial((items[1],))
7✔
600
        angle_distribution = (items[3] == 1)  # Flag for angular distribution
7✔
601
        NLS = items[4]  # Number of l-values
7✔
602
        num_l_convergence = items[5]  # Number of l-values for convergence
7✔
603

604
        # Read resonance widths, J values, etc
605
        channel_radius = {}
7✔
606
        scattering_radius = {}
7✔
607
        records = []
7✔
608
        for i in range(NLS):
7✔
609
            items, values = get_list_record(file_obj)
7✔
610
            apl = Polynomial((items[1],)) if items[1] != 0.0 else ap
7✔
611
            l_value = items[2]
7✔
612
            awri = items[0]
7✔
613

614
            # Calculate channel radius from ENDF-102 equation D.14
615
            a = Polynomial((0.123 * (NEUTRON_MASS*awri)**(1./3.) + 0.08,))
7✔
616

617
            # Construct scattering and channel radius
618
            if nro == 0:
7✔
619
                scattering_radius[l_value] = apl
7✔
620
                if naps == 0:
7✔
621
                    channel_radius[l_value] = a
×
622
                elif naps == 1:
7✔
623
                    channel_radius[l_value] = apl
7✔
624
            elif nro == 1:
×
625
                if naps == 0:
×
626
                    channel_radius[l_value] = a
×
627
                    scattering_radius[l_value] = ape
×
628
                elif naps == 1:
×
629
                    channel_radius[l_value] = scattering_radius[l_value] = ape
×
630
                elif naps == 2:
×
631
                    channel_radius[l_value] = apl
×
632
                    scattering_radius[l_value] = ape
×
633

634
            energy = values[0::6]
7✔
635
            spin = values[1::6]
7✔
636
            gn = values[2::6]
7✔
637
            gg = values[3::6]
7✔
638
            gfa = values[4::6]
7✔
639
            gfb = values[5::6]
7✔
640

641
            for i, E in enumerate(energy):
7✔
642
                records.append([energy[i], l_value, spin[i], gn[i], gg[i],
7✔
643
                                gfa[i], gfb[i]])
644

645
        # Create pandas DataFrame with resonance data
646
        columns = ['energy', 'L', 'J', 'neutronWidth', 'captureWidth',
7✔
647
                   'fissionWidthA', 'fissionWidthB']
648
        parameters = pd.DataFrame.from_records(records, columns=columns)
7✔
649

650
        # Create instance of ReichMoore
651
        rm = cls(target_spin, energy_min, energy_max,
7✔
652
                 channel_radius, scattering_radius)
653
        rm.parameters = parameters
7✔
654
        rm.angle_distribution = angle_distribution
7✔
655
        rm.num_l_convergence = num_l_convergence
7✔
656
        rm.atomic_weight_ratio = awri
7✔
657

658
        return rm
7✔
659

660
    def _prepare_resonances(self):
7✔
661
        df = self.parameters.copy()
×
662

663
        # Penetration and shift factors
664
        p = np.zeros(len(df))
×
665
        s = np.zeros(len(df))
×
666

667
        l_values = []
×
668
        lj_values = []
×
669

670
        A = self.atomic_weight_ratio
×
671
        for i, E, l, J, gn, gg, gfa, gfb in df.itertuples():
×
672
            if l not in l_values:
×
673
                l_values.append(l)
×
674
            if (l, abs(J)) not in lj_values:
×
675
                lj_values.append((l, abs(J)))
×
676

677
            # Determine penetration and shift corresponding to resonance energy
678
            k = wave_number(A, E)
×
679
            rho = k*self.channel_radius[l](E)
×
680
            p[i], s[i] = penetration_shift(l, rho)
×
681

682
        df['p'] = p
×
683
        df['s'] = s
×
684

685
        self._l_values = np.array(l_values)
×
686
        for (l, J) in lj_values:
×
687
            self._parameter_matrix[l, J] = df[(df.L == l) &
×
688
                                              (abs(df.J) == J)].values
689

690
        self._prepared = True
×
691

692

693
class RMatrixLimited(ResonanceRange):
7✔
694
    """R-matrix limited resolved resonance formalism data.
695

696
    R-matrix limited resolved resonance data is identified by LRF=7 in the
697
    ENDF-6 format.
698

699
    Parameters
700
    ----------
701
    energy_min : float
702
        Minimum energy of the resolved resonance range in eV
703
    energy_max : float
704
        Maximum energy of the resolved resonance range in eV
705
    particle_pairs : list of dict
706
        List of particle pairs. Each particle pair is represented by a
707
        dictionary that contains the mass, atomic number, spin, and parity of
708
        each particle as well as other characteristics.
709
    spin_groups : list of dict
710
        List of spin groups. Each spin group is characterized by channels,
711
        resonance energies, and resonance widths.
712

713
    Attributes
714
    ----------
715
    reduced_width : bool
716
        Flag indicating whether channel widths in eV or reduced-width amplitudes
717
        in eV^1/2 are given
718
    formalism : int
719
        Flag to specify which formulae for the R-matrix are to be used
720
    particle_pairs : list of dict
721
        List of particle pairs. Each particle pair is represented by a
722
        dictionary that contains the mass, atomic number, spin, and parity of
723
        each particle as well as other characteristics.
724
    spin_groups : list of dict
725
        List of spin groups. Each spin group is characterized by channels,
726
        resonance energies, and resonance widths.
727

728
    """
729

730
    def __init__(self, energy_min, energy_max, particle_pairs, spin_groups):
7✔
731
        super().__init__(0.0, energy_min, energy_max, None, None)
7✔
732
        self.reduced_width = False
7✔
733
        self.formalism = 3
7✔
734
        self.particle_pairs = particle_pairs
7✔
735
        self.spin_groups = spin_groups
7✔
736

737
    @classmethod
7✔
738
    def from_endf(cls, ev, file_obj, items):
7✔
739
        """Read R-Matrix limited resonance data from an ENDF evaluation.
740

741
        Parameters
742
        ----------
743
        ev : openmc.data.endf.Evaluation
744
            ENDF evaluation
745
        file_obj : file-like object
746
            ENDF file positioned at the second record of a resonance range
747
            subsection in MF=2, MT=151
748
        items : list
749
            Items from the CONT record at the start of the resonance range
750
            subsection
751

752
        Returns
753
        -------
754
        openmc.data.RMatrixLimited
755
            R-matrix limited resonance parameters
756

757
        """
758
        energy_min, energy_max = items[0:2]
7✔
759

760
        items = get_cont_record(file_obj)
7✔
761
        reduced_width = (items[2] == 1)  # reduced width amplitude?
7✔
762
        formalism = items[3]  # Specify which formulae are used
7✔
763
        n_spin_groups = items[4]  # Number of Jpi values (NJS)
7✔
764

765
        particle_pairs = []
7✔
766
        spin_groups = []
7✔
767

768
        items, values = get_list_record(file_obj)
7✔
769
        n_pairs = items[5]//2  # Number of particle pairs (NPP)
7✔
770
        for i in range(n_pairs):
7✔
771
            first = {'mass': values[12*i],
7✔
772
                     'z': int(values[12*i + 2]),
773
                     'spin': values[12*i + 4],
774
                     'parity': values[12*i + 10]}
775
            second = {'mass': values[12*i + 1],
7✔
776
                      'z': int(values[12*i + 3]),
777
                      'spin': values[12*i + 5],
778
                      'parity': values[12*i + 11]}
779

780
            q_value = values[12*i + 6]
7✔
781
            penetrability = values[12*i + 7]
7✔
782
            shift = values[12*i + 8]
7✔
783
            mt = int(values[12*i + 9])
7✔
784

785
            particle_pairs.append(ParticlePair(
7✔
786
                first, second, q_value, penetrability, shift, mt))
787

788
        # loop over spin groups
789
        for i in range(n_spin_groups):
7✔
790
            items, values = get_list_record(file_obj)
7✔
791
            J = items[0]
7✔
792
            if J == 0.0:
7✔
793
                parity = '+' if items[1] == 1.0 else '-'
7✔
794
            else:
795
                parity = '+' if J > 0. else '-'
7✔
796
                J = abs(J)
7✔
797
            kbk = items[2]
7✔
798
            kps = items[3]
7✔
799
            n_channels = items[5]
7✔
800
            channels = []
7✔
801
            for j in range(n_channels):
7✔
802
                channel = {}
7✔
803
                channel['particle_pair'] = particle_pairs[
7✔
804
                    int(values[6*j]) - 1]
805
                channel['l'] = values[6*j + 1]
7✔
806
                channel['spin'] = values[6*j + 2]
7✔
807
                channel['boundary'] = values[6*j + 3]
7✔
808
                channel['effective_radius'] = values[6*j + 4]
7✔
809
                channel['true_radius'] = values[6*j + 5]
7✔
810
                channels.append(channel)
7✔
811

812
            # Read resonance energies and widths
813
            items, values = get_list_record(file_obj)
7✔
814
            n_resonances = items[3]
7✔
815
            records = []
7✔
816
            m = n_channels//6 + 1
7✔
817
            for j in range(n_resonances):
7✔
818
                energy = values[6*m*j]
7✔
819
                records.append([energy] + [values[6*m*j + k + 1]
7✔
820
                                           for k in range(n_channels)])
821

822
            # Determine column names
823
            columns = ['energy']
7✔
824
            for channel in channels:
7✔
825
                mt = channel['particle_pair'].mt
7✔
826
                if mt == 2:
7✔
827
                    columns.append('neutronWidth')
7✔
828
                elif mt == 18:
7✔
829
                    columns.append('fissionWidth')
×
830
                elif mt == 102:
7✔
831
                    columns.append('captureWidth')
7✔
832
                else:
833
                    columns.append(f'width (MT={mt})')
7✔
834

835
            # Create Pandas dataframe with resonance parameters
836
            parameters = pd.DataFrame.from_records(records, columns=columns)
7✔
837

838
            # Construct SpinGroup instance and add to list
839
            sg = SpinGroup(J, parity, channels, parameters)
7✔
840
            spin_groups.append(sg)
7✔
841

842
            # Optional extension (Background R-Matrix)
843
            if kbk > 0:
7✔
844
                items, values = get_list_record(file_obj)
×
845
                lbk = items[4]
×
846
                if lbk == 1:
×
847
                    params, rbr = get_tab1_record(file_obj)
×
848
                    params, rbi = get_tab1_record(file_obj)
×
849

850
            # Optional extension (Tabulated phase shifts)
851
            if kps > 0:
7✔
852
                items, values = get_list_record(file_obj)
×
853
                lps = items[4]
×
854
                if lps == 1:
×
855
                    params, psr = get_tab1_record(file_obj)
×
856
                    params, psi = get_tab1_record(file_obj)
×
857

858
        rml = cls(energy_min, energy_max, particle_pairs, spin_groups)
7✔
859
        rml.reduced_width = reduced_width
7✔
860
        rml.formalism = formalism
7✔
861

862
        return rml
7✔
863

864

865
class ParticlePair:
7✔
866
    def __init__(self, first, second, q_value, penetrability,
7✔
867
                 shift, mt):
868
        self.first = first
7✔
869
        self.second = second
7✔
870
        self.q_value = q_value
7✔
871
        self.penetrability = penetrability
7✔
872
        self.shift = shift
7✔
873
        self.mt = mt
7✔
874

875

876
class SpinGroup:
7✔
877
    """Resonance spin group
878

879
    Attributes
880
    ----------
881
    spin : float
882
        Total angular momentum (nuclear spin)
883
    parity : {'+', '-'}
884
        Even (+) or odd(-) parity
885
    channels : list of openmc.data.Channel
886
        Available channels
887
    parameters : pandas.DataFrame
888
        Energies/widths for each resonance/channel
889

890
    """
891

892
    def __init__(self, spin, parity, channels, parameters):
7✔
893
        self.spin = spin
7✔
894
        self.parity = parity
7✔
895
        self.channels = channels
7✔
896
        self.parameters = parameters
7✔
897

898
    def __repr__(self):
7✔
899
        return f'<SpinGroup: Jpi={self.spin}{self.parity}>'
×
900

901

902
class Unresolved(ResonanceRange):
7✔
903
    """Unresolved resonance parameters as identified by LRU=2 in MF=2.
904

905
    Parameters
906
    ----------
907
    target_spin : float
908
        Intrinsic spin, :math:`I`, of the target nuclide
909
    energy_min : float
910
        Minimum energy of the unresolved resonance range in eV
911
    energy_max : float
912
        Maximum energy of the unresolved resonance range in eV
913
    channel : openmc.data.Function1D
914
        Channel radii as a function of energy
915
    scattering : openmc.data.Function1D
916
        Scattering radii as a function of energy
917

918
    Attributes
919
    ----------
920
    add_to_background : bool
921
        If True, file 3 contains partial cross sections to be added to the
922
        average unresolved cross sections calculated from parameters.
923
    atomic_weight_ratio : float
924
        Atomic weight ratio of the target nuclide
925
    channel_radius : openmc.data.Function1D
926
        Channel radii as a function of energy
927
    energies : Iterable of float
928
        Energies at which parameters are tabulated
929
    energy_max : float
930
        Maximum energy of the unresolved resonance range in eV
931
    energy_min : float
932
        Minimum energy of the unresolved resonance range in eV
933
    parameters : list of pandas.DataFrame
934
        Average resonance parameters at each energy
935
    scattering_radius : openmc.data.Function1D
936
        Scattering radii as a function of energy
937
    target_spin : float
938
        Intrinsic spin, :math:`I`, of the target nuclide
939

940
    """
941

942
    def __init__(self, target_spin, energy_min, energy_max, channel, scattering):
7✔
943
        super().__init__(target_spin, energy_min, energy_max, channel,
7✔
944
                         scattering)
945
        self.energies = None
7✔
946
        self.parameters = None
7✔
947
        self.add_to_background = False
7✔
948
        self.atomic_weight_ratio = None
7✔
949

950
    @classmethod
7✔
951
    def from_endf(cls, file_obj, items, fission_widths):
7✔
952
        """Read unresolved resonance data from an ENDF evaluation.
953

954
        Parameters
955
        ----------
956
        file_obj : file-like object
957
            ENDF file positioned at the second record of a resonance range
958
            subsection in MF=2, MT=151
959
        items : list
960
            Items from the CONT record at the start of the resonance range
961
            subsection
962
        fission_widths : bool
963
            Whether fission widths are given
964

965
        Returns
966
        -------
967
        openmc.data.Unresolved
968
            Unresolved resonance region parameters
969

970
        """
971
        # Read energy-dependent scattering radius if present
972
        energy_min, energy_max = items[0:2]
7✔
973
        nro, naps = items[4:6]
7✔
974
        if nro != 0:
7✔
975
            params, ape = get_tab1_record(file_obj)
×
976

977
        # Get SPI, AP, and LSSF
978
        formalism = items[3]
7✔
979
        if not (fission_widths and formalism == 1):
7✔
980
            items = get_cont_record(file_obj)
7✔
981
            target_spin = items[0]
7✔
982
            if nro == 0:
7✔
983
                ap = Polynomial((items[1],))
7✔
984
            add_to_background = (items[2] == 0)
7✔
985

986
        if not fission_widths and formalism == 1:
7✔
987
            # Case A -- fission widths not given, all parameters are
988
            # energy-independent
989
            NLS = items[4]
×
990
            columns = ['L', 'J', 'd', 'amun', 'gn0', 'gg']
×
991
            records = []
×
992
            for ls in range(NLS):
×
993
                items, values = get_list_record(file_obj)
×
994
                awri = items[0]
×
995
                l = items[2]
×
996
                NJS = items[5]
×
997
                for j in range(NJS):
×
998
                    d, j, amun, gn0, gg = values[6*j:6*j + 5]
×
999
                    records.append([l, j, d, amun, gn0, gg])
×
1000
            parameters = pd.DataFrame.from_records(records, columns=columns)
×
1001
            energies = None
×
1002

1003
        elif fission_widths and formalism == 1:
7✔
1004
            # Case B -- fission widths given, only fission widths are
1005
            # energy-dependent
1006
            items, energies = get_list_record(file_obj)
×
1007
            target_spin = items[0]
×
1008
            if nro == 0:
×
1009
                ap = Polynomial((items[1],))
×
1010
            add_to_background = (items[2] == 0)
×
1011
            NE, NLS = items[4:6]
×
1012
            records = []
×
1013
            columns = ['L', 'J', 'E', 'd', 'amun', 'amuf', 'gn0', 'gg', 'gf']
×
1014
            for ls in range(NLS):
×
1015
                items = get_cont_record(file_obj)
×
1016
                awri = items[0]
×
1017
                l = items[2]
×
1018
                NJS = items[4]
×
1019
                for j in range(NJS):
×
1020
                    items, values = get_list_record(file_obj)
×
1021
                    muf = items[3]
×
1022
                    d = values[0]
×
1023
                    j = values[1]
×
1024
                    amun = values[2]
×
1025
                    gn0 = values[3]
×
1026
                    gg = values[4]
×
1027
                    gfs = values[6:]
×
1028
                    for E, gf in zip(energies, gfs):
×
1029
                        records.append([l, j, E, d, amun, muf, gn0, gg, gf])
×
1030
            parameters = pd.DataFrame.from_records(records, columns=columns)
×
1031

1032
        elif formalism == 2:
7✔
1033
            # Case C -- all parameters are energy-dependent
1034
            NLS = items[4]
7✔
1035
            columns = ['L', 'J', 'E', 'd', 'amux', 'amun', 'amuf', 'gx', 'gn0',
7✔
1036
                       'gg', 'gf']
1037
            records = []
7✔
1038
            for ls in range(NLS):
7✔
1039
                items = get_cont_record(file_obj)
7✔
1040
                awri = items[0]
7✔
1041
                l = items[2]
7✔
1042
                NJS = items[4]
7✔
1043
                for j in range(NJS):
7✔
1044
                    items, values = get_list_record(file_obj)
7✔
1045
                    ne = items[5]
7✔
1046
                    j = items[0]
7✔
1047
                    amux = values[2]
7✔
1048
                    amun = values[3]
7✔
1049
                    amuf = values[5]
7✔
1050
                    energies = []
7✔
1051
                    for k in range(1, ne + 1):
7✔
1052
                        E = values[6*k]
7✔
1053
                        d = values[6*k + 1]
7✔
1054
                        gx = values[6*k + 2]
7✔
1055
                        gn0 = values[6*k + 3]
7✔
1056
                        gg = values[6*k + 4]
7✔
1057
                        gf = values[6*k + 5]
7✔
1058
                        energies.append(E)
7✔
1059
                        records.append([l, j, E, d, amux, amun, amuf, gx, gn0,
7✔
1060
                                        gg, gf])
1061
            parameters = pd.DataFrame.from_records(records, columns=columns)
7✔
1062

1063
        # Calculate channel radius from ENDF-102 equation D.14
1064
        a = Polynomial((0.123 * (NEUTRON_MASS*awri)**(1./3.) + 0.08,))
7✔
1065

1066
        # Determine scattering and channel radius
1067
        if nro == 0:
7✔
1068
            scattering_radius = ap
7✔
1069
            if naps == 0:
7✔
1070
                channel_radius = a
7✔
1071
            elif naps == 1:
×
1072
                channel_radius = ap
×
1073
        elif nro == 1:
×
1074
            scattering_radius = ape
×
1075
            if naps == 0:
×
1076
                channel_radius = a
×
1077
            elif naps == 1:
×
1078
                channel_radius = ape
×
1079
            elif naps == 2:
×
1080
                channel_radius = ap
×
1081

1082
        urr = cls(target_spin, energy_min, energy_max, channel_radius,
7✔
1083
                  scattering_radius)
1084
        urr.parameters = parameters
7✔
1085
        urr.add_to_background = add_to_background
7✔
1086
        urr.atomic_weight_ratio = awri
7✔
1087
        urr.energies = energies
7✔
1088

1089
        return urr
7✔
1090

1091

1092
_FORMALISMS = {0: ResonanceRange,
7✔
1093
               1: SingleLevelBreitWigner,
1094
               2: MultiLevelBreitWigner,
1095
               3: ReichMoore,
1096
               7: RMatrixLimited}
1097

1098
_RESOLVED = (SingleLevelBreitWigner, MultiLevelBreitWigner,
7✔
1099
             ReichMoore, RMatrixLimited)
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