Skip to main content

Particles, four-vectors, and selecting the electron

REC::Particle is where an analysis starts. Each row is one reconstructed particle:

columnmeaning
pidPDG code — the reconstruction's identity guess
px, py, pzmomentum components (GeV), lab frame
vx, vy, vzvertex position (cm) — where the track came from
vtvertex time (ns)
charge−1, 0, +1
betav/cv/c from time-of-flight — the hadron-PID workhorse
chi2pidhow well the timing matches the assigned pid (smaller = better)
statuswhich detector system, encoded (see below)

Four-vectors

Momentum plus a mass gives a four-vector, and four-vectors give you everything else — energy, invariant mass, boosts. There's no dedicated type in the data; you build it from the columns. A couple of tiny helpers keep the rest of the tutorial readable:

import awkward as ak
import numpy as np

def p3(part): # |p|
return np.sqrt(part.px**2 + part.py**2 + part.pz**2)

def energy(part, mass): # E = sqrt(p² + m²)
return np.sqrt(part.px**2 + part.py**2 + part.pz**2 + mass**2)

def theta(part): # polar angle (rad)
return np.arccos(part.pz / p3(part))

These work on a single particle or a whole jagged column — that's the point of Awkward. Everything downstream is array math over all events at once.

Real analyses use vector

The scikit-hep vector library adds proper Momentum4D records with .mass, .boost, .deltaphi, etc., and integrates with Awkward. We keep it explicit here so you see the arithmetic; on real work, vector.zip({"px":…, "py":…, "pz":…, "E":…}) is worth it.

status: which detector, and the trigger

status encodes the detector system a particle was reconstructed in. The magnitude tells you the region:

region = np.abs(p.status) // 1000 # 1 = Forward Tagger, 2 = Forward Detector, 4 = Central
  • Forward Detector (|status| in 2000–3999): tracked in the drift chambers, seen by HTCC + calorimeters. Where the scattered electron lives.
  • Central Detector (|status| ≥ 4000): tracked in the solenoid CVT.
  • Forward Tagger (|status| in 1000–1999): very forward, electrons/photons.

The trigger electron — the scattered electron that fired the data-acquisition trigger — is a Forward-Detector electron, and in a DST it is conventionally the first REC::Particle row. The robust way to pick it (and what you'd do on real data) is: take Forward-Detector electrons and keep the highest-momentum one.

Selecting the scattered electron

p = f.arrays("REC::Particle", ["pid", "px", "py", "pz", "vz", "charge", "beta", "chi2pid", "status"])

is_e = p.pid == 11
in_fd = (np.abs(p.status) >= 2000) & (np.abs(p.status) < 4000)
electrons = p[is_e & in_fd] # jagged: FD electrons per event

# require at least one, then take the highest-momentum electron per event
has_e = ak.num(electrons.pid) > 0
electrons = electrons[has_e] # drop events with no FD electron
lead = ak.argmax(p3(electrons), axis=1, keepdims=True)
ele = ak.firsts(electrons[lead]) # one electron record per surviving event

len(ele) # events with a scattered-electron candidate
p3(ele) # its momentum, one value per event

ak.argmax(..., keepdims=True) then ak.firsts is the idiom for "the row that maximises something, per event": argmax gives the index, keepdims keeps the jagged shape so the index selects correctly, and firsts unwraps the length-1 list to a plain per-event record.

Alignment

Once you filter events (here, has_e), every array you compare must be filtered the same way, or they won't line up. A clean pattern is to compute all your per-event masks first, combine them into one event mask, and apply it once at the end.

Other particles

The same masking picks out anything:

photons = p[p.pid == 22]
pips = p[p.pid == 211] # π⁺
protons = p[p.pid == 2212]

# quick momentum histogram of every π⁺ in the file
import numpy as np
pip_p = ak.to_numpy(ak.flatten(p3(pips)))
np.histogram(pip_p, bins=40, range=(0, 6))

For charged hadrons, identity from pid alone is only as good as the timing; the honest separation of π+\pi^+/K+K^+/pp uses beta vs momentum, and you'll often re-derive it yourself. For electrons and photons, the calorimeter and Cherenkov banks are the real discriminators — which is the next chapter but one.

But first, with a clean scattered electron in hand, we can already do real physics: the inclusive DIS kinematics.

Inclusive DIS →