Consolidate helper layers in functor/selection code
The new ThOr functor framework goes to great lengths to avoid hardcoding the C++ types of functor arguments, i.e. instead of
struct TransverseMomentum {
double operator()( LHCb::Particle const* particle ) const {
return particle->pt();
}
};
we have something more like
struct TransverseMomentum {
template <typename Particle>
auto operator()( Particle const& particle ) const {
return particle.pt();
}
};
where this freedom to alter the details of the argument type allows
- distinct types for conceptually different objects, such as charged basic particles, neutral basic particles and composite particles
- technical details to be specified and used for compile-time despatch. For example:
- target level of vectorisation (scalar, SSE, AVX2, ...)
- policies around read/write behaviour (contiguous read/write, scatter/gather, compress, ...)
Compared to LoKi, this has the advantage that functors do not need to be "namespaced" (PT
for Particles, TrPT
for Tracks, and so on), but it implies that the functors have to be defined generically, without specific knowledge of their argument type.
Taking the example of computing the transverse momentum: the functor body does not know whether the relevant underlying storage is a "particle like" (p_x, p_y)
or a "track like" (t_x, t_y, q/p)
. Some convention is needed for how and where the relevant computation is included. For example:
- the relevant (charged basics, neutral basics, composites) containers could all define a
pt()
accessor, thePT
functor just calls.pt()
- an intermediate free-standing function (for example
Sel::get::pt( particle )
) contains some "if has.px()
and.py()
compute\sqrt{p_x^2+p_y^2}
else use(t_x, t_y, q/p)
..." logic), the functor just calls this helper - the logic of 2. is written directly in the functor definition
The third option is likely to involve too much fragmentation of this sort of logic, so @olupton suggests the first two are better choices.
The second option is the most powerful, and the first is arguably the simplest. There are, unfortunately, some use-cases where the first option is insufficiently powerful. This limitation arises when data structures are defined as logical "zips" of different containers, for example "charged basic particles" being a zip of tracks, mass hypotheses and PID information. The "new" zip implementation introduced in LHCb!2811 (merged) does not allow accessors to be defined that take input from multiple elements of the zip:
for ( auto row : make_zip( tracks, mass_hypotheses, ... ) {
// energy can be computed:
auto const three_momentum = row.threeMomentum(); // comes from `tracks`
auto const mass = row.mass(); // comes from `mass_hypotheses`
auto const energy = sqrt( mass * mass + three_momentum.mag2() );
// but it cannot be defined as an accessor:
row.energy(); // cannot work, because it mixes information from `tracks` and `mass_hypotheses`
}
Clearly in a large number of cases (all(?) PID information, for example) this is not a relevant limitation, but in some cases it is relevant:
- as noted above, charged basic particles where 3-momentum and assigned mass come from different places
- for charged basic particles with bremsstrahlung (see LHCb#107) the bremsstrahlung photons should (optionally?) be counted in kinematic quantities like the transverse momentum
Option number 2 listed above avoids this limitation, as a free-standing helper sees all elements of the zip. See here for an example. It seems likely that the best solution is to route a few computations via free standing helper functions (Sel::get::momentum( particle )
, Sel::get::threeMomentum( particle )
?), but omitting this indirection layer for simpler quantities (ISMUON
-> .IsMuon()
, for example).