Skip to content

Expressions and selections

Variables, selections and weights are all expressions: strings in Python syntax evaluated with NumPy/Awkward semantics over the branches of the tree.

Syntax

Feature Example
Branch names Muon_pt, MET
Names that are not identifiers `jet1_b-tag` > 0.5 (backticks)
Arithmetic sqrt(px**2 + py**2), MET / 1000, -eta
Comparisons pt > 20, charge != 0, 20 < pt < 100 (chained)
Boolean logic pt > 20 and abs(eta) < 2.5, not isTight, or &, \|, ~
Indexing Muon_pt[:, 0], Muon_pt[Muon_pt > 20]
Constants pi, e, inf, nan, True, False

and, or, not and chained comparisons are rewritten to element-wise operations, so they work on arrays. Dotted names such as ReconstructedParticles.momentum.x are read as one branch name (see below); any other attribute access, lambdas, comprehensions, string literals and calls to anything but the functions below are rejected at parse time with an ExpressionError. An unknown branch raises MissingBranchError with close-match suggestions.

Functions

Element-wise (NumPy): abs, sqrt, cbrt, exp, expm1, log, log10, log2, log1p, power, hypot, sin, cos, tan, arcsin, arccos, arctan, arctan2, sinh, cosh, tanh, arcsinh, arccosh, arctanh, deg2rad, rad2deg, floor, ceil, round, trunc, sign, minimum, maximum, clip, isnan, isinf, isfinite, where.

Per-event reductions over jagged branches (they reduce the innermost list, i.e. over the objects of each event, like ROOT's Length$, Sum$, Max$):

Function Meaning
count(x), len(x) number of objects
sum(x), prod(x) sum / product over objects
min(x), max(x) extreme value (None for empty events)
mean(x), std(x) mean / standard deviation over objects
any(x), all(x) boolean reductions
argmin(x), argmax(x) index of the extreme object
first(x) the leading object (None for empty events)

Kinematics from Cartesian components, as stored by EDM4hep (momentum.x/y/z, energy) and many flat ntuples (px, py, pz, E):

Function Meaning
pt(px, py), p(px, py, pz) transverse and total momentum
theta(px, py, pz), costheta(px, py, pz) polar angle and its cosine
eta(px, py, pz), phi(px, py) pseudorapidity and azimuth
mass(E, px, py, pz) invariant mass (0 for space-like input)

Reductions applied to a flat (per-event) branch raise an error.

Sub-branches of object collections

podio/EDM4hep and other files with split object branches list their leaves as Collection.field.component. Write them as they appear, dots included:

rf.plot(
    f,
    "pt(ReconstructedParticles.momentum.x, ReconstructedParticles.momentum.y)",
    selection="abs(costheta(ReconstructedParticles.momentum.x, "
    "ReconstructedParticles.momentum.y, ReconstructedParticles.momentum.z)) < 0.9",
)

Names with other unusual characters (spaces, -) go in backticks: `jet1_b-tag` > 0.5.

Per-event versus per-object

A branch is per-event (flat, depth 1) or per-object (jagged, depth 2 or more). Every expression inherits the structure of the branches it uses: Muon_pt > 20 is per-object, count(Muon_pt) >= 2 is per-event, and Muon_pt > 20 and MET > 50 is per-object (the per-event part broadcasts).

The rules for combining a variable with a selection:

Variable Selection Result
per-event per-event events passing the selection
per-object per-event all objects of the passing events, flattened
per-object per-object objects passing the selection, flattened
per-object per-object of another collection SelectionError
per-event per-object SelectionError: reduce with any(), all() or count()

The last row is where TTree::Draw silently fills the per-event value once per passing object; rootfig refuses and tells you how to be explicit:

rf.plot(f, "MET", selection="any(Muon_pt > 20)")  # events with a hard muon
rf.plot(f, "MET", selection="sum(Muon_pt > 20) >= 2")  # at least two hard muons

(count(x) is the number of objects, sum(mask) the number of objects passing; count(Muon_pt > 20) would count all muons.) Fixed-size branches (float x[3]) and two-dimensional NumPy arrays are per-object like variable-length lists.

Weights

Variable Weight Result
per-event per-event one weight per event
per-object per-event the event weight is broadcast to each object
per-object per-object (same collection) one weight per object
per-event per-object IncompatibleWeightError

Weights defined on a Sample and passed to plot() are multiplied; Sample(scale=...) multiplies a constant in as well. A blank weight string means no weight. Bin variances are always the sums of squared weights (hist Weight storage).

Missing and non-finite values

None values (e.g. max(Muon_pt) of an event with no muons) never enter a histogram; in a selection they count as False. nan and inf values are dropped with a RootfigWarning telling you how many, or raise with nonfinite="error". Both counts are recorded in the histogram's stats.

Selections in load()

rf.load returns arrays for events, so its selection must be per-event. Apply object-level cuts inside the expressions instead:

rf.load(f, {"hard_muons": "Muon_pt[Muon_pt > 20]"}, selection="nMuon > 0")

Composing cuts

Cut objects combine with &, | and ~ and keep an optional label:

base = rf.Cut("nMuon >= 2", label="2 muons")
signal_region = base & "abs(Muon_eta) < 2.4" & ~rf.Cut("has_bjet")

Plain strings are accepted wherever a Cut is.