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:
The pindex_column_index is the cached integer index of the pindex column in the
detector bank's schema — get it once, reuse it in every event. 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 "reader.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::reader reader;
reader.open(argv[1]);
auto list = reader.getBanks({"REC::Particle", "REC::Calorimeter"});
auto& parts = list[0];
auto& calo = list[1];
// Cache column indices once, outside the event loop.
const int pid_col = parts.getSchema().getEntryOrder("pid");
const int status_col = parts.getSchema().getEntryOrder("status");
const int calo_pindex = calo.getSchema().getEntryOrder("pindex");
const int calo_energy = calo.getSchema().getEntryOrder("energy");
twig::h1d hEtot(100, 0.0, 8.0);
while (reader.next(list)) {
for (int row = 0; row < parts.getRows(); row++) {
// Forward-detector electron only.
if (parts.getInt(pid_col, row) != 11) continue;
int s = std::abs(parts.getInt(status_col, 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(calo_energy, cRow);
}
hEtot.fill(E);
}
}
hEtot.print();
return 0;
}
What's happening:
- Read both banks in the same banklist so
reader.next(list)fills them together. - Cache every column index the inner loop touches — the string → index lookup is the single biggest time sink in naive CLAS12 code.
- 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:
auto list = reader.getBanks({
"REC::Particle", "REC::Calorimeter", "REC::Cherenkov", "REC::Scintillator"
});
auto& parts = list[0];
auto& calo = list[1];
auto& cher = list[2];
auto& scin = list[3];
const int pid_col = parts.getSchema().getEntryOrder("pid");
const int calo_pindex = calo.getSchema().getEntryOrder("pindex");
const int calo_energy = calo.getSchema().getEntryOrder("energy");
const int cher_pindex = cher.getSchema().getEntryOrder("pindex");
const int cher_det = cher.getSchema().getEntryOrder("detector");
const int cher_nphe = cher.getSchema().getEntryOrder("nphe");
const int scin_pindex = scin.getSchema().getEntryOrder("pindex");
const int scin_det = scin.getSchema().getEntryOrder("detector");
const int scin_time = scin.getSchema().getEntryOrder("time");
const int HTCC_ID = 15; // org.jlab.detector.base.DetectorType
const int FTOF_ID = 12;
while (reader.next(list)) {
for (int row = 0; row < parts.getRows(); row++) {
if (parts.getInt(pid_col, row) != 11) continue;
// Total calorimeter energy for this electron
float ECAL = 0.0f;
for (int r : calo.getRowListLinked(row, calo_pindex))
ECAL += calo.getFloat(calo_energy, r);
// Total HTCC photo-electrons
int nphe = 0;
for (int r : cher.getRowListLinked(row, cher_pindex))
if (cher.getInt(cher_det, r) == HTCC_ID)
nphe += cher.getInt(cher_nphe, r);
// FTOF hit time (first match)
float tof = -1.0f;
for (int r : scin.getRowListLinked(row, scin_pindex)) {
if (scin.getInt(scin_det, r) == FTOF_ID) {
tof = scin.getFloat(scin_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:
const int pid_col = parts.getSchema().getEntryOrder("pid");
const int calo_pindex = calo.getSchema().getEntryOrder("pindex");
for (int cRow = 0; cRow < calo.getRows(); cRow++) {
int part_row = calo.getInt(calo_pindex, cRow);
if (part_row < 0 || part_row >= parts.getRows()) continue; // unmatched / bad link
int pid = parts.getInt(pid_col, 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:
- Cache every column index you touch in the hot loop, not just
pindex.getSchema().getEntryOrder(name)costs a map lookup per call — doing it once per column per run is free; doing it once per event is a ~100× slowdown. - 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.