Skip to content

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 in REC::Track.
  • pindex can be −1. Some detector rows are not associated with any REC::Particle row (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:

auto linked = calorimeter.getRowListLinked(particle_row, pindex_column_index);

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:

  1. Read both banks in the same banklist so reader.next(list) fills them together.
  2. Cache every column index the inner loop touches — the string → index lookup is the single biggest time sink in naive CLAS12 code.
  3. Loop particles, keep Forward-Detector electrons (|status| in [2000, 4000)), and use getRowListLinked() to fetch every calorimeter row belonging to that electron. Sum energy across 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 by pindex yourself, 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