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

openmc-dev / openmc / 12776996362

14 Jan 2025 09:49PM UTC coverage: 84.938% (+0.2%) from 84.729%
12776996362

Pull #3133

github

web-flow
Merge 0495246d9 into 549cc0973
Pull Request #3133: Kinetics parameters using Iterated Fission Probability

318 of 330 new or added lines in 10 files covered. (96.36%)

1658 existing lines in 66 files now uncovered.

50402 of 59340 relevant lines covered (84.94%)

33987813.96 hits per line

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

85.88
/openmc/deplete/microxs.py
1
"""MicroXS module
2

3
A class for storing microscopic cross section data that can be used with the
4
IndependentOperator class for depletion.
5
"""
6

7
from __future__ import annotations
12✔
8
from collections.abc import Iterable, Sequence
12✔
9
from tempfile import TemporaryDirectory
12✔
10

11
import pandas as pd
12✔
12
import numpy as np
12✔
13

14
from openmc.checkvalue import check_type, check_value, check_iterable_type, PathLike
12✔
15
from openmc.exceptions import DataError
12✔
16
from openmc.utility_funcs import change_directory
12✔
17
from openmc import StatePoint
12✔
18
from openmc.mgxs import GROUP_STRUCTURES
12✔
19
from openmc.data import REACTION_MT
12✔
20
import openmc
12✔
21
from .chain import Chain, REACTIONS
12✔
22
from .coupled_operator import _find_cross_sections, _get_nuclides_with_data
12✔
23
import openmc.lib
12✔
24
from openmc.mpi import comm
12✔
25

26
_valid_rxns = list(REACTIONS)
12✔
27
_valid_rxns.append('fission')
12✔
28
_valid_rxns.append('damage-energy')
12✔
29

30

31
def _resolve_chain_file_path(chain_file: str | None):
12✔
32
    if chain_file is None:
12✔
33
        chain_file = openmc.config.get('chain_file')
×
34
        if 'chain_file' not in openmc.config:
×
35
            raise DataError(
×
36
                "No depletion chain specified and could not find depletion "
37
                "chain in openmc.config['chain_file']"
38
            )
39
    return chain_file
12✔
40

41

42
def get_microxs_and_flux(
12✔
43
        model: openmc.Model,
44
        domains,
45
        nuclides: Iterable[str] | None = None,
46
        reactions: Iterable[str] | None = None,
47
        energies: Iterable[float] | str | None = None,
48
        chain_file: PathLike | None = None,
49
        run_kwargs=None
50
    ) -> tuple[list[np.ndarray], list[MicroXS]]:
51
    """Generate a microscopic cross sections and flux from a Model
52

53
    .. versionadded:: 0.14.0
54

55
    Parameters
56
    ----------
57
    model : openmc.Model
58
        OpenMC model object. Must contain geometry, materials, and settings.
59
    domains : list of openmc.Material or openmc.Cell or openmc.Universe, or openmc.MeshBase
60
        Domains in which to tally reaction rates.
61
    nuclides : list of str
62
        Nuclides to get cross sections for. If not specified, all burnable
63
        nuclides from the depletion chain file are used.
64
    reactions : list of str
65
        Reactions to get cross sections for. If not specified, all neutron
66
        reactions listed in the depletion chain file are used.
67
    energies : iterable of float or str
68
        Energy group boundaries in [eV] or the name of the group structure
69
    chain_file : str, optional
70
        Path to the depletion chain XML file that will be used in depletion
71
        simulation. Used to determine cross sections for materials not
72
        present in the inital composition. Defaults to
73
        ``openmc.config['chain_file']``.
74
    run_kwargs : dict, optional
75
        Keyword arguments passed to :meth:`openmc.Model.run`
76

77
    Returns
78
    -------
79
    list of numpy.ndarray
80
        Flux in each group in [n-cm/src] for each domain
81
    list of MicroXS
82
        Cross section data in [b] for each domain
83

84
    """
85
    # Save any original tallies on the model
86
    original_tallies = model.tallies
12✔
87

88
    # Determine what reactions and nuclides are available in chain
89
    chain_file = _resolve_chain_file_path(chain_file)
12✔
90
    chain = Chain.from_xml(chain_file)
12✔
91
    if reactions is None:
12✔
92
        reactions = chain.reactions
12✔
93
    if not nuclides:
12✔
94
        cross_sections = _find_cross_sections(model)
×
95
        nuclides_with_data = _get_nuclides_with_data(cross_sections)
×
96
        nuclides = [nuc.name for nuc in chain.nuclides
×
97
                    if nuc.name in nuclides_with_data]
98

99
    # Set up the reaction rate and flux tallies
100
    if energies is None:
12✔
101
        energy_filter = openmc.EnergyFilter([0.0, 100.0e6])
12✔
102
    elif isinstance(energies, str):
×
103
        energy_filter = openmc.EnergyFilter.from_group_structure(energies)
×
104
    else:
105
        energy_filter = openmc.EnergyFilter(energies)
×
106
    if isinstance(domains, openmc.MeshBase):
12✔
107
        domain_filter = openmc.MeshFilter(domains)
12✔
108
    elif isinstance(domains[0], openmc.Material):
12✔
109
        domain_filter = openmc.MaterialFilter(domains)
12✔
110
    elif isinstance(domains[0], openmc.Cell):
×
111
        domain_filter = openmc.CellFilter(domains)
×
112
    elif isinstance(domains[0], openmc.Universe):
×
113
        domain_filter = openmc.UniverseFilter(domains)
×
114
    else:
115
        raise ValueError(f"Unsupported domain type: {type(domains[0])}")
×
116

117
    rr_tally = openmc.Tally(name='MicroXS RR')
12✔
118
    rr_tally.filters = [domain_filter, energy_filter]
12✔
119
    rr_tally.nuclides = nuclides
12✔
120
    rr_tally.multiply_density = False
12✔
121
    rr_tally.scores = reactions
12✔
122

123
    flux_tally = openmc.Tally(name='MicroXS flux')
12✔
124
    flux_tally.filters = [domain_filter, energy_filter]
12✔
125
    flux_tally.scores = ['flux']
12✔
126
    model.tallies = openmc.Tallies([rr_tally, flux_tally])
12✔
127

128
    if openmc.lib.is_initialized:
12✔
129
        openmc.lib.finalize()
×
130

131
        if comm.rank == 0:
×
132
            model.export_to_model_xml()
×
133
        comm.barrier()
×
134
        # Reinitialize with tallies
135
        openmc.lib.init(intracomm=comm)
×
136

137
    # create temporary run
138
    with TemporaryDirectory() as temp_dir:
12✔
139
        if run_kwargs is None:
12✔
140
            run_kwargs = {}
12✔
141
        else:
142
            run_kwargs = dict(run_kwargs)
×
143
        run_kwargs.setdefault('cwd', temp_dir)
12✔
144
        statepoint_path = model.run(**run_kwargs)
12✔
145

146
        if comm.rank == 0:
12✔
147
            with StatePoint(statepoint_path) as sp:
12✔
148
                rr_tally = sp.tallies[rr_tally.id]
12✔
149
                rr_tally._read_results()
12✔
150
                flux_tally = sp.tallies[flux_tally.id]
12✔
151
                flux_tally._read_results()
12✔
152

153
    rr_tally = comm.bcast(rr_tally)
12✔
154
    flux_tally = comm.bcast(flux_tally)
12✔
155
    # Get reaction rates and flux values
156
    reaction_rates = rr_tally.get_reshaped_data()  # (domains, groups, nuclides, reactions)
12✔
157
    flux = flux_tally.get_reshaped_data()  # (domains, groups, 1, 1)
12✔
158

159
    # Make energy groups last dimension
160
    reaction_rates = np.moveaxis(reaction_rates, 1, -1)  # (domains, nuclides, reactions, groups)
12✔
161
    flux = np.moveaxis(flux, 1, -1)  # (domains, 1, 1, groups)
12✔
162

163
    # Divide RR by flux to get microscopic cross sections
164
    xs = np.empty_like(reaction_rates) # (domains, nuclides, reactions, groups)
12✔
165
    d, _, _, g = np.nonzero(flux)
12✔
166
    xs[d, ..., g] = reaction_rates[d, ..., g] / flux[d, :, :, g]
12✔
167

168
    # Reset tallies
169
    model.tallies = original_tallies
12✔
170

171
    # Create lists where each item corresponds to one domain
172
    fluxes = list(flux.squeeze((1, 2)))
12✔
173
    micros = [MicroXS(xs_i, nuclides, reactions) for xs_i in xs]
12✔
174
    return fluxes, micros
12✔
175

176

177
class MicroXS:
12✔
178
    """Microscopic cross section data for use in transport-independent depletion.
179

180
    .. versionadded:: 0.13.1
181

182
    .. versionchanged:: 0.14.0
183
        Class was heavily refactored and no longer subclasses pandas.DataFrame.
184

185
    Parameters
186
    ----------
187
    data : numpy.ndarray of floats
188
        3D array containing microscopic cross section values for each
189
        nuclide, reaction, and energy group. Cross section values are assumed to
190
        be in [b], and indexed by [nuclide, reaction, energy group]
191
    nuclides : list of str
192
        List of nuclide symbols for that have data for at least one
193
        reaction.
194
    reactions : list of str
195
        List of reactions. All reactions must match those in
196
        :data:`openmc.deplete.chain.REACTIONS`
197

198
    """
199
    def __init__(self, data: np.ndarray, nuclides: list[str], reactions: list[str]):
12✔
200
        # Validate inputs
201
        if len(data.shape) != 3:
12✔
202
            raise ValueError('Data array must be 3D.')
12✔
203
        if data.shape[:2] != (len(nuclides), len(reactions)):
12✔
UNCOV
204
            raise ValueError(
×
205
                f'Nuclides list of length {len(nuclides)} and '
206
                f'reactions array of length {len(reactions)} do not '
207
                f'match dimensions of data array of shape {data.shape}')
208
        check_iterable_type('nuclides', nuclides, str)
12✔
209
        check_iterable_type('reactions', reactions, str)
12✔
210
        check_type('data', data, np.ndarray, expected_iter_type=float)
12✔
211
        for reaction in reactions:
12✔
212
            check_value('reactions', reaction, _valid_rxns)
12✔
213

214
        self.data = data
12✔
215
        self.nuclides = nuclides
12✔
216
        self.reactions = reactions
12✔
217
        self._index_nuc = {nuc: i for i, nuc in enumerate(nuclides)}
12✔
218
        self._index_rx = {rx: i for i, rx in enumerate(reactions)}
12✔
219

220
    @classmethod
12✔
221
    def from_multigroup_flux(
12✔
222
        cls,
223
        energies: Sequence[float] | str,
224
        multigroup_flux: Sequence[float],
225
        chain_file: PathLike | None = None,
226
        temperature: float = 293.6,
227
        nuclides: Sequence[str] | None = None,
228
        reactions: Sequence[str] | None = None,
229
        **init_kwargs: dict,
230
    ) -> MicroXS:
231
        """Generated microscopic cross sections from a known flux.
232

233
        The size of the MicroXS matrix depends on the chain file and cross
234
        sections available. MicroXS entry will be 0 if the nuclide cross section
235
        is not found.
236

237
        .. versionadded:: 0.15.0
238

239
        Parameters
240
        ----------
241
        energies : iterable of float or str
242
            Energy group boundaries in [eV] or the name of the group structure
243
        multi_group_flux : iterable of float
244
            Energy-dependent multigroup flux values
245
        chain_file : str, optional
246
            Path to the depletion chain XML file that will be used in depletion
247
            simulation.  Defaults to ``openmc.config['chain_file']``.
248
        temperature : int, optional
249
            Temperature for cross section evaluation in [K].
250
        nuclides : list of str, optional
251
            Nuclides to get cross sections for. If not specified, all burnable
252
            nuclides from the depletion chain file are used.
253
        reactions : list of str, optional
254
            Reactions to get cross sections for. If not specified, all neutron
255
            reactions listed in the depletion chain file are used.
256
        **init_kwargs : dict
257
            Keyword arguments passed to :func:`openmc.lib.init`
258

259
        Returns
260
        -------
261
        MicroXS
262
        """
263

264
        check_type("temperature", temperature, (int, float))
12✔
265
        # if energy is string then use group structure of that name
266
        if isinstance(energies, str):
12✔
267
            energies = GROUP_STRUCTURES[energies]
12✔
268
        else:
269
            # if user inputs energies check they are ascending (low to high) as
270
            # some depletion codes use high energy to low energy.
271
            if not np.all(np.diff(energies) > 0):
12✔
UNCOV
272
                raise ValueError('Energy group boundaries must be in ascending order')
×
273

274
        # check dimension consistency
275
        if len(multigroup_flux) != len(energies) - 1:
12✔
UNCOV
276
            raise ValueError('Length of flux array should be len(energies)-1')
×
277

278
        chain_file_path = _resolve_chain_file_path(chain_file)
12✔
279
        chain = Chain.from_xml(chain_file_path)
12✔
280

281
        cross_sections = _find_cross_sections(model=None)
12✔
282
        nuclides_with_data = _get_nuclides_with_data(cross_sections)
12✔
283

284
        # If no nuclides were specified, default to all nuclides from the chain
285
        if not nuclides:
12✔
286
            nuclides = chain.nuclides
12✔
287
            nuclides = [nuc.name for nuc in nuclides]
12✔
288

289
        # Get reaction MT values. If no reactions specified, default to the
290
        # reactions available in the chain file
291
        if reactions is None:
12✔
292
            reactions = chain.reactions
12✔
293
        mts = [REACTION_MT[name] for name in reactions]
12✔
294

295
        # Normalize multigroup flux
296
        multigroup_flux = np.array(multigroup_flux)
12✔
297
        multigroup_flux /= multigroup_flux.sum()
12✔
298

299
        # Create 3D array for microscopic cross sections
300
        microxs_arr = np.zeros((len(nuclides), len(mts), 1))
12✔
301

302
        # Create a material with all nuclides
303
        mat_all_nucs = openmc.Material()
12✔
304
        for nuc in nuclides:
12✔
305
            if nuc in nuclides_with_data:
12✔
306
                mat_all_nucs.add_nuclide(nuc, 1.0)
12✔
307
        mat_all_nucs.set_density("atom/b-cm", 1.0)
12✔
308

309
        # Create simple model containing the above material
310
        surf1 = openmc.Sphere(boundary_type="vacuum")
12✔
311
        surf1_cell = openmc.Cell(fill=mat_all_nucs, region=-surf1)
12✔
312
        model = openmc.Model()
12✔
313
        model.geometry = openmc.Geometry([surf1_cell])
12✔
314
        model.settings = openmc.Settings(
12✔
315
            particles=1, batches=1, output={'summary': False})
316

317
        with change_directory(tmpdir=True):
12✔
318
            # Export model within temporary directory
319
            model.export_to_model_xml()
12✔
320

321
            with openmc.lib.run_in_memory(**init_kwargs):
12✔
322
                # For each nuclide and reaction, compute the flux-averaged
323
                # cross section
324
                for nuc_index, nuc in enumerate(nuclides):
12✔
325
                    if nuc not in nuclides_with_data:
12✔
UNCOV
326
                        continue
×
327
                    lib_nuc = openmc.lib.nuclides[nuc]
12✔
328
                    for mt_index, mt in enumerate(mts):
12✔
329
                        xs = lib_nuc.collapse_rate(
12✔
330
                            mt, temperature, energies, multigroup_flux
331
                        )
332
                        microxs_arr[nuc_index, mt_index, 0] = xs
12✔
333

334
        return cls(microxs_arr, nuclides, reactions)
12✔
335

336
    @classmethod
12✔
337
    def from_csv(cls, csv_file, **kwargs):
12✔
338
        """Load data from a comma-separated values (csv) file.
339

340
        Parameters
341
        ----------
342
        csv_file : str
343
            Relative path to csv-file containing microscopic cross section
344
            data. Cross section values are assumed to be in [b]
345
        **kwargs : dict
346
            Keyword arguments to pass to :func:`pandas.read_csv()`.
347

348
        Returns
349
        -------
350
        MicroXS
351

352
        """
353
        if 'float_precision' not in kwargs:
12✔
354
            kwargs['float_precision'] = 'round_trip'
12✔
355

356
        df = pd.read_csv(csv_file, **kwargs)
12✔
357
        df.set_index(['nuclides', 'reactions', 'groups'], inplace=True)
12✔
358
        nuclides = list(df.index.unique(level='nuclides'))
12✔
359
        reactions = list(df.index.unique(level='reactions'))
12✔
360
        groups = list(df.index.unique(level='groups'))
12✔
361
        shape = (len(nuclides), len(reactions), len(groups))
12✔
362
        data = df.values.reshape(shape)
12✔
363
        return cls(data, nuclides, reactions)
12✔
364

365
    def __getitem__(self, index):
12✔
366
        nuc, rx = index
12✔
367
        i_nuc = self._index_nuc[nuc]
12✔
368
        i_rx = self._index_rx[rx]
12✔
369
        return self.data[i_nuc, i_rx]
12✔
370

371
    def to_csv(self, *args, **kwargs):
12✔
372
        """Write data to a comma-separated values (csv) file
373

374
        Parameters
375
        ----------
376
        *args
377
            Positional arguments passed to :meth:`pandas.DataFrame.to_csv`
378
        **kwargs
379
            Keyword arguments passed to :meth:`pandas.DataFrame.to_csv`
380

381
        """
382
        groups = self.data.shape[2]
12✔
383
        multi_index = pd.MultiIndex.from_product(
12✔
384
            [self.nuclides, self.reactions, range(1, groups + 1)],
385
            names=['nuclides', 'reactions', 'groups']
386
        )
387
        df = pd.DataFrame({'xs': self.data.flatten()}, index=multi_index)
12✔
388
        df.to_csv(*args, **kwargs)
12✔
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