72 const std::string& calibrationPath, std::string&
error) {
74 std::ifstream input{calibrationPath};
76 error =
"cannot open ASCII calibration file: " + calibrationPath;
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;
86 std::size_t lineNumber = 0U;
87 bool calibrationRecordsStarted =
false;
88 while (std::getline(input, line)) {
90 if (line.empty() || line[0] ==
'#')
continue;
91 if (hasEmptyCsvField(line)) {
92 error =
"empty CSV field at line " + std::to_string(lineNumber);
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);
104 if (fields.size() != 3U) {
105 error =
"invalid META row at line " + std::to_string(lineNumber);
108 if (fields[1] ==
"schemaVersion") {
109 if (!parseInteger(fields[2], schemaVersion)) {
110 error =
"invalid schemaVersion";
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";
120 }
else if (fields[1] ==
"phiBinsPerFold") {
121 if (!parseInteger(fields[2], lut->m_phiBinsPerFold)) {
122 error =
"invalid phiBinsPerFold";
125 }
else if (fields[1] ==
"absEtaMin") {
126 if (!parseFloatingPoint(fields[2], lut->m_absEtaMin)) {
127 error =
"invalid absEtaMin";
130 }
else if (fields[1] ==
"absEtaMax") {
131 if (!parseFloatingPoint(fields[2], lut->m_absEtaMax)) {
132 error =
"invalid absEtaMax";
135 }
else if (fields[1] ==
"payloadMode") {
136 lut->m_isDevelopmentPayload = fields[2] ==
"development";
140 calibrationRecordsStarted =
true;
142 if (lut->m_etaBins == 0U || lut->m_phiBinsPerFold == 0U) {
143 error =
"META dimensions must precede calibration records";
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);
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);
165 const std::size_t
index =
static_cast<std::size_t
>(
eta) *
166 lut->m_phiBinsPerFold +
167 static_cast<std::size_t
>(
phi);
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);
176 binSeen[
index] =
true;
177 }
else if (record ==
"THRESHOLD") {
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);
189 bin.thresholdCutsGeV[code] = cut;
190 bin.thresholdStatuses[code] =
192 thresholdSeen[
index][code] =
true;
193 }
else if (record ==
"KNOT") {
194 unsigned int knotIndex = 0U;
196 if (fields.size() != 6U ||
197 !parseInteger(fields[3], knotIndex) ||
200 error =
"invalid KNOT row at line " + std::to_string(lineNumber);
203 knotsByBin[
index].emplace_back(knotIndex, knot);
205 error =
"unknown calibration record at line " +
206 std::to_string(lineNumber) +
": " + record;
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";
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);
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);
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;
245 if (indexedKnots.size() < 2U) {
246 error =
"fewer than two knots for bin " + std::to_string(
index);
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);
266 previousInversePt = knot.inversePtGeVInv;
267 previousResponse = knot.responseMagnitudeRad;
268 lut->m_knots.push_back(knot);
352 const float eta,
const float phi,
const float signedDTheta)
const {
356 if (result.etaBin < 0 || result.phiFoldBin < 0 ||
357 !std::isfinite(signedDTheta)) {
361 if (
bin ==
nullptr)
return result;
362 result.modelValid =
true;
363 result.estimatedCharge = chargeFromSignedDTheta(signedDTheta);
364 result.chargeEstimateValid = result.estimatedCharge != 0;
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) {
372 if (magnitude == 0.F) {
376 result.ptEstimateValid =
true;
378 inversePt = magnitude /
bin->linearSlopeMagnitudeRadGeV;
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;
390 if (!result.ptEstimateValid)
return result;
395 result.ptEstimateGeV =
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];