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

openmc-dev / openmc / 19058781736

04 Nov 2025 05:26AM UTC coverage: 82.008% (-3.1%) from 85.155%
19058781736

Pull #3252

github

web-flow
Merge b8a72730f into bd76fc056
Pull Request #3252: Adding vtkhdf option to write vtk data

16714 of 23236 branches covered (71.93%)

Branch coverage included in aggregate %.

61 of 66 new or added lines in 1 file covered. (92.42%)

3175 existing lines in 103 files now uncovered.

54243 of 63288 relevant lines covered (85.71%)

43393337.77 hits per line

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

56.1
/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
    ce_cross_sections: str | None = None,
132
    mg_cross_sections: str | None = None,
133
    enrichment: float | None = None,
134
    plot_CE: bool = True,
135
    orders: Iterable[int] | None = None,
136
    divisor_orders: Iterable[int] | None = None,
137
    energy_axis_units: str = "eV",
138
    **kwargs,
139
) -> "plt.Figure" | None:
140
    """Creates a figure of continuous-energy cross sections for this item.
141

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

185
        .. versionadded:: 0.15.0
186

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

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

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

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

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

210
    all_types = []
11✔
211

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

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

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

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

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

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

258
        E *= axis_scaling_factor[energy_axis_units]
11✔
259

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

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

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

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

288
    return fig
11✔
289

290

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

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

318
    Returns
319
    -------
320
    energy_grid : numpy.ndarray
321
        Energies at which cross sections are calculated, in units of eV
322
    data : numpy.ndarray
323
        Cross sections calculated at the energy grid described by energy_grid
324

325
    """
326

327
    # Check types
328
    cv.check_type('this', this, (str, openmc.Material))
11✔
329
    cv.check_type('temperature', temperature, Real)
11✔
330
    if sab_name:
11✔
331
        cv.check_type('sab_name', sab_name, str)
11✔
332
    if enrichment:
11✔
333
        cv.check_type('enrichment', enrichment, Real)
×
334

335
    if isinstance(this, str):
11✔
336
        if this in ELEMENT_NAMES:
11✔
337
            energy_grid, data = _calculate_cexs_elem_mat(
11✔
338
                this, types, temperature, cross_sections, sab_name, enrichment
339
            )
340
        else:
341
            energy_grid, xs = _calculate_cexs_nuclide(
11✔
342
                this, types, temperature, sab_name, cross_sections,
343
                ncrystal_cfg
344
            )
345

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

356
    return energy_grid, data
11✔
357

358

359
def _calculate_cexs_nuclide(this, types, temperature=294., sab_name=None,
11✔
360
                            cross_sections=None, ncrystal_cfg=None):
361
    """Calculates continuous-energy cross sections of a requested type.
362

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

384
    Returns
385
    -------
386
    energy_grid : numpy.ndarray
387
        Energies at which cross sections are calculated, in units of eV
388
    data : Iterable of Callable
389
        Requested cross section functions
390

391
    """
392

393
    # Load the library
394
    library = openmc.data.DataLibrary.from_xml(cross_sections)
11✔
395

396
    # Convert temperature to format needed for access in the library
397
    strT = f"{int(round(temperature))}K"
11✔
398
    T = temperature
11✔
399

400
    # Now we can create the data sets to be plotted
401
    energy_grid = []
11✔
402
    xs = []
11✔
403
    lib = library.get_by_material(this)
11✔
404
    if lib is not None:
11✔
405
        nuc = openmc.data.IncidentNeutron.from_hdf5(lib['path'])
11✔
406
        # Obtain the nearest temperature
407
        if strT in nuc.temperatures:
11✔
408
            nucT = strT
11✔
409
        else:
410
            delta_T = np.array(nuc.kTs) - T * openmc.data.K_BOLTZMANN
×
411
            closest_index = np.argmin(np.abs(delta_T))
×
412
            nucT = nuc.temperatures[closest_index]
×
413

414
        # Prep S(a,b) data if needed
415
        if sab_name:
11✔
416
            sab = openmc.data.ThermalScattering.from_hdf5(
11✔
417
                library.get_by_material(sab_name, data_type='thermal')['path'])
418
            # Obtain the nearest temperature
419
            if strT in sab.temperatures:
11✔
UNCOV
420
                sabT = strT
×
421
            else:
422
                delta_T = np.array(sab.kTs) - T * openmc.data.K_BOLTZMANN
11✔
423
                closest_index = np.argmin(np.abs(delta_T))
11✔
424
                sabT = sab.temperatures[closest_index]
11✔
425

426
            # Create an energy grid composed the S(a,b) and the nuclide's grid
427
            grid = nuc.energy[nucT]
11✔
428
            sab_Emax = 0.
11✔
429
            sab_funcs = []
11✔
430
            if sab.elastic is not None:
11✔
431
                elastic = sab.elastic.xs[sabT]
×
432
                if isinstance(elastic, openmc.data.CoherentElastic):
×
433
                    grid = np.union1d(grid, elastic.bragg_edges)
×
434
                    if elastic.bragg_edges[-1] > sab_Emax:
×
435
                        sab_Emax = elastic.bragg_edges[-1]
×
436
                elif isinstance(elastic, openmc.data.Tabulated1D):
×
437
                    grid = np.union1d(grid, elastic.x)
×
438
                    if elastic.x[-1] > sab_Emax:
×
439
                        sab_Emax = elastic.x[-1]
×
UNCOV
440
                sab_funcs.append(elastic)
×
441
            if sab.inelastic is not None:
11✔
442
                inelastic = sab.inelastic.xs[sabT]
11✔
443
                grid = np.union1d(grid, inelastic.x)
11✔
444
                if inelastic.x[-1] > sab_Emax:
11✔
445
                        sab_Emax = inelastic.x[-1]
11✔
446
                sab_funcs.append(inelastic)
11✔
447
            energy_grid = grid
11✔
448
        else:
449
            energy_grid = nuc.energy[nucT]
11✔
450

451
        # Parse the types
452
        mts = []
11✔
453
        ops = []
11✔
454
        yields = []
11✔
455
        for line in types:
11✔
456
            if line in PLOT_TYPES:
11✔
457
                tmp_mts = [mtj for mti in PLOT_TYPES_MT[line] for mtj in
11✔
458
                           nuc.get_reaction_components(mti)]
459
                mts.append(tmp_mts)
11✔
460
                if line.startswith('nu'):
11✔
UNCOV
461
                    yields.append(True)
×
462
                else:
463
                    yields.append(False)
11✔
464
                if XI_MT in tmp_mts:
11✔
UNCOV
465
                    ops.append((np.add,) * (len(tmp_mts) - 2) + (np.multiply,))
×
466
                else:
467
                    ops.append((np.add,) * (len(tmp_mts) - 1))
11✔
468
            elif line in openmc.data.REACTION_MT:
11✔
469
                mt_number = openmc.data.REACTION_MT[line]
11✔
470
                cv.check_type('MT in types', mt_number, Integral)
11✔
471
                cv.check_greater_than('MT in types', mt_number, 0)
11✔
472
                tmp_mts = nuc.get_reaction_components(mt_number)
11✔
473
                mts.append(tmp_mts)
11✔
474
                ops.append((np.add,) * (len(tmp_mts) - 1))
11✔
475
                yields.append(False)
11✔
476
            elif isinstance(line, int):
11✔
477
                # Not a built-in type, we have to parse it ourselves
478
                cv.check_type('MT in types', line, Integral)
11✔
479
                cv.check_greater_than('MT in types', line, 0)
11✔
480
                tmp_mts = nuc.get_reaction_components(line)
11✔
481
                mts.append(tmp_mts)
11✔
482
                ops.append((np.add,) * (len(tmp_mts) - 1))
11✔
483
                yields.append(False)
11✔
484
            else:
UNCOV
485
                raise TypeError("Invalid type", line)
×
486

487
        for i, mt_set in enumerate(mts):
11✔
488
            # Get the reaction xs data from the nuclide
489
            funcs = []
11✔
490
            op = ops[i]
11✔
491
            for mt in mt_set:
11✔
492
                if mt == 2:
11✔
493
                    if sab_name:
11✔
494
                        # Then we need to do a piece-wise function of
495
                        # The S(a,b) and non-thermal data
496
                        sab_sum = openmc.data.Sum(sab_funcs)
11✔
497
                        pw_funcs = openmc.data.Regions1D(
11✔
498
                            [sab_sum, nuc[mt].xs[nucT]],
499
                            [sab_Emax])
500
                        funcs.append(pw_funcs)
11✔
501
                    elif ncrystal_cfg:
11✔
502
                        import NCrystal
×
503
                        nc_scatter = NCrystal.createScatter(ncrystal_cfg)
×
504
                        nc_func = nc_scatter.xsect
×
505
                        nc_emax = 5 # eV # this should be obtained from NCRYSTAL_MAX_ENERGY
×
UNCOV
506
                        energy_grid = np.union1d(np.geomspace(min(energy_grid),
×
507
                                                              1.1*nc_emax,
508
                                                              1000),energy_grid) # NCrystal does not have
509
                                                                                 # an intrinsic energy grid
UNCOV
510
                        pw_funcs = openmc.data.Regions1D(
×
511
                            [nc_func, nuc[mt].xs[nucT]],
512
                            [nc_emax])
UNCOV
513
                        funcs.append(pw_funcs)
×
514
                    else:
515
                        funcs.append(nuc[mt].xs[nucT])
11✔
516
                elif mt in nuc:
11✔
517
                    if yields[i]:
11✔
518
                        # Get the total yield first if available. This will be
519
                        # used primarily for fission.
UNCOV
520
                        for prod in chain(nuc[mt].products,
×
521
                                          nuc[mt].derived_products):
UNCOV
522
                            if prod.particle == 'neutron' and \
×
523
                                prod.emission_mode == 'total':
UNCOV
524
                                func = openmc.data.Combination(
×
525
                                    [nuc[mt].xs[nucT], prod.yield_],
526
                                    [np.multiply])
527
                                funcs.append(func)
×
UNCOV
528
                                break
×
529
                        else:
530
                            # Total doesn't exist so we have to create from
531
                            # prompt and delayed. This is used for scatter
532
                            # multiplication.
533
                            func = None
×
UNCOV
534
                            for prod in chain(nuc[mt].products,
×
535
                                              nuc[mt].derived_products):
UNCOV
536
                                if prod.particle == 'neutron' and \
×
537
                                    prod.emission_mode != 'total':
538
                                    if func:
×
UNCOV
539
                                        func = openmc.data.Combination(
×
540
                                            [prod.yield_, func], [np.add])
541
                                    else:
542
                                        func = prod.yield_
×
543
                            if func:
×
UNCOV
544
                                funcs.append(openmc.data.Combination(
×
545
                                    [func, nuc[mt].xs[nucT]], [np.multiply]))
546
                            else:
547
                                # If func is still None, then there were no
548
                                # products. In that case, assume the yield is
549
                                # one as its not provided for some summed
550
                                # reactions like MT=4
UNCOV
551
                                funcs.append(nuc[mt].xs[nucT])
×
552
                    else:
553
                        funcs.append(nuc[mt].xs[nucT])
11✔
554
                elif mt == UNITY_MT:
×
555
                    funcs.append(lambda x: 1.)
×
556
                elif mt == XI_MT:
×
557
                    awr = nuc.atomic_weight_ratio
×
558
                    alpha = ((awr - 1.) / (awr + 1.))**2
×
559
                    xi = 1. + alpha * np.log(alpha) / (1. - alpha)
×
UNCOV
560
                    funcs.append(lambda x: xi)
×
561
                else:
UNCOV
562
                    funcs.append(lambda x: 0.)
×
563
            funcs = funcs if funcs else [lambda x: 0.]
11✔
564
            xs.append(openmc.data.Combination(funcs, op))
11✔
565
    else:
UNCOV
566
        raise ValueError(this + " not in library")
×
567

568
    return energy_grid, xs
11✔
569

570

571
def _calculate_cexs_elem_mat(this, types, temperature=294.,
11✔
572
                             cross_sections=None, sab_name=None,
573
                             enrichment=None):
574
    """Calculates continuous-energy cross sections of a requested type.
575

576
    Parameters
577
    ----------
578
    this : openmc.Material or str
579
        Object to source data from. Element can be input as str
580
    types : Iterable of values of PLOT_TYPES
581
        The type of cross sections to calculate
582
    temperature : float, optional
583
        Temperature in Kelvin to plot. If not specified, a default
584
        temperature of 294K will be plotted. Note that the nearest
585
        temperature in the library for each nuclide will be used as opposed
586
        to using any interpolation.
587
    cross_sections : str, optional
588
        Location of cross_sections.xml file. Default is None.
589
    sab_name : str, optional
590
        Name of S(a,b) library to apply to MT=2 data when applicable.
591
    enrichment : float, optional
592
        Enrichment for U235 in weight percent. For example, input 4.95 for
593
        4.95 weight percent enriched U. Default is None
594
        (natural composition).
595

596
    Returns
597
    -------
598
    energy_grid : numpy.ndarray
599
        Energies at which cross sections are calculated, in units of eV
600
    data : numpy.ndarray
601
        Cross sections calculated at the energy grid described by energy_grid
602

603
    """
604

605
    if isinstance(this, openmc.Material):
11✔
606
        if this.temperature is not None:
11✔
UNCOV
607
            T = this.temperature
×
608
        else:
609
            T = temperature
11✔
610
    else:
611
        T = temperature
11✔
612

613
    # Load the library
614
    library = openmc.data.DataLibrary.from_xml(cross_sections)
11✔
615

616
    ncrystal_cfg = None
11✔
617
    if isinstance(this, openmc.Material):
11✔
618
        # Expand elements in to nuclides with atomic densities
619
        nuc_fractions = this.get_nuclide_atom_densities()
11✔
620
        # Create a dict of [nuclide name] = nuclide object to carry forward
621
        # with a common nuclides format between openmc.Material and Elements
622
        nuclides = {nuclide: nuclide for nuclide in nuc_fractions}
11✔
623
        # Add NCrystal cfg string if it exists
624
        ncrystal_cfg = this.ncrystal_cfg
11✔
625
    else:
626
        # Expand elements in to nuclides with atomic densities
627
        nuclides = openmc.Element(this).expand(1., 'ao', enrichment=enrichment,
11✔
628
                               cross_sections=cross_sections)
629
        # For ease of processing split out the nuclide and its fraction
630
        nuc_fractions = {nuclide[0]: nuclide[1] for nuclide in nuclides}
11✔
631
        # Create a dict of [nuclide name] = nuclide object to carry forward
632
        # with a common nuclides format between openmc.Material and Elements
633
        nuclides = {nuclide[0]: nuclide[0] for nuclide in nuclides}
11✔
634

635
    # Identify the nuclides which have S(a,b) data
636
    sabs = {}
11✔
637
    for nuclide in nuclides.items():
11✔
638
        sabs[nuclide[0]] = None
11✔
639
    if isinstance(this, openmc.Material):
11✔
640
        for sab_name, _ in this._sab:
11✔
641
            sab = openmc.data.ThermalScattering.from_hdf5(
11✔
642
                library.get_by_material(sab_name, data_type='thermal')['path'])
643
            for nuc in sab.nuclides:
11✔
644
                sabs[nuc] = sab_name
11✔
645
    else:
646
        if sab_name:
11✔
647
            sab = openmc.data.ThermalScattering.from_hdf5(
×
648
                library.get_by_material(sab_name, data_type='thermal')['path'])
649
            for nuc in sab.nuclides:
×
UNCOV
650
                sabs[nuc] = sab_name
×
651

652
    # Now we can create the data sets to be plotted
653
    xs = {}
11✔
654
    E = []
11✔
655
    for nuclide in nuclides.items():
11✔
656
        name = nuclide[0]
11✔
657
        nuc = nuclide[1]
11✔
658
        sab_name = sabs[name]
11✔
659
        temp_E, temp_xs = calculate_cexs(nuc, types, T, sab_name, cross_sections,
11✔
660
                                         ncrystal_cfg=ncrystal_cfg
661
                                         )
662
        E.append(temp_E)
11✔
663
        # Since the energy grids are different, store the cross sections as
664
        # a tabulated function so they can be calculated on any grid needed.
665
        xs[name] = [openmc.data.Tabulated1D(temp_E, temp_xs[line])
11✔
666
                    for line in range(len(types))]
667

668
    # Condense the data for every nuclide
669
    # First create a union energy grid
670
    energy_grid = E[0]
11✔
671
    for grid in E[1:]:
11✔
672
        energy_grid = np.union1d(energy_grid, grid)
11✔
673

674
    # Now we can combine all the nuclidic data
675
    data = np.zeros((len(types), len(energy_grid)))
11✔
676
    for line in range(len(types)):
11✔
677
        if types[line] == 'unity':
11✔
678
            data[line, :] = 1.
×
679
        else:
680
            for nuclide in nuclides.items():
11✔
681
                name = nuclide[0]
11✔
682
                data[line, :] += (nuc_fractions[name] *
11✔
683
                                  xs[name][line](energy_grid))
684

685
    return energy_grid, data
11✔
686

687

688
def calculate_mgxs(this, types, orders=None, temperature=294.,
11✔
689
                   cross_sections=None, ce_cross_sections=None,
690
                   enrichment=None):
691
    """Calculates multi-group cross sections of a requested type.
692

693
    If the data for the nuclide or macroscopic object in the library is
694
    represented as angle-dependent data then this method will return the
695
    geometric average cross section over all angles.
696

697
    Parameters
698
    ----------
699
    this : str or openmc.Material
700
        Object to source data from. Nuclides and elements can be input as a str
701
    types : Iterable of values of PLOT_TYPES_MGXS
702
        The type of cross sections to calculate
703
    orders : Iterable of Integral, optional
704
        The scattering order or delayed group index to use for the
705
        corresponding entry in types. Defaults to the 0th order for scattering
706
        and the total delayed neutron data.
707
    temperature : float, optional
708
        Temperature in Kelvin to plot. If not specified, a default
709
        temperature of 294K will be plotted. Note that the nearest
710
        temperature in the library for each nuclide will be used as opposed
711
        to using any interpolation.
712
    cross_sections : str, optional
713
        Location of MGXS HDF5 Library file. Default is None.
714
    ce_cross_sections : str, optional
715
        Location of continuous-energy cross_sections.xml file. Default is None.
716
    enrichment : float, optional
717
        Enrichment for U235 in weight percent. For example, input 4.95 for
718
        4.95 weight percent enriched U. Default is None
719
        (natural composition).
720

721
    Returns
722
    -------
723
    energy_grid : numpy.ndarray
724
        Energies at which cross sections are calculated, in units of eV
725
    data : numpy.ndarray
726
        Cross sections calculated at the energy grid described by energy_grid
727

728
    """
729

730
    # Check types
731
    cv.check_type('temperature', temperature, Real)
×
732
    if enrichment:
×
733
        cv.check_type('enrichment', enrichment, Real)
×
734
    cv.check_iterable_type('types', types, str)
×
735

736
    cv.check_type("cross_sections", cross_sections, str)
×
737
    library = openmc.MGXSLibrary.from_hdf5(cross_sections)
×
738

739
    if this in ELEMENT_NAMES or isinstance(this, openmc.Material):
×
740
        mgxs = _calculate_mgxs_elem_mat(this, types, library, orders,
×
741
                                        temperature, ce_cross_sections,
742
                                        enrichment)
743
    elif isinstance(this, str):
×
744
        mgxs = _calculate_mgxs_nuc_macro(this, types, library, orders,
×
745
                                         temperature)
746
    else:
747
        raise TypeError("Invalid type")
×
748

749
    # Convert the data to the format needed
750
    data = np.zeros((len(types), 2 * library.energy_groups.num_groups))
×
751
    energy_grid = np.zeros(2 * library.energy_groups.num_groups)
×
752
    for g in range(library.energy_groups.num_groups):
×
753
        energy_grid[g * 2: g * 2 + 2] = \
×
754
            library.energy_groups.group_edges[g: g + 2]
755
    # Ensure the energy will show on a log-axis by replacing 0s with a
756
    # sufficiently small number
757
    energy_grid[0] = max(energy_grid[0], _MIN_E)
×
758

759
    for line in range(len(types)):
×
760
        for g in range(library.energy_groups.num_groups):
×
761
            data[line, g * 2: g * 2 + 2] = mgxs[line, g]
×
762

763
    return energy_grid[::-1], data
×
764

765

766
def _calculate_mgxs_nuc_macro(this, types, library, orders=None,
11✔
767
                              temperature=294.):
768
    """Determines the multi-group cross sections of a nuclide or macroscopic
769
    object.
770

771
    If the data for the nuclide or macroscopic object in the library is
772
    represented as angle-dependent data then this method will return the
773
    geometric average cross section over all angles.
774

775
    Parameters
776
    ----------
777
    this : str
778
        Object to source data from
779
    types : Iterable of str
780
        The type of cross sections to calculate; values can either be those
781
        in openmc.PLOT_TYPES_MGXS
782
    library : openmc.MGXSLibrary
783
        MGXS Library containing the data of interest
784
    orders : Iterable of Integral, optional
785
        The scattering order or delayed group index to use for the
786
        corresponding entry in types. Defaults to the 0th order for scattering
787
        and the total delayed neutron data.
788
    temperature : float, optional
789
        Temperature in Kelvin to plot. If not specified, a default
790
        temperature of 294K will be plotted. Note that the nearest
791
        temperature in the library for each nuclide will be used as opposed
792
        to using any interpolation.
793

794
    Returns
795
    -------
796
    data : numpy.ndarray
797
        Cross sections calculated at the energy grid described by energy_grid
798

799
    """
800

801
    # Check the parameters and grab order/delayed groups
802
    if orders:
×
803
        cv.check_iterable_type('orders', orders, Integral,
×
804
                               min_depth=len(types), max_depth=len(types))
805
    else:
806
        orders = [None] * len(types)
×
807
    for i, line in enumerate(types):
×
808
        cv.check_type("line", line, str)
×
809
        cv.check_value("line", line, PLOT_TYPES_MGXS)
×
810
        if orders[i]:
×
811
            cv.check_greater_than("order value", orders[i], 0, equality=True)
×
812

813
    xsdata = library.get_by_name(this)
×
814

815
    if xsdata is not None:
×
816
        # Obtain the nearest temperature
817
        t = np.abs(xsdata.temperatures - temperature).argmin()
×
818

819
        # Get the data
820
        data = np.zeros((len(types), library.energy_groups.num_groups))
×
821
        for i, line in enumerate(types):
×
822
            if 'fission' in line and not xsdata.fissionable:
×
823
                continue
×
824
            elif line == 'unity':
×
825
                data[i, :] = 1.
×
826
            else:
827
                # Now we have to get the cross section data and properly
828
                # treat it depending on the requested type.
829
                # First get the data in a generic fashion
830
                temp_data = getattr(xsdata, _PLOT_MGXS_ATTR[line])[t]
×
831
                shape = temp_data.shape[:]
×
832
                # If we have angular data, then want the geometric
833
                # average over all provided angles.  Since the angles are
834
                # equi-distant, un-weighted averaging will suffice
835
                if xsdata.representation == 'angle':
×
836
                    temp_data = np.mean(temp_data, axis=(0, 1))
×
837

838
                # Now we can look at the shape of the data to identify how
839
                # it should be modified to produce an array of values
840
                # with groups.
841
                if shape in (xsdata.xs_shapes["[G']"],
×
842
                             xsdata.xs_shapes["[G]"]):
843
                    # Then the data is already an array vs groups so copy
844
                    # and move along
845
                    data[i, :] = temp_data
×
846
                elif shape == xsdata.xs_shapes["[G][G']"]:
×
847
                    # Sum the data over outgoing groups to create our array vs
848
                    # groups
849
                    data[i, :] = np.sum(temp_data, axis=1)
×
850
                elif shape == xsdata.xs_shapes["[DG]"]:
×
851
                    # Then we have a constant vs groups with a value for each
852
                    # delayed group. The user-provided value of orders tells us
853
                    # which delayed group we want. If none are provided, then
854
                    # we sum all the delayed groups together.
855
                    if orders[i]:
×
856
                        if orders[i] < len(shape[0]):
×
857
                            data[i, :] = temp_data[orders[i]]
×
858
                    else:
859
                        data[i, :] = np.sum(temp_data[:])
×
860
                elif shape in (xsdata.xs_shapes["[DG][G']"],
×
861
                               xsdata.xs_shapes["[DG][G]"]):
862
                    # Then we have an array vs groups with values for each
863
                    # delayed group. The user-provided value of orders tells us
864
                    # which delayed group we want. If none are provided, then
865
                    # we sum all the delayed groups together.
866
                    if orders[i]:
×
867
                        if orders[i] < len(shape[0]):
×
868
                            data[i, :] = temp_data[orders[i], :]
×
869
                    else:
870
                        data[i, :] = np.sum(temp_data[:, :], axis=0)
×
871
                elif shape == xsdata.xs_shapes["[DG][G][G']"]:
×
872
                    # Then we have a delayed group matrix. We will first
873
                    # remove the outgoing group dependency
874
                    temp_data = np.sum(temp_data, axis=-1)
×
875
                    # And then proceed in exactly the same manner as the
876
                    # "[DG][G']" or "[DG][G]" shapes in the previous block.
877
                    if orders[i]:
×
878
                        if orders[i] < len(shape[0]):
×
879
                            data[i, :] = temp_data[orders[i], :]
×
880
                    else:
881
                        data[i, :] = np.sum(temp_data[:, :], axis=0)
×
882
                elif shape == xsdata.xs_shapes["[G][G'][Order]"]:
×
883
                    # This is a scattering matrix with angular data
884
                    # First remove the outgoing group dependence
885
                    temp_data = np.sum(temp_data, axis=1)
×
886
                    # The user either provided a specific order or we resort
887
                    # to the default 0th order
888
                    if orders[i]:
×
889
                        order = orders[i]
×
890
                    else:
891
                        order = 0
×
892
                    # If the order is available, store the data for that order
893
                    # if it is not available, then the expansion coefficient
894
                    # is zero and thus we already have the correct value.
895
                    if order < shape[1]:
×
896
                        data[i, :] = temp_data[:, order]
×
897
    else:
898
        raise ValueError(f"{this} not present in provided MGXS library")
×
899

900
    return data
×
901

902

903
def _calculate_mgxs_elem_mat(this, types, library, orders=None,
11✔
904
                             temperature=294., ce_cross_sections=None,
905
                             enrichment=None):
906
    """Determines the multi-group cross sections of an element or material
907
    object.
908

909
    If the data for the nuclide or macroscopic object in the library is
910
    represented as angle-dependent data then this method will return the
911
    geometric average cross section over all angles.
912

913
    Parameters
914
    ----------
915
    this : str or openmc.Material
916
        Object to source data from. Elements can be input as a str
917
    types : Iterable of str
918
        The type of cross sections to calculate; values can either be those
919
        in openmc.PLOT_TYPES_MGXS
920
    library : openmc.MGXSLibrary
921
        MGXS Library containing the data of interest
922
    orders : Iterable of Integral, optional
923
        The scattering order or delayed group index to use for the
924
        corresponding entry in types. Defaults to the 0th order for scattering
925
        and the total delayed neutron data.
926
    temperature : float, optional
927
        Temperature in Kelvin to plot. If not specified, a default
928
        temperature of 294K will be plotted. Note that the nearest
929
        temperature in the library for each nuclide will be used as opposed
930
        to using any interpolation.
931
    ce_cross_sections : str, optional
932
        Location of continuous-energy cross_sections.xml file. Default is None.
933
        This is used only for expanding the elements
934
    enrichment : float, optional
935
        Enrichment for U235 in weight percent. For example, input 4.95 for
936
        4.95 weight percent enriched U. Default is None
937
        (natural composition).
938

939
    Returns
940
    -------
941
    data : numpy.ndarray
942
        Cross sections calculated at the energy grid described by energy_grid
943

944
    """
945

946
    if isinstance(this, openmc.Material):
×
947
        if this.temperature is not None:
×
948
            T = this.temperature
×
949
        else:
950
            T = temperature
×
951

952
        # Check to see if we have nuclides/elements or a macroscopic object
953
        if this._macroscopic is not None:
×
954
            # We have macroscopics
955
            nuclides = {this._macroscopic: this.density}
×
956
        else:
957
            # Expand elements in to nuclides with atomic densities
958
            nuclides = this.get_nuclide_atom_densities()
×
959

960
        # For ease of processing split out nuc and nuc_density
961
        nuc_fraction = list(nuclides.values())
×
962
    else:
963
        T = temperature
×
964
        # Expand elements in to nuclides with atomic densities
965
        nuclides = openmc.Element(this).expand(100., 'ao', enrichment=enrichment,
×
966
                               cross_sections=ce_cross_sections)
967

968
        # For ease of processing split out nuc and nuc_fractions
969
        nuc_fraction = [nuclide[1] for nuclide in nuclides]
×
970

971
    nuc_data = []
×
972
    for nuclide in nuclides.items():
×
973
        nuc_data.append(_calculate_mgxs_nuc_macro(nuclide[0], types, library,
×
974
                                                  orders, T))
975

976
    # Combine across the nuclides
977
    data = np.zeros((len(types), library.energy_groups.num_groups))
×
978
    for line in range(len(types)):
×
979
        if types[line] == 'unity':
×
980
            data[line, :] = 1.
×
981
        else:
982
            for n in range(len(nuclides)):
×
983
                data[line, :] += nuc_fraction[n] * nuc_data[n][line, :]
×
984

985
    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

© 2025 Coveralls, Inc