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);

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:

  1. The sequential chain walks every event; each iteration hands you a chain_event.
  2. event.getBank("REC::Particle") and event.getBank("REC::Calorimeter") look both banks up by name in the file's dictionary — no pre-built schema needed.
  3. getRowListLinked() takes the pindex column as an integer index, so resolve it from the calorimeter schema once per event, before the row loop.
  4. 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:

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 for pindex — not once per row. With the sequential chain you 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 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