ATLAS Offline Software
Loading...
Searching...
No Matches
TgcL0FloatingPtLut.cxx
Go to the documentation of this file.
1/*
2 Copyright (C) 2002-2026 CERN for the benefit of the ATLAS collaboration
3*/
5
7
8#include <algorithm>
9#include <array>
10#include <cmath>
11#include <exception>
12#include <fstream>
13#include <limits>
14#include <numbers>
15#include <string_view>
16#include <type_traits>
17
18namespace {
19
20template <std::integral T>
21bool parseInteger(std::string_view text, T& value)
22{
23 try {
24 CxxUtils::convertToNumber(text, value);
25 return true;
26 } catch (const std::exception&) {
27 return false;
28 }
29}
30
31template <std::floating_point T>
32bool parseFloatingPoint(std::string_view text, T& value)
33{
34 try {
35 CxxUtils::convertToNumber(text, value);
36 return true;
37 } catch (const std::exception&) {
38 return false;
39 }
40}
41
42constexpr bool hasEmptyCsvField(const std::string_view line) {
43 return line.empty() || line.front() == ',' || line.back() == ',' ||
44 line.find(",,") != std::string_view::npos;
45}
46
47std::uint8_t encodePtValue(const float ptGeV) {
48 if (!std::isfinite(ptGeV) || ptGeV <= 0.F) return 0U;
49 const long encoded = std::lround(2.F * std::min(
51 return static_cast<std::uint8_t>(std::clamp(encoded, 0L, 255L));
52}
53
54std::int8_t chargeFromSignedDTheta(const float signedDTheta) {
55 if (!std::isfinite(signedDTheta) || signedDTheta == 0.F) return 0;
56 return static_cast<std::int8_t>(-std::copysign(1.F, signedDTheta));
57}
58
59} // namespace
60
61namespace L0Muon {
62
63std::unique_ptr<TgcL0FloatingPtLut> TgcL0FloatingPtLut::loadAscii(
64 const std::string& calibrationPath, std::string& error) {
65 error.clear();
66 std::ifstream input{calibrationPath};
67 if (!input) {
68 error = "cannot open ASCII calibration file: " + calibrationPath;
69 return nullptr;
70 }
71
72 auto lut = std::unique_ptr<TgcL0FloatingPtLut>{new TgcL0FloatingPtLut};
73 unsigned int schemaVersion = 0U;
74 std::vector<bool> binSeen;
75 std::vector<std::array<bool, 16>> thresholdSeen;
76 std::vector<std::vector<std::pair<unsigned int, Knot>>> knotsByBin;
77 std::string line;
78 std::size_t lineNumber = 0U;
79 bool calibrationRecordsStarted = false;
80 while (std::getline(input, line)) {
81 ++lineNumber;
82 if (line.empty() || line[0] == '#') continue;
83 if (hasEmptyCsvField(line)) {
84 error = "empty CSV field at line " + std::to_string(lineNumber);
85 return nullptr;
86 }
87 const auto fields = CxxUtils::tokenize(line, ',');
88 if (fields.empty()) continue;
89 const std::string& record = fields[0];
90 if (record == "META") {
91 if (calibrationRecordsStarted) {
92 error = "META row after calibration records at line " +
93 std::to_string(lineNumber);
94 return nullptr;
95 }
96 if (fields.size() != 3U) {
97 error = "invalid META row at line " + std::to_string(lineNumber);
98 return nullptr;
99 }
100 if (fields[1] == "schemaVersion") {
101 if (!parseInteger(fields[2], schemaVersion)) {
102 error = "invalid schemaVersion";
103 return nullptr;
104 }
105 } else if (fields[1] == "payloadVersion") {
106 lut->m_version = fields[2];
107 } else if (fields[1] == "etaBins") {
108 if (!parseInteger(fields[2], lut->m_etaBins)) {
109 error = "invalid etaBins";
110 return nullptr;
111 }
112 } else if (fields[1] == "phiBinsPerFold") {
113 if (!parseInteger(fields[2], lut->m_phiBinsPerFold)) {
114 error = "invalid phiBinsPerFold";
115 return nullptr;
116 }
117 } else if (fields[1] == "absEtaMin") {
118 if (!parseFloatingPoint(fields[2], lut->m_absEtaMin)) {
119 error = "invalid absEtaMin";
120 return nullptr;
121 }
122 } else if (fields[1] == "absEtaMax") {
123 if (!parseFloatingPoint(fields[2], lut->m_absEtaMax)) {
124 error = "invalid absEtaMax";
125 return nullptr;
126 }
127 } else if (fields[1] == "payloadMode") {
128 lut->m_isDevelopmentPayload = fields[2] == "development";
129 }
130 continue;
131 }
132 calibrationRecordsStarted = true;
133
134 if (lut->m_etaBins == 0U || lut->m_phiBinsPerFold == 0U) {
135 error = "META dimensions must precede calibration records";
136 return nullptr;
137 }
138 const std::size_t count = static_cast<std::size_t>(lut->m_etaBins) *
139 lut->m_phiBinsPerFold;
140 if (lut->m_bins.empty()) {
141 lut->m_bins.resize(count);
142 binSeen.assign(count, false);
143 thresholdSeen.resize(count);
144 knotsByBin.resize(count);
145 }
146
147 int eta = -1;
148 int phi = -1;
149 if (fields.size() < 3U || !parseInteger(fields[1], eta) ||
150 !parseInteger(fields[2], phi) || eta < 0 || phi < 0 ||
151 eta >= static_cast<int>(lut->m_etaBins) ||
152 phi >= static_cast<int>(lut->m_phiBinsPerFold)) {
153 error = "invalid calibration bin at line " +
154 std::to_string(lineNumber);
155 return nullptr;
156 }
157 const std::size_t index = static_cast<std::size_t>(eta) *
158 lut->m_phiBinsPerFold +
159 static_cast<std::size_t>(phi);
160 Bin& bin = lut->m_bins[index];
161 if (record == "BIN") {
162 if (fields.size() != 5U || binSeen[index] ||
163 !parseFloatingPoint(fields[3], bin.linearSlopeMagnitudeRadGeV) ||
164 !parseFloatingPoint(fields[4], bin.transitionPtGeV)) {
165 error = "invalid BIN row at line " + std::to_string(lineNumber);
166 return nullptr;
167 }
168 binSeen[index] = true;
169 } else if (record == "THRESHOLD") {
170 int code = 0;
171 int status = 0;
172 float cut = 0.F;
173 if (fields.size() != 6U || !parseInteger(fields[3], code) ||
174 !parseFloatingPoint(fields[4], cut) ||
175 !parseInteger(fields[5], status) || code < 1 || code > 14 ||
176 status < 1 || status > 5 || thresholdSeen[index][code]) {
177 error = "invalid THRESHOLD row at line " +
178 std::to_string(lineNumber);
179 return nullptr;
180 }
181 bin.thresholdCutsGeV[code] = cut;
182 bin.thresholdStatuses[code] =
183 static_cast<TgcL0FloatingThresholdCalibrationStatus>(status);
184 thresholdSeen[index][code] = true;
185 } else if (record == "KNOT") {
186 unsigned int knotIndex = 0U;
187 Knot knot;
188 if (fields.size() != 6U ||
189 !parseInteger(fields[3], knotIndex) ||
190 !parseFloatingPoint(fields[4], knot.inversePtGeVInv) ||
191 !parseFloatingPoint(fields[5], knot.responseMagnitudeRad)) {
192 error = "invalid KNOT row at line " + std::to_string(lineNumber);
193 return nullptr;
194 }
195 knotsByBin[index].emplace_back(knotIndex, knot);
196 } else {
197 error = "unknown calibration record at line " +
198 std::to_string(lineNumber) + ": " + record;
199 return nullptr;
200 }
201 }
202
203 if (schemaVersion != 1U || lut->m_version.empty() ||
204 !std::isfinite(lut->m_absEtaMin) ||
205 !std::isfinite(lut->m_absEtaMax) ||
206 !(lut->m_absEtaMax > lut->m_absEtaMin) || lut->m_bins.empty()) {
207 error = "incomplete or unsupported ASCII calibration metadata";
208 return nullptr;
209 }
210 for (std::size_t index = 0U; index < lut->m_bins.size(); ++index) {
211 Bin& bin = lut->m_bins[index];
212 if (!binSeen[index] ||
213 !std::isfinite(bin.linearSlopeMagnitudeRadGeV) ||
214 !std::isfinite(bin.transitionPtGeV) ||
215 !(bin.linearSlopeMagnitudeRadGeV > 0.F) ||
216 !(bin.transitionPtGeV > 0.F)) {
217 error = "missing or invalid BIN record for bin " +
218 std::to_string(index);
219 return nullptr;
220 }
221 float previousCut = 0.F;
222 for (unsigned int code = 1U; code <= 14U; ++code) {
223 const float cut = bin.thresholdCutsGeV[code];
224 if (!thresholdSeen[index][code] || !std::isfinite(cut) ||
225 !(cut > 0.F) || cut < previousCut) {
226 error = "missing or invalid threshold for bin " +
227 std::to_string(index);
228 return nullptr;
229 }
230 previousCut = cut;
231 }
232 auto& indexedKnots = knotsByBin[index];
233 std::sort(indexedKnots.begin(), indexedKnots.end(),
234 [](const auto& left, const auto& right) {
235 return left.first < right.first;
236 });
237 if (indexedKnots.size() < 2U) {
238 error = "fewer than two knots for bin " + std::to_string(index);
239 return nullptr;
240 }
241 bin.knotOffset = static_cast<std::uint32_t>(lut->m_knots.size());
242 unsigned int expectedIndex = 0U;
243 float previousInversePt = -1.F;
244 float previousResponse = -1.F;
245 for (const auto& [knotIndex, knot] : indexedKnots) {
246 if (knotIndex != expectedIndex++ ||
247 !std::isfinite(knot.inversePtGeVInv) ||
248 !std::isfinite(knot.responseMagnitudeRad) ||
249 !(knot.inversePtGeVInv > 0.F) ||
250 !(knot.responseMagnitudeRad >= 0.F) ||
251 knot.inversePtGeVInv < previousInversePt ||
252 knot.responseMagnitudeRad < previousResponse) {
253 error = "invalid or non-monotonic knot sequence for bin " +
254 std::to_string(index);
255 return nullptr;
256 }
257 previousInversePt = knot.inversePtGeVInv;
258 previousResponse = knot.responseMagnitudeRad;
259 if (lut->m_knots.size() > bin.knotOffset &&
260 knot.responseMagnitudeRad ==
261 lut->m_knots.back().responseMagnitudeRad) {
262 // A monotonic calibration can contain a response plateau. Its
263 // inverse is ambiguous, so retain the largest inverse-pT value and
264 // therefore the most conservative pT estimate.
265 lut->m_knots.back() = knot;
266 } else {
267 lut->m_knots.push_back(knot);
268 }
269 }
270 bin.knotCount = static_cast<std::uint32_t>(lut->m_knots.size() -
271 bin.knotOffset);
272 if (bin.knotCount < 2U) {
273 error = "fewer than two distinct knot responses for bin " +
274 std::to_string(index);
275 return nullptr;
276 }
277 }
278
279 return lut;
280}
281
282int TgcL0FloatingPtLut::etaBin(const float eta) const {
283 if (!std::isfinite(eta)) return -1;
284 const float absEta = std::abs(eta);
285 if (absEta < m_absEtaMin || absEta > m_absEtaMax) return -1;
286 if (absEta == m_absEtaMax) return static_cast<int>(m_etaBins) - 1;
287 const float scaled = (absEta - m_absEtaMin) * m_etaBins /
289 const int bin = static_cast<int>(std::floor(scaled));
290 return bin >= 0 && bin < static_cast<int>(m_etaBins) ? bin : -1;
291}
292
293int TgcL0FloatingPtLut::phiFoldBin(const float phi) const {
294 if (!std::isfinite(phi)) return -1;
295 const float period = 2.F * std::numbers::pi_v<float> / 8.F;
296 float folded = std::fmod(phi, period);
297 if (folded < 0.F) folded += period;
298 float fraction = folded / period;
299 if (fraction >= 1.F) fraction = 0.F;
300 return std::clamp(
301 static_cast<int>(std::floor(fraction * m_phiBinsPerFold)), 0,
302 static_cast<int>(m_phiBinsPerFold) - 1);
303}
304
306 const int eta, const int phi) const {
307 if (eta < 0 || eta >= static_cast<int>(m_etaBins) || phi < 0 ||
308 phi >= static_cast<int>(m_phiBinsPerFold)) {
309 return nullptr;
310 }
311 return &m_bins[static_cast<std::size_t>(eta) * m_phiBinsPerFold + phi];
312}
313
315 const Bin& bin, const float responseMagnitudeRad,
316 float& inversePtGeVInv) const {
317 const auto begin = m_knots.begin() + bin.knotOffset;
318 const auto end = begin + bin.knotCount;
319 if (responseMagnitudeRad <= begin->responseMagnitudeRad) {
320 inversePtGeVInv = begin->inversePtGeVInv;
321 return true;
322 }
323 const auto upper = std::lower_bound(
324 begin, end, responseMagnitudeRad,
325 [](const Knot& knot, const float value) {
326 return knot.responseMagnitudeRad < value;
327 });
328 if (upper != end) {
329 if (upper->responseMagnitudeRad == responseMagnitudeRad) {
330 inversePtGeVInv = upper->inversePtGeVInv;
331 return true;
332 }
333 const Knot& low = *(upper - 1);
334 const float delta = upper->responseMagnitudeRad -
336 if (!(delta > 0.F)) return false;
337 const float fraction =
338 (responseMagnitudeRad - low.responseMagnitudeRad) / delta;
339 inversePtGeVInv = low.inversePtGeVInv +
340 fraction * (upper->inversePtGeVInv -
341 low.inversePtGeVInv);
342 return true;
343 }
344 const Knot& last = *(end - 1);
345 const Knot& previous = *(end - 2);
346 const float deltaResponse =
347 last.responseMagnitudeRad - previous.responseMagnitudeRad;
348 if (!(deltaResponse > 0.F)) return false;
349 inversePtGeVInv = last.inversePtGeVInv +
350 (last.inversePtGeVInv - previous.inversePtGeVInv) /
351 deltaResponse *
352 (responseMagnitudeRad - last.responseMagnitudeRad);
353 inversePtGeVInv = std::clamp(inversePtGeVInv, 0.F,
354 last.inversePtGeVInv);
355 return true;
356}
357
359 const float eta, const float phi, const float signedDTheta) const {
361 result.etaBin = etaBin(eta);
362 result.phiFoldBin = phiFoldBin(phi);
363 if (result.etaBin < 0 || result.phiFoldBin < 0 ||
364 !std::isfinite(signedDTheta)) {
365 return result;
366 }
367 const Bin* bin = findBin(result.etaBin, result.phiFoldBin);
368 if (bin == nullptr) return result;
369 result.modelValid = true;
370 result.estimatedCharge = chargeFromSignedDTheta(signedDTheta);
371 result.chargeEstimateValid = result.estimatedCharge != 0;
372
373 const float magnitude = std::abs(signedDTheta);
374 const float transitionMagnitude =
375 bin->linearSlopeMagnitudeRadGeV / bin->transitionPtGeV;
376 float inversePt = 0.F;
377 if (magnitude <= transitionMagnitude) {
378 result.responseMode = TgcL0FloatingPtResponseMode::Linear;
379 if (magnitude == 0.F) {
380 // Zero bending means that only a lower bound can be represented. Keep
381 // the diagnostic raw value finite while saturating the operational pT.
382 result.rawPtEstimateGeV = s_maxEncodedPtGeV;
383 result.ptEstimateValid = true;
384 } else {
385 inversePt = magnitude / bin->linearSlopeMagnitudeRadGeV;
386 }
387 } else {
389 if (!invertFloatingResponse(*bin, magnitude, inversePt)) return result;
390 }
391 if (!result.ptEstimateValid) {
392 if (!std::isfinite(inversePt) || inversePt <= 0.F) return result;
393 result.rawPtEstimateGeV = 1.F / inversePt;
394 result.ptEstimateValid = std::isfinite(result.rawPtEstimateGeV) &&
395 result.rawPtEstimateGeV > 0.F;
396 }
397 if (!result.ptEstimateValid) return result;
398
399 // The raw floating-point estimate is diagnostic provenance. Candidate
400 // ordering, thresholds and downstream EDM values use the representable
401 // 8-bit range and therefore saturate at 127.5 GeV.
402 result.ptEstimateGeV =
403 std::min(result.rawPtEstimateGeV, s_maxEncodedPtGeV);
404 result.estimatedPtValueIndex = encodePtValue(result.ptEstimateGeV);
405 for (int code = 14; code >= 1; --code) {
406 const float cut = bin->thresholdCutsGeV[code];
407 if (result.ptEstimateGeV >= cut) {
408 result.thresholdCode = static_cast<std::uint8_t>(code);
409 result.thresholdCutGeV = cut;
410 result.thresholdCalibrationStatus = bin->thresholdStatuses[code];
411 break;
412 }
413 }
414 return result;
415}
416
417} // namespace L0Muon
Scalar eta() const
pseudorapidity method
Scalar phi() const
phi method
int upper(int c)
static std::unique_ptr< TgcL0FloatingPtLut > loadAscii(const std::string &calibrationPath, std::string &error)
Load the human-readable calibration payload.
const Bin * findBin(int etaBin, int phiFoldBin) const
static constexpr float s_maxEncodedPtGeV
TgcL0FloatingPtEvaluation evaluate(float eta, float phi, float signedDTheta) const
bool invertFloatingResponse(const Bin &bin, float responseMagnitudeRad, float &inversePtGeVInv) const
int count(std::string s, const std::string &regx)
count how many occurances of a regx are in a string
Definition hcg.cxx:148
std::vector< std::string > tokenize(std::string_view the_str, std::string_view delimiters)
Splits the string into smaller substrings.
void convertToNumber(std::string_view str, dType &number)
TgcL0FloatingThresholdCalibrationStatus
Definition index.py:1
void sort(typename DataModel_detail::iterator< DVL > beg, typename DataModel_detail::iterator< DVL > end)
Specialization of sort for DataVector/List.