64 const std::string& calibrationPath, std::string&
error) {
66 std::ifstream input{calibrationPath};
68 error =
"cannot open ASCII calibration file: " + calibrationPath;
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;
78 std::size_t lineNumber = 0U;
79 bool calibrationRecordsStarted =
false;
80 while (std::getline(input, line)) {
82 if (line.empty() || line[0] ==
'#')
continue;
83 if (hasEmptyCsvField(line)) {
84 error =
"empty CSV field at line " + std::to_string(lineNumber);
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);
96 if (fields.size() != 3U) {
97 error =
"invalid META row at line " + std::to_string(lineNumber);
100 if (fields[1] ==
"schemaVersion") {
101 if (!parseInteger(fields[2], schemaVersion)) {
102 error =
"invalid schemaVersion";
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";
112 }
else if (fields[1] ==
"phiBinsPerFold") {
113 if (!parseInteger(fields[2], lut->m_phiBinsPerFold)) {
114 error =
"invalid phiBinsPerFold";
117 }
else if (fields[1] ==
"absEtaMin") {
118 if (!parseFloatingPoint(fields[2], lut->m_absEtaMin)) {
119 error =
"invalid absEtaMin";
122 }
else if (fields[1] ==
"absEtaMax") {
123 if (!parseFloatingPoint(fields[2], lut->m_absEtaMax)) {
124 error =
"invalid absEtaMax";
127 }
else if (fields[1] ==
"payloadMode") {
128 lut->m_isDevelopmentPayload = fields[2] ==
"development";
132 calibrationRecordsStarted =
true;
134 if (lut->m_etaBins == 0U || lut->m_phiBinsPerFold == 0U) {
135 error =
"META dimensions must precede calibration records";
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);
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);
157 const std::size_t
index =
static_cast<std::size_t
>(
eta) *
158 lut->m_phiBinsPerFold +
159 static_cast<std::size_t
>(
phi);
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);
168 binSeen[
index] =
true;
169 }
else if (record ==
"THRESHOLD") {
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);
181 bin.thresholdCutsGeV[code] = cut;
182 bin.thresholdStatuses[code] =
184 thresholdSeen[
index][code] =
true;
185 }
else if (record ==
"KNOT") {
186 unsigned int knotIndex = 0U;
188 if (fields.size() != 6U ||
189 !parseInteger(fields[3], knotIndex) ||
192 error =
"invalid KNOT row at line " + std::to_string(lineNumber);
195 knotsByBin[
index].emplace_back(knotIndex, knot);
197 error =
"unknown calibration record at line " +
198 std::to_string(lineNumber) +
": " + record;
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";
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);
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);
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;
237 if (indexedKnots.size() < 2U) {
238 error =
"fewer than two knots for bin " + std::to_string(
index);
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);
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) {
265 lut->m_knots.back() = knot;
267 lut->m_knots.push_back(knot);
270 bin.knotCount =
static_cast<std::uint32_t
>(lut->m_knots.size() -
272 if (
bin.knotCount < 2U) {
273 error =
"fewer than two distinct knot responses for bin " +
274 std::to_string(
index);
359 const float eta,
const float phi,
const float signedDTheta)
const {
363 if (result.etaBin < 0 || result.phiFoldBin < 0 ||
364 !std::isfinite(signedDTheta)) {
368 if (
bin ==
nullptr)
return result;
369 result.modelValid =
true;
370 result.estimatedCharge = chargeFromSignedDTheta(signedDTheta);
371 result.chargeEstimateValid = result.estimatedCharge != 0;
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) {
379 if (magnitude == 0.F) {
383 result.ptEstimateValid =
true;
385 inversePt = magnitude /
bin->linearSlopeMagnitudeRadGeV;
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;
397 if (!result.ptEstimateValid)
return result;
402 result.ptEstimateGeV =
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];