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

openmc-dev / openmc / 4851820307

pending completion
4851820307

push

github

GitHub
more flexible cross section plotting (#2478)

40 of 63 new or added lines in 1 file covered. (63.49%)

113 existing lines in 1 file now uncovered.

42622 of 50921 relevant lines covered (83.7%)

82539136.05 hits per line

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

51.9
/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
        return f'{this} {type}'
14✔
62
    elif this.name is '':
14✔
63
        return f'Material {this.id} {type}'
14✔
64
    else:
NEW
65
        return f'{this.name} {type}'
×
66

67

68
def _get_yaxis_label(reactions, divisor_types):
14✔
69
    """Gets a y axis label for the type of data plotted"""
70

71
    if all(isinstance(item, str) for item in reactions.keys()):
14✔
72
        stem = 'Microscopic'
14✔
73
        if divisor_types:
14✔
NEW
74
            mid, units = 'Data', ''
×
75
        else:
76
            mid, units = 'Cross Section', '[b]'
14✔
77
    elif all(isinstance(item, openmc.Material) for item in reactions.keys()):
14✔
78
        stem = 'Macroscopic'
14✔
79
        if divisor_types:
14✔
NEW
80
            mid, units = 'Data', ''
×
81
        else:
82
            mid, units = 'Cross Section', '[1/cm]'
14✔
83
    else:
84
        msg = "Mixture of openmc.Material and elements/nuclides. Invalid type for plotting"
14✔
85
        raise TypeError(msg)
14✔
86

87
    return f'{stem} {mid} {units}'
14✔
88

89

90
def _get_title(reactions):
14✔
91
    """Gets a title for the type of data plotted"""
92
    if len(reactions) == 1:
14✔
93
        this, = reactions
14✔
94
        name = this.name if isinstance(this, openmc.Material) else this
14✔
95
        return f'Cross Section Plot For {name}'
14✔
96
    else:
97
        return 'Cross Section Plot'
14✔
98

99

100
def plot_xs(reactions, divisor_types=None, temperature=294., axis=None,
14✔
101
            sab_name=None, ce_cross_sections=None, mg_cross_sections=None,
102
            enrichment=None, plot_CE=True, orders=None, divisor_orders=None,
103
            **kwargs):
104
    """Creates a figure of continuous-energy cross sections for this item.
105

106
    Parameters
107
    ----------
108
    reactions : dict
109
        keys can be either a nuclide or element in string form or an
110
        openmc.Material object. Values are the type of cross sections to
111
        include in the plot.
112
    divisor_types : Iterable of values of PLOT_TYPES, optional
113
        Cross section types which will divide those produced by types
114
        before plotting. A type of 'unity' can be used to effectively not
115
        divide some types.
116
    temperature : float, optional
117
        Temperature in Kelvin to plot. If not specified, a default
118
        temperature of 294K will be plotted. Note that the nearest
119
        temperature in the library for each nuclide will be used as opposed
120
        to using any interpolation.
121
    axis : matplotlib.axes, optional
122
        A previously generated axis to use for plotting. If not specified,
123
        a new axis and figure will be generated.
124
    sab_name : str, optional
125
        Name of S(a,b) library to apply to MT=2 data when applicable.
126
    ce_cross_sections : str, optional
127
        Location of cross_sections.xml file. Default is None.
128
    mg_cross_sections : str, optional
129
        Location of MGXS HDF5 Library file. Default is None.
130
    enrichment : float, optional
131
        Enrichment for U235 in weight percent. For example, input 4.95 for
132
        4.95 weight percent enriched U. Default is None.
133
    plot_CE : bool, optional
134
        Denotes whether or not continuous-energy will be plotted. Defaults to
135
        plotting the continuous-energy data.
136
    orders : Iterable of Integral, optional
137
        The scattering order or delayed group index to use for the
138
        corresponding entry in types. Defaults to the 0th order for scattering
139
        and the total delayed neutron data. This only applies to plots of
140
        multi-group data.
141
    divisor_orders : Iterable of Integral, optional
142
        Same as orders, but for divisor_types
143
    **kwargs :
144
        All keyword arguments are passed to
145
        :func:`matplotlib.pyplot.figure`.
146

147
    Returns
148
    -------
149
    fig : matplotlib.figure.Figure
150
        If axis is None, then a Matplotlib Figure of the generated
151
        cross section will be returned. Otherwise, a value of
152
        None will be returned as the figure and axes have already been
153
        generated.
154

155
    """
156
    import matplotlib.pyplot as plt
14✔
157

158
    cv.check_type("plot_CE", plot_CE, bool)
14✔
159

160
    # Generate the plot
161
    if axis is None:
14✔
162
        fig, ax = plt.subplots(**kwargs)
14✔
163
    else:
UNCOV
164
        fig = None
×
UNCOV
165
        ax = axis
×
166

167
    all_types = []
14✔
168

169
    for this, types in reactions.items():
14✔
170
        all_types = all_types + types
14✔
171

172
        if plot_CE:
14✔
173
            cv.check_type("this", this, (str, openmc.Material))
14✔
174
            # Calculate for the CE cross sections
175
            E, data = calculate_cexs(this, types, temperature, sab_name,
14✔
176
                                    ce_cross_sections, enrichment)
177
            if divisor_types:
14✔
NEW
178
                cv.check_length('divisor types', divisor_types, len(types))
×
NEW
179
                Ediv, data_div = calculate_cexs(this, divisor_types, temperature,
×
180
                                                sab_name, ce_cross_sections,
181
                                                enrichment)
182

183
                # Create a new union grid, interpolate data and data_div on to that
184
                # grid, and then do the actual division
NEW
185
                Enum = E[:]
×
NEW
186
                E = np.union1d(Enum, Ediv)
×
NEW
187
                data_new = np.zeros((len(types), len(E)))
×
188

NEW
189
                for line in range(len(types)):
×
NEW
190
                    data_new[line, :] = \
×
191
                        np.divide(np.interp(E, Enum, data[line, :]),
192
                                np.interp(E, Ediv, data_div[line, :]))
NEW
193
                    if divisor_types[line] != 'unity':
×
NEW
194
                        types[line] = types[line] + ' / ' + divisor_types[line]
×
NEW
195
                data = data_new
×
196
        else:
197
            # Calculate for MG cross sections
NEW
198
            E, data = calculate_mgxs(this, types, orders, temperature,
×
199
                                    mg_cross_sections, ce_cross_sections,
200
                                    enrichment)
201

NEW
202
            if divisor_types:
×
NEW
203
                cv.check_length('divisor types', divisor_types, len(types))
×
NEW
204
                Ediv, data_div = calculate_mgxs(this, divisor_types,
×
205
                                                divisor_orders, temperature,
206
                                                mg_cross_sections,
207
                                                ce_cross_sections, enrichment)
208

209
                # Perform the division
NEW
210
                for line in range(len(types)):
×
NEW
211
                    data[line, :] /= data_div[line, :]
×
NEW
212
                    if divisor_types[line] != 'unity':
×
NEW
213
                        types[line] += ' / ' + divisor_types[line]
×
214

215
        # Plot the data
216
        for i in range(len(data)):
14✔
217
            data[i, :] = np.nan_to_num(data[i, :])
14✔
218
            if np.sum(data[i, :]) > 0.:
14✔
219
                ax.plot(E, data[i, :], label=_get_legend_label(this, types[i]))
14✔
220

221
    # Set to loglog or semilogx depending on if we are plotting a data
222
    # type which we expect to vary linearly
223
    if set(all_types).issubset(PLOT_TYPES_LINEAR):
14✔
NEW
224
        ax.set_xscale('log')
×
NEW
225
        ax.set_yscale('linear')
×
226
    else:
227
        ax.set_xscale('log')
14✔
228
        ax.set_yscale('log')
14✔
229

230
    ax.set_xlabel('Energy [eV]')
14✔
231
    if plot_CE:
14✔
232
        ax.set_xlim(_MIN_E, _MAX_E)
14✔
233
    else:
234
        ax.set_xlim(E[-1], E[0])
×
235

236
    ax.set_ylabel(_get_yaxis_label(reactions, divisor_types))
14✔
237
    ax.legend(loc='best')
14✔
238
    ax.set_title(_get_title(reactions))
14✔
239

240
    return fig
14✔
241

242

243

244
def calculate_cexs(this, types, temperature=294., sab_name=None,
14✔
245
                   cross_sections=None, enrichment=None, ncrystal_cfg=None):
246
    """Calculates continuous-energy cross sections of a requested type.
247

248
    Parameters
249
    ----------
250
    this : str or openmc.Material
251
        Object to source data from. Nuclides and elements should be input as a
252
        str
253
    types : Iterable of values of PLOT_TYPES
254
        The type of cross sections to calculate
255
    temperature : float, optional
256
        Temperature in Kelvin to plot. If not specified, a default
257
        temperature of 294K will be plotted. Note that the nearest
258
        temperature in the library for each nuclide will be used as opposed
259
        to using any interpolation.
260
    sab_name : str, optional
261
        Name of S(a,b) library to apply to MT=2 data when applicable.
262
    cross_sections : str, optional
263
        Location of cross_sections.xml file. Default is None.
264
    enrichment : float, optional
265
        Enrichment for U235 in weight percent. For example, input 4.95 for
266
        4.95 weight percent enriched U. Default is None
267
        (natural composition).
268
    ncrystal_cfg : str, optional
269
        Configuration string for NCrystal material.
270

271
    Returns
272
    -------
273
    energy_grid : numpy.ndarray
274
        Energies at which cross sections are calculated, in units of eV
275
    data : numpy.ndarray
276
        Cross sections calculated at the energy grid described by energy_grid
277

278
    """
279

280
    # Check types
281
    cv.check_type('this', this, (str, openmc.Material))
14✔
282
    cv.check_type('temperature', temperature, Real)
14✔
283
    if sab_name:
14✔
284
        cv.check_type('sab_name', sab_name, str)
14✔
285
    if enrichment:
14✔
UNCOV
286
        cv.check_type('enrichment', enrichment, Real)
×
287

288
    if isinstance(this, str):
14✔
289
        if this in ELEMENT_NAMES:
14✔
290
            energy_grid, data = _calculate_cexs_elem_mat(
14✔
291
                this, types, temperature, cross_sections, sab_name, enrichment
292
            )
293
        else:
294
            energy_grid, xs = _calculate_cexs_nuclide(
14✔
295
                this, types, temperature, sab_name, cross_sections,
296
                ncrystal_cfg
297
            )
298

299
            # Convert xs (Iterable of Callable) to a grid of cross section values
300
            # calculated on the points in energy_grid for consistency with the
301
            # element and material functions.
302
            data = np.zeros((len(types), len(energy_grid)))
14✔
303
            for line in range(len(types)):
14✔
304
                data[line, :] = xs[line](energy_grid)
14✔
305
    else:
306
        energy_grid, data = _calculate_cexs_elem_mat(this, types, temperature,
14✔
307
                                                     cross_sections)
308

309
    return energy_grid, data
14✔
310

311

312
def _calculate_cexs_nuclide(this, types, temperature=294., sab_name=None,
14✔
313
                            cross_sections=None, ncrystal_cfg=None):
314
    """Calculates continuous-energy cross sections of a requested type.
315

316
    Parameters
317
    ----------
318
    this : str
319
        Nuclide object to source data from
320
    types : Iterable of str or Integral
321
        The type of cross sections to calculate; values can either be those
322
        in openmc.PLOT_TYPES or keys from openmc.data.REACTION_MT which
323
        correspond to a reaction description e.g '(n,2n)' or integers which
324
        correspond to reaction channel (MT) numbers.
325
    temperature : float, optional
326
        Temperature in Kelvin to plot. If not specified, a default
327
        temperature of 294K will be plotted. Note that the nearest
328
        temperature in the library for each nuclide will be used as opposed
329
        to using any interpolation.
330
    sab_name : str, optional
331
        Name of S(a,b) library to apply to MT=2 data when applicable.
332
    cross_sections : str, optional
333
        Location of cross_sections.xml file. Default is None.
334
    ncrystal_cfg : str, optional
335
        Configuration string for NCrystal material.
336

337
    Returns
338
    -------
339
    energy_grid : numpy.ndarray
340
        Energies at which cross sections are calculated, in units of eV
341
    data : Iterable of Callable
342
        Requested cross section functions
343

344
    """
345

346
    # Load the library
347
    library = openmc.data.DataLibrary.from_xml(cross_sections)
14✔
348

349
    # Convert temperature to format needed for access in the library
350
    strT = f"{int(round(temperature))}K"
14✔
351
    T = temperature
14✔
352

353
    # Now we can create the data sets to be plotted
354
    energy_grid = []
14✔
355
    xs = []
14✔
356
    lib = library.get_by_material(this)
14✔
357
    if lib is not None:
14✔
358
        nuc = openmc.data.IncidentNeutron.from_hdf5(lib['path'])
14✔
359
        # Obtain the nearest temperature
360
        if strT in nuc.temperatures:
14✔
361
            nucT = strT
14✔
362
        else:
UNCOV
363
            delta_T = np.array(nuc.kTs) - T * openmc.data.K_BOLTZMANN
×
UNCOV
364
            closest_index = np.argmin(np.abs(delta_T))
×
UNCOV
365
            nucT = nuc.temperatures[closest_index]
×
366

367
        # Prep S(a,b) data if needed
368
        if sab_name:
14✔
369
            sab = openmc.data.ThermalScattering.from_hdf5(sab_name)
14✔
370
            # Obtain the nearest temperature
371
            if strT in sab.temperatures:
14✔
UNCOV
372
                sabT = strT
×
373
            else:
374
                delta_T = np.array(sab.kTs) - T * openmc.data.K_BOLTZMANN
14✔
375
                closest_index = np.argmin(np.abs(delta_T))
14✔
376
                sabT = sab.temperatures[closest_index]
14✔
377

378
            # Create an energy grid composed the S(a,b) and the nuclide's grid
379
            grid = nuc.energy[nucT]
14✔
380
            sab_Emax = 0.
14✔
381
            sab_funcs = []
14✔
382
            if sab.elastic is not None:
14✔
UNCOV
383
                elastic = sab.elastic.xs[sabT]
×
UNCOV
384
                if isinstance(elastic, openmc.data.CoherentElastic):
×
UNCOV
385
                    grid = np.union1d(grid, elastic.bragg_edges)
×
UNCOV
386
                    if elastic.bragg_edges[-1] > sab_Emax:
×
UNCOV
387
                        sab_Emax = elastic.bragg_edges[-1]
×
388
                elif isinstance(elastic, openmc.data.Tabulated1D):
×
389
                    grid = np.union1d(grid, elastic.x)
×
390
                    if elastic.x[-1] > sab_Emax:
×
UNCOV
391
                        sab_Emax = elastic.x[-1]
×
UNCOV
392
                sab_funcs.append(elastic)
×
393
            if sab.inelastic is not None:
14✔
394
                inelastic = sab.inelastic.xs[sabT]
14✔
395
                grid = np.union1d(grid, inelastic.x)
14✔
396
                if inelastic.x[-1] > sab_Emax:
14✔
397
                        sab_Emax = inelastic.x[-1]
14✔
398
                sab_funcs.append(inelastic)
14✔
399
            energy_grid = grid
14✔
400
        else:
401
            energy_grid = nuc.energy[nucT]
14✔
402

403
        # Parse the types
404
        mts = []
14✔
405
        ops = []
14✔
406
        yields = []
14✔
407
        for line in types:
14✔
408
            if line in PLOT_TYPES:
14✔
409
                tmp_mts = [mtj for mti in PLOT_TYPES_MT[line] for mtj in
14✔
410
                           nuc.get_reaction_components(mti)]
411
                mts.append(tmp_mts)
14✔
412
                if line.startswith('nu'):
14✔
413
                    yields.append(True)
×
414
                else:
415
                    yields.append(False)
14✔
416
                if XI_MT in tmp_mts:
14✔
417
                    ops.append((np.add,) * (len(tmp_mts) - 2) + (np.multiply,))
×
418
                else:
419
                    ops.append((np.add,) * (len(tmp_mts) - 1))
14✔
420
            elif line in openmc.data.REACTION_MT:
14✔
UNCOV
421
                mt_number = openmc.data.REACTION_MT[line]
×
UNCOV
422
                cv.check_type('MT in types', mt_number, Integral)
×
UNCOV
423
                cv.check_greater_than('MT in types', mt_number, 0)
×
UNCOV
424
                tmp_mts = nuc.get_reaction_components(mt_number)
×
UNCOV
425
                mts.append(tmp_mts)
×
UNCOV
426
                ops.append((np.add,) * (len(tmp_mts) - 1))
×
UNCOV
427
                yields.append(False)
×
428
            elif isinstance(line, int):
14✔
429
                # Not a built-in type, we have to parse it ourselves
430
                cv.check_type('MT in types', line, Integral)
14✔
431
                cv.check_greater_than('MT in types', line, 0)
14✔
432
                tmp_mts = nuc.get_reaction_components(line)
14✔
433
                mts.append(tmp_mts)
14✔
434
                ops.append((np.add,) * (len(tmp_mts) - 1))
14✔
435
                yields.append(False)
14✔
436
            else:
UNCOV
437
                raise TypeError("Invalid type", line)
×
438

439
        for i, mt_set in enumerate(mts):
14✔
440
            # Get the reaction xs data from the nuclide
441
            funcs = []
14✔
442
            op = ops[i]
14✔
443
            for mt in mt_set:
14✔
444
                if mt == 2:
14✔
445
                    if sab_name:
14✔
446
                        # Then we need to do a piece-wise function of
447
                        # The S(a,b) and non-thermal data
448
                        sab_sum = openmc.data.Sum(sab_funcs)
14✔
449
                        pw_funcs = openmc.data.Regions1D(
14✔
450
                            [sab_sum, nuc[mt].xs[nucT]],
451
                            [sab_Emax])
452
                        funcs.append(pw_funcs)
14✔
453
                    elif ncrystal_cfg:
14✔
UNCOV
454
                        import NCrystal
×
UNCOV
455
                        nc_scatter = NCrystal.createScatter(ncrystal_cfg)
×
UNCOV
456
                        nc_func = nc_scatter.crossSectionNonOriented
×
UNCOV
457
                        nc_emax = 5 # eV # this should be obtained from NCRYSTAL_MAX_ENERGY
×
UNCOV
458
                        energy_grid = np.union1d(np.geomspace(min(energy_grid),
×
459
                                                              1.1*nc_emax,
460
                                                              1000),energy_grid) # NCrystal does not have
461
                                                                                 # an intrinsic energy grid
462
                        pw_funcs = openmc.data.Regions1D(
×
463
                            [nc_func, nuc[mt].xs[nucT]],
464
                            [nc_emax])
UNCOV
465
                        funcs.append(pw_funcs)
×
466
                    else:
467
                        funcs.append(nuc[mt].xs[nucT])
14✔
468
                elif mt in nuc:
14✔
469
                    if yields[i]:
14✔
470
                        # Get the total yield first if available. This will be
471
                        # used primarily for fission.
UNCOV
472
                        for prod in chain(nuc[mt].products,
×
473
                                          nuc[mt].derived_products):
UNCOV
474
                            if prod.particle == 'neutron' and \
×
475
                                prod.emission_mode == 'total':
UNCOV
476
                                func = openmc.data.Combination(
×
477
                                    [nuc[mt].xs[nucT], prod.yield_],
478
                                    [np.multiply])
479
                                funcs.append(func)
×
480
                                break
×
481
                        else:
482
                            # Total doesn't exist so we have to create from
483
                            # prompt and delayed. This is used for scatter
484
                            # multiplication.
UNCOV
485
                            func = None
×
UNCOV
486
                            for prod in chain(nuc[mt].products,
×
487
                                              nuc[mt].derived_products):
UNCOV
488
                                if prod.particle == 'neutron' and \
×
489
                                    prod.emission_mode != 'total':
490
                                    if func:
×
UNCOV
491
                                        func = openmc.data.Combination(
×
492
                                            [prod.yield_, func], [np.add])
493
                                    else:
UNCOV
494
                                        func = prod.yield_
×
UNCOV
495
                            if func:
×
UNCOV
496
                                funcs.append(openmc.data.Combination(
×
497
                                    [func, nuc[mt].xs[nucT]], [np.multiply]))
498
                            else:
499
                                # If func is still None, then there were no
500
                                # products. In that case, assume the yield is
501
                                # one as its not provided for some summed
502
                                # reactions like MT=4
UNCOV
503
                                funcs.append(nuc[mt].xs[nucT])
×
504
                    else:
505
                        funcs.append(nuc[mt].xs[nucT])
14✔
UNCOV
506
                elif mt == UNITY_MT:
×
UNCOV
507
                    funcs.append(lambda x: 1.)
×
UNCOV
508
                elif mt == XI_MT:
×
UNCOV
509
                    awr = nuc.atomic_weight_ratio
×
510
                    alpha = ((awr - 1.) / (awr + 1.))**2
×
511
                    xi = 1. + alpha * np.log(alpha) / (1. - alpha)
×
UNCOV
512
                    funcs.append(lambda x: xi)
×
513
                else:
UNCOV
514
                    funcs.append(lambda x: 0.)
×
515
            funcs = funcs if funcs else [lambda x: 0.]
14✔
516
            xs.append(openmc.data.Combination(funcs, op))
14✔
517
    else:
UNCOV
518
        raise ValueError(this + " not in library")
×
519

520
    return energy_grid, xs
14✔
521

522

523
def _calculate_cexs_elem_mat(this, types, temperature=294.,
14✔
524
                             cross_sections=None, sab_name=None,
525
                             enrichment=None):
526
    """Calculates continuous-energy cross sections of a requested type.
527

528
    Parameters
529
    ----------
530
    this : openmc.Material or str
531
        Object to source data from. Element can be input as str
532
    types : Iterable of values of PLOT_TYPES
533
        The type of cross sections to calculate
534
    temperature : float, optional
535
        Temperature in Kelvin to plot. If not specified, a default
536
        temperature of 294K will be plotted. Note that the nearest
537
        temperature in the library for each nuclide will be used as opposed
538
        to using any interpolation.
539
    cross_sections : str, optional
540
        Location of cross_sections.xml file. Default is None.
541
    sab_name : str, optional
542
        Name of S(a,b) library to apply to MT=2 data when applicable.
543
    enrichment : float, optional
544
        Enrichment for U235 in weight percent. For example, input 4.95 for
545
        4.95 weight percent enriched U. Default is None
546
        (natural composition).
547

548
    Returns
549
    -------
550
    energy_grid : numpy.ndarray
551
        Energies at which cross sections are calculated, in units of eV
552
    data : numpy.ndarray
553
        Cross sections calculated at the energy grid described by energy_grid
554

555
    """
556

557
    if isinstance(this, openmc.Material):
14✔
558
        if this.temperature is not None:
14✔
UNCOV
559
            T = this.temperature
×
560
        else:
561
            T = temperature
14✔
562
    else:
563
        T = temperature
14✔
564

565
    # Load the library
566
    library = openmc.data.DataLibrary.from_xml(cross_sections)
14✔
567

568
    ncrystal_cfg = None
14✔
569
    if isinstance(this, openmc.Material):
14✔
570
        # Expand elements in to nuclides with atomic densities
571
        nuc_fractions = this.get_nuclide_atom_densities()
14✔
572
        # Create a dict of [nuclide name] = nuclide object to carry forward
573
        # with a common nuclides format between openmc.Material and Elements
574
        nuclides = {nuclide: nuclide for nuclide in nuc_fractions}
14✔
575
        # Add NCrystal cfg string if it exists
576
        ncrystal_cfg = this.ncrystal_cfg
14✔
577
    else:
578
        # Expand elements in to nuclides with atomic densities
579
        nuclides = openmc.Element(this).expand(1., 'ao', enrichment=enrichment,
14✔
580
                               cross_sections=cross_sections)
581
        # For ease of processing split out the nuclide and its fraction
582
        nuc_fractions = {nuclide[0]: nuclide[1] for nuclide in nuclides}
14✔
583
        # Create a dict of [nuclide name] = nuclide object to carry forward
584
        # with a common nuclides format between openmc.Material and Elements
585
        nuclides = {nuclide[0]: nuclide[0] for nuclide in nuclides}
14✔
586

587
    # Identify the nuclides which have S(a,b) data
588
    sabs = {}
14✔
589
    for nuclide in nuclides.items():
14✔
590
        sabs[nuclide[0]] = None
14✔
591
    if isinstance(this, openmc.Material):
14✔
592
        for sab_name, _ in this._sab:
14✔
593
            sab = openmc.data.ThermalScattering.from_hdf5(
14✔
594
                library.get_by_material(sab_name, data_type='thermal')['path'])
595
            for nuc in sab.nuclides:
14✔
596
                sabs[nuc] = library.get_by_material(sab_name,
14✔
597
                        data_type='thermal')['path']
598
    else:
599
        if sab_name:
14✔
UNCOV
600
            sab = openmc.data.ThermalScattering.from_hdf5(sab_name)
×
UNCOV
601
            for nuc in sab.nuclides:
×
UNCOV
602
                sabs[nuc] = library.get_by_material(sab_name,
×
603
                        data_type='thermal')['path']
604

605
    # Now we can create the data sets to be plotted
606
    xs = {}
14✔
607
    E = []
14✔
608
    for nuclide in nuclides.items():
14✔
609
        name = nuclide[0]
14✔
610
        nuc = nuclide[1]
14✔
611
        sab_tab = sabs[name]
14✔
612
        temp_E, temp_xs = calculate_cexs(nuc, types, T, sab_tab, cross_sections,
14✔
613
                                         ncrystal_cfg=ncrystal_cfg
614
                                         )
615
        E.append(temp_E)
14✔
616
        # Since the energy grids are different, store the cross sections as
617
        # a tabulated function so they can be calculated on any grid needed.
618
        xs[name] = [openmc.data.Tabulated1D(temp_E, temp_xs[line])
14✔
619
                    for line in range(len(types))]
620

621
    # Condense the data for every nuclide
622
    # First create a union energy grid
623
    energy_grid = E[0]
14✔
624
    for grid in E[1:]:
14✔
625
        energy_grid = np.union1d(energy_grid, grid)
14✔
626

627
    # Now we can combine all the nuclidic data
628
    data = np.zeros((len(types), len(energy_grid)))
14✔
629
    for line in range(len(types)):
14✔
630
        if types[line] == 'unity':
14✔
UNCOV
631
            data[line, :] = 1.
×
632
        else:
633
            for nuclide in nuclides.items():
14✔
634
                name = nuclide[0]
14✔
635
                data[line, :] += (nuc_fractions[name] *
14✔
636
                                  xs[name][line](energy_grid))
637

638
    return energy_grid, data
14✔
639

640

641
def calculate_mgxs(this, types, orders=None, temperature=294.,
14✔
642
                   cross_sections=None, ce_cross_sections=None,
643
                   enrichment=None):
644
    """Calculates multi-group cross sections of a requested type.
645

646
    If the data for the nuclide or macroscopic object in the library is
647
    represented as angle-dependent data then this method will return the
648
    geometric average cross section over all angles.
649

650
    Parameters
651
    ----------
652
    this : str or openmc.Material
653
        Object to source data from. Nuclides and elements can be input as a str
654
    types : Iterable of values of PLOT_TYPES_MGXS
655
        The type of cross sections to calculate
656
    orders : Iterable of Integral, optional
657
        The scattering order or delayed group index to use for the
658
        corresponding entry in types. Defaults to the 0th order for scattering
659
        and the total delayed neutron data.
660
    temperature : float, optional
661
        Temperature in Kelvin to plot. If not specified, a default
662
        temperature of 294K will be plotted. Note that the nearest
663
        temperature in the library for each nuclide will be used as opposed
664
        to using any interpolation.
665
    cross_sections : str, optional
666
        Location of MGXS HDF5 Library file. Default is None.
667
    ce_cross_sections : str, optional
668
        Location of continuous-energy cross_sections.xml file. Default is None.
669
    enrichment : float, optional
670
        Enrichment for U235 in weight percent. For example, input 4.95 for
671
        4.95 weight percent enriched U. Default is None
672
        (natural composition).
673

674
    Returns
675
    -------
676
    energy_grid : numpy.ndarray
677
        Energies at which cross sections are calculated, in units of eV
678
    data : numpy.ndarray
679
        Cross sections calculated at the energy grid described by energy_grid
680

681
    """
682

683
    # Check types
UNCOV
684
    cv.check_type('temperature', temperature, Real)
×
UNCOV
685
    if enrichment:
×
UNCOV
686
        cv.check_type('enrichment', enrichment, Real)
×
UNCOV
687
    cv.check_iterable_type('types', types, str)
×
688

UNCOV
689
    cv.check_type("cross_sections", cross_sections, str)
×
UNCOV
690
    library = openmc.MGXSLibrary.from_hdf5(cross_sections)
×
691

UNCOV
692
    if this in ELEMENT_NAMES or isinstance(this, openmc.Material):
×
UNCOV
693
        mgxs = _calculate_mgxs_elem_mat(this, types, library, orders,
×
694
                                        temperature, ce_cross_sections,
695
                                        enrichment)
UNCOV
696
    elif isinstance(this, str):
×
UNCOV
697
        mgxs = _calculate_mgxs_nuc_macro(this, types, library, orders,
×
698
                                         temperature)
699
    else:
UNCOV
700
        raise TypeError("Invalid type")
×
701

702
    # Convert the data to the format needed
UNCOV
703
    data = np.zeros((len(types), 2 * library.energy_groups.num_groups))
×
UNCOV
704
    energy_grid = np.zeros(2 * library.energy_groups.num_groups)
×
UNCOV
705
    for g in range(library.energy_groups.num_groups):
×
UNCOV
706
        energy_grid[g * 2: g * 2 + 2] = \
×
707
            library.energy_groups.group_edges[g: g + 2]
708
    # Ensure the energy will show on a log-axis by replacing 0s with a
709
    # sufficiently small number
710
    energy_grid[0] = max(energy_grid[0], _MIN_E)
×
711

712
    for line in range(len(types)):
×
UNCOV
713
        for g in range(library.energy_groups.num_groups):
×
714
            data[line, g * 2: g * 2 + 2] = mgxs[line, g]
×
715

UNCOV
716
    return energy_grid[::-1], data
×
717

718

719
def _calculate_mgxs_nuc_macro(this, types, library, orders=None,
14✔
720
                              temperature=294.):
721
    """Determines the multi-group cross sections of a nuclide or macroscopic
722
    object.
723

724
    If the data for the nuclide or macroscopic object in the library is
725
    represented as angle-dependent data then this method will return the
726
    geometric average cross section over all angles.
727

728
    Parameters
729
    ----------
730
    this : str
731
        Object to source data from
732
    types : Iterable of str
733
        The type of cross sections to calculate; values can either be those
734
        in openmc.PLOT_TYPES_MGXS
735
    library : openmc.MGXSLibrary
736
        MGXS Library containing the data of interest
737
    orders : Iterable of Integral, optional
738
        The scattering order or delayed group index to use for the
739
        corresponding entry in types. Defaults to the 0th order for scattering
740
        and the total delayed neutron data.
741
    temperature : float, optional
742
        Temperature in Kelvin to plot. If not specified, a default
743
        temperature of 294K will be plotted. Note that the nearest
744
        temperature in the library for each nuclide will be used as opposed
745
        to using any interpolation.
746

747
    Returns
748
    -------
749
    data : numpy.ndarray
750
        Cross sections calculated at the energy grid described by energy_grid
751

752
    """
753

754
    # Check the parameters and grab order/delayed groups
UNCOV
755
    if orders:
×
UNCOV
756
        cv.check_iterable_type('orders', orders, Integral,
×
757
                               min_depth=len(types), max_depth=len(types))
758
    else:
UNCOV
759
        orders = [None] * len(types)
×
UNCOV
760
    for i, line in enumerate(types):
×
UNCOV
761
        cv.check_type("line", line, str)
×
UNCOV
762
        cv.check_value("line", line, PLOT_TYPES_MGXS)
×
UNCOV
763
        if orders[i]:
×
UNCOV
764
            cv.check_greater_than("order value", orders[i], 0, equality=True)
×
765

UNCOV
766
    xsdata = library.get_by_name(this)
×
767

UNCOV
768
    if xsdata is not None:
×
769
        # Obtain the nearest temperature
UNCOV
770
        t = np.abs(xsdata.temperatures - temperature).argmin()
×
771

772
        # Get the data
UNCOV
773
        data = np.zeros((len(types), library.energy_groups.num_groups))
×
UNCOV
774
        for i, line in enumerate(types):
×
UNCOV
775
            if 'fission' in line and not xsdata.fissionable:
×
UNCOV
776
                continue
×
UNCOV
777
            elif line == 'unity':
×
UNCOV
778
                data[i, :] = 1.
×
779
            else:
780
                # Now we have to get the cross section data and properly
781
                # treat it depending on the requested type.
782
                # First get the data in a generic fashion
UNCOV
783
                temp_data = getattr(xsdata, _PLOT_MGXS_ATTR[line])[t]
×
784
                shape = temp_data.shape[:]
×
785
                # If we have angular data, then want the geometric
786
                # average over all provided angles.  Since the angles are
787
                # equi-distant, un-weighted averaging will suffice
788
                if xsdata.representation == 'angle':
×
789
                    temp_data = np.mean(temp_data, axis=(0, 1))
×
790

791
                # Now we can look at the shape of the data to identify how
792
                # it should be modified to produce an array of values
793
                # with groups.
UNCOV
794
                if shape in (xsdata.xs_shapes["[G']"],
×
795
                             xsdata.xs_shapes["[G]"]):
796
                    # Then the data is already an array vs groups so copy
797
                    # and move along
798
                    data[i, :] = temp_data
×
799
                elif shape == xsdata.xs_shapes["[G][G']"]:
×
800
                    # Sum the data over outgoing groups to create our array vs
801
                    # groups
802
                    data[i, :] = np.sum(temp_data, axis=1)
×
803
                elif shape == xsdata.xs_shapes["[DG]"]:
×
804
                    # Then we have a constant vs groups with a value for each
805
                    # delayed group. The user-provided value of orders tells us
806
                    # which delayed group we want. If none are provided, then
807
                    # we sum all the delayed groups together.
808
                    if orders[i]:
×
809
                        if orders[i] < len(shape[0]):
×
UNCOV
810
                            data[i, :] = temp_data[orders[i]]
×
811
                    else:
UNCOV
812
                        data[i, :] = np.sum(temp_data[:])
×
813
                elif shape in (xsdata.xs_shapes["[DG][G']"],
×
814
                               xsdata.xs_shapes["[DG][G]"]):
815
                    # Then we have an array vs groups with values for each
816
                    # delayed group. The user-provided value of orders tells us
817
                    # which delayed group we want. If none are provided, then
818
                    # we sum all the delayed groups together.
819
                    if orders[i]:
×
UNCOV
820
                        if orders[i] < len(shape[0]):
×
UNCOV
821
                            data[i, :] = temp_data[orders[i], :]
×
822
                    else:
823
                        data[i, :] = np.sum(temp_data[:, :], axis=0)
×
824
                elif shape == xsdata.xs_shapes["[DG][G][G']"]:
×
825
                    # Then we have a delayed group matrix. We will first
826
                    # remove the outgoing group dependency
827
                    temp_data = np.sum(temp_data, axis=-1)
×
828
                    # And then proceed in exactly the same manner as the
829
                    # "[DG][G']" or "[DG][G]" shapes in the previous block.
UNCOV
830
                    if orders[i]:
×
UNCOV
831
                        if orders[i] < len(shape[0]):
×
UNCOV
832
                            data[i, :] = temp_data[orders[i], :]
×
833
                    else:
834
                        data[i, :] = np.sum(temp_data[:, :], axis=0)
×
835
                elif shape == xsdata.xs_shapes["[G][G'][Order]"]:
×
836
                    # This is a scattering matrix with angular data
837
                    # First remove the outgoing group dependence
838
                    temp_data = np.sum(temp_data, axis=1)
×
839
                    # The user either provided a specific order or we resort
840
                    # to the default 0th order
UNCOV
841
                    if orders[i]:
×
UNCOV
842
                        order = orders[i]
×
843
                    else:
844
                        order = 0
×
845
                    # If the order is available, store the data for that order
846
                    # if it is not available, then the expansion coefficient
847
                    # is zero and thus we already have the correct value.
848
                    if order < shape[1]:
×
849
                        data[i, :] = temp_data[:, order]
×
850
    else:
UNCOV
851
        raise ValueError(f"{this} not present in provided MGXS library")
×
852

UNCOV
853
    return data
×
854

855

856
def _calculate_mgxs_elem_mat(this, types, library, orders=None,
14✔
857
                             temperature=294., ce_cross_sections=None,
858
                             enrichment=None):
859
    """Determines the multi-group cross sections of an element or material
860
    object.
861

862
    If the data for the nuclide or macroscopic object in the library is
863
    represented as angle-dependent data then this method will return the
864
    geometric average cross section over all angles.
865

866
    Parameters
867
    ----------
868
    this : str or openmc.Material
869
        Object to source data from. Elements can be input as a str
870
    types : Iterable of str
871
        The type of cross sections to calculate; values can either be those
872
        in openmc.PLOT_TYPES_MGXS
873
    library : openmc.MGXSLibrary
874
        MGXS Library containing the data of interest
875
    orders : Iterable of Integral, optional
876
        The scattering order or delayed group index to use for the
877
        corresponding entry in types. Defaults to the 0th order for scattering
878
        and the total delayed neutron data.
879
    temperature : float, optional
880
        Temperature in Kelvin to plot. If not specified, a default
881
        temperature of 294K will be plotted. Note that the nearest
882
        temperature in the library for each nuclide will be used as opposed
883
        to using any interpolation.
884
    ce_cross_sections : str, optional
885
        Location of continuous-energy cross_sections.xml file. Default is None.
886
        This is used only for expanding the elements
887
    enrichment : float, optional
888
        Enrichment for U235 in weight percent. For example, input 4.95 for
889
        4.95 weight percent enriched U. Default is None
890
        (natural composition).
891

892
    Returns
893
    -------
894
    data : numpy.ndarray
895
        Cross sections calculated at the energy grid described by energy_grid
896

897
    """
898

UNCOV
899
    if isinstance(this, openmc.Material):
×
UNCOV
900
        if this.temperature is not None:
×
UNCOV
901
            T = this.temperature
×
902
        else:
UNCOV
903
            T = temperature
×
904

905
        # Check to see if we have nuclides/elements or a macroscopic object
UNCOV
906
        if this._macroscopic is not None:
×
907
            # We have macroscopics
UNCOV
908
            nuclides = {this._macroscopic: this.density}
×
909
        else:
910
            # Expand elements in to nuclides with atomic densities
UNCOV
911
            nuclides = this.get_nuclide_atom_densities()
×
912

913
        # For ease of processing split out nuc and nuc_density
UNCOV
914
        nuc_fraction = list(nuclides.values())
×
915
    else:
UNCOV
916
        T = temperature
×
917
        # Expand elements in to nuclides with atomic densities
UNCOV
918
        nuclides = openmc.Element(this).expand(100., 'ao', enrichment=enrichment,
×
919
                               cross_sections=ce_cross_sections)
920

921
        # For ease of processing split out nuc and nuc_fractions
UNCOV
922
        nuc_fraction = [nuclide[1] for nuclide in nuclides]
×
923

924
    nuc_data = []
×
925
    for nuclide in nuclides.items():
×
926
        nuc_data.append(_calculate_mgxs_nuc_macro(nuclide[0], types, library,
×
927
                                                  orders, T))
928

929
    # Combine across the nuclides
UNCOV
930
    data = np.zeros((len(types), library.energy_groups.num_groups))
×
931
    for line in range(len(types)):
×
UNCOV
932
        if types[line] == 'unity':
×
933
            data[line, :] = 1.
×
934
        else:
UNCOV
935
            for n in range(len(nuclides)):
×
936
                data[line, :] += nuc_fraction[n] * nuc_data[n][line, :]
×
937

UNCOV
938
    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