Skip to content

manalyzer

This page describes manalyzer, the midas analysis framework for C++.

For the other ways of analysing midas data, and for showing the results on web pages, see Data analysis.

API reference: manalyzer (TARunObject, TAFlowEvent, TARegister), midasio (TMEvent)

Operators who only need to run an existing analyzer can skip to Running an analyzer.

manalyzer is the midas analysis framework, in $MIDASSYS/manalyzer. It replaces ROOTANA and the old mana analyzer.

  • The same code works online and offline. An analyzer reads midas files given on its command line, or connects to the experiment and reads events from an event buffer if no files are given. It can also be built without midas, to analyse files on a computer without a midas installation
  • The analysis is split into modules. Each module does one task, e.g. unpacking the data of one detector or filling one set of histograms. Modules pass results to each other in flow events, which can hold any C++ objects
  • Objects are created and destroyed for each run. Each module creates a run object at the start of a run, and destroys it at the end. This avoids many sources of errors related to handling new runs.
  • ROOT is optional. With ROOT, histograms are saved in a ROOT file for each run, and can be watched live in a web browser with JSROOT
  • Modules can run in parallel, each in its own thread (--mt)

Writing an analyzer

Start from one of the examples in $MIDASSYS/manalyzer, e.g. manalyzer_example_cxx.cxx (pure C++) or manalyzer_example_root.cxx (with ROOT histograms). The built examples are listed in Programs. A module is a class derived from TARunObject, which overrides the methods it needs:

#include "manalyzer.h"
#include "midasio.h"

class MyModule: public TARunObject
{
public:
   MyModule(TARunInfo* runinfo) : TARunObject(runinfo)
   {
      fModuleName = "MyModule";
   }

   void BeginRun(TARunInfo* runinfo)
   {
      // create histograms, load calibrations, ...
   }

   TAFlowEvent* Analyze(TARunInfo* runinfo, TMEvent* event, TAFlags* flags, TAFlowEvent* flow)
   {
      TMBank* bank = event->FindBank("ADC0");
      if (bank) {
         const uint16_t* adc = (const uint16_t*) event->GetBankData(bank);
         // ... use the bank->data_size / 2 values
      }
      return flow;
   }

   void EndRun(TARunInfo* runinfo)
   {
      // fit histograms, print results, ...
   }
};

static TARegister tar(new TAFactoryTemplate<MyModule>);

The methods of a module

Override the TARunObject methods that your module needs. They all do nothing by default.

Method Called
Constructor At the start of each run. Set fModuleName and fModuleOrder here
BeginRun() At the start of each run. Create histograms, initialise data structures and load calibrations here
Analyze() For each midas event. Unpack the banks, do calculations and fill histograms here. Return the flow event
AnalyzeFlowEvent() For each flow event, after all the modules' Analyze(). Also for flow events from the flow event queue. See Analyze() and AnalyzeFlowEvent()
AnalyzeSpecialEvent() For the begin-of-run (0x8000) and end-of-run (0x8001) events with the ODB dumps, when reading files
NextSubrun() When the analyzer moves on to the next subrun file of the same run. BeginRun() and EndRun() aren't called
PauseRun(), ResumeRun() When the run is paused or resumed (online only)
PreEndRun() After the last midas event of the run, before EndRun(). See Processing left-over data at the end of a run
EndRun() At the end of each run. Fit histograms and save results here
Destructor At the end of each run, after EndRun()

Registering modules

There is no main() function to write, and no function to call to add a module to the analyzer. The last line of the example above does it.

static TARegister tar(new TAFactoryTemplate<MyModule>);

It creates a static TARegister object, which is constructed when the program starts, before main() runs. Its constructor adds the module's factory to manalyzer's list of modules. The main() function in manalyzer_main then uses the factories to create the modules at the start of each run.

So any module whose source file is built into the program is used, and a module is added by adding its source file to the build. If you put modules in a static library, the linker may leave out a file that nothing else refers to, and that module is then silently missing.

The order in which modules run

Each event is passed to the modules one after the other, so a module can use the flow event objects added by the modules before it. Set the order in the module's constructor with fModuleOrder: modules with a smaller number run first.

MyModule(TARunInfo* runinfo) : TARunObject(runinfo)
{
   fModuleName = "MyModule";
   fModuleOrder = 10;   // run after the modules with a smaller fModuleOrder
}

The default is 0. Modules with the same number run in the order they were registered, which depends on the compiler and linker, so always set fModuleOrder if the order matters. Use numbers between 0 and 9998: manalyzer's built-in event dump module uses -1, and its interactive module 9999. Run the analyzer with -t to print the modules in the order they run.

Module lifespan

The module objects (TARunObject) are created at the start of each run and destroyed at the end. Keep all the data for a run in them, rather than in global variables. This avoids the crashes at the end of a run, or when moving from one run to the next, that were common with older analyzers. These happened when code kept using ROOT objects that had been deleted when their ROOT file was closed.

The same applies to the objects that manalyzer passes to your methods (TARunInfo, TARootHelper and the flow events). manalyzer owns them, so don't keep pointers to them.

The order in which manalyzer calls the methods
  • When the analyzer starts: the factories' constructors and Init()
  • At the start of a run:
    • the factories' NewRunObject(), which call the modules' constructors
    • the ROOT output file is opened
    • when reading files, runinfo->fBorOdbDump is loaded and the ODB is set up from it
    • BeginRun()
    • when reading files, AnalyzeSpecialEvent() for the begin-of-run event (0x8000)
  • For each event: Analyze(), then AnalyzeFlowEvent() if there is a flow event
  • When moving to the next subrun file: AnalyzeSpecialEvent() for the end-of-run event of the old file (0x8001) and the begin-of-run event of the new file (0x8000), and NextSubrun(). BeginRun() and EndRun() aren't called
  • At the end of a run:
    • when reading files, runinfo->fEorOdbDump is loaded, and AnalyzeSpecialEvent() is called for the end-of-run event (0x8001)
    • PreEndRun(), then AnalyzeFlowEvent() for the flow events it queued
    • in multithreaded mode, manalyzer waits for all the queues to empty and the threads to stop
    • when reading files, the ODB is set up from fEorOdbDump
    • EndRun()
    • the modules' destructors
  • When the analyzer stops: the end of the run (if it hasn't happened yet), then the factories' Finish() and destructors

Data that lasts longer than one run

The factory, unlike the module, lasts for the whole time the analyzer runs. TAFactoryTemplate<MyModule> (see Registering modules) is the simplest factory.

For things that last longer than one run, write your own factory, derived from TAFactory, and keep them in it. Its methods are:

  • NewRunObject(), which you must write. It's called at the start of each run, and creates the module's run object. Pass the module a pointer to the data that the factory keeps
  • Init() (optional), which is called once when the analyzer starts, with the module's command-line arguments
  • Finish() (optional), which is called once when the analyzer stops, e.g. to print a summary of all the runs
  • Usage() (optional), which prints the module's command-line arguments in the analyzer's -h output

There's an example in Command-line arguments for your modules, and another in manalyzer_example_cxx.cxx.

Command-line arguments for your modules

manalyzer passes the arguments after -- on its command line to the modules, without looking at them:

./myana.exe run00123.mid.lz4 -- --verbose --threshold=50

Every module gets all of these arguments, so each module should ignore the ones it doesn't know. There are two ways to read them:

The arguments are in runinfo->fArgs. Read them in the module's constructor or BeginRun():

MyModule(TARunInfo* runinfo) : TARunObject(runinfo)
{
   fModuleName = "MyModule";
   for (const std::string& arg : runinfo->fArgs) {
      if (arg == "--verbose")
         fVerbose = true;
   }
}

This is the simplest way, but the arguments are read again at the start of every run, and they aren't listed in the analyzer's -h output.

Write your own factory. Its Init() gets the arguments once, when the analyzer starts, and its Usage() lists them in the analyzer's -h output. The factory keeps the settings for the whole analysis, and passes them to the module each time it creates it:

// Settings from the command line
struct MyFlags
{
   bool fVerbose = false;
   double fThreshold = 100;
};

class MyModule: public TARunObject
{
public:
   MyFlags* fFlags = nullptr;

   MyModule(TARunInfo* runinfo, MyFlags* flags) : TARunObject(runinfo), fFlags(flags)
   {
      fModuleName = "MyModule";
   }

   // ... use fFlags->fVerbose and fFlags->fThreshold
};

class MyModuleFactory: public TAFactory
{
public:
   MyFlags fFlags;

   void Usage()
   {
      printf("\tMyModule:\n");
      printf("\t--verbose: print each event\n");
      printf("\t--threshold=N: ignore ADC values below N (default 100)\n");
   }

   void Init(const std::vector<std::string>& args)
   {
      for (const std::string& arg : args) {
         if (arg == "--verbose")
            fFlags.fVerbose = true;
         else if (arg.rfind("--threshold=", 0) == 0)
            fFlags.fThreshold = std::stod(arg.substr(12));
      }
   }

   TARunObject* NewRunObject(TARunInfo* runinfo)
   {
      return new MyModule(runinfo, &fFlags);
   }
};

static TARegister tar(new MyModuleFactory);

The factory replaces TAFactoryTemplate<MyModule> in the TARegister line.

Passing results between modules

Modules pass C++ objects to each other in "flow events". For example, an ADC module may unpack and calibrate the ADC data into an array of pulse heights, which a later module combines with the TDC hits from another module. This is easier than putting the results into midas banks.

Derive the objects to pass from TAFlowEvent. A flow event is a linked list of these objects: each new object is added to the front of the list given to its constructor. Return the new front of the list from Analyze() or AnalyzeFlowEvent():

class AdcHits : public TAFlowEvent
{
public:
   std::vector<double> fPulseHeights;

   AdcHits(TAFlowEvent* flow) : TAFlowEvent(flow) {}
};

// In the ADC module
TAFlowEvent* Analyze(TARunInfo* runinfo, TMEvent* event, TAFlags* flags, TAFlowEvent* flow)
{
   AdcHits* hits = new AdcHits(flow);
   // ... fill hits->fPulseHeights from the ADC bank
   return hits;
}

// In a later module
TAFlowEvent* AnalyzeFlowEvent(TARunInfo* runinfo, TAFlags* flags, TAFlowEvent* flow)
{
   AdcHits* hits = flow->Find<AdcHits>();
   if (hits) {
      // ... use hits->fPulseHeights
   }
   return flow;
}

Find<T>() returns the first object of type T in the list, or NULL. To go through all the objects, follow fNext. manalyzer deletes the flow event and all its objects after the last module has seen it.

manalyzer_example_flow.cxx has more examples.

Analyze() and AnalyzeFlowEvent()

manalyzer passes each midas event through the modules twice:

  1. Analyze() of every module, in order. Each one gets the midas event, and the flow event returned by the module before it (NULL for the first module).
  2. AnalyzeFlowEvent() of every module, in order, with the flow event from the end of the first pass. This pass only happens if a module returned a flow event, and no module set TAFlag_SKIP or TAFlag_QUIT.
Analyze() AnalyzeFlowEvent()
Gets the midas event Yes No, only the flow event
Called for Each midas event Each flow event: the one from Analyze(), and each one from the flow event queue
Thread, in multithreaded mode The main thread, one module after another The module's own thread, in parallel with the other modules
Typical use Unpacking the midas banks into flow event objects Calculations and histograms that use the flow event objects

So a common pattern is to unpack the banks in one module's Analyze(), and to do the rest of the analysis in the modules' AnalyzeFlowEvent(). This splits the analysis into steps that run in parallel with --mt, and the same code also works for physics events from the flow event queue, which have no midas event.

Several physics events in one midas event

If a midas event contains several physics events, unpack each one into its own flow event, and add it to the flow event queue with runinfo->AddToFlowQueue():

TAFlowEvent* Analyze(TARunInfo* runinfo, TMEvent* event, TAFlags* flags, TAFlowEvent* flow)
{
   while (PhysicsEvent* e = unpack_next_physics_event(event)) {
      runinfo->AddToFlowQueue(e);
   }
   return flow;
}

After all the modules have analysed the midas event, manalyzer passes each queued flow event through the modules, as if it were a new event. Only AnalyzeFlowEvent() is called for them, because there's no midas event. The flags work the same way, and modules can add objects to the flow event. Each flow event is deleted after the last module, and then manalyzer goes on to the next midas event. manalyzer_example_flow_queue.cxx has an example.

Processing left-over data at the end of a run

Some data can only be processed at the end of a run, after the last midas event, e.g. incomplete data that an unpacker of a continuous data stream keeps between Analyze() calls, or pending events in an event builder. EndRun() is too late for this.

PreEndRun() is called after the last midas event, before EndRun(). Unpack the left-over data there into flow events, and queue them with runinfo->AddToFlowQueue(). They are then processed by the modules' AnalyzeFlowEvent(), as above.

Skipping events and stopping the analysis

Analyze() and AnalyzeFlowEvent() can set flags to change what manalyzer does with the event, e.g. *flags |= TAFlag_SKIP:

Flag Meaning
TAFlag_SKIP Don't pass this event to the later modules, e.g. in a module that filters events
TAFlag_QUIT Stop the analysis. The usual end of run is still done
TAFlag_WRITE Write this event to the output midas file (-o)
TAFlag_DISPLAY Mark this event as interesting, for the interactive mode (-i)
TAFlag_SKIP_PROFILE Leave this call out of the profiler's timing, e.g. for a method that didn't do anything

Run information (TARunInfo)

runinfo, which is passed to every method, has the information about the run being analysed. When the analyzer moves on to the next subrun file of a run, it's kept, and only the file name changes.

Member Contents
fRunNo The run number. Online, this is 0 if no run is in progress
fFileName The file being analysed. Empty when online
fOdb The ODB, as an MVOdb. Online, this is the live ODB. When reading files, it's the ODB dump in the begin-of-run event, and the one in the end-of-run event before EndRun() (midas::odb objects also read these dumps). If there isn't one, it's an empty ODB, which returns the default values
fRoot The ROOT helper. NULL if manalyzer was built without ROOT
fArgs The arguments after -- on the analyzer's command line (see Command-line arguments for your modules)
fMtInfo The multithreading queues and locks. NULL if not in multithreaded mode
fBorOdbDump, fEorOdbDump The ODB dumps from the begin-of-run and end-of-run events

ROOT output (TARootHelper)

With ROOT, runinfo->fRoot has the ROOT output file and the other ROOT objects. It's created and deleted at the same time as runinfo.

By default, ROOT objects created by the modules (e.g. with new TH1D(...)) are created in the ROOT output file of the run, root_output_files/outputNNNNN.root. They're saved when the file is closed at the end of the run, and then deleted. In JSROOT, they're shown under Files › outputNNNNN.root.

To be sure that a histogram goes into the output file, call runinfo->fRoot->fOutputFile->cd() before creating it. For objects that shouldn't be saved (e.g. event or waveform displays), call runinfo->fRoot->fgDir->cd() first. They're then kept in memory, and shown in JSROOT under manalyzer.

Member Contents
fOutputFile The ROOT output file. It's always open while the run is analysed
fOutputFileName Its name
fgDir A ROOT directory in memory, for objects that aren't saved in the output file
fgHttpServer The JSROOT web server, if the analyzer was started with -R
fgApp The ROOT graphics environment, in graphics mode (-g). NULL otherwise
fgUserOutputDirectory, fgUserOutputFileName The output directory and file name set with -D and -O

Multithreading

With --mt, each module runs in its own thread, and flow events are passed from one module's thread to the next through queues. This can be much faster when several modules do a lot of work.

Rules for code that runs in multithreaded mode:

  • Analyze() and AnalyzeSpecialEvent() are always called in the main thread. AnalyzeFlowEvent() is called in the module's own thread
  • The flow event and the flags belong to the thread that's processing them, so they don't need locking
  • A module's own data members don't need locking, because each module is only used by its own thread
  • runinfo is shared by all the threads. Lock TAMultithreadHelper::gfLock to use anything in it that isn't thread-safe (e.g. fRoot)
  • Lock TAMultithreadHelper::gfLock around code that isn't thread-safe, such as many ROOT fitting functions:

    {
       std::lock_guard<std::mutex> lock(TAMultithreadHelper::gfLock);
       histogram->Fit("gaus");
    } // unlocked here
    
  • Put objects into flow events as pointers created with new, not as local variables, and delete them in the flow event's destructor, not in a module. Once an object is in a flow event, the module that created it no longer owns it

manalyzer turns on ROOT's thread safety (ROOT::EnableThreadSafety() and ROOT::EnableImplicitMT()). See Multi-threading in ROOT for which ROOT classes are thread-safe. To find race conditions, use Helgrind. It also reports printing with std::cout from several threads, which only mixes up the output.

Compiling an analyzer

To build the analyzer, put the module in my_module.cxx, with this CMakeLists.txt next to it. The manalyzer component of midas provides the manalyzer_main library, which contains the main() function and the rest of manalyzer:

cmake_minimum_required(VERSION 3.14)
project(myana)

find_package(Midas REQUIRED COMPONENTS manalyzer PATHS $ENV{MIDASSYS})

add_executable(myana.exe my_module.cxx)
target_link_libraries(myana.exe manalyzer_main)

Then build it, with MIDASSYS set to your midas installation:

mkdir build
cd build
cmake ..
make

Several modules in one analyzer

add_executable() can list several source files, each with its own module. All the modules built into the analyzer run together, in the order of their fModuleOrder (see Registering modules). To add a module, add its source file to the list:

add_executable(myana.exe my_module.cxx adc_module.cxx tdc_module.cxx)

Running an analyzer

./myana.exe run00123.mid.lz4        # analyse one file (offline)
./myana.exe run00123*.mid.lz4       # analyse all the subrun files of a run
./myana.exe -R8082                  # connect to the experiment (online), with a JSROOT web server on port 8082
./myana.exe --demo -e100            # analyse 100 events made up by manalyzer, without data or midas
./myana.exe run00123.mid.lz4 -- -v  # pass "-v" to the modules
All command-line arguments
Option Meaning
-h List all the options, including those of your modules
-eNNN Only analyse NNN events. 0 means no limit
-sNNN Skip NNN events before starting the analysis
--demo Analyse events made up by manalyzer, without data or midas
-oFILE.mid Write the events marked with TAFlag_WRITE to this midas file
-RPORT Start a JSROOT web server on this port
--jsroot Keep the JSROOT web server running after the analysis has finished
-DDIR, -OFILE.root Write the ROOT output file to this directory, or with this name
-Hhostname, -Eexptname Connect to this experiment (online)
--midas-buffer NAME Read events from this event buffer, instead of SYSTEM
--midas-sampling MODE How to read events from the event buffer: GET_NONBLOCKING (the default: as many as the analyzer can process), GET_ALL (every event, even if this slows down the data taking) or GET_RECENT. See Blocking vs non-blocking access
--midas-event-id ID, --midas-trigger-mask MASK Only read events with this event ID or trigger mask
--midas-progname NAME The analyzer's program name in midas. The default is ana
--dump Print each event, with the event dump module
--save-odb Save the ODB dumps in the begin-of-run and end-of-run events to files
-g Graphics mode: start the ROOT graphics environment when analysing files
-i Interactive mode
--mt Multithreaded mode. --mtqlNNN sets the length of each module's queue (default 1000), and --mtseNNN and --mtsfNNN how long a thread sleeps when its queue is empty or the next queue is full (default 10 µs)
--no-profiler Turn off the profiler, which times each module
-t Print the calls to constructors, destructors and methods
-- Pass the rest of the arguments to the modules (see Command-line arguments for your modules)

Viewing analysis results

If manalyzer was built with ROOT, each run's histograms are saved as ROOT files. The default location is root_output_files/outputNNNNN.root, but this can be changed with -D and -O. You can open the file with ROOT, or view it in a web browser with mjsroot (mjsroot.exe -R8083 outputNNNNN.root).

These arguments give you other ways to see the results:

Arguments What you get
-RPORT A JSROOT web server on that port, which shows the histograms as they're filled. Open http://localhost:PORT in a web browser, or reach it through mhttpd with a proxy and a menu link (see JSROOT)
-RPORT --jsroot The same, but the web server keeps running after an offline analysis has finished, so you can look at the final histograms. Stop it with Ctrl-C
-g ROOT's graphics environment is started, so modules can draw in their own ROOT windows (e.g. a TCanvas that shows each event). See manalyzer_example_root_graphics.cxx
-i Interactive mode: after the modules have analysed each event, the analyzer stops and waits for a command (n for the next event, aNNN to analyse NNN events, c to continue to the next event marked with TAFlag_DISPLAY, q to quit, h for help). Combine it with -g or -R to look at each event's plots
--dump Summary information about each midas event is printed to screen. It doesn't show the contents of the data banks or the results of analysis modules.
-oFILE.mid The events marked with TAFlag_WRITE are written to a midas file, e.g. to keep only the interesting events

Without ROOT, the analyzer only shows what your modules print or write to files. An online analyzer can also make its results available to custom web pages, which plot them with mplot.