If you want a formatted (or easier-to-read) version of this file, scroll to the bottom of GramsSim/README.md
for instructions. If you're reading this on github, then it's already formatted.
Table of contents generated with markdown-toc
A "data object" is a C++ type (class, struct, etc.) that is written as
a Branch (column) of a TTree (n-tuple) in a ROOT file. ROOT requires a dictionary to read and write data objects. The GramsDataObj
package defines the data objects and creates the dictionary.
Without the dictionary, files created by GramsSim
are generally unreadable. This section describes what you have to do to make sure your programs can "see" the dictionary.
The short version of the rest of this section: Follow the examples in the scripts
directory. Make sure the files Dictionary_rdict.pcm
and libDictionary.so
are in the same directory as your programs.
Your "build directory" is the one in which you built GramsSim
. If you followed the exact directions in GramsSim/README.md, that directory will be named GramsSim-work
, but it can have any name you choose.
Once you've compiled GramsSim
(via make
), you will see two files in the build directory: Dictionary_rdict.pcm
and libDictionary.so
(these are different in Mac OS1) If you want to develop code outside your original build directory, these files must be copied from the build directory to the dictionary that contains your programs.
There are three kinds of programs that might use the dictionary:
-
If you want to compile a C++ program (the code is in a file that ends in
.cc
or.cxx
), see the examples in thescripts
. You can start withdEdxExample.cc
; the comments at the top of the file show how to compile the program. -
For Python scripts, you need this line near the top of your code (in Linux2):
ROOT.gSystem.Load("./libDictionary.so")
See
SimpleAnalysis.py
for an example.
-
ROOT macros; that is, files ending in
.C
that are meant to be executed from within an interactive ROOT session; an example isSimpleAnalysis.C
.If you run the
root
command from within the build directory, interactive ROOT will executerootlogon.C
. This applies to both interactive sessions, and to running macros from the command line; e.g.,root scripts/SimpleAnalysis.C
If you examine
rootlogon.C
, you'll see it loads both the dictionary and the header (.h
files) for the data objects. If you want to run a ROOT macro from a different location, you'll have to copyDictionary_rdict.pcm
,libDictionary.so
, androotlogon.C
to the new location and possibly editrootlogon.C
for the header file path.An advantage of having
rootlogon.C
in in your current directory is that you can run ROOT, invoke the TBrowser, and use the the dictionary classes directory; e.g.,root TBrowser tb auto event = grams::EventID(0,23); cout << event << endl;
![]() |
---|
How the GramsSim files, trees, and data objects are connected. |
The data objects are defined by header files in the GramsDataObj/include/
directory. If any implementations are needed, they're
either in .icc
files in the include/
directory or in .cc
files
in the src/ directory.
If you want to look up the header files, a copy of all the user-related headers is made
into the include/
directory within your build directory.
Because of the way ROOT dictionary generation works, the single
Linkdef.hh file must include the necessary
#pragma
definitions for every data object. If you add a new data
object, be sure to edit this file.
Each data object should include an overloaded operator<<, whether it's a class, struct, or list. This means that the object can be displayed easily in C++. For example:
auto tracklist = new grams::MCTrackList();
// Assign values to (*tracklist), then:
std::cout << *tracklist << std::endl;
Some of the data objects are lists (std::set,
std::multiset, std::map, etc.). Others are C++ types such
as struct
or class
. For the non-list types, an explicit or
implicit operator<< is defined. This means that these objects can be
sorted, or used as keys in sorted containers like std::set
.
Each of the main TTrees produced by the analysis programs have the following properties:
Each tree has one row for each value of grams::EventID
(see below). They're all in sync
row-for-row; if row 2345 in TTree gramsg4
refers to a given event,
row 2345 in TTree DetSim
refers to the same event.
The standard ROOT method for working with multiple TTrees with related rows, but different columns, is to use friend trees. This is a way of "adding columns" to a tree without modifying the original file.
For example, consider tree gramsg4
in file gramsg4.root
and tree
DetSim
in file gramsdetsim.root
. To process both trees at
once:
// Open the first file and its tree.
auto myFile = TFile::Open("gramsdetsim.root");
auto tree = myFile->Get<TTree>("DetSim");
// Declare that the gramsg4 tree is a friend to the DetSim tree.
tree->AddFriend("gramsg4","gramsg4.root");
// Define the TTreeReader for this combined tree.
auto reader = new TTreeReader(tree);
// Create a TTreeReaderValue for each column in the combined tree
// whose value we'll use. Note that we have multiple columns named
// "EventID" in the combined tree, so specify which one to use.
// TTReaderValue behaves like a pointer. For example, we'll have to
// use (*EventID) later in the code.
TTreeReaderValue<grams::EventID> EventID (*reader, "ElecSim.EventID");
TTreeReaderValue<grams::MCLArHits> Hits (*reader, "LArHits");
TTreeReaderValue<grams::ElectronClusters> Clusters (*reader, "ElectronClusters");
// For every event in the combined tree:
while (reader->Next()) {
// Do whatever with *EventID, *Hits, and *Clusters.
}
Each TTree has a grams::EventID column, to help
make sure the trees maintain their row-to-row correspondence. Each tree
also has an index based on the EventID
. When processing a
TTree, typically one reads the rows (entries) sequentially.
For example, to read the data product MCTrackList, one might do:
// Define the input file.
auto input = new TFile("gramsg4.root");
// Define which TTree to read.
TTreeReader myReader("gramsg4", input);
// Define which variable(s) we'll read. This behaves like a pointer;
// in the code we'll use "*tracklist".
TTreeReaderValue<grams::MCTrackList> tracklist(myReader, "TrackInfo");
// For each row in the TTree:
while (myReader.Next()) {
// ... do something with *tracklist ...
}
However, if you want to read a specific row in the TTree, you can use
the index. For these trees, the index is defined using
grams::EventID::Index()
. For example, if you know that you want to
look at the specific event "Run=0, Event=1234", instead of looping
with myReader.Next()
as in the above example, you could do:
// Define the input file.
auto input = new TFile("gramsg4.root");
// Define the input tree and branch:
TTree* myTree = input->Get<TTree>("gramsg4");;
auto tracklist = new grams::MCTrackList();
myTree->SetBranchAddress("TrackList",&tracklist);
// Select a particular row:
grams::EventID myEvent(0,1234);
myTree->GetEntryWithIndex( myEvent.Index() );
// ... at this point, (*tracklist) is the list of tracks for run=0, event=1234
The advantage of this approach is it uses the index to access that portion of the input file directly, instead of sequentially searching for a given entry.
As noted below, if we start using some other way to identify events other than run/event numbers, then the above code must be modified.
Many of the data objects are organized in the form of maps, with keys in the form of a std:::tuple. For example, in MCLArHits.h:
// map<key, MCLArHit>. where the key is std::tuple<trackID,hitID>.
typedef std::map< std::tuple<int,int>, MCLArHit > MCLArHits;
This is to make it easier to do "back-tracing" of objects that are located in different columns/branches in different trees. For example, assume you are going through ElectronClusters and you wish to examine the LArHit that the cluster came from:
auto mcLArHits = new grams::MCLArHits();
auto clusters = new grams::ElectronClusters();
// Assume you've read *mcLArHits and *clusters from
// their respective friend trees.
// For every cluster in the map
for ( const auto& [key, cluster] : (*clusters) ) {
// Look at the fields of the cluster's key:
const auto& [ trackID, hitID, clusterID ] = key;
// Then the key for the corresponding hit that
// contains that cluster is:
const auto hitKey = std::make_tuple( trackID, hitID );
// This is the hit that the cluser is in:
const auto hit& = (*mcLArHits)[ hitKey ];
}
This is illustrated in greater detail in scripts/AllFilesExample.cc and scripts/AllFilesExample.py.
Many of the data objects are organized in the form of maps. In C++, a std::map is a container whose elements are stored in (key,value) pairs. If you're familiar with Python, they're similar to dicts.
As you look through the description of the data objects below, consult the include/
directory within your build directory for the header files. These are the files that define the methods for accessing the values stored in these objects. Documentation may be inaccurate; the code is the actual definition.
The EventID object encapsulates what, in many experiments, is simply the run and event number. However, in a balloon or satellite experiment, it may be that there are different methods for assigning an event ID; e.g., UTC time.
As a precaution (and also to saving on typing if (run == N && event == M)
), the grams::EventID
class is used instead. You can sort on
grams::EventID
or test it for equality, without having to modify the code
if there's a switch from "run/event" to distinguish events.
![]() |
---|
Sketch of the grams::EventID data object. |
The grams::EventID
object is created in GramsG4, then copied
from one file to another as the friend trees are created in subsequent programs
in the analysis chain (such as GramsDetSim).
As of Jun-2024, an EventID object identifies a single simulated particle, but that may change. One point made by Georgia Karagiorgi: Don't limit an "event" to be a single particle. As we begin to incorporate overlays and pile-up in the analysis, the "EventID" may refer to a trigger window or something similar.
This data object contains the "MC Truth" information for the particle tracks produced in GramsG4 for a single event; see that page for additional information.
![]() |
---|
Sketch of the grams::MCTrackList data object. |
This data object contains the "MC Truth" information associated with the ionization energy deposits in the liquid argon as determined by GramsG4 for a single event; see that page for additional information.
![]() |
---|
Sketch of the grams::MCLArHits data object. |
This data object contains the "MC Truth" information for ionization energy deposited in the scintillator strips by the GramsG4 simulation for a single event; see that page for additional information.
![]() |
---|
Sketch of the grams::MCScintHits data object. |
This data object contains electron-cluster information produced by GramsDetSim for a single event, which models the drift of the ionization deposited in the LAr as recorded in MCLArHits; see the GramsDetSim
page for additional information.
![]() |
---|
Sketch of the grams::ElectronClusters data object. |
This data object, created by GramsReadoutSim for each event, contains the association of the electron clusters created in GramsDetSim to the elements of the readout geometry. See the GramsReadoutSim
page for additional information.
![]() |
---|
Sketch of the grams::ReadoutMap data object. |
This data object is created by GramsElecSim for each event. It contains the analog and digital waveforms for the elements of the readout geometry as induced by the electron clusters in grams::ElectronClusters
and mapped to the readout by grams::ReadoutMap
. See the GramsElecSim
page for additional information.
![]() |
---|
Sketch of the grams::ReadoutWaveforms data object. |
Footnotes
-
If you're running a Mac, the files that must be copied are:
Dictionary_rdict.pcm
,libDictionary.dylib
, andDictionary.rootmap
. The reason for the difference is that Mac OS X Darwin handles its shared libraries in a different manner than other systems. ↩ -
On a Mac, the file's name will be
libDictionary.dylib
. ↩