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

openmc-dev / openmc / 20501343020

25 Dec 2025 07:38AM UTC coverage: 82.185% (+0.05%) from 82.139%
20501343020

Pull #3692

github

web-flow
Merge fc35e8018 into 3f06a42ab
Pull Request #3692: Fix a bug in rotational periodic boundary conditions

17096 of 23665 branches covered (72.24%)

Branch coverage included in aggregate %.

36 of 37 new or added lines in 3 files covered. (97.3%)

198 existing lines in 11 files now uncovered.

55209 of 64313 relevant lines covered (85.84%)

43488756.38 hits per line

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

90.73
/openmc/filter.py
1
from __future__ import annotations
11✔
2
from abc import ABCMeta
11✔
3
from collections.abc import Iterable, Sequence
11✔
4
import hashlib
11✔
5
from itertools import product
11✔
6
from numbers import Real, Integral
11✔
7
import warnings
11✔
8

9
import lxml.etree as ET
11✔
10
import numpy as np
11✔
11
import pandas as pd
11✔
12

13
import openmc
11✔
14
import openmc.checkvalue as cv
11✔
15
from .cell import Cell
11✔
16
from .material import Material
11✔
17
from .mixin import IDManagerMixin
11✔
18
from .surface import Surface
11✔
19
from .universe import UniverseBase
11✔
20
from ._xml import get_elem_list, get_text
11✔
21

22

23
_FILTER_TYPES = (
11✔
24
    'universe', 'material', 'cell', 'cellborn', 'surface', 'mesh', 'energy',
25
    'energyout', 'mu', 'musurface', 'polar', 'azimuthal', 'distribcell', 'delayedgroup',
26
    'energyfunction', 'cellfrom', 'materialfrom', 'legendre', 'spatiallegendre',
27
    'sphericalharmonics', 'zernike', 'zernikeradial', 'particle', 'cellinstance',
28
    'collision', 'time', 'parentnuclide', 'weight', 'meshborn', 'meshsurface',
29
    'meshmaterial',
30
)
31

32
_CURRENT_NAMES = (
11✔
33
    'x-min out', 'x-min in', 'x-max out', 'x-max in',
34
    'y-min out', 'y-min in', 'y-max out', 'y-max in',
35
    'z-min out', 'z-min in', 'z-max out', 'z-max in'
36
)
37

38
_PARTICLES = {'neutron', 'photon', 'electron', 'positron'}
11✔
39

40

41
class FilterMeta(ABCMeta):
11✔
42
    """Metaclass for filters that ensures class names are appropriate."""
43

44
    def __new__(cls, name, bases, namespace, **kwargs):
11✔
45
        # Check the class name.
46
        required_suffix = 'Filter'
11✔
47
        if not name.endswith(required_suffix):
11✔
48
            raise ValueError("All filter class names must end with 'Filter'")
×
49

50
        # Create a 'short_name' attribute that removes the 'Filter' suffix.
51
        namespace['short_name'] = name[:-len(required_suffix)]
11✔
52

53
        # Subclass methods can sort of inherit the docstring of parent class
54
        # methods.  If a function is defined without a docstring, most (all?)
55
        # Python interpreters will search through the parent classes to see if
56
        # there is a docstring for a function with the same name, and they will
57
        # use that docstring.  However, Sphinx does not have that functionality.
58
        # This chunk of code handles this docstring inheritance manually so that
59
        # the autodocumentation will pick it up.
60
        if name != required_suffix:
11✔
61
            # Look for newly-defined functions that were also in Filter.
62
            for func_name in namespace:
11✔
63
                if func_name in Filter.__dict__:
11✔
64
                    # Inherit the docstring from Filter if not defined.
65
                    if isinstance(namespace[func_name],
11✔
66
                                  (classmethod, staticmethod)):
67
                        new_doc = namespace[func_name].__func__.__doc__
11✔
68
                        old_doc = Filter.__dict__[func_name].__func__.__doc__
11✔
69
                        if new_doc is None and old_doc is not None:
11✔
70
                            namespace[func_name].__func__.__doc__ = old_doc
11✔
71
                    else:
72
                        new_doc = namespace[func_name].__doc__
11✔
73
                        old_doc = Filter.__dict__[func_name].__doc__
11✔
74
                        if new_doc is None and old_doc is not None:
11✔
75
                            namespace[func_name].__doc__ = old_doc
11✔
76

77
        # Make the class.
78
        return super().__new__(cls, name, bases, namespace, **kwargs)
11✔
79

80

81
def _repeat_and_tile(bins, repeat_factor, data_size):
11✔
82
    filter_bins = np.repeat(bins, repeat_factor)
11✔
83
    tile_factor = data_size // len(filter_bins)
11✔
84
    return np.tile(filter_bins, tile_factor)
11✔
85

86

87
class Filter(IDManagerMixin, metaclass=FilterMeta):
11✔
88
    """Tally modifier that describes phase-space and other characteristics.
89

90
    Parameters
91
    ----------
92
    bins : Integral or Iterable of Integral or Iterable of Real
93
        The bins for the filter. This takes on different meaning for different
94
        filters. See the docstrings for subclasses of this filter or the online
95
        documentation for more details.
96
    filter_id : int
97
        Unique identifier for the filter
98

99
    Attributes
100
    ----------
101
    bins : Integral or Iterable of Integral or Iterable of Real
102
        The bins for the filter
103
    id : int
104
        Unique identifier for the filter
105
    num_bins : Integral
106
        The number of filter bins
107
    shape : tuple
108
        The shape of the filter
109

110
    """
111

112
    next_id = 1
11✔
113
    used_ids = set()
11✔
114

115
    def __init__(self, bins, filter_id=None):
11✔
116
        self.bins = bins
11✔
117
        self.id = filter_id
11✔
118

119
    def __eq__(self, other):
11✔
120
        if type(self) is not type(other):
11✔
121
            return False
11✔
122
        elif len(self.bins) != len(other.bins):
11✔
123
            return False
11✔
124
        else:
125
            return np.allclose(self.bins, other.bins)
11✔
126

127
    def __gt__(self, other):
11✔
128
        if type(self) is not type(other):
11✔
129
            if self.short_name in _FILTER_TYPES and \
11✔
130
                other.short_name in _FILTER_TYPES:
131
                delta = _FILTER_TYPES.index(self.short_name) - \
×
132
                        _FILTER_TYPES.index(other.short_name)
133
                return delta > 0
×
134
            else:
135
                return False
11✔
136
        else:
137
            return max(self.bins) > max(other.bins)
11✔
138

139
    def __lt__(self, other):
11✔
140
        return not self > other
11✔
141

142
    def __hash__(self):
11✔
143
        string = type(self).__name__ + '\n'
11✔
144
        string += '{: <16}=\t{}\n'.format('\tBins', self.bins)
11✔
145
        return hash(string)
11✔
146

147
    def __repr__(self):
11✔
148
        string = type(self).__name__ + '\n'
11✔
149
        string += '{: <16}=\t{}\n'.format('\tBins', self.bins)
11✔
150
        string += '{: <16}=\t{}\n'.format('\tID', self.id)
11✔
151
        return string
11✔
152

153
    @classmethod
11✔
154
    def _recursive_subclasses(cls):
11✔
155
        """Return all subclasses and their subclasses, etc."""
156
        all_subclasses = []
11✔
157

158
        for subclass in cls.__subclasses__():
11✔
159
            all_subclasses.append(subclass)
11✔
160
            all_subclasses.extend(subclass._recursive_subclasses())
11✔
161

162
        return all_subclasses
11✔
163

164
    @classmethod
11✔
165
    def from_hdf5(cls, group, **kwargs):
11✔
166
        """Construct a new Filter instance from HDF5 data.
167

168
        Parameters
169
        ----------
170
        group : h5py.Group
171
            HDF5 group to read from
172

173
        Keyword arguments
174
        -----------------
175
        meshes : dict
176
            Dictionary mapping integer IDs to openmc.MeshBase objects.  Only
177
            used for openmc.MeshFilter objects.
178

179
        """
180

181
        filter_id = int(group.name.split('/')[-1].lstrip('filter '))
11✔
182

183
        # If the HDF5 'type' variable matches this class's short_name, then
184
        # there is no overridden from_hdf5 method.  Pass the bins to __init__.
185
        if group['type'][()].decode() == cls.short_name.lower():
11✔
186
            out = cls(group['bins'][()], filter_id=filter_id)
11✔
187
            out._num_bins = group['n_bins'][()]
11✔
188
            return out
11✔
189

190
        # Search through all subclasses and find the one matching the HDF5
191
        # 'type'.  Call that class's from_hdf5 method.
192
        for subclass in cls._recursive_subclasses():
11✔
193
            if group['type'][()].decode() == subclass.short_name.lower():
11✔
194
                return subclass.from_hdf5(group, **kwargs)
11✔
195

196
        raise ValueError("Unrecognized Filter class: '"
×
197
                         + group['type'][()].decode() + "'")
198

199
    @property
11✔
200
    def bins(self):
11✔
201
        return self._bins
11✔
202

203
    @bins.setter
11✔
204
    def bins(self, bins):
11✔
205
        self.check_bins(bins)
11✔
206
        self._bins = bins
11✔
207

208
    @property
11✔
209
    def num_bins(self):
11✔
210
        return len(self.bins)
11✔
211

212
    @property
11✔
213
    def shape(self):
11✔
214
        return (self.num_bins,)
11✔
215

216
    def check_bins(self, bins):
11✔
217
        """Make sure given bins are valid for this filter.
218

219
        Raises
220
        ------
221
        TypeError
222
        ValueError
223

224
        """
225

226
        pass
11✔
227

228
    def to_xml_element(self):
11✔
229
        """Return XML Element representing the Filter.
230

231
        Returns
232
        -------
233
        element : lxml.etree._Element
234
            XML element containing filter data
235

236
        """
237
        element = ET.Element('filter')
11✔
238
        element.set('id', str(self.id))
11✔
239
        element.set('type', self.short_name.lower())
11✔
240

241
        subelement = ET.SubElement(element, 'bins')
11✔
242
        subelement.text = ' '.join(str(b) for b in self.bins)
11✔
243
        return element
11✔
244

245
    @classmethod
11✔
246
    def from_xml_element(cls, elem, **kwargs):
11✔
247
        """Generate a filter from an XML element
248

249
        Parameters
250
        ----------
251
        elem : lxml.etree._Element
252
            XML element
253
        **kwargs
254
            Keyword arguments (e.g., mesh information)
255

256
        Returns
257
        -------
258
        openmc.Filter
259
            Filter object
260

261
        """
262
        filter_type = get_text(elem, "type")
11✔
263

264
        # If the filter type matches this class's short_name, then
265
        # there is no overridden from_xml_element method
266
        if filter_type == cls.short_name.lower():
11✔
267
            # Get bins from element -- the default here works for any filters
268
            # that just store a list of bins that can be represented as integers
269
            filter_id = int(get_text(elem, "id"))
11✔
270
            bins = get_elem_list(elem, "bins", int) or []
11✔
271
            return cls(bins, filter_id=filter_id)
11✔
272

273
        # Search through all subclasses and find the one matching the HDF5
274
        # 'type'.  Call that class's from_hdf5 method
275
        for subclass in cls._recursive_subclasses():
11✔
276
            if filter_type == subclass.short_name.lower():
11✔
277
                return subclass.from_xml_element(elem, **kwargs)
11✔
278

279

280
    def can_merge(self, other):
11✔
281
        """Determine if filter can be merged with another.
282

283
        Parameters
284
        ----------
285
        other : openmc.Filter
286
            Filter to compare with
287

288
        Returns
289
        -------
290
        bool
291
            Whether the filter can be merged
292

293
        """
294
        return type(self) is type(other)
11✔
295

296
    def merge(self, other):
11✔
297
        """Merge this filter with another.
298

299
        Parameters
300
        ----------
301
        other : openmc.Filter
302
            Filter to merge with
303

304
        Returns
305
        -------
306
        merged_filter : openmc.Filter
307
            Filter resulting from the merge
308

309
        """
310

311
        if not self.can_merge(other):
11✔
312
            msg = f'Unable to merge "{type(self)}" with "{type(other)}"'
×
313
            raise ValueError(msg)
×
314

315
        # Merge unique filter bins
316
        merged_bins = np.concatenate((self.bins, other.bins))
11✔
317
        merged_bins = np.unique(merged_bins, axis=0)
11✔
318

319
        # Create a new filter with these bins and a new auto-generated ID
320
        return type(self)(merged_bins)
11✔
321

322
    def is_subset(self, other):
11✔
323
        """Determine if another filter is a subset of this filter.
324

325
        If all of the bins in the other filter are included as bins in this
326
        filter, then it is a subset of this filter.
327

328
        Parameters
329
        ----------
330
        other : openmc.Filter
331
            The filter to query as a subset of this filter
332

333
        Returns
334
        -------
335
        bool
336
            Whether or not the other filter is a subset of this filter
337

338
        """
339

340
        if type(self) is not type(other):
11✔
341
            return False
11✔
342

343
        for b in other.bins:
11✔
344
            if b not in self.bins:
11✔
345
                return False
11✔
346

347
        return True
11✔
348

349
    def get_bin_index(self, filter_bin):
11✔
350
        """Returns the index in the Filter for some bin.
351

352
        Parameters
353
        ----------
354
        filter_bin : int or tuple
355
            The bin is the integer ID for 'material', 'surface', 'cell',
356
            'cellborn', and 'universe' Filters. The bin is an integer for the
357
            cell instance ID for 'distribcell' Filters. The bin is a 2-tuple of
358
            floats for 'energy' and 'energyout' filters corresponding to the
359
            energy boundaries of the bin of interest. The bin is an (x,y,z)
360
            3-tuple for 'mesh' filters corresponding to the mesh cell of
361
            interest.
362

363
        Returns
364
        -------
365
        filter_index : int
366
             The index in the Tally data array for this filter bin.
367

368
        """
369

370
        if filter_bin not in self.bins:
11✔
371
            msg = ('Unable to get the bin index for Filter since '
×
372
                   f'"{filter_bin}" is not one of the bins')
373
            raise ValueError(msg)
×
374

375
        if isinstance(self.bins, np.ndarray):
11✔
376
            return np.where(self.bins == filter_bin)[0][0]
11✔
377
        else:
378
            return self.bins.index(filter_bin)
11✔
379

380
    def get_pandas_dataframe(self, data_size, stride, **kwargs):
11✔
381
        """Builds a Pandas DataFrame for the Filter's bins.
382

383
        This method constructs a Pandas DataFrame object for the filter with
384
        columns annotated by filter bin information. This is a helper method for
385
        :meth:`Tally.get_pandas_dataframe`.
386

387
        Parameters
388
        ----------
389
        data_size : int
390
            The total number of bins in the tally corresponding to this filter
391
        stride : int
392
            Stride in memory for the filter
393

394
        Keyword arguments
395
        -----------------
396
        paths : bool
397
            Only used for DistribcellFilter.  If True (default), expand
398
            distribcell indices into multi-index columns describing the path
399
            to that distribcell through the CSG tree.  NOTE: This option assumes
400
            that all distribcell paths are of the same length and do not have
401
            the same universes and cells but different lattice cell indices.
402

403
        Returns
404
        -------
405
        pandas.DataFrame
406
            A Pandas DataFrame with columns of strings that characterize the
407
            filter's bins. The number of rows in the DataFrame is the same as
408
            the total number of bins in the corresponding tally, with the filter
409
            bin appropriately tiled to map to the corresponding tally bins.
410

411
        See also
412
        --------
413
        Tally.get_pandas_dataframe(), CrossFilter.get_pandas_dataframe()
414

415
        """
416
        # Initialize Pandas DataFrame
417
        df = pd.DataFrame()
11✔
418

419
        filter_bins = np.repeat(self.bins, stride)
11✔
420
        tile_factor = data_size // len(filter_bins)
11✔
421
        filter_bins = np.tile(filter_bins, tile_factor)
11✔
422
        df = pd.concat([df, pd.DataFrame(
11✔
423
            {self.short_name.lower(): filter_bins})])
424

425
        return df
11✔
426

427

428
class WithIDFilter(Filter):
11✔
429
    """Abstract parent for filters of types with IDs (Cell, Material, etc.)."""
430
    def __init__(self, bins, filter_id=None):
11✔
431
        bins = np.atleast_1d(bins)
11✔
432

433
        # Make sure bins are either integers or appropriate objects
434
        cv.check_iterable_type('filter bins', bins,
11✔
435
                               (Integral, self.expected_type))
436

437
        # Extract ID values
438
        bins = np.array([b if isinstance(b, Integral) else b.id
11✔
439
                         for b in bins])
440
        super().__init__(bins, filter_id)
11✔
441

442
    def check_bins(self, bins):
11✔
443
        # Check the bin values.
444
        for edge in bins:
11✔
445
            cv.check_greater_than('filter bin', edge, 0, equality=True)
11✔
446

447

448
class UniverseFilter(WithIDFilter):
11✔
449
    """Bins tally event locations based on the Universe they occurred in.
450

451
    Parameters
452
    ----------
453
    bins : openmc.UniverseBase, int, or iterable thereof
454
        The Universes to tally. Either :class:`openmc.UniverseBase` objects or their
455
        Integral ID numbers can be used.
456
    filter_id : int
457
        Unique identifier for the filter
458

459
    Attributes
460
    ----------
461
    bins : Iterable of Integral
462
        openmc.UniverseBase IDs.
463
    id : int
464
        Unique identifier for the filter
465
    num_bins : Integral
466
        The number of filter bins
467

468
    """
469
    expected_type = UniverseBase
11✔
470

471

472
class MaterialFilter(WithIDFilter):
11✔
473
    """Bins tally event locations based on the Material they occurred in.
474

475
    Parameters
476
    ----------
477
    bins : openmc.Material, Integral, or iterable thereof
478
        The material(s) to tally. Either :class:`openmc.Material` objects or their
479
        Integral ID numbers can be used.
480
    filter_id : int
481
        Unique identifier for the filter
482

483
    Attributes
484
    ----------
485
    bins : Iterable of Integral
486
        openmc.Material IDs.
487
    id : int
488
        Unique identifier for the filter
489
    num_bins : Integral
490
        The number of filter bins
491

492
    """
493
    expected_type = Material
11✔
494

495

496
class MaterialFromFilter(WithIDFilter):
11✔
497
    """Bins tally event locations based on the Material they occurred in.
498

499
    Parameters
500
    ----------
501
    bins : openmc.Material, Integral, or iterable thereof
502
        The material(s) to tally. Either :class:`openmc.Material` objects or their
503
        Integral ID numbers can be used.
504
    filter_id : int
505
        Unique identifier for the filter
506

507
    Attributes
508
    ----------
509
    bins : Iterable of Integral
510
        openmc.Material IDs.
511
    id : int
512
        Unique identifier for the filter
513
    num_bins : Integral
514
        The number of filter bins
515

516
    """
517
    expected_type = Material
11✔
518

519

520
class CellFilter(WithIDFilter):
11✔
521
    """Bins tally event locations based on the Cell they occurred in.
522

523
    Parameters
524
    ----------
525
    bins : openmc.Cell, int, or iterable thereof
526
        The cells to tally. Either :class:`openmc.Cell` objects or their ID numbers can
527
        be used.
528
    filter_id : int
529
        Unique identifier for the filter
530

531
    Attributes
532
    ----------
533
    bins : Iterable of Integral
534
        openmc.Cell IDs.
535
    id : int
536
        Unique identifier for the filter
537
    num_bins : Integral
538
        The number of filter bins
539

540
    """
541
    expected_type = Cell
11✔
542

543

544
class CellFromFilter(WithIDFilter):
11✔
545
    """Bins tally on which cell the particle came from.
546

547
    Parameters
548
    ----------
549
    bins : openmc.Cell, Integral, or iterable thereof
550
        The cell(s) to tally. Either :class:`openmc.Cell` objects or their
551
        integral ID numbers can be used.
552
    filter_id : int
553
        Unique identifier for the filter
554

555
    Attributes
556
    ----------
557
    bins : Integral or Iterable of Integral
558
        Cell IDs.
559
    id : int
560
        Unique identifier for the filter
561
    num_bins : Integral
562
        The number of filter bins
563

564
    """
565
    expected_type = Cell
11✔
566

567

568
class CellBornFilter(WithIDFilter):
11✔
569
    """Bins tally events based on which cell the particle was born in.
570

571
    Parameters
572
    ----------
573
    bins : openmc.Cell, Integral, or iterable thereof
574
        The birth cells to tally. Either :class:`openmc.Cell` objects or their
575
        integral ID numbers can be used.
576
    filter_id : int
577
        Unique identifier for the filter
578

579
    Attributes
580
    ----------
581
    bins : Iterable of Integral
582
        Cell IDs.
583
    id : int
584
        Unique identifier for the filter
585
    num_bins : Integral
586
        The number of filter bins
587

588
    """
589
    expected_type = Cell
11✔
590

591

592
# Temporary alias for CellbornFilter
593
def CellbornFilter(*args, **kwargs):
11✔
594
    warnings.warn('The name of "CellbornFilter" has changed to '
×
595
                  '"CellBornFilter". "CellbornFilter" will be '
596
                  'removed in the future.', FutureWarning)
597
    return CellBornFilter(*args, **kwargs)
×
598

599

600
class CellInstanceFilter(Filter):
11✔
601
    """Bins tally events based on which cell instance a particle is in.
602

603
    This filter is similar to :class:`DistribcellFilter` but allows one to
604
    select particular instances to be tallied (instead of obtaining *all*
605
    instances by default) and allows instances from different cells to be
606
    specified in a single filter.
607

608
    .. versionadded:: 0.12
609

610
    Parameters
611
    ----------
612
    bins : iterable of 2-tuples or numpy.ndarray
613
        The cell instances to tally, given as 2-tuples. For the first value in
614
        the tuple, either openmc.Cell objects or their integral ID numbers can
615
        be used. The second value indicates the cell instance.
616
    filter_id : int
617
        Unique identifier for the filter
618

619
    Attributes
620
    ----------
621
    bins : numpy.ndarray
622
        2D numpy array of cell IDs and instances
623
    id : int
624
        Unique identifier for the filter
625
    num_bins : Integral
626
        The number of filter bins
627

628
    See Also
629
    --------
630
    DistribcellFilter
631

632
    """
633
    def __init__(self, bins, filter_id=None):
11✔
634
        self.bins = bins
11✔
635
        self.id = filter_id
11✔
636

637
    @Filter.bins.setter
11✔
638
    def bins(self, bins):
11✔
639
        pairs = np.empty((len(bins), 2), dtype=int)
11✔
640
        for i, (cell, instance) in enumerate(bins):
11✔
641
            cv.check_type('cell', cell, (openmc.Cell, Integral))
11✔
642
            cv.check_type('instance', instance, Integral)
11✔
643
            pairs[i, 0] = cell if isinstance(cell, Integral) else cell.id
11✔
644
            pairs[i, 1] = instance
11✔
645
        self._bins = pairs
11✔
646

647
    def get_pandas_dataframe(self, data_size, stride, **kwargs):
11✔
648
        """Builds a Pandas DataFrame for the Filter's bins.
649

650
        This method constructs a Pandas DataFrame object for the filter with
651
        columns annotated by filter bin information. This is a helper method for
652
        :meth:`Tally.get_pandas_dataframe`.
653

654
        Parameters
655
        ----------
656
        data_size : int
657
            The total number of bins in the tally corresponding to this filter
658
        stride : int
659
            Stride in memory for the filter
660

661
        Returns
662
        -------
663
        pandas.DataFrame
664
            A Pandas DataFrame with a multi-index column for the cell instance.
665
            The number of rows in the DataFrame is the same as the total number
666
            of bins in the corresponding tally, with the filter bin appropriately
667
            tiled to map to the corresponding tally bins.
668

669
        See also
670
        --------
671
        Tally.get_pandas_dataframe(), CrossFilter.get_pandas_dataframe()
672

673
        """
674
        # Repeat and tile bins as necessary to account for other filters.
675
        bins = np.repeat(self.bins, stride, axis=0)
11✔
676
        tile_factor = data_size // len(bins)
11✔
677
        bins = np.tile(bins, (tile_factor, 1))
11✔
678

679
        columns = pd.MultiIndex.from_product([[self.short_name.lower()],
11✔
680
                                              ['cell', 'instance']])
681
        return pd.DataFrame(bins, columns=columns)
11✔
682

683
    def to_xml_element(self):
11✔
684
        """Return XML Element representing the Filter.
685

686
        Returns
687
        -------
688
        element : lxml.etree._Element
689
            XML element containing filter data
690

691
        """
692
        element = ET.Element('filter')
11✔
693
        element.set('id', str(self.id))
11✔
694
        element.set('type', self.short_name.lower())
11✔
695

696
        subelement = ET.SubElement(element, 'bins')
11✔
697
        subelement.text = ' '.join(str(i) for i in self.bins.ravel())
11✔
698
        return element
11✔
699

700
    @classmethod
11✔
701
    def from_xml_element(cls, elem, **kwargs):
11✔
702
        filter_id = int(get_text(elem, "id"))
11✔
703
        bins = get_elem_list(elem, "bins", int) or []
11✔
704
        cell_instances = list(zip(bins[::2], bins[1::2]))
11✔
705
        return cls(cell_instances, filter_id=filter_id)
11✔
706

707

708
class SurfaceFilter(WithIDFilter):
11✔
709
    """Filters particles by surface crossing
710

711
    Parameters
712
    ----------
713
    bins : openmc.Surface, int, or iterable of Integral
714
        The surfaces to tally over. Either openmc.Surface objects or their ID
715
        numbers can be used.
716
    filter_id : int
717
        Unique identifier for the filter
718

719
    Attributes
720
    ----------
721
    bins : Iterable of Integral
722
        The surfaces to tally over. Either openmc.Surface objects or their ID
723
        numbers can be used.
724
    id : int
725
        Unique identifier for the filter
726
    num_bins : Integral
727
        The number of filter bins
728

729
    """
730
    expected_type = Surface
11✔
731

732

733
class ParticleFilter(Filter):
11✔
734
    """Bins tally events based on the particle type.
735

736
    Parameters
737
    ----------
738
    bins : str, or sequence of str
739
        The particles to tally represented as strings ('neutron', 'photon',
740
        'electron', 'positron').
741
    filter_id : int
742
        Unique identifier for the filter
743

744
    Attributes
745
    ----------
746
    bins : sequence of str
747
        The particles to tally
748
    id : int
749
        Unique identifier for the filter
750
    num_bins : Integral
751
        The number of filter bins
752

753
    """
754
    def __eq__(self, other):
11✔
755
        if type(self) is not type(other):
×
756
            return False
×
757
        elif len(self.bins) != len(other.bins):
×
758
            return False
×
759
        else:
760
            return np.all(self.bins == other.bins)
×
761

762
    __hash__ = Filter.__hash__
11✔
763

764
    @Filter.bins.setter
11✔
765
    def bins(self, bins):
11✔
766
        cv.check_type('bins', bins, Sequence, str)
11✔
767
        bins = np.atleast_1d(bins)
11✔
768
        for edge in bins:
11✔
769
            cv.check_value('filter bin', edge, _PARTICLES)
11✔
770
        self._bins = bins
11✔
771

772
    @classmethod
11✔
773
    def from_hdf5(cls, group, **kwargs):
11✔
774
        if group['type'][()].decode() != cls.short_name.lower():
11✔
775
            raise ValueError("Expected HDF5 data for filter type '"
×
776
                             + cls.short_name.lower() + "' but got '"
777
                             + group['type'][()].decode() + " instead")
778

779
        particles = [b.decode() for b in group['bins'][()]]
11✔
780
        filter_id = int(group.name.split('/')[-1].lstrip('filter '))
11✔
781
        return cls(particles, filter_id=filter_id)
11✔
782

783
    @classmethod
11✔
784
    def from_xml_element(cls, elem, **kwargs):
11✔
785
        filter_id = int(get_text(elem, "id"))
11✔
786
        bins = get_elem_list(elem, "bins", str) or []
11✔
787
        return cls(bins, filter_id=filter_id)
11✔
788

789

790
class ParentNuclideFilter(ParticleFilter):
11✔
791
    """Bins tally events based on the parent nuclide
792

793
    Parameters
794
    ----------
795
    bins : str, or iterable of str
796
        Names of nuclides (e.g., 'Ni65')
797
    filter_id : int
798
        Unique identifier for the filter
799

800
    Attributes
801
    ----------
802
    bins : iterable of str
803
        Names of nuclides
804
    id : int
805
        Unique identifier for the filter
806
    num_bins : Integral
807
        The number of filter bins
808

809
    """
810
    @Filter.bins.setter
11✔
811
    def bins(self, bins):
11✔
812
        bins = np.atleast_1d(bins)
11✔
813
        cv.check_iterable_type('filter bins', bins, str)
11✔
814
        self._bins = bins
11✔
815

816

817
class MeshFilter(Filter):
11✔
818
    """Bins tally event locations by mesh elements.
819

820
    Parameters
821
    ----------
822
    mesh : openmc.MeshBase
823
        The mesh object that events will be tallied onto
824
    filter_id : int
825
        Unique identifier for the filter
826

827
    Attributes
828
    ----------
829
    mesh : openmc.MeshBase
830
        The mesh object that events will be tallied onto
831
    id : int
832
        Unique identifier for the filter
833
    translation : Iterable of float
834
        This array specifies a vector that is used to translate (shift) the mesh
835
        for this filter
836
    rotation : Iterable of float
837
        This array specifies the angles in degrees about the x, y, and z axes
838
        that the mesh should be rotated. The rotation applied is an intrinsic
839
        rotation with specified Tait-Bryan angles. That is to say, if the angles
840
        are :math:`(\phi, \theta, \psi)`, then the rotation matrix applied is
841
        :math:`R_z(\psi) R_y(\theta) R_x(\phi)` or
842

843
        .. math::
844

845
           \left [ \begin{array}{ccc} \cos\theta \cos\psi & -\cos\phi \sin\psi
846
           + \sin\phi \sin\theta \cos\psi & \sin\phi \sin\psi + \cos\phi
847
           \sin\theta \cos\psi \\ \cos\theta \sin\psi & \cos\phi \cos\psi +
848
           \sin\phi \sin\theta \sin\psi & -\sin\phi \cos\psi + \cos\phi
849
           \sin\theta \sin\psi \\ -\sin\theta & \sin\phi \cos\theta & \cos\phi
850
           \cos\theta \end{array} \right ]
851

852
        A rotation matrix can also be specified directly by setting this
853
        attribute to a nested list (or 2D numpy array) that specifies each
854
        element of the matrix.
855
    bins : list of tuple
856
        A list of mesh indices for each filter bin, e.g. [(1, 1, 1), (2, 1, 1),
857
        ...]
858
    num_bins : Integral
859
        The number of filter bins
860

861
    """
862

863
    def __init__(self, mesh, filter_id=None):
11✔
864
        self.mesh = mesh
11✔
865
        self.id = filter_id
11✔
866
        self._translation = None
11✔
867
        self._rotation = None
11✔
868

869
    def __hash__(self):
11✔
870
        string = type(self).__name__ + '\n'
11✔
871
        string += '{: <16}=\t{}\n'.format('\tMesh ID', self.mesh.id)
11✔
872
        return hash(string)
11✔
873

874
    def __repr__(self):
11✔
875
        string = type(self).__name__ + '\n'
11✔
876
        string += '{: <16}=\t{}\n'.format('\tMesh ID', self.mesh.id)
11✔
877
        string += '{: <16}=\t{}\n'.format('\tID', self.id)
11✔
878
        string += '{: <16}=\t{}\n'.format('\tTranslation', self.translation)
11✔
879
        string += '{: <16}=\t{}\n'.format('\tRotation', self.rotation)
11✔
880
        return string
11✔
881

882
    @classmethod
11✔
883
    def from_hdf5(cls, group, **kwargs):
11✔
884
        if group['type'][()].decode() != cls.short_name.lower():
11✔
UNCOV
885
            raise ValueError("Expected HDF5 data for filter type '"
×
886
                             + cls.short_name.lower() + "' but got '"
887
                             + group['type'][()].decode() + " instead")
888

889
        if 'meshes' not in kwargs:
11✔
UNCOV
890
            raise ValueError(cls.__name__ + " requires a 'meshes' keyword "
×
891
                             "argument.")
892

893
        mesh_id = group['bins'][()]
11✔
894
        mesh_obj = kwargs['meshes'][mesh_id]
11✔
895
        filter_id = int(group.name.split('/')[-1].lstrip('filter '))
11✔
896

897
        out = cls(mesh_obj, filter_id=filter_id)
11✔
898

899
        translation = group.get('translation')
11✔
900
        if translation:
11✔
901
            out.translation = translation[()]
11✔
902

903
        rotation = group.get('rotation')
11✔
904
        if rotation:
11✔
905
            out.rotation = rotation[()]
11✔
906

907
        return out
11✔
908

909
    @property
11✔
910
    def mesh(self):
11✔
911
        return self._mesh
11✔
912

913
    @mesh.setter
11✔
914
    def mesh(self, mesh):
11✔
915
        cv.check_type('filter mesh', mesh, openmc.MeshBase)
11✔
916
        self._mesh = mesh
11✔
917
        if isinstance(mesh, openmc.UnstructuredMesh):
11✔
918
            if mesh.has_statepoint_data:
11✔
919
                self.bins = list(range(len(mesh.volumes)))
3✔
920
            else:
921
                self.bins = []
11✔
922
        else:
923
            self.bins = list(mesh.indices)
11✔
924

925
    @property
11✔
926
    def shape(self):
11✔
927
        return self.mesh.dimension
11✔
928

929
    @property
11✔
930
    def translation(self):
11✔
931
        return self._translation
11✔
932

933
    @translation.setter
11✔
934
    def translation(self, t):
11✔
935
        cv.check_type('mesh filter translation', t, Iterable, Real)
11✔
936
        cv.check_length('mesh filter translation', t, 3)
11✔
937
        self._translation = np.asarray(t)
11✔
938

939
    @property
11✔
940
    def rotation(self):
11✔
941
        return self._rotation
11✔
942

943
    @rotation.setter
11✔
944
    def rotation(self, rotation):
11✔
945
        cv.check_length('mesh filter rotation', rotation, 3)
11✔
946
        self._rotation = np.asarray(rotation)
11✔
947

948
    def can_merge(self, other):
11✔
949
        # Mesh filters cannot have more than one bin
UNCOV
950
        return False
×
951

952
    def get_pandas_dataframe(self, data_size, stride, **kwargs):
11✔
953
        """Builds a Pandas DataFrame for the Filter's bins.
954

955
        This method constructs a Pandas DataFrame object for the filter with
956
        columns annotated by filter bin information. This is a helper method for
957
        :meth:`Tally.get_pandas_dataframe`.
958

959
        Parameters
960
        ----------
961
        data_size : int
962
            The total number of bins in the tally corresponding to this filter
963
        stride : int
964
            Stride in memory for the filter
965

966
        Returns
967
        -------
968
        pandas.DataFrame
969
            A Pandas DataFrame with three columns describing the x,y,z mesh
970
            cell indices corresponding to each filter bin.  The number of rows
971
            in the DataFrame is the same as the total number of bins in the
972
            corresponding tally, with the filter bin appropriately tiled to map
973
            to the corresponding tally bins.
974

975
        See also
976
        --------
977
        Tally.get_pandas_dataframe(), CrossFilter.get_pandas_dataframe()
978

979
        """
980
        # Initialize Pandas DataFrame
981
        df = pd.DataFrame()
11✔
982

983
        # Initialize dictionary to build Pandas Multi-index column
984
        filter_dict = {}
11✔
985

986
        # Append mesh ID as outermost index of multi-index
987
        mesh_key = f'mesh {self.mesh.id}'
11✔
988

989
        # Find mesh dimensions - use 3D indices for simplicity
990
        n_dim = len(self.mesh.dimension)
11✔
991
        if n_dim == 3:
11✔
UNCOV
992
            nx, ny, nz = self.mesh.dimension
×
993
        elif n_dim == 2:
11✔
994
            nx, ny = self.mesh.dimension
11✔
995
            nz = 1
11✔
996
        else:
UNCOV
997
            nx = self.mesh.dimension
×
UNCOV
998
            ny = nz = 1
×
999

1000
        # Generate multi-index sub-column for x-axis
1001
        filter_dict[mesh_key, 'x'] = _repeat_and_tile(
11✔
1002
            np.arange(1, nx + 1), stride, data_size)
1003

1004
        # Generate multi-index sub-column for y-axis
1005
        filter_dict[mesh_key, 'y'] = _repeat_and_tile(
11✔
1006
            np.arange(1, ny + 1), nx * stride, data_size)
1007

1008
        # Generate multi-index sub-column for z-axis
1009
        filter_dict[mesh_key, 'z'] = _repeat_and_tile(
11✔
1010
            np.arange(1, nz + 1), nx * ny * stride, data_size)
1011

1012
        # Initialize a Pandas DataFrame from the mesh dictionary
1013
        df = pd.concat([df, pd.DataFrame(filter_dict)])
11✔
1014

1015
        return df
11✔
1016

1017
    def to_xml_element(self):
11✔
1018
        """Return XML Element representing the Filter.
1019

1020
        Returns
1021
        -------
1022
        element : lxml.etree._Element
1023
            XML element containing filter data
1024

1025
        """
1026
        element = ET.Element('filter')
11✔
1027
        element.set('id', str(self.id))
11✔
1028
        element.set('type', self.short_name.lower())
11✔
1029
        subelement = ET.SubElement(element, 'bins')
11✔
1030
        subelement.text = str(self.mesh.id)
11✔
1031
        if self.translation is not None:
11✔
1032
            element.set('translation', ' '.join(map(str, self.translation)))
11✔
1033
        if self.rotation is not None:
11✔
1034
            element.set('rotation', ' '.join(map(str, self.rotation.ravel())))
11✔
1035
        return element
11✔
1036

1037
    @classmethod
11✔
1038
    def from_xml_element(cls, elem: ET.Element, **kwargs) -> MeshFilter:
11✔
1039
        mesh_id = int(get_text(elem, 'bins'))
11✔
1040
        mesh_obj = kwargs['meshes'][mesh_id]
11✔
1041
        filter_id = int(get_text(elem, "id"))
11✔
1042
        out = cls(mesh_obj, filter_id=filter_id)
11✔
1043

1044
        translation = get_elem_list(elem, "translation", float) or []
11✔
1045
        if translation:
11✔
1046
            out.translation = translation
11✔
1047

1048
        rotation = get_elem_list(elem, 'rotation', float) or []
11✔
1049
        if rotation:
11✔
1050
            if len(rotation) == 3:
11✔
1051
                out.rotation = rotation
11✔
1052
            elif len(rotation) == 9:
11✔
1053
                out.rotation = np.array(rotation).reshape(3, 3)
11✔
1054
        return out
11✔
1055

1056

1057
class MeshBornFilter(MeshFilter):
11✔
1058
    """Filter events by the mesh cell a particle originated from.
1059

1060
    Parameters
1061
    ----------
1062
    mesh : openmc.MeshBase
1063
        The mesh object that events will be tallied onto
1064
    filter_id : int
1065
        Unique identifier for the filter
1066

1067
    Attributes
1068
    ----------
1069
    mesh : openmc.MeshBase
1070
        The mesh object that events will be tallied onto
1071
    id : int
1072
        Unique identifier for the filter
1073
    translation : Iterable of float
1074
        This array specifies a vector that is used to translate (shift)
1075
        the mesh for this filter
1076
    bins : list of tuple
1077
        A list of mesh indices for each filter bin, e.g. [(1, 1, 1), (2, 1, 1),
1078
        ...]
1079
    num_bins : Integral
1080
        The number of filter bins
1081

1082
    """
1083

1084

1085
class MeshMaterialFilter(MeshFilter):
11✔
1086
    """Filter events by combinations of mesh elements and materials.
1087

1088
    .. versionadded:: 0.15.3
1089

1090
    Parameters
1091
    ----------
1092
    mesh : openmc.MeshBase
1093
        The mesh object that events will be tallied onto
1094
    bins : iterable of 2-tuples or numpy.ndarray
1095
        Combinations of (mesh element, material) to tally, given as 2-tuples.
1096
        The first value in the tuple represents the index of the mesh element,
1097
        and the second value indicates the material (either a
1098
        :class:`openmc.Material` instance of the ID).
1099
    filter_id : int
1100
        Unique identifier for the filter
1101

1102
    """
1103
    def __init__(self, mesh: openmc.MeshBase, bins, filter_id=None):
11✔
1104
        self.mesh = mesh
11✔
1105
        self.bins = bins
11✔
1106
        self.id = filter_id
11✔
1107
        self._translation = None
11✔
1108

1109
    @classmethod
11✔
1110
    def from_volumes(cls, mesh: openmc.MeshBase, volumes: openmc.MeshMaterialVolumes):
11✔
1111
        """Construct a MeshMaterialFilter from a MeshMaterialVolumes object.
1112

1113
        Parameters
1114
        ----------
1115
        mesh : openmc.MeshBase
1116
            The mesh object that events will be tallied onto
1117
        volumes : openmc.MeshMaterialVolumes
1118
            The mesh material volumes to use for the filter
1119

1120
        Returns
1121
        -------
1122
        MeshMaterialFilter
1123
            A new MeshMaterialFilter instance
1124

1125
        """
1126
        # Get flat arrays of material IDs and element indices
1127
        mat_ids = volumes._materials[volumes._materials > -1]
11✔
1128
        elems, _ = np.where(volumes._materials > -1)
11✔
1129

1130
        # Stack them into a 2D array of (element, material) pairs
1131
        bins = np.column_stack((elems, mat_ids))
11✔
1132
        return cls(mesh, bins)
11✔
1133

1134
    def __hash__(self):
11✔
1135
        data = (type(self).__name__, self.mesh.id, tuple(self.bins.ravel()))
11✔
1136
        return hash(data)
11✔
1137

1138
    def __repr__(self):
11✔
1139
        string = type(self).__name__ + '\n'
11✔
1140
        string += '{: <16}=\t{}\n'.format('\tID', self.id)
11✔
1141
        string += '{: <16}=\t{}\n'.format('\tMesh ID', self.mesh.id)
11✔
1142
        string += '{: <16}=\n{}\n'.format('\tBins', self.bins)
11✔
1143
        string += '{: <16}=\t{}\n'.format('\tTranslation', self.translation)
11✔
1144
        return string
11✔
1145

1146
    @property
11✔
1147
    def shape(self):
11✔
1148
        return (self.num_bins,)
11✔
1149

1150
    @property
11✔
1151
    def mesh(self):
11✔
1152
        return self._mesh
11✔
1153

1154
    @mesh.setter
11✔
1155
    def mesh(self, mesh):
11✔
1156
        cv.check_type('filter mesh', mesh, openmc.MeshBase)
11✔
1157
        self._mesh = mesh
11✔
1158

1159
    @Filter.bins.setter
11✔
1160
    def bins(self, bins):
11✔
1161
        pairs = np.empty((len(bins), 2), dtype=int)
11✔
1162
        for i, (elem, mat) in enumerate(bins):
11✔
1163
            cv.check_type('element', elem, Integral)
11✔
1164
            cv.check_type('material', mat, (Integral, openmc.Material))
11✔
1165
            pairs[i, 0] = elem
11✔
1166
            pairs[i, 1] = mat if isinstance(mat, Integral) else mat.id
11✔
1167
        self._bins = pairs
11✔
1168

1169
    def to_xml_element(self):
11✔
1170
        """Return XML element representing the filter.
1171

1172
        Returns
1173
        -------
1174
        element : lxml.etree._Element
1175
            XML element containing filter data
1176

1177
        """
1178
        element = ET.Element('filter')
11✔
1179
        element.set('id', str(self.id))
11✔
1180
        element.set('type', self.short_name.lower())
11✔
1181
        element.set('mesh', str(self.mesh.id))
11✔
1182

1183
        if self.translation is not None:
11✔
UNCOV
1184
            element.set('translation', ' '.join(map(str, self.translation)))
×
1185

1186
        subelement = ET.SubElement(element, 'bins')
11✔
1187
        subelement.text = ' '.join(str(i) for i in self.bins.ravel())
11✔
1188

1189
        return element
11✔
1190

1191
    @classmethod
11✔
1192
    def from_xml_element(cls, elem: ET.Element, **kwargs) -> MeshMaterialFilter:
11✔
1193
        filter_id = int(get_text(elem, "id"))
11✔
1194
        mesh_id = int(get_text(elem, "mesh"))
11✔
1195
        mesh_obj = kwargs['meshes'][mesh_id]
11✔
1196
        bins = get_elem_list(elem, "bins", int) or []
11✔
1197
        bins = list(zip(bins[::2], bins[1::2]))
11✔
1198
        out = cls(mesh_obj, bins, filter_id=filter_id)
11✔
1199

1200
        translation = get_elem_list(elem, "translation", float) or []
11✔
1201
        if translation:
11✔
UNCOV
1202
            out.translation = translation
×
1203
        return out
11✔
1204

1205
    @classmethod
11✔
1206
    def from_hdf5(cls, group, **kwargs):
11✔
1207
        if group['type'][()].decode() != cls.short_name.lower():
11✔
UNCOV
1208
            raise ValueError("Expected HDF5 data for filter type '"
×
1209
                             + cls.short_name.lower() + "' but got '"
1210
                             + group['type'][()].decode() + " instead")
1211

1212
        if 'meshes' not in kwargs:
11✔
UNCOV
1213
            raise ValueError(cls.__name__ + " requires a 'meshes' keyword "
×
1214
                             "argument.")
1215

1216
        mesh_id = group['mesh'][()]
11✔
1217
        mesh_obj = kwargs['meshes'][mesh_id]
11✔
1218
        bins = group['bins'][()]
11✔
1219
        filter_id = int(group.name.split('/')[-1].lstrip('filter '))
11✔
1220
        out = cls(mesh_obj, bins, filter_id=filter_id)
11✔
1221

1222
        translation = group.get('translation')
11✔
1223
        if translation:
11✔
UNCOV
1224
            out.translation = translation[()]
×
1225

1226
        return out
11✔
1227

1228
    def get_pandas_dataframe(self, data_size, stride, **kwargs):
11✔
1229
        """Builds a Pandas DataFrame for the Filter's bins.
1230

1231
        This method constructs a Pandas DataFrame object for the filter with
1232
        columns annotated by filter bin information. This is a helper method for
1233
        :meth:`Tally.get_pandas_dataframe`.
1234

1235
        Parameters
1236
        ----------
1237
        data_size : int
1238
            The total number of bins in the tally corresponding to this filter
1239
        stride : int
1240
            Stride in memory for the filter
1241

1242
        Returns
1243
        -------
1244
        pandas.DataFrame
1245
            A Pandas DataFrame with a multi-index column for the cell instance.
1246
            The number of rows in the DataFrame is the same as the total number
1247
            of bins in the corresponding tally, with the filter bin appropriately
1248
            tiled to map to the corresponding tally bins.
1249

1250
        See also
1251
        --------
1252
        Tally.get_pandas_dataframe(), CrossFilter.get_pandas_dataframe()
1253

1254
        """
1255
        # Repeat and tile bins as necessary to account for other filters.
1256
        bins = np.repeat(self.bins, stride, axis=0)
11✔
1257
        tile_factor = data_size // len(bins)
11✔
1258
        bins = np.tile(bins, (tile_factor, 1))
11✔
1259

1260
        columns = pd.MultiIndex.from_product([[self.short_name.lower()],
11✔
1261
                                              ['element', 'material']])
1262
        return pd.DataFrame(bins, columns=columns)
11✔
1263

1264

1265
class MeshSurfaceFilter(MeshFilter):
11✔
1266
    """Filter events by surface crossings on a mesh.
1267

1268
    Parameters
1269
    ----------
1270
    mesh : openmc.MeshBase
1271
        The mesh object that events will be tallied onto
1272
    filter_id : int
1273
        Unique identifier for the filter
1274

1275
    Attributes
1276
    ----------
1277
    mesh : openmc.MeshBase
1278
        The mesh object that events will be tallied onto
1279
    translation : Iterable of float
1280
        This array specifies a vector that is used to translate (shift)
1281
        the mesh for this filter
1282
    id : int
1283
        Unique identifier for the filter
1284
    bins : list of tuple
1285
        A list of mesh indices / surfaces for each filter bin, e.g. [(1, 1,
1286
        'x-min out'), (1, 1, 'x-min in'), ...]
1287
    num_bins : Integral
1288
        The number of filter bins
1289

1290
    """
1291
    @property
11✔
1292
    def shape(self):
11✔
1293
        return (self.num_bins,)
11✔
1294

1295
    @MeshFilter.mesh.setter
11✔
1296
    def mesh(self, mesh):
11✔
1297
        cv.check_type('filter mesh', mesh, openmc.MeshBase)
11✔
1298
        self._mesh = mesh
11✔
1299

1300
        # Take the product of mesh indices and current names
1301
        n_dim = mesh.n_dimension
11✔
1302
        self.bins = [mesh_tuple + (surf,) for mesh_tuple, surf in
11✔
1303
                     product(mesh.indices, _CURRENT_NAMES[:4*n_dim])]
1304

1305
    def get_pandas_dataframe(self, data_size, stride, **kwargs):
11✔
1306
        """Builds a Pandas DataFrame for the Filter's bins.
1307

1308
        This method constructs a Pandas DataFrame object for the filter with
1309
        columns annotated by filter bin information. This is a helper method for
1310
        :meth:`Tally.get_pandas_dataframe`.
1311

1312
        Parameters
1313
        ----------
1314
        data_size : int
1315
            The total number of bins in the tally corresponding to this filter
1316
        stride : int
1317
            Stride in memory for the filter
1318

1319
        Returns
1320
        -------
1321
        pandas.DataFrame
1322
            A Pandas DataFrame with three columns describing the x,y,z mesh
1323
            cell indices corresponding to each filter bin.  The number of rows
1324
            in the DataFrame is the same as the total number of bins in the
1325
            corresponding tally, with the filter bin appropriately tiled to map
1326
            to the corresponding tally bins.
1327

1328
        See also
1329
        --------
1330
        Tally.get_pandas_dataframe(), CrossFilter.get_pandas_dataframe()
1331

1332
        """
1333
        # Initialize Pandas DataFrame
1334
        df = pd.DataFrame()
11✔
1335

1336
        # Initialize dictionary to build Pandas Multi-index column
1337
        filter_dict = {}
11✔
1338

1339
        # Append mesh ID as outermost index of multi-index
1340
        mesh_key = f'mesh {self.mesh.id}'
11✔
1341

1342
        # Find mesh dimensions - use 3D indices for simplicity
1343
        n_surfs = 4 * len(self.mesh.dimension)
11✔
1344
        if len(self.mesh.dimension) == 3:
11✔
UNCOV
1345
            nx, ny, nz = self.mesh.dimension
×
1346
        elif len(self.mesh.dimension) == 2:
11✔
1347
            nx, ny = self.mesh.dimension
11✔
1348
            nz = 1
11✔
1349
        else:
UNCOV
1350
            nx = self.mesh.dimension
×
UNCOV
1351
            ny = nz = 1
×
1352

1353
        # Generate multi-index sub-column for x-axis
1354
        filter_dict[mesh_key, 'x'] = _repeat_and_tile(
11✔
1355
            np.arange(1, nx + 1), n_surfs * stride, data_size)
1356

1357
        # Generate multi-index sub-column for y-axis
1358
        if len(self.mesh.dimension) > 1:
11✔
1359
            filter_dict[mesh_key, 'y'] = _repeat_and_tile(
11✔
1360
                np.arange(1, ny + 1), n_surfs * nx * stride, data_size)
1361

1362
        # Generate multi-index sub-column for z-axis
1363
        if len(self.mesh.dimension) > 2:
11✔
UNCOV
1364
            filter_dict[mesh_key, 'z'] = _repeat_and_tile(
×
1365
                np.arange(1, nz + 1), n_surfs * nx * ny * stride, data_size)
1366

1367
        # Generate multi-index sub-column for surface
1368
        filter_dict[mesh_key, 'surf'] = _repeat_and_tile(
11✔
1369
            _CURRENT_NAMES[:n_surfs], stride, data_size)
1370

1371
        # Initialize a Pandas DataFrame from the mesh dictionary
1372
        return pd.concat([df, pd.DataFrame(filter_dict)])
11✔
1373

1374

1375
class CollisionFilter(Filter):
11✔
1376
    """Bins tally events based on the number of collisions.
1377

1378
    .. versionadded:: 0.12.2
1379

1380
    Parameters
1381
    ----------
1382
    bins : Iterable of int
1383
        A list or iterable of the number of collisions, as integer values.
1384
        The events whose post-scattering collision number equals one of
1385
        the provided values will be counted.
1386
    filter_id : int
1387
        Unique identifier for the filter
1388

1389
    Attributes
1390
    ----------
1391
    id : int
1392
        Unique identifier for the filter
1393
    bins : numpy.ndarray
1394
        An array of integer values representing the number of collisions events
1395
        by which to filter
1396
    num_bins : int
1397
        The number of filter bins
1398

1399
    """
1400

1401
    def __init__(self, bins, filter_id=None):
11✔
1402
        self.bins = np.asarray(bins)
11✔
1403
        self.id = filter_id
11✔
1404

1405
    def check_bins(self, bins):
11✔
1406
        for x in bins:
11✔
1407
            # Values should be integers
1408
            cv.check_type('filter value', x, Integral)
11✔
1409
            cv.check_greater_than('filter value', x, 0, equality=True)
11✔
1410

1411

1412
class RealFilter(Filter):
11✔
1413
    """Tally modifier that describes phase-space and other characteristics
1414

1415
    Parameters
1416
    ----------
1417
    values : iterable of float
1418
        A list of values for which each successive pair constitutes a range of
1419
        values for a single bin
1420
    filter_id : int
1421
        Unique identifier for the filter
1422

1423
    Attributes
1424
    ----------
1425
    values : numpy.ndarray
1426
        An array of values for which each successive pair constitutes a range of
1427
        values for a single bin
1428
    id : int
1429
        Unique identifier for the filter
1430
    bins : numpy.ndarray
1431
        An array of shape (N, 2) where each row is a pair of values indicating a
1432
        filter bin range
1433
    num_bins : int
1434
        The number of filter bins
1435

1436
    """
1437
    def __init__(self, values, filter_id=None):
11✔
1438
        self.values = np.asarray(values)
11✔
1439
        self.bins = np.vstack((self.values[:-1], self.values[1:])).T
11✔
1440
        self.id = filter_id
11✔
1441

1442
    def __gt__(self, other):
11✔
1443
        if type(self) is type(other):
11✔
1444
            # Compare largest/smallest bin edges in filters
1445
            # This logic is used when merging tallies with real filters
1446
            return self.values[0] >= other.values[-1]
11✔
1447
        else:
1448
            return super().__gt__(other)
11✔
1449

1450
    def __repr__(self):
11✔
1451
        string = type(self).__name__ + '\n'
11✔
1452
        string += '{: <16}=\t{}\n'.format('\tValues', self.values)
11✔
1453
        string += '{: <16}=\t{}\n'.format('\tID', self.id)
11✔
1454
        return string
11✔
1455

1456
    @Filter.bins.setter
11✔
1457
    def bins(self, bins):
11✔
1458
        Filter.bins.__set__(self, np.asarray(bins))
11✔
1459

1460
    def check_bins(self, bins):
11✔
1461
        for v0, v1 in bins:
11✔
1462
            # Values should be real
1463
            cv.check_type('filter value', v0, Real)
11✔
1464
            cv.check_type('filter value', v1, Real)
11✔
1465

1466
            # Make sure that each tuple has values that are increasing
1467
            if v1 <= v0:
11✔
1468
                raise ValueError(f'Values {v0} and {v1} appear to be out of '
11✔
1469
                                 'order')
1470

1471
        for pair0, pair1 in zip(bins[:-1], bins[1:]):
11✔
1472
            # Successive pairs should be ordered
1473
            if pair1[1] < pair0[1]:
11✔
UNCOV
1474
                raise ValueError(f'Values {pair1[1]} and {pair0[1]} appear to '
×
1475
                                 'be out of order')
1476

1477
    def can_merge(self, other):
11✔
1478
        if type(self) is not type(other):
11✔
1479
            return False
11✔
1480

1481
        if self.bins[0, 0] == other.bins[-1][1]:
11✔
1482
            # This low edge coincides with other's high edge
UNCOV
1483
            return True
×
1484
        elif self.bins[-1][1] == other.bins[0, 0]:
11✔
1485
            # This high edge coincides with other's low edge
1486
            return True
11✔
1487
        else:
1488
            return False
11✔
1489

1490
    def merge(self, other):
11✔
1491
        if not self.can_merge(other):
11✔
UNCOV
1492
            msg = f'Unable to merge "{type(self)}" with "{type(other)}" filters'
×
UNCOV
1493
            raise ValueError(msg)
×
1494

1495
        # Merge unique filter bins
1496
        merged_values = np.concatenate((self.values, other.values))
11✔
1497
        merged_values = np.unique(merged_values)
11✔
1498

1499
        # Create a new filter with these bins and a new auto-generated ID
1500
        return type(self)(sorted(merged_values))
11✔
1501

1502
    def is_subset(self, other):
11✔
1503
        """Determine if another filter is a subset of this filter.
1504

1505
        If all of the bins in the other filter are included as bins in this
1506
        filter, then it is a subset of this filter.
1507

1508
        Parameters
1509
        ----------
1510
        other : openmc.Filter
1511
            The filter to query as a subset of this filter
1512

1513
        Returns
1514
        -------
1515
        bool
1516
            Whether or not the other filter is a subset of this filter
1517

1518
        """
1519

1520
        if type(self) is not type(other):
11✔
1521
            return False
11✔
1522
        elif self.num_bins != other.num_bins:
11✔
UNCOV
1523
            return False
×
1524
        else:
1525
            return np.allclose(self.values, other.values)
11✔
1526

1527
    def get_bin_index(self, filter_bin):
11✔
1528
        i = np.where(self.bins[:, 1] == filter_bin[1])[0]
3✔
1529
        if len(i) == 0:
3✔
UNCOV
1530
            msg = ('Unable to get the bin index for Filter since '
×
1531
                   f'"{filter_bin}" is not one of the bins')
UNCOV
1532
            raise ValueError(msg)
×
1533
        else:
1534
            return i[0]
3✔
1535

1536
    def get_pandas_dataframe(self, data_size, stride, **kwargs):
11✔
1537
        """Builds a Pandas DataFrame for the Filter's bins.
1538

1539
        This method constructs a Pandas DataFrame object for the filter with
1540
        columns annotated by filter bin information. This is a helper method for
1541
        :meth:`Tally.get_pandas_dataframe`.
1542

1543
        Parameters
1544
        ----------
1545
        data_size : int
1546
            The total number of bins in the tally corresponding to this filter
1547
        stride : int
1548
            Stride in memory for the filter
1549

1550
        Returns
1551
        -------
1552
        pandas.DataFrame
1553
            A Pandas DataFrame with one column of the lower energy bound and one
1554
            column of upper energy bound for each filter bin.  The number of
1555
            rows in the DataFrame is the same as the total number of bins in the
1556
            corresponding tally, with the filter bin appropriately tiled to map
1557
            to the corresponding tally bins.
1558

1559
        See also
1560
        --------
1561
        Tally.get_pandas_dataframe(), CrossFilter.get_pandas_dataframe()
1562

1563
        """
1564
        # Initialize Pandas DataFrame
1565
        df = pd.DataFrame()
11✔
1566

1567
        # Extract the lower and upper energy bounds, then repeat and tile
1568
        # them as necessary to account for other filters.
1569
        lo_bins = np.repeat(self.bins[:, 0], stride)
11✔
1570
        hi_bins = np.repeat(self.bins[:, 1], stride)
11✔
1571
        tile_factor = data_size // len(lo_bins)
11✔
1572
        lo_bins = np.tile(lo_bins, tile_factor)
11✔
1573
        hi_bins = np.tile(hi_bins, tile_factor)
11✔
1574

1575
        # Add the new energy columns to the DataFrame.
1576
        if hasattr(self, 'units'):
11✔
1577
            units = f' [{self.units}]'
11✔
1578
        else:
1579
            units = ''
11✔
1580

1581
        df.loc[:, self.short_name.lower() + ' low' + units] = lo_bins
11✔
1582
        df.loc[:, self.short_name.lower() + ' high' + units] = hi_bins
11✔
1583

1584
        return df
11✔
1585

1586
    def to_xml_element(self):
11✔
1587
        """Return XML Element representing the Filter.
1588

1589
        Returns
1590
        -------
1591
        element : lxml.etree._Element
1592
            XML element containing filter data
1593

1594
        """
1595
        element = super().to_xml_element()
11✔
1596
        element[0].text = ' '.join(str(x) for x in self.values)
11✔
1597
        return element
11✔
1598

1599
    @classmethod
11✔
1600
    def from_xml_element(cls, elem, **kwargs):
11✔
1601
        filter_id = int(get_text(elem, "id"))
11✔
1602
        bins = get_elem_list(elem, "bins", float) or []
11✔
1603
        return cls(bins, filter_id=filter_id)
11✔
1604

1605

1606
class EnergyFilter(RealFilter):
11✔
1607
    """Bins tally events based on incident particle energy.
1608

1609
    Parameters
1610
    ----------
1611
    values : Iterable of Real
1612
        A list of values for which each successive pair constitutes a range of
1613
        energies in [eV] for a single bin. Entries must be positive and ascending.
1614
    filter_id : int
1615
        Unique identifier for the filter
1616

1617
    Attributes
1618
    ----------
1619
    values : numpy.ndarray
1620
        An array of values for which each successive pair constitutes a range of
1621
        energies in [eV] for a single bin
1622
    id : int
1623
        Unique identifier for the filter
1624
    bins : numpy.ndarray
1625
        An array of shape (N, 2) where each row is a pair of energies in [eV]
1626
        for a single filter bin
1627
    num_bins : int
1628
        The number of filter bins
1629

1630
    """
1631
    units = 'eV'
11✔
1632

1633
    def __init__(self, values, filter_id=None):
11✔
1634
        cv.check_length('values', values, 2)
11✔
1635
        super().__init__(values, filter_id)
11✔
1636

1637
    def get_bin_index(self, filter_bin):
11✔
1638
        # Use lower energy bound to find index for RealFilters
1639
        deltas = np.abs(self.bins[:, 1] - filter_bin[1]) / filter_bin[1]
11✔
1640
        min_delta = np.min(deltas)
11✔
1641
        if min_delta < 1E-3:
11✔
1642
            return deltas.argmin()
11✔
1643
        else:
UNCOV
1644
            msg = ('Unable to get the bin index for Filter since '
×
1645
                   f'"{filter_bin}" is not one of the bins')
UNCOV
1646
            raise ValueError(msg)
×
1647

1648
    def check_bins(self, bins):
11✔
1649
        super().check_bins(bins)
11✔
1650
        for v0, v1 in bins:
11✔
1651
            cv.check_greater_than('filter value', v0, 0., equality=True)
11✔
1652
            cv.check_greater_than('filter value', v1, 0., equality=True)
11✔
1653

1654
    def get_tabular(self, values, **kwargs):
11✔
1655
        """Create a tabulated distribution based on tally results with an energy filter
1656

1657
        This method provides an easy way to create a distribution in energy
1658
        (e.g., a source spectrum) based on tally results that were obtained from
1659
        using an :class:`~openmc.EnergyFilter`.
1660

1661
        .. versionadded:: 0.13.3
1662

1663
        Parameters
1664
        ----------
1665
        values : iterable of float
1666
            Array of numeric values, typically from a tally result
1667
        **kwargs
1668
            Keyword arguments passed to :class:`openmc.stats.Tabular`
1669

1670
        Returns
1671
        -------
1672
        openmc.stats.Tabular
1673
            Tabular distribution with histogram interpolation
1674
        """
1675

1676
        probabilities = np.array(values, dtype=float)
11✔
1677
        probabilities /= probabilities.sum()
11✔
1678

1679
        # Determine probability per eV, adding extra 0 at the end since it is a histogram
1680
        probability_per_ev = probabilities / np.diff(self.values)
11✔
1681
        probability_per_ev = np.append(probability_per_ev, 0.0)
11✔
1682

1683
        kwargs.setdefault('interpolation', 'histogram')
11✔
1684
        return openmc.stats.Tabular(self.values, probability_per_ev, **kwargs)
11✔
1685

1686
    @property
11✔
1687
    def lethargy_bin_width(self):
11✔
1688
        """Calculates the base 10 log width of energy bins which is useful when
1689
        plotting the normalized flux.
1690

1691
        Returns
1692
        -------
1693
        numpy.array
1694
            Array of bin widths
1695
        """
1696
        return np.log10(self.bins[:, 1]/self.bins[:, 0])
11✔
1697

1698
    @classmethod
11✔
1699
    def from_group_structure(cls, group_structure):
11✔
1700
        """Construct an EnergyFilter instance from a standard group structure.
1701

1702
        .. versionadded:: 0.13.1
1703

1704
        Parameters
1705
        ----------
1706
        group_structure : str
1707
            Name of the group structure. Must be a valid key of
1708
            openmc.mgxs.GROUP_STRUCTURES dictionary.
1709

1710
        """
1711

1712
        cv.check_value('group_structure', group_structure, openmc.mgxs.GROUP_STRUCTURES.keys())
11✔
1713
        return cls(openmc.mgxs.GROUP_STRUCTURES[group_structure.upper()])
11✔
1714

1715

1716
class EnergyoutFilter(EnergyFilter):
11✔
1717
    """Bins tally events based on outgoing particle energy.
1718

1719
    Parameters
1720
    ----------
1721
    values : Iterable of Real
1722
        A list of values for which each successive pair constitutes a range of
1723
        energies in [eV] for a single bin
1724
    filter_id : int
1725
        Unique identifier for the filter
1726

1727
    Attributes
1728
    ----------
1729
    values : numpy.ndarray
1730
        An array of values for which each successive pair constitutes a range of
1731
        energies in [eV] for a single bin
1732
    id : int
1733
        Unique identifier for the filter
1734
    bins : numpy.ndarray
1735
        An array of shape (N, 2) where each row is a pair of energies in [eV]
1736
        for a single filter bin
1737
    num_bins : int
1738
        The number of filter bins
1739

1740
    """
1741

1742
class TimeFilter(RealFilter):
11✔
1743
    """Bins tally events based on the particle's time.
1744

1745
    .. versionadded:: 0.13.0
1746

1747
    Parameters
1748
    ----------
1749
    values : iterable of float
1750
        A list of values for which each successive pair constitutes a range of
1751
        time in [s] for a single bin
1752
    filter_id : int
1753
        Unique identifier for the filter
1754

1755
    Attributes
1756
    ----------
1757
    values : numpy.ndarray
1758
        An array of values for which each successive pair constitutes a range of
1759
        time in [s] for a single bin
1760
    id : int
1761
        Unique identifier for the filter
1762
    bins : numpy.ndarray
1763
        An array of shape (N, 2) where each row is a pair of time in [s]
1764
        for a single filter bin
1765
    num_bins : int
1766
        The number of filter bins
1767

1768
    """
1769
    units = 's'
11✔
1770

1771
    def get_bin_index(self, filter_bin):
11✔
1772
        # Use lower energy bound to find index for RealFilters
UNCOV
1773
        deltas = np.abs(self.bins[:, 1] - filter_bin[1]) / filter_bin[1]
×
UNCOV
1774
        min_delta = np.min(deltas)
×
UNCOV
1775
        if min_delta < 1e-3:
×
UNCOV
1776
            return deltas.argmin()
×
1777
        else:
UNCOV
1778
            msg = ('Unable to get the bin index for Filter since '
×
1779
                   f'"{filter_bin}" is not one of the bins')
UNCOV
1780
            raise ValueError(msg)
×
1781

1782
    def check_bins(self, bins):
11✔
1783
        super().check_bins(bins)
11✔
1784
        for v0, v1 in bins:
11✔
1785
            cv.check_greater_than('filter value', v0, 0., equality=True)
11✔
1786
            cv.check_greater_than('filter value', v1, 0., equality=True)
11✔
1787

1788

1789
def _path_to_levels(path):
11✔
1790
    """Convert distribcell path to list of levels
1791

1792
    Parameters
1793
    ----------
1794
    path : str
1795
        Distribcell path
1796

1797
    Returns
1798
    -------
1799
    list
1800
        List of levels in path
1801

1802
    """
1803
    # Split path into universes/cells/lattices
1804
    path_items = path.split('->')
11✔
1805

1806
    # Pair together universe and cell information from the same level
1807
    idx = [i for i, item in enumerate(path_items) if item.startswith('u')]
11✔
1808
    for i in reversed(idx):
11✔
1809
        univ_id = int(path_items.pop(i)[1:])
11✔
1810
        cell_id = int(path_items.pop(i)[1:])
11✔
1811
        path_items.insert(i, ('universe', univ_id, cell_id))
11✔
1812

1813
    # Reformat lattice into tuple
1814
    idx = [i for i, item in enumerate(path_items) if isinstance(item, str)]
11✔
1815
    for i in idx:
11✔
1816
        item = path_items.pop(i)[1:-1]
11✔
1817
        lat_id, lat_xyz = item.split('(')
11✔
1818
        lat_id = int(lat_id)
11✔
1819
        lat_xyz = tuple(int(x) for x in lat_xyz.split(','))
11✔
1820
        path_items.insert(i, ('lattice', lat_id, lat_xyz))
11✔
1821

1822
    return path_items
11✔
1823

1824

1825
class DistribcellFilter(Filter):
11✔
1826
    """Bins tally event locations on instances of repeated cells.
1827

1828
    This filter provides a separate score for each unique instance of a repeated
1829
    cell in a geometry. Note that only one cell can be specified in this filter.
1830
    The related :class:`CellInstanceFilter` allows one to obtain scores for
1831
    particular cell instances as well as instances from different cells.
1832

1833
    Parameters
1834
    ----------
1835
    cell : openmc.Cell or Integral
1836
        The distributed cell to tally. Either an openmc.Cell or an Integral
1837
        cell ID number can be used.
1838
    filter_id : int
1839
        Unique identifier for the filter
1840

1841
    Attributes
1842
    ----------
1843
    bins : Iterable of Integral
1844
        An iterable with one element---the ID of the distributed Cell.
1845
    id : int
1846
        Unique identifier for the filter
1847
    num_bins : int
1848
        The number of filter bins
1849
    paths : list of str
1850
        The paths traversed through the CSG tree to reach each distribcell
1851
        instance (for 'distribcell' filters only)
1852

1853
    See Also
1854
    --------
1855
    CellInstanceFilter
1856

1857
    """
1858

1859
    def __init__(self, cell, filter_id=None):
11✔
1860
        self._paths = None
11✔
1861
        super().__init__(cell, filter_id)
11✔
1862

1863
    @classmethod
11✔
1864
    def from_hdf5(cls, group, **kwargs):
11✔
1865
        if group['type'][()].decode() != cls.short_name.lower():
11✔
UNCOV
1866
            raise ValueError("Expected HDF5 data for filter type '"
×
1867
                             + cls.short_name.lower() + "' but got '"
1868
                             + group['type'][()].decode() + " instead")
1869

1870
        filter_id = int(group.name.split('/')[-1].lstrip('filter '))
11✔
1871

1872
        out = cls(group['bins'][()], filter_id=filter_id)
11✔
1873
        out._num_bins = group['n_bins'][()]
11✔
1874

1875
        return out
11✔
1876

1877
    @property
11✔
1878
    def num_bins(self):
11✔
1879
        # Need to handle number of bins carefully -- for distribcell tallies, we
1880
        # need to know how many instances of the cell there are
1881
        return self._num_bins
11✔
1882

1883
    @property
11✔
1884
    def paths(self):
11✔
1885
        if self._paths is None:
11✔
1886
            if not hasattr(self, '_geometry'):
11✔
UNCOV
1887
                raise ValueError(
×
1888
                    "Model must be exported before the 'paths' attribute is" \
1889
                    "available for a DistribcellFilter.")
1890

1891
            # Determine paths for cell instances
1892
            self._geometry.determine_paths()
11✔
1893

1894
            # Get paths for the corresponding cell
1895
            cell_id = self.bins[0]
11✔
1896
            cell = self._geometry.get_all_cells()[cell_id]
11✔
1897
            self._paths = cell.paths
11✔
1898

1899
        return self._paths
11✔
1900

1901
    @Filter.bins.setter
11✔
1902
    def bins(self, bins):
11✔
1903
        # Format the bins as a 1D numpy array.
1904
        bins = np.atleast_1d(bins)
11✔
1905

1906
        # Make sure there is only 1 bin.
1907
        if not len(bins) == 1:
11✔
UNCOV
1908
            msg = (f'Unable to add bins "{bins}" to a DistribcellFilter since '
×
1909
                  'only a single distribcell can be used per tally')
UNCOV
1910
            raise ValueError(msg)
×
1911

1912
        # Check the type and extract the id, if necessary.
1913
        cv.check_type('distribcell bin', bins[0], (Integral, openmc.Cell))
11✔
1914
        if isinstance(bins[0], openmc.Cell):
11✔
1915
            bins = np.atleast_1d(bins[0].id)
11✔
1916

1917
        self._bins = bins
11✔
1918

1919
    def can_merge(self, other):
11✔
1920
        # Distribcell filters cannot have more than one bin
UNCOV
1921
        return False
×
1922

1923
    def get_bin_index(self, filter_bin):
11✔
1924
        # Filter bins for distribcells are indices of each unique placement of
1925
        # the Cell in the Geometry (consecutive integers starting at 0).
1926
        return filter_bin
11✔
1927

1928
    def get_pandas_dataframe(self, data_size, stride, **kwargs):
11✔
1929
        """Builds a Pandas DataFrame for the Filter's bins.
1930

1931
        This method constructs a Pandas DataFrame object for the filter with
1932
        columns annotated by filter bin information. This is a helper method for
1933
        :meth:`Tally.get_pandas_dataframe`.
1934

1935
        Parameters
1936
        ----------
1937
        data_size : int
1938
            The total number of bins in the tally corresponding to this filter
1939
        stride : int
1940
            Stride in memory for the filter
1941

1942
        Keyword arguments
1943
        -----------------
1944
        paths : bool
1945
            If True (default), expand distribcell indices into multi-index
1946
            columns describing the path to that distribcell through the CSG
1947
            tree.  NOTE: This option assumes that all distribcell paths are of
1948
            the same length and do not have the same universes and cells but
1949
            different lattice cell indices.
1950

1951
        Returns
1952
        -------
1953
        pandas.DataFrame
1954
            A Pandas DataFrame with columns describing distributed cells. The
1955
            dataframe will have either:
1956

1957
            1. a single column with the cell instance IDs (without summary info)
1958
            2. separate columns for the cell IDs, universe IDs, and lattice IDs
1959
               and x,y,z cell indices corresponding to each (distribcell paths).
1960

1961
            The number of rows in the DataFrame is the same as the total number
1962
            of bins in the corresponding tally, with the filter bin
1963
            appropriately tiled to map to the corresponding tally bins.
1964

1965
        See also
1966
        --------
1967
        Tally.get_pandas_dataframe(), CrossFilter.get_pandas_dataframe()
1968

1969
        """
1970
        # Initialize Pandas DataFrame
1971
        df = pd.DataFrame()
11✔
1972

1973
        level_df = None
11✔
1974

1975
        paths = kwargs.setdefault('paths', True)
11✔
1976

1977
        # Create Pandas Multi-index columns for each level in CSG tree
1978
        if paths:
11✔
1979

1980
            # Distribcell paths require linked metadata from the Summary
1981
            if self.paths is None:
11✔
UNCOV
1982
                msg = 'Unable to construct distribcell paths since ' \
×
1983
                      'the Summary is not linked to the StatePoint'
UNCOV
1984
                raise ValueError(msg)
×
1985

1986
            # Make copy of array of distribcell paths to use in
1987
            # Pandas Multi-index column construction
1988
            num_offsets = len(self.paths)
11✔
1989
            paths = [_path_to_levels(p) for p in self.paths]
11✔
1990

1991
            # Loop over CSG levels in the distribcell paths
1992
            num_levels = len(paths[0])
11✔
1993
            for i_level in range(num_levels):
11✔
1994
                # Use level key as first index in Pandas Multi-index column
1995
                level_key = f'level {i_level + 1}'
11✔
1996

1997
                # Create a dictionary for this level for Pandas Multi-index
1998
                level_dict = {}
11✔
1999

2000
                # Use the first distribcell path to determine if level
2001
                # is a universe/cell or lattice level
2002
                path = paths[0]
11✔
2003
                if path[i_level][0] == 'lattice':
11✔
2004
                    # Initialize prefix Multi-index keys
2005
                    lat_id_key = (level_key, 'lat', 'id')
11✔
2006
                    lat_x_key = (level_key, 'lat', 'x')
11✔
2007
                    lat_y_key = (level_key, 'lat', 'y')
11✔
2008
                    lat_z_key = (level_key, 'lat', 'z')
11✔
2009

2010
                    # Allocate NumPy arrays for each CSG level and
2011
                    # each Multi-index column in the DataFrame
2012
                    level_dict[lat_id_key] = np.empty(num_offsets)
11✔
2013
                    level_dict[lat_x_key] = np.empty(num_offsets)
11✔
2014
                    level_dict[lat_y_key] = np.empty(num_offsets)
11✔
2015
                    if len(path[i_level][2]) == 3:
11✔
UNCOV
2016
                        level_dict[lat_z_key] = np.empty(num_offsets)
×
2017

2018
                else:
2019
                    # Initialize prefix Multi-index keys
2020
                    univ_key = (level_key, 'univ', 'id')
11✔
2021
                    cell_key = (level_key, 'cell', 'id')
11✔
2022

2023
                    # Allocate NumPy arrays for each CSG level and
2024
                    # each Multi-index column in the DataFrame
2025
                    level_dict[univ_key] = np.empty(num_offsets)
11✔
2026
                    level_dict[cell_key] = np.empty(num_offsets)
11✔
2027

2028
                # Populate Multi-index arrays with all distribcell paths
2029
                for i, path in enumerate(paths):
11✔
2030

2031
                    level = path[i_level]
11✔
2032
                    if level[0] == 'lattice':
11✔
2033
                        # Assign entry to Lattice Multi-index column
2034
                        level_dict[lat_id_key][i] = level[1]
11✔
2035
                        level_dict[lat_x_key][i] = level[2][0]
11✔
2036
                        level_dict[lat_y_key][i] = level[2][1]
11✔
2037
                        if len(level[2]) == 3:
11✔
UNCOV
2038
                            level_dict[lat_z_key][i] = level[2][2]
×
2039

2040
                    else:
2041
                        # Assign entry to Universe, Cell Multi-index columns
2042
                        level_dict[univ_key][i] = level[1]
11✔
2043
                        level_dict[cell_key][i] = level[2]
11✔
2044

2045
                # Tile the Multi-index columns
2046
                for level_key, level_bins in level_dict.items():
11✔
2047
                    level_dict[level_key] = _repeat_and_tile(
11✔
2048
                        level_bins, stride, data_size)
2049

2050
                # Initialize a Pandas DataFrame from the level dictionary
2051
                if level_df is None:
11✔
2052
                    level_df = pd.DataFrame(level_dict)
11✔
2053
                else:
2054
                    level_df = pd.concat([level_df, pd.DataFrame(level_dict)],
11✔
2055
                                         axis=1)
2056

2057
        # Create DataFrame column for distribcell instance IDs
2058
        # NOTE: This is performed regardless of whether the user
2059
        # requests Summary geometric information
2060
        filter_bins = _repeat_and_tile(
11✔
2061
            np.arange(self.num_bins), stride, data_size)
2062
        df = pd.DataFrame({self.short_name.lower() : filter_bins})
11✔
2063

2064
        # Concatenate with DataFrame of distribcell instance IDs
2065
        if level_df is not None:
11✔
2066
            level_df = level_df.dropna(axis=1, how='all')
11✔
2067
            level_df = level_df.astype(int)
11✔
2068
            df = pd.concat([level_df, df], axis=1)
11✔
2069

2070
        return df
11✔
2071

2072

2073
class MuFilter(RealFilter):
11✔
2074
    """Bins tally events based on particle scattering angle.
2075

2076
    Parameters
2077
    ----------
2078
    values : int or Iterable of Real
2079
        A grid of scattering angles which events will binned into. Values
2080
        represent the cosine of the scattering angle. If an iterable is given,
2081
        the values will be used explicitly as grid points. If a single int is
2082
        given, the range [-1, 1] will be divided up equally into that number of
2083
        bins.
2084
    filter_id : int
2085
        Unique identifier for the filter
2086

2087
    Attributes
2088
    ----------
2089
    values : numpy.ndarray
2090
        An array of values for which each successive pair constitutes a range of
2091
        scattering angle cosines for a single bin
2092
    id : int
2093
        Unique identifier for the filter
2094
    bins : numpy.ndarray
2095
        An array of shape (N, 2) where each row is a pair of scattering angle
2096
        cosines for a single filter bin
2097
    num_bins : Integral
2098
        The number of filter bins
2099

2100
    """
2101
    def __init__(self, values, filter_id=None):
11✔
2102
        if isinstance(values, Integral):
11✔
2103
            values = np.linspace(-1., 1., values + 1)
11✔
2104
        super().__init__(values, filter_id)
11✔
2105

2106
    def check_bins(self, bins):
11✔
2107
        super().check_bins(bins)
11✔
2108
        for x in np.ravel(bins):
11✔
2109
            if not np.isclose(x, -1.):
11✔
2110
                cv.check_greater_than('filter value', x, -1., equality=True)
11✔
2111
            if not np.isclose(x, 1.):
11✔
2112
                cv.check_less_than('filter value', x, 1., equality=True)
11✔
2113

2114

2115
class MuSurfaceFilter(MuFilter):
11✔
2116
    """Bins tally events based on the angle of surface crossing.
2117

2118
    This filter bins events based on the cosine of the angle between the
2119
    direction of the particle and the normal to the surface at the point it
2120
    crosses. Only used in conjunction with a SurfaceFilter and current score.
2121

2122
    .. versionadded:: 0.15.1
2123

2124
    Parameters
2125
    ----------
2126
    values : int or Iterable of Real
2127
        A grid of surface crossing angles which the events will be divided into.
2128
        Values represent the cosine of the angle between the direction of the
2129
        particle and the normal to the surface at the point it crosses. If an
2130
        iterable is given, the values will be used explicitly as grid points. If
2131
        a single int is given, the range [-1, 1] will be divided equally into
2132
        that number of bins.
2133
    filter_id : int
2134
        Unique identifier for the filter
2135

2136
    Attributes
2137
    ----------
2138
    values : numpy.ndarray
2139
        An array of values for which each successive pair constitutes a range of
2140
        surface crossing angle cosines for a single bin.
2141
    id : int
2142
        Unique identifier for the filter
2143
    bins : numpy.ndarray
2144
        An array of shape (N, 2) where each row is a pair of cosines of surface
2145
        crossing angle for a single filter
2146
    num_bins : Integral
2147
        The number of filter bins
2148

2149
    """
2150
    # Note: inherits implementation from MuFilter
2151

2152

2153
class PolarFilter(RealFilter):
11✔
2154
    """Bins tally events based on the incident particle's direction.
2155

2156
    Parameters
2157
    ----------
2158
    values : int or Iterable of Real
2159
        A grid of polar angles which events will binned into. Values represent
2160
        an angle in radians relative to the z-axis. If an iterable is given, the
2161
        values will be used explicitly as grid points. If a single int is given,
2162
        the range [0, pi] will be divided up equally into that number of bins.
2163
    filter_id : int
2164
        Unique identifier for the filter
2165

2166
    Attributes
2167
    ----------
2168
    values : numpy.ndarray
2169
        An array of values for which each successive pair constitutes a range of
2170
        polar angles in [rad] for a single bin
2171
    id : int
2172
        Unique identifier for the filter
2173
    bins : numpy.ndarray
2174
        An array of shape (N, 2) where each row is a pair of polar angles for a
2175
        single filter bin
2176
    id : int
2177
        Unique identifier for the filter
2178
    num_bins : Integral
2179
        The number of filter bins
2180

2181
    """
2182
    units = 'rad'
11✔
2183

2184
    def __init__(self, values, filter_id=None):
11✔
2185
        if isinstance(values, Integral):
11✔
UNCOV
2186
            values = np.linspace(0., np.pi, values + 1)
×
2187
        super().__init__(values, filter_id)
11✔
2188

2189
    def check_bins(self, bins):
11✔
2190
        super().check_bins(bins)
11✔
2191
        for x in np.ravel(bins):
11✔
2192
            if not np.isclose(x, 0.):
11✔
2193
                cv.check_greater_than('filter value', x, 0., equality=True)
11✔
2194
            if not np.isclose(x, np.pi):
11✔
2195
                cv.check_less_than('filter value', x, np.pi, equality=True)
11✔
2196

2197

2198
class AzimuthalFilter(RealFilter):
11✔
2199
    """Bins tally events based on the incident particle's direction.
2200

2201
    Parameters
2202
    ----------
2203
    values : int or Iterable of Real
2204
        A grid of azimuthal angles which events will binned into. Values
2205
        represent an angle in radians relative to the x-axis and perpendicular
2206
        to the z-axis. If an iterable is given, the values will be used
2207
        explicitly as grid points. If a single int is given, the range
2208
        [-pi, pi) will be divided up equally into that number of bins.
2209
    filter_id : int
2210
        Unique identifier for the filter
2211

2212
    Attributes
2213
    ----------
2214
    values : numpy.ndarray
2215
        An array of values for which each successive pair constitutes a range of
2216
        azimuthal angles in [rad] for a single bin
2217
    id : int
2218
        Unique identifier for the filter
2219
    bins : numpy.ndarray
2220
        An array of shape (N, 2) where each row is a pair of azimuthal angles
2221
        for a single filter bin
2222
    num_bins : Integral
2223
        The number of filter bins
2224

2225
    """
2226
    units = 'rad'
11✔
2227

2228
    def __init__(self, values, filter_id=None):
11✔
2229
        if isinstance(values, Integral):
11✔
UNCOV
2230
            values = np.linspace(-np.pi, np.pi, values + 1)
×
2231
        super().__init__(values, filter_id)
11✔
2232

2233
    def check_bins(self, bins):
11✔
2234
        super().check_bins(bins)
11✔
2235
        for x in np.ravel(bins):
11✔
2236
            if not np.isclose(x, -np.pi):
11✔
2237
                cv.check_greater_than('filter value', x, -np.pi, equality=True)
11✔
2238
            if not np.isclose(x, np.pi):
11✔
2239
                cv.check_less_than('filter value', x, np.pi, equality=True)
11✔
2240

2241

2242
class DelayedGroupFilter(Filter):
11✔
2243
    """Bins fission events based on the produced neutron precursor groups.
2244

2245
    Parameters
2246
    ----------
2247
    bins : iterable of int
2248
        The delayed neutron precursor groups.  For example, ENDF/B-VII.1 uses
2249
        6 precursor groups so a tally with all groups will have bins =
2250
        [1, 2, 3, 4, 5, 6].
2251
    filter_id : int
2252
        Unique identifier for the filter
2253

2254
    Attributes
2255
    ----------
2256
    bins : iterable of int
2257
        The delayed neutron precursor groups.  For example, ENDF/B-VII.1 uses
2258
        6 precursor groups so a tally with all groups will have bins =
2259
        [1, 2, 3, 4, 5, 6].
2260
    id : int
2261
        Unique identifier for the filter
2262
    num_bins : Integral
2263
        The number of filter bins
2264

2265
    """
2266
    def check_bins(self, bins):
11✔
2267
        # Check the bin values.
2268
        for g in bins:
11✔
2269
            cv.check_greater_than('delayed group', g, 0)
11✔
2270

2271

2272
class EnergyFunctionFilter(Filter):
11✔
2273
    """Multiplies tally scores by an arbitrary function of incident energy.
2274

2275
    The arbitrary function is described by a piecewise linear-linear
2276
    interpolation of energy and y values.  Values outside of the given energy
2277
    range will be evaluated as zero.
2278

2279
    Parameters
2280
    ----------
2281
    energy : Iterable of Real
2282
        A grid of energy values in [eV]
2283
    y : iterable of Real
2284
        A grid of interpolant values in [eV]
2285
    interpolation : str
2286
        Interpolation scheme: {'histogram', 'linear-linear', 'linear-log',
2287
        'log-linear', 'log-log', 'quadratic', 'cubic'}
2288
    filter_id : int
2289
        Unique identifier for the filter
2290

2291
    Attributes
2292
    ----------
2293
    energy : Iterable of Real
2294
        A grid of energy values in [eV]
2295
    y : iterable of Real
2296
        A grid of interpolant values in [eV]
2297
    interpolation : str
2298
        Interpolation scheme: {'histogram', 'linear-linear', 'linear-log',
2299
        'log-linear', 'log-log', 'quadratic', 'cubic'}
2300
    id : int
2301
        Unique identifier for the filter
2302
    num_bins : Integral
2303
        The number of filter bins (always 1 for this filter)
2304

2305
    """
2306

2307
    # keys selected to match those in function.py where possible
2308
    # skip 6 b/c ENDF-6 reserves this value for
2309
    # "special one-dimensional interpolation law"
2310
    INTERPOLATION_SCHEMES = {1: 'histogram', 2: 'linear-linear',
11✔
2311
                             3: 'linear-log', 4: 'log-linear',
2312
                             5: 'log-log', 7: 'quadratic',
2313
                             8: 'cubic'}
2314

2315
    def __init__(self, energy, y, interpolation='linear-linear', filter_id=None):
11✔
2316
        self.energy = energy
11✔
2317
        self.y = y
11✔
2318
        self.id = filter_id
11✔
2319
        self.interpolation = interpolation
11✔
2320

2321
    def __eq__(self, other):
11✔
2322
        if type(self) is not type(other):
11✔
UNCOV
2323
            return False
×
2324
        elif not self.interpolation == other.interpolation:
11✔
UNCOV
2325
            return False
×
2326
        elif not all(self.energy == other.energy):
11✔
UNCOV
2327
            return False
×
2328
        else:
2329
            return all(self.y == other.y)
11✔
2330

2331
    def __gt__(self, other):
11✔
UNCOV
2332
        if type(self) is not type(other):
×
UNCOV
2333
            if self.short_name in _FILTER_TYPES and \
×
2334
                other.short_name in _FILTER_TYPES:
UNCOV
2335
                delta = _FILTER_TYPES.index(self.short_name) - \
×
2336
                        _FILTER_TYPES.index(other.short_name)
UNCOV
2337
                return delta > 0
×
2338
            else:
UNCOV
2339
                return False
×
2340
        else:
UNCOV
2341
            return False
×
2342

2343
    def __lt__(self, other):
11✔
UNCOV
2344
        if type(self) is not type(other):
×
UNCOV
2345
            if self.short_name in _FILTER_TYPES and \
×
2346
                other.short_name in _FILTER_TYPES:
UNCOV
2347
                delta = _FILTER_TYPES.index(self.short_name) - \
×
2348
                        _FILTER_TYPES.index(other.short_name)
UNCOV
2349
                return delta < 0
×
2350
            else:
UNCOV
2351
                return False
×
2352
        else:
UNCOV
2353
            return False
×
2354

2355
    def __hash__(self):
11✔
2356
        string = type(self).__name__ + '\n'
11✔
2357
        string += '{: <16}=\t{}\n'.format('\tEnergy', self.energy)
11✔
2358
        string += '{: <16}=\t{}\n'.format('\tInterpolant', self.y)
11✔
2359
        string += '{: <16}=\t{}\n'.format('\tInterpolation', self.interpolation)
11✔
2360
        return hash(string)
11✔
2361

2362
    def __repr__(self):
11✔
2363
        string = type(self).__name__ + '\n'
11✔
2364
        string += '{: <16}=\t{}\n'.format('\tEnergy', self.energy)
11✔
2365
        string += '{: <16}=\t{}\n'.format('\tInterpolant', self.y)
11✔
2366
        string += '{: <16}=\t{}\n'.format('\tInterpolation', self.interpolation)
11✔
2367
        string += '{: <16}=\t{}\n'.format('\tID', self.id)
11✔
2368
        return string
11✔
2369

2370
    @classmethod
11✔
2371
    def from_hdf5(cls, group, **kwargs):
11✔
2372
        if group['type'][()].decode() != cls.short_name.lower():
11✔
UNCOV
2373
            raise ValueError("Expected HDF5 data for filter type '"
×
2374
                             + cls.short_name.lower() + "' but got '"
2375
                             + group['type'][()].decode() + " instead")
2376

2377
        energy = group['energy'][()]
11✔
2378
        y_grp = group['y']
11✔
2379
        y = y_grp[()]
11✔
2380
        filter_id = int(group.name.split('/')[-1].lstrip('filter '))
11✔
2381

2382
        out = cls(energy, y, filter_id=filter_id)
11✔
2383
        if 'interpolation' in y_grp.attrs:
11✔
2384
            out.interpolation =  \
11✔
2385
                cls.INTERPOLATION_SCHEMES[y_grp.attrs['interpolation'][()]]
2386

2387
        return out
11✔
2388

2389
    @classmethod
11✔
2390
    def from_tabulated1d(cls, tab1d):
11✔
2391
        """Construct a filter from a Tabulated1D object.
2392

2393
        Parameters
2394
        ----------
2395
        tab1d : openmc.data.Tabulated1D
2396
            A linear-linear Tabulated1D object with only a single interpolation
2397
            region.
2398

2399
        Returns
2400
        -------
2401
        EnergyFunctionFilter
2402

2403
        """
2404
        cv.check_type('EnergyFunctionFilter tab1d', tab1d,
11✔
2405
                      openmc.data.Tabulated1D)
2406
        if tab1d.n_regions > 1:
11✔
UNCOV
2407
            raise ValueError('Only Tabulated1Ds with a single interpolation '
×
2408
                             'region are supported')
2409
        interpolation_val = tab1d.interpolation[0]
11✔
2410
        if interpolation_val not in cls.INTERPOLATION_SCHEMES.keys():
11✔
UNCOV
2411
            raise ValueError('Only histogram, linear-linear, linear-log, log-linear, and '
×
2412
                             'log-log Tabulated1Ds are supported')
2413
        return cls(tab1d.x, tab1d.y, cls.INTERPOLATION_SCHEMES[interpolation_val])
11✔
2414

2415
    @property
11✔
2416
    def energy(self):
11✔
2417
        return self._energy
11✔
2418

2419
    @energy.setter
11✔
2420
    def energy(self, energy):
11✔
2421
        # Format the bins as a 1D numpy array.
2422
        energy = np.atleast_1d(energy)
11✔
2423

2424
        # Make sure the values are Real and positive.
2425
        cv.check_type('filter energy grid', energy, Iterable, Real)
11✔
2426
        for E in energy:
11✔
2427
            cv.check_greater_than('filter energy grid', E, 0, equality=True)
11✔
2428

2429
        self._energy = energy
11✔
2430

2431
    @property
11✔
2432
    def y(self):
11✔
2433
        return self._y
11✔
2434

2435
    @y.setter
11✔
2436
    def y(self, y):
11✔
2437
        # Format the bins as a 1D numpy array.
2438
        y = np.atleast_1d(y)
11✔
2439

2440
        # Make sure the values are Real.
2441
        cv.check_type('filter interpolant values', y, Iterable, Real)
11✔
2442

2443
        self._y = y
11✔
2444

2445
    @property
11✔
2446
    def interpolation(self):
11✔
2447
        return self._interpolation
11✔
2448

2449
    @interpolation.setter
11✔
2450
    def interpolation(self, val):
11✔
2451
        cv.check_type('interpolation', val, str)
11✔
2452
        cv.check_value('interpolation', val, self.INTERPOLATION_SCHEMES.values())
11✔
2453

2454
        if val == 'quadratic' and len(self.energy) < 3:
11✔
UNCOV
2455
            raise ValueError('Quadratic interpolation requires 3 or more values.')
×
2456

2457
        if val == 'cubic' and len(self.energy) < 4:
11✔
UNCOV
2458
            raise ValueError('Cubic interpolation requires 3 or more values.')
×
2459

2460
        self._interpolation = val
11✔
2461

2462
    @property
11✔
2463
    def bins(self):
11✔
UNCOV
2464
        raise AttributeError('EnergyFunctionFilters have no bins.')
×
2465

2466
    @bins.setter
11✔
2467
    def bins(self, bins):
11✔
UNCOV
2468
        raise RuntimeError('EnergyFunctionFilters have no bins.')
×
2469

2470
    @property
11✔
2471
    def num_bins(self):
11✔
2472
        return 1
11✔
2473

2474
    def to_xml_element(self):
11✔
2475
        """Return XML Element representing the Filter.
2476

2477
        Returns
2478
        -------
2479
        element : lxml.etree._Element
2480
            XML element containing filter data
2481

2482
        """
2483
        element = ET.Element('filter')
11✔
2484
        element.set('id', str(self.id))
11✔
2485
        element.set('type', self.short_name.lower())
11✔
2486

2487
        subelement = ET.SubElement(element, 'energy')
11✔
2488
        subelement.text = ' '.join(str(e) for e in self.energy)
11✔
2489

2490
        subelement = ET.SubElement(element, 'y')
11✔
2491
        subelement.text = ' '.join(str(y) for y in self.y)
11✔
2492

2493
        subelement = ET.SubElement(element, 'interpolation')
11✔
2494
        subelement.text = self.interpolation
11✔
2495

2496
        return element
11✔
2497

2498
    @classmethod
11✔
2499
    def from_xml_element(cls, elem, **kwargs):
11✔
2500
        filter_id = int(get_text(elem, "id"))
11✔
2501
        energy = get_elem_list(elem, "energy", float) or []
11✔
2502
        y = get_elem_list(elem, "y", float) or []
11✔
2503
        out = cls(energy, y, filter_id=filter_id)
11✔
2504
        interpolation = get_text(elem, "interpolation")
11✔
2505
        if interpolation is not None:
11✔
2506
            out.interpolation = interpolation
11✔
2507
        return out
11✔
2508

2509
    def can_merge(self, other):
11✔
UNCOV
2510
        return False
×
2511

2512
    def is_subset(self, other):
11✔
UNCOV
2513
        return self == other
×
2514

2515
    def get_bin_index(self, filter_bin):
11✔
2516
        # This filter only has one bin.  Always return 0.
UNCOV
2517
        return 0
×
2518

2519
    def get_pandas_dataframe(self, data_size, stride, **kwargs):
11✔
2520
        """Builds a Pandas DataFrame for the Filter's bins.
2521

2522
        This method constructs a Pandas DataFrame object for the filter with
2523
        columns annotated by filter bin information. This is a helper method for
2524
        :meth:`Tally.get_pandas_dataframe`.
2525

2526
        Parameters
2527
        ----------
2528
        data_size : int
2529
            The total number of bins in the tally corresponding to this filter
2530
        stride : int
2531
            Stride in memory for the filter
2532

2533
        Returns
2534
        -------
2535
        pandas.DataFrame
2536
            A Pandas DataFrame with a column that is filled with a hash of this
2537
            filter. EnergyFunctionFilters have only 1 bin so the purpose of this
2538
            DataFrame column is to differentiate the filter from other
2539
            EnergyFunctionFilters. The number of rows in the DataFrame is the
2540
            same as the total number of bins in the corresponding tally.
2541

2542
        See also
2543
        --------
2544
        Tally.get_pandas_dataframe(), CrossFilter.get_pandas_dataframe()
2545

2546
        """
2547
        df = pd.DataFrame()
11✔
2548

2549
        # There is no clean way of sticking all the energy, y data into a
2550
        # DataFrame so instead we'll just make a column with the filter name
2551
        # and fill it with a hash of the __repr__.  We want a hash that is
2552
        # reproducible after restarting the interpreter so we'll use hashlib.md5
2553
        # rather than the intrinsic hash().
2554
        hash_fun = hashlib.md5()
11✔
2555
        hash_fun.update(repr(self).encode('utf-8'))
11✔
2556
        out = hash_fun.hexdigest()
11✔
2557

2558
        # The full 16 bytes make for a really wide column.  Just 7 bytes (14
2559
        # hex characters) of the digest are probably sufficient.
2560
        out = out[:14]
11✔
2561

2562
        filter_bins = _repeat_and_tile(out, stride, data_size)
11✔
2563
        df = pd.concat([df, pd.DataFrame(
11✔
2564
            {self.short_name.lower(): filter_bins})])
2565

2566
        return df
11✔
2567

2568

2569
class WeightFilter(RealFilter):
11✔
2570
    """Bins tally events based on the incoming particle weight.
2571

2572
    Parameters
2573
    ----------
2574
    Values : Iterable of float
2575
        A list or iterable of the weight boundaries, as float values.
2576
    filter_id : int
2577
        Unique identifier for the filter
2578

2579
    Attributes
2580
    ----------
2581
    id : int
2582
        Unique identifier for the filter
2583
    bins : numpy.ndarray
2584
        An array of integer values representing the weights by which to filter
2585
    num_bins : int
2586
        The number of filter bins
2587
    values : numpy.ndarray
2588
        Array of weight boundaries
2589
    """
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