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