Reconstructed particles

import awkward as ak
import matplotlib.pyplot as plt
import numpy as np
import uproot

from lcio_checks.util import config, load_or_make

f = uproot.open(f"{config['data_dir']}/P2f_z_eehiq.root")["MyLCTuple"]
rc = f.arrays(filter_name="rc*", entry_stop=-1)
rp = rc[rc.rccid == 101]

RP Collections in the LCTuple

The reconstructed particles in the LCTuple rcXXX namespace are the combination of multiple LCIO collections. The LCIO collection is tracked in the rccid field.

rccid

LCIO collection

101

PandoraPFOs

102

BCALParticles

103

PrimaryVertex_RP

104

BuildUpVertex_RP

105

BuildUpVertex_V0_RP

Note: If you want to work with PFO objects I would observe in my detector, only use 101 and 102.

Counts in the sample per LCIO collection:

assert {101, 102, 103, 104, 105}.issuperset(np.unique(ak.flatten(rc.rccid)))

for cid, counts in zip(*np.unique(ak.flatten(rc.rccid), return_counts=True)):
    print(f"{cid}:{counts:>7d}")
print(f"\nNumber of events in the sample: {len(rc)}.")
101: 698556
103:  43199
104:    380
105:    145

Number of events in the sample: 43199.

Vertex collections

For now, we do not plan to study them further. Let us only mention that all particles in these collection have their type defined as 3.

assert list(np.unique(ak.flatten(rc[rc.rccid > 102].rctyp).to_numpy())) == [3]

Particle types in PandoraPFOs

assert 102 not in np.unique(ak.flatten(rc.rccid))

import pandas as pd

uniq, counts = np.unique(
    np.abs(ak.flatten(rc[rc.rccid == 101].rctyp)), return_counts=True
)
df = pd.DataFrame(counts, index=uniq, columns=["counts"])
df.index.name = "Particle species"
df
counts
Particle species
11 94921
13 152
22 292288
211 234886
310 273
2112 75572
3122 464

A closer look into the Pandora PFOs

@load_or_make(["pfo_energy_per_event"])
def pfo_energy_per_event():
    bins = np.arange(0, 501, 3)
    fig, ax = plt.subplots(figsize=(4, 4))
    ax.hist(ak.sum(rp.rcene, axis=1), bins=bins)
    ax.set_title("energy per event in PFOs")
    return (fig,)


@load_or_make(["pfo_energy_per_event_after_acceptance"])
def pfo_energy_per_event_after_acceptance():
    rcps = rp[rp.rcene != 0]
    bins = np.arange(0, 321, 3)
    fig, axs = plt.subplots(ncols=2, figsize=(10, 4))
    for cos_theta in [1.01, 0.999999, 0.99999, 0.9999, 0.999, 0.99, 0.9]:
        x = ak.sum(rcps[np.abs(rcps.rcmoz) / rcps.rcene < cos_theta].rcene, axis=1)
        axs[0].hist(x, bins=bins[10:], label=str(cos_theta), alpha=0.7)
        axs[1].hist(x, bins=bins, label=str(cos_theta), alpha=0.7)
        axs[1].legend(title="Per particle |cos$\\theta$|$\\leq$", ncol=2)
        axs[1].set_yscale("log")
    fig.suptitle("energy per event in PFOs after angular acceptance cut")
    return (fig,)


pfo_energy_per_event()
pfo_energy_per_event_after_acceptance();
../_images/ReconstructedParticles_9_0.png ../_images/ReconstructedParticles_9_1.png

Angular acceptance

def particles_per_angle(rcps, bins):
    _, ax = plt.subplots(figsize=(6, 4))
    c_theta = np.abs(rcps.rcmoz) / rcps.rcene
    ax.hist(ak.flatten(c_theta), bins=bins, label="all")
    ax.hist(ak.flatten(c_theta[np.abs(rcps.rctyp) == 11]), bins=bins, label=r"$e^\pm$")
    ax.set_title("PFOs per angle")
    ax.set_xlabel("|cos$\\theta$|")
    ax.set_yscale("log")
    ax.legend()
    return ax


particles_per_angle(rp, bins=np.linspace(0, 1, 100))
particles_per_angle(rp, bins=np.linspace(0.99, 1, 100));
../_images/ReconstructedParticles_11_0.png ../_images/ReconstructedParticles_11_1.png