The Gaudi framework
Learning Objectives
- "Learn about the Gaudi framework."
Goals of the design
Gaudi is being improved with the following goals in mind:
- Improve scaling by making the interface simpler and more uniform. This allows simpler code, thereby allowing more complex code to be written.
- Improve memory usage, to be cache friendly and avoid pointers to pointers. Use C++11 move where possible.
- Thread-safe code, with no state or local threading. This allows Gaudi to be multithreaded in the future.
In order to reach those goals, the following choices should be made when designing Gaudi algorithms:
- Proper
const
usage: Since C++11,const
andmutable
are defined in terms of data races (see this video). - No global state; otherwise it would be impossible to multithread.
- Tools: should be private or public.
- Ban
beginEvent
/endEvent
, since there might be multiple events. - No explicit
new
/delete
usage (C++11 goal). - Remove
get/put<MyData>("")
for safer constructs.
Gaudi properties
One of the standout features of new Gaudi is in the new properties; they can be directly defined in the class definition instead of the implementation and used everywhere in the class, instead of requiring special calls in the constructor and custom management of the data. So the following now creates a property:
Gaudi::Property<int> m_some_int{this, "SomeInt", 0, "Description of some int"};
This defines a member variable of type Gaudi::Property<int>
, and calls the constructor on initialization with several parameters. The first is this
, a pointer to the current class. The second is the property name. The third is the default value. You can also optionally give a description.
Notes on the new syntax
The usage of this
is a common feature of the new interfaces, giving Gaudi components access to the algorithms that contain them. The location of this
is not consistent across the components, however, as you will see with AnyDataHandle
.
AnyDataHandle
The requirement to inherit from DataObject
has been lifted with AnyDataHandle
. This provides a wrapper than can be put into the TES, and can be retrieved from it as well. Assuming you have some object called Anything
, you can wrap it in AnyDataHandleWrapper
and then use the traditional get/put syntax:
auto vec = new AnyDataWrapper<Anything>(Anything{1,2,3,4});
put(vec, "/Event/Anything");
auto base = getIfExists<AnyDataWrapperBase>("/Event/Anything");
const auto i = dynamic_cast<const AnyDataWrapper<Anything>*>(base);
const Anything anything = i->getData();
This, however, is significantly convoluted. A much cleaner way to add an AnyDataHandle
is to construct it as a member of your class, just like a Gaudi::Property
, and then use the .get
and .put
methods.
AnyDataHandle<Anything> m_anything{"/Event/Anything", Gaudi::DataHandle::Writer, this};
m_anything.put(Anything{1,2,3,4});
AnyDataHandle<Anything> m_anything{"/Event/Anything", Gaudi::DataHandle::Reader, this};
const Anything* p_anything = m_anything.get();
Notes on the new syntax
Adding AnyDataHandle as a member variable breaks compatibility with the using
statement, so you will need to explicitly define the constructor.
This works by replacing inheritance with type erasure.
Gaudi::Functional
Rather than provide a series of specialised tools, the new Gaudi::
Functional` provides a general building block that is well defined and multithreading friendly. This standardizes the common pattern of getting data our of the TES, working on it, and putting it back in (in a different location). This reduces the amount of code, makes it more uniform, and encourages 'best practice' procedures in coding in the event store. The "annoying details", or scaffolding, of the design is left to the
framework.
A functional, in general, looks like this example of a transform algorithm to sum two bits of data:
class MySum
: public TransformAlgorithm<OutputData(const Input1&, const Input2&)> {
MySum(const std::string& name, ISvcLocator* pSvc)
: TransformAlgorithm(
name,
pSvc, {
KeyValue("Input1Loc", "Data1"),
KeyValue("Input2Loc", "Data2") },
KeyValue("OutputLoc", "Output/Data")) { }
OutputData operator()(const Input1& in1, const Input2& in2) const override {
return in1 + in2;
}
This example highlights several features of the new design. This starts by inheriting from a templated class, where the template defines the input and output expected. Different functionals may have no input and/or output, and might have multiple inputs or outputs. The constructor takes KeyValue objects that define the data inputs and outputs, with multiple data elements input as a initializer list. The operator()
method is overridden, and is const
to ensure thread safety
and proper design. This takes the inputs and returns an output.
The functionals available in the framework are named by the data they work on (with examples):
- One input, no output:
Consumer
Rec/RecAlgs
:EventTimeMonitor
,ProcStatusAbortMoni
,TimingTuple
- No input, one or more output:
Producer
- Should be used for File IO, constant data
- True/False only as output:
FilterPredicate
Phys/LoKiHlt
:HDRFilter
,L0Filter
,ODINFilter
Phys/LoKiGen
:MCFilter
Hlt/HltDAQ
:HltRoutingBitsFilter
Rec/LumiAlgs
:FilterFillingScheme
,FilterOnLumiSummary
- One or more input, one output:
Transformer
- One or more input, more than one output:
MultiTransformer
- Identical inputs, one output:
MergingTransformer
Calo/CaloPIDs
:InCaloAcceptanceAlg
Tr/TrackUtils
:TrackListMerger
- One input, identical outputs:
SplittingTransformer
Hlt/HltDAQ
:HltRawBankDecoderBase
- Converting a scalar transformation to a vector one:
ScalarTransformer
Calo/CaloReco
:CaloElectronAlg
,CaloSinglePhotonAlg
- This is used for vector to vector transformations where the same algorithm is applied on each vector element with 1 to 1 mapping.
A note on stateless algorithms
A stateless algorithm (one that does not store state between events) provides several important benefits:
- Thread safety
- Better scalability
- Leaner code
The downside is that a lot of code needs to be migrated.
Producer
The simplest of the functional algorithms, this produces Out1
to OutN
given nothing:
class MyProducer : public Gaudi::Functional::Consumer<int()> {
public:
MyProducer(const std::string& name, ISvcLocator* svcLoc)
: Producer( name, svcLoc,
KeyValue("OutputLocation", {"MyNumber"})) {}
int operator()() const override {
return 314;
}
}
This inherits from the templated Producer, where the template is the signature of the operator()
function. Here, it is a function that produces an int given nothing. When making the constructor, the base class is called with one more argument, the output(s) of the function as KeyValue
, where the first argment is the key (output location), and the second one is a list of locations in the TES that will be produced (here, it is /Event/MyNumber
).
The important part of the class is the operator()
, which produces an int (just 314 in this example) that gets placed on the TES. The Producer is currenlty unused in the LHCb codebase, but is intended for use for IO from files, and for random number generation.
If you want to produce several outputs, you can return a tuple, and give a list of KeyValues, one for each tuple member.
Consumer
This is a class that takes in TES data. It looks like:
class MyConsumer : public Gaudi::Functional::Consumer<void(const int&)> {
public:
MyConsumer(const std::string& name, ISvcLocator* svcLoc)
: Consumer( name, svcLoc,
KeyValue("InputLocation", {"MyNumber"})) {}
void operator()(const int& input) const override {
info << "Printing input: " << input << endmsg;
}
}
The class is created inheriting the templated class Consumer
, with the signature of the operator()
function as it's first argument as before.
The constructor should include the KeyValue
input that it will use. The Consumer in the example above takes the int we put in the TES before and prints it to the info log. The value in the TES is obtained by reference.
FilterPredicate
This blocks algorithms behind it, returns filterPassed
.
Transformers
Split or merge containers. These take one or more inputs, and produce one or more outputs.
Conversion from the old framework to the new
- Try to convert everything to a
Gaudi::Functional
algorithm. Split into smaller basic algorithms and build aSuperAlgorithm
from the smaller ones if needed. - At least try to migrate to data handles + tool handles.
- Make sure the event loop code is
const
; do not cache event dependent data. Interface code, especially for tools, should beconst
. - Try to migrate away from
beginEvent
,endEvent
incidents. - Please add as many tests as possible!
An example
An example with some of these features can be found by running:
local:~ $ svn export https://github.com/lhcb/DevelopKit/trunk/code/GaudiNewAPI GaudiNewAPI
local:~ $ cd GaudiNewAPI/GaudiFunctional
local:GaudiFunctional $ source build_and_run.sh
This package has two examples, GaudiFunctional
and GaudiDataTrans
, which use the new system to transfer data in and out of the TES using DataHandles and the functional approach. Feel free to explore and modify these examples. If you come up with a new useful example, please submit it as a PR.