Cross-Bank Linking via pindex¶
Every detector-level CLAS12 bank carries a pindex column that points back to a row in
REC::Particle. Joining the two is the single most common non-trivial operation in
CLAS12 analysis — it's how you answer "how much energy did the electron deposit in the
calorimeter?", "did this pion fire the HTCC?", "what was the track's entry position into
the ECAL?".
The data model¶
flowchart LR
P0["REC::Particle<br/>row 0: electron"]
P1["REC::Particle<br/>row 1: π⁺"]
P2["REC::Particle<br/>row 2: proton"]
C0["REC::Calorimeter row 0<br/>pindex=0, PCAL, E=0.6"]
C1["REC::Calorimeter row 1<br/>pindex=0, ECin, E=1.8"]
C2["REC::Calorimeter row 2<br/>pindex=0, ECout, E=0.4"]
C3["REC::Calorimeter row 3<br/>pindex=1, PCAL, E=0.02"]
C0 -->|pindex=0| P0
C1 -->|pindex=0| P0
C2 -->|pindex=0| P0
C3 -->|pindex=1| P1
Key properties:
- One-to-many. A single particle can produce multiple rows in a detector bank (e.g.,
the electron above lights up PCAL, ECin, and ECout — three rows, all with
pindex=0). - Some particles produce no rows. A forward-tagger photon will have no row in
REC::Scintillator. A neutral may have no row inREC::Track. pindexcan be −1. Some detector rows are not associated with anyREC::Particlerow (e.g., unmatched tracks). Filter these out before use.
The idiom: bank::getRowListLinked¶
HIPO has a built-in helper for the join. Given a particle row, it returns the list of
rows in another bank whose pindex column equals that row:
pindex_column is the integer index of the pindex column — getRowListLinked
takes a column index, not a name. Resolve it from the detector bank's schema with
getSchema().getEntryOrder("pindex") once per event, outside the row loop. See
Performance — cache column indices.
Full example: total calorimeter energy of the electron¶
Sum the energy of every PCAL + EC hit linked to a candidate electron.
#include "chain.h"
#include "twig.h"
#include <cmath>
#include <cstdio>
int main(int argc, char** argv) {
if (argc < 2) { std::fprintf(stderr, "usage: %s <file.hipo>\n", argv[0]); return 1; }
hipo::chain ch;
ch.add(argv[1]);
twig::h1d hEtot(100, 0.0, 8.0);
for (auto& [event, file_idx, event_idx] : ch) {
auto parts = event.getBank("REC::Particle");
auto calo = event.getBank("REC::Calorimeter");
// getRowListLinked() needs the pindex column as an integer index;
// resolve it once per event, outside the row loop.
const int calo_pindex = calo.getSchema().getEntryOrder("pindex");
for (int row = 0; row < parts.getRows(); row++) {
// Forward-detector electron only.
if (parts.getInt("pid", row) != 11) continue;
int s = std::abs(parts.getInt("status", row));
if (s < 2000 || s >= 4000) continue;
// Sum energy of every REC::Calorimeter row pointing at this particle.
float E = 0.0f;
for (int cRow : calo.getRowListLinked(row, calo_pindex)) {
E += calo.getFloat("energy", cRow);
}
hEtot.fill(E);
}
}
hEtot.print();
return 0;
}
What's happening:
- The sequential
chainwalks every event; each iteration hands you achain_event. event.getBank("REC::Particle")andevent.getBank("REC::Calorimeter")look both banks up by name in the file's dictionary — no pre-built schema needed.getRowListLinked()takes thepindexcolumn as an integer index, so resolve it from the calorimeter schema once per event, before the row loop.- Loop particles, keep Forward-Detector electrons (
|status|in[2000, 4000)), and usegetRowListLinked()to fetch every calorimeter row belonging to that electron. Sumenergyacross those rows.
Linking to multiple detector banks¶
The same pattern extends to REC::Scintillator, REC::Cherenkov, REC::Track, and
REC::Traj — they all have a pindex column. Cache the index once per bank:
hipo::chain ch;
ch.add("data.hipo");
const int HTCC_ID = 15; // org.jlab.detector.base.DetectorType
const int FTOF_ID = 12;
for (auto& [event, file_idx, event_idx] : ch) {
auto parts = event.getBank("REC::Particle");
auto calo = event.getBank("REC::Calorimeter");
auto cher = event.getBank("REC::Cherenkov");
auto scin = event.getBank("REC::Scintillator");
// getRowListLinked() needs each pindex column as an integer index.
const int calo_pindex = calo.getSchema().getEntryOrder("pindex");
const int cher_pindex = cher.getSchema().getEntryOrder("pindex");
const int scin_pindex = scin.getSchema().getEntryOrder("pindex");
for (int row = 0; row < parts.getRows(); row++) {
if (parts.getInt("pid", row) != 11) continue;
// Total calorimeter energy for this electron
float ECAL = 0.0f;
for (int r : calo.getRowListLinked(row, calo_pindex))
ECAL += calo.getFloat("energy", r);
// Total HTCC photo-electrons
int nphe = 0;
for (int r : cher.getRowListLinked(row, cher_pindex))
if (cher.getInt("detector", r) == HTCC_ID)
nphe += cher.getInt("nphe", r);
// FTOF hit time (first match)
float tof = -1.0f;
for (int r : scin.getRowListLinked(row, scin_pindex)) {
if (scin.getInt("detector", r) == FTOF_ID) {
tof = scin.getFloat("time", r);
break;
}
}
// … use ECAL, nphe, tof for your cuts …
}
}
Linking the other way: which particle produced this hit?¶
Sometimes you want to start from a detector row and find the particle it belongs to.
That's just the pindex value itself:
for (auto& [event, file_idx, event_idx] : ch) {
auto parts = event.getBank("REC::Particle");
auto calo = event.getBank("REC::Calorimeter");
for (int cRow = 0; cRow < calo.getRows(); cRow++) {
int part_row = calo.getInt("pindex", cRow);
if (part_row < 0 || part_row >= parts.getRows()) continue; // unmatched / bad link
int pid = parts.getInt("pid", part_row);
// …
}
}
Always bounds-check pindex
pindex is a short, and nothing in the library checks that it's a valid row
in REC::Particle. In rare cases (corrupt events, pipeline bugs) it can point past
the end of the particle bank. Guard with
pindex >= 0 && pindex < parts.getRows() before indexing.
Performance¶
In a real CLAS12 analysis loop:
- Resolve column indices outside the row loop.
getSchema().getEntryOrder(name)costs a map lookup, so call it once per event — as the examples do forpindex— not once per row. With the sequentialchainyou get a fresh bank each event, so the top of the loop body is the natural place to resolve any index the inner loops need. - If you loop over a large number of particles × detector banks per event,
getRowListLinked()is O(n) in the detector bank's row count. For extreme cases you can pre-bucket detector rows bypindexyourself, but for typical CLAS12 event multiplicities the built-in helper is fast enough.
See Recipes — Row filtering for filtering patterns and Performance — Optimization Tips for more on caching.
Next¶
- Put it all together in a First Analysis.
- The full column catalogue of every
REC::*bank → The REC:: bank family. - Generic row-list operations (filter, manual lists, reset) → Recipes — Row filtering.