![]() |
sandreco
1.0.8
A modular reconstruction software for SAND
|
Reads the SRTruthBranch produced by truth_filler and produces SRCommonRecoBranch (out_common) and SRNDBranch (out_nd) for downstream CAF consumers (caf_streamer, gauss_smearing/gluckstern_smearing). "Perfect" reco: every field is copied/derived 1:1 from truth, no smearing — gauss_smearing/gluckstern_smearing perturb this output afterwards.
Which particles get a reconstructed object is not decided by PDG alone: see §0 Tracker gate below.
(dashed arrows: cross-reference links, not data copies — recoobj/part are the bidirectional SRRecoParticle ↔ SRTrack/SRShower link, truth is the reco→truth link, both TrueParticleID/SRRecoBaseID/SRRecoParticleID, not nested objects)
A true particle becomes an SRTrack/SRShower only if both hold:
Condition 2 is the gate. It is evaluated by tracker_g4ids_from_edep(), which walks the sand::edep_reader tree once per event and collects the G4IDs of the trajectories with hits in sand::subdetector_t::STT or ::DRIFT — the two tracker technologies, "Straw" and "DriftVolume" in edep-sim's sensitive-detector names (sand::string_to_component, include/common/sand.h). Which one a file uses depends on the simulated geometry, so fast_reco accepts either. particle_slots_from_true() then looks each particle's G4ID up in that sorted vector with std::binary_search, after converting it to a spill id with G4IdMap::to_spill_id(): SRTrueParticle::G4ID is per interaction, the edep-sim ids are per spill (see the truth_filler README, Track ids: interaction vs spill). The same conversion is used to find the trajectory SRTrack::enddir is taken from.
Particles that fail the gate are not dropped: they still get their own SRRecoParticle (with kinematics, parent, daughters), only with recoobj and origRecoObjType left unset. SRRecoParticlesBranch::sandreco therefore always holds exactly one entry per true_ixn.prim/true_ixn.sec, in that order — an invariant the parent/daughters index arithmetic depends on.
Two consequences worth knowing:
TG4HitSegment::Contrib[0]), so a γ or a π0 owns no hits of its own and gets no SRShower. tracker.showers in practice holds e± only, including the e+e− of a conversion, which are trajectories in their own right.tracker had an entry per track-/shower-like PDG whether or not it ever crossed the tracker.Requires sand::edep_reader as a context instance — fast_reco reads it directly, it is not a per-event requirement, so no change to the process' reqs.
Stored in common_reco_branch.ixn.sandreco[i]. Filled directly in fast_reco::run(), one per truth_branch.nu[i].
| Field | Type | Source | Notes |
|---|---|---|---|
id | long int | true_ixn.id | |
vtx | SRVector3D | true_ixn.vtx | Copied, no smearing |
dir | SRDirectionBranch | direction_from_true(true_ixn) | See §2 |
nuhyp | SRNeutrinoHypothesisBranch | neutrino_hypothesis_from_true(true_ixn) | See §3 |
Enu | SRNeutrinoEnergyBranch | energy_from_true(true_ixn) | See §4 |
part | SRRecoParticlesBranch | reco_particles_from_true(true_ixn, tracker_ids, ixn_idx) | See §5 |
truth | vector<size_t> | {ixn_idx} | Index into truth_branch.nu |
truthOverlap | vector<float> | {1.0} | Perfect reco: 100% overlap |
Filled: 8 fields. Not filled (left at default): preselected (false), isFromTrigger (true — correct default, SAND has no cosmic-overlay concept yet).
Filled by direction_from_true(). All four estimators collapse to the same value — a deliberate choice, not a placeholder: since this is a "perfect" reco, every field is a copy of the truth, not a value derived bottom-up from reconstructed particles.
| Field | Source |
|---|---|
calo | normalize(true_ixn.momentum) |
heshw | normalize(true_ixn.momentum) |
lngtrk | normalize(true_ixn.momentum) |
part_mom_sum | normalize(true_ixn.momentum) |
Filled: 4/4 fields.
Filled by neutrino_hypothesis_from_true() — a "perfect classifier": one-hot encoding on the true class instead of a network score.
| Field | Source |
|---|---|
isnubar | 1 if true_ixn.pdg < 0 else 0 |
nc | 1 if !true_ixn.iscc else 0 |
| nue / numu / nutau | 1 for the flavor matching |true_ixn.pdg| when iscc, else 0 | | protons0/1/2/N | One-hot bucket ({0,1,2,≥3}) of true_ixn.nproton | | chgpi0/1/2/N | One-hot bucket of true_ixn.npip + true_ixn.npim | | pizero0/1/2/N | One-hot bucket of true_ixn.npi0 | | neutron0/1/2/N | One-hot bucket of true_ixn.nneutron |
Filled: 21/21 fields.
Filled by energy_from_true(). Same "perfect reco" choice as dir: every estimator collapses to true_ixn.E.
| Field | Source |
|---|---|
calo, lep_calo, mu_range, mu_mcs, mu_mcs_llhd, e_calo, e_had, mu_had, regcnn, part_energy_sum | true_ixn.E |
Filled: 10/10 fields.
Stored in common_reco_branch.ixn.sandreco[i].part.sandreco[j], one per true_ixn.prim/true_ixn.sec entry. Filled by reco_particle_from_true() (+ recoobj/parent/daughters set in reco_particles_from_true()).
| Field | Type | Source | Notes |
|---|---|---|---|
primary | bool | id.type == TrueParticleID::kPrimary | |
pdg | int | true_part.pdg | |
score | float | Hardcoded 1.0 | Perfect PID |
E | float | true_part.p.E | GeV |
p | SRVector3D | {true_part.p.px, py, pz} | GeV/c |
start | SRVector3D | true_part.start_pos | cm |
end | SRVector3D | true_part.end_pos | cm |
origRecoObjType | RecoObjType | kTrack/kShower from the slot, else kUnknownRecoObj | Set together with recoobj, never from the PDG alone — see §0 |
parent | int | Index of the parent's SRRecoParticle in the same part.sandreco, via true_part.parentID | -1 if no parent (primaries) |
daughters | vector<unsigned int> | Reverse of parent: this particle's index, pushed onto its parent's daughters | |
truth | vector<TrueParticleID> | {id} | id = {ixn_idx, kPrimary\|kSecondary, i}, not true_part.ancestor_id |
truthOverlap | vector<float> | {1.0} | |
recoobj | SRRecoBaseID | {ixn_idx, kSANDTrackerTrack\|kSANDTrackerShower, slot.track_idx\|shower_idx} | Only for particles that pass the tracker gate (§0); unset (kUnknown) otherwise |
Filled: 13/17 fields. Not filled: E_method (→ PartEMethod::kCalorimetry when picked up), tgtA (→ from true_ixn.targetPDG). Open question (not answerable without new infrastructure): contained, walldist — no containment query exists in the repo yet.
Stored in the spill-level nd_reco_branch.sand.tracker.tracks[k], one per primary/secondary whose PDG is track-like (μ, π±, K±, p) and that left hits in the tracker (§0). Filled by track_from_true() (+ part set in sand_tracker_from_true()).
| Field | Type | Source | Notes |
|---|---|---|---|
start | SRVector3D | true_part.start_pos | cm |
end | SRVector3D | true_part.end_pos | cm |
dir | SRVector3D | normalize({true_part.p.px, py, pz}) | |
enddir | SRVector3D | = dir | No smearing → start/end direction coincide |
time | double | true_part.time | ns |
E | float | true_part.p.E | GeV — see unit note below |
Evis | float | = E | |
len_cm | float | distance(start, end) | cm |
charge | short int | TDatabasePDG::Instance()->GetParticle(pdg)->Charge() / 3 | 0 if PDG unknown |
qual | float | Hardcoded 1.0 | Perfect quality |
truth | vector<TrueParticleID> | {id} | Same id as the matching SRRecoParticle |
truthOverlap | vector<float> | {1.0} | |
part | SRRecoParticleID | {ixn_idx, kSandreco, slot.part_idx} | Points back to the matching SRRecoParticle |
Filled: 13/14 fields. Not filled: len_gcm2 (length in g/cm² — needs material density integration along the track, not derivable from truth alone).
Stored in the spill-level nd_reco_branch.sand.tracker.showers[k], one per primary/secondary whose PDG is shower-like (e±, γ, π0) and that left hits in the tracker (§0) — in practice e± only, see §0. Filled by shower_from_true() (+ part set in sand_tracker_from_true()).
| Field | Type | Source | Notes |
|---|---|---|---|
start | SRVector3D | true_part.start_pos | cm |
direction | SRVector3D | normalize({true_part.p.px, py, pz}) | |
Evis | float | true_part.p.E | GeV — see unit note below |
truth | vector<TrueParticleID> | {id} | |
truthOverlap | vector<float> | {1.0} | |
part | SRRecoParticleID | {ixn_idx, kSandreco, slot.part_idx} |
Filled: 6/11 fields. Not filled: time, qual, len_cm, initial_dEdx, conversionGap — all require actual shower-shape reconstruction (dE/dx profile, conversion point), not derivable from a truth copy.
| Link | ID type | Direction | Resolution |
|---|---|---|---|
SRTrack/SRShower::part | SRRecoParticleID ({ixn, type=kSandreco, ipart}) | Track/Shower → Particle | sr.common.ixn.sandreco[ixn].part.sandreco[ipart] |
SRRecoParticle::recoobj | SRRecoBaseID ({ixn, type=kSANDTrackerTrack\|kSANDTrackerShower, irecoobj}) | Particle → Track/Shower | sr.nd.sand.tracker.tracks\|showers[irecoobj] — spill-level, ixn is not used to resolve it |
*.truth[i] (all three) | TrueParticleID ({ixn, type=kPrimary\|kSecondary, part}) | any reco object → truth | truth_branch.nu[ixn].prim\|sec[part] |
All three share the same ixn value across common/nd branches by construction: fast_reco appends one SRInteraction and one SRSANDInt per truth_branch.nu[i] in the same loop, so they stay index-aligned. The SRSANDInt itself is left empty (no cross-subdetector trkmatch): the tracks/showers of every interaction are appended to the single spill-level sand.tracker, so irecoobj keeps counting across interactions. nd_reco_branch.sand.ixn has no equivalent of .grain's ID scheme wired up (out of scope — .grain itself is untouched).
SRNDBranch::trkmatch/shwmatch (SRNDTrkAssnBranch/SRNDShwAssnBranch): cross-detector track/shower matching (ND-LAr ↔ TMS ↔ MINERvA ↔ GAr) — no SAND ID field exists in SRNDTrackAssn/SRNDShowerAssn, nothing to populate here without a duneanaobj schema change upstream.
SRTrack::E is documented upstream as MeV (SRTrack.h: ///< Track energy estimate in MeV); fast_reco keeps it in GeV instead, consistent with SRRecoParticle::E and every other energy field it fills — a deliberate choice, not an oversight. Anything consuming SRTrack::E downstream expecting MeV per the upstream doc will read a value 1000× too small.