Handler Developers Guide¶
One can use this page as a tutorial, trying to compile the examples listed below, or use following cheatsheet to quickly navigate to the particular recipe.
TL;DR¶
A cheatsheet for handler developer:
Feature |
Recipe |
---|---|
Det. name to det. ID conversion |
inherit |
Iterate over hits of certain type |
inherit |
ROOT histogram in certain |
inherit |
Use calibration data |
Inherit |
Create a hit in an event |
Use |
Bare minimum and specialized recipes are explained below.
Bare Minimum¶
A handler is pretty much a standalone program that is embedded in a larger framework. The purpose of the framework is to provide general common tasks like setting up an execution environment and initialize the infrastructure, while handlers are the subject of user’s development. They can do anything that ordinary C/C++ program does, but typically they are intended to do something with an event. Therefore, the _handler_ should
By definition user’s handler need to implement an interface defined in
AbstractHandler
abstract class. Only mandatory method of this
class is process_event() that shall accept reference to an event. This method
shall also return a result indicating how the pipeline shall proceed from this
event.
#include "na64dp/abstractHandler.hh"
class MyHandler : public AbstractHandler {
virtual ProcRes process_event(Event &) {
cout << "Processing event." << std::endl;
return kOk; // means "proceed as usual"
}
};
Another basic need is, of course, make this handler available at a runtime (due to a fundamental C/C++ limitation, program can not not automatically discover sublcasses and we shall do it by some explicit specification). So, minimal requirements to define a new handler are:
1. Be able to process event => inherit AbstractHandler
and
implement process_event()
.
2. Make handler available for creation from config => define a function
under REGISTER_HANDLER
macro.
So, minimal working example can look like:
#include "na64dp/abstractHandler.hh"
using namespace std;
using namespace na64dp;
using namespace na64dp::event;
class MyHandler : public AbstractHandler {
virtual ProcRes process_event(Event & e) {
cout << "Processing event #" << e.id << std::endl;
return kOk;
}
};
REGISTER_HANDLER( MyHandler, mgr, cfg ) {
return new MyHandler();
}
We do not recommend using namespace
in general code as it can
potentially lead to a nasty bugs at the level of library code. However, if user
is not very familiar with namespaces, it is perfectly ok to do
using namespace
at the level of handler code.
A REGISTER_HANDLER
macro definition performs some advanced stuff
under the hood. It was written to hide some tedious compiler magic from regular
user and provide a syntax similar to general function definition. First argument
of REGISTER_HANDLER
is the name by which your handler will be known in
pipeline configuration files, two other arguments, mgr
and cfg
, we will
explain in next section – they are needed if you need additional features like
using calibration data.
This “bare minimum” handler can access all the data in an event, but it is hardly useful without knowledge of current setup details (detector names, calibration data, geometry, etc). Acces to this additional information is done (as it is common for C++) by subclassing of additional (utility) classes.
Additional Features¶
Use Calibration Data¶
By _calibrations_ we imply any kind of information that depends on the run time or run number, like list of detectors or their positions as well as _calibrations_ themself: various scaling factors, zeroes, etc.
To make the framework operate with calibration data of a certain type one have to … (out of scope of this page)
Use Detector Names¶
To perform conversions from string identifier (CORAL’s TBname, e.g. “GM01X1__”, “ECAL0”, etc) to DetID and vice versa, one have to gain access to nameutils::DetectorNaming run time-dependent data. One (direct) way is to inherit handler from calib::Handle<nameutils::DetectorNaming> making it a subscriber to naming “calibration data” updates. I.e.:
Then, in the class’ methods one can perform conversions with []
operator
of DetectorNaming
object retrieved by
calib::Handle<nameutils::DetectorNaming>::get()
:
const nameutils::DetectorNaming & naming
= calib::Handle<nameutils::DetectorNaming>::get();
DetID a = naming["GM03X1__"];
std::cout << naming[a] << std::endl;
This (direct) way is a bit tedious. Generally, we need the naming when dealing with individual hits that is a subject for next recipe that, as a side benifit, provides a nice shortcut.
Iterating Over Hits of Certain Type¶
One way is to directly iterate over the hits in an event like this:
AbstractHandler::ProcRes
process_event( event::Event & e ) {
for( auto & sadcHit : e.sadcHits ) {
std::cout << "sadcHit: " << sadcHit->eDep << std::endl;
}
for( auto & apvHit : e.apvHits ) {
std::cout << "apvHit: " << sadcHit->a02 << std::endl;
}
}
Practically, it is a pretty common case when one need to iterate over hits of single certain type (say, SADC). There are some additional features that are typically required for this iteration:
1. Selecting hits by their detector ID. Say, we would like to operate with certain projection only, or apply this particular handler to certain detector kin (like, “do for GEMs and do not for MMs”). 2. Have string name for the hit (like described in previous recipe). 3. Look up for a certain hit and do some actions actions once it is found (like remove hit from event’s collection, discriminate this event by blocking event propagation, etc).
For this common case a utility template base class AbstractHitHandler
is
introduced. It is parameterised with hit type and implements
AbstractHandler::process_event()
with a method performing pre-selection of
hits. Selected hits are forwarded to AbstractHitHandler::process_hit()
abstract method that user class have to implement. For instance:
class MyHandler : public AbstractHitHandler<SADCHit> {
public:
// Simple constructor without hit pre-selection
MyHandler( calib::Dispatcher & dsp )
: AbstractHitHandler<SADCHit>(dsp) {}
// Alternative constructor, performing hit pre-selection
MyHandler( calib::Dispatcher & dsp, std::string selection )
: AbstractHitHandler<SADCHit>(dsp, selection) {}
virtual bool process_hit(EventID eid, HitKey k, event::SADCHit & hit) {
std::cout << "Got hit from detector " << naming()[k]
<< " having energy deposition " << hit.eDep
<< std::endl;
}
};
Principles¶
Handlers development principles:
Each handler has to be intended for a single and clear purpose. Please, try to avoid handlers that do few things. I.e. tasks of deriving some value based on the event data and depicting it on a histogram would be better to keep in two dedicated handlers: a deriving one and a plotting one, passing the derived value via the event structure. This rule has resemblance to the well-known UNIX principle, and has a long history of confirmation by practical experience.
For handlers creating hits, as a rule, the event structure must not contain (or refer to) data allocated directly on heap. Instead, use the _lmem()_ returned result. Besides of the performance reasons (using pool memory allocator is generally faster than allocating on heap), this idea helps to avoid leaks and unintended dependencies.
Tend to avoid hardcoding values in the handlers. Any parameter, if possible, must be provided via the configuration file or provided with calibration data loaders. For instance, if handler plots a histogram, the binning an ranges must be provided as configuration values to constructor of this class, for the rare exception when they defined by some hardware or physics features (e.g. our SADC detectors sampling always had been of 32 samples). Yet, it is not a strict rule – a testing temporary handler usually have some static blocks because of their simplicity.