Extending the Data Structure

Planning

In order to extend the data structure (RAT::DS), you must decide several things:

What sort of data would you like to record?

If it is an atomic value, like a float or an integer, your task will be easy. If it is several pieces of data, it may make more sense to make a new class to put in the event data structure.

The only types allowed in the data structure are standard C++ atomic types like int and double (and ROOT-specific typedefs to them), STL containers, like vector and map, and classes which follow the RAT::DS style. That is, they are subclasses of TObject which follow the RAT style conventions. Note that C-style arrays and ROOT container classes (e.g. TClonesArray) are not allowed!

What do you want to call it?

Short (but not cryptic) names are preferred, and it helps to make them unique over all branches of the event data structure. This simplifies plotting with TTree::Draw() in ROOT later, so that the branch need not be specified to disambiguate a name.

Where will this data be created and where will it be used?

Data stored in the event structure must be “created” somewhere, either in a generator or a processor. You should decide where the code which computes this information should go. If the information is at all optional, it’s best to put the computation in a separate processor so users who don’t need the data can skip it and save running time.

Also remember that RAT::DS is the interface by which processors “communicate” with each other. Intermediate calculations should not be stored in the data structure unless they would be of interest to an end user. However, any data from one processor that is the input to the calculation of another processor should be stored in the data structure.

Adding Existing Data Types to Objects

If you are adding data of an already existing data type, you can just edit the appropriate header file and recompile. No other action is required, except editing the appropriate processor or generators to actually put some data there!

Creating a New Class for the Data Structure

If you need to create a new class, you need to:

Make the header file

Your new class must subclass TObject and be declared within the namespace RAT::DS. For ease, copy an existing event class and edit it. Do not forget to put the new class name in the !ClassDef macro.

Note that DS class members should not use a prefix like “f” (that is, use “pmtCount” rather than “fPMTCount”), since they are part of the user interface, in a sense. Data members should be private, with the interface defined by “getter” and “setter” methods. The name of the file should match the name of the class.

For example, a class with PMT information might look like this:

/**
 * @class RAT::DS::PMT
 * @author A. Hamsterton <hamster@urodents.edu>
 * @brief Example of PMT information
 *
 * This is a long description of the class. This whole
 * block is here so we can generate Doxygen documentation.
 */

#ifndef __RAT_DS_PMT___
#define __RAT_DS_PMT___

#include <TObject.h>
#include <vector>

namespace RAT {
  namespace DS {

class MyClass : public TObject {
public:
  MyClass() {}
  virtual ~MyClass() {}

  /// PMT channel number
  int GetPMTID() { return pmtID; }
  void SetPMTID(int _pmtID) { pmtID = _pmtID; }

  /// Digitized hit time information
  int GetADCTime() { return adcTime; }
  void SetADCTime(int _adcTime) { adcTime = _adcTime; }

  /// Digitized hit charge information
  int GetADCCharge() { return adcCharge; }
  void SetADCCharge(int _adcCharge) { adcCharge = _adcCharge; }

  /// True hit time information
  int GetTime() { return time; }
  void SetTime(int _time) { time = _time; }

  /// True hit charge information
  int GetCharge() { return charge; }
  void SetCharge(int _charge) { charge = _charge; }

private:
  int pmtID;  ///< PMT channel number;
  int adcTime;  ///< Digitized hit time
  int adcCharge;  ///< Digitized hit charge
  float time;  ///< True hit time
  float charge;  ///< True hit charge

  ClassDef(PMT, 1)
};

  }  // namespace DS
}  // namespace RAT

#endif  // __RAT_DS_PMT__

Make an implementation

Make in implementation in a cc file with the same name as the header, if necessary. Remember that DS classes should not do very much work – they are vessels for data which is computed elsewhere (such as a generator or processor). Never should a DS class itself be computing the data that it stores.

Add to the ROOT dictionary

Edit the LinkDef fragment for the ds module, src/ds/include/RAT/DS/LinkDef.hh, to ensure your new class is added to the ROOT dictionary, which is needed for the object to be streamed to disk and for ROOT macros to work properly. You will need to add a line that looks like:

#pragma link C++ class RAT::DS::PMT + ;

And further down, in the __MAKECINT__ block, there needs to be a line for any STL container of your class, like:

#pragma link C++ class vector < RAT::DS::PMT>;

Register the header for the dictionary

The dictionary generator also needs to see the header that declares your class. Make sure the ds module’s CMakeLists.txt (src/ds/CMakeLists.txt) lists it in its rat_dictionary_headers() call, e.g.:

rat_dictionary_headers(
  RAT/DS/PMT.hh
  # ...other headers...
)

Each module registers its own headers and LinkDef fragment via rat_dictionary_headers() and rat_dictionary_linkdef() (see cmake/RATDictionary.cmake). These accumulate into a single global dictionary that is generated once in src/CMakeLists.txt, so you no longer need to touch any central build file.

Recompile

Rebuild with make (or re-run make from your build directory) to regenerate the dictionary and pick up the new class.

Extending the Data Structure in a Private Experiment

The techniques above are appropriate when the new data belongs in RATPAC itself and can be shared with everyone. Private experiments, however, often need to attach experiment-specific data to the top-level event object, RAT::DS::Root, without modifying the shared class. This is the supported way to extend the data structure for a private experiment: subclass RAT::DS::Root in your own code base and register the subclass with RAT::DS::RootFactory, rather than editing RATPAC’s own data-structure classes.

RAT::DS::RootFactory acts as the single point where the top-level event object is constructed. A new RAT::DS::Root only ever originates in a producer – the object that introduces events into the data stream, such as Gsim for simulated events or InROOTProducer for events read back from a ROOT file. These producers obtain the event object from RAT::DS::RootFactory::Create() instead of calling new DS::Root directly. (The ROOT I/O classes DSReader, DSWriter, and OutROOTProc also build their branch buffer through the factory so that the on-disk type matches, but they do not introduce new events – only producers do.)

By default the factory returns a plain RAT::DS::Root. If you register the name of a subclass, the factory instead constructs and returns that subclass everywhere, so your extended object flows through the whole simulation and I/O chain transparently.

Subclass RAT::DS::Root

In your private code base, create a class that inherits from RAT::DS::Root and adds the members and accessors you need. It must have a default (I/O) constructor and a ClassDef, following the same conventions as any other data-structure class:

namespace MyExp {
  namespace DS {

class Root : public RAT::DS::Root {
public:
  Root() {}
  virtual ~Root() {}

  double GetMyValue() const { return myValue; }
  void SetMyValue(double v) { myValue = v; }

private:
  double myValue;  ///< Experiment-specific quantity

  ClassDef(MyExp::DS::Root, 1);
};

  }  // namespace DS
}  // namespace MyExp

Because official processors only ever see the RAT::DS::Root interface, your extra data is invisible to them, but it is written to and read back from disk with the rest of the event.

Register the subclass in the ROOT dictionary

ROOT needs dictionary information for your subclass so that it can be constructed by name and streamed to disk. Contribute the header and a LinkDef fragment from your module’s CMakeLists.txt using the helpers in cmake/RATDictionary.cmake, exactly as an in-tree module would:

rat_dictionary_headers(MyExp/DS/Root.hh)
rat_dictionary_linkdef(MyExp/DS/LinkDef.hh)

where your LinkDef.hh fragment contains the usual line:

#pragma link C++ class MyExp::DS::Root + ;

Select the subclass at start-up

Tell the factory which class to build by calling:

RAT::DS::RootFactory::SetClassName("MyExp::DS::Root");

This must run once, early, before any event object is created (for example in your experiment’s main or an initialization hook). From that point on, every RAT::DS::RootFactory::Create() returns a MyExp::DS::Root.

The factory validates the request at run time: it looks up the class in the ROOT dictionary, checks that it actually inherits from RAT::DS::Root, and throws a std::runtime_error if the name is unknown or does not derive from RAT::DS::Root. If the name is left empty (or set to "RAT::DS::Root"), the factory simply returns the base object, so RATPAC’s default behavior is unchanged when no experiment override is registered.

Update Documentation

Don’t forget to update the documentation in $RATROOT/doc to reflect the changes you have made to the data structure! Remember to explain what the data is and what units it is stored in.