Skip to main content
Track reconstruction runs after signal processing on each module. The entry point is charge_reco in workflow.py, followed by charge_reco_whole for cross-module steps. The key modules are hit_finder.py, track_2d.py, track_3d.py, stitch_tracks.py, and single_hits.py.

Hit Finding

hf.find_hits()
Hit finding extracts time-bounded pulse objects from each channel’s ROI. The ROI mask (inverted mask_daq) defines candidate regions. Only regions with at least dt_min = 10 samples are considered. Algorithm per channel:
  1. Identify contiguous ROI intervals using np.diff on the boolean mask.
  2. For induction channels, adjacent positive and negative ROI intervals separated by fewer than merge_tdc_thr = 10 samples are merged into a single candidate.
  3. The ADC excerpt is passed to hit_search, which dispatches to either hit_search_collection_nb or hit_search_induction_nb depending on the view type.
  4. Padding is added to each found hit:
    • Left: up to pad_left = 10 samples (stops at another hit boundary)
    • Right: up to pad_right = 20 samples
Amplitude thresholds:
"hit_finder": {
  "dt_min": 10,
  "min_thr": 0.5,
  "coll": { "amp_sig": [2.5, 5.5] },
  "ind":  { "amp_sig": 2.0, "merge_tdc_thr": 10 },
  "pad":  { "left": 10, "right": 20 }
}
  • Collection view: two thresholds used by the algorithm — thr1 = 2.5 × RMS (lower, starts the hit) and thr2 = 5.5 × RMS (used to split overlapping pulses).
  • Induction view: single threshold thr3 = 2.0 × RMS for bipolar hit detection.
  • min_thr = 0.5 ADC sets an absolute floor so that thresholds cannot go below this value even on very quiet channels.
After hit finding, hits are sorted by time and position, their charge is computed by integrating the ADC waveform over the padded window, and positions are converted from (channel, TDC) to physical coordinates using the drift velocity.

R-Tree Spatial Index

clu.hits_rtree([cf.imod])
Immediately after hit finding, a 4-dimensional R-tree index is built over all hits in the current module:
dc.rtree_hit_idx.insert(
    h.ID,
    (h.module, h.view, h.X, min(h.Z_start, h.Z_stop),
     h.module, h.view, h.X, max(h.Z_start, h.Z_stop))
)
The index coordinates are (module, view, X-position, Z-position). This index is queried throughout 2D tracking and single-hit finding for fast range searches.

2D Track Finding (Hough Transform)

trk2d.find_tracks_hough([cf.imod])
Tracks are found independently in each wire view. The algorithm proceeds in two phases: seeding via Hough transform, followed by extension with a Kalman-style filter. Phase 1: Hough seeding For each unvisited hit, the algorithm retrieves all nearby hits within a window of hough_win_X = 10 cm in X and hough_win_Z = 20 cm in Z using the R-tree. At least hough_n_min = 5 hits must be present to proceed. The Hough transform is computed over these neighbouring hits:
thetas = np.deg2rad(np.arange(-90.0, 90.0, theta_res))  # theta_res = 1.0 degree
rhos = np.arange(-diag_len, diag_len + 1, rho_res)       # rho_res = 0.25 cm
The best line through the query hit is found by maximising the accumulator score. Lines with score below hough_min_score = 4 are rejected. Phase 2: Kalman-style extension Once a seed is found, hits are added one at a time in order of proximity. A hit is accepted if:
  • Its Kalman chi2 improvement is below chi2cut = 8.0
  • It is not further than max_gap = 2.0 cm from the predicted position
  • Its direction is consistent with the current track slope
After extension completes, refilter_and_find_drays runs a spline fit on the assembled hits and identifies delta-ray candidates using:
  1. A minimum spanning tree (MST) to find branching points
  2. Distance from the spline (dray_thr = 2.0 cm to dray_dmax = 6.0 cm) to classify hits as delta rays
Tracks with fewer than min_nb_hits = 5 hits or slopes exceeding slope_max = 50 are discarded. Default parameters:
"track_2d": {
  "min_nb_hits": 5,
  "chi2cut": 8.0,
  "hough_win_X": 10.0,
  "hough_win_Z": 20.0,
  "hough_n_min": 5,
  "hough_theta_res": 1.0,
  "hough_rho_res": 0.25,
  "hough_min_score": 4,
  "max_gap": 2.0,
  "slope_max": 50
}

2D Track Stitching (Within Module)

stitch.stitch2D_in_module([cf.imod])
After 2D tracking, track segments that belong to the same physical track but were reconstructed separately are merged. For each pair of 2D tracks in the same view, three compatibility tests are applied:
  1. Alignment: The dot product of the direction vectors must exceed align_thr = 0.98.
  2. Distance of minimum approach (DMA): All four endpoint-to-segment distances must be below dma_thr = 3.0 cm (for overlapping tracks).
  3. Endpoint distance: Track endpoints must be within dist_thr = 10.0 cm (for non-overlapping tracks).
Compatibility is determined per-track-pair using tracks2D_compatibility, and connected components in the compatibility graph are merged together.
"stitching_2d": {
  "in_module": {
    "align_thr": 0.98,
    "dma_thr": 3.0,
    "dist_thr": 10.0
  }
}
A second, looser stitching mode (from_3d) is also available with relaxed thresholds (align_thr = 0.96, dma_thr = 8.0, dist_thr = 15.0) and is invoked from inside the 3D builder when two same-view tracks appear to belong to the same 3D track.

3D Track Reconstruction

trk3d.find_track_3D_rtree_new([cf.imod])
3D tracks are built by combining one 2D track from each of the three wire views. Step 1: R-tree overlap search For each 2D track, the algorithm searches the R-tree for tracks in the other two views that overlap in Z (drift time) within a tolerance of trk_ztol = 3.0 cm. Only tracks satisfying minimum quality cuts are considered:
  • Straight-line length ≥ len_min = 2.0 cm
  • Z extent ≥ trk_min_dz = 15.0 cm
  • X extent ≥ trk_min_dx = 8.0 cm
Step 2: Graph-based matching All combinations of three-view overlapping tracks are tested. Compatible sets are recorded, and a sparse adjacency matrix is built. Connected components give the final sets of 2D tracks to combine into each 3D track. Step 3: Trajectory computation (complete_trajectories) For each matched set of three 2D tracks, 3D positions are computed iteratively: each point on one track is combined with the splined position of the other tracks at the same Z, solving the 2×2 linear system from wire view geometry:
A = np.array([[-cos(ang_track), cos(ang_other)],
              [-sin(ang_track), sin(ang_other)]])
xy = A.dot([pos_spl, pos]) / D
Step 4: Track quality check (check_track_3D) DBSCAN is applied to all 3D points (with eps = 10, min_samples = 1). Tracks with more than 2 significant clusters are rejected. Outlier hits from small clusters are removed. Step 5: Track finalisation (finalize_3d_track) Splines are fitted to the 3D trajectory in Z slices, and the average cross-view point spread d_match is computed. Tracks exceeding d_slice_max = 20 cm are rejected.
"track_3d": {
  "trk_ztol": 3.0,
  "len_min": 2.0,
  "trk_min_dz": 15.0,
  "trk_min_dx": 8.0,
  "min_z_overlap": 10.0,
  "goodness": { "eps": 10, "n_min": 10, "d_slice_max": 20 }
}

3D Track Reconstruction with Missing View

trk3d.find_3D_tracks_with_missing_view([cf.imod])
After the main 3D builder, a second pass attempts to reconstruct 3D tracks when exactly one view is missing. Only track combinations with exactly one None view are considered. Additional quality requirements specific to this mode:
"missing_view": {
  "d_thresh": 0.5,
  "min_z_overlap": 10.0,
  "trk_min_dz": 15.0,
  "r_z_thr": 0.8,
  "q_thr": 0.15
}
  • r_z_thr = 0.8: the ratio of Z overlap to total Z extent must exceed this value.
  • q_thr = 0.15: each track must contribute at least 15% of the total charge in the overlap region.

Cross-Module Track Stitching

After all individual modules have been processed, charge_reco_whole performs detector-level 3D stitching. Module-to-module stitching (stitch3D_across_modules): Tracks near module boundaries (within boundary_tol = 5.0 cm) are tested for pairwise compatibility:
  • Endpoint distance: at least one pair of endpoints within dist_thr = 8.0 cm
  • Alignment: direction vector dot product above align_thr = 0.98
Compatible tracks are merged into cross-module tracks with a module_crosser flag set at the crossing point. Cathode stitching (stitch3D_across_cathode): Tracks from opposite drift volumes are tested as cathode-crossers. The test checks alignment plus two cases:
  • Early/on-time: The absolute Z distance between endpoints is within dz_thresh = 10.0 cm, and the X/Y offsets are within dx_thresh = 10.0 cm and dy_thresh = 10.0 cm.
  • Late: Both endpoints are near the cathode plane (maximum drift distance), and the X/Y displacement ratio matches the expected geometric relationship from the track angle.
Cathode-crossing pairs are flagged but not merged; they remain as two separate track objects linked by a cathode_crosser_ID.
"stitching_3d": {
  "module": { "dist_thr": 8.0, "align_thr": 0.98, "boundary_tol": 5.0 },
  "cathode": { "dx_thresh": 10.0, "dy_thresh": 10.0, "dz_thresh": 10.0,
               "align_thresh": 0.96, "boundary_tol": 5.0 }
}
Detector-specific stitching order:
DetectorModule stitchingCathode stitching
PDHD[0,1] then [2,3][[0,1], [2,3]]
PDVD[0,1] then [2,3][[2,3], [0,1]]

Track Timing

tmg.compute_all_track_timing()
After stitching, absolute timing is computed for all 3D tracks. This uses the reconstructed track endpoints relative to the anode and cathode positions to determine the event time t0.

Single-Hit (Blip) Finder

sh.single_hit_finder([cf.imod])
After track reconstruction, free hits (not assigned to any 2D or 3D track) are clustered and matched across views to find isolated charge depositions (blips). Step 1: Per-view DBSCAN clustering For each view, free hits are clustered in (X, Z) space using DBSCAN with cluster_eps = 2.0 cm. Clusters with more than max_per_view = 3 hits are discarded as being too large to be a point-like deposition. Step 2: Cross-view time matching For each clustered free hit, an R-tree search finds hits in the other two views that overlap in time. View-level compatibility checks filter the candidates. Step 3: 3D position resolution Compatible hits from all three views are passed to h3d.compute_xy to solve for the 3D intersection point. Combinations where any view hit is more than outlier_dmax = 2.5 cm from the median position are rejected. Step 4: Veto and storage A spatial veto checks for other activity within dist_veto = 5.0 cm of the candidate blip using the R-tree. Single hits with a barycentric spread exceeding max_bary = 20.0 cm are discarded. Passing candidates are stored as singleHits objects with their 3D position, charge, and distances to the nearest 3D track.
"single_hit": {
  "max_per_view": 3,
  "outlier_dmax": 2.5,
  "cluster_eps": 2.0,
  "dist_veto": 5.0,
  "max_bary": 20.0
}

Build docs developers (and LLMs) love