Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ShipSoft/FairShip/llms.txt

Use this file to discover all available pages before exploring further.

FairShip’s tracking system converts digitised straw tube hits into fully fitted tracks with momentum vectors and covariance matrices. The system is split into three loosely coupled stages: pattern recognition (grouping hits into track candidates), track fitting (estimating track parameters and uncertainties via a Kalman filter), and — optionally — an alternative ACTS-based pipeline. A dedicated benchmarking tool measures the performance of each component quantitatively. This page documents each stage in detail, explains the data flow between them, and shows how to run and interpret the benchmark.

Pattern recognition

python/shipPatRec.py is the central dispatcher for all pattern recognition algorithms. Its public API is a single function:
def execute(smeared_hits, ship_geo, method: str = "") -> dict:
    ...

Input format

smeared_hits is a Python list of dicts, one entry per digitised straw hit:
smeared_hit = {
    "digiHit": key,       # integer index into Digi_strawtubesHits
    "xtop":    xtop,      # x-coordinate of straw wire top endpoint (cm)
    "ytop":    ytop,      # y-coordinate of straw wire top endpoint (cm)
    "z":       ztop,      # z-coordinate of straw wire top endpoint (cm)
    "xbot":    xbot,      # x-coordinate of straw wire bottom endpoint (cm)
    "ybot":    ybot,      # y-coordinate of straw wire bottom endpoint (cm)
    "dist":    dist2wire, # smeared distance from track to wire (cm)
    "detID":   detID,     # detector ID encoding station/layer/straw
}
The dist field holds the drift-distance measurement after applying the spatial resolution smearing controlled by --noStrawSmearing in ShipReco.py. The geometry constants used by all algorithms are set from two module-level globals:
r_scale = 1.0                                         # global scale factor
max_x   = global_variables.ShipGeo.strawtubes_geo.width  # == 200 cm

Output format

execute() returns a dict of track candidates keyed by integer index:
recognized_tracks = {
    0: {"y12":      [hit_dict, ...],   # Y-view hits in stations 1-2
        "stereo12": [hit_dict, ...],   # stereo hits in stations 1-2
        "y34":      [hit_dict, ...],   # Y-view hits in stations 3-4
        "stereo34": [hit_dict, ...]},  # stereo hits in stations 3-4
    1: {...},
    ...
}
Each hit dict in the per-view lists is the same format as the input. Downstream track fitting reads these sub-lists to build GenFit measurement objects.

Algorithm selection

The method argument routes to the appropriate algorithm:
if method == "TemplateMatching":
    recognized_tracks = template_matching_pattern_recognition(smeared_hits, ship_geo)
elif method == "FH":
    recognized_tracks = fast_hough_transform_pattern_recognition(smeared_hits, ship_geo)
elif method == "AR":
    recognized_tracks = artificial_retina_pattern_recognition(smeared_hits, ship_geo)
else:
    # Fallback: assign all hits to a single track candidate split by view
    hits_y12, hits_stereo12, hits_y34, hits_stereo34 = hits_split(smeared_hits)
    recognized_tracks[0] = {
        "y12": hits_y12, "stereo12": hits_stereo12,
        "y34": hits_y34, "stereo34": hits_stereo34,
    }
The AR algorithm — activated by --patRec AR in ShipReco.py — is the production default. It uses a retina scoring function that accumulates votes for (slope, intercept) bins from each hit pair. The bin with the highest accumulated score seeds a track candidate; hits within a tolerance band around the seed direction are then assigned to it. The process repeats after removing assigned hits until no high-scoring bins remain.AR is robust in medium-multiplicity events (typical SHiP signal topologies) and produces low ghost and clone rates.

Track fitting

Track candidates from pattern recognition are fitted by python/shipStrawTracking.py using the GenFit2 DAF (Deterministic Annealing Filter), a robust variant of the Kalman filter that down-weights outlier measurements iteratively.

GenFit2 setup

Before the event loop, ShipReco.py initialises three singleton objects that GenFit requires:
# Material effects interface — must be created before any tracking
geoMat = ROOT.genfit.TGeoMaterialInterface()

# Magnetic field interface
bfield = ROOT.genfit.FairShipFields()
bfield.setField(fieldMaker.getGlobalField())
fM = ROOT.genfit.FieldManager.getInstance()
fM.init(bfield)

# Material effects singleton
ROOT.genfit.MaterialEffects.getInstance().init(geoMat)
genfit.TGeoMaterialInterface() must be created immediately after the TGeo geometry is loaded and before any pattern recognition or fitting call. Creating it later causes a crash due to uninitialised geometry nodes.

Measurement construction

For each straw hit in a candidate, the fitter constructs a genfit::WireMeasurement from the straw wire endpoints (top and bottom) and the drift distance. Wire measurements encode the left-right ambiguity inherent in drift chambers; DAF resolves this ambiguity by simultaneously fitting both hypotheses and weighting by consistency.

Output

Each fitted track is stored as a genfit::Track* in the FitTracks branch. The following accessors are most commonly used in analysis:
for key, atrack in enumerate(sTree.FitTracks):
    fitStatus   = atrack.getFitStatus()
    if not fitStatus.isFitConverged():
        continue
    ndf         = fitStatus.getNdf()
    chi2        = fitStatus.getChi2()
    fittedState = atrack.getFittedState()
    pos         = fittedState.getPos()       # ROOT.TVector3
    mom         = fittedState.getMom()       # ROOT.TVector3
    cov6D       = fittedState.get6DCov()     # 6×6 covariance matrix
    pdg         = fittedState.getPDG()
    p_mag       = fittedState.getMomMag()
The fitTrack2MC vector maps each fitted-track index to the MC particle index:
mc_key = sTree.fitTrack2MC[key]   # -1 means ghost (no MC match)
if mc_key >= 0:
    mc_part = sTree.MCTrack[mc_key]

Vertex finding

python/shipVertex.Task is the vertex-finding module called from ShipDigiReco.reconstruct(). It loops over all pairs of tracks in goodTracks, extrapolates each track to an initial closest-approach estimate, then iterates until the vertex Z position converges to within 0.1 cm.
class Task:
    def __init__(self, hp, sTree, mcTree=None) -> None:
        self.sTree = sTree
        self.fPartArray = ROOT.std.vector("ShipParticle")()
        self.Particles  = self.sTree.Branch("Particles", self.fPartArray)
        ...

    def execute(self) -> None:
        self.TwoTrackVertex()
Each accepted vertex candidate is stored as a ShipParticle with its 4-momentum, production vertex, DOCA, and vertex covariance matrix. Vertex finding can be disabled entirely with --noVertexing in ShipReco.py.

ACTS integration (experimental)

python/ACTSReco.py provides an alternative tracking backend using the ACTS framework. It is invoked via the dedicated script macro/run_ACTSTracking.py — it is not a --patRec option in ShipReco.py. The ACTS script has its own argument set:
python macro/run_ACTSTracking.py \
    -f sim_my-run.root \
    -g geo_my-run.root \
    --detector StrawTracker \
    --realPR \
    --vertexing
Key flags for run_ACTSTracking.py:
ArgumentDefaultDescription
-f / --inputFilerequiredInput simulation ROOT file
-g / --geoFilerequiredGeometry ROOT file
-n / --nEvents1 000 000Maximum events to process
-o / --outputDirCWDOutput directory
--detectorSiliconTargetDetector geometry: SiliconTarget, MTC, SND, StrawTracker
--realPRFalseEnable pattern recognition
--vertexingFalseEnable vertex fitting
--DQMFalseEnable ACTS DQM histograms
--minHits6Minimum hits to build a valid track
--minPt0Minimum transverse momentum (GeV)
The ACTS pipeline uses the following ACTS example components:
from acts.examples.reconstruction import (
    addKalmanTracks,
    addSeeding,
    addVertexFitting,
    SeedingAlgorithm,
    TrackSmearingSigmas,
    VertexFinder,
)
from acts.examples.simulation import (
    addDigitization,
    addDigiParticleSelection,
    ParticleSelectorConfig,
)
Four detector geometries are accepted by --detector; three have builders implemented in ACTSReco.py:
--detector valueBuilder classField
StrawTrackeracts.examples.StrawtubeBuilderFull FairShip field map
MTCacts.examples.MTCBuilderConstant B = −1.2 T (y)
SiliconTargetacts.examples.SiTargetBuilderZero field
SND(not yet implemented in ACTSReco.py)
ACTS support requires a FairShip build with ACTS enabled. Check that import acts succeeds in your environment before using run_ACTSTracking.py. The output file is named {inputfile_basename}_tracked.root and written to the directory specified by --outputDir.

Tracking benchmark

macro/run_tracking_benchmark.py orchestrates a complete performance measurement by running simulation, reconstruction, and metric computation as separate subprocesses (required because FairRoot singletons prevent multiple FairRunSim instances in one process).

Running the benchmark

# 200-event benchmark with a fixed seed
python macro/run_tracking_benchmark.py \
    -n 200 \
    --seed 42 \
    --tag ci-benchmark \
    --output-json tracking_metrics.json

# 1000-event multi-track benchmark
python macro/run_tracking_benchmark.py \
    -n 1000 \
    --nTracks 5 \
    --tag multi-track \
    --output-json multi_metrics.json

# Via pixi (uses pre-configured settings for CI)
pixi run ci-tracking-benchmark

Benchmark arguments

ArgumentDefaultDescription
-n / --nEvents1000Number of simulated events
--pID13PDG ID of gun particle (13 = µ⁻)
--Estart1.0 GeVLow end of energy range
--Eend100.0 GeVHigh end of energy range
--Vz8300 cmGun z-position (~1 m upstream of T1)
--Dx200 cmTransverse spread in x
--Dy300 cmTransverse spread in y
--nTracks1Tracks per event
--tagbenchmarkOutput file name prefix
--output-json{outputDir}/tracking_metrics.jsonJSON metrics output path (defaults to outputDir if not set)
--seed42Random seed
-o / --outputDir.Directory for all output files
--debug0FairLogger verbosity level: 0=info, 1=debug, 2=debug1, 3=debug2

Output files

FileContents
sim_{tag}.rootParticle-gun simulation
geo_{tag}.rootGeometry file
sim_{tag}_rec.rootReconstruction output
tracking_benchmark_histos.rootDiagnostic histograms
tracking_metrics.jsonNumerical performance metrics

Metrics computed by tracking_benchmark.py

python/tracking_benchmark.py defines TrackingBenchmark, which compares reconstructed tracks against MC truth using the following parameters:
class TrackingBenchmark:
    def __init__(
        self,
        sim_file:    str,
        reco_file:   str,
        geo_file:    str,
        purity_cut:  float = 0.70,   # min hit-purity fraction for a match
        min_hits:    int   = 25,     # min straw hits for reconstructibility
        min_stations:int   = 3,      # min tracking stations for reconstructibility
    ) -> None:

Track finding efficiency

Fraction of reconstructible MC tracks for which at least one reco track satisfies the purity cut. Reconstructibility requires ≥ min_hits straw hits and ≥ min_stations stations crossed. Uncertainty is computed via the Wilson score interval (wilson_interval(k, n)) at 1σ.

Clone rate

Fraction of reco tracks matched to an MC track that was already claimed by another reco track with a higher purity score. A clone indicates that a single real track was reconstructed twice.

Ghost rate

Fraction of reco tracks that do not satisfy the purity cut with any MC track. Ghosts arise from combinatorial fake track candidates in the pattern recognition.

Momentum resolution

σ(Δp/p) from a Gaussian fit to the relative momentum residual distribution of matched tracks. Stored in the JSON metrics file and as a histogram in tracking_benchmark_histos.root.

Wilson score interval

The wilson_interval(k, n) function in tracking_benchmark.py computes the 68% (1σ) half-width of the Wilson score confidence interval for a binomial proportion:
def wilson_interval(k: int, n: int) -> float:
    if n == 0:
        return 0.0
    z = 1.0  # 1-sigma
    p = k / n
    denom = 1 + z**2 / n
    spread = z * math.sqrt(p * (1 - p) / n + z**2 / (4 * n**2)) / denom
    return spread
This is preferred over a naive binomial error because it remains well-behaved when the efficiency is close to 0 or 1 and for small sample sizes.

Interpreting the JSON output

{
  "efficiency":          0.923,
  "efficiency_err":      0.012,
  "clone_rate":          0.008,
  "ghost_rate":          0.041,
  "momentum_resolution": 0.017,
  "n_events":            200,
  "n_reconstructible":   384,
  "n_matched":           354
}
For CI regression testing, compare efficiency and ghost_rate against a known-good baseline stored alongside the benchmark JSON. The Wilson error efficiency_err gives you a principled threshold for declaring a regression: a drop of more than 2–3σ in efficiency on the same sample warrants investigation.

Build docs developers (and LLMs) love