Tallies#

This guide covers how to inspect and build tallies: what a tally scores, which cells or surfaces it covers, and how its tally multiplier modifies it. Every tally (F) input and tally multiplier (FM) input in a problem is available as a real object through problem.tallies, addressable by number like any other MontePy collection.

The Tally Object Hierarchy#

Every tally in a problem is stored in problem.tallies, a Tallies collection, and can be accessed by its number like any other collection in MontePy.

tally = problem.tallies[4]
print(tally)
CellFluxTally: 4

MontePy picks the class for you based on the tally’s type digit (e.g., the 4 in F4). Tally is the base class, and it has one subclass for every tally type:

Quantity Tallied

Type Digit

MontePy Class

Shorthand Alias

Surface current

F1

SurfaceCurrentTally

montepy.F1Tally

Average surface flux

F2

SurfaceFluxTally

montepy.F2Tally

Cell flux

F4

CellFluxTally

montepy.F4Tally

Point/ring detector flux

F5

DetectorTally

montepy.F5Tally

Energy deposition

F6

EnergyDepositionTally

montepy.F6Tally

Fission energy deposition

F7

FissionEnergyDepositionTally

montepy.F7Tally

Pulse height (energy deposition in a detector)

F8

EnergyDetectorPulseTally

montepy.F8Tally

Collision heating

+F6

CollisionHeatingTally

montepy.PlusF6Tally

Charge deposition

+F8

ChargeDepositionTally

montepy.PlusF8Tally

The Shorthand Alias is just another name for the same class, e.g. montepy.F4Tally is CellFluxTally.

The leading + on +F6/+F8 is a real, separate MCNP tally variant, not a typo of the plain card: +F6 (collision heating) and +F8 (charge deposition) score a different physical quantity than plain F6/F8, so MontePy treats them as distinct classes even though they share the same type digit.

Underneath these, there are two intermediate classes worth knowing about: SurfaceTally for tallies that score on surfaces (i.e., F1 and F2), and CellTally for tallies that score in cells (i.e., F4, F6, F7, and F8). You can check tally_type to get the TallyType for any tally.

>>> tally.tally_type
<TallyType.CELL_FLUX: 4>

Geometry Filters#

The cells or surfaces a tally scores over are its geometry filter, exposed as groups, a list of TallyGroup objects.

This section covers flat cell/surface lists, where every entry is a FlatGroup. A FlatGroup knows the numbers it covers, and whether they form a single averaged bin or separate bins. Repeated structures and lattices use the </[...]/U= syntax instead, which produces PathGroup entries; see Universe and Lattice Paths below.

In the simple, “flat” case, a tally just lists cells or surfaces one after another, and MCNP creates a separate bin for each one:

tally = problem.tallies[4]
for group in tally.groups:
    cells = [problem.cells[n] for n in group.old_numbers]
    print(*cells, group.is_grouped)
Cell: 1 False
Cell: 2 False
Cell: 3 False

is_grouped is False for every group here, since F4:n 1 2 3 has no parentheses: each of cells 1, 2, and 3 gets its own separate bin. old_numbers is looked up by hand above to show how it relates to the raw numbers on the card, but once a group is linked to a problem you don’t need to do that lookup yourself: cells_or_surfaces gives you the resolved objects directly.

>>> [c.number for c in tally.groups[0].cells_or_surfaces]
[1]

Wrapping cells or surfaces in parentheses instead unions them into a single bin, averaged for normalized tally types like F2/F4/F6/F7, or summed for F1/F8, rather than reported separately. A tally can mix flat entries and multiple parenthesized groups on the same input, and each group becomes its own entry in groups:

grouped = montepy.CellFluxTally("f14:n (1 2) (3)")
for group in grouped.groups:
    cells = [problem.cells[n] for n in group.old_numbers]
    print(*cells, group.is_grouped)
Cell: 1 Cell: 2 True
Cell: 3 True

Notice that the second group here still has is_grouped set to True, even though it only covers a single cell: what matters is whether the parentheses were there, not how many cells are inside them. Without the parentheses, f14:n 1 2 3 would instead produce three separate, ungrouped bins, exactly like the cell flux tally example above. To build flat and grouped bins like these from scratch instead of reading them from an existing tally, see Building Tallies from Scratch below.

For cell tallies, cells gives you the flattened, deduplicated set of every cell the tally touches, across all of its groups. Surface tallies have the equivalent surfaces.

>>> print(tally.cells)
Cells: [1, 2, 3]

Use cells/surfaces when you just want to know what’s involved. Use groups when the bin structure itself matters.

Checking whether a specific cell is scored by a tally works the way you’d expect:

>>> problem.cells[1] in tally
True
>>> problem.cells[99] in tally
False

A tally can also end with a trailing T, for “total”: an extra bin that’s the union of every other bin on the input, rather than a scoring region of its own. Because it isn’t really its own region, MontePy doesn’t represent it as another entry in groups. Instead it’s a separate flag, include_total:

totaled = montepy.CellFluxTally("f24:n (1 2) (3) T")
for group in totaled.groups:
    cells = [problem.cells[n] for n in group.old_numbers]
    print(*cells, group.is_grouped)
print(totaled.include_total)
Cell: 1 Cell: 2 True
Cell: 3 True
True

Notice that groups only has the two real bins; the T never shows up as a third entry there, no matter how many bins came before it. Unlike scores and filters, include_total is settable, so you can turn total-bin reporting on (or off) for any tally, including one you build from scratch with add_cell()/add_group() (see Building Tallies from Scratch below):

>>> totaled.include_total = False
>>> totaled.include_total
False
>>> "T" in totaled.mcnp_str()
False

Building Tallies from Scratch#

It’s also possible to build a tally from scratch. Instantiate the concrete subclass for the quantity you want to score (e.g. CellFluxTally for flux), give it a number, and add cells or surfaces to it.

new_tally = montepy.CellFluxTally(number=104)
new_tally.add_cell(problem.cells[1])
new_tally.add_cell(problem.cells[2])
problem.tallies.append(new_tally)

add_cell() adds a cell as its own separate bin. If you want a group of cells averaged into a single bin instead, use add_group():

new_tally.add_group([problem.cells[1], problem.cells[3]])
>>> for group in new_tally.groups:
...     print(group.old_numbers, group.is_grouped)
[1] False
[2] False
[1, 3] True

SurfaceTally has the matching add_surface() and add_group().

Each of these has a matching removal method: remove_cell()/remove_surface() removes the single-item bin that add_cell/add_surface would have created, and remove_group() removes any group outright, whether it came from add_cell, add_group, or add_path_group. A cell or surface only drops out of cells/ surfaces once no remaining group references it.

>>> new_tally.remove_cell(problem.cells[2])
>>> for group in new_tally.groups:
...     print(group.old_numbers, group.is_grouped)
[1] False
[1, 3] True
>>> problem.cells[2] in new_tally.cells
False

Scores and Filters#

Note

“Score” and “filter” aren’t MCNP terms. MontePy borrows this vocabulary from OpenMC, since it’s more general than anything MCNP itself uses, and describes the same underlying concepts. See OpenMC’s tallies user guide for more on this terminology.

Sometimes you don’t need the raw group structure, you just want to know what physical quantity a tally is measuring, and for which particles. scores and filters give you a shallow, read-only summary of this.

>>> tally.scores
[<Score.FLUX: 2>]

Every tally type has a default score: Score is an enum with entries like FLUX, CURRENT, and ENERGY_DEPOSITION. This default is overridden if the tally has a tally multiplier attached; see Tally Multipliers below. filters returns a list of ParticleFilter and SpatialFilter objects, whichever apply:

f2 = problem.tallies[2]
for filt in f2.filters:
    print(filt)
ParticleFilter(':p')
SpatialFilter([FlatGroup([1005], grouped=False)])

Note

These aren’t a new way to write tallies to the input file, they’re just a convenient way to read what’s already there. scores and filters aren’t settable.

Cloning Tallies#

Like most MontePy objects, tallies support clone(), which copies a tally and gives it a new, unused number of the same tally type.

clone = tally.clone()
>>> clone.number
14

Tallies also have something the other objects don’t: clone_as(). This copies a tally’s scoring geometry into a different tally type. It’s handy when you already have flux tallied over a set of cells and you also want the heating in those same cells, without retyping the cell list:

heating = tally.clone_as(montepy.EnergyDepositionTally)
>>> print(heating)
EnergyDepositionTally: 16
>>> print(heating.cells)
Cells: [1, 2, 3]
>>> heating.scores
[<Score.ENERGY_DEPOSITION: 6>]

clone_as only allows conversions within the same family: surface tallies (i.e., F1 and F2) convert to other surface tallies, and cell tallies (i.e., F4, F6, F7, and F8) convert to other cell tallies. F5 point/ring detectors are their own family, since they don’t have cells or surfaces to carry over. Trying to cross families raises a ValueError.

The +F6/+F8 modifier variants (see the table above) are part of the same family as their plain counterparts, so clone_as converts freely between them too:

collision_heating = heating.clone_as(montepy.CollisionHeatingTally)
>>> print(collision_heating)
CollisionHeatingTally: 26
>>> collision_heating.scores
[<Score.COLLISION_HEATING: 9>]
>>> "+" in collision_heating.mcnp_str()
True

Tally Multipliers#

A tally multiplier input multiplies a tally’s flux or current by a cross section, turning a plain flux tally into a reaction rate, a heating rate, or similar. MontePy represents this as a TallyMultiplier, linked to its tally through multiplier.

fm = montepy.TallyMultiplier("fm4 (1.0 26 16 103)")
problem.tallies.append(fm)
>>> tally.multiplier is fm
True

An FMn input is linked to its tally purely by number, the same way an MTn thermal scattering input gets linked to material n. You can append the TallyMultiplier and its Tally to the problem in either order, and MontePy will connect them once both are present. Because that link is the whole point of an FMn card, MontePy keeps it consistent for you: deleting a tally that has a linked multiplier removes the now-orphaned FM card from the problem too (with a warning), and renumbering a tally renumbers its linked multiplier to match.

Since a TallyMultiplier’s number always tracks its parent tally’s rather than being independently assignable, it has its own clone(), separate from clone(): pass the tally to attach the clone to (it takes that tally’s number), or omit it for a detached, unregistered copy.

fm_clone = fm.clone(problem.tallies[14])
>>> fm_clone.number
14
>>> problem.tallies[14].multiplier is fm_clone
True

The bulk of a tally multiplier input is its bins, a list of MultiplierBin. Each bin holds one or more MultiplierSet or SpecialMultiplierSet terms, and optionally an AttenuatorSet.

term = fm.bins[0].terms[0]
>>> term.constant
1.0
>>> term.material
26

Once a tally has a multiplier, its scores switches from the generic default to a list of MultiplierScore, one for every output bin the multiplier defines. This gives you the full recipe (constant, material, reaction, and any attenuator) for every column of the tally’s output.

>>> tally.scores
[MultiplierScore(constant=1.0, material=26, reaction=ReactionExpression(Reaction(16), ReactionOperator.MULTIPLY, Reaction(103)), kind=None, attenuator=None)]

Building Reaction Expressions#

A reaction list on a tally multiplier input, like 16 103 in FM4 (1.0 26 16 103), is really a small expression: multiply reaction 16 by reaction 103. Rather than making you build this out of strings, MontePy lets you write it as an actual Python expression, using * for multiply, + for add, and - for subtract.

Note

Raw MT numbers like 16 are hard to read and easy to mistype. Reaction has named constants for the common ones, e.g. Reaction.N_2N for MT 16; use those instead of Reaction(16) whenever a name is available. Raw numbers are still there for the reactions that don’t have one.

>>> from montepy import Reaction
>>> expr = Reaction.N_2N * Reaction.N_P
>>> expr == term.reactions[0]
True
>>> fm.mcnp_str()
'fm4 (1.0 26 16 103)'

That’s exactly the reaction expression already attached to tally above, built by hand, matching the actual MCNP text of fm. Python’s own operator precedence already does the right thing for a longer list too, exactly like the MCNP manual says a reaction list should work, so you don’t need to write unnecessary parentheses to get the correct grouping.

>>> bigger_expr = Reaction.N_2N * Reaction.N_P + Reaction.N_D
>>> bigger_expr.left == expr
True
>>> bigger_expr.operator
<ReactionOperator.ADD: ':'>

You can go one step further and build a whole MultiplierSet with &, joining a material number to a reaction expression:

>>> built = 26 & Reaction.N_2N * Reaction.N_P
>>> built == term
True
>>> fm.mcnp_str()
'fm4 (1.0 26 16 103)'

You can scale the constant afterward with *:

>>> scaled = 1.5 * built
>>> scaled.constant
1.5

AttenuatorSet supports the same kind of chaining with &, for building up multiple attenuating layers:

>>> from montepy import AttenuatorSet, AttenuatorLayer
>>> attenuator = AttenuatorSet(1.0, [AttenuatorLayer(26, 0.5)])
>>> attenuator = attenuator & AttenuatorLayer(27, 0.3, is_atom_density=False)
>>> len(attenuator.layers)
2
>>> fm2 = montepy.TallyMultiplier("fm104:n (1.0 -1 26 0.5 27 -0.3)")
>>> attenuator == fm2.bins[0].attenuator
True
>>> fm2.mcnp_str()
'fm104:n (1.0 -1 26 0.5 27 -0.3)'

These operators build and compare MultiplierSet/ ReactionExpression values, but a TallyMultiplier needs one more step to actually add them to a card: wrap each term (and optional attenuator) in a MultiplierBin, then use add_bin() (and remove_bin() to take one back out). bins itself stays a read-only view, the same way groups does for a Tally.

from montepy.data_inputs.tally_multiplier import MultiplierBin

fm3 = montepy.TallyMultiplier(number=4)
fm3.add_bin(MultiplierBin([built]))
>>> fm3.bins[0].terms[0] == built
True
>>> fm3.mcnp_str()
'FM4 1.0 26 16 103 '

Universe and Lattice Paths#

This section is for the less common case: tallying a specific cell inside a specific universe or lattice, using the < path syntax. Most tallies don’t need this.

A tally group written with < becomes a PathGroup instead of a FlatGroup. Each step in the chain is still a FlatGroup, accessible through levels, ordered from innermost to outermost.

In the simplest case, a chain is just cell numbers separated by <, read innermost-to-outermost, e.g. “cell 2, inside cell 5”:

simple_path = montepy.CellFluxTally("f104:n (2 < 5)")
for level in simple_path.groups[0].levels:
    print(level.old_numbers)
[2]
[5]

A level can narrow further with a lattice-element index in brackets ([i j k]), and a universe designator (u=n) can stand in for an entire level, meaning “any cell in universe n”:

path_tally = montepy.CellFluxTally("f114:n (u=1 < 2[0 0 0] < 5)")
for level in path_tally.groups[0].levels:
    print(level.old_numbers, level.universe_spec)
[] 1
[2] None
[5] None

The first level has no cell number at all, just a universe designator (u=1), meaning “any cell in universe 1”. The second level narrows that down to lattice element [0 0 0] of cell 2, and the third level says that whole path has to live inside cell 5. That lattice element is available directly through lattice_indices, parallel to old_numbers, as a list of LatticeIndex:

>>> path_tally.groups[0].levels[1].lattice_indices[0].dimensions
(0, 0, 0)

You can also build a path group from scratch with add_path_group() and inside():

scratch_tally = montepy.CellFluxTally(number=304)
pg = scratch_tally.add_path_group(problem.cells[1])
pg.inside(problem.cells[2])
>>> for level in pg.levels:
...     print(level.old_numbers)
[1]
[2]

References#