6. Histograms and counters¶
Modern Gaudi implements histograms and counters as monitoring entities declared as members of algorithms. They are accumulated locally — concurrently if the algorithm runs on several threads —, registered with a central monitoring hub, and at the end of the job handed over to sinks for persistency (see Logging and monitoring sinks). The old “book into HistogramService, fill, and inspect in finalize” pattern is therefore replaced by “declare as member, fill, done”.
6.1. Counters¶
Counters live in Gaudi::Accumulators (header Gaudi/Accumulators.h).
The most common flavours are:
CounterAn integral counter (number of events/candidates seen, …). Has a ++ operator and nbEntries method
AveragingCounterKeeps count and mean of the values added to it. Only has a += operator, nbEntries, sum and mean methods
SigmaCounterKeep count, mean and second moment of the values added to it. Only has a += operator, the methods of
AveragingCounterplus biased_sample_variance, unbiased_sample_variance, standard_deviation and meanErr methodsStatCounterKeep count, mean, second moment, min and max of the values added to it. Only has a += operator, the methods of
SigmaCounterplus min and max methodsBinomialCounterAn efficiency counter, tracking the number of passes and of trials. Only has a += operator taking a boolean and methods nEntries, nTrueEntries, nFalseEntries, efficiency and efficiencyErr methods
Each counter class is templated with :
template <typename Arithmetic = double, atomicity Atomicity = atomicity::full>
So by default counters use double and are thread safe. Any arithmetic type
can be used and atomicity can be set to atomicity::none for faster, but
non thread safe counters.
Counters are highly extensible and can be easily composed based on Accumulators, see the documentation in Accumulator.h file.
An example of counter extension enabling easier logging is :
MsgCounterA counter also printing a given message to the message service on each call to ++ operator. Also has a mas count after which it will stop logging and just report once “Suppressing message …”. To be used for all errors and warnings.
Usage (adapted from
GaudiTestSuite/src/FunctionalAlgorithms/EventCounter.cpp):
1 2 3 4 5 | struct EventCounter final : Gaudi::Functional::Consumer<void()> {
using Consumer::Consumer;
void operator()() const override { ++m_count; }
mutable Gaudi::Accumulators::Counter<> m_count{ this, "count" };
};
|
Construction needs the owner component (
this) and a name; this is enough for the entity to be registered with theGaudi::Monitoring::Huband printed/persisted at the end of the job.Counters are
mutableby design: they are the one piece of member state algorithms are allowed to modify while running concurrently. Accumulation is thread-safe (atomic operations per entry per thread, merged when reported).Counter sets (maps of counters indexed by a string key, e.g. one counter per cut) are available as well.
6.2. Optimizing counter usage via buffers¶
When counters are thread safe, updating them involves more than a simple operation. Typically atomics are involved. In cases where this is repeated in a tigh loop, it can become costful.
A simple solution is provided by the framework under the name of buffer`s. Every counter has a ``buffer` method allowing to get a local, thread unsafe and fast version of the counter, which is automatically merged into the main counter when exiting the scope. Typical code looks like this :
1 2 3 4 5 6 | AveragingCounter<float, atomicity::full> avg;
{
auto buf = avg.buffer();
for ( int i = 0; i < 1000; i++ ) buf += i;
// single update of original counter when buf is destroyed
}
|
The buffer behave exactly like the orginal counter, except it holds only a local count if you would call nbEntries for example.
6.3. Histograms¶
Histograms are only a special typeof counters. They live in namespace
Gaudi::Accumulators (headers in Gaudi/Accumulators/ directory).
There is a variety of different Histograms available to the end user, depending on their content, behavior and need for runtime configuration or not. Let’s detail all of them
6.3.1. Histograms are templated¶
Template parameters :
template <unsigned int ND, atomicity Atomicity = atomicity::full, typename Arithmetic = double,
typename AxisTupleType = make_tuple_t<Axis<Arithmetic>, ND>>
NDthis is the dimension of the Histogram. Any dimension is supported in principle although only dimension up to 3 included can make it to disk or can be converted to ROOT histograms
Atomicitylike any counters, thread safety is the default, but atomicity::none provides a faster, non thread safe version
Arithmeticagain like any counter, the internal Arithmetic type, double as a default
AxisTupleTypethe set of Axis types for the histogram. Usually the default is all fine
6.3.2. Static or regular¶
Every histogram type exists in 2 main versions, namely the Static and regular version.
E.g. StaticHistogram<1> vs Histogram<1> for the most basic 1D histogram.
The Static version (header Gaudi/Accumulators/StaticHistogram.h)
is the prefered one but supposes knowledge of all parameters
at compile time. Here is a typical example :
1 2 3 4 5 6 7 8 9 | #include <Gaudi/Accumulators/StaticHistogram.h>
mutable Gaudi::Accumulators::StaticHistogram<1> m_hOccupancy{
this, "HistoName", "The nice title",
Gaudi::Accumulators::Axis<double>{ 100, 0., 1. } };
void operator()() const override {
++m_hOccupancy[value]; // count occurrences of 'value'
}
|
The constructor requires an owner (algorithm, service or tool), a name, a title and a list of axis matching the dimension. The default Axis class is constructed with number of bins, lower bound and upper bound plus optional title and set of labels. Once a StaticHistogram is declared, title and axis cannot be changed anymore
The non static version of Histograms (header Gaudi/Accumulators/Histogram.h)
works similarly but allows to defer the creation of the histogram, and
thus the setting of title and axis to the initialization phase.
Typical usage looks like this :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #include <Gaudi/Accumulators/Histogram.h>
mutable Gaudi::Accumulators::Histogram<1> m_hOccupancy{
this, "HistoName" };
Gaudi::Property<unsigned int> m_nbins{ this, "NBins", 100 };
StatusCode initialize() {
m_hOccupancy.setTitle( std::format( "My Histo with {} bins", m_nbins.value() ) );
m_hOccupancy.setAxis<0>( { m_nbins.value(), 0., 1. } );
}
void operator()() const override {
++m_hOccupancy[value]; // count occurrences of 'value'
}
|
6.3.3. Histogram creation time¶
Static Histograms are created when the constructor of the Algorithm / Service / Tool is executed while non static ones will only be constructed at the end of initialization by default and can thus use properties for example, as shown above.
However, we can even delay further the construction of non static histograms
and make it on demand by passing extra “false” argument to their constructor
asking to no initialize by default and then calling createHistogram by hand.
Typical code looks like :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | #include <Gaudi/Accumulators/Histogram.h>
mutable Gaudi::Accumulators::Histogram<1> m_hOccupancy{
this, "HistoName", "", {}, false }; // title and axis will be overwritten
Gaudi::Property<unsigned int> m_nbins{ this, "NBins", 100 };
Gaudi::Property<bool> m_buildHistos{ this, "BuildHistos", true };
StatusCode initialize() {
if ( m_buildHistos ) {
m_hOccupancy.setTitle( std::format( "My Histo with {} bins", m_nbins.value() ) );
m_hOccupancy.setAxis<0>( { m_nbins.value(), 0., 1. } );
m_hOccupancy.createHistogram( this );
}
}
void operator()() const override {
if ( m_buildHistos ) {
++m_hOccupancy[value]; // count occurrences of 'value'
}
}
|
Once createHistogram has been called, no further changes of title of axis
can be done obviously. createHistogram can also be called in operator().
Take care though of the thread safety, as you can call it only once
6.3.4. Histogram Properties¶
Non static histograms can be configured from python easily via Properties automatically declared for each of them in their owner :
<histo_name>_Titlewhere <histo_name> is the name of the histogram given to the C++ constructor (with all non alphanumeric characters replaced by _ if necessary) allows to set the histogram title
<histo_name>_Axis<n>same thing for <histo_name> and <n> if the axis number starting from 0. It allows to define an Axis
Example code in C++ and the python configuration :
1 2 3 4 5 6 7 8 9 10 11 12 | #include <Gaudi/Accumulators/Histogram.h>
#include <Gaudi/Functional/Consumer.h>
struct EventHisto final : Gaudi::Functional::Consumer<void(int n)> {
using Consumer::Consumer;
void operator()(int n) const override { ++m_hOccupancy[n]; }
mutable Gaudi::Accumulators::Histogram<1> m_hOccupancy{
this, "HistoName" };
};
DECLARE_COMPONENT( EventHisto )
|
1 2 3 4 5 | from GaudiConfig2 import Configurables as C
C.EventHisto( "MyhistoAlgo",
HistoName_Title = "title from python",
HistoName_Axis0 = (100, -5.0, 5.0) )
|
6.3.5. Histogram, WeightedHistogram, ProfileHistogram, RootHistogram¶
Histograms exist in 4 flavors, independently of their template parameters and the fact that they are static or not :
(Static)Histogram<...>regular histogram, with a count of entries per bin. The code to add a new entry is
++histo[n]in 1D or++histo[{i, j}]in 2D(Static)WeightedHistogram<...>A weighted variant, where each entry in the bins has a weight The code to add a new entry is thus
histo[n] += win 1D andhisto[{i, j}] += win 2D(Static)ProfileHistogram<...>A profile variant, keeping mean and spread of a quantity within each bin. The code to add in new entry is similar to the weighted case :
histo[n] += valuein 1D andhisto[{i, j}] += valuein 2D(Static)WeightedProfileHistogram<...>A weighted profile variant, keeping mean and spread of a weighted quantity within each bin. The code to add in new entry is thus :
histo[n] += { value, weight }in 1D andhisto[{i, j}] += { value, weigth }in 2D
All previously described histograms only keep values per bin, with no overall statistical data like overall mean of inserted values. This can be problematic when converting these to ROOT histograms. The missing values (sum, mean, sum of squares in 1D) can be approximated but the original value is lost. Hence the extra
(Static)RootHistogram<...>A regular histogram keeping extra global statistics, namely - sum and sum of squares (sum2) in 1D - sumx, sumy, sumx2, sumy2, sumxy in 2D - sumx, sumy, sumz, sumx2, sumy2, sumz2, sumxy, sumxz, sumyz in 3D This allows to compute the different means, standard deviations or covariances Header
Gaudi/Accumulators/(Static)RootHistogram.his required.
6.3.6. HistogramArray, HistogramMap¶
The Gaudi framework has direct support for arrays and maps of histograms
through the HistogramArray (header Gaudi/Accumulators/HistogramArray.h) and
HistogramMap (header Gaudi/Accumulators/HistogramMap.h) classes.
These are used as wrappers around std::array and std::map of histograms sharing the same axis but with different names and titles potentially. They are templated by the type of Histogram used :
1 2 3 4 5 | template <typename Histo, std::size_t N>
struct HistogramArray
template <typename Key, typename Histo>
struct HistogramMap
|
They have 2 constructors each allowing to adapt names and titles for each entry. Here is the HistogramArray versions, the HistogramMap works the same with the key type instead of integers :
1 2 3 4 5 | template <typename OWNER>
HistogramArray( OWNER* owner, std::string_view name, std::string_view title, AxisType... allAxis )
template <typename OWNER, std::invocable<int> FormatName, std::invocable<int> FormatTitle>
HistogramArray( OWNER* owner, FormatName&& fname, FormatTitle&& ftitle, AxisType... allAxis )
|
The first version requires an owner, name, title and axis. The name and title
will be used with std::format passing the index of the given histogram as first
and only parameter. Thus a string like "Title of Histo {}" will translate into
"Title of Histo 0", "Title of Histo 1", …
In the second version, fname and ftitle are callables taking an integer as input and returning the title and name for the given histogram respectivly.
Here are some examples :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | // Array of 5 1D histograms with simple names and titles
// Names will be GaudiH1D-0, GaudiH1D-1, ... and similarly for titles
Gaudi::Accumulators::HistogramArray<Gaudi::Accumulators::Histogram<1>, 5> histo1d{
&algo, "GaudiH1D-{}", "A Gaudi 1D histogram - number {}", { 21, -10.5, 10.5, "X" } };
++histo1d[3][-10.0];
// Array of 5 2D weighted histograms with simple names and titles
// Names will be Name0, Name1, ... and similarly for titles
Gaudi::Accumulators::HistogramArray<Gaudi::Accumulators::WeightedHistogram<2>, 7> histo2dw{
&algo, "Name{}", "Title {}", { 21, -10.5, 10.5, "X" }, { 21, -10.5, 10.5, "Y" } };
histo2dw[1][{ -10.0, -10.0 }] += 0.25;
// Array of histograms with custom name and titles
// Names will be GaudiH1D-0-0, GaudiH1D-1-1, GaudiH1D-2-4, ...
// Titles will be "Title 1 of 5", "Title 2 of 5", "Title 3 of 5", ...
Gaudi::Accumulators::HistogramArray<Gaudi::Accumulators::Histogram<1>, 5> histoCustom{
&algo,
[]( int n ) { return fmt::format( "GaudiH1D-{}-{}", n, n*n ); },
[nb = 5]( int n ) {
return fmt::format( "Title {} of {}", n+1, nb );
},
{ 21, -10.5, 10.5, "X" } };
++histoCustom[2][-10.0];
// Map of 1D histograms with simple names and titles, key being an int
// Names will be GaudiH1D-N and similarly for titles where N is the key associated
Gaudi::Accumulators::HistogramMap<int, Gaudi::Accumulators::Histogram<1>> histo1d{
&algo, "GaudiH1D-{}", "A Gaudi 1D histogram - number {}", { 21, -10.5, 10.5, "X" } };
++histo1d[3][-10.0];
// Map of 2D weighted histograms with simple names and titles, key being int
// Names will be Name0, Name1, ... and similarly for titles
Gaudi::Accumulators::HistogramMap<int, Gaudi::Accumulators::WeightedHistogram<2>> histo2dw{
&algo, "Name{}", "Title {}", { 21, -10.5, 10.5, "X" }, { 21, -10.5, 10.5, "Y" } };
histo2dw[1][{ -10.0, -10.0 }] += 0.25;
// Map of histograms with custom name and titles, key is a pair<int, string>
// Names will be GaudiH1D-<N>-<S>, where the key was the pair (<n>, <S>)
// Titles will be "Title <S> (<N>)"
Gaudi::Accumulators::HistogramMap<std::pair<int, std::string>, Gaudi::Accumulators::Histogram<1>> histoCustom{
&algo,
[]( std::pair<int, std::string> const& p ) { return fmt::format( "GaudiH1D-{}-{}", p.first, p.second ); },
[]( std::pair<int, std::string> const& p ) {
return fmt::format( "Title {} ({})", p.first, p.second );
},
{ 21, -10.5, 10.5, "X" } };
++histoCustom[{3, "three"}][-10.0];
|
6.4. Persistency¶
All counters and thus histograms are automatically registered to the
Gaudi::Monitoring::Hub by their owner component. At regular points
of the finite state machine — and at finalization — the hub collects
all entities and forwards them to the registered sinks (a JSON dump
for the counters, a json summary plus a ROOT file for histograms, …).
Which sinks are active is a job configuration option, described in
Logging and monitoring sinks. User code does not write histograms to files by
itself.
6.5. Legacy interfaces¶
The
IHistogramSvc-based booking API of the old guide still works for historic code but should not be used for new code: the corresponding histograms are not covered by the monitoring machinery and the interface ties user code to ROOT.GaudiAlg-style histogram helpers (book,plot…) are likewise legacy.