• 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

91.54
/openmc/weight_windows.py
1
from __future__ import annotations
12✔
2
from numbers import Real, Integral
12✔
3
from collections.abc import Iterable, Sequence
12✔
4
import warnings
12✔
5

6
import lxml.etree as ET
12✔
7
import numpy as np
12✔
8
import h5py
12✔
9

10
import openmc
12✔
11
from openmc.filter import _PARTICLES
12✔
12
from openmc.mesh import MeshBase, RectilinearMesh, CylindricalMesh, SphericalMesh, UnstructuredMesh
12✔
13
import openmc.checkvalue as cv
12✔
14
from openmc.checkvalue import PathLike
12✔
15
from ._xml import get_text, clean_indentation
12✔
16
from .mixin import IDManagerMixin
12✔
17

18

19
class WeightWindows(IDManagerMixin):
12✔
20
    """Mesh-based weight windows
21

22
    This class enables you to specify weight window parameters that are used in
23
    a simulation. Multiple sets of weight windows can be defined for different
24
    meshes and different particles. An iterable of :class:`WeightWindows`
25
    instances can be assigned to the :attr:`openmc.Settings.weight_windows`
26
    attribute, which is then exported to XML.
27

28
    Weight window lower/upper bounds are to be specified for each combination of
29
    a mesh element and an energy bin. Thus the total number of bounds should be
30
    equal to the product of the number of mesh bins and the number of energy
31
    bins.
32

33
    .. versionadded:: 0.13
34

35
    Parameters
36
    ----------
37
    mesh : openmc.MeshBase
38
        Mesh for the weight windows
39
    lower_ww_bounds : Iterable of Real
40
        A list of values for which each value is the lower bound of a weight
41
        window
42
    upper_ww_bounds : Iterable of Real
43
        A list of values for which each value is the upper bound of a weight
44
        window
45
    upper_bound_ratio : float
46
        Ratio of the lower to upper weight window bounds
47
    energy_bounds : Iterable of Real
48
        A list of values for which each successive pair constitutes a range of
49
        energies in [eV] for a single bin. If no energy bins are provided, the
50
        maximum and minimum energy for the data available at runtime.
51
    particle_type : {'neutron', 'photon'}
52
        Particle type the weight windows apply to
53
    survival_ratio : float
54
        Ratio of the survival weight to the lower weight window bound for
55
        rouletting
56
    max_lower_bound_ratio : float
57
        Maximum allowed ratio of a particle's weight to the weight window's
58
        lower bound. A factor will be applied to raise the weight window to be
59
        lower than the particle's weight by a factor of max_lower_bound_ratio
60
        during transport if exceeded.
61
    max_split : int
62
        Maximum allowable number of particles when splitting
63
    weight_cutoff : float
64
        Threshold below which particles will be terminated
65
    id : int
66
       Unique identifier for the weight window settings. If not specified, an
67
       identifier will automatically be assigned.
68

69
    Attributes
70
    ----------
71
    id : int
72
       Unique identifier for the weight window settings.
73
    mesh : openmc.MeshBase
74
        Mesh for the weight windows with dimension (ni, nj, nk)
75
    particle_type : str
76
        Particle type the weight windows apply to
77
    energy_bounds : Iterable of Real
78
        A list of values for which each successive pair constitutes a range of
79
        energies in [eV] for a single bin
80
    num_energy_bins : int
81
        Number of energy bins
82
    lower_ww_bounds : numpy.ndarray of float
83
        An array of values for which each value is the lower bound of a weight
84
        window. Shape: (ni, nj, nk, num_energy_bins) for StructuredMesh;
85
        (num_elements, num_energy_bins) for UnstructuredMesh
86
    upper_ww_bounds : numpy.ndarray of float
87
        An array of values for which each value is the upper bound of a weight
88
        window. Shape: (ni, nj, nk, num_energy_bins) for StructuredMesh;
89
        (num_elements, num_energy_bins) for UnstructuredMesh
90
    survival_ratio : float
91
        Ratio of the survival weight to the lower weight window bound for
92
        rouletting
93
    max_lower_bound_ratio: float
94
        Maximum allowed ratio of a particle's weight to the weight window's
95
        lower bound. (Default: 1.0)
96
    max_split : int
97
        Maximum allowable number of particles when splitting
98
    weight_cutoff : float
99
        Threshold below which particles will be terminated
100

101
    See Also
102
    --------
103
    openmc.Settings
104

105
    """
106
    next_id = 1
12✔
107
    used_ids = set()
12✔
108

109
    def __init__(
12✔
110
        self,
111
        mesh: MeshBase,
112
        lower_ww_bounds: Iterable[float],
113
        upper_ww_bounds: Iterable[float] | None = None,
114
        upper_bound_ratio: float | None = None,
115
        energy_bounds: Iterable[Real] | None = None,
116
        particle_type: str = 'neutron',
117
        survival_ratio: float = 3,
118
        max_lower_bound_ratio: float | None = None,
119
        max_split: int = 10,
120
        weight_cutoff: float = 1.e-38,
121
        id: int | None = None
122
    ):
123
        self.mesh = mesh
12✔
124
        self.id = id
12✔
125
        self.particle_type = particle_type
12✔
126
        self._energy_bounds = None
12✔
127
        if energy_bounds is not None:
12✔
128
            self.energy_bounds = energy_bounds
12✔
129
        self.lower_ww_bounds = lower_ww_bounds
12✔
130

131
        if upper_ww_bounds is not None and upper_bound_ratio:
12✔
132
            raise ValueError("Exactly one of upper_ww_bounds and "
×
133
                             "upper_bound_ratio must be present.")
134

135
        if upper_ww_bounds is None and upper_bound_ratio is None:
12✔
136
            raise ValueError("Exactly one of upper_ww_bounds and "
×
137
                             "upper_bound_ratio must be present.")
138

139
        if upper_bound_ratio:
12✔
140
            self.upper_ww_bounds = [
12✔
141
                lb * upper_bound_ratio for lb in self.lower_ww_bounds
142
            ]
143

144
        if upper_ww_bounds is not None:
12✔
145
            self.upper_ww_bounds = upper_ww_bounds
12✔
146

147
        if len(self.lower_ww_bounds) != len(self.upper_ww_bounds):
12✔
148
            raise ValueError('Size of the lower and upper weight '
×
149
                             'window bounds do not match')
150

151
        self.survival_ratio = survival_ratio
12✔
152

153
        self._max_lower_bound_ratio = None
12✔
154
        if max_lower_bound_ratio is not None:
12✔
155
            self.max_lower_bound_ratio = max_lower_bound_ratio
12✔
156

157
        self.max_split = max_split
12✔
158
        self.weight_cutoff = weight_cutoff
12✔
159

160
    def __repr__(self) -> str:
12✔
161
        string = type(self).__name__ + '\n'
12✔
162
        string += '{: <16}=\t{}\n'.format('\tID', self._id)
12✔
163
        string += '{: <16}=\t{}\n'.format('\tMesh', self.mesh)
12✔
164
        string += '{: <16}=\t{}\n'.format('\tParticle Type', self._particle_type)
12✔
165
        string += '{: <16}=\t{}\n'.format('\tEnergy Bounds', self._energy_bounds)
12✔
166
        string += '{: <16}=\t{}\n'.format('\tMax lower bound ratio', self.max_lower_bound_ratio)
12✔
167
        string += '{: <16}=\t{}\n'.format('\tLower WW Bounds', self._lower_ww_bounds)
12✔
168
        string += '{: <16}=\t{}\n'.format('\tUpper WW Bounds', self._upper_ww_bounds)
12✔
169
        string += '{: <16}=\t{}\n'.format('\tSurvival Ratio', self._survival_ratio)
12✔
170
        string += '{: <16}=\t{}\n'.format('\tMax Split', self._max_split)
12✔
171
        string += '{: <16}=\t{}\n'.format('\tWeight Cutoff', self._weight_cutoff)
12✔
172
        return string
12✔
173

174
    def __eq__(self, other: WeightWindows) -> bool:
12✔
175
        # ensure that `other` is a WeightWindows object
176
        if not isinstance(other, WeightWindows):
12✔
177
            return False
×
178

179
        # TODO: add ability to check mesh equality
180

181
        # check several attributes directly
182
        attrs = ('particle_type',
12✔
183
                 'survival_ratio',
184
                 'max_lower_bound_ratio',
185
                 'max_split',
186
                 'weight_cutoff')
187
        for attr in attrs:
12✔
188
            if getattr(self, attr) != getattr(other, attr):
12✔
189
                return False
×
190

191
        # save most expensive checks for last
192
        if not np.array_equal(self.energy_bounds, other.energy_bounds):
12✔
193
            return False
×
194

195
        if not np.array_equal(self.lower_ww_bounds, other.lower_ww_bounds):
12✔
196
            return False
×
197

198
        if not np.array_equal(self.upper_ww_bounds, other.upper_ww_bounds):
12✔
199
            return False
×
200

201
        return True
12✔
202

203
    @property
12✔
204
    def mesh(self) -> MeshBase:
12✔
205
        return self._mesh
12✔
206

207
    @mesh.setter
12✔
208
    def mesh(self, mesh: MeshBase):
12✔
209
        cv.check_type('Weight window mesh', mesh, MeshBase)
12✔
210
        self._mesh = mesh
12✔
211

212
    @property
12✔
213
    def particle_type(self) -> str:
12✔
214
        return self._particle_type
12✔
215

216
    @particle_type.setter
12✔
217
    def particle_type(self, pt: str):
12✔
218
        cv.check_value('Particle type', pt, _PARTICLES)
12✔
219
        self._particle_type = pt
12✔
220

221
    @property
12✔
222
    def energy_bounds(self) -> Iterable[Real]:
12✔
223
        return self._energy_bounds
12✔
224

225
    @energy_bounds.setter
12✔
226
    def energy_bounds(self, bounds: Iterable[float]):
12✔
227
        cv.check_type('Energy bounds', bounds, Iterable, Real)
12✔
228
        self._energy_bounds = np.asarray(bounds)
12✔
229

230
    @property
12✔
231
    def num_energy_bins(self) -> int:
12✔
232
        if self.energy_bounds is None:
12✔
233
            return 1
12✔
234
        return self.energy_bounds.size - 1
12✔
235

236
    @property
12✔
237
    def lower_ww_bounds(self) -> np.ndarray:
12✔
238
        return self._lower_ww_bounds
12✔
239

240
    @lower_ww_bounds.setter
12✔
241
    def lower_ww_bounds(self, bounds: Iterable[float]):
12✔
242
        cv.check_iterable_type('Lower WW bounds',
12✔
243
                               bounds,
244
                               Real,
245
                               min_depth=1,
246
                               max_depth=4)
247
        # reshape data according to mesh and energy bins
248
        bounds = np.asarray(bounds)
12✔
249
        if isinstance(self.mesh, UnstructuredMesh):
12✔
250
            bounds = bounds.reshape(-1, self.num_energy_bins)
3✔
251
        else:
252
            bounds = bounds.reshape(*self.mesh.dimension, self.num_energy_bins)
12✔
253
        self._lower_ww_bounds = bounds
12✔
254

255
    @property
12✔
256
    def upper_ww_bounds(self) -> np.ndarray:
12✔
257
        return self._upper_ww_bounds
12✔
258

259
    @upper_ww_bounds.setter
12✔
260
    def upper_ww_bounds(self, bounds: Iterable[float]):
12✔
261
        cv.check_iterable_type('Upper WW bounds',
12✔
262
                               bounds,
263
                               Real,
264
                               min_depth=1,
265
                               max_depth=4)
266
        # reshape data according to mesh and energy bins
267
        bounds = np.asarray(bounds)
12✔
268
        if isinstance(self.mesh, UnstructuredMesh):
12✔
269
            bounds = bounds.reshape(-1, self.num_energy_bins)
3✔
270
        else:
271
            bounds = bounds.reshape(*self.mesh.dimension, self.num_energy_bins)
12✔
272
        self._upper_ww_bounds = bounds
12✔
273

274
    @property
12✔
275
    def survival_ratio(self) -> float:
12✔
276
        return self._survival_ratio
12✔
277

278
    @survival_ratio.setter
12✔
279
    def survival_ratio(self, val: float):
12✔
280
        cv.check_type('Survival ratio', val, Real)
12✔
281
        cv.check_greater_than('Survival ratio', val, 1.0, True)
12✔
282
        self._survival_ratio = val
12✔
283

284
    @property
12✔
285
    def max_lower_bound_ratio(self) -> float:
12✔
286
        return self._max_lower_bound_ratio
12✔
287

288
    @max_lower_bound_ratio.setter
12✔
289
    def max_lower_bound_ratio(self, val: float):
12✔
290
        cv.check_type('Maximum lower bound ratio', val, Real)
12✔
291
        cv.check_greater_than('Maximum lower bound ratio', val, 1.0, equality=True)
12✔
292
        self._max_lower_bound_ratio = val
12✔
293

294
    @property
12✔
295
    def max_split(self) -> int:
12✔
296
        return self._max_split
12✔
297

298
    @max_split.setter
12✔
299
    def max_split(self, val: int):
12✔
300
        cv.check_type('Max split', val, Integral)
12✔
301
        self._max_split = val
12✔
302

303
    @property
12✔
304
    def weight_cutoff(self) -> float:
12✔
305
        return self._weight_cutoff
12✔
306

307
    @weight_cutoff.setter
12✔
308
    def weight_cutoff(self, cutoff: float):
12✔
309
        cv.check_type('Weight cutoff', cutoff, Real)
12✔
310
        cv.check_greater_than('Weight cutoff', cutoff, 0.0, True)
12✔
311
        self._weight_cutoff = cutoff
12✔
312

313
    def to_xml_element(self) -> ET.Element:
12✔
314
        """Return an XML representation of the weight window settings
315

316
        Returns
317
        -------
318
        element : lxml.etree._Element
319
            XML element containing the weight window information
320
        """
321
        element = ET.Element('weight_windows')
12✔
322

323
        element.set('id', str(self._id))
12✔
324

325
        subelement = ET.SubElement(element, 'mesh')
12✔
326
        subelement.text = str(self.mesh.id)
12✔
327

328
        subelement = ET.SubElement(element, 'particle_type')
12✔
329
        subelement.text = self.particle_type
12✔
330

331
        if self.energy_bounds is not None:
12✔
332
            subelement = ET.SubElement(element, 'energy_bounds')
12✔
333
            subelement.text = ' '.join(str(e) for e in self.energy_bounds)
12✔
334

335
        subelement = ET.SubElement(element, 'lower_ww_bounds')
12✔
336
        subelement.text = ' '.join(str(b) for b in self.lower_ww_bounds.ravel('F'))
12✔
337

338
        subelement = ET.SubElement(element, 'upper_ww_bounds')
12✔
339
        subelement.text = ' '.join(str(b) for b in self.upper_ww_bounds.ravel('F'))
12✔
340

341
        subelement = ET.SubElement(element, 'survival_ratio')
12✔
342
        subelement.text = str(self.survival_ratio)
12✔
343

344
        if self.max_lower_bound_ratio is not None:
12✔
345
            subelement = ET.SubElement(element, 'max_lower_bound_ratio')
12✔
346
            subelement.text = str(self.max_lower_bound_ratio)
12✔
347

348
        subelement = ET.SubElement(element, 'max_split')
12✔
349
        subelement.text = str(self.max_split)
12✔
350

351
        subelement = ET.SubElement(element, 'weight_cutoff')
12✔
352
        subelement.text = str(self.weight_cutoff)
12✔
353

354
        return element
12✔
355

356
    @classmethod
12✔
357
    def from_xml_element(cls, elem: ET.Element, meshes: dict[int, MeshBase]) -> WeightWindows:
12✔
358
        """Generate weight window settings from an XML element
359

360
        Parameters
361
        ----------
362
        elem : lxml.etree._Element
363
            XML element
364
        meshes : dict
365
            Dictionary mapping IDs to mesh objects
366

367
        Returns
368
        -------
369
        openmc.WeightWindows
370
            Weight windows object
371
        """
372
        # Get mesh for weight windows
373
        mesh_id = int(get_text(elem, 'mesh'))
12✔
374
        if mesh_id not in meshes:
12✔
UNCOV
375
            raise ValueError(f'Could not locate mesh with ID "{mesh_id}"')
×
376
        mesh = meshes[mesh_id]
12✔
377

378
        # Read all other parameters
379
        lower_ww_bounds = [float(l) for l in get_text(elem, 'lower_ww_bounds').split()]
12✔
380
        upper_ww_bounds = [float(u) for u in get_text(elem, 'upper_ww_bounds').split()]
12✔
381
        e_bounds = [float(b) for b in get_text(elem, 'energy_bounds').split()]
12✔
382
        particle_type = get_text(elem, 'particle_type')
12✔
383
        survival_ratio = float(get_text(elem, 'survival_ratio'))
12✔
384

385
        ww_shape = (len(e_bounds) - 1,) + mesh.dimension[::-1]
12✔
386
        lower_ww_bounds = np.array(lower_ww_bounds).reshape(ww_shape).T
12✔
387
        upper_ww_bounds = np.array(upper_ww_bounds).reshape(ww_shape).T
12✔
388

389
        max_lower_bound_ratio = None
12✔
390
        if get_text(elem, 'max_lower_bound_ratio'):
12✔
UNCOV
391
            max_lower_bound_ratio = float(get_text(elem, 'max_lower_bound_ratio'))
×
392

393
        max_split = int(get_text(elem, 'max_split'))
12✔
394
        weight_cutoff = float(get_text(elem, 'weight_cutoff'))
12✔
395
        id = int(get_text(elem, 'id'))
12✔
396

397
        return cls(
12✔
398
            mesh=mesh,
399
            lower_ww_bounds=lower_ww_bounds,
400
            upper_ww_bounds=upper_ww_bounds,
401
            energy_bounds=e_bounds,
402
            particle_type=particle_type,
403
            survival_ratio=survival_ratio,
404
            max_lower_bound_ratio=max_lower_bound_ratio,
405
            max_split=max_split,
406
            weight_cutoff=weight_cutoff,
407
            id=id
408
        )
409

410
    @classmethod
12✔
411
    def from_hdf5(cls, group: h5py.Group, meshes: dict[int, MeshBase]) -> WeightWindows:
12✔
412
        """Create weight windows from HDF5 group
413

414
        Parameters
415
        ----------
416
        group : h5py.Group
417
            Group in HDF5 file
418
        meshes : dict
419
            Dictionary mapping IDs to mesh objects
420

421
        Returns
422
        -------
423
        openmc.WeightWindows
424
            A weight window object
425
        """
426

427
        id = int(group.name.split('/')[-1].lstrip('weight_windows'))
12✔
428
        mesh_id = group['mesh'][()]
12✔
429
        mesh = meshes[mesh_id]
12✔
430

431
        ptype = group['particle_type'][()].decode()
12✔
432
        e_bounds = group['energy_bounds'][()]
12✔
433
        # weight window bounds are stored with the shape (e, k, j, i)
434
        # in C++ and HDF5 -- the opposite of how they are stored here
435
        shape = (e_bounds.size - 1,  *mesh.dimension[::-1])
12✔
436
        lower_ww_bounds = group['lower_ww_bounds'][()].reshape(shape).T
12✔
437
        upper_ww_bounds = group['upper_ww_bounds'][()].reshape(shape).T
12✔
438
        survival_ratio = group['survival_ratio'][()]
12✔
439

440
        max_lower_bound_ratio = None
12✔
441
        if group.get('max_lower_bound_ratio') is not None:
12✔
442
            max_lower_bound_ratio = group['max_lower_bound_ratio'][()]
12✔
443

444
        max_split = group['max_split'][()]
12✔
445
        weight_cutoff = group['weight_cutoff'][()]
12✔
446

447
        return cls(
12✔
448
            mesh=mesh,
449
            lower_ww_bounds=lower_ww_bounds,
450
            upper_ww_bounds=upper_ww_bounds,
451
            energy_bounds=e_bounds,
452
            particle_type=ptype,
453
            survival_ratio=survival_ratio,
454
            max_lower_bound_ratio=max_lower_bound_ratio,
455
            max_split=max_split,
456
            weight_cutoff=weight_cutoff,
457
            id=id
458
        )
459

460

461
def wwinp_to_wws(path: PathLike) -> list[WeightWindows]:
12✔
462
    """Create WeightWindows instances from a wwinp file
463

464
    .. versionadded:: 0.13.1
465

466
    Parameters
467
    ----------
468
    path : str or pathlib.Path
469
        Path to the wwinp file
470

471
    Returns
472
    -------
473
    list of openmc.WeightWindows
474
    """
475

476
    with open(path) as wwinp:
12✔
477
        # BLOCK 1
478
        header = wwinp.readline().split(None, 4)
12✔
479
        # read file type, time-dependence, number of
480
        # particles, mesh type and problem identifier
481
        _if, iv, ni, nr = [int(x) for x in header[:4]]
12✔
482

483
        # header value checks
484
        if _if != 1:
12✔
UNCOV
485
            raise ValueError(f'Found incorrect file type, if: {_if}')
×
486

487
        if iv > 1:
12✔
488
            # read number of time bins for each particle, 'nt(1...ni)'
489
            nt = np.fromstring(wwinp.readline(), sep=' ', dtype=int)
12✔
490

491
            # raise error if time bins are present for now
492
            raise ValueError('Time-dependent weight windows '
12✔
493
                             'are not yet supported')
494
        else:
495
            nt = ni * [1]
12✔
496

497
        # read number of energy bins for each particle, 'ne(1...ni)'
498
        ne = np.fromstring(wwinp.readline(), sep=' ', dtype=int)
12✔
499

500
        # read coarse mesh dimensions and lower left corner
501
        mesh_description = np.fromstring(wwinp.readline(), sep=' ')
12✔
502
        nfx, nfy, nfz = mesh_description[:3].astype(int)
12✔
503
        xyz0 = mesh_description[3:]
12✔
504

505
        # read cylindrical and spherical mesh vectors if present
506
        if nr == 16:
12✔
507
            # read number of coarse bins
508
            line_arr = np.fromstring(wwinp.readline(), sep=' ')
12✔
509
            ncx, ncy, ncz = line_arr[:3].astype(int)
12✔
510
            # read polar vector (x1, y1, z1)
511
            xyz1 = line_arr[3:]
12✔
512
            # read azimuthal vector (x2, y2, z2)
513
            line_arr = np.fromstring(wwinp.readline(), sep=' ')
12✔
514
            xyz2 = line_arr[:3]
12✔
515

516
            # oriented polar and azimuthal vectors aren't yet supported
517
            if np.count_nonzero(xyz1) or np.count_nonzero(xyz2):
12✔
518
                raise NotImplementedError('Custom sphere/cylinder orientations are not supported')
12✔
519

520
            # read geometry type
521
            nwg = int(line_arr[-1])
12✔
522

523
        elif nr == 10:
12✔
524
            # read rectilinear data:
525
            # number of coarse mesh bins and mesh type
526
            ncx, ncy, ncz, nwg = \
12✔
527
                np.fromstring(wwinp.readline(), sep=' ').astype(int)
528
        else:
UNCOV
529
            raise RuntimeError(f'Invalid mesh description (nr) found: {nr}')
×
530

531
        # read BLOCK 2 and BLOCK 3 data into a single array
532
        ww_data = np.fromstring(wwinp.read(), sep=' ')
12✔
533

534
    # extract mesh data from the ww_data array
535
    start_idx = 0
12✔
536

537
    # first values in the mesh definition arrays are the first
538
    # coordinate of the grid
539
    end_idx = start_idx + 1 + 3 * ncx
12✔
540
    i0, i_vals = ww_data[start_idx], ww_data[start_idx+1:end_idx]
12✔
541
    start_idx = end_idx
12✔
542

543
    end_idx = start_idx + 1 + 3 * ncy
12✔
544
    j0, j_vals = ww_data[start_idx], ww_data[start_idx+1:end_idx]
12✔
545
    start_idx = end_idx
12✔
546

547
    end_idx = start_idx + 1 + 3 * ncz
12✔
548
    k0, k_vals = ww_data[start_idx], ww_data[start_idx+1:end_idx]
12✔
549
    start_idx = end_idx
12✔
550

551
    # mesh consistency checks
552
    if nr == 16 and nwg == 1 or nr == 10 and nwg != 1:
12✔
UNCOV
553
        raise ValueError(f'Mesh description in header ({nr}) '
×
554
                         f'does not match the mesh type ({nwg})')
555

556
    if nr == 10 and (xyz0 != (i0, j0, k0)).any():
12✔
UNCOV
557
        raise ValueError(f'Mesh origin in the header ({xyz0}) '
×
558
                         f' does not match the origin in the mesh '
559
                         f' description ({i0, j0, k0})')
560

561
    # create openmc mesh object
562
    grids = []
12✔
563
    mesh_definition = [(i0, i_vals, nfx), (j0, j_vals, nfy), (k0, k_vals, nfz)]
12✔
564
    for grid0, grid_vals, n_pnts in mesh_definition:
12✔
565
        # file spec checks for the mesh definition
566
        if (grid_vals[2::3] != 1.0).any():
12✔
UNCOV
567
            raise ValueError('One or more mesh ratio value, qx, '
×
568
                             'is not equal to one')
569

570
        s = int(grid_vals[::3].sum())
12✔
571
        if s != n_pnts:
12✔
UNCOV
572
            raise ValueError(f'Sum of the fine bin entries, {s}, does '
×
573
                             f'not match the number of fine bins, {n_pnts}')
574

575
        # extend the grid based on the next coarse bin endpoint, px
576
        # and the number of fine bins in the coarse bin, sx
577
        intervals = grid_vals.reshape(-1, 3)
12✔
578
        coords = [grid0]
12✔
579
        for sx, px, qx in intervals:
12✔
580
            coords += np.linspace(coords[-1], px, int(sx + 1)).tolist()[1:]
12✔
581

582
        grids.append(np.array(coords))
12✔
583

584
    if nwg == 1:
12✔
585
        mesh = RectilinearMesh()
12✔
586
        mesh.x_grid, mesh.y_grid, mesh.z_grid = grids
12✔
587
    elif nwg == 2:
12✔
588
        mesh = CylindricalMesh(
12✔
589
            r_grid=grids[0],
590
            z_grid=grids[1],
591
            phi_grid=grids[2],
592
            origin = xyz0,
593
        )
594
    elif nwg == 3:
12✔
595
        mesh = SphericalMesh(
12✔
596
            r_grid=grids[0],
597
            theta_grid=grids[1],
598
            phi_grid=grids[2],
599
            origin = xyz0
600
        )
601

602
    # extract weight window values from array
603
    wws = []
12✔
604
    for ne_i, nt_i, particle_type in zip(ne, nt, ('neutron', 'photon')):
12✔
605
        # no information to read for this particle if
606
        # either the energy bins or time bins are empty
607
        if ne_i == 0 or nt_i == 0:
12✔
608
            continue
12✔
609

610
        if iv > 1:
12✔
611
            # time bins are parsed but unused for now
612
            end_idx = start_idx + nt_i
×
613
            time_bounds = ww_data[start_idx:end_idx]
×
614
            np.insert(time_bounds, (0,), (0.0,))
×
UNCOV
615
            start_idx = end_idx
×
616

617
        # read energy boundaries
618
        end_idx = start_idx + ne_i
12✔
619
        energy_bounds = np.insert(ww_data[start_idx:end_idx], (0,), (0.0,))
12✔
620
        # convert from MeV to eV
621
        energy_bounds *= 1e6
12✔
622
        start_idx = end_idx
12✔
623

624
        # read weight window values
625
        end_idx = start_idx + (nfx * nfy * nfz) * nt_i * ne_i
12✔
626

627
        # read values and reshape according to ordering
628
        # slowest to fastest: t, e, z, y, x
629
        # reorder with transpose since our ordering is x, y, z, e, t
630
        ww_shape = (nt_i, ne_i, nfz, nfy, nfx)
12✔
631
        ww_values = ww_data[start_idx:end_idx].reshape(ww_shape).T
12✔
632
        # Only use first time bin since we don't support time dependent weight
633
        # windows yet.
634
        ww_values = ww_values[:, :, :, :, 0]
12✔
635
        start_idx = end_idx
12✔
636

637
        # create a weight window object
638
        ww = WeightWindows(id=None,
12✔
639
                           mesh=mesh,
640
                           lower_ww_bounds=ww_values,
641
                           upper_bound_ratio=5.0,
642
                           energy_bounds=energy_bounds,
643
                           particle_type=particle_type)
644
        wws.append(ww)
12✔
645

646
    return wws
12✔
647

648

649
class WeightWindowGenerator:
12✔
650
    """Class passed to setting to govern weight window generation
651
    using the OpenMC executable
652

653
    Parameters
654
    ----------
655
    mesh : :class:`openmc.MeshBase`
656
        Mesh used to represent the weight windows spatially
657
    energy_bounds : Iterable of Real
658
        A list of values for which each successive pair constitutes a range of
659
        energies in [eV] for a single bin. If no energy bins are provided, the
660
        maximum and minimum energy for the data available at runtime.
661
    particle_type : {'neutron', 'photon'}
662
        Particle type the weight windows apply to
663
    method : {'magic'}
664
        The weight window generation methodology applied during an update. Only
665
        'magic' is currently supported.
666
    max_realizations : int
667
        The upper limit for number of tally realizations when generating weight
668
        windows.
669
    update_interval : int
670
        The number of tally realizations between updates.
671
    on_the_fly : bool
672
        Whether or not to apply weight windows on the fly.
673

674
    Attributes
675
    ----------
676
    mesh : openmc.MeshBase
677
        Mesh used to represent the weight windows spatially
678
    energy_bounds : Iterable of Real
679
        A list of values for which each successive pair constitutes a range of
680
        energies in [eV] for a single bin
681
    particle_type : {'neutron', 'photon'}
682
        Particle type the weight windows apply to
683
    method : {'magic'}
684
        The weight window generation methodology applied during an update. Only
685
        'magic' is currently supported.
686
    max_realizations : int
687
        The upper limit for number of tally realizations when generating weight
688
        windows.
689
    update_interval : int
690
        The number of tally realizations between updates.
691
    update_parameters : dict
692
        A set of parameters related to the update.
693
    on_the_fly : bool
694
        Whether or not to apply weight windows on the fly.
695
    """
696

697
    _MAGIC_PARAMS = {'value': str, 'threshold': float, 'ratio': float}
12✔
698

699
    def __init__(
12✔
700
        self,
701
        mesh: openmc.MeshBase,
702
        energy_bounds: Sequence[float] | None = None,
703
        particle_type: str = 'neutron',
704
        method: str = 'magic',
705
        max_realizations: int = 1,
706
        update_interval: int = 1,
707
        on_the_fly: bool = True
708
    ):
709
        self._update_parameters = None
12✔
710

711
        self.mesh = mesh
12✔
712
        self._energy_bounds = None
12✔
713
        if energy_bounds is not None:
12✔
714
            self.energy_bounds = energy_bounds
12✔
715
        self.particle_type = particle_type
12✔
716
        self.method = method
12✔
717
        self.max_realizations = max_realizations
12✔
718
        self.update_interval = update_interval
12✔
719
        self.on_the_fly = on_the_fly
12✔
720

721
    def __repr__(self):
12✔
722
        string = type(self).__name__ + '\n'
×
723
        string += f'\t{"Mesh":<20}=\t{self.mesh.id}\n'
×
724
        string += f'\t{"Particle:":<20}=\t{self.particle_type}\n'
×
725
        string += f'\t{"Energy Bounds:":<20}=\t{self.energy_bounds}\n'
×
726
        string += f'\t{"Method":<20}=\t{self.method}\n'
×
727
        string += f'\t{"Max Realizations:":<20}=\t{self.max_realizations}\n'
×
728
        string += f'\t{"Update Interval:":<20}=\t{self.update_interval}\n'
×
729
        string += f'\t{"On The Fly:":<20}=\t{self.on_the_fly}\n'
×
730
        if self.update_parameters is not None:
×
731
            string += f'\t{"Update Parameters:":<20}\n\t\t\t{self.update_parameters}\n'
×
UNCOV
732
        string
×
733

UNCOV
734
        return string
×
735

736
    @property
12✔
737
    def mesh(self) -> openmc.MeshBase:
12✔
738
        return self._mesh
12✔
739

740
    @mesh.setter
12✔
741
    def mesh(self, m: openmc.MeshBase):
12✔
742
        cv.check_type('mesh', m, openmc.MeshBase)
12✔
743
        self._mesh = m
12✔
744

745
    @property
12✔
746
    def energy_bounds(self) -> Iterable[Real]:
12✔
747
        return self._energy_bounds
12✔
748

749
    @energy_bounds.setter
12✔
750
    def energy_bounds(self, eb: Iterable[float]):
12✔
751
        cv.check_type('energy bounds', eb, Iterable, Real)
12✔
752
        self._energy_bounds = eb
12✔
753

754
    @property
12✔
755
    def particle_type(self) -> str:
12✔
756
        return self._particle_type
12✔
757

758
    @particle_type.setter
12✔
759
    def particle_type(self, pt: str):
12✔
760
        cv.check_value('particle type', pt, ('neutron', 'photon'))
12✔
761
        self._particle_type = pt
12✔
762

763
    @property
12✔
764
    def method(self) -> str:
12✔
765
        return self._method
12✔
766

767
    @method.setter
12✔
768
    def method(self, m: str):
12✔
769
        cv.check_type('generation method', m, str)
12✔
770
        cv.check_value('generation method', m, {'magic'})
12✔
771
        self._method = m
12✔
772
        if self._update_parameters is not None:
12✔
773
            try:
×
774
                self._check_update_parameters()
×
775
            except (TypeError, KeyError):
×
UNCOV
776
                warnings.warn(f'Update parameters are invalid for the "{m}" method.')
×
777

778
    @property
12✔
779
    def max_realizations(self) -> int:
12✔
780
        return self._max_realizations
12✔
781

782
    @max_realizations.setter
12✔
783
    def max_realizations(self, m: int):
12✔
784
        cv.check_type('max tally realizations', m, Integral)
12✔
785
        cv.check_greater_than('max tally realizations', m, 0)
12✔
786
        self._max_realizations = m
12✔
787

788
    @property
12✔
789
    def update_interval(self) -> int:
12✔
790
        return self._update_interval
12✔
791

792
    @update_interval.setter
12✔
793
    def update_interval(self, ui: int):
12✔
794
        cv.check_type('update interval', ui, Integral)
12✔
795
        cv.check_greater_than('update interval', ui , 0)
12✔
796
        self._update_interval = ui
12✔
797

798
    @property
12✔
799
    def update_parameters(self) -> dict:
12✔
800
        return self._update_parameters
12✔
801

802
    def _check_update_parameters(self, params: dict):
12✔
803
        if self.method == 'magic':
12✔
804
            check_params = self._MAGIC_PARAMS
12✔
805

806
        for key, val in params.items():
12✔
807
            if key not in check_params:
12✔
UNCOV
808
                raise ValueError(f'Invalid param "{key}" for {self.method} '
×
809
                                  'weight window generation')
810
            cv.check_type(f'weight window generation param: "{key}"', val, self._MAGIC_PARAMS[key])
12✔
811

812
    @update_parameters.setter
12✔
813
    def update_parameters(self, params: dict):
12✔
814
        self._check_update_parameters(params)
12✔
815
        self._update_parameters = params
12✔
816

817
    @property
12✔
818
    def on_the_fly(self) -> bool:
12✔
819
        return self._on_the_fly
12✔
820

821
    @on_the_fly.setter
12✔
822
    def on_the_fly(self, otf: bool):
12✔
823
        cv.check_type('on the fly generation', otf, bool)
12✔
824
        self._on_the_fly = otf
12✔
825

826
    def _update_parameters_subelement(self, element: ET.Element):
12✔
827
        if not self.update_parameters:
12✔
UNCOV
828
            return
×
829
        params_element = ET.SubElement(element, 'update_parameters')
12✔
830
        for pname, value in self.update_parameters.items():
12✔
831
            param_element = ET.SubElement(params_element, pname)
12✔
832
            param_element.text = str(value)
12✔
833

834
    @classmethod
12✔
835
    def _sanitize_update_parameters(cls, method: str, update_parameters: dict):
12✔
836
        """
837
        Attempt to convert update parameters to their appropriate types
838

839
        Parameters
840
        ----------
841
        method : str
842
            The update method for which these update parameters should comply
843
        update_parameters : dict
844
            The update parameters as-read from the XML node (keys: str, values: str)
845
        """
846
        if method == 'magic':
12✔
847
            check_params = cls._MAGIC_PARAMS
12✔
848

849
        for param, param_type in check_params.items():
12✔
850
            if param in update_parameters:
12✔
851
                update_parameters[param] = param_type(update_parameters[param])
12✔
852

853
    def to_xml_element(self):
12✔
854
        """Creates a 'weight_window_generator' element to be written to an XML file.
855
        """
856
        element = ET.Element('weight_windows_generator')
12✔
857

858
        mesh_elem = ET.SubElement(element, 'mesh')
12✔
859
        mesh_elem.text = str(self.mesh.id)
12✔
860
        if self.energy_bounds is not None:
12✔
861
            subelement = ET.SubElement(element, 'energy_bounds')
12✔
862
            subelement.text = ' '.join(str(e) for e in self.energy_bounds)
12✔
863
        particle_elem = ET.SubElement(element, 'particle_type')
12✔
864
        particle_elem.text = self.particle_type
12✔
865
        realizations_elem = ET.SubElement(element, 'max_realizations')
12✔
866
        realizations_elem.text = str(self.max_realizations)
12✔
867
        update_interval_elem = ET.SubElement(element, 'update_interval')
12✔
868
        update_interval_elem.text = str(self.update_interval)
12✔
869
        otf_elem = ET.SubElement(element, 'on_the_fly')
12✔
870
        otf_elem.text = str(self.on_the_fly).lower()
12✔
871
        method_elem = ET.SubElement(element, 'method')
12✔
872
        method_elem.text = self.method
12✔
873
        if self.update_parameters is not None:
12✔
874
            self._update_parameters_subelement(element)
12✔
875

876
        clean_indentation(element)
12✔
877

878
        return element
12✔
879

880
    @classmethod
12✔
881
    def from_xml_element(cls, elem: ET.Element, meshes: dict) -> WeightWindowGenerator:
12✔
882
        """
883
        Create a weight window generation object from an XML element
884

885
        Parameters
886
        ----------
887
        elem : xml.etree.ElementTree.Element
888
            XML element
889
        meshes : dict
890
            A dictionary with IDs as keys and openmc.MeshBase instances as values
891

892
        Returns
893
        -------
894
        openmc.WeightWindowGenerator
895
        """
896

897
        mesh_id = int(get_text(elem, 'mesh'))
12✔
898
        mesh = meshes[mesh_id]
12✔
899

900
        energy_bounds = [float(x) for x in get_text(elem, 'energy_bounds').split()]
12✔
901
        particle_type = get_text(elem, 'particle_type')
12✔
902

903
        wwg = cls(mesh, energy_bounds, particle_type)
12✔
904

905
        wwg.max_realizations = int(get_text(elem, 'max_realizations'))
12✔
906
        wwg.update_interval = int(get_text(elem, 'update_interval'))
12✔
907
        wwg.on_the_fly = bool(get_text(elem, 'on_the_fly'))
12✔
908
        wwg.method = get_text(elem, 'method')
12✔
909

910
        if elem.find('update_parameters') is not None:
12✔
911
            update_parameters = {}
12✔
912
            params_elem = elem.find('update_parameters')
12✔
913
            for entry in params_elem:
12✔
914
                update_parameters[entry.tag] = entry.text
12✔
915

916
            cls._sanitize_update_parameters(wwg.method, update_parameters)
12✔
917
            wwg.update_parameters = update_parameters
12✔
918

919
        return wwg
12✔
920

921
def hdf5_to_wws(path='weight_windows.h5'):
12✔
922
    """Create WeightWindows instances from a weight windows HDF5 file
923

924
    .. versionadded:: 0.14.0
925

926
    Parameters
927
    ----------
928
    path : cv.PathLike
929
        Path to the weight windows hdf5 file
930

931
    Returns
932
    -------
933
    list of openmc.WeightWindows
934
    """
935

936
    with h5py.File(path) as h5_file:
12✔
937
        # read in all of the meshes in the mesh node
938
        meshes = {}
12✔
939
        for mesh_group in h5_file['meshes']:
12✔
940
            mesh = MeshBase.from_hdf5(h5_file['meshes'][mesh_group])
12✔
941
            meshes[mesh.id] = mesh
12✔
942
        return [WeightWindows.from_hdf5(ww, meshes) for ww in h5_file['weight_windows'].values()]
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