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

openmc-dev / openmc / 12954288577

24 Jan 2025 05:13PM UTC coverage: 84.833% (-0.1%) from 84.928%
12954288577

Pull #2671

github

web-flow
Merge d2ca87df5 into 560bd22bc
Pull Request #2671: Adding methods to automatically apply results to existing Tally objects.

53 of 65 new or added lines in 7 files covered. (81.54%)

59 existing lines in 20 files now uncovered.

50089 of 59044 relevant lines covered (84.83%)

34992387.74 hits per line

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

76.49
/openmc/arithmetic.py
1
from collections.abc import Iterable
12✔
2
import copy
12✔
3

4
import numpy as np
12✔
5
import pandas as pd
12✔
6

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

11

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

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

18

19
class CrossScore:
12✔
20
    """A special-purpose tally score used to encapsulate all combinations of two
21
    tally's scores as an outer product for tally arithmetic.
22

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

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

43
    """
44

45
    def __init__(self, left_score, right_score, binary_op):
12✔
46
        self.left_score = left_score
12✔
47
        self.right_score = right_score
12✔
48
        self.binary_op = binary_op
12✔
49

50
    def __hash__(self):
12✔
51
        return hash(repr(self))
12✔
52

53
    def __eq__(self, other):
12✔
54
        return str(other) == str(self)
12✔
55

56
    def __repr__(self):
12✔
57
        return f'({self.left_score} {self.binary_op} {self.right_score})'
12✔
58

59
    @property
12✔
60
    def left_score(self):
12✔
61
        return self._left_score
12✔
62

63
    @left_score.setter
12✔
64
    def left_score(self, left_score):
12✔
65
        cv.check_type('left_score', left_score,
12✔
66
                      (str, CrossScore, AggregateScore))
67
        self._left_score = left_score
12✔
68

69
    @property
12✔
70
    def right_score(self):
12✔
71
        return self._right_score
12✔
72

73
    @right_score.setter
12✔
74
    def right_score(self, right_score):
12✔
75
        cv.check_type('right_score', right_score,
12✔
76
                      (str, CrossScore, AggregateScore))
77
        self._right_score = right_score
12✔
78

79
    @property
12✔
80
    def binary_op(self):
12✔
81
        return self._binary_op
12✔
82

83
    @binary_op.setter
12✔
84
    def binary_op(self, binary_op):
12✔
85
        cv.check_type('binary_op', binary_op, str)
12✔
86
        cv.check_value('binary_op', binary_op, _TALLY_ARITHMETIC_OPS)
12✔
87
        self._binary_op = binary_op
12✔
88

89

90
class CrossNuclide:
12✔
91
    """A special-purpose nuclide used to encapsulate all combinations of two
92
    tally's nuclides as an outer product for tally arithmetic.
93

94
    Parameters
95
    ----------
96
    left_nuclide : str or CrossNuclide
97
        The left nuclide in the outer product
98
    right_nuclide : str or CrossNuclide
99
        The right nuclide in the outer product
100
    binary_op : str
101
        The tally arithmetic binary operator (e.g., '+', '-', etc.) used to
102
        combine two tally's nuclides with this CrossNuclide
103

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

114
    """
115

116
    def __init__(self, left_nuclide, right_nuclide, binary_op):
12✔
117
        self.left_nuclide = left_nuclide
12✔
118
        self.right_nuclide = right_nuclide
12✔
119
        self.binary_op = binary_op
12✔
120

121
    def __hash__(self):
12✔
122
        return hash(repr(self))
12✔
123

124
    def __eq__(self, other):
12✔
125
        return str(other) == str(self)
12✔
126

127
    def __repr__(self):
12✔
128
        return self.name
12✔
129

130
    @property
12✔
131
    def left_nuclide(self):
12✔
132
        return self._left_nuclide
12✔
133

134
    @left_nuclide.setter
12✔
135
    def left_nuclide(self, left_nuclide):
12✔
136
        cv.check_type('left_nuclide', left_nuclide,
12✔
137
                      (str, CrossNuclide, AggregateNuclide))
138
        self._left_nuclide = left_nuclide
12✔
139

140
    @property
12✔
141
    def right_nuclide(self):
12✔
142
        return self._right_nuclide
12✔
143

144
    @right_nuclide.setter
12✔
145
    def right_nuclide(self, right_nuclide):
12✔
146
        cv.check_type('right_nuclide', right_nuclide,
12✔
147
                      (str, CrossNuclide, AggregateNuclide))
148
        self._right_nuclide = right_nuclide
12✔
149

150
    @property
12✔
151
    def binary_op(self):
12✔
152
        return self._binary_op
12✔
153

154
    @binary_op.setter
12✔
155
    def binary_op(self, binary_op):
12✔
156
        cv.check_type('binary_op', binary_op, str)
12✔
157
        cv.check_value('binary_op', binary_op, _TALLY_ARITHMETIC_OPS)
12✔
158
        self._binary_op = binary_op
12✔
159

160
    @property
12✔
161
    def name(self):
12✔
162
        return f'({self.left_nuclide} {self.binary_op} {self.right_nuclide})'
12✔
163

164

165
class CrossFilter:
12✔
166
    """A special-purpose filter used to encapsulate all combinations of two
167
    tally's filter bins as an outer product for tally arithmetic.
168

169
    Parameters
170
    ----------
171
    left_filter : openmc.Filter or CrossFilter
172
        The left filter in the outer product
173
    right_filter : openmc.Filter or CrossFilter
174
        The right filter in the outer product
175
    binary_op : str
176
        The tally arithmetic binary operator (e.g., '+', '-', etc.) used to
177
        combine two tally's filter bins with this CrossFilter
178

179
    Attributes
180
    ----------
181
    type : str
182
        The type of the crossfilter (e.g., 'energy / energy')
183
    left_filter : openmc.Filter or CrossFilter
184
        The left filter in the outer product
185
    right_filter : openmc.Filter or CrossFilter
186
        The right filter in the outer product
187
    binary_op : str
188
        The tally arithmetic binary operator (e.g., '+', '-', etc.) used to
189
        combine two tally's filter bins with this CrossFilter
190
    bins : dict of Iterable
191
        A dictionary of the bins from each filter keyed by the types of the
192
        left / right filters
193
    num_bins : Integral
194
        The number of filter bins (always 1 if aggregate_filter is defined)
195

196
    """
197

198
    def __init__(self, left_filter, right_filter, binary_op):
12✔
199
        self.left_filter = left_filter
×
200
        self.right_filter = right_filter
×
201
        self.binary_op = binary_op
×
202

203
    def __hash__(self):
12✔
204
        return hash((self.left_filter, self.right_filter))
×
205

206
    def __eq__(self, other):
12✔
207
        return str(other) == str(self)
×
208

209
    def __repr__(self):
12✔
210
        filter_bins = '({} {} {})'.format(self.left_filter.bins,
×
211
                                          self.binary_op,
212
                                          self.right_filter.bins)
213
        parts = [
×
214
            'CrossFilter',
215
            '{: <16}=\t{}'.format('\tType', self.type),
216
            '{: <16}=\t{}'.format('\tBins', filter_bins)
217
        ]
218
        return '\n'.join(parts)
×
219

220
    @property
12✔
221
    def left_filter(self):
12✔
222
        return self._left_filter
×
223

224
    @left_filter.setter
12✔
225
    def left_filter(self, left_filter):
12✔
226
        cv.check_type('left_filter', left_filter,
×
227
                      (openmc.Filter, CrossFilter, AggregateFilter))
228
        self._left_filter = left_filter
×
229

230
    @property
12✔
231
    def right_filter(self):
12✔
232
        return self._right_filter
×
233

234
    @right_filter.setter
12✔
235
    def right_filter(self, right_filter):
12✔
236
        cv.check_type('right_filter', right_filter,
×
237
                      (openmc.Filter, CrossFilter, AggregateFilter))
238
        self._right_filter = right_filter
×
239

240
    @property
12✔
241
    def binary_op(self):
12✔
242
        return self._binary_op
×
243

244
    @binary_op.setter
12✔
245
    def binary_op(self, binary_op):
12✔
246
        cv.check_type('binary_op', binary_op, str)
×
247
        cv.check_value('binary_op', binary_op, _TALLY_ARITHMETIC_OPS)
×
248
        self._binary_op = binary_op
×
249

250
    @property
12✔
251
    def type(self):
12✔
252
        left_type = self.left_filter.type
×
253
        right_type = self.right_filter.type
×
254
        return f'({left_type} {self.binary_op} {right_type})'
×
255

256
    @property
12✔
257
    def bins(self):
12✔
258
        return self._left_filter.bins, self._right_filter.bins
×
259

260
    @property
12✔
261
    def num_bins(self):
12✔
262
        if self.left_filter is not None and self.right_filter is not None:
×
263
            return self.left_filter.num_bins * self.right_filter.num_bins
×
264
        else:
265
            return 0
×
266

267
    def get_bin_index(self, filter_bin):
12✔
268
        """Returns the index in the CrossFilter for some bin.
269

270
        Parameters
271
        ----------
272
        filter_bin : 2-tuple
273
            A 2-tuple where each value corresponds to the bin of interest
274
            in the left and right filter, respectively. A bin is the integer
275
            ID for 'material', 'surface', 'cell', 'cellborn', and 'universe'
276
            Filters. The bin is an integer for the cell instance ID for
277
            'distribcell' Filters. The bin is a 2-tuple of floats for 'energy'
278
            and 'energyout' filters corresponding to the energy boundaries of
279
            the bin of interest.  The bin is a (x,y,z) 3-tuple for 'mesh'
280
            filters corresponding to the mesh cell of interest.
281

282
        Returns
283
        -------
284
        filter_index : Integral
285
             The index in the Tally data array for this filter bin.
286

287
        """
288

289
        left_index = self.left_filter.get_bin_index(filter_bin[0])
×
290
        right_index = self.right_filter.get_bin_index(filter_bin[0])
×
291
        filter_index = left_index * self.right_filter.num_bins + right_index
×
292
        return filter_index
×
293

294
    def get_pandas_dataframe(self, data_size, summary=None):
12✔
295
        """Builds a Pandas DataFrame for the CrossFilter's bins.
296

297
        This method constructs a Pandas DataFrame object for the CrossFilter
298
        with columns annotated by filter bin information. This is a helper
299
        method for the Tally.get_pandas_dataframe(...) method. This method
300
        recursively builds and concatenates Pandas DataFrames for the left
301
        and right filters and crossfilters.
302

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

307
        Parameters
308
        ----------
309
        data_size : Integral
310
            The total number of bins in the tally corresponding to this filter
311
        summary : None or Summary
312
            An optional Summary object to be used to construct columns for
313
            distribcell tally filters (default is None). The geometric
314
            information in the Summary object is embedded into a Multi-index
315
            column with a geometric "path" to each distribcell instance.
316

317
        Returns
318
        -------
319
        pandas.DataFrame
320
            A Pandas DataFrame with columns of strings that characterize the
321
            crossfilter's bins. Each entry in the DataFrame will include one
322
            or more binary operations used to construct the crossfilter's bins.
323
            The number of rows in the DataFrame is the same as the total number
324
            of bins in the corresponding tally, with the filter bins
325
            appropriately tiled to map to the corresponding tally bins.
326

327
        See also
328
        --------
329
        Tally.get_pandas_dataframe(), Filter.get_pandas_dataframe()
330

331
        """
332

333
        # If left and right filters are identical, do not combine bins
334
        if self.left_filter == self.right_filter:
×
335
            df = self.left_filter.get_pandas_dataframe(data_size, summary)
×
336

337
        # If left and right filters are different, combine their bins
338
        else:
339
            left_df = self.left_filter.get_pandas_dataframe(data_size, summary)
×
340
            right_df = self.right_filter.get_pandas_dataframe(data_size, summary)
×
341
            left_df = left_df.astype(str)
×
342
            right_df = right_df.astype(str)
×
343
            df = f'({left_df} {self.binary_op} {right_df})'
×
344

345
        return df
×
346

347

348
class AggregateScore:
12✔
349
    """A special-purpose tally score used to encapsulate an aggregate of a
350
    subset or all of tally's scores for tally aggregation.
351

352
    Parameters
353
    ----------
354
    scores : Iterable of str or CrossScore
355
        The scores included in the aggregation
356
    aggregate_op : str
357
        The tally aggregation operator (e.g., 'sum', 'avg', etc.) used
358
        to aggregate across a tally's scores with this AggregateScore
359

360
    Attributes
361
    ----------
362
    scores : Iterable of str or CrossScore
363
        The scores included in the aggregation
364
    aggregate_op : str
365
        The tally aggregation operator (e.g., 'sum', 'avg', etc.) used
366
        to aggregate across a tally's scores with this AggregateScore
367

368
    """
369

370
    def __init__(self, scores=None, aggregate_op=None):
12✔
371

372
        self._scores = None
12✔
373
        self._aggregate_op = None
12✔
374

375
        if scores is not None:
12✔
376
            self.scores = scores
12✔
377
        if aggregate_op is not None:
12✔
378
            self.aggregate_op = aggregate_op
12✔
379

380
    def __hash__(self):
12✔
381
        return hash(repr(self))
×
382

383
    def __eq__(self, other):
12✔
384
        return str(other) == str(self)
×
385

386
    def __repr__(self):
12✔
387
        string = ', '.join(map(str, self.scores))
×
388
        string = f'{self.aggregate_op}({string})'
×
389
        return string
×
390

391
    @property
12✔
392
    def scores(self):
12✔
393
        return self._scores
×
394

395
    @scores.setter
12✔
396
    def scores(self, scores):
12✔
397
        cv.check_iterable_type('scores', scores, str)
12✔
398
        self._scores = scores
12✔
399

400
    @property
12✔
401
    def aggregate_op(self):
12✔
402
        return self._aggregate_op
×
403

404
    @aggregate_op.setter
12✔
405
    def aggregate_op(self, aggregate_op):
12✔
406
        cv.check_type('aggregate_op', aggregate_op, (str, CrossScore))
12✔
407
        cv.check_value('aggregate_op', aggregate_op, _TALLY_AGGREGATE_OPS)
12✔
408
        self._aggregate_op = aggregate_op
12✔
409

410
    @property
12✔
411
    def name(self):
12✔
412

413
        # Append each score in the aggregate to the string
414
        string = '(' + ', '.join(self.scores) + ')'
×
415
        return string
×
416

417

418
class AggregateNuclide:
12✔
419
    """A special-purpose tally nuclide used to encapsulate an aggregate of a
420
    subset or all of tally's nuclides for tally aggregation.
421

422
    Parameters
423
    ----------
424
    nuclides : Iterable of str or CrossNuclide
425
        The nuclides included in the aggregation
426
    aggregate_op : str
427
        The tally aggregation operator (e.g., 'sum', 'avg', etc.) used
428
        to aggregate across a tally's nuclides with this AggregateNuclide
429

430
    Attributes
431
    ----------
432
    nuclides : Iterable of str or CrossNuclide
433
        The nuclides included in the aggregation
434
    aggregate_op : str
435
        The tally aggregation operator (e.g., 'sum', 'avg', etc.) used
436
        to aggregate across a tally's nuclides with this AggregateNuclide
437

438
    """
439

440
    def __init__(self, nuclides=None, aggregate_op=None):
12✔
441

442
        self._nuclides = None
12✔
443
        self._aggregate_op = None
12✔
444

445
        if nuclides is not None:
12✔
446
            self.nuclides = nuclides
12✔
447
        if aggregate_op is not None:
12✔
448
            self.aggregate_op = aggregate_op
12✔
449

450
    def __hash__(self):
12✔
451
        return hash(repr(self))
×
452

453
    def __eq__(self, other):
12✔
454
        return str(other) == str(self)
×
455

456
    def __repr__(self):
12✔
NEW
457
        return f'{self.aggregate_op}{self.name}'
×
458

459
    @property
12✔
460
    def nuclides(self):
12✔
461
        return self._nuclides
×
462

463
    @nuclides.setter
12✔
464
    def nuclides(self, nuclides):
12✔
465
        cv.check_iterable_type('nuclides', nuclides, (str, CrossNuclide))
12✔
466
        self._nuclides = nuclides
12✔
467

468
    @property
12✔
469
    def aggregate_op(self):
12✔
470
        return self._aggregate_op
×
471

472
    @aggregate_op.setter
12✔
473
    def aggregate_op(self, aggregate_op):
12✔
474
        cv.check_type('aggregate_op', aggregate_op, str)
12✔
475
        cv.check_value('aggregate_op', aggregate_op, _TALLY_AGGREGATE_OPS)
12✔
476
        self._aggregate_op = aggregate_op
12✔
477

478
    @property
12✔
479
    def name(self):
12✔
480
        # Append each nuclide in the aggregate to the string
NEW
481
        names = [str(nuclide) for nuclide in self.nuclides]
×
NEW
482
        return '(' + ', '.join(map(str, names)) + ')'
×
483

484

485
class AggregateFilter:
12✔
486
    """A special-purpose tally filter used to encapsulate an aggregate of a
487
    subset or all of a tally filter's bins for tally aggregation.
488

489
    Parameters
490
    ----------
491
    aggregate_filter : openmc.Filter or CrossFilter
492
        The filter included in the aggregation
493
    bins : Iterable of tuple
494
        The filter bins included in the aggregation
495
    aggregate_op : str
496
        The tally aggregation operator (e.g., 'sum', 'avg', etc.) used
497
        to aggregate across a tally filter's bins with this AggregateFilter
498

499
    Attributes
500
    ----------
501
    type : str
502
        The type of the aggregatefilter (e.g., 'sum(energy)', 'sum(cell)')
503
    aggregate_filter : openmc.Filter
504
        The filter included in the aggregation
505
    aggregate_op : str
506
        The tally aggregation operator (e.g., 'sum', 'avg', etc.) used
507
        to aggregate across a tally filter's bins with this AggregateFilter
508
    bins : Iterable of tuple
509
        The filter bins included in the aggregation
510
    num_bins : Integral
511
        The number of filter bins (always 1 if aggregate_filter is defined)
512

513
    """
514

515
    def __init__(self, aggregate_filter, bins=None, aggregate_op=None):
12✔
516

517
        self._type = f'{aggregate_op}({aggregate_filter.short_name.lower()})'
12✔
518
        self._bins = None
12✔
519

520
        self._aggregate_filter = None
12✔
521
        self._aggregate_op = None
12✔
522

523
        self.aggregate_filter = aggregate_filter
12✔
524
        if bins is not None:
12✔
525
            self.bins = bins
12✔
526
        if aggregate_op is not None:
12✔
527
            self.aggregate_op = aggregate_op
12✔
528

529
    def __hash__(self):
12✔
530
        return hash(repr(self))
12✔
531

532
    def __eq__(self, other):
12✔
533
        return str(other) == str(self)
12✔
534

535
    def __gt__(self, other):
12✔
536
        if self.type != other.type:
12✔
537
            if self.aggregate_filter.type in _FILTER_TYPES and \
×
538
              other.aggregate_filter.type in _FILTER_TYPES:
539
                delta = _FILTER_TYPES.index(self.aggregate_filter.type) - \
×
540
                        _FILTER_TYPES.index(other.aggregate_filter.type)
541
                return delta > 0
×
542
            else:
543
                return False
×
544
        else:
545
            return False
12✔
546

547
    def __lt__(self, other):
12✔
548
        return not self > other
12✔
549

550
    def __repr__(self):
12✔
551
        parts = [
12✔
552
            'AggregateFilter',
553
            '{: <16}=\t{}'.format('\tType', self.type),
554
            '{: <16}=\t{}'.format('\tBins', self.bins)
555
        ]
556
        return '\n'.join(parts)
12✔
557

558
    @property
12✔
559
    def aggregate_filter(self):
12✔
560
        return self._aggregate_filter
12✔
561

562
    @aggregate_filter.setter
12✔
563
    def aggregate_filter(self, aggregate_filter):
12✔
564
        cv.check_type('aggregate_filter', aggregate_filter,
12✔
565
                      (openmc.Filter, CrossFilter))
566
        self._aggregate_filter = aggregate_filter
12✔
567

568
    @property
12✔
569
    def aggregate_op(self):
12✔
570
        return self._aggregate_op
×
571

572
    @aggregate_op.setter
12✔
573
    def aggregate_op(self, aggregate_op):
12✔
574
        cv.check_type('aggregate_op', aggregate_op, str)
12✔
575
        cv.check_value('aggregate_op', aggregate_op, _TALLY_AGGREGATE_OPS)
12✔
576
        self._aggregate_op = aggregate_op
12✔
577

578
    @property
12✔
579
    def type(self):
12✔
580
        return self._type
12✔
581

582
    @type.setter
12✔
583
    def type(self, filter_type):
12✔
584
        if filter_type not in _FILTER_TYPES:
×
585
            msg = f'Unable to set AggregateFilter type to "{filter_type}" ' \
×
586
                  'since it is not one of the supported types'
587
            raise ValueError(msg)
×
588

589
        self._type = filter_type
×
590

591
    @property
12✔
592
    def bins(self):
12✔
593
        return self._bins
12✔
594

595
    @bins.setter
12✔
596
    def bins(self, bins):
12✔
597
        cv.check_iterable_type('bins', bins, Iterable)
12✔
598
        self._bins = list(map(tuple, bins))
12✔
599

600
    @property
12✔
601
    def num_bins(self):
12✔
602
        return len(self.bins) if self.aggregate_filter else 0
12✔
603

604
    @property
12✔
605
    def shape(self):
12✔
606
        return (self.num_bins,)
12✔
607

608
    def get_bin_index(self, filter_bin):
12✔
609
        """Returns the index in the AggregateFilter for some bin.
610

611
        Parameters
612
        ----------
613
        filter_bin : Integral or tuple of Real
614
            A tuple of value(s) corresponding to the bin of interest in
615
            the aggregated filter. The bin is the integer ID for 'material',
616
            'surface', 'cell', 'cellborn', and 'universe' Filters. The bin
617
            is the integer cell instance ID for 'distribcell' Filters. The
618
            bin is a 2-tuple of floats for 'energy' and 'energyout' filters
619
            corresponding to the energy boundaries of the bin of interest.
620
            The bin is a (x,y,z) 3-tuple for 'mesh' filters corresponding to
621
            the mesh cell of interest.
622

623
        Returns
624
        -------
625
        filter_index : Integral
626
             The index in the Tally data array for this filter bin. For an
627
             AggregateTally the filter bin index is always unity.
628

629
        Raises
630
        ------
631
        ValueError
632
            When the filter_bin is not part of the aggregated filter's bins
633

634
        """
635

636
        if filter_bin not in self.bins:
×
637
            msg = ('Unable to get the bin index for AggregateFilter since '
×
638
                   f'"{filter_bin}" is not one of the bins')
639
            raise ValueError(msg)
×
640
        else:
641
            return self.bins.index(filter_bin)
×
642

643
    def get_pandas_dataframe(self, data_size, stride, summary=None, **kwargs):
12✔
644
        """Builds a Pandas DataFrame for the AggregateFilter's bins.
645

646
        This method constructs a Pandas DataFrame object for the AggregateFilter
647
        with columns annotated by filter bin information. This is a helper
648
        method for the Tally.get_pandas_dataframe(...) method.
649

650
        Parameters
651
        ----------
652
        data_size : int
653
            The total number of bins in the tally corresponding to this filter
654
        stride : int
655
            Stride in memory for the filter
656
        summary : None or Summary
657
            An optional Summary object to be used to construct columns for
658
            distribcell tally filters (default is None). NOTE: This parameter
659
            is not used by the AggregateFilter and simply mirrors the method
660
            signature for the CrossFilter.
661

662
        Returns
663
        -------
664
        pandas.DataFrame
665
            A Pandas DataFrame with columns of strings that characterize the
666
            aggregatefilter's bins. Each entry in the DataFrame will include
667
            one or more aggregation operations used to construct the
668
            aggregatefilter's bins. The number of rows in the DataFrame is the
669
            same as the total number of bins in the corresponding tally, with
670
            the filter bins appropriately tiled to map to the corresponding
671
            tally bins.
672

673
        See also
674
        --------
675
        Tally.get_pandas_dataframe(), Filter.get_pandas_dataframe(),
676
        CrossFilter.get_pandas_dataframe()
677

678
        """
679
        # Create NumPy array of the bin tuples for repeating / tiling
680
        filter_bins = np.empty(self.num_bins, dtype=tuple)
12✔
681
        for i, bin in enumerate(self.bins):
12✔
682
            filter_bins[i] = bin
12✔
683

684
        # Repeat and tile bins as needed for DataFrame
685
        filter_bins = np.repeat(filter_bins, stride)
12✔
686
        tile_factor = data_size / len(filter_bins)
12✔
687
        filter_bins = np.tile(filter_bins, tile_factor)
12✔
688

689
        # Create DataFrame with aggregated bins
690
        df = pd.DataFrame({self.type: filter_bins})
12✔
691
        return df
12✔
692

693
    def can_merge(self, other):
12✔
694
        """Determine if AggregateFilter can be merged with another.
695

696
        Parameters
697
        ----------
698
        other : AggregateFilter
699
            Filter to compare with
700

701
        Returns
702
        -------
703
        bool
704
            Whether the filter can be merged
705

706
        """
707

708
        if not isinstance(other, AggregateFilter):
12✔
709
            return False
×
710

711
        # Filters must be of the same type
712
        elif self.type != other.type:
12✔
713
            return False
×
714

715
        # None of the bins in this filter should match in the other filter
716
        return not any(b in other.bins for b in self.bins)
12✔
717

718
    def merge(self, other):
12✔
719
        """Merge this aggregatefilter with another.
720

721
        Parameters
722
        ----------
723
        other : AggregateFilter
724
            Filter to merge with
725

726
        Returns
727
        -------
728
        merged_filter : AggregateFilter
729
            Filter resulting from the merge
730

731
        """
732

733
        if not self.can_merge(other):
12✔
734
            msg = f'Unable to merge "{self.type}" with "{other.type}" filters'
×
735
            raise ValueError(msg)
×
736

737
        # Create deep copy of filter to return as merged filter
738
        merged_filter = copy.deepcopy(self)
12✔
739

740
        # Merge unique filter bins
741
        merged_bins = self.bins + other.bins
12✔
742

743
        # Sort energy bin edges
744
        if 'energy' in self.type:
12✔
745
            merged_bins = sorted(merged_bins)
×
746

747
        # Assign merged bins to merged filter
748
        merged_filter.bins = list(merged_bins)
12✔
749
        return merged_filter
12✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc