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

openmc-dev / openmc / 8567808459

05 Apr 2024 09:37AM UTC coverage: 84.733%. First build
8567808459

push

github

web-flow
changing y axis label for heating plots (#2859)

Co-authored-by: Paul Romano <paul.k.romano@gmail.com>

13 of 14 new or added lines in 1 file covered. (92.86%)

47770 of 56377 relevant lines covered (84.73%)

33976534.7 hits per line

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

55.64
/openmc/plotter.py
1
from itertools import chain
14✔
2
from numbers import Integral, Real
14✔
3

4
import numpy as np
14✔
5

6
import openmc.checkvalue as cv
14✔
7
import openmc.data
14✔
8

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

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

25
# Special MT values
26
UNITY_MT = -1
14✔
27
XI_MT = -2
14✔
28

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

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

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

54

55
ELEMENT_NAMES = list(openmc.data.ELEMENT_SYMBOL.values())[1:]
14✔
56

57

58
def _get_legend_label(this, type):
14✔
59
    """Gets a label for the element or nuclide or material and reaction plotted"""
60
    if isinstance(this, str):
14✔
61
        if type in openmc.data.DADZ:
14✔
62
            z, a, m = openmc.data.zam(this)
14✔
63
            da, dz = openmc.data.DADZ[type]
14✔
64
            gnds_name = openmc.data.gnds_name(z + dz, a + da, m)
14✔
65
            return f'{this} {type} {gnds_name}'
14✔
66
        return f'{this} {type}'
14✔
67
    elif this.name == '':
14✔
68
        return f'Material {this.id} {type}'
14✔
69
    else:
70
        return f'{this.name} {type}'
×
71

72

73
def _get_yaxis_label(reactions, divisor_types):
14✔
74
    """Gets a y axis label for the type of data plotted"""
75

76
    heat_values = {"heating", "heating-local", "damage-energy"}
14✔
77

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

96
    if divisor_types:
14✔
NEW
97
        mid, units = "Data", ""
×
98
    else:
99
        mid = "Cross Section"
14✔
100
        units = {
14✔
101
            "Macroscopic": "[1/cm]",
102
            "Microscopic": "[b]",
103
            "Heating": "[eV-barn]",
104
        }[stem]
105

106
    return f'{stem} {mid} {units}'
14✔
107

108
def _get_title(reactions):
14✔
109
    """Gets a title for the type of data plotted"""
110
    if len(reactions) == 1:
14✔
111
        this, = reactions
14✔
112
        name = this.name if isinstance(this, openmc.Material) else this
14✔
113
        return f'Cross Section Plot For {name}'
14✔
114
    else:
115
        return 'Cross Section Plot'
14✔
116

117

118
def plot_xs(reactions, divisor_types=None, temperature=294., axis=None,
14✔
119
            sab_name=None, ce_cross_sections=None, mg_cross_sections=None,
120
            enrichment=None, plot_CE=True, orders=None, divisor_orders=None,
121
            energy_axis_units="eV", **kwargs):
122
    """Creates a figure of continuous-energy cross sections for this item.
123

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

167
        .. versionadded:: 0.14.1
168

169
    Returns
170
    -------
171
    fig : matplotlib.figure.Figure
172
        If axis is None, then a Matplotlib Figure of the generated
173
        cross section will be returned. Otherwise, a value of
174
        None will be returned as the figure and axes have already been
175
        generated.
176

177
    """
178
    import matplotlib.pyplot as plt
14✔
179

180
    cv.check_type("plot_CE", plot_CE, bool)
14✔
181
    cv.check_value("energy_axis_units", energy_axis_units, {"eV", "keV", "MeV"})
14✔
182

183
    axis_scaling_factor = {"eV": 1.0, "keV": 1e-3, "MeV": 1e-6}
14✔
184

185
    # Generate the plot
186
    if axis is None:
14✔
187
        fig, ax = plt.subplots(**kwargs)
14✔
188
    else:
189
        fig = None
×
190
        ax = axis
×
191

192
    all_types = []
14✔
193

194
    for this, types in reactions.items():
14✔
195
        all_types = all_types + types
14✔
196

197
        if plot_CE:
14✔
198
            cv.check_type("this", this, (str, openmc.Material))
14✔
199
            # Calculate for the CE cross sections
200
            E, data = calculate_cexs(this, types, temperature, sab_name,
14✔
201
                                    ce_cross_sections, enrichment)
202
            if divisor_types:
14✔
203
                cv.check_length('divisor types', divisor_types, len(types))
×
204
                Ediv, data_div = calculate_cexs(this, divisor_types, temperature,
×
205
                                                sab_name, ce_cross_sections,
206
                                                enrichment)
207

208
                # Create a new union grid, interpolate data and data_div on to that
209
                # grid, and then do the actual division
210
                Enum = E[:]
×
211
                E = np.union1d(Enum, Ediv)
×
212
                data_new = np.zeros((len(types), len(E)))
×
213

214
                for line in range(len(types)):
×
215
                    data_new[line, :] = \
×
216
                        np.divide(np.interp(E, Enum, data[line, :]),
217
                                np.interp(E, Ediv, data_div[line, :]))
218
                    if divisor_types[line] != 'unity':
×
219
                        types[line] = types[line] + ' / ' + divisor_types[line]
×
220
                data = data_new
×
221
        else:
222
            # Calculate for MG cross sections
223
            E, data = calculate_mgxs(this, types, orders, temperature,
×
224
                                    mg_cross_sections, ce_cross_sections,
225
                                    enrichment)
226

227
            if divisor_types:
×
228
                cv.check_length('divisor types', divisor_types, len(types))
×
229
                Ediv, data_div = calculate_mgxs(this, divisor_types,
×
230
                                                divisor_orders, temperature,
231
                                                mg_cross_sections,
232
                                                ce_cross_sections, enrichment)
233

234
                # Perform the division
235
                for line in range(len(types)):
×
236
                    data[line, :] /= data_div[line, :]
×
237
                    if divisor_types[line] != 'unity':
×
238
                        types[line] += ' / ' + divisor_types[line]
×
239

240
        E *= axis_scaling_factor[energy_axis_units]
14✔
241

242
        # Plot the data
243
        for i in range(len(data)):
14✔
244
            data[i, :] = np.nan_to_num(data[i, :])
14✔
245
            if np.sum(data[i, :]) > 0.:
14✔
246
                ax.plot(E, data[i, :], label=_get_legend_label(this, types[i]))
14✔
247

248
    # Set to loglog or semilogx depending on if we are plotting a data
249
    # type which we expect to vary linearly
250
    if set(all_types).issubset(PLOT_TYPES_LINEAR):
14✔
251
        ax.set_xscale('log')
×
252
        ax.set_yscale('linear')
×
253
    else:
254
        ax.set_xscale('log')
14✔
255
        ax.set_yscale('log')
14✔
256

257
    ax.set_xlabel(f"Energy [{energy_axis_units}]")
14✔
258
    if plot_CE:
14✔
259
        ax.set_xlim(
14✔
260
            _MIN_E * axis_scaling_factor[energy_axis_units],
261
            _MAX_E * axis_scaling_factor[energy_axis_units],
262
        )
263
    else:
264
        ax.set_xlim(E[-1], E[0])
×
265

266
    ax.set_ylabel(_get_yaxis_label(reactions, divisor_types))
14✔
267
    ax.legend(loc='best')
14✔
268
    ax.set_title(_get_title(reactions))
14✔
269

270
    return fig
14✔
271

272

273

274
def calculate_cexs(this, types, temperature=294., sab_name=None,
14✔
275
                   cross_sections=None, enrichment=None, ncrystal_cfg=None):
276
    """Calculates continuous-energy cross sections of a requested type.
277

278
    Parameters
279
    ----------
280
    this : str or openmc.Material
281
        Object to source data from. Nuclides and elements should be input as a
282
        str
283
    types : Iterable of values of PLOT_TYPES
284
        The type of cross sections to calculate
285
    temperature : float, optional
286
        Temperature in Kelvin to plot. If not specified, a default
287
        temperature of 294K will be plotted. Note that the nearest
288
        temperature in the library for each nuclide will be used as opposed
289
        to using any interpolation.
290
    sab_name : str, optional
291
        Name of S(a,b) library to apply to MT=2 data when applicable.
292
    cross_sections : str, optional
293
        Location of cross_sections.xml file. Default is None.
294
    enrichment : float, optional
295
        Enrichment for U235 in weight percent. For example, input 4.95 for
296
        4.95 weight percent enriched U. Default is None
297
        (natural composition).
298
    ncrystal_cfg : str, optional
299
        Configuration string for NCrystal material.
300

301
    Returns
302
    -------
303
    energy_grid : numpy.ndarray
304
        Energies at which cross sections are calculated, in units of eV
305
    data : numpy.ndarray
306
        Cross sections calculated at the energy grid described by energy_grid
307

308
    """
309

310
    # Check types
311
    cv.check_type('this', this, (str, openmc.Material))
14✔
312
    cv.check_type('temperature', temperature, Real)
14✔
313
    if sab_name:
14✔
314
        cv.check_type('sab_name', sab_name, str)
14✔
315
    if enrichment:
14✔
316
        cv.check_type('enrichment', enrichment, Real)
×
317

318
    if isinstance(this, str):
14✔
319
        if this in ELEMENT_NAMES:
14✔
320
            energy_grid, data = _calculate_cexs_elem_mat(
14✔
321
                this, types, temperature, cross_sections, sab_name, enrichment
322
            )
323
        else:
324
            energy_grid, xs = _calculate_cexs_nuclide(
14✔
325
                this, types, temperature, sab_name, cross_sections,
326
                ncrystal_cfg
327
            )
328

329
            # Convert xs (Iterable of Callable) to a grid of cross section values
330
            # calculated on the points in energy_grid for consistency with the
331
            # element and material functions.
332
            data = np.zeros((len(types), len(energy_grid)))
14✔
333
            for line in range(len(types)):
14✔
334
                data[line, :] = xs[line](energy_grid)
14✔
335
    else:
336
        energy_grid, data = _calculate_cexs_elem_mat(this, types, temperature,
14✔
337
                                                     cross_sections)
338

339
    return energy_grid, data
14✔
340

341

342
def _calculate_cexs_nuclide(this, types, temperature=294., sab_name=None,
14✔
343
                            cross_sections=None, ncrystal_cfg=None):
344
    """Calculates continuous-energy cross sections of a requested type.
345

346
    Parameters
347
    ----------
348
    this : str
349
        Nuclide object to source data from
350
    types : Iterable of str or Integral
351
        The type of cross sections to calculate; values can either be those
352
        in openmc.PLOT_TYPES or keys from openmc.data.REACTION_MT which
353
        correspond to a reaction description e.g '(n,2n)' or integers which
354
        correspond to reaction channel (MT) numbers.
355
    temperature : float, optional
356
        Temperature in Kelvin to plot. If not specified, a default
357
        temperature of 294K will be plotted. Note that the nearest
358
        temperature in the library for each nuclide will be used as opposed
359
        to using any interpolation.
360
    sab_name : str, optional
361
        Name of S(a,b) library to apply to MT=2 data when applicable.
362
    cross_sections : str, optional
363
        Location of cross_sections.xml file. Default is None.
364
    ncrystal_cfg : str, optional
365
        Configuration string for NCrystal material.
366

367
    Returns
368
    -------
369
    energy_grid : numpy.ndarray
370
        Energies at which cross sections are calculated, in units of eV
371
    data : Iterable of Callable
372
        Requested cross section functions
373

374
    """
375

376
    # Load the library
377
    library = openmc.data.DataLibrary.from_xml(cross_sections)
14✔
378

379
    # Convert temperature to format needed for access in the library
380
    strT = f"{int(round(temperature))}K"
14✔
381
    T = temperature
14✔
382

383
    # Now we can create the data sets to be plotted
384
    energy_grid = []
14✔
385
    xs = []
14✔
386
    lib = library.get_by_material(this)
14✔
387
    if lib is not None:
14✔
388
        nuc = openmc.data.IncidentNeutron.from_hdf5(lib['path'])
14✔
389
        # Obtain the nearest temperature
390
        if strT in nuc.temperatures:
14✔
391
            nucT = strT
14✔
392
        else:
393
            delta_T = np.array(nuc.kTs) - T * openmc.data.K_BOLTZMANN
×
394
            closest_index = np.argmin(np.abs(delta_T))
×
395
            nucT = nuc.temperatures[closest_index]
×
396

397
        # Prep S(a,b) data if needed
398
        if sab_name:
14✔
399
            sab = openmc.data.ThermalScattering.from_hdf5(sab_name)
14✔
400
            # Obtain the nearest temperature
401
            if strT in sab.temperatures:
14✔
402
                sabT = strT
×
403
            else:
404
                delta_T = np.array(sab.kTs) - T * openmc.data.K_BOLTZMANN
14✔
405
                closest_index = np.argmin(np.abs(delta_T))
14✔
406
                sabT = sab.temperatures[closest_index]
14✔
407

408
            # Create an energy grid composed the S(a,b) and the nuclide's grid
409
            grid = nuc.energy[nucT]
14✔
410
            sab_Emax = 0.
14✔
411
            sab_funcs = []
14✔
412
            if sab.elastic is not None:
14✔
413
                elastic = sab.elastic.xs[sabT]
×
414
                if isinstance(elastic, openmc.data.CoherentElastic):
×
415
                    grid = np.union1d(grid, elastic.bragg_edges)
×
416
                    if elastic.bragg_edges[-1] > sab_Emax:
×
417
                        sab_Emax = elastic.bragg_edges[-1]
×
418
                elif isinstance(elastic, openmc.data.Tabulated1D):
×
419
                    grid = np.union1d(grid, elastic.x)
×
420
                    if elastic.x[-1] > sab_Emax:
×
421
                        sab_Emax = elastic.x[-1]
×
422
                sab_funcs.append(elastic)
×
423
            if sab.inelastic is not None:
14✔
424
                inelastic = sab.inelastic.xs[sabT]
14✔
425
                grid = np.union1d(grid, inelastic.x)
14✔
426
                if inelastic.x[-1] > sab_Emax:
14✔
427
                        sab_Emax = inelastic.x[-1]
14✔
428
                sab_funcs.append(inelastic)
14✔
429
            energy_grid = grid
14✔
430
        else:
431
            energy_grid = nuc.energy[nucT]
14✔
432

433
        # Parse the types
434
        mts = []
14✔
435
        ops = []
14✔
436
        yields = []
14✔
437
        for line in types:
14✔
438
            if line in PLOT_TYPES:
14✔
439
                tmp_mts = [mtj for mti in PLOT_TYPES_MT[line] for mtj in
14✔
440
                           nuc.get_reaction_components(mti)]
441
                mts.append(tmp_mts)
14✔
442
                if line.startswith('nu'):
14✔
443
                    yields.append(True)
×
444
                else:
445
                    yields.append(False)
14✔
446
                if XI_MT in tmp_mts:
14✔
447
                    ops.append((np.add,) * (len(tmp_mts) - 2) + (np.multiply,))
×
448
                else:
449
                    ops.append((np.add,) * (len(tmp_mts) - 1))
14✔
450
            elif line in openmc.data.REACTION_MT:
14✔
451
                mt_number = openmc.data.REACTION_MT[line]
14✔
452
                cv.check_type('MT in types', mt_number, Integral)
14✔
453
                cv.check_greater_than('MT in types', mt_number, 0)
14✔
454
                tmp_mts = nuc.get_reaction_components(mt_number)
14✔
455
                mts.append(tmp_mts)
14✔
456
                ops.append((np.add,) * (len(tmp_mts) - 1))
14✔
457
                yields.append(False)
14✔
458
            elif isinstance(line, int):
14✔
459
                # Not a built-in type, we have to parse it ourselves
460
                cv.check_type('MT in types', line, Integral)
14✔
461
                cv.check_greater_than('MT in types', line, 0)
14✔
462
                tmp_mts = nuc.get_reaction_components(line)
14✔
463
                mts.append(tmp_mts)
14✔
464
                ops.append((np.add,) * (len(tmp_mts) - 1))
14✔
465
                yields.append(False)
14✔
466
            else:
467
                raise TypeError("Invalid type", line)
×
468

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

550
    return energy_grid, xs
14✔
551

552

553
def _calculate_cexs_elem_mat(this, types, temperature=294.,
14✔
554
                             cross_sections=None, sab_name=None,
555
                             enrichment=None):
556
    """Calculates continuous-energy cross sections of a requested type.
557

558
    Parameters
559
    ----------
560
    this : openmc.Material or str
561
        Object to source data from. Element can be input as str
562
    types : Iterable of values of PLOT_TYPES
563
        The type of cross sections to calculate
564
    temperature : float, optional
565
        Temperature in Kelvin to plot. If not specified, a default
566
        temperature of 294K will be plotted. Note that the nearest
567
        temperature in the library for each nuclide will be used as opposed
568
        to using any interpolation.
569
    cross_sections : str, optional
570
        Location of cross_sections.xml file. Default is None.
571
    sab_name : str, optional
572
        Name of S(a,b) library to apply to MT=2 data when applicable.
573
    enrichment : float, optional
574
        Enrichment for U235 in weight percent. For example, input 4.95 for
575
        4.95 weight percent enriched U. Default is None
576
        (natural composition).
577

578
    Returns
579
    -------
580
    energy_grid : numpy.ndarray
581
        Energies at which cross sections are calculated, in units of eV
582
    data : numpy.ndarray
583
        Cross sections calculated at the energy grid described by energy_grid
584

585
    """
586

587
    if isinstance(this, openmc.Material):
14✔
588
        if this.temperature is not None:
14✔
589
            T = this.temperature
×
590
        else:
591
            T = temperature
14✔
592
    else:
593
        T = temperature
14✔
594

595
    # Load the library
596
    library = openmc.data.DataLibrary.from_xml(cross_sections)
14✔
597

598
    ncrystal_cfg = None
14✔
599
    if isinstance(this, openmc.Material):
14✔
600
        # Expand elements in to nuclides with atomic densities
601
        nuc_fractions = this.get_nuclide_atom_densities()
14✔
602
        # Create a dict of [nuclide name] = nuclide object to carry forward
603
        # with a common nuclides format between openmc.Material and Elements
604
        nuclides = {nuclide: nuclide for nuclide in nuc_fractions}
14✔
605
        # Add NCrystal cfg string if it exists
606
        ncrystal_cfg = this.ncrystal_cfg
14✔
607
    else:
608
        # Expand elements in to nuclides with atomic densities
609
        nuclides = openmc.Element(this).expand(1., 'ao', enrichment=enrichment,
14✔
610
                               cross_sections=cross_sections)
611
        # For ease of processing split out the nuclide and its fraction
612
        nuc_fractions = {nuclide[0]: nuclide[1] for nuclide in nuclides}
14✔
613
        # Create a dict of [nuclide name] = nuclide object to carry forward
614
        # with a common nuclides format between openmc.Material and Elements
615
        nuclides = {nuclide[0]: nuclide[0] for nuclide in nuclides}
14✔
616

617
    # Identify the nuclides which have S(a,b) data
618
    sabs = {}
14✔
619
    for nuclide in nuclides.items():
14✔
620
        sabs[nuclide[0]] = None
14✔
621
    if isinstance(this, openmc.Material):
14✔
622
        for sab_name, _ in this._sab:
14✔
623
            sab = openmc.data.ThermalScattering.from_hdf5(
14✔
624
                library.get_by_material(sab_name, data_type='thermal')['path'])
625
            for nuc in sab.nuclides:
14✔
626
                sabs[nuc] = library.get_by_material(sab_name,
14✔
627
                        data_type='thermal')['path']
628
    else:
629
        if sab_name:
14✔
630
            sab = openmc.data.ThermalScattering.from_hdf5(sab_name)
×
631
            for nuc in sab.nuclides:
×
632
                sabs[nuc] = library.get_by_material(sab_name,
×
633
                        data_type='thermal')['path']
634

635
    # Now we can create the data sets to be plotted
636
    xs = {}
14✔
637
    E = []
14✔
638
    for nuclide in nuclides.items():
14✔
639
        name = nuclide[0]
14✔
640
        nuc = nuclide[1]
14✔
641
        sab_tab = sabs[name]
14✔
642
        temp_E, temp_xs = calculate_cexs(nuc, types, T, sab_tab, cross_sections,
14✔
643
                                         ncrystal_cfg=ncrystal_cfg
644
                                         )
645
        E.append(temp_E)
14✔
646
        # Since the energy grids are different, store the cross sections as
647
        # a tabulated function so they can be calculated on any grid needed.
648
        xs[name] = [openmc.data.Tabulated1D(temp_E, temp_xs[line])
14✔
649
                    for line in range(len(types))]
650

651
    # Condense the data for every nuclide
652
    # First create a union energy grid
653
    energy_grid = E[0]
14✔
654
    for grid in E[1:]:
14✔
655
        energy_grid = np.union1d(energy_grid, grid)
14✔
656

657
    # Now we can combine all the nuclidic data
658
    data = np.zeros((len(types), len(energy_grid)))
14✔
659
    for line in range(len(types)):
14✔
660
        if types[line] == 'unity':
14✔
661
            data[line, :] = 1.
×
662
        else:
663
            for nuclide in nuclides.items():
14✔
664
                name = nuclide[0]
14✔
665
                data[line, :] += (nuc_fractions[name] *
14✔
666
                                  xs[name][line](energy_grid))
667

668
    return energy_grid, data
14✔
669

670

671
def calculate_mgxs(this, types, orders=None, temperature=294.,
14✔
672
                   cross_sections=None, ce_cross_sections=None,
673
                   enrichment=None):
674
    """Calculates multi-group cross sections of a requested type.
675

676
    If the data for the nuclide or macroscopic object in the library is
677
    represented as angle-dependent data then this method will return the
678
    geometric average cross section over all angles.
679

680
    Parameters
681
    ----------
682
    this : str or openmc.Material
683
        Object to source data from. Nuclides and elements can be input as a str
684
    types : Iterable of values of PLOT_TYPES_MGXS
685
        The type of cross sections to calculate
686
    orders : Iterable of Integral, optional
687
        The scattering order or delayed group index to use for the
688
        corresponding entry in types. Defaults to the 0th order for scattering
689
        and the total delayed neutron data.
690
    temperature : float, optional
691
        Temperature in Kelvin to plot. If not specified, a default
692
        temperature of 294K will be plotted. Note that the nearest
693
        temperature in the library for each nuclide will be used as opposed
694
        to using any interpolation.
695
    cross_sections : str, optional
696
        Location of MGXS HDF5 Library file. Default is None.
697
    ce_cross_sections : str, optional
698
        Location of continuous-energy cross_sections.xml file. Default is None.
699
    enrichment : float, optional
700
        Enrichment for U235 in weight percent. For example, input 4.95 for
701
        4.95 weight percent enriched U. Default is None
702
        (natural composition).
703

704
    Returns
705
    -------
706
    energy_grid : numpy.ndarray
707
        Energies at which cross sections are calculated, in units of eV
708
    data : numpy.ndarray
709
        Cross sections calculated at the energy grid described by energy_grid
710

711
    """
712

713
    # Check types
714
    cv.check_type('temperature', temperature, Real)
×
715
    if enrichment:
×
716
        cv.check_type('enrichment', enrichment, Real)
×
717
    cv.check_iterable_type('types', types, str)
×
718

719
    cv.check_type("cross_sections", cross_sections, str)
×
720
    library = openmc.MGXSLibrary.from_hdf5(cross_sections)
×
721

722
    if this in ELEMENT_NAMES or isinstance(this, openmc.Material):
×
723
        mgxs = _calculate_mgxs_elem_mat(this, types, library, orders,
×
724
                                        temperature, ce_cross_sections,
725
                                        enrichment)
726
    elif isinstance(this, str):
×
727
        mgxs = _calculate_mgxs_nuc_macro(this, types, library, orders,
×
728
                                         temperature)
729
    else:
730
        raise TypeError("Invalid type")
×
731

732
    # Convert the data to the format needed
733
    data = np.zeros((len(types), 2 * library.energy_groups.num_groups))
×
734
    energy_grid = np.zeros(2 * library.energy_groups.num_groups)
×
735
    for g in range(library.energy_groups.num_groups):
×
736
        energy_grid[g * 2: g * 2 + 2] = \
×
737
            library.energy_groups.group_edges[g: g + 2]
738
    # Ensure the energy will show on a log-axis by replacing 0s with a
739
    # sufficiently small number
740
    energy_grid[0] = max(energy_grid[0], _MIN_E)
×
741

742
    for line in range(len(types)):
×
743
        for g in range(library.energy_groups.num_groups):
×
744
            data[line, g * 2: g * 2 + 2] = mgxs[line, g]
×
745

746
    return energy_grid[::-1], data
×
747

748

749
def _calculate_mgxs_nuc_macro(this, types, library, orders=None,
14✔
750
                              temperature=294.):
751
    """Determines the multi-group cross sections of a nuclide or macroscopic
752
    object.
753

754
    If the data for the nuclide or macroscopic object in the library is
755
    represented as angle-dependent data then this method will return the
756
    geometric average cross section over all angles.
757

758
    Parameters
759
    ----------
760
    this : str
761
        Object to source data from
762
    types : Iterable of str
763
        The type of cross sections to calculate; values can either be those
764
        in openmc.PLOT_TYPES_MGXS
765
    library : openmc.MGXSLibrary
766
        MGXS Library containing the data of interest
767
    orders : Iterable of Integral, optional
768
        The scattering order or delayed group index to use for the
769
        corresponding entry in types. Defaults to the 0th order for scattering
770
        and the total delayed neutron data.
771
    temperature : float, optional
772
        Temperature in Kelvin to plot. If not specified, a default
773
        temperature of 294K will be plotted. Note that the nearest
774
        temperature in the library for each nuclide will be used as opposed
775
        to using any interpolation.
776

777
    Returns
778
    -------
779
    data : numpy.ndarray
780
        Cross sections calculated at the energy grid described by energy_grid
781

782
    """
783

784
    # Check the parameters and grab order/delayed groups
785
    if orders:
×
786
        cv.check_iterable_type('orders', orders, Integral,
×
787
                               min_depth=len(types), max_depth=len(types))
788
    else:
789
        orders = [None] * len(types)
×
790
    for i, line in enumerate(types):
×
791
        cv.check_type("line", line, str)
×
792
        cv.check_value("line", line, PLOT_TYPES_MGXS)
×
793
        if orders[i]:
×
794
            cv.check_greater_than("order value", orders[i], 0, equality=True)
×
795

796
    xsdata = library.get_by_name(this)
×
797

798
    if xsdata is not None:
×
799
        # Obtain the nearest temperature
800
        t = np.abs(xsdata.temperatures - temperature).argmin()
×
801

802
        # Get the data
803
        data = np.zeros((len(types), library.energy_groups.num_groups))
×
804
        for i, line in enumerate(types):
×
805
            if 'fission' in line and not xsdata.fissionable:
×
806
                continue
×
807
            elif line == 'unity':
×
808
                data[i, :] = 1.
×
809
            else:
810
                # Now we have to get the cross section data and properly
811
                # treat it depending on the requested type.
812
                # First get the data in a generic fashion
813
                temp_data = getattr(xsdata, _PLOT_MGXS_ATTR[line])[t]
×
814
                shape = temp_data.shape[:]
×
815
                # If we have angular data, then want the geometric
816
                # average over all provided angles.  Since the angles are
817
                # equi-distant, un-weighted averaging will suffice
818
                if xsdata.representation == 'angle':
×
819
                    temp_data = np.mean(temp_data, axis=(0, 1))
×
820

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

883
    return data
×
884

885

886
def _calculate_mgxs_elem_mat(this, types, library, orders=None,
14✔
887
                             temperature=294., ce_cross_sections=None,
888
                             enrichment=None):
889
    """Determines the multi-group cross sections of an element or material
890
    object.
891

892
    If the data for the nuclide or macroscopic object in the library is
893
    represented as angle-dependent data then this method will return the
894
    geometric average cross section over all angles.
895

896
    Parameters
897
    ----------
898
    this : str or openmc.Material
899
        Object to source data from. Elements can be input as a str
900
    types : Iterable of str
901
        The type of cross sections to calculate; values can either be those
902
        in openmc.PLOT_TYPES_MGXS
903
    library : openmc.MGXSLibrary
904
        MGXS Library containing the data of interest
905
    orders : Iterable of Integral, optional
906
        The scattering order or delayed group index to use for the
907
        corresponding entry in types. Defaults to the 0th order for scattering
908
        and the total delayed neutron data.
909
    temperature : float, optional
910
        Temperature in Kelvin to plot. If not specified, a default
911
        temperature of 294K will be plotted. Note that the nearest
912
        temperature in the library for each nuclide will be used as opposed
913
        to using any interpolation.
914
    ce_cross_sections : str, optional
915
        Location of continuous-energy cross_sections.xml file. Default is None.
916
        This is used only for expanding the elements
917
    enrichment : float, optional
918
        Enrichment for U235 in weight percent. For example, input 4.95 for
919
        4.95 weight percent enriched U. Default is None
920
        (natural composition).
921

922
    Returns
923
    -------
924
    data : numpy.ndarray
925
        Cross sections calculated at the energy grid described by energy_grid
926

927
    """
928

929
    if isinstance(this, openmc.Material):
×
930
        if this.temperature is not None:
×
931
            T = this.temperature
×
932
        else:
933
            T = temperature
×
934

935
        # Check to see if we have nuclides/elements or a macroscopic object
936
        if this._macroscopic is not None:
×
937
            # We have macroscopics
938
            nuclides = {this._macroscopic: this.density}
×
939
        else:
940
            # Expand elements in to nuclides with atomic densities
941
            nuclides = this.get_nuclide_atom_densities()
×
942

943
        # For ease of processing split out nuc and nuc_density
944
        nuc_fraction = list(nuclides.values())
×
945
    else:
946
        T = temperature
×
947
        # Expand elements in to nuclides with atomic densities
948
        nuclides = openmc.Element(this).expand(100., 'ao', enrichment=enrichment,
×
949
                               cross_sections=ce_cross_sections)
950

951
        # For ease of processing split out nuc and nuc_fractions
952
        nuc_fraction = [nuclide[1] for nuclide in nuclides]
×
953

954
    nuc_data = []
×
955
    for nuclide in nuclides.items():
×
956
        nuc_data.append(_calculate_mgxs_nuc_macro(nuclide[0], types, library,
×
957
                                                  orders, T))
958

959
    # Combine across the nuclides
960
    data = np.zeros((len(types), library.energy_groups.num_groups))
×
961
    for line in range(len(types)):
×
962
        if types[line] == 'unity':
×
963
            data[line, :] = 1.
×
964
        else:
965
            for n in range(len(nuclides)):
×
966
                data[line, :] += nuc_fraction[n] * nuc_data[n][line, :]
×
967

968
    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