.. _handlers dev: 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 ``Handle`` or ``AbstractHitHandler`` Iterate over hits of certain type inherit ``AbstractHitHandler`` or directly use event's hits collection ROOT histogram in certain ``TDirectory`` inherit ``TDirAdapter`` Use calibration data Inherit ``Handle`` Create a hit in an event Use ``lmem().create()`` template ========================================== ======================================================================= 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 :cpp:class:`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. .. code-block:: cpp #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 :cpp:class:`AbstractHandler` and implement ``process_event()``. 2. *Make handler available for creation from config* => define a function under :c:macro:`REGISTER_HANDLER` macro. So, minimal working example can look like: .. code-block:: cpp #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 :c:macro:`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 :c:macro:`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` making it a subscriber to naming "calibration data" updates. I.e.: .. code-block::cpp class MyHandler : calib::Handle { MyHandler( calib::Dispatcher & dsp ) : calib::Handle(dsp) { } }; Then, in the class' methods one can perform conversions with ``[]`` operator of :cpp:class:`DetectorNaming` object retrieved by ``calib::Handle::get()``: .. code-block:: cpp const nameutils::DetectorNaming & naming = calib::Handle::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: .. code-block:: cpp 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 :cpp: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: .. code-block:: cpp class MyHandler : public AbstractHitHandler { public: // Simple constructor without hit pre-selection MyHandler( calib::Dispatcher & dsp ) : AbstractHitHandler(dsp) {} // Alternative constructor, performing hit pre-selection MyHandler( calib::Dispatcher & dsp, std::string selection ) : AbstractHitHandler(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. .. _UNIX principle: https://en.wikipedia.org/wiki/Unix_philosophy