Skip to content

Commit fe00a7e

Browse files
committed
Feat: add collision tagger
1 parent cc0fcac commit fe00a7e

5 files changed

Lines changed: 662 additions & 3 deletions

File tree

‎PWGCF/Femto/Core/collisionTagger.h‎

Lines changed: 362 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,362 @@
1+
// Copyright 2019-2025 CERN and copyright holders of ALICE O2.
2+
// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders.
3+
// All rights not expressly granted are reserved.
4+
//
5+
// This software is distributed under the terms of the GNU General Public
6+
// License v3 (GPL Version 3), copied verbatim in the file "COPYING".
7+
//
8+
// In applying this license CERN does not waive the privileges and immunities
9+
// granted to it by virtue of its status as an Intergovernmental Organization
10+
// or submit itself to any jurisdiction.
11+
12+
/// \file collisionTagger.h
13+
/// \brief Shared machinery for the self-configuring collision tagger (TableProducer/femtoProducerCollisionTagger.cxx)
14+
/// and for pair/triplet tasks that want to read back their own assigned tag bit(s).
15+
///
16+
/// The tagger walks the running workflow's device list once, and for every matching pair/triplet
17+
/// task instance it finds, assigns the next free bit(s) of FColTags::collisionTag to that
18+
/// instance's particle role(s), in discovery order. A consuming task cannot read the tagger's
19+
/// bit assignment back out of it directly: RunningWorkflowInfo only exposes each device's
20+
/// CLI/JSON-resolved option values, a static snapshot taken once at workflow construction -- it is
21+
/// never updated by what another device's init() computes at runtime, and there is no ordering
22+
/// guarantee between different devices' init() calls regardless. So instead, discoverTaggedRoles()
23+
/// is the single canonical implementation of the bit-assignment walk, included and called
24+
/// identically by the tagger (to assign bits and fill FColTags) and by every consuming task (to
25+
/// recompute, from the same static device list, which bit(s) ended up belonging to its own device
26+
/// name). Keeping this walk in one place is what keeps the two sides from silently drifting apart.
27+
/// \author Anton Riedel, TU München, anton.riedel@tum.de
28+
29+
#ifndef PWGCF_FEMTO_CORE_COLLISIONTAGGER_H_
30+
#define PWGCF_FEMTO_CORE_COLLISIONTAGGER_H_
31+
32+
#include "PWGCF/Femto/Core/dataTypes.h"
33+
34+
#include <CommonConstants/MathConstants.h>
35+
#include <Framework/Configurable.h>
36+
#include <Framework/DeviceSpec.h>
37+
#include <Framework/RunningWorkflowInfo.h>
38+
39+
#include <cmath>
40+
#include <string>
41+
#include <vector>
42+
43+
namespace o2::analysis::femto::collisiontagger
44+
{
45+
46+
constexpr int MaxTagBits = 8 * sizeof(datatypes::CollisionTagType);
47+
48+
// Supporting an arbitrary number of same-type "subwagons" (same task, different cuts, all running
49+
// in one train) rules out reusing Partition<T> for the cut check itself: it is a compile-time
50+
// construct, permanently bound to one specific Configurable object's fields. So instead, each
51+
// matching sibling device's configured cut VALUES are copied into a plain value struct (no
52+
// Configurable, no Partition), one push_back per discovered subwagon, and checked by hand against
53+
// each particle -- the same approach femtoDreamCollisionMasker.cxx uses for its own multi-variant
54+
// support. The check functions below are a direct, faithful port of the corresponding
55+
// MAKE_*_PARTITION macro in Core/partitions.h, just expressed as a runtime check instead of a
56+
// compiled Gandiva expression.
57+
struct TrackSelectionValues {
58+
int chargeAbs = 1;
59+
int chargeSign = 1;
60+
float ptMin = 0.f;
61+
float ptMax = 1e6f;
62+
float etaMin = -1e6f;
63+
float etaMax = 1e6f;
64+
float phiMin = 0.f;
65+
float phiMax = 1.f * o2::constants::math::TwoPI;
66+
datatypes::TrackMaskType maskLowMomentum = 0;
67+
datatypes::TrackMaskType maskHighMomentum = 0;
68+
datatypes::TrackMaskType rejectionMaskLowMomentum = 0;
69+
datatypes::TrackMaskType rejectionMaskHighMomentum = 0;
70+
float pidThres = 1e6f;
71+
};
72+
73+
struct V0SelectionValues {
74+
float ptMin = 0.f;
75+
float ptMax = 1e6f;
76+
float etaMin = -1e6f;
77+
float etaMax = 1e6f;
78+
float phiMin = 0.f;
79+
float phiMax = 1.f * o2::constants::math::TwoPI;
80+
float massMin = 0.f;
81+
float massMax = 1e6f;
82+
datatypes::V0MaskType mask = 0;
83+
int sign = 0; // only used for lambdas; k0shorts carry a dummy value for compatibility
84+
};
85+
86+
// one bit = one pair type for one task instance: both sides' selections, read together, since the
87+
// bit means "both are present", not "particle 1 is present" or "particle 2 is present" separately
88+
struct TrackTrackVariant {
89+
TrackSelectionValues sel1;
90+
TrackSelectionValues sel2;
91+
bool sameSpecies;
92+
int bit;
93+
};
94+
95+
struct TrackV0Variant {
96+
TrackSelectionValues trackSel;
97+
V0SelectionValues v0Sel;
98+
int bit;
99+
};
100+
101+
inline TrackSelectionValues readTrackSelection(o2::framework::DeviceSpec const& device, std::string const& prefix)
102+
{
103+
TrackSelectionValues v;
104+
for (auto const& option : device.options) {
105+
if (option.name == prefix + ".chargeAbs") {
106+
v.chargeAbs = option.defaultValue.get<int>();
107+
} else if (option.name == prefix + ".chargeSign") {
108+
v.chargeSign = option.defaultValue.get<int>();
109+
} else if (option.name == prefix + ".ptMin") {
110+
v.ptMin = option.defaultValue.get<float>();
111+
} else if (option.name == prefix + ".ptMax") {
112+
v.ptMax = option.defaultValue.get<float>();
113+
} else if (option.name == prefix + ".etaMin") {
114+
v.etaMin = option.defaultValue.get<float>();
115+
} else if (option.name == prefix + ".etaMax") {
116+
v.etaMax = option.defaultValue.get<float>();
117+
} else if (option.name == prefix + ".phiMin") {
118+
v.phiMin = option.defaultValue.get<float>();
119+
} else if (option.name == prefix + ".phiMax") {
120+
v.phiMax = option.defaultValue.get<float>();
121+
} else if (option.name == prefix + ".maskLowMomentum") {
122+
v.maskLowMomentum = option.defaultValue.get<datatypes::TrackMaskType>();
123+
} else if (option.name == prefix + ".maskHighMomentum") {
124+
v.maskHighMomentum = option.defaultValue.get<datatypes::TrackMaskType>();
125+
} else if (option.name == prefix + ".rejectionMaskLowMomentum") {
126+
v.rejectionMaskLowMomentum = option.defaultValue.get<datatypes::TrackMaskType>();
127+
} else if (option.name == prefix + ".rejectionMaskHighMomentum") {
128+
v.rejectionMaskHighMomentum = option.defaultValue.get<datatypes::TrackMaskType>();
129+
} else if (option.name == prefix + ".pidThres") {
130+
v.pidThres = option.defaultValue.get<float>();
131+
}
132+
}
133+
return v;
134+
}
135+
136+
inline V0SelectionValues readV0Selection(o2::framework::DeviceSpec const& device, std::string const& prefix)
137+
{
138+
V0SelectionValues v;
139+
for (auto const& option : device.options) {
140+
if (option.name == prefix + ".ptMin") {
141+
v.ptMin = option.defaultValue.get<float>();
142+
} else if (option.name == prefix + ".ptMax") {
143+
v.ptMax = option.defaultValue.get<float>();
144+
} else if (option.name == prefix + ".etaMin") {
145+
v.etaMin = option.defaultValue.get<float>();
146+
} else if (option.name == prefix + ".etaMax") {
147+
v.etaMax = option.defaultValue.get<float>();
148+
} else if (option.name == prefix + ".phiMin") {
149+
v.phiMin = option.defaultValue.get<float>();
150+
} else if (option.name == prefix + ".phiMax") {
151+
v.phiMax = option.defaultValue.get<float>();
152+
} else if (option.name == prefix + ".massMin") {
153+
v.massMin = option.defaultValue.get<float>();
154+
} else if (option.name == prefix + ".massMax") {
155+
v.massMax = option.defaultValue.get<float>();
156+
} else if (option.name == prefix + ".mask") {
157+
v.mask = option.defaultValue.get<datatypes::V0MaskType>();
158+
} else if (option.name == prefix + ".sign") {
159+
v.sign = option.defaultValue.get<int>();
160+
}
161+
}
162+
return v;
163+
}
164+
165+
// reads the current value of a plain (non-group-prefixed) boolean option on a sibling device,
166+
// e.g. one of its doprocessXXX-backed PROCESS_SWITCH flags
167+
inline bool hasEnabledOption(o2::framework::DeviceSpec const& device, std::string const& name)
168+
{
169+
for (auto const& option : device.options) {
170+
if (option.name == name) {
171+
return option.defaultValue.get<bool>();
172+
}
173+
}
174+
return false;
175+
}
176+
177+
// mirrors MAKE_TRACK_PARTITION(selection) in Core/partitions.h
178+
template <typename T>
179+
bool trackPasses(T const& track, TrackSelectionValues const& sel)
180+
{
181+
if (sel.chargeSign != 0 && ((sel.chargeSign > 0) != (track.signedPt() > 0.f))) {
182+
return false;
183+
}
184+
float absPt = std::fabs(sel.chargeAbs * track.signedPt());
185+
if (!(absPt > sel.ptMin && absPt < sel.ptMax)) {
186+
return false;
187+
}
188+
if (!(track.eta() > sel.etaMin && track.eta() < sel.etaMax)) {
189+
return false;
190+
}
191+
if (!(track.phi() > sel.phiMin && track.phi() < sel.phiMax)) {
192+
return false;
193+
}
194+
float momentum = absPt * std::cosh(track.eta());
195+
if (momentum <= sel.pidThres) {
196+
return (track.mask() & sel.maskLowMomentum) == sel.maskLowMomentum &&
197+
(track.mask() & sel.rejectionMaskLowMomentum) == static_cast<datatypes::TrackMaskType>(0);
198+
}
199+
return (track.mask() & sel.maskHighMomentum) == sel.maskHighMomentum &&
200+
(track.mask() & sel.rejectionMaskHighMomentum) == static_cast<datatypes::TrackMaskType>(0);
201+
}
202+
203+
// mirrors MAKE_LAMBDA_PARTITION(selection) in Core/partitions.h
204+
template <typename T>
205+
bool lambdaPasses(T const& v0, V0SelectionValues const& sel)
206+
{
207+
if (sel.sign != 0 && ((sel.sign > 0) != (v0.signedPt() > 0.f))) {
208+
return false;
209+
}
210+
float absPt = std::fabs(v0.signedPt());
211+
if (!(absPt > sel.ptMin && absPt < sel.ptMax)) {
212+
return false;
213+
}
214+
if (!(v0.eta() > sel.etaMin && v0.eta() < sel.etaMax)) {
215+
return false;
216+
}
217+
if (!(v0.phi() > sel.phiMin && v0.phi() < sel.phiMax)) {
218+
return false;
219+
}
220+
if (!(v0.mass() > sel.massMin && v0.mass() < sel.massMax)) {
221+
return false;
222+
}
223+
return (v0.mask() & sel.mask) == sel.mask;
224+
}
225+
226+
// mirrors MAKE_K0SHORT_PARTITION(selection) in Core/partitions.h
227+
template <typename T>
228+
bool k0shortPasses(T const& v0, V0SelectionValues const& sel)
229+
{
230+
if (!(v0.pt() > sel.ptMin && v0.pt() < sel.ptMax)) {
231+
return false;
232+
}
233+
if (!(v0.eta() > sel.etaMin && v0.eta() < sel.etaMax)) {
234+
return false;
235+
}
236+
if (!(v0.phi() > sel.phiMin && v0.phi() < sel.phiMax)) {
237+
return false;
238+
}
239+
if (!(v0.mass() > sel.massMin && v0.mass() < sel.massMax)) {
240+
return false;
241+
}
242+
return (v0.mask() & sel.mask) == sel.mask;
243+
}
244+
245+
// Role names and device-name substrings, declared once: a consuming task needs the exact same
246+
// role spelling discoverTaggedRoles() below uses, to look its own bit back up via findOwnBit() --
247+
// fed in as ConfTagging's template argument below, so a task's role is fixed at compile time
248+
// rather than a loose string repeated at every call site. Declared as named `char` arrays, not
249+
// `const char*` pointing at string literals: a pointer non-type template argument must be the
250+
// address of a named object with static storage duration, and the standard specifically
251+
// disallows forming that address from a string literal directly (two identical literals aren't
252+
// guaranteed the same address). A named array has none of that ambiguity, and decays to
253+
// `const char*` on its own wherever a plain pointer is expected (ConfTagging<role::TrackTrackPair>
254+
// below, the std::string construction in discoverTaggedRoles(), etc.). `inline` (not just
255+
// `constexpr`) matters too: without it, a namespace-scope variable has internal linkage, so each
256+
// translation unit would get its own copy at its own address -- fine for a value, but the same
257+
// template instantiation must denote the same entity everywhere it's used. `inline` guarantees
258+
// one shared address. The device-name substrings are, unavoidably, the literal
259+
// o2physics_add_dpl_workflow() names from the respective CMakeLists.txt -- there is no way to
260+
// derive those at compile time, so they are hardcoded here, exactly once, rather than repeated at
261+
// every call site.
262+
namespace role
263+
{
264+
inline constexpr char TrackTrackPair[] = "TrackTrackPair";
265+
inline constexpr char TrackLambdaPair[] = "TrackLambdaPair";
266+
inline constexpr char TrackK0shortPair[] = "TrackK0shortPair";
267+
} // namespace role
268+
269+
namespace devicename
270+
{
271+
constexpr const char* FemtoPairTrackTrack = "femto-pair-track-track";
272+
constexpr const char* FemtoPairTrackV0 = "femto-pair-track-v0";
273+
} // namespace devicename
274+
275+
// One assigned bit of FColTags::collisionTag: `deviceName` is the matched sibling device's own
276+
// (unique) DPL device name, `role` names which particle-selection group of that device the bit
277+
// covers (e.g. "TrackSelection1", "LambdaSelection1"), and `bit` is its position in collisionTag.
278+
// One assigned bit means "this collision has what it takes to form a pair of this TYPE": both the
279+
// particle-1-role candidate and the particle-2-role candidate (or, for a same-species pair, at
280+
// least 2 distinct candidates passing the one shared selection) are present. One bit per pair type
281+
// per task instance -- not one bit per particle role -- since during mixing the same bit is
282+
// checked identically on both the anchor and the partner collision (either one could end up
283+
// supplying either role across the full mixing sweep).
284+
struct TaggedRole {
285+
std::string deviceName;
286+
std::string role;
287+
int bit;
288+
};
289+
290+
// The single canonical bit-assignment walk: scans the workflow's device list in order and, for
291+
// every recognized pair/triplet task instance found, consumes the next free bit(s) for its
292+
// role(s) -- exactly what femtoProducerCollisionTagger.cxx uses to decide what to tag, and what a
293+
// consuming task calls to recompute (from the same static device list) which bit(s) ended up
294+
// being its own. Keep this in sync with femtoProducerCollisionTagger.cxx's supported task types: whichever
295+
// task types are added there must be added here identically, in the same order, or the tagger and
296+
// its consumers will silently disagree about which bit means what.
297+
inline std::vector<TaggedRole> discoverTaggedRoles(o2::framework::RunningWorkflowInfo const& workflows)
298+
{
299+
std::vector<TaggedRole> roles;
300+
int counter = 0;
301+
for (o2::framework::DeviceSpec const& device : workflows.devices) {
302+
if (device.name.find(devicename::FemtoPairTrackTrack) != std::string::npos) {
303+
// Gated on the one process function that actually consumes this bit: an instance that
304+
// never enables processMixedEventWithTagging will never look this bit up, so assigning it
305+
// one anyway would just waste a slot out of the hard-capped budget (MaxTagBits below) for
306+
// nothing.
307+
if (hasEnabledOption(device, "processMixedEventWithTagging")) {
308+
roles.push_back({device.name, role::TrackTrackPair, counter++});
309+
}
310+
} else if (device.name.find(devicename::FemtoPairTrackV0) != std::string::npos) {
311+
// Each species gates independently on its own *MixedEventWithTagging switch -- Lambda and
312+
// K0short are activated completely independently of each other (e.g. same-event-only for
313+
// one species, tagged-mixed-event for the other is a legitimate combination), and, same as
314+
// TrackTrackPair above, same-event/Mc/WithFitVars variants are irrelevant here since none
315+
// of them ever consume a tag bit.
316+
if (hasEnabledOption(device, "processLambdaMixedEventWithTagging")) {
317+
roles.push_back({device.name, role::TrackLambdaPair, counter++});
318+
}
319+
if (hasEnabledOption(device, "processK0shortMixedEventWithTagging")) {
320+
roles.push_back({device.name, role::TrackK0shortPair, counter++});
321+
}
322+
}
323+
// TODO: add the remaining pair/triplet task types (track-cascade, track-kink, v0-v0, d0-d0,
324+
// resonances, triplets, ...), following the same pattern. Whatever is added here must also be
325+
// added to femtoProducerCollisionTagger.cxx's init(), in the same order, since that is where the
326+
// selection VALUES behind each of these bits actually get read and checked.
327+
}
328+
return roles;
329+
}
330+
331+
// Convenience for a consuming task: find the bit this device's own `role` was assigned, or -1 if
332+
// it was never reached (e.g. this device's own process switch wasn't enabled on the tagger, so it
333+
// never requested this role at all).
334+
inline int findOwnBit(std::vector<TaggedRole> const& roles, std::string const& ownDeviceName, std::string const& role)
335+
{
336+
for (auto const& r : roles) {
337+
if (r.deviceName == ownDeviceName && r.role == role) {
338+
return r.bit;
339+
}
340+
}
341+
return -1;
342+
}
343+
344+
// Carries a consuming task's own, fixed pair-type role (Role, one of the role:: constants above,
345+
// supplied at the instantiation site -- e.g. ConfTagging<role::TrackTrackPair> confTagging;) and
346+
// the bit that role was assigned, discovered once in init() into `mask` (not read from the JSON
347+
// config). Bundling both together means a call like findOwnBit(roles, ownDevice.name,
348+
// confTagging.role) always uses the exact role this same object's mask gets set for, instead of
349+
// the role being a second, independently-spelled literal at the call site. Every task that opts
350+
// into tagger-filtered mixing instantiates one of these; wrapped in a ConfigurableGroup (rather
351+
// than a bare Configurable) so more fields can be added here later without having to touch every
352+
// consumer.
353+
template <char const* role>
354+
struct ConfTagging : o2::framework::ConfigurableGroup {
355+
std::string prefix = std::string("Tagging");
356+
static constexpr char const* Role = role;
357+
o2::framework::Configurable<datatypes::CollisionTagType> internalTagMask{"internalTagMask", 0, "internal: discovered collision tagger bitmask, set automatically in init() -- do not set manually"};
358+
};
359+
360+
} // namespace o2::analysis::femto::collisiontagger
361+
362+
#endif // PWGCF_FEMTO_CORE_COLLISIONTAGGER_H_

‎PWGCF/Femto/DataModel/FemtoTables.h‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,18 @@ DECLARE_SOA_TABLE_STAGED_VERSIONED(FColMasks_001, "FCOLMASK", 1, //! collision m
143143
using FColMasks = FColMasks_001;
144144
using StoredFColMasks = StoredFColMasks_001;
145145

146+
// table for collision tags: one bit per (wagon, pair type) combination the collision tagger was
147+
// self-configured to check, set if the collision has what it takes to form at least one pair of
148+
// that type (both particle-1-role and particle-2-role candidates present, or, for a same-species
149+
// pair, at least 2 distinct candidates passing the one shared selection). Used to pre-filter the
150+
// mixed-event pool instead of discovering empty collisions only after slicing. Which bit means
151+
// what is assigned at self-configuration time and logged by the tagger; see
152+
// PWGCF/Femto/TableProducer/femtoProducerCollisionTagger.cxx.
153+
DECLARE_SOA_TABLE_STAGED_VERSIONED(FColTags_001, "FCOLTAG", 1, //! collision tags
154+
femtocollisions::CollisionTag);
155+
using FColTags = FColTags_001;
156+
using StoredFColTags = StoredFColTags_001;
157+
146158
DECLARE_SOA_TABLE_STAGED_VERSIONED(FColSphericities_001, "FCOLSPHERICITY", 1, //! sphericity
147159
femtocollisions::Sphericity);
148160
using FColSphericities = FColSphericities_001;

‎PWGCF/Femto/TableProducer/CMakeLists.txt‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ o2physics_add_dpl_workflow(femto-producer
1414
PUBLIC_LINK_LIBRARIES O2Physics::AnalysisCore O2Physics::EventFilteringUtils O2Physics::PWGEMPhotonMesonCore
1515
COMPONENT_NAME Analysis)
1616

17+
o2physics_add_dpl_workflow(femto-producer-collision-tagger
18+
SOURCES ./femtoProducerCollisionTagger.cxx
19+
PUBLIC_LINK_LIBRARIES O2::Framework O2Physics::AnalysisCore
20+
COMPONENT_NAME Analysis)
21+
1722
o2physics_add_dpl_workflow(femto-producer-derived-to-derived
1823
SOURCES ./femtoProducerDerivedToDerived.cxx
1924
PUBLIC_LINK_LIBRARIES O2::Framework O2Physics::AnalysisCore

0 commit comments

Comments
 (0)