|
| 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 | +} |
0 commit comments