modified: pccal/fit_pc_energy_calibration.C changes for the adc limit and the lower limit to fit for the pc based on each weire separately

This commit is contained in:
Vignesh Sitaraman 2026-09-30 16:43:20 -04:00
parent c9339a1165
commit 12c4c87ba0

View File

@ -13,6 +13,8 @@
#include <TPaveText.h>
#include <TLine.h>
#include <fstream>
#include <sstream>
#include <string>
#include <iostream>
#include <vector>
#include <iomanip>
@ -29,13 +31,67 @@
// calibration and is excluded here rather than trusted. Update this list as more
// dead channels are identified; currently anode 9 and 12.
void fit_pc_energy_calibration(const std::string& dataset_filter = "",
const std::vector<int>& known_dead_wires = {9, 12})
const std::vector<int>& known_dead_wires = {},
const std::string& ratio_reference = "",
const std::string& ref_sides = "both")
{
std::vector<bool> isDead(48, false);
for (int w : known_dead_wires)
if (w >= 0 && w < 48)
isDead[w] = true;
const bool useRef = !ratio_reference.empty();
std::vector<double> refSlope(48, 0.0);
std::vector<bool> refOk(48, false);
std::vector<int> refMethod(48, -1); // -1 = wire absent from the reference
// rescaleSide[s]: s=0 anode, s=1 cathode. A side that isn't rescaled is copied
// from the reference verbatim in Pass 2.
bool rescaleSide[2] = {true, true};
if (useRef)
{
if (ref_sides == "anode")
rescaleSide[1] = false;
else if (ref_sides == "cathode")
rescaleSide[0] = false;
else if (ref_sides != "both")
{
std::cerr << "fit_pc_energy_calibration: ref_sides must be \"both\", \"anode\" or \"cathode\", got '"
<< ref_sides << "' -- aborting." << std::endl;
return;
}
std::ifstream rf(ratio_reference);
if (!rf.is_open())
{
std::cerr << "fit_pc_energy_calibration: can't open ratio_reference '" << ratio_reference
<< "' -- aborting rather than silently writing an unreferenced table." << std::endl;
return;
}
std::string rline;
int nRef = 0;
while (std::getline(rf, rline))
{
std::istringstream rs(rline);
int rw, rm;
double rsl, rint;
if (!(rs >> rw >> rsl >> rint >> rm) || rw < 0 || rw >= 48)
continue;
refSlope[rw] = rsl;
refMethod[rw] = rm;
refOk[rw] = (rsl > 0.0) && (rm == 1 || rm == 2 || rm == 4);
nRef += refOk[rw];
}
std::cout << "fit_pc_energy_calibration: ratio_reference " << ratio_reference << ": "
<< nRef << "/48 usable wire(s); rescaling " << ref_sides
<< (ref_sides == "both" ? "" : " only, other side copied from the reference") << std::endl;
for (int w = 0; w < 48; ++w)
if (!rescaleSide[w < 24 ? 0 : 1] && refMethod[w] < 0)
{
std::cerr << "fit_pc_energy_calibration: wire " << w << " is on the side to copy but missing from "
<< ratio_reference << " -- aborting rather than inventing a row." << std::endl;
return;
}
}
std::vector<std::pair<double, double>> pts[48]; // [wire] -> (ADC, dE_gas MeV)
TSystemDirectory dir("pc_calib_raw", "pc_calib_raw");
@ -48,8 +104,10 @@ void fit_pc_energy_calibration(const std::string& dataset_filter = "",
}
int nFiles = 0;
int nFilesLegacyOverflow = 0;
long long nOverflowCut = 0;
long long nOverflowCutPerWire[48] = {0};
const double kLegacyOverflowADC = 64000.0;
TIter next(files);
TSystemFile *f;
@ -68,13 +126,34 @@ void fit_pc_energy_calibration(const std::string& dataset_filter = "",
if (!infile.is_open())
continue;
std::vector<double> overflowThr(48, kLegacyOverflowADC);
bool hasOverflowHeader = false;
std::string line;
while (std::getline(infile, line))
{
if (line.empty())
continue;
if (line[0] == '#')
{
std::istringstream hs(line.substr(1));
std::string key;
int hw;
double thr;
if (hs >> key && key == "overflow" && hs >> hw >> thr && hw >= 0 && hw < 48)
{
overflowThr[hw] = thr;
hasOverflowHeader = true;
}
continue;
}
std::istringstream ls(line);
int wire;
double adc, dE_gas;
while (infile >> wire >> adc >> dE_gas)
{
if (!(ls >> wire >> adc >> dE_gas))
continue;
if (wire >= 0 && wire < 48)
{
if (adc < 64000.0) {
if (adc < overflowThr[wire]) {
pts[wire].push_back({adc, dE_gas});
} else {
nOverflowCut++;
@ -82,13 +161,23 @@ void fit_pc_energy_calibration(const std::string& dataset_filter = "",
}
}
}
if (!hasOverflowHeader)
{
++nFilesLegacyOverflow;
std::cerr << "fit_pc_energy_calibration: " << name.Data()
<< " has no '# overflow' header (pre-dates it) -- using flat legacy cut ADC < "
<< kLegacyOverflowADC << ", which is wrong for gain-matched wires and for any"
<< " cathode run with CATHODE_GAIN != 1. Regenerate it with the current TrackRecon.C."
<< std::endl;
}
++nFiles;
}
std::cout << "fit_pc_energy_calibration: read " << nFiles
<< " run file(s) from pc_calib_raw/ (Filter: '" << dataset_filter << "')" << std::endl;
<< " run file(s) from pc_calib_raw/ (Filter: '" << dataset_filter << "'), "
<< nFilesLegacyOverflow << " with legacy flat overflow cut" << std::endl;
std::cout << "fit_pc_energy_calibration: cut " << nOverflowCut
<< " points due to ADC >= 64k overflow." << std::endl;
<< " points at the per-wire ADC overflow threshold." << std::endl;
std::cout << "fit_pc_energy_calibration: per-wire overflow breakdown (wire: cut / kept):" << std::endl;
for (int wire = 0; wire < 48; ++wire)
{
@ -130,19 +219,31 @@ void fit_pc_energy_calibration(const std::string& dataset_filter = "",
int b = std::min(kFloorHistBins - 1, static_cast<int>(v / binW));
if (b >= 0) hist[b]++;
}
int tallest = *std::max_element(hist.begin(), hist.end());
if (tallest <= 0)
std::vector<double> sm(kFloorHistBins, 0.0);
for (int b = 0; b < kFloorHistBins; ++b)
{
int lo = std::max(0, b - 1), hi = std::min(kFloorHistBins - 1, b + 1);
double s = 0.0;
for (int k = lo; k <= hi; ++k)
s += hist[k];
sm[b] = s / (hi - lo + 1);
}
double tallest = *std::max_element(sm.begin(), sm.end());
if (tallest <= 0.0)
return 0.0;
// Scan from the highest-ADC bin down; the first local maximum that's
// prominent enough is taken as "the" peak.
int peakBin = -1;
for (int b = kFloorHistBins - 1; b >= 1; --b)
const double kValleyRiseSigma = 2.0;
const double kValleyDepthFrac = 0.5;
int b = kFloorHistBins - 2;
while (b >= 1)
{
if (hist[b] < kPeakProminenceFrac * tallest)
int peakBin = -1;
for (; b >= 1; --b)
{
if (sm[b] < kPeakProminenceFrac * tallest)
continue;
bool isLocalMax = hist[b] >= hist[b - 1] && (b == kFloorHistBins - 1 || hist[b] >= hist[b + 1]);
if (isLocalMax)
if (sm[b] >= sm[b - 1] && sm[b] >= sm[b + 1])
{
peakBin = b;
break;
@ -151,17 +252,32 @@ void fit_pc_energy_calibration(const std::string& dataset_filter = "",
if (peakBin <= 0)
return 0.0; // no clear peak below the top edge -- nothing to cut against
// Walk down from the peak to the valley: the floor is the bin where the
// count stops falling and starts rising again (the start of a lower
// population), or the histogram runs out.
int valleyBin = peakBin;
for (int b = peakBin - 1; b >= 0; --b)
int valleyBin = peakBin, riseBin = -1;
for (int v = peakBin - 1; v >= 0; --v)
{
if (hist[b] > hist[valleyBin])
break;
valleyBin = b;
if (sm[v] <= sm[valleyBin])
{
valleyBin = v;
continue;
}
double tol = kValleyRiseSigma * std::sqrt(std::max(sm[valleyBin], 1.0) / 3.0);
if (sm[v] > sm[valleyBin] + tol)
{
riseBin = v;
break;
}
}
if (riseBin < 0)
return 0.0; // single population all the way down -- nothing to cut
double depthTol = kValleyRiseSigma * std::sqrt(std::max(sm[peakBin], 1.0) / 3.0);
if (sm[valleyBin] <= kValleyDepthFrac * sm[peakBin] &&
sm[peakBin] - sm[valleyBin] > depthTol)
return valleyBin * binW;
b = riseBin; // shoulder, not a peak -- keep looking below
}
return 0.0;
};
std::vector<std::pair<double, double>> ptsFit[48]; // math uses this; pts[] stays raw for display
@ -298,6 +414,40 @@ void fit_pc_energy_calibration(const std::string& dataset_filter = "",
<< consensusCathodeDEgas << " MeV from " << cathodeConsensusPts.size()
<< " point(s) across " << nTrustedCathode << " trusted cathode wire(s)" << std::endl;
double refK[2] = {0.0, 0.0};
if (useRef)
{
std::vector<double> kw[2];
for (int wire = 0; wire < 48; ++wire)
{
if (!refOk[wire] || isDead[wire] || !ok_arr[wire] || isOutlier[wire])
continue;
kw[wire < 24 ? 0 : 1].push_back(slope_lsq[wire] / refSlope[wire]);
}
for (int s = 0; s < 2; ++s)
{
refK[s] = medianOf(kw[s]);
std::cout << "fit_pc_energy_calibration: ratio_reference scale K_" << (s == 0 ? "anode" : "cathode")
<< " = " << refK[s] << " (median over " << kw[s].size() << " trusted wire(s))"
<< (!rescaleSide[s] ? " -- NOT applied (ref_sides), rows copied from the reference"
: (refK[s] > 0.0 ? "" : " -- NOT fitted, this side keeps per-wire results"))
<< std::endl;
}
// Per-wire cross-check: what each wire's own least-squares fit says its scale
// should be, relative to K. Far from 1 means the reference ratio for that wire
// disagrees with this data -- the table still uses the reference, this is only
// so it's visible.
std::cout << "fit_pc_energy_calibration: per-wire (own lsq / (K*ref)) -- 1.00 = reference ratio agrees with data:" << std::endl;
for (int wire = 0; wire < 48; ++wire)
{
int s = (wire < 24) ? 0 : 1;
if (!refOk[wire] || isDead[wire] || !ok_arr[wire] || refK[s] <= 0.0)
continue;
std::cout << " " << (s == 0 ? "anode " : "cathode ") << (s == 0 ? wire : wire - 24) << ": "
<< Form("%.2f", slope_lsq[wire] / (refK[s] * refSlope[wire])) << std::endl;
}
}
// --- Setup ROOT Diagnostic Graphics ---
gROOT->SetBatch(kTRUE); // Run silently without popping up windows
gStyle->SetOptStat(0);
@ -326,7 +476,8 @@ void fit_pc_energy_calibration(const std::string& dataset_filter = "",
double n = n_arr[wire];
bool ok = ok_arr[wire];
double slope = 1.0, intercept = 0.0;
int method = 0; // 0 = identity/no data, 1 = least-squares, 2 = peak-matched (a1c2 consensus), 3 = known dead
int method = 0; // 0 = identity/no data, 1 = least-squares, 2 = peak-matched (a1c2 consensus), 3 = known dead,
// 4 = ratio_reference x per-side scale
if (isDead[wire])
{
@ -376,6 +527,17 @@ void fit_pc_energy_calibration(const std::string& dataset_filter = "",
// else: n>=1 but medX<=0 and no lsq fit either -- falls through to identity below.
}
if (useRef && !rescaleSide[wire < 24 ? 0 : 1])
{
slope = refSlope[wire]; // side excluded by ref_sides: reference row verbatim
method = refMethod[wire];
}
else if (useRef && refOk[wire] && !isDead[wire] && refK[wire < 24 ? 0 : 1] > 0.0)
{
slope = refK[wire < 24 ? 0 : 1] * refSlope[wire];
method = 4; // reference ratio x per-side physics scale
}
if (method == 0 && !isDead[wire]) {
std::cerr << "fit_pc_energy_calibration: wire " << wire << " has " << n
<< " point(s) and no usable consensus target -- writing identity (slope=1, intercept=0)" << std::endl;
@ -411,7 +573,7 @@ void fit_pc_energy_calibration(const std::string& dataset_filter = "",
if (drawFit) {
fitLine = new TF1(Form("fit_%d", wire), "[0]*x", 0, maxX * 1.05); // Formula is strictly y = m*x
fitLine->SetParameter(0, slope);
fitLine->SetLineColor(method == 2 ? kMagenta : kRed);
fitLine->SetLineColor(method == 4 ? kBlue : (method == 2 ? kMagenta : kRed));
fitLine->SetLineWidth(2);
pt = new TPaveText(0.15, 0.72, 0.55, 0.88, "NDC");
@ -422,6 +584,8 @@ void fit_pc_energy_calibration(const std::string& dataset_filter = "",
pt->AddText("b = 0 (Fixed)");
if (method == 2)
pt->AddText("PEAK-MATCHED (a1c2 consensus)");
if (method == 4)
pt->AddText(Form("REF RATIO x K = %.3g", refK[wire < 24 ? 0 : 1]));
if (floorADC[wire] > 0.0)
pt->AddText(Form("floor cut @ %.0f ADC", floorADC[wire]));
}