Custom Analysis

Topics Covered

  • Per-event data structures and loading

  • Histogramming data

  • Using rebdsim classes in custom analysis

  • Based on models in bdsim/examples/trajectories.

Contents

Preparation

  • BDSIM has been compiled and installed.

  • The (DY)LD_LIBRARY_PATH and ROOT_INCLUDE_PATH environmental variables are set as described in Setup.

  • ROOT can be imported in Python

  • pymadx and pybdsim have been installed.

Note

This example model will only work with Geant4.10.4 and upwards.

Generating Data To Analyse

For the purpose of this example we need to generate a data file to analyse. We use the example described in Trajectory Generation. We can generate a data file with:

cd bdsim/examples/trajectories
bdsim --file=proton-target.gmad --outfile=run1 --batch --ngenerate=200 --seed=123

This will generate a file called run1.root that we will use to analyse.

Broadly speaking, the example generates trajectories of muons and neutrinos with their link back the primary proton (e.g. likely a pion). The model uses biasing so we must use the weights in the data.

Simple Data Loop

Using pybdsim, which in turn uses the Python interface to ROOT, we can load the data and see all of it individually. pybdsim internally uses ROOT to load the bdsim shared libraries that contain the definitions of all the C++ classes used in the output. This script is in bdsim/examples/trajectories/simpleloop.py and contains:

import pybdsim
import ROOT

def Analyse(filename, outputfilename):
d = pybdsim.Data.Load(filename)

particles    = [13,-13,14,-14]
particlesStr = [str(p) for p in particles]
names        = [r'$\mu^-$',     r'$\mu^+$',
                r'$\nu_{\mu}$', r'$\overline{\nu}_{\mu}$']

# setup histograms - 1x 2D histogram in X,Z for each particle
oZX = {}
for p,ps in zip(particles,particlesStr):
    # x dimension of histogram: 50 bins in Z from 0 to 10m
    # y dimension of histogram: 50 bins in X from -1 to 1 m
    # note the histogram has no concept of units - it's purely just a
    # number so we need to choose ranges appropriately
    h = ROOT.TH2D("Origin_ZX_"+ps, "Origin of "+ps,
                  50, 0, 10,
                  50, -0.5, 0.5)
    oZX[p] = h

eventTree = d.GetEventTree()
# using this python syntax for name in thing, ROOT will make the variable
# 'name' ('event') here, have the exact same layout as we see in a TBrowser
# and as is in the file.
for event in eventTree:
    # now loop over the trajectories stored for this event
    for i in range(event.Trajectory.n):
        # get the particle ID of the trajectory
        partID = event.Trajectory.partID[i]
        if partID not in particles:
            continue # skip ones we don't want to histogram

        # i-th trajectory and the 0th (ie first) point of the trajectory position vector
        originPos = event.Trajectory.XYZ[i][0]
        x,z = originPos[0],originPos[2]
        weight = event.Trajectory.postWeights[i][0]

        oZX[partID].Fill(z, x, weight)

# now write histograms out to a new ROOT file
f = ROOT.TFile(outputfilename, "RECREATE")
for pid,histogram in oZX.items():
    histogram.AddDirectory(True) # bdsim / rebdsim classes turn off AddDirectory by default
    histogram.Write()
f.Close()

This loops over each event and inside that event loops over each trajectory. If it matches one of the desired particle IDs we histogram the X,Z (global) coordinates of its starting point.

To view the histograms, see Quick Recipes.

Rebdsim Average Histograms

The above histograms are simple ROOT histograms. These are a count of a quantity in each bin. However, often we are interested in the mean or average per-event rate so that we can easily scale this to a realistic beam or setup. i.e. we typically never simulate the same number of events as real life particles, but enough to gain a statistical understanding of the outcome. With the simple histograms we can of course, normalise to the number of events if we keep track of it. If the quantity however, is more than 1 thing per event, then the variance and error on the mean will not be correct. This is discussed in Per-Entry and Simple Histograms.

Normally, in rebdsim, the histograms are per-event averages. We can reproduce this ourselves in Python using classes from rebdsim. The following script is included in bdsim/examples/trajectories/rebdsimaveragehistograms.py. The same histograms are created as in the earlier part of this example but are a per-event average.

import pybdsim
import ROOT

def Analyse(filename, outputfilename):
    d = pybdsim.Data.Load(filename)

    particles    = [13,-13,14,-14]
    particlesStr = [str(p) for p in particles]
    names        = [r'$\mu^-$',     r'$\mu^+$',
                    r'$\nu_{\mu}$', r'$\overline{\nu}_{\mu}$']

    # setup histograms - 1x 2D histogram in X,Z for each particle
    # a base hsitogram is one we will use for each event and empty it afterwards
    baseHistograms = {}
    for p,ps in zip(particles,particlesStr):
        # x dimension of histogram: 50 bins in Z from 0 to 10m
        # y dimension of histogram: 50 bins in X from -1 to 1 m
        # note the histogram has no concept of units - it's purely just a
        # number so we need to choose ranges appropriately
        h = ROOT.TH2D("Origin_ZX_"+ps+"BASE", "Origin of "+ps, 50, 0, 10, 50, -0.5, 0.5)
        baseHistograms[p] = h

    # make accumulators that calculate a rolling average for each histogram
    accumulators = {}
    for p,hist in baseHistograms.items():
        # note we must have a different name for the resultant accumulated histogram, so we strip
        # off the 'BASE' suffix we added (knowingly) to the base histogram
        accumulators[p] = ROOT.HistogramAccumulator(hist, hist.GetName().strip("BASE"), hist.GetTitle())

    eventTree = d.GetEventTree()
    # using this python syntax for name in thing, ROOT will make the variable
    # 'name' ('event') here, have the exact same layout as we see in a TBrowser
    # and as is in the file.
    for event in eventTree:
        for p,h in baseHistograms.items():
            h.Reset()

        # now loop over the trajectories stored for this event
        for i in range(event.Trajectory.n):
            # get the particle ID of the trajectory
            partID = event.Trajectory.partID[i]
            if partID not in particles:
                continue # skip ones we don't want to histogram

            # i-th trajectory and the 0th (ie first) point of the trajectory position vector
            originPos = event.Trajectory.XYZ[i][0]
            x,z = originPos[0],originPos[2]
            weight = event.Trajectory.postWeights[i][0]

            baseHistograms[partID].Fill(z, x, weight)

        # at the end of the event we 'accumulate' one event
        for p,h in baseHistograms.items():
            accumulators[p].Accumulate(h)

    # now we terminate the accumulators - ie normalise to the number of events
    results = {}
    for p,h in accumulators.items():
        results[p] = h.Terminate()

    # now write histograms out to a new ROOT file
    f = ROOT.TFile(outputfilename, "RECREATE")
    for p,h in results.items():
        h.AddDirectory(True) # bdsim / rebdsim classes turn off AddDirectory by default
        h.Write()
    f.Close()

The key concept is we create a regular histogram but also an accumulator based on it. We fill the histogram possibly several times for a given event, and at the end of the event we mark this by accumulating that individual histogram. At the start of each event we use the same base histogram again but we reset it to empty it.

To view the histograms, see Quick Recipes.