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 |
|
|
|
Average surface flux |
|
|
|
Cell flux |
|
|
|
Point/ring detector flux |
|
|
|
Energy deposition |
|
|
|
Fission energy deposition |
|
|
|
Pulse height (energy deposition in a detector) |
|
|
|
Collision heating |
|
|
|
Charge deposition |
|
|
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]