Skip to content

Commit a5d697b

Browse files
committed
ALICE3: TRK and FT3 topology dictionaries per layer, their builder and macro
Signed-off-by: Felix Schlepper <felix.schlepper@cern.ch>
1 parent 86084e0 commit a5d697b

9 files changed

Lines changed: 1032 additions & 0 deletions

File tree

‎Detectors/Upgrades/ALICE3/TRKFT3/common/CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@
1212
add_subdirectory(simulation)
1313
add_subdirectory(reconstruction)
1414
add_subdirectory(workflow)
15+
add_subdirectory(macros)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copyright 2019-2020 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+
13+
o2_add_test_root_macro(CheckTopologyDictionary.C
14+
PUBLIC_LINK_LIBRARIES O2::TRKReconstruction
15+
O2::DetectorsBase
16+
O2::SimulationDataFormat
17+
LABELS alice3 COMPILE_ONLY)
Lines changed: 341 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,341 @@
1+
// Copyright 2019-2026 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 CheckTopologyDictionary.C
13+
/// \brief Builds the TRK or FT3 topology dictionary from MC timeframes and checks it on others
14+
///
15+
/// Each timeframe directory holds the clusters with their MC labels
16+
/// (o2clus_trk.root, o2clus_ft3.root), the signal hits (sgn_HitsTRK.root,
17+
/// sgn_HitsFT3.root) and the geometry. A signal cluster is taken with its
18+
/// particle's crossing of the chip, except at the matrix's edge (it may have
19+
/// lost pixels) and where the particle left more clusters on the chip than it
20+
/// crossed it (pieces of one crossing).
21+
///
22+
/// With training timeframes the dictionary is built from them and written to
23+
/// \a dictionaryFile, else read from it. On the check timeframes it prints per
24+
/// layer the pulls residual / width without and with the offsets (median and
25+
/// normalised MAD) and writes per entry the residuals' count, median and
26+
/// width beside the entry's (tree "entries" in \a output).
27+
///
28+
/// root -b -q 'CheckTopologyDictionary.C+("TRK", "TRKTopologyDictionary.root", {"tf7", "tf8"}, {"tf2", "tf3"})'
29+
30+
#if !defined(__CLING__) || defined(__ROOTCLING__)
31+
#include "DataFormatsTRKFT3/Cluster.h"
32+
#include "DataFormatsTRKFT3/Hit.h"
33+
#include "DetectorsBase/GeometryManager.h"
34+
#include "DetectorsCommonDataFormats/DetID.h"
35+
#include "FT3Base/GeometryTGeo.h"
36+
#include "MathUtils/Utils.h"
37+
#include "SimulationDataFormat/MCCompLabel.h"
38+
#include "SimulationDataFormat/MCTruthContainer.h"
39+
#include "TRKBase/GeometryTGeo.h"
40+
#include "TRKBase/Specs.h"
41+
#include "TRKReconstruction/BuildTopologyDictionary.h"
42+
#include <TFile.h>
43+
#include <TTree.h>
44+
#include <fairlogger/Logger.h>
45+
#include <fmt/format.h>
46+
#include <algorithm>
47+
#include <array>
48+
#include <cmath>
49+
#include <limits>
50+
#include <map>
51+
#include <memory>
52+
#include <span>
53+
#include <stdexcept>
54+
#include <string>
55+
#include <tuple>
56+
#include <vector>
57+
#endif
58+
59+
namespace check_dictionary
60+
{
61+
62+
using o2::math_utils::Point3D;
63+
64+
template <int DetID>
65+
auto& geometry()
66+
{
67+
if constexpr (DetID == o2::detectors::DetID::TRK) {
68+
return *o2::trk::GeometryTGeo::Instance();
69+
} else {
70+
return *o2::ft3::GeometryTGeo::Instance();
71+
}
72+
}
73+
74+
/// The median and normalised MAD of \a values, which it reorders.
75+
std::pair<float, float> medianAndWidth(std::vector<float>& values)
76+
{
77+
const auto median = [](std::vector<float>& v) {
78+
const auto middle = v.begin() + static_cast<std::ptrdiff_t>(v.size() / 2);
79+
std::nth_element(v.begin(), middle, v.end());
80+
return v.size() % 2 == 1 ? *middle : 0.5f * (*std::max_element(v.begin(), middle) + *middle);
81+
};
82+
if (values.empty()) {
83+
return {0.f, 0.f};
84+
}
85+
const float m = median(values);
86+
std::vector<float> deviations(values.size());
87+
std::ranges::transform(values, deviations.begin(), [m](float v) { return std::abs(v - m); });
88+
return {m, 1.4826f * median(deviations)};
89+
}
90+
91+
/// The o2sim tree of \a path, held open by \a file.
92+
TTree& treeOf(std::unique_ptr<TFile>& file, const std::string& path)
93+
{
94+
file.reset(TFile::Open(path.c_str()));
95+
auto* tree = file && !file->IsZombie() ? file->Get<TTree>("o2sim") : nullptr;
96+
if (tree == nullptr) {
97+
throw std::runtime_error(fmt::format("{}: no o2sim tree", path));
98+
}
99+
return *tree;
100+
}
101+
102+
/// Reads entry 0 of \a tree's branch \a name into \a object.
103+
template <typename T>
104+
void readBranch(TTree& tree, const std::string& name, T& object)
105+
{
106+
auto* branch = tree.GetBranch(name.c_str());
107+
if (branch == nullptr) {
108+
throw std::runtime_error(fmt::format("no branch {}", name));
109+
}
110+
auto* pointer = &object;
111+
branch->SetAddress(&pointer);
112+
branch->GetEntry(0);
113+
branch->ResetAddress();
114+
}
115+
116+
/// Calls \a f(cluster, pattern, start, end) for the selected signal clusters
117+
/// of \a directory, with their particle's crossing in O2's local frame.
118+
template <int DetID, typename F>
119+
void forEachCluster(const std::string& directory, F&& f)
120+
{
121+
using Dictionary = o2::trkft3::TopologyDictionary<DetID>;
122+
using ClusterType = o2::trkft3::Cluster<DetID>;
123+
const std::string name = o2::detectors::DetID(DetID).getName();
124+
struct Segment {
125+
int track, chip;
126+
Point3D<float> start, end; ///< global frame
127+
};
128+
const auto keyOf = [](const Segment& segment) { return std::pair{segment.track, segment.chip}; };
129+
130+
// The signal hits, by event, sorted by particle and chip.
131+
std::vector<std::vector<Segment>> events;
132+
{
133+
std::unique_ptr<TFile> file;
134+
auto& tree = treeOf(file, fmt::format("{}/sgn_Hits{}.root", directory, name));
135+
std::vector<o2::trkft3::Hit> hits, *hitsPointer = &hits;
136+
tree.SetBranchAddress(fmt::format("{}Hit", name).c_str(), &hitsPointer);
137+
events.resize(tree.GetEntries());
138+
for (std::size_t event = 0; event < events.size(); ++event) {
139+
tree.GetEntry(static_cast<Long64_t>(event));
140+
for (const auto& hit : hits) {
141+
events[event].push_back({hit.GetTrackID(), hit.GetDetectorID(), {hit.GetStartX(), hit.GetStartY(), hit.GetStartZ()}, {hit.GetX(), hit.GetY(), hit.GetZ()}});
142+
}
143+
std::ranges::sort(events[event], {}, keyOf);
144+
}
145+
tree.ResetBranchAddresses();
146+
}
147+
148+
std::unique_ptr<TFile> file;
149+
auto& tree = treeOf(file, fmt::format("{}/o2clus_{}.root", directory, DetID == o2::detectors::DetID::TRK ? "trk" : "ft3"));
150+
for (int stream = 0; tree.GetBranch(fmt::format("{}ClusterComp_{}", name, stream).c_str()) != nullptr; ++stream) {
151+
std::vector<ClusterType> clusters;
152+
std::vector<unsigned char> patterns;
153+
o2::dataformats::MCTruthContainer<o2::MCCompLabel> labels;
154+
readBranch(tree, fmt::format("{}ClusterComp_{}", name, stream), clusters);
155+
readBranch(tree, fmt::format("{}ClusterPatt_{}", name, stream), patterns);
156+
readBranch(tree, fmt::format("{}ClusterMCTruth_{}", name, stream), labels);
157+
158+
// The signal clusters with their particle's hits on the chip; the clusters of each particle and chip.
159+
struct Candidate {
160+
const ClusterType* cluster;
161+
const unsigned char* pattern;
162+
std::span<const Segment> segments;
163+
std::tuple<int, int, int> key; ///< event, track, chip
164+
};
165+
std::vector<Candidate> candidates;
166+
std::map<std::tuple<int, int, int>, int> nClusters;
167+
const unsigned char* next = patterns.data();
168+
for (std::size_t i = 0; i < clusters.size(); ++i) {
169+
const auto& cluster = clusters[i];
170+
const auto* pattern = next;
171+
next += 2 + ((pattern[0] * pattern[1] + 7) / 8);
172+
const auto ofCluster = labels.getLabels(i);
173+
const auto signal = std::find_if(ofCluster.begin(), ofCluster.end(), [](const auto& label) { return label.isValid() && !label.isNoise() && label.getSourceID() == 0; });
174+
if (signal == ofCluster.end()) {
175+
continue;
176+
}
177+
const auto& segments = events.at(signal->getEventID());
178+
const auto range = std::ranges::equal_range(segments, std::pair{signal->getTrackID(), static_cast<int>(cluster.chipID)}, {}, keyOf);
179+
if (range.empty()) {
180+
continue;
181+
}
182+
candidates.push_back({&cluster, pattern, {range.begin(), range.end()}, {signal->getEventID(), signal->getTrackID(), cluster.chipID}});
183+
++nClusters[candidates.back().key];
184+
}
185+
186+
for (const auto& candidate : candidates) {
187+
const auto& cluster = *candidate.cluster;
188+
const auto* pattern = candidate.pattern;
189+
// Left out: at the matrix's edge, or one of several pieces of a crossing.
190+
const bool vd = cluster.subDetID == 0;
191+
const int nRows = vd ? o2::trk::constants::VD::petal::layer::nRows[cluster.layer] : o2::trk::constants::moduleMLOT::chip::nRows;
192+
const int nCols = vd ? o2::trk::constants::VD::petal::layer::nCols : o2::trk::constants::moduleMLOT::chip::nCols;
193+
if (cluster.row == 0 || cluster.col == 0 || cluster.row + pattern[0] >= nRows || cluster.col + pattern[1] >= nCols ||
194+
nClusters[candidate.key] > static_cast<int>(candidate.segments.size())) {
195+
continue;
196+
}
197+
// A particle crossing the chip more than once: the crossing whose middle is nearest the centre of gravity.
198+
const auto& l2g = geometry<DetID>().getMatrixL2G(cluster.chipID);
199+
const auto cog = Dictionary::centreOfGravity(cluster, pattern);
200+
float nearest = std::numeric_limits<float>::max();
201+
Point3D<float> start, end;
202+
for (const auto& segment : candidate.segments) {
203+
const auto a = l2g ^ segment.start, b = l2g ^ segment.end;
204+
const auto fa = Dictionary::localToFlat(cluster, a), fb = Dictionary::localToFlat(cluster, b);
205+
const float distance = std::hypot(0.5f * (fa.X() + fb.X()) - cog.X(), 0.5f * (fa.Z() + fb.Z()) - cog.Z());
206+
if (distance < nearest) {
207+
nearest = distance;
208+
start = a;
209+
end = b;
210+
}
211+
}
212+
f(cluster, pattern, start, end);
213+
}
214+
}
215+
LOGP(info, "{}: read", directory);
216+
}
217+
218+
template <int DetID>
219+
void check(const std::string& dictionaryFile, const std::vector<std::string>& timeframes, const std::vector<std::string>& training, const std::string& output,
220+
bool applyOffsets, int nThreads)
221+
{
222+
using Dictionary = o2::trkft3::TopologyDictionary<DetID>;
223+
const auto& directories = training.empty() ? timeframes : training;
224+
if (directories.empty()) {
225+
throw std::invalid_argument("no timeframe directories");
226+
}
227+
o2::base::GeometryManager::loadGeometry(directories.front() + "/o2sim_geometry-aligned.root");
228+
geometry<DetID>().fillMatrixCache(o2::math_utils::bit2Mask(o2::math_utils::TransformType::L2G));
229+
230+
std::unique_ptr<Dictionary> dictionary;
231+
if (!training.empty()) {
232+
int nLayers = 0;
233+
if constexpr (DetID == o2::detectors::DetID::TRK) {
234+
nLayers = o2::trk::constants::VD::petal::nLayers + geometry<DetID>().getNumberOfLayersMLOT();
235+
} else {
236+
nLayers = geometry<DetID>().getNumberOfLayers();
237+
}
238+
o2::trkft3::BuildTopologyDictionary<DetID> builder(nLayers, {.nThreads = nThreads});
239+
for (const auto& directory : training) {
240+
forEachCluster<DetID>(directory, [&](const auto& cluster, const auto* pattern, const auto& start, const auto& end) { builder.accountCluster(cluster, pattern, start, end); });
241+
}
242+
builder.build();
243+
builder.setApplyOffsets(applyOffsets);
244+
builder.saveDictionaryRoot(dictionaryFile);
245+
dictionary = std::make_unique<Dictionary>(builder.getDictionary());
246+
LOGP(info, "written to {}", dictionaryFile);
247+
} else {
248+
dictionary.reset(Dictionary::loadFrom(dictionaryFile));
249+
}
250+
dictionary->print();
251+
if (timeframes.empty()) {
252+
return;
253+
}
254+
const bool applied = dictionary->getApplyOffsets();
255+
256+
// The residuals hit - centre of gravity at the layer's depth, by layer and entry, across and along.
257+
const int nLayers = dictionary->getNumberOfLayers();
258+
dictionary->setApplyOffsets(false);
259+
std::vector<std::map<int, std::array<std::vector<float>, 2>>> residuals(nLayers);
260+
for (const auto& directory : timeframes) {
261+
forEachCluster<DetID>(directory, [&](const auto& cluster, const auto* pattern, const auto& start, const auto& end) {
262+
const int layer = Dictionary::layerOf(cluster);
263+
if (layer < 0 || layer >= nLayers) {
264+
throw std::runtime_error(fmt::format("{}: on no layer of the dictionary's {}", cluster.asString(), nLayers));
265+
}
266+
const int id = dictionary->findID(layer, pattern);
267+
const auto position = dictionary->getFlatCoordinates(cluster, pattern, id);
268+
const auto a = Dictionary::localToFlat(cluster, start), b = Dictionary::localToFlat(cluster, end);
269+
const float t = b.Y() != a.Y() ? (position.Y() - a.Y()) / (b.Y() - a.Y()) : -1.f;
270+
if (t >= 0.f && t <= 1.f) {
271+
auto& entry = residuals[layer][id];
272+
entry[0].push_back(a.X() + (t * (b.X() - a.X())) - position.X());
273+
entry[1].push_back(a.Z() + (t * (b.Z() - a.Z())) - position.Z());
274+
}
275+
});
276+
}
277+
278+
const std::unique_ptr<TFile> out{TFile::Open(output.c_str(), "RECREATE")};
279+
auto* entries = new TTree("entries", "per layer and entry, across (x) and along (z): the residuals against the entry [cm]");
280+
int tLayer = 0, tId = 0, tCount = 0;
281+
bool tGroup = false;
282+
std::array<float, 2> tMedian{}, tWidth{}, tEntryOffset{}, tEntryWidth{};
283+
entries->Branch("layer", &tLayer);
284+
entries->Branch("id", &tId);
285+
entries->Branch("isGroup", &tGroup);
286+
entries->Branch("count", &tCount);
287+
for (int axis = 0; axis < 2; ++axis) {
288+
const char* a = axis == 0 ? "x" : "z";
289+
entries->Branch(fmt::format("{}Median", a).c_str(), &tMedian[axis]);
290+
entries->Branch(fmt::format("{}Width", a).c_str(), &tWidth[axis]);
291+
entries->Branch(fmt::format("{}EntryOffset", a).c_str(), &tEntryOffset[axis]);
292+
entries->Branch(fmt::format("{}EntryWidth", a).c_str(), &tEntryWidth[axis]);
293+
}
294+
LOGP(info, "pulls residual / width per layer, median and normalised MAD, without | with the offsets (the dictionary's: {})", applied ? "applied" : "not applied");
295+
for (int layer = 0; layer < nLayers; ++layer) {
296+
std::array<std::vector<float>, 2> pulls, offsetPulls;
297+
for (auto& [id, values] : residuals[layer]) {
298+
const auto& entry = dictionary->getEntry(layer, id);
299+
const std::array<float, 2> width{entry.xWidth, entry.zWidth}, offset{entry.xOffset, entry.zOffset};
300+
for (int axis = 0; axis < 2; ++axis) {
301+
for (const float r : values[axis]) {
302+
pulls[axis].push_back(r / width[axis]);
303+
offsetPulls[axis].push_back((r - offset[axis]) / width[axis]);
304+
}
305+
std::tie(tMedian[axis], tWidth[axis]) = medianAndWidth(values[axis]);
306+
tEntryOffset[axis] = offset[axis];
307+
tEntryWidth[axis] = width[axis];
308+
}
309+
tLayer = layer;
310+
tId = id;
311+
tGroup = entry.isGroup;
312+
tCount = static_cast<int>(values[0].size());
313+
entries->Fill();
314+
}
315+
const auto [x, xWidth] = medianAndWidth(pulls[0]);
316+
const auto [z, zWidth] = medianAndWidth(pulls[1]);
317+
const auto [xo, xoWidth] = medianAndWidth(offsetPulls[0]);
318+
const auto [zo, zoWidth] = medianAndWidth(offsetPulls[1]);
319+
LOGP(info, " layer {:2}, {:9} residuals: across {:6.3f} {:5.3f} | {:6.3f} {:5.3f}, along {:6.3f} {:5.3f} | {:6.3f} {:5.3f}", layer, pulls[0].size(), x, xWidth, xo,
320+
xoWidth, z, zWidth, zo, zoWidth);
321+
}
322+
out->Write();
323+
LOGP(info, "written to {}", output);
324+
}
325+
326+
} // namespace check_dictionary
327+
328+
/// \a detector "TRK" or "FT3"; \a timeframes the directories to check on, \a training those to build from (none: \a dictionaryFile is read);
329+
/// \a applyOffsets whether a built dictionary adds the entries' median offsets; \a nThreads the depth scan's.
330+
void CheckTopologyDictionary(const std::string& detector, const std::string& dictionaryFile, const std::vector<std::string>& timeframes,
331+
const std::vector<std::string>& training = {}, const std::string& output = "CheckTopologyDictionary.root", bool applyOffsets = false,
332+
int nThreads = 8)
333+
{
334+
if (detector == "TRK") {
335+
check_dictionary::check<o2::detectors::DetID::TRK>(dictionaryFile, timeframes, training, output, applyOffsets, nThreads);
336+
} else if (detector == "FT3") {
337+
check_dictionary::check<o2::detectors::DetID::FT3>(dictionaryFile, timeframes, training, output, applyOffsets, nThreads);
338+
} else {
339+
LOGP(fatal, "unknown detector {}: TRK or FT3", detector);
340+
}
341+
}

‎Detectors/Upgrades/ALICE3/TRKFT3/common/reconstruction/CMakeLists.txt‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ endif()
1616
o2_add_library(TRKReconstruction
1717
TARGETVARNAME targetName
1818
SOURCES src/Clusterer.cxx
19+
src/TopologyDictionary.cxx
20+
src/BuildTopologyDictionary.cxx
1921
$<$<BOOL:${Acts_FOUND}>:src/ClustererACTS.cxx>
2022
PUBLIC_LINK_LIBRARIES
2123
Microsoft.GSL::GSL
@@ -30,3 +32,8 @@ o2_add_library(TRKReconstruction
3032
if(Acts_FOUND)
3133
target_compile_definitions(${targetName} PUBLIC O2_WITH_ACTS)
3234
endif()
35+
36+
o2_target_root_dictionary(TRKReconstruction
37+
HEADERS include/TRKReconstruction/TopologyDictionary.h
38+
include/TRKReconstruction/BuildTopologyDictionary.h
39+
LINKDEF src/TRKReconstructionLinkDef.h)

0 commit comments

Comments
 (0)