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

openmc-dev / openmc / 21509323874

30 Jan 2026 08:24AM UTC coverage: 81.978% (+0.03%) from 81.953%
21509323874

Pull #3758

github

web-flow
Merge 69ea7015d into 7b4617aff
Pull Request #3758: add the capability to operate with photon cross sections in the plotter.py module

17257 of 24017 branches covered (71.85%)

Branch coverage included in aggregate %.

88 of 127 new or added lines in 2 files covered. (69.29%)

44 existing lines in 2 files now uncovered.

55810 of 65113 relevant lines covered (85.71%)

43956842.08 hits per line

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

57.7
/openmc/plotter.py
1
from __future__ import annotations
11✔
2
from itertools import chain
11✔
3
from numbers import Integral, Real
11✔
4
from typing import Dict, Iterable, List
11✔
5

6
import numpy as np
11✔
7

8
import openmc.checkvalue as cv
11✔
9
import openmc.data
11✔
10

11
# Supported keywords for continuous-energy cross section plotting
12
PLOT_TYPES = {'total', 'scatter', 'elastic', 'inelastic', 'fission',
11✔
13
              'absorption', 'capture', 'nu-fission', 'nu-scatter', 'unity',
14
              'slowing-down power', 'damage'}
15

16
# Supported keywords for multi-group cross section plotting
17
PLOT_TYPES_MGXS = {'total', 'absorption', 'scatter', 'fission',
11✔
18
                   'kappa-fission', 'nu-fission', 'prompt-nu-fission',
19
                   'deleyed-nu-fission', 'chi', 'chi-prompt', 'chi-delayed',
20
                   'inverse-velocity', 'beta', 'decay-rate', 'unity'}
21
# Create a dictionary which can be used to convert PLOT_TYPES_MGXS to the
22
# openmc.XSdata attribute name needed to access the data
23
_PLOT_MGXS_ATTR = {line: line.replace(' ', '_').replace('-', '_')
11✔
24
                   for line in PLOT_TYPES_MGXS}
25
_PLOT_MGXS_ATTR['scatter'] = 'scatter_matrix'
11✔
26

27
# Special MT values
28
UNITY_MT = -1
11✔
29
XI_MT = -2
11✔
30

31
# MTs to combine to generate associated plot_types
32
_INELASTIC = [mt for mt in openmc.data.SUM_RULES[3] if mt != 27]
11✔
33
PLOT_TYPES_MT = {
11✔
34
    'total': openmc.data.SUM_RULES[1],
35
    'scatter': [2] + _INELASTIC,
36
    'elastic': [2],
37
    'inelastic': _INELASTIC,
38
    'fission': [18],
39
    'absorption': [27],
40
    'capture': [101],
41
    'nu-fission': [18],
42
    'nu-scatter': [2] + _INELASTIC,
43
    'unity': [UNITY_MT],
44
    'slowing-down power': [2] + [XI_MT],
45
    'damage': [444]
46
}
47

48
# Types of plots to plot linearly in y
49
PLOT_TYPES_LINEAR = {'nu-fission / fission', 'nu-scatter / scatter',
11✔
50
                     'nu-fission / absorption', 'fission / absorption'}
51

52
# Minimum and maximum energies for plotting (units of eV)
53
_MIN_E = 1.e-5
11✔
54
_MAX_E = 20.e6
11✔
55

56

57
ELEMENT_NAMES = list(openmc.data.ELEMENT_SYMBOL.values())[1:]
11✔
58

59

60
def _get_legend_label(this, type):
11✔
61
    """Gets a label for the element or nuclide or material and reaction plotted"""
62
    if isinstance(this, str):
11✔
63
        if type in openmc.data.DADZ:
11✔
64
            if this in ELEMENT_NAMES:
11✔
65
                return f'{this} {type}'
11✔
66
            else:  # this is a nuclide so the legend can contain more information
67
                z, a, m = openmc.data.zam(this)
11✔
68
                da, dz = openmc.data.DADZ[type]
11✔
69
                gnds_name = openmc.data.gnds_name(z + dz, a + da, m)
11✔
70
                # makes a string with nuclide reaction and new nuclide
71
                # For example "Be9 (n,2n) Be8"
72
                return f'{this} {type} {gnds_name}'
11✔
73
        return f'{this} {type}'
11✔
74
    elif this.name == '':
11✔
75
        return f'Material {this.id} {type}'
11✔
76
    else:
77
        return f'{this.name} {type}'
×
78

79

80
def _get_yaxis_label(reactions, divisor_types):
11✔
81
    """Gets a y axis label for the type of data plotted"""
82

83
    heat_values = {"heating", "heating-local", "damage-energy"}
11✔
84

85
    # if all the types are heating a different stem and unit is needed
86
    if all(set(value).issubset(heat_values) for value in reactions.values()):
11✔
87
        stem = "Heating"
11✔
88
    elif all(isinstance(item, str) for item in reactions.keys()):
11✔
89
        for nuc_reactions in reactions.values():
11✔
90
            for reaction in nuc_reactions:
11✔
91
                if reaction in heat_values:
11✔
92
                    raise TypeError(
11✔
93
                        "Mixture of heating and Microscopic reactions. "
94
                        "Invalid type for plotting"
95
                    )
96
        stem = "Microscopic"
11✔
97
    elif all(isinstance(item, openmc.Material) for item in reactions.keys()):
11✔
98
        stem = 'Macroscopic'
11✔
99
    else:
100
        msg = "Mixture of openmc.Material and elements/nuclides. Invalid type for plotting"
11✔
101
        raise TypeError(msg)
11✔
102

103
    if divisor_types:
11✔
104
        mid, units = "Data", ""
×
105
    else:
106
        mid = "Cross Section"
11✔
107
        units = {
11✔
108
            "Macroscopic": "[1/cm]",
109
            "Microscopic": "[b]",
110
            "Heating": "[eV-barn]",
111
        }[stem]
112

113
    return f'{stem} {mid} {units}'
11✔
114

115
def _get_title(reactions):
11✔
116
    """Gets a title for the type of data plotted"""
117
    if len(reactions) == 1:
11✔
118
        this, = reactions
11✔
119
        name = this.name if isinstance(this, openmc.Material) else this
11✔
120
        return f'Cross Section Plot For {name}'
11✔
121
    else:
122
        return 'Cross Section Plot'
11✔
123

124

125
def plot_xs(
11✔
126
    reactions: Dict[str | openmc.Material, List[str]],
127
    divisor_types: Iterable[str] | None = None,
128
    temperature: float = 294.0,
129
    axis: "plt.Axes" | None = None,
130
    sab_name: str | None = None,
131
    incident_particle: str = 'neutron',
132
    ce_cross_sections: str | None = None,
133
    mg_cross_sections: str | None = None,
134
    enrichment: float | None = None,
135
    plot_CE: bool = True,
136
    orders: Iterable[int] | None = None,
137
    divisor_orders: Iterable[int] | None = None,
138
    energy_axis_units: str = "eV",
139
    **kwargs,
140
) -> "plt.Figure" | None:
141
    """Creates a figure of continuous-energy cross sections for this item.
142

143
    Parameters
144
    ----------
145
    reactions : dict
146
        keys can be either a nuclide or element in string form or an
147
        openmc.Material object. Values are a list of the types of
148
        cross sections to include in the plot.
149
    divisor_types : Iterable of values of PLOT_TYPES, optional
150
        Cross section types which will divide those produced by types
151
        before plotting. A type of 'unity' can be used to effectively not
152
        divide some types.
153
    temperature : float, optional
154
        Temperature in Kelvin to plot. If not specified, a default
155
        temperature of 294K will be plotted. Note that the nearest
156
        temperature in the library for each nuclide will be used as opposed
157
        to using any interpolation.
158
    axis : matplotlib.axes, optional
159
        A previously generated axis to use for plotting. If not specified,
160
        a new axis and figure will be generated.
161
    sab_name : str, optional
162
        Name of S(a,b) library to apply to MT=2 data when applicable.
163
    ce_cross_sections : str, optional
164
        Location of cross_sections.xml file. Default is None.
165
    mg_cross_sections : str, optional
166
        Location of MGXS HDF5 Library file. Default is None.
167
    enrichment : float, optional
168
        Enrichment for U235 in weight percent. For example, input 4.95 for
169
        4.95 weight percent enriched U. Default is None.
170
    plot_CE : bool, optional
171
        Denotes whether or not continuous-energy will be plotted. Defaults to
172
        plotting the continuous-energy data.
173
    orders : Iterable of Integral, optional
174
        The scattering order or delayed group index to use for the
175
        corresponding entry in types. Defaults to the 0th order for scattering
176
        and the total delayed neutron data. This only applies to plots of
177
        multi-group data.
178
    divisor_orders : Iterable of Integral, optional
179
        Same as orders, but for divisor_types
180
    **kwargs :
181
        All keyword arguments are passed to
182
        :func:`matplotlib.pyplot.figure`.
183
    energy_axis_units : {'eV', 'keV', 'MeV'}
184
        Units used on the plot energy axis
185

186
        .. versionadded:: 0.15.0
187

188
    Returns
189
    -------
190
    fig : matplotlib.figure.Figure
191
        If axis is None, then a Matplotlib Figure of the generated
192
        cross section will be returned. Otherwise, a value of
193
        None will be returned as the figure and axes have already been
194
        generated.
195

196
    """
197
    import matplotlib.pyplot as plt
11✔
198

199
    cv.check_type("plot_CE", plot_CE, bool)
11✔
200
    cv.check_value("energy_axis_units", energy_axis_units, {"eV", "keV", "MeV"})
11✔
201

202
    axis_scaling_factor = {"eV": 1.0, "keV": 1e-3, "MeV": 1e-6}
11✔
203

204
    # Generate the plot
205
    if axis is None:
11✔
206
        fig, ax = plt.subplots(**kwargs)
11✔
207
    else:
208
        fig = None
×
209
        ax = axis
×
210

211
    all_types = []
11✔
212

213
    for this, types in reactions.items():
11✔
214
        all_types = all_types + types
11✔
215

216
        if plot_CE:
11✔
217
            cv.check_type("this", this, (str, openmc.Material))
11✔
218
            # Calculate for the CE cross sections
219
            E, data = calculate_cexs(this, types, incident_particle,temperature, sab_name,
11✔
220
                                    ce_cross_sections, enrichment)
221
            if divisor_types:
11✔
222
                cv.check_length('divisor types', divisor_types, len(types))
×
NEW
223
                Ediv, data_div = calculate_cexs(this, divisor_types, incident_particle, temperature,
×
224
                                                sab_name, ce_cross_sections,
225
                                                enrichment)
226

227
                # Create a new union grid, interpolate data and data_div on to that
228
                # grid, and then do the actual division
229
                Enum = E[:]
×
230
                E = np.union1d(Enum, Ediv)
×
231
                data_new = np.zeros((len(types), len(E)))
×
232

233
                for line in range(len(types)):
×
234
                    data_new[line, :] = \
×
235
                        np.divide(np.interp(E, Enum, data[line, :]),
236
                                np.interp(E, Ediv, data_div[line, :]))
237
                    if divisor_types[line] != 'unity':
×
238
                        types[line] = types[line] + ' / ' + divisor_types[line]
×
239
                data = data_new
×
240
        else:
241
            # Calculate for MG cross sections
242
            E, data = calculate_mgxs(this, types, orders, temperature,
×
243
                                    mg_cross_sections, ce_cross_sections,
244
                                    enrichment)
245

246
            if divisor_types:
×
247
                cv.check_length('divisor types', divisor_types, len(types))
×
248
                Ediv, data_div = calculate_mgxs(this, divisor_types,
×
249
                                                divisor_orders, temperature,
250
                                                mg_cross_sections,
251
                                                ce_cross_sections, enrichment)
252

253
                # Perform the division
254
                for line in range(len(types)):
×
255
                    data[line, :] /= data_div[line, :]
×
256
                    if divisor_types[line] != 'unity':
×
257
                        types[line] += ' / ' + divisor_types[line]
×
258

259
        E *= axis_scaling_factor[energy_axis_units]
11✔
260

261
        # Plot the data
262
        for i in range(len(data)):
11✔
263
            data[i, :] = np.nan_to_num(data[i, :])
11✔
264
            if np.sum(data[i, :]) > 0.:
11✔
265
                ax.plot(E, data[i, :], label=_get_legend_label(this, types[i]))
11✔
266

267
    # Set to loglog or semilogx depending on if we are plotting a data
268
    # type which we expect to vary linearly
269
    if set(all_types).issubset(PLOT_TYPES_LINEAR):
11✔
270
        ax.set_xscale('log')
×
271
        ax.set_yscale('linear')
×
272
    else:
273
        ax.set_xscale('log')
11✔
274
        ax.set_yscale('log')
11✔
275

276
    ax.set_xlabel(f"Energy [{energy_axis_units}]")
11✔
277
    if plot_CE:
11✔
278
        ax.set_xlim(
11✔
279
            _MIN_E * axis_scaling_factor[energy_axis_units],
280
            _MAX_E * axis_scaling_factor[energy_axis_units],
281
        )
282
    else:
283
        ax.set_xlim(E[-1], E[0])
×
284

285
    ax.set_ylabel(_get_yaxis_label(reactions, divisor_types))
11✔
286
    ax.legend(loc='best')
11✔
287
    ax.set_title(_get_title(reactions))
11✔
288

289
    return fig
11✔
290

291

292
def calculate_cexs(this, types, incident_particle='neutron', temperature=294., sab_name=None,
11✔
293
                   cross_sections=None, enrichment=None, ncrystal_cfg=None):
294
    """Calculates continuous-energy cross sections of a requested type.
295

296
    Parameters
297
    ----------
298
    this : str or openmc.Material
299
        Object to source data from. Nuclides and elements should be input as a
300
        str
301
    types : Iterable of values of PLOT_TYPES
302
        The type of cross sections to calculate
303
    incident_particle : str
304
        The incident particle used to fetch the appropriate library.
305
        Can be only 'neutron' or 'photon'.
306
    temperature : float, optional
307
        Temperature in Kelvin to plot. If not specified, a default
308
        temperature of 294K will be plotted. Note that the nearest
309
        temperature in the library for each nuclide will be used as opposed
310
        to using any interpolation.
311
    sab_name : str, optional
312
        Name of S(a,b) library to apply to MT=2 data when applicable.
313
    cross_sections : str, optional
314
        Location of cross_sections.xml file. Default is None.
315
    enrichment : float, optional
316
        Enrichment for U235 in weight percent. For example, input 4.95 for
317
        4.95 weight percent enriched U. Default is None
318
        (natural composition).
319
    ncrystal_cfg : str, optional
320
        Configuration string for NCrystal material.
321

322
    Returns
323
    -------
324
    energy_grid : numpy.ndarray
325
        Energies at which cross sections are calculated, in units of eV
326
    data : numpy.ndarray
327
        Cross sections calculated at the energy grid described by energy_grid
328

329
    """
330

331
    # Check types
332
    cv.check_type('this', this, (str, openmc.Material))
11✔
333
    cv.check_type('temperature', temperature, Real)
11✔
334
    cv.check_value("incident particle", incident_particle, ['neutron', 'photon'])
11✔
335
    if sab_name:
11✔
336
        cv.check_type('sab_name', sab_name, str)
11✔
337
    if enrichment:
11✔
338
        cv.check_type('enrichment', enrichment, Real)
×
339

340
    if isinstance(this, str):
11✔
341
        if this in ELEMENT_NAMES:
11✔
342
            energy_grid, data = _calculate_cexs_elem_mat(
11✔
343
                this, types, incident_particle, temperature, cross_sections, sab_name, enrichment
344
            )
345
        else:
346
            energy_grid, xs = _calculate_cexs_nuclide(
11✔
347
                this, types, incident_particle, temperature, sab_name, cross_sections,
348
                ncrystal_cfg
349
            )
350

351
            # Convert xs (Iterable of Callable) to a grid of cross section values
352
            # calculated on the points in energy_grid for consistency with the
353
            # element and material functions.
354
            data = np.zeros((len(types), len(energy_grid)))
11✔
355
            for line in range(len(types)):
11✔
356
                data[line, :] = xs[line](energy_grid)
11✔
357
    else:
358
        energy_grid, data = _calculate_cexs_elem_mat(this, types, incident_particle, temperature,
11✔
359
                                                     cross_sections)
360

361
    return energy_grid, data
11✔
362

363

364
def _calculate_cexs_nuclide(this, types, incident_particle='neutron', temperature=294., sab_name=None,
11✔
365
                            cross_sections=None, ncrystal_cfg=None):
366
    """Calculates continuous-energy cross sections of a requested type.
367

368
    Parameters
369
    ----------
370
    this : str
371
        Nuclide object to source data from
372
    types : Iterable of str or Integral
373
        The type of cross sections to calculate; values can either be those
374
        in openmc.PLOT_TYPES or keys from openmc.data.REACTION_MT which
375
        correspond to a reaction description e.g '(n,2n)' or integers which
376
        correspond to reaction channel (MT) numbers.
377
    incident_particle : str
378
        The incident particle used to fetch the appropriate library.
379
        Can be only 'neutron' or 'photon'.
380
    temperature : float, optional
381
        Temperature in Kelvin to plot. If not specified, a default
382
        temperature of 294K will be plotted. Note that the nearest
383
        temperature in the library for each nuclide will be used as opposed
384
        to using any interpolation.
385
    sab_name : str, optional
386
        Name of S(a,b) library to apply to MT=2 data when applicable.
387
    cross_sections : str, optional
388
        Location of cross_sections.xml file. Default is None.
389
    ncrystal_cfg : str, optional
390
        Configuration string for NCrystal material.
391

392
    Returns
393
    -------
394
    energy_grid : numpy.ndarray
395
        Energies at which cross sections are calculated, in units of eV
396
    data : Iterable of Callable
397
        Requested cross section functions
398

399
    """
400

401
    # Load the library
402
    library = openmc.data.DataLibrary.from_xml(cross_sections)
11✔
403
    if incident_particle == 'photon':
11✔
404
        try:
11✔
405
            z = openmc.data.zam(this)[0]
11✔
406
            nuclide = openmc.data.ATOMIC_SYMBOL[z]
11✔
NEW
407
        except (ValueError, KeyError, TypeError):
×
NEW
408
            if this not in openmc.data.ELEMENT_SYMBOL.values():
×
NEW
409
                raise ValueError(f"Element '{this}' not found in ELEMENT_SYMBOL.")
×
NEW
410
            nuclide = this
×
411
    else:
412
        nuclide = this
11✔
413
    lib = library.get_by_material(nuclide, data_type=incident_particle)
11✔
414
    if lib is None:
11✔
NEW
415
        raise ValueError(this + " not in library")
×
416

417
    # Convert temperature to format needed for access in the library
418
    strT = f"{int(round(temperature))}K"
11✔
419
    T = temperature
11✔
420

421
    # Now we can create the data sets to be plotted
422
    energy_grid = []
11✔
423
    xs = []
11✔
424

425
    if incident_particle == 'neutron':
11✔
426
        nuc = openmc.data.IncidentNeutron.from_hdf5(lib['path'])
11✔
427
        # Obtain the nearest temperature
428
        if strT in nuc.temperatures:
11✔
429
            nucT = strT
11✔
430
        else:
431
            delta_T = np.array(nuc.kTs) - T * openmc.data.K_BOLTZMANN
×
432
            closest_index = np.argmin(np.abs(delta_T))
×
433
            nucT = nuc.temperatures[closest_index]
×
434

435
        # Prep S(a,b) data if needed
436
        if sab_name:
11✔
437
            sab = openmc.data.ThermalScattering.from_hdf5(
11✔
438
                library.get_by_material(sab_name, data_type='thermal')['path'])
439
            # Obtain the nearest temperature
440
            if strT in sab.temperatures:
11✔
441
                sabT = strT
×
442
            else:
443
                delta_T = np.array(sab.kTs) - T * openmc.data.K_BOLTZMANN
11✔
444
                closest_index = np.argmin(np.abs(delta_T))
11✔
445
                sabT = sab.temperatures[closest_index]
11✔
446

447
            # Create an energy grid composed the S(a,b) and the nuclide's grid
448
            grid = nuc.energy[nucT]
11✔
449
            sab_Emax = 0.
11✔
450
            sab_funcs = []
11✔
451
            if sab.elastic is not None:
11✔
452
                elastic = sab.elastic.xs[sabT]
×
453
                if isinstance(elastic, openmc.data.CoherentElastic):
×
454
                    grid = np.union1d(grid, elastic.bragg_edges)
×
455
                    if elastic.bragg_edges[-1] > sab_Emax:
×
456
                        sab_Emax = elastic.bragg_edges[-1]
×
457
                elif isinstance(elastic, openmc.data.Tabulated1D):
×
458
                    grid = np.union1d(grid, elastic.x)
×
459
                    if elastic.x[-1] > sab_Emax:
×
460
                        sab_Emax = elastic.x[-1]
×
461
                sab_funcs.append(elastic)
×
462
            if sab.inelastic is not None:
11✔
463
                inelastic = sab.inelastic.xs[sabT]
11✔
464
                grid = np.union1d(grid, inelastic.x)
11✔
465
                if inelastic.x[-1] > sab_Emax:
11✔
466
                    sab_Emax = inelastic.x[-1]
11✔
467
                sab_funcs.append(inelastic)
11✔
468
            energy_grid = grid
11✔
469
        else:
470
            energy_grid = nuc.energy[nucT]
11✔
471
    elif incident_particle == 'photon':
11✔
472
        nuc = openmc.data.IncidentPhoton.from_hdf5(lib['path'])
11✔
473
        if any(type(line) is not int for line in types):
11✔
474
            raise TypeError("Photon cross sections can only be requested "
11✔
475
                            "with integer MT numbers.")
476

477

478
    # Parse the types
479
    mts = []
11✔
480
    ops = []
11✔
481
    yields = []
11✔
482
    for line in types:
11✔
483
        if line in PLOT_TYPES:
11✔
484
            tmp_mts = [mtj for mti in PLOT_TYPES_MT[line] for mtj in
11✔
485
                        nuc.get_reaction_components(mti)]
486
            mts.append(tmp_mts)
11✔
487
            if line.startswith('nu'):
11✔
NEW
488
                yields.append(True)
×
489
            else:
490
                yields.append(False)
11✔
491
            if XI_MT in tmp_mts:
11✔
NEW
492
                ops.append((np.add,) * (len(tmp_mts) - 2) + (np.multiply,))
×
493
            else:
494
                ops.append((np.add,) * (len(tmp_mts) - 1))
11✔
495
        elif line in openmc.data.REACTION_MT:
11✔
496
            mt_number = openmc.data.REACTION_MT[line]
11✔
497
            cv.check_type('MT in types', mt_number, Integral)
11✔
498
            cv.check_greater_than('MT in types', mt_number, 0)
11✔
499
            tmp_mts = nuc.get_reaction_components(mt_number)
11✔
500
            mts.append(tmp_mts)
11✔
501
            ops.append((np.add,) * (len(tmp_mts) - 1))
11✔
502
            yields.append(False)
11✔
503
        elif isinstance(line, int):
11✔
504
            # Not a built-in type, we have to parse it ourselves
505
            cv.check_type('MT in types', line, Integral)
11✔
506
            cv.check_greater_than('MT in types', line, 0)
11✔
507
            tmp_mts = nuc.get_reaction_components(line)
11✔
508
            mts.append(tmp_mts)
11✔
509
            ops.append((np.add,) * (len(tmp_mts) - 1))
11✔
510
            yields.append(False)
11✔
511
        else:
NEW
512
            raise TypeError("Invalid type", line)
×
513

514
    for i, mt_set in enumerate(mts):
11✔
515
        # Get the reaction xs data from the nuclide
516
        funcs = []
11✔
517
        op = ops[i]
11✔
518
        for mt in mt_set:
11✔
519
            if mt == 2:
11✔
520
                if sab_name:
11✔
521
                    # Then we need to do a piece-wise function of
522
                    # The S(a,b) and non-thermal data
523
                    sab_sum = openmc.data.Sum(sab_funcs)
11✔
524
                    pw_funcs = openmc.data.Regions1D(
11✔
525
                        [sab_sum, nuc[mt].xs[nucT]],
526
                        [sab_Emax])
527
                    funcs.append(pw_funcs)
11✔
528
                elif ncrystal_cfg:
11✔
NEW
529
                    import NCrystal
×
NEW
530
                    nc_scatter = NCrystal.createScatter(ncrystal_cfg)
×
NEW
531
                    nc_func = nc_scatter.xsect
×
NEW
532
                    nc_emax = 5 # eV # this should be obtained from NCRYSTAL_MAX_ENERGY
×
NEW
533
                    energy_grid = np.union1d(np.geomspace(min(energy_grid),
×
534
                                                            1.1*nc_emax,
535
                                                            1000),energy_grid) # NCrystal does not have
536
                                                                                # an intrinsic energy grid
NEW
537
                    pw_funcs = openmc.data.Regions1D(
×
538
                        [nc_func, nuc[mt].xs[nucT]],
539
                        [nc_emax])
NEW
540
                    funcs.append(pw_funcs)
×
541
                else:
542
                    funcs.append(nuc[mt].xs[nucT])
11✔
543
            elif mt in nuc:
11✔
544
                if yields[i]:
11✔
545
                    # Get the total yield first if available. This will be
546
                    # used primarily for fission.
NEW
547
                    for prod in chain(nuc[mt].products,
×
548
                                        nuc[mt].derived_products):
NEW
549
                        if prod.particle == 'neutron' and \
×
550
                            prod.emission_mode == 'total':
NEW
551
                            func = openmc.data.Combination(
×
552
                                [nuc[mt].xs[nucT], prod.yield_],
553
                                [np.multiply])
NEW
554
                            funcs.append(func)
×
NEW
555
                            break
×
556
                    else:
557
                        # Total doesn't exist so we have to create from
558
                        # prompt and delayed. This is used for scatter
559
                        # multiplication.
NEW
560
                        func = None
×
UNCOV
561
                        for prod in chain(nuc[mt].products,
×
562
                                            nuc[mt].derived_products):
563
                            if prod.particle == 'neutron' and \
×
564
                                prod.emission_mode != 'total':
NEW
565
                                if func:
×
NEW
566
                                    func = openmc.data.Combination(
×
567
                                        [prod.yield_, func], [np.add])
568
                                else:
NEW
569
                                    func = prod.yield_
×
NEW
570
                        if func:
×
NEW
571
                            funcs.append(openmc.data.Combination(
×
572
                                [func, nuc[mt].xs[nucT]], [np.multiply]))
573
                        else:
574
                            # If func is still None, then there were no
575
                            # products. In that case, assume the yield is
576
                            # one as its not provided for some summed
577
                            # reactions like MT=4
NEW
578
                            funcs.append(nuc[mt].xs[nucT])
×
579
                else:
580
                    # general MT that can called with 
581
                    # photons or neutrons
582
                    if incident_particle == 'photon':
11✔
583
                        temp_xs = nuc[mt].xs
11✔
584
                        energy_grid = np.union1d(energy_grid, temp_xs.x)
11✔
585
                    if incident_particle == 'neutron':
11✔
586
                        temp_xs = nuc[mt].xs[nucT]
11✔
587
                    funcs.append(temp_xs)
11✔
NEW
588
            elif mt == UNITY_MT:
×
NEW
589
                funcs.append(lambda x: 1.)
×
NEW
590
            elif mt == XI_MT:
×
NEW
591
                awr = nuc.atomic_weight_ratio
×
NEW
592
                alpha = ((awr - 1.) / (awr + 1.))**2
×
NEW
593
                xi = 1. + alpha * np.log(alpha) / (1. - alpha)
×
NEW
594
                funcs.append(lambda x: xi)
×
595
            else:
NEW
596
                funcs.append(lambda x: 0.)
×
597
        funcs = funcs if funcs else [lambda x: 0.]
11✔
598
        xs.append(openmc.data.Combination(funcs, op))
11✔
599

600
    if  len(energy_grid) == 0:
11✔
601
        energy_grid = np.array([_MIN_E, _MAX_E], dtype=float)
11✔
602

603
    return energy_grid, xs
11✔
604

605

606
def _calculate_cexs_elem_mat(this, types, incident_particle='neutron', temperature=294.,
11✔
607
                             cross_sections=None, sab_name=None,
608
                             enrichment=None):
609
    """Calculates continuous-energy cross sections of a requested type.
610

611
    Parameters
612
    ----------
613
    this : openmc.Material or str
614
        Object to source data from. Element can be input as str
615
    types : Iterable of values of PLOT_TYPES
616
        The type of cross sections to calculate
617
    incident_particle : str
618
        The incident particle used to fetch the appropriate library.
619
        Can be only 'neutron' or 'photon'.
620
    temperature : float, optional
621
        Temperature in Kelvin to plot. If not specified, a default
622
        temperature of 294K will be plotted. Note that the nearest
623
        temperature in the library for each nuclide will be used as opposed
624
        to using any interpolation.
625
    cross_sections : str, optional
626
        Location of cross_sections.xml file. Default is None.
627
    sab_name : str, optional
628
        Name of S(a,b) library to apply to MT=2 data when applicable.
629
    enrichment : float, optional
630
        Enrichment for U235 in weight percent. For example, input 4.95 for
631
        4.95 weight percent enriched U. Default is None
632
        (natural composition).
633

634
    Returns
635
    -------
636
    energy_grid : numpy.ndarray
637
        Energies at which cross sections are calculated, in units of eV
638
    data : numpy.ndarray
639
        Cross sections calculated at the energy grid described by energy_grid
640

641
    """
642

643
    cv.check_value("incident particle", incident_particle, ['neutron', 'photon'])
11✔
644

645
    if isinstance(this, openmc.Material):
11✔
646
        if this.temperature is not None:
11✔
647
            T = this.temperature
×
648
        else:
649
            T = temperature
11✔
650
    else:
651
        T = temperature
11✔
652

653
    # Load the library
654
    library = openmc.data.DataLibrary.from_xml(cross_sections)
11✔
655

656
    ncrystal_cfg = None
11✔
657
    if isinstance(this, openmc.Material):
11✔
658
        # Expand elements in to nuclides with atomic densities
659
        nuc_fractions = this.get_nuclide_atom_densities()
11✔
660
        # Create a dict of [nuclide name] = nuclide object to carry forward
661
        # with a common nuclides format between openmc.Material and Elements
662
        nuclides = {nuclide: nuclide for nuclide in nuc_fractions}
11✔
663
        # Add NCrystal cfg string if it exists
664
        ncrystal_cfg = this.ncrystal_cfg
11✔
665
    else:
666
        # Expand elements in to nuclides with atomic densities
667
        nuclides = openmc.Element(this).expand(1., 'ao', enrichment=enrichment,
11✔
668
                               cross_sections=cross_sections)
669
        # For ease of processing split out the nuclide and its fraction
670
        nuc_fractions = {nuclide[0]: nuclide[1] for nuclide in nuclides}
11✔
671
        # Create a dict of [nuclide name] = nuclide object to carry forward
672
        # with a common nuclides format between openmc.Material and Elements
673
        nuclides = {nuclide[0]: nuclide[0] for nuclide in nuclides}
11✔
674

675
    sabs = {}
11✔
676
    if incident_particle == 'neutron':
11✔
677
        # Identify the nuclides which have S(a,b) data
678
        for nuclide in nuclides.items():
11✔
679
            sabs[nuclide[0]] = None
11✔
680
        if isinstance(this, openmc.Material):
11✔
681
            for mat_sab_name, _ in this._sab:
11✔
682
                sab = openmc.data.ThermalScattering.from_hdf5(
11✔
683
                    library.get_by_material(mat_sab_name, data_type='thermal')['path'])
684
                for nuc in sab.nuclides:
11✔
685
                    sabs[nuc] = mat_sab_name
11✔
686
        else:
687
            if sab_name:
11✔
NEW
688
                sab = openmc.data.ThermalScattering.from_hdf5(
×
689
                    library.get_by_material(sab_name, data_type='thermal')['path'])
NEW
690
                for nuc in sab.nuclides:
×
NEW
691
                    sabs[nuc] = sab_name
×
692

693
    # Now we can create the data sets to be plotted
694
    xs = {}
11✔
695
    E = []
11✔
696
    for nuclide in nuclides.items():
11✔
697
        name = nuclide[0]
11✔
698
        nuc = nuclide[1]
11✔
699
        nuc_sab_name = sabs.get(name)
11✔
700
        temp_E, temp_xs = calculate_cexs(nuc, types, incident_particle, T, nuc_sab_name, cross_sections,
11✔
701
                                         ncrystal_cfg=ncrystal_cfg
702
                                         )
703
        E.append(temp_E)
11✔
704
        # Since the energy grids are different, store the cross sections as
705
        # a tabulated function so they can be calculated on any grid needed.
706
        xs[name] = [openmc.data.Tabulated1D(temp_E, temp_xs[line])
11✔
707
                    for line in range(len(types))]
708

709
    # Condense the data for every nuclide
710
    # First create a union energy grid
711
    energy_grid = E[0]
11✔
712
    for grid in E[1:]:
11✔
713
        energy_grid = np.union1d(energy_grid, grid)
11✔
714

715
    # Now we can combine all the nuclidic data
716
    data = np.zeros((len(types), len(energy_grid)))
11✔
717
    for line in range(len(types)):
11✔
718
        if types[line] == 'unity':
11✔
719
            data[line, :] = 1.
×
720
        else:
721
            for nuclide in nuclides.items():
11✔
722
                name = nuclide[0]
11✔
723
                data[line, :] += (nuc_fractions[name] *
11✔
724
                                  xs[name][line](energy_grid))
725

726
    return energy_grid, data
11✔
727

728

729
def calculate_mgxs(this, types, orders=None, temperature=294.,
11✔
730
                   cross_sections=None, ce_cross_sections=None,
731
                   enrichment=None):
732
    """Calculates multi-group cross sections of a requested type.
733

734
    If the data for the nuclide or macroscopic object in the library is
735
    represented as angle-dependent data then this method will return the
736
    geometric average cross section over all angles.
737

738
    Parameters
739
    ----------
740
    this : str or openmc.Material
741
        Object to source data from. Nuclides and elements can be input as a str
742
    types : Iterable of values of PLOT_TYPES_MGXS
743
        The type of cross sections to calculate
744
    orders : Iterable of Integral, optional
745
        The scattering order or delayed group index to use for the
746
        corresponding entry in types. Defaults to the 0th order for scattering
747
        and the total delayed neutron data.
748
    temperature : float, optional
749
        Temperature in Kelvin to plot. If not specified, a default
750
        temperature of 294K will be plotted. Note that the nearest
751
        temperature in the library for each nuclide will be used as opposed
752
        to using any interpolation.
753
    cross_sections : str, optional
754
        Location of MGXS HDF5 Library file. Default is None.
755
    ce_cross_sections : str, optional
756
        Location of continuous-energy cross_sections.xml file. Default is None.
757
    enrichment : float, optional
758
        Enrichment for U235 in weight percent. For example, input 4.95 for
759
        4.95 weight percent enriched U. Default is None
760
        (natural composition).
761

762
    Returns
763
    -------
764
    energy_grid : numpy.ndarray
765
        Energies at which cross sections are calculated, in units of eV
766
    data : numpy.ndarray
767
        Cross sections calculated at the energy grid described by energy_grid
768

769
    """
770

771
    # Check types
772
    cv.check_type('temperature', temperature, Real)
×
773
    if enrichment:
×
774
        cv.check_type('enrichment', enrichment, Real)
×
775
    cv.check_iterable_type('types', types, str)
×
776

777
    cv.check_type("cross_sections", cross_sections, str)
×
778
    library = openmc.MGXSLibrary.from_hdf5(cross_sections)
×
779

780
    if this in ELEMENT_NAMES or isinstance(this, openmc.Material):
×
781
        mgxs = _calculate_mgxs_elem_mat(this, types, library, orders,
×
782
                                        temperature, ce_cross_sections,
783
                                        enrichment)
784
    elif isinstance(this, str):
×
785
        mgxs = _calculate_mgxs_nuc_macro(this, types, library, orders,
×
786
                                         temperature)
787
    else:
788
        raise TypeError("Invalid type")
×
789

790
    # Convert the data to the format needed
791
    data = np.zeros((len(types), 2 * library.energy_groups.num_groups))
×
792
    energy_grid = np.zeros(2 * library.energy_groups.num_groups)
×
793
    for g in range(library.energy_groups.num_groups):
×
794
        energy_grid[g * 2: g * 2 + 2] = \
×
795
            library.energy_groups.group_edges[g: g + 2]
796
    # Ensure the energy will show on a log-axis by replacing 0s with a
797
    # sufficiently small number
798
    energy_grid[0] = max(energy_grid[0], _MIN_E)
×
799

800
    for line in range(len(types)):
×
801
        for g in range(library.energy_groups.num_groups):
×
802
            data[line, g * 2: g * 2 + 2] = mgxs[line, g]
×
803

804
    return energy_grid[::-1], data
×
805

806

807
def _calculate_mgxs_nuc_macro(this, types, library, orders=None,
11✔
808
                              temperature=294.):
809
    """Determines the multi-group cross sections of a nuclide or macroscopic
810
    object.
811

812
    If the data for the nuclide or macroscopic object in the library is
813
    represented as angle-dependent data then this method will return the
814
    geometric average cross section over all angles.
815

816
    Parameters
817
    ----------
818
    this : str
819
        Object to source data from
820
    types : Iterable of str
821
        The type of cross sections to calculate; values can either be those
822
        in openmc.PLOT_TYPES_MGXS
823
    library : openmc.MGXSLibrary
824
        MGXS Library containing the data of interest
825
    orders : Iterable of Integral, optional
826
        The scattering order or delayed group index to use for the
827
        corresponding entry in types. Defaults to the 0th order for scattering
828
        and the total delayed neutron data.
829
    temperature : float, optional
830
        Temperature in Kelvin to plot. If not specified, a default
831
        temperature of 294K will be plotted. Note that the nearest
832
        temperature in the library for each nuclide will be used as opposed
833
        to using any interpolation.
834

835
    Returns
836
    -------
837
    data : numpy.ndarray
838
        Cross sections calculated at the energy grid described by energy_grid
839

840
    """
841

842
    # Check the parameters and grab order/delayed groups
843
    if orders:
×
844
        cv.check_iterable_type('orders', orders, Integral,
×
845
                               min_depth=len(types), max_depth=len(types))
846
    else:
847
        orders = [None] * len(types)
×
848
    for i, line in enumerate(types):
×
849
        cv.check_type("line", line, str)
×
850
        cv.check_value("line", line, PLOT_TYPES_MGXS)
×
851
        if orders[i]:
×
852
            cv.check_greater_than("order value", orders[i], 0, equality=True)
×
853

854
    xsdata = library.get_by_name(this)
×
855

856
    if xsdata is not None:
×
857
        # Obtain the nearest temperature
858
        t = np.abs(xsdata.temperatures - temperature).argmin()
×
859

860
        # Get the data
861
        data = np.zeros((len(types), library.energy_groups.num_groups))
×
862
        for i, line in enumerate(types):
×
863
            if 'fission' in line and not xsdata.fissionable:
×
864
                continue
×
865
            elif line == 'unity':
×
866
                data[i, :] = 1.
×
867
            else:
868
                # Now we have to get the cross section data and properly
869
                # treat it depending on the requested type.
870
                # First get the data in a generic fashion
871
                temp_data = getattr(xsdata, _PLOT_MGXS_ATTR[line])[t]
×
872
                shape = temp_data.shape[:]
×
873
                # If we have angular data, then want the geometric
874
                # average over all provided angles.  Since the angles are
875
                # equi-distant, un-weighted averaging will suffice
876
                if xsdata.representation == 'angle':
×
877
                    temp_data = np.mean(temp_data, axis=(0, 1))
×
878

879
                # Now we can look at the shape of the data to identify how
880
                # it should be modified to produce an array of values
881
                # with groups.
882
                if shape in (xsdata.xs_shapes["[G']"],
×
883
                             xsdata.xs_shapes["[G]"]):
884
                    # Then the data is already an array vs groups so copy
885
                    # and move along
886
                    data[i, :] = temp_data
×
887
                elif shape == xsdata.xs_shapes["[G][G']"]:
×
888
                    # Sum the data over outgoing groups to create our array vs
889
                    # groups
890
                    data[i, :] = np.sum(temp_data, axis=1)
×
891
                elif shape == xsdata.xs_shapes["[DG]"]:
×
892
                    # Then we have a constant vs groups with a value for each
893
                    # delayed group. The user-provided value of orders tells us
894
                    # which delayed group we want. If none are provided, then
895
                    # we sum all the delayed groups together.
896
                    if orders[i]:
×
897
                        if orders[i] < len(shape[0]):
×
898
                            data[i, :] = temp_data[orders[i]]
×
899
                    else:
900
                        data[i, :] = np.sum(temp_data[:])
×
901
                elif shape in (xsdata.xs_shapes["[DG][G']"],
×
902
                               xsdata.xs_shapes["[DG][G]"]):
903
                    # Then we have an array vs groups with values for each
904
                    # delayed group. The user-provided value of orders tells us
905
                    # which delayed group we want. If none are provided, then
906
                    # we sum all the delayed groups together.
907
                    if orders[i]:
×
908
                        if orders[i] < len(shape[0]):
×
909
                            data[i, :] = temp_data[orders[i], :]
×
910
                    else:
911
                        data[i, :] = np.sum(temp_data[:, :], axis=0)
×
912
                elif shape == xsdata.xs_shapes["[DG][G][G']"]:
×
913
                    # Then we have a delayed group matrix. We will first
914
                    # remove the outgoing group dependency
915
                    temp_data = np.sum(temp_data, axis=-1)
×
916
                    # And then proceed in exactly the same manner as the
917
                    # "[DG][G']" or "[DG][G]" shapes in the previous block.
918
                    if orders[i]:
×
919
                        if orders[i] < len(shape[0]):
×
920
                            data[i, :] = temp_data[orders[i], :]
×
921
                    else:
922
                        data[i, :] = np.sum(temp_data[:, :], axis=0)
×
923
                elif shape == xsdata.xs_shapes["[G][G'][Order]"]:
×
924
                    # This is a scattering matrix with angular data
925
                    # First remove the outgoing group dependence
926
                    temp_data = np.sum(temp_data, axis=1)
×
927
                    # The user either provided a specific order or we resort
928
                    # to the default 0th order
929
                    if orders[i]:
×
930
                        order = orders[i]
×
931
                    else:
932
                        order = 0
×
933
                    # If the order is available, store the data for that order
934
                    # if it is not available, then the expansion coefficient
935
                    # is zero and thus we already have the correct value.
936
                    if order < shape[1]:
×
937
                        data[i, :] = temp_data[:, order]
×
938
    else:
939
        raise ValueError(f"{this} not present in provided MGXS library")
×
940

941
    return data
×
942

943

944
def _calculate_mgxs_elem_mat(this, types, library, orders=None,
11✔
945
                             temperature=294., ce_cross_sections=None,
946
                             enrichment=None):
947
    """Determines the multi-group cross sections of an element or material
948
    object.
949

950
    If the data for the nuclide or macroscopic object in the library is
951
    represented as angle-dependent data then this method will return the
952
    geometric average cross section over all angles.
953

954
    Parameters
955
    ----------
956
    this : str or openmc.Material
957
        Object to source data from. Elements can be input as a str
958
    types : Iterable of str
959
        The type of cross sections to calculate; values can either be those
960
        in openmc.PLOT_TYPES_MGXS
961
    library : openmc.MGXSLibrary
962
        MGXS Library containing the data of interest
963
    orders : Iterable of Integral, optional
964
        The scattering order or delayed group index to use for the
965
        corresponding entry in types. Defaults to the 0th order for scattering
966
        and the total delayed neutron data.
967
    temperature : float, optional
968
        Temperature in Kelvin to plot. If not specified, a default
969
        temperature of 294K will be plotted. Note that the nearest
970
        temperature in the library for each nuclide will be used as opposed
971
        to using any interpolation.
972
    ce_cross_sections : str, optional
973
        Location of continuous-energy cross_sections.xml file. Default is None.
974
        This is used only for expanding the elements
975
    enrichment : float, optional
976
        Enrichment for U235 in weight percent. For example, input 4.95 for
977
        4.95 weight percent enriched U. Default is None
978
        (natural composition).
979

980
    Returns
981
    -------
982
    data : numpy.ndarray
983
        Cross sections calculated at the energy grid described by energy_grid
984

985
    """
986

987
    if isinstance(this, openmc.Material):
×
988
        if this.temperature is not None:
×
989
            T = this.temperature
×
990
        else:
991
            T = temperature
×
992

993
        # Check to see if we have nuclides/elements or a macroscopic object
994
        if this._macroscopic is not None:
×
995
            # We have macroscopics
996
            nuclides = {this._macroscopic: this.density}
×
997
        else:
998
            # Expand elements in to nuclides with atomic densities
999
            nuclides = this.get_nuclide_atom_densities()
×
1000

1001
        # For ease of processing split out nuc and nuc_density
1002
        nuc_fraction = list(nuclides.values())
×
1003
    else:
1004
        T = temperature
×
1005
        # Expand elements in to nuclides with atomic densities
1006
        nuclides = openmc.Element(this).expand(100., 'ao', enrichment=enrichment,
×
1007
                               cross_sections=ce_cross_sections)
1008

1009
        # For ease of processing split out nuc and nuc_fractions
1010
        nuc_fraction = [nuclide[1] for nuclide in nuclides]
×
1011

1012
    nuc_data = []
×
1013
    for nuclide in nuclides.items():
×
1014
        nuc_data.append(_calculate_mgxs_nuc_macro(nuclide[0], types, library,
×
1015
                                                  orders, T))
1016

1017
    # Combine across the nuclides
1018
    data = np.zeros((len(types), library.energy_groups.num_groups))
×
1019
    for line in range(len(types)):
×
1020
        if types[line] == 'unity':
×
1021
            data[line, :] = 1.
×
1022
        else:
1023
            for n in range(len(nuclides)):
×
1024
                data[line, :] += nuc_fraction[n] * nuc_data[n][line, :]
×
1025

1026
    return data
×
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