getParent() in python

Hi all,

I was playing around with the example to read .root output objects with python. All works well [getKineticEnergyInitial() and other get…() functions]. The only problem i am facing regards when i try to get the parent of a track from MCTrack with getParent(). (code below)

import sys
sys.path.append("/home/luca/Documents/allpix/ROOT/root_install/lib")
# ROOT imports
import ROOT
from ROOT import TFile, gDirectory, gSystem, TClass
import pandas as pd
import numpy as np

import os, sys
import argparse
import os.path as path


#print("Current Working Directory:", os.getcwd())
root_file_name = "/home/luca/Documents/tests/output/ROOT_objects.root"
detector_name = "detector1"
lib_path = "/home/luca/Documents/allpix/allpix-squared/lib/libAllpixObjects.so"

if lib_path is not None:  # Try to find Allpix Library
    lib_file_name = (str(lib_path))
    if (not os.path.isfile(lib_file_name)):
        print("WARNING: ", lib_file_name, " does not exist, exiting")
        exit(1)
elif os.path.isfile(path.abspath(path.join(__file__ ,"..","..","opt","allpix-squared","lib","libAllpixObjects.so"))): # For native installs
    lib_file_name = path.abspath(path.join(__file__ ,"..","..","opt","allpix-squared","lib","libAllpixObjects.so"))

elif os.path.isfile(path.join(path.sep, "opt","allpix-squared","lib","libAllpixObjects.so")): # For Docker installs
    lib_file_name = path.join(path.sep, "opt","allpix-squared","lib","libAllpixObjects.so")

else:
    print("WARNING: No Allpix Objects Library found, exiting")
    exit(1)

if (not os.path.isfile(lib_file_name)):
    print("WARNING: no allpix library found, exiting (Use -l to manually set location of libraries)")
    exit(1)
if (not os.path.isfile(root_file_name)):
    print("WARNING: " + root_file_name + " does not exist, exiting")
    exit(1)

gSystem.Load(lib_file_name)
rootfile = ROOT.TFile(root_file_name)
gDirectory.ls()


if not rootfile.GetDirectory("detectors/" + detector_name):
    print("\nDetector does not exist. Please choose one of the following detectors:")
    gDirectory.cd("detectors")
    list_of_keys = gDirectory.GetListOfKeys()
    for key in list_of_keys:
        print(key.GetName())
    exit(1)
McParticle = rootfile.Get('MCParticle')
McTrack = rootfile.Get('MCTrack')
PixelHit = rootfile.Get('PixelHit')
empty_mc_branch = 0
column_names = ["event_ID", "init_x", "init_y", "init_z", "time", "init_kin", "process_name", "particle_ID","parent","volume_origin"]
df = pd.DataFrame(columns=column_names)
data_array = [None]*10

print("Loading in Data...")
for iev in range(0, McTrack.GetEntries()):
    #here we enter one event at a time. Each event can either contain or not interactions with the detector
    McTrack.GetEntry(iev)
    PixelHit.GetEntry(iev)
    McParticle.GetEntry(iev)
    McTrack_branch = McTrack.GetBranch("global")
    PixelHit_branch = PixelHit.GetBranch(detector_name)
    McParticle_branch = McParticle.GetBranch(detector_name)
    if (not McTrack_branch):
        Warning("WARNING: cannot find McTrack branch in the TTree with detector name: " + detector_name + ",  exiting")
        exit(1)

    data_array[0]=iev #add event number

    br_mc_track = getattr(McTrack, McTrack_branch.GetName())
    br_mc_particle = getattr(McParticle, McParticle_branch.GetName())

    if br_mc_track.size() < 1:
        empty_mc_branch += 1;
        continue
    if iev == 257:
        print("a")
    for mc_track in br_mc_track:
        #here we record all the interactions the event had with the detector togheter with the tracks generated
        data_array[1] = mc_track.getStartPoint().x()
        data_array[2] = mc_track.getStartPoint().y()
        data_array[3] = mc_track.getStartPoint().z()
        data_array[4] = mc_track.getGlobalStartTime()
        data_array[5] = mc_track.getKineticEnergyInitial()
        data_array[6] = mc_track.getCreationProcessName()
        data_array[7] = mc_track.getParticleID()
        data_array[8] = mc_track.getParent()
        data_array[9] = mc_track.getOriginatingVolumeName()

        # Create a dictionary from the array and column names
        new_row = dict(zip(df.columns, data_array))
        print(iev)

        # Convert the dictionary to a DataFrame and append it
        df = pd.concat([df, pd.DataFrame([new_row])], ignore_index=True)

Basically in the results i get what i guess is a null pointer for all cases in which the track is the first one to be generated from the source (and therefore has no parent) and for secondary tracks (which should have a parent) i get None (see picture below).

Am i doing something wrong here? Should i use MCParticle instead to get to the parent track ID?

And is there a function to get the track ID from MCTrack in python (like getTrackID() )? Cause for now i was getting the track ID in python by looking at the mc_track variable which i’m not sure is the best way.

My idea was to use MCTrack alone to get the order of the tracks for one event since it can also track in the whole world simulation volume, contrary from MCParticle, so i could always get the full parent-child hierarchy even for particles generated outside the active detector volume.

Thanks in advance for your time, bests

Luca

PS: on a second note i tried also in a C++ script to get from MCTrack the parent track but there i get a different problem in which i always get the same exact parent ID for all secondary tracks, therefore i suspect that there i am not using getParent() on the correct thing, therefore not getting the actual parent of the tracks but maybe the ID of the same branch every time or of the TTree? I wonder if something like this could be happening here too in python.

Hi @luca_tero ,

Interesting! I haven’t worked much with analysis using Python, but I’ll try to figure this out.

There is no getTrackID() function I’m afraid. The tracks don’t know their own ID.

For the C++, I think it also makes sense that you get the same parent for (just about) all secondary tracks, as the parents are reset per event. So, if you have a photon coming in (let’s call it Track 1) and generating an electron (Track 2) in one event, the parent of the electron is the photon (Track 2 parent of Track 1). If this same thing happens in another event, again the photon will be Track 1 (as these things are local to the events), and the electron Track 2, so it will look the same.

Now, the Python;
I observe the same behaviour with your code; reporting None when there is a parent, and a (somewhat cryptic) nullptr when not.
First of all, I added a print-statement that calls the print function of the MCTrack, just to get direct access to the info (by doing mc_track.print(ROOT.std.cout)). Here is an excerpt:

--- Printing MCTrack information for track (0xe7f4160) ---------------------------
Particle type (PDG ID):     211
Production process:        none (G4 process type: -1)
Production in G4Volume:   World
Termination in G4Volume:  World
Initial position:           2.27518 mm |   0.204376 mm |       -200 mm
Final position:             2.27228 mm |   0.204666 mm |        220 mm
Initial time:                     0 ns
Final time:                 1.40097 ns
Initial kinetic energy:      120000 MeV | Final kinetic energy:        119999 MeV   
Initial total energy:        120140 MeV | Final total energy:          120139 MeV   
Linked parent: <nullptr>
----------------------------------------------------------------------------------
92

--- Printing MCTrack information for track (0xe7f4160) ---------------------------
Particle type (PDG ID):     211
Production process:        none (G4 process type: -1)
Production in G4Volume:   World
Termination in G4Volume:  World
Initial position:         -0.251914 mm |   -1.19108 mm |       -200 mm
Final position:           -0.254554 mm |   -1.19117 mm |        220 mm
Initial time:                     0 ns
Final time:                 1.40097 ns
Initial kinetic energy:      120000 MeV | Final kinetic energy:        119999 MeV   
Initial total energy:        120140 MeV | Final total energy:          120138 MeV   
Linked parent: <nullptr>
----------------------------------------------------------------------------------
92

--- Printing MCTrack information for track (0xe8ebdb0) ---------------------------
Particle type (PDG ID):      11
Production process:       hIoni (G4 process type: 2)
Production in G4Volume:  sensor_telescope0_phys
Termination in G4Volume: sensor_telescope0_phys
Initial position:         -0.252036 mm |   -1.19121 mm |   0.624016 mm
Final position:           -0.342329 mm |  -0.998303 mm |   0.634747 mm
Initial time:               0.66921 ns
Final time:                0.671074 ns
Initial kinetic energy:    0.215528 MeV | Final kinetic energy:             0 MeV   
Initial total energy:      0.726527 MeV | Final total energy:        0.510999 MeV   
Linked parent: 0xe7f4160
----------------------------------------------------------------------------------

We can see two things from this: 1. for different events, the track ID/name is the same (0xe7f4160 on this particular run; the memory address).
2. When we have a child particle, there is actually a linked parent!
Now we need to figure out why Python just gives us None in this case.

Adding a statement print("Parent:", mc_track.getParent()), we end up with an output that is
Parent: Name: allpix::MCTrack Title: NOLINT
when there is a parent. A bit funky, but it certainly finds the object!
Now the question is what to do with it…
I figured out a way to get the address of the underlying ROOT object at least, if you want to use that as ID:

print("ID:", hex(ROOT.addressof(mc_track)))
print("Parent:", hex(ROOT.addressof(mc_track.getParent())))

This gives e.g.

--- Printing MCTrack information for track (0xfcc1040) ---------------------------
Particle type (PDG ID):     211
Production process:        none (G4 process type: -1)
Production in G4Volume:   World
Termination in G4Volume:  World
Initial position:           1.00904 mm |  -0.695134 mm |       -200 mm
Final position:             1.01005 mm |  -0.692109 mm |        220 mm
Initial time:                     0 ns
Final time:                 1.40097 ns
Initial kinetic energy:      120000 MeV | Final kinetic energy:        119996 MeV   
Initial total energy:        120140 MeV | Final total energy:          120136 MeV   
Linked parent: <nullptr>
----------------------------------------------------------------------------------
ID: 0xfcc1040
Parent: 0x0

--- Printing MCTrack information for track (0xfe17f20) ---------------------------
Particle type (PDG ID):      11
Production process:       hIoni (G4 process type: 2)
Production in G4Volume:  chip_telescope0_phys
Termination in G4Volume: sensor_telescope0_phys
Initial position:           1.00894 mm |  -0.694777 mm |   0.927358 mm
Final position:            0.858441 mm |   -1.01453 mm |   0.723065 mm
Initial time:              0.670222 ns
Final time:                0.672854 ns
Initial kinetic energy:    0.347404 MeV | Final kinetic energy:             0 MeV   
Initial total energy:      0.858403 MeV | Final total energy:        0.510999 MeV   
Linked parent: 0xfcc1040
----------------------------------------------------------------------------------
ID: 0xfe17f20
Parent: 0xfcc1040

I hope this helps!

Kind regards,
Håkan

Hi @luca_tero @hwennlof ,

I didn’t test this myself now, but could this be an issue arising from not storing all objects?
By default, an MCTrack object is only stored in case it actually interacts with a sensor. If a primary particle does not reach the sensor, but its secondary does, this could cause an issue like this.

There is the option to record all MCTrack objects, which is enabled using record_all_tracks = true in the DepositionGeant4 module, see the documentation. Enabling this would be worth a try.

Cheers
Paul

Hi @hwennlof and @pschutze and thanks for the help!

regarding using

print("ID:", hex(ROOT.addressof(mc_track)))
print("Parent:", hex(ROOT.addressof(mc_track.getParent())))

i think this was exactly what i was looking for. When printing the full MCTrack object in the txt output writer i could some sort of track ID ( 0xe8ebdb0 ) for example and linked parent 0xe7f4160 but on python i could not recover this information.

{For context i am using all of this for a compton scattering imager cause i thought it would have been enough to use MCTrack to get the interaction location of the primary photons in the detector as well as recovering the deposited energies (IE sum of initial kinetic energy of secondary particles from eachinteraction) and recover the parent hierarchy in the case in which there are multiple scatterings of the primary photon in the detector. Not sure if there is a smarter/more efficient way to do that.}

Regarding adding record_all_tracks = true i tried that and it does not solve the problem directly as shown in the resulting pandas dataframe below


(unless we add the two lines of python code mentioned above), but it definitely includes all the track that also do not interact with the detector. Therefore i think it will be necessary when reconstructing the full parent hierarchy in multi scattered primary particles so thanks for the tip!

Thanks again for your help, bests

Luca

1 Like

Hi @luca_tero ,

I am confused for the moment. Could you summarize which issues are remaining?
It would be nice if you could send us your configuration files and maybe also your analysis script so we can try to reproduce and isolate the problem.

Cheers
Paul

Hi!

so far no issues left i was just commenting on what worked and what did not trying different things.

Thank you for your help, bests

Luca