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

openmc-dev / openmc / 23503641174

24 Mar 2026 05:36PM UTC coverage: 81.328% (-0.1%) from 81.447%
23503641174

Pull #3886

github

web-flow
Merge 63fd9c86f into 6cd39073b
Pull Request #3886: Implement python tally types

17611 of 25453 branches covered (69.19%)

Branch coverage included in aggregate %.

244 of 297 new or added lines in 12 files covered. (82.15%)

238 existing lines in 4 files now uncovered.

58250 of 67825 relevant lines covered (85.88%)

44553638.72 hits per line

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

79.0
/openmc/arithmetic.py
1
from collections.abc import Iterable
11✔
2
import copy
11✔
3
from typing import Generic, TypeVar
11✔
4

5
import numpy as np
11✔
6
import pandas as pd
11✔
7

8
import openmc
11✔
9
import openmc.checkvalue as cv
11✔
10
from .filter import _FILTER_TYPES
11✔
11

12

13
# Acceptable tally arithmetic binary operations
14
_TALLY_ARITHMETIC_OPS = {'+', '-', '*', '/', '^'}
11✔
15

16
# Acceptable tally aggregation operations
17
_TALLY_AGGREGATE_OPS = {'sum', 'avg'}
11✔
18

19
T = TypeVar("T")
11✔
20

21
class CrossScore(Generic[T]):
11✔
22
    """A special-purpose tally score used to encapsulate all combinations of two
23
    tally's scores as an outer product for tally arithmetic.
24

25
    Parameters
26
    ----------
27
    left_score : str or CrossScore
28
        The left score in the outer product
29
    right_score : str or CrossScore
30
        The right score in the outer product
31
    binary_op : str
32
        The tally arithmetic binary operator (e.g., '+', '-', etc.) used to
33
        combine two tally's scores with this CrossNuclide
34

35
    Attributes
36
    ----------
37
    left_score : str or CrossScore
38
        The left score in the outer product
39
    right_score : str or CrossScore
40
        The right score in the outer product
41
    binary_op : str
42
        The tally arithmetic binary operator (e.g., '+', '-', etc.) used to
43
        combine two tally's scores with this CrossScore
44

45
    """
46

47
    def __init__(self, left_score, right_score, binary_op):
11✔
48
        self.left_score = left_score
11✔
49
        self.right_score = right_score
11✔
50
        self.binary_op = binary_op
11✔
51

52
    def __hash__(self):
11✔
53
        return hash(repr(self))
11✔
54

55
    def __eq__(self, other):
11✔
56
        return str(other) == str(self)
11✔
57

58
    def __repr__(self):
11✔
59
        return f'({self.left_score} {self.binary_op} {self.right_score})'
11✔
60
        
61
    def __iter__(self):
11✔
62
        yield self.left_score
11✔
63
        yield self.right_score
11✔
64

65
    @property
11✔
66
    def left_score(self):
11✔
67
        return self._left_score
11✔
68

69
    @left_score.setter
11✔
70
    def left_score(self, left_score):
11✔
71
        cv.check_type('left_score', left_score,
11✔
72
                      (str, CrossScore, AggregateScore))
73
        self._left_score = left_score
11✔
74

75
    @property
11✔
76
    def right_score(self):
11✔
77
        return self._right_score
11✔
78

79
    @right_score.setter
11✔
80
    def right_score(self, right_score):
11✔
81
        cv.check_type('right_score', right_score,
11✔
82
                      (str, CrossScore, AggregateScore))
83
        self._right_score = right_score
11✔
84

85
    @property
11✔
86
    def binary_op(self):
11✔
87
        return self._binary_op
11✔
88

89
    @binary_op.setter
11✔
90
    def binary_op(self, binary_op):
11✔
91
        cv.check_type('binary_op', binary_op, str)
11✔
92
        cv.check_value('binary_op', binary_op, _TALLY_ARITHMETIC_OPS)
11✔
93
        self._binary_op = binary_op
11✔
94

95

96
class CrossNuclide(Generic[T]):
11✔
97
    """A special-purpose nuclide used to encapsulate all combinations of two
98
    tally's nuclides as an outer product for tally arithmetic.
99

100
    Parameters
101
    ----------
102
    left_nuclide : str or CrossNuclide
103
        The left nuclide in the outer product
104
    right_nuclide : str or CrossNuclide
105
        The right nuclide in the outer product
106
    binary_op : str
107
        The tally arithmetic binary operator (e.g., '+', '-', etc.) used to
108
        combine two tally's nuclides with this CrossNuclide
109

110
    Attributes
111
    ----------
112
    left_nuclide : str or CrossNuclide
113
        The left nuclide in the outer product
114
    right_nuclide : str or CrossNuclide
115
        The right nuclide in the outer product
116
    binary_op : str
117
        The tally arithmetic binary operator (e.g., '+', '-', etc.) used to
118
        combine two tally's nuclides with this CrossNuclide
119

120
    """
121

122
    def __init__(self, left_nuclide, right_nuclide, binary_op):
11✔
123
        self.left_nuclide = left_nuclide
11✔
124
        self.right_nuclide = right_nuclide
11✔
125
        self.binary_op = binary_op
11✔
126

127
    def __hash__(self):
11✔
128
        return hash(repr(self))
11✔
129

130
    def __eq__(self, other):
11✔
131
        return str(other) == str(self)
11✔
132

133
    def __repr__(self):
11✔
134
        return self.name
11✔
135
        
136
    def __iter__(self):
11✔
137
        yield self.left_nuclide
11✔
138
        yield self.right_nuclide
11✔
139

140
    @property
11✔
141
    def left_nuclide(self):
11✔
142
        return self._left_nuclide
11✔
143

144
    @left_nuclide.setter
11✔
145
    def left_nuclide(self, left_nuclide):
11✔
146
        cv.check_type('left_nuclide', left_nuclide,
11✔
147
                      (str, CrossNuclide, AggregateNuclide))
148
        self._left_nuclide = left_nuclide
11✔
149

150
    @property
11✔
151
    def right_nuclide(self):
11✔
152
        return self._right_nuclide
11✔
153

154
    @right_nuclide.setter
11✔
155
    def right_nuclide(self, right_nuclide):
11✔
156
        cv.check_type('right_nuclide', right_nuclide,
11✔
157
                      (str, CrossNuclide, AggregateNuclide))
158
        self._right_nuclide = right_nuclide
11✔
159

160
    @property
11✔
161
    def binary_op(self):
11✔
162
        return self._binary_op
11✔
163

164
    @binary_op.setter
11✔
165
    def binary_op(self, binary_op):
11✔
166
        cv.check_type('binary_op', binary_op, str)
11✔
167
        cv.check_value('binary_op', binary_op, _TALLY_ARITHMETIC_OPS)
11✔
168
        self._binary_op = binary_op
11✔
169

170
    @property
11✔
171
    def name(self):
11✔
172
        return f'({self.left_nuclide} {self.binary_op} {self.right_nuclide})'
11✔
173

174

175
class CrossFilter(Generic[T]):
11✔
176
    """A special-purpose filter used to encapsulate all combinations of two
177
    tally's filter bins as an outer product for tally arithmetic.
178

179
    Parameters
180
    ----------
181
    left_filter : openmc.Filter or CrossFilter
182
        The left filter in the outer product
183
    right_filter : openmc.Filter or CrossFilter
184
        The right filter in the outer product
185
    binary_op : str
186
        The tally arithmetic binary operator (e.g., '+', '-', etc.) used to
187
        combine two tally's filter bins with this CrossFilter
188

189
    Attributes
190
    ----------
191
    type : str
192
        The type of the crossfilter (e.g., 'energy / energy')
193
    left_filter : openmc.Filter or CrossFilter
194
        The left filter in the outer product
195
    right_filter : openmc.Filter or CrossFilter
196
        The right filter in the outer product
197
    binary_op : str
198
        The tally arithmetic binary operator (e.g., '+', '-', etc.) used to
199
        combine two tally's filter bins with this CrossFilter
200
    bins : dict of Iterable
201
        A dictionary of the bins from each filter keyed by the types of the
202
        left / right filters
203
    num_bins : Integral
204
        The number of filter bins (always 1 if aggregate_filter is defined)
205

206
    """
207

208
    def __init__(self, left_filter, right_filter, binary_op):
11✔
209
        self.left_filter = left_filter
×
210
        self.right_filter = right_filter
×
211
        self.binary_op = binary_op
×
212

213
    def __hash__(self):
11✔
214
        return hash((self.left_filter, self.right_filter))
×
215

216
    def __eq__(self, other):
11✔
217
        return str(other) == str(self)
×
218

219
    def __repr__(self):
11✔
220
        filter_bins = '({} {} {})'.format(self.left_filter.bins,
×
221
                                          self.binary_op,
222
                                          self.right_filter.bins)
223
        parts = [
×
224
            'CrossFilter',
225
            '{: <16}=\t{}'.format('\tType', self.type),
226
            '{: <16}=\t{}'.format('\tBins', filter_bins)
227
        ]
228
        return '\n'.join(parts)
×
229
        
230
    def __iter__(self):
11✔
NEW
231
        yield self.left_filter
×
NEW
232
        yield self.right_filter
×
233

234
    @property
11✔
235
    def left_filter(self):
11✔
236
        return self._left_filter
×
237

238
    @left_filter.setter
11✔
239
    def left_filter(self, left_filter):
11✔
240
        cv.check_type('left_filter', left_filter,
×
241
                      (openmc.Filter, CrossFilter, AggregateFilter))
242
        self._left_filter = left_filter
×
243

244
    @property
11✔
245
    def right_filter(self):
11✔
246
        return self._right_filter
×
247

248
    @right_filter.setter
11✔
249
    def right_filter(self, right_filter):
11✔
250
        cv.check_type('right_filter', right_filter,
×
251
                      (openmc.Filter, CrossFilter, AggregateFilter))
252
        self._right_filter = right_filter
×
253

254
    @property
11✔
255
    def binary_op(self):
11✔
256
        return self._binary_op
×
257

258
    @binary_op.setter
11✔
259
    def binary_op(self, binary_op):
11✔
260
        cv.check_type('binary_op', binary_op, str)
×
261
        cv.check_value('binary_op', binary_op, _TALLY_ARITHMETIC_OPS)
×
262
        self._binary_op = binary_op
×
263

264
    @property
11✔
265
    def type(self):
11✔
266
        left_type = self.left_filter.type
×
267
        right_type = self.right_filter.type
×
268
        return f'({left_type} {self.binary_op} {right_type})'
×
269

270
    @property
11✔
271
    def bins(self):
11✔
272
        return self._left_filter.bins, self._right_filter.bins
×
273

274
    @property
11✔
275
    def num_bins(self):
11✔
276
        if self.left_filter is not None and self.right_filter is not None:
×
277
            return self.left_filter.num_bins * self.right_filter.num_bins
×
278
        else:
279
            return 0
×
280

281
    def get_bin_index(self, filter_bin):
11✔
282
        """Returns the index in the CrossFilter for some bin.
283

284
        Parameters
285
        ----------
286
        filter_bin : 2-tuple
287
            A 2-tuple where each value corresponds to the bin of interest
288
            in the left and right filter, respectively. A bin is the integer
289
            ID for 'material', 'surface', 'cell', 'cellborn', and 'universe'
290
            Filters. The bin is an integer for the cell instance ID for
291
            'distribcell' Filters. The bin is a 2-tuple of floats for 'energy'
292
            and 'energyout' filters corresponding to the energy boundaries of
293
            the bin of interest.  The bin is a (x,y,z) 3-tuple for 'mesh'
294
            filters corresponding to the mesh cell of interest.
295

296
        Returns
297
        -------
298
        filter_index : Integral
299
             The index in the Tally data array for this filter bin.
300

301
        """
302

303
        left_index = self.left_filter.get_bin_index(filter_bin[0])
×
304
        right_index = self.right_filter.get_bin_index(filter_bin[0])
×
305
        filter_index = left_index * self.right_filter.num_bins + right_index
×
306
        return filter_index
×
307

308
    def get_pandas_dataframe(self, data_size, summary=None):
11✔
309
        """Builds a Pandas DataFrame for the CrossFilter's bins.
310

311
        This method constructs a Pandas DataFrame object for the CrossFilter
312
        with columns annotated by filter bin information. This is a helper
313
        method for the TallyBase.get_pandas_dataframe(...) method. This method
314
        recursively builds and concatenates Pandas DataFrames for the left
315
        and right filters and crossfilters.
316

317
        This capability has been tested for Pandas >=0.13.1. However, it is
318
        recommended to use v0.16 or newer versions of Pandas since this method
319
        uses Pandas' Multi-index functionality.
320

321
        Parameters
322
        ----------
323
        data_size : Integral
324
            The total number of bins in the tally corresponding to this filter
325
        summary : None or Summary
326
            An optional Summary object to be used to construct columns for
327
            distribcell tally filters (default is None). The geometric
328
            information in the Summary object is embedded into a Multi-index
329
            column with a geometric "path" to each distribcell instance.
330

331
        Returns
332
        -------
333
        pandas.DataFrame
334
            A Pandas DataFrame with columns of strings that characterize the
335
            crossfilter's bins. Each entry in the DataFrame will include one
336
            or more binary operations used to construct the crossfilter's bins.
337
            The number of rows in the DataFrame is the same as the total number
338
            of bins in the corresponding tally, with the filter bins
339
            appropriately tiled to map to the corresponding tally bins.
340

341
        See also
342
        --------
343
        TallyBase.get_pandas_dataframe(), Filter.get_pandas_dataframe()
344

345
        """
346

347
        # If left and right filters are identical, do not combine bins
348
        if self.left_filter == self.right_filter:
×
349
            df = self.left_filter.get_pandas_dataframe(data_size, summary)
×
350

351
        # If left and right filters are different, combine their bins
352
        else:
353
            left_df = self.left_filter.get_pandas_dataframe(data_size, summary)
×
354
            right_df = self.right_filter.get_pandas_dataframe(data_size, summary)
×
355
            left_df = left_df.astype(str)
×
356
            right_df = right_df.astype(str)
×
357
            df = f'({left_df} {self.binary_op} {right_df})'
×
358

359
        return df
×
360

361

362
class AggregateScore(Generic[T]):
11✔
363
    """A special-purpose tally score used to encapsulate an aggregate of a
364
    subset or all of tally's scores for tally aggregation.
365

366
    Parameters
367
    ----------
368
    scores : Iterable of str or CrossScore
369
        The scores included in the aggregation
370
    aggregate_op : str
371
        The tally aggregation operator (e.g., 'sum', 'avg', etc.) used
372
        to aggregate across a tally's scores with this AggregateScore
373

374
    Attributes
375
    ----------
376
    scores : Iterable of str or CrossScore
377
        The scores included in the aggregation
378
    aggregate_op : str
379
        The tally aggregation operator (e.g., 'sum', 'avg', etc.) used
380
        to aggregate across a tally's scores with this AggregateScore
381

382
    """
383

384
    def __init__(self, scores=None, aggregate_op=None):
11✔
385

386
        self._scores = None
11✔
387
        self._aggregate_op = None
11✔
388

389
        if scores is not None:
11✔
390
            self.scores = scores
11✔
391
        if aggregate_op is not None:
11✔
392
            self.aggregate_op = aggregate_op
11✔
393

394
    def __hash__(self):
11✔
395
        return hash(repr(self))
×
396

397
    def __eq__(self, other):
11✔
398
        return str(other) == str(self)
11✔
399

400
    def __repr__(self):
11✔
401
        string = ', '.join(map(str, self.scores))
11✔
402
        string = f'{self.aggregate_op}({string})'
11✔
403
        return string
11✔
404
        
405
    def __iter__(self):
11✔
406
        yield from self._scores       
11✔
407

408
    @property
11✔
409
    def scores(self):
11✔
410
        return self._scores
11✔
411

412
    @scores.setter
11✔
413
    def scores(self, scores):
11✔
414
        cv.check_iterable_type('scores', scores, str)
11✔
415
        self._scores = scores
11✔
416

417
    @property
11✔
418
    def aggregate_op(self):
11✔
419
        return self._aggregate_op
11✔
420

421
    @aggregate_op.setter
11✔
422
    def aggregate_op(self, aggregate_op):
11✔
423
        cv.check_type('aggregate_op', aggregate_op, (str, CrossScore))
11✔
424
        cv.check_value('aggregate_op', aggregate_op, _TALLY_AGGREGATE_OPS)
11✔
425
        self._aggregate_op = aggregate_op
11✔
426

427
    @property
11✔
428
    def name(self):
11✔
429

430
        # Append each score in the aggregate to the string
431
        string = '(' + ', '.join(self.scores) + ')'
×
432
        return string
×
433

434

435
class AggregateNuclide(Generic[T]):
11✔
436
    """A special-purpose tally nuclide used to encapsulate an aggregate of a
437
    subset or all of tally's nuclides for tally aggregation.
438

439
    Parameters
440
    ----------
441
    nuclides : Iterable of str or CrossNuclide
442
        The nuclides included in the aggregation
443
    aggregate_op : str
444
        The tally aggregation operator (e.g., 'sum', 'avg', etc.) used
445
        to aggregate across a tally's nuclides with this AggregateNuclide
446

447
    Attributes
448
    ----------
449
    nuclides : Iterable of str or CrossNuclide
450
        The nuclides included in the aggregation
451
    aggregate_op : str
452
        The tally aggregation operator (e.g., 'sum', 'avg', etc.) used
453
        to aggregate across a tally's nuclides with this AggregateNuclide
454

455
    """
456

457
    def __init__(self, nuclides=None, aggregate_op=None):
11✔
458

459
        self._nuclides = None
11✔
460
        self._aggregate_op = None
11✔
461

462
        if nuclides is not None:
11✔
463
            self.nuclides = nuclides
11✔
464
        if aggregate_op is not None:
11✔
465
            self.aggregate_op = aggregate_op
11✔
466

467
    def __hash__(self):
11✔
468
        return hash(repr(self))
×
469

470
    def __eq__(self, other):
11✔
471
        return str(other) == str(self)
×
472

473
    def __repr__(self):
11✔
474
        return f'{self.aggregate_op}{self.name}'
×
475
        
476
    def __iter__(self):
11✔
477
        yield from self._nuclides
11✔
478

479
    @property
11✔
480
    def nuclides(self):
11✔
481
        return self._nuclides
×
482

483
    @nuclides.setter
11✔
484
    def nuclides(self, nuclides):
11✔
485
        cv.check_iterable_type('nuclides', nuclides, (str, CrossNuclide))
11✔
486
        self._nuclides = nuclides
11✔
487

488
    @property
11✔
489
    def aggregate_op(self):
11✔
490
        return self._aggregate_op
×
491

492
    @aggregate_op.setter
11✔
493
    def aggregate_op(self, aggregate_op):
11✔
494
        cv.check_type('aggregate_op', aggregate_op, str)
11✔
495
        cv.check_value('aggregate_op', aggregate_op, _TALLY_AGGREGATE_OPS)
11✔
496
        self._aggregate_op = aggregate_op
11✔
497

498
    @property
11✔
499
    def name(self):
11✔
500
        # Append each nuclide in the aggregate to the string
501
        names = [str(nuclide) for nuclide in self.nuclides]
×
502
        return '(' + ', '.join(map(str, names)) + ')'
×
503

504

505
class AggregateFilter(Generic[T]):
11✔
506
    """A special-purpose tally filter used to encapsulate an aggregate of a
507
    subset or all of a tally filter's bins for tally aggregation.
508

509
    Parameters
510
    ----------
511
    aggregate_filter : openmc.Filter or CrossFilter
512
        The filter included in the aggregation
513
    bins : Iterable of tuple
514
        The filter bins included in the aggregation
515
    aggregate_op : str
516
        The tally aggregation operator (e.g., 'sum', 'avg', etc.) used
517
        to aggregate across a tally filter's bins with this AggregateFilter
518

519
    Attributes
520
    ----------
521
    type : str
522
        The type of the aggregatefilter (e.g., 'sum(energy)', 'sum(cell)')
523
    aggregate_filter : openmc.Filter
524
        The filter included in the aggregation
525
    aggregate_op : str
526
        The tally aggregation operator (e.g., 'sum', 'avg', etc.) used
527
        to aggregate across a tally filter's bins with this AggregateFilter
528
    bins : Iterable of tuple
529
        The filter bins included in the aggregation
530
    num_bins : Integral
531
        The number of filter bins (always 1 if aggregate_filter is defined)
532

533
    """
534

535
    def __init__(self, aggregate_filter, bins=None, aggregate_op=None):
11✔
536

537
        self._type = f'{aggregate_op}({aggregate_filter.short_name.lower()})'
11✔
538
        self._bins = None
11✔
539

540
        self._aggregate_filter = None
11✔
541
        self._aggregate_op = None
11✔
542

543
        self.aggregate_filter = aggregate_filter
11✔
544
        if bins is not None:
11✔
545
            self.bins = bins
11✔
546
        if aggregate_op is not None:
11✔
547
            self.aggregate_op = aggregate_op
11✔
548

549
    def __hash__(self):
11✔
550
        return hash(repr(self))
11✔
551

552
    def __eq__(self, other):
11✔
553
        return str(other) == str(self)
11✔
554

555
    def __gt__(self, other):
11✔
556
        if self.type != other.type:
11✔
557
            if self.aggregate_filter.type in _FILTER_TYPES and \
×
558
              other.aggregate_filter.type in _FILTER_TYPES:
559
                delta = _FILTER_TYPES.index(self.aggregate_filter.type) - \
×
560
                        _FILTER_TYPES.index(other.aggregate_filter.type)
561
                return delta > 0
×
562
            else:
563
                return False
×
564
        else:
565
            return False
11✔
566

567
    def __lt__(self, other):
11✔
568
        return not self > other
11✔
569

570
    def __repr__(self):
11✔
571
        parts = [
11✔
572
            'AggregateFilter',
573
            '{: <16}=\t{}'.format('\tType', self.type),
574
            '{: <16}=\t{}'.format('\tBins', self.bins)
575
        ]
576
        return '\n'.join(parts)
11✔
577
        
578
    def __iter__(self):
11✔
579
        yield self._aggregate_filter     
11✔
580

581
    @property
11✔
582
    def aggregate_filter(self):
11✔
583
        return self._aggregate_filter
11✔
584

585
    @aggregate_filter.setter
11✔
586
    def aggregate_filter(self, aggregate_filter):
11✔
587
        cv.check_type('aggregate_filter', aggregate_filter,
11✔
588
                      (openmc.Filter, CrossFilter))
589
        self._aggregate_filter = aggregate_filter
11✔
590

591
    @property
11✔
592
    def aggregate_op(self):
11✔
593
        return self._aggregate_op
×
594

595
    @aggregate_op.setter
11✔
596
    def aggregate_op(self, aggregate_op):
11✔
597
        cv.check_type('aggregate_op', aggregate_op, str)
11✔
598
        cv.check_value('aggregate_op', aggregate_op, _TALLY_AGGREGATE_OPS)
11✔
599
        self._aggregate_op = aggregate_op
11✔
600

601
    @property
11✔
602
    def type(self):
11✔
603
        return self._type
11✔
604

605
    @type.setter
11✔
606
    def type(self, filter_type):
11✔
607
        if filter_type not in _FILTER_TYPES:
×
608
            msg = f'Unable to set AggregateFilter type to "{filter_type}" ' \
×
609
                  'since it is not one of the supported types'
610
            raise ValueError(msg)
×
611

612
        self._type = filter_type
×
613

614
    @property
11✔
615
    def bins(self):
11✔
616
        return self._bins
11✔
617

618
    @bins.setter
11✔
619
    def bins(self, bins):
11✔
620
        cv.check_iterable_type('bins', bins, Iterable)
11✔
621
        self._bins = list(map(tuple, bins))
11✔
622

623
    @property
11✔
624
    def num_bins(self):
11✔
625
        return len(self.bins) if self.aggregate_filter else 0
11✔
626

627
    @property
11✔
628
    def shape(self):
11✔
629
        return (self.num_bins,)
11✔
630

631
    def get_bin_index(self, filter_bin):
11✔
632
        """Returns the index in the AggregateFilter for some bin.
633

634
        Parameters
635
        ----------
636
        filter_bin : Integral or tuple of Real
637
            A tuple of value(s) corresponding to the bin of interest in
638
            the aggregated filter. The bin is the integer ID for 'material',
639
            'surface', 'cell', 'cellborn', and 'universe' Filters. The bin
640
            is the integer cell instance ID for 'distribcell' Filters. The
641
            bin is a 2-tuple of floats for 'energy' and 'energyout' filters
642
            corresponding to the energy boundaries of the bin of interest.
643
            The bin is a (x,y,z) 3-tuple for 'mesh' filters corresponding to
644
            the mesh cell of interest.
645

646
        Returns
647
        -------
648
        filter_index : Integral
649
             The index in the Tally data array for this filter bin. For an
650
             AggregateTally the filter bin index is always unity.
651

652
        Raises
653
        ------
654
        ValueError
655
            When the filter_bin is not part of the aggregated filter's bins
656

657
        """
658

659
        if filter_bin not in self.bins:
×
660
            msg = ('Unable to get the bin index for AggregateFilter since '
×
661
                   f'"{filter_bin}" is not one of the bins')
662
            raise ValueError(msg)
×
663
        else:
664
            return self.bins.index(filter_bin)
×
665

666
    def get_pandas_dataframe(self, data_size, stride, summary=None, **kwargs):
11✔
667
        """Builds a Pandas DataFrame for the AggregateFilter's bins.
668

669
        This method constructs a Pandas DataFrame object for the AggregateFilter
670
        with columns annotated by filter bin information. This is a helper
671
        method for the TallyBase.get_pandas_dataframe(...) method.
672

673
        Parameters
674
        ----------
675
        data_size : int
676
            The total number of bins in the tally corresponding to this filter
677
        stride : int
678
            Stride in memory for the filter
679
        summary : None or Summary
680
            An optional Summary object to be used to construct columns for
681
            distribcell tally filters (default is None). NOTE: This parameter
682
            is not used by the AggregateFilter and simply mirrors the method
683
            signature for the CrossFilter.
684

685
        Returns
686
        -------
687
        pandas.DataFrame
688
            A Pandas DataFrame with columns of strings that characterize the
689
            aggregatefilter's bins. Each entry in the DataFrame will include
690
            one or more aggregation operations used to construct the
691
            aggregatefilter's bins. The number of rows in the DataFrame is the
692
            same as the total number of bins in the corresponding tally, with
693
            the filter bins appropriately tiled to map to the corresponding
694
            tally bins.
695

696
        See also
697
        --------
698
        TallyBase.get_pandas_dataframe(), Filter.get_pandas_dataframe(),
699
        CrossFilter.get_pandas_dataframe()
700

701
        """
702
        # Create NumPy array of the bin tuples for repeating / tiling
703
        filter_bins = np.empty(self.num_bins, dtype=tuple)
11✔
704
        for i, bin in enumerate(self.bins):
11✔
705
            filter_bins[i] = bin
11✔
706

707
        # Repeat and tile bins as needed for DataFrame
708
        filter_bins = np.repeat(filter_bins, stride)
11✔
709
        tile_factor = data_size / len(filter_bins)
11✔
710
        filter_bins = np.tile(filter_bins, tile_factor)
11✔
711

712
        # Create DataFrame with aggregated bins
713
        df = pd.DataFrame({self.type: filter_bins})
11✔
714
        return df
11✔
715

716
    def can_merge(self, other):
11✔
717
        """Determine if AggregateFilter can be merged with another.
718

719
        Parameters
720
        ----------
721
        other : AggregateFilter
722
            Filter to compare with
723

724
        Returns
725
        -------
726
        bool
727
            Whether the filter can be merged
728

729
        """
730

731
        if not isinstance(other, AggregateFilter):
11✔
732
            return False
×
733

734
        # Filters must be of the same type
735
        elif self.type != other.type:
11✔
736
            return False
×
737

738
        # None of the bins in this filter should match in the other filter
739
        return not any(b in other.bins for b in self.bins)
11✔
740

741
    def merge(self, other):
11✔
742
        """Merge this aggregatefilter with another.
743

744
        Parameters
745
        ----------
746
        other : AggregateFilter
747
            Filter to merge with
748

749
        Returns
750
        -------
751
        merged_filter : AggregateFilter
752
            Filter resulting from the merge
753

754
        """
755

756
        if not self.can_merge(other):
11✔
757
            msg = f'Unable to merge "{self.type}" with "{other.type}" filters'
×
758
            raise ValueError(msg)
×
759

760
        # Create deep copy of filter to return as merged filter
761
        merged_filter = copy.deepcopy(self)
11✔
762

763
        # Merge unique filter bins
764
        merged_bins = self.bins + other.bins
11✔
765

766
        # Sort energy bin edges
767
        if 'energy' in self.type:
11✔
768
            merged_bins = sorted(merged_bins)
×
769

770
        # Assign merged bins to merged filter
771
        merged_filter.bins = list(merged_bins)
11✔
772
        return merged_filter
11✔
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