MUSIC unknown
Analysis for the MUSIC active-target ionization chamber
Loading...
Searching...
No Matches
CalibrateBeam.cpp
Go to the documentation of this file.
1#include "CalibrateBeam.hpp"
2#include <Rtypes.h>
3#include <TH2D.h>
4#include <utility>
5
6const Int_t kMaxChannels = 36;
7// Raw total of one strip: strips 0 and 17 read once, strips 1-16 the sum
8// of their two ends, indexed by strip - 1.
9inline Double_t StripTotalAdc(const UShort_t *l, const UShort_t *r, UShort_t s0,
10 UShort_t s17, Int_t s) {
11 if (s <= 0)
12 return Double_t(s0);
13 if (s >= 17)
14 return Double_t(s17);
15 return Double_t(l[s - 1]) + Double_t(r[s - 1]);
16}
17
21inline Int_t GatePartner(Int_t s) { return s <= 1 ? 0 : s - 1; }
22const Long64_t kMinSamples = 200;
23const Long64_t kSampleCap = 20000;
28const Long64_t kPairCap = 100000;
29
30// Defined below; forward-declared for the anchor reduction.
31void RobustPeakSeed(const std::vector<Float_t> &v, Double_t &mode,
32 Double_t &sigma);
33
34std::vector<ChannelCal> CalibrateBeam::BuildChannels() {
35 std::vector<ChannelCal> chans;
36 ChannelCal c{};
37 c.name = "Strip0";
38 c.side = 'S';
39 c.strip = 0;
40 chans.push_back(c);
41 c.name = "Strip17";
42 c.side = 'S';
43 c.strip = 17;
44 chans.push_back(c);
45 for (Int_t s = 1; s <= 16; s++) {
46 c.name = Form("L%d", s);
47 c.side = 'L';
48 c.strip = s;
49 chans.push_back(c);
50 c.name = Form("R%d", s);
51 c.side = 'R';
52 c.strip = s;
53 chans.push_back(c);
54 }
55 c.name = "Cathode";
56 c.side = 'C';
57 c.strip = -1;
58 chans.push_back(c);
59 // The grid: anchored on its modal beam peak like a strip, so the analysis
60 // can gate on it in beam units (GATE_AXIS_GRID).
61 c.name = "Grid";
62 c.side = 'G';
63 c.strip = -1;
64 chans.push_back(c);
65 return chans;
66}
67
68Char_t LongSide(Int_t strip) { return (strip % 2 == 0) ? 'R' : 'L'; }
69
70Bool_t IsCalibrated(const ChannelCal &c) { return c.fit_adc > 0; }
71
72Double_t Gain(const ChannelCal &c) {
73 return c.gain >= 0.0 ? c.gain : 1.0 / c.fit_adc;
74}
75
80BeamFit2D FindBeamGateStrips(const FileSpec &spec, Int_t sx, Int_t sy,
81 const TString &run_label,
82 const TString &plot_subdir,
83 Bool_t save_plot = kTRUE) {
84 BeamFit2D out;
85
86 TString sub = FileSet::EventsName(spec) + ".root";
87 TFile *sf = IO::OpenForReading(sub);
88 if (!sf || sf->IsZombie()) {
89 if (sf)
90 delete sf;
91 return out;
92 }
93 TTree *tree = static_cast<TTree *>(sf->Get("events"));
94 if (!tree) {
95 sf->Close();
96 delete sf;
97 return out;
98 }
99 // Raw ADC, read before any calibration exists (this fit produces it), so read
100 // the branches directly. strip1 total = L1 + R1; strip2 total = L2 + R2.
101 UShort_t leftdE_adc[16], rightdE_adc[16], strip0_adc = 0, strip17_adc = 0;
102 tree->SetBranchAddress("LeftdE", leftdE_adc);
103 tree->SetBranchAddress("RightdE", rightdE_adc);
104 tree->SetBranchAddress("Strip0dE", &strip0_adc);
105 tree->SetBranchAddress("Strip17dE", &strip17_adc);
106
111 const Int_t kBeamGateNBins = 1024;
112 TH2F *h =
113 new TH2F(Form("h2_gate_s%d_s%d_%s", sx, sy, run_label.Data()),
114 Form(";Strip%d #DeltaE [ADC];Strip%d #DeltaE [ADC]", sx, sy),
115 kBeamGateNBins, 0.0, Constants::ActiveStripEMaxAdc(),
116 kBeamGateNBins, 0.0, Constants::ActiveStripEMaxAdc());
117 h->SetDirectory(nullptr);
118 Long64_t n = tree->GetEntries();
119 // The same events unbinned (up to a cap), for the clipped moments below.
120 const Long64_t kMaxPoints = 2000000;
121 std::vector<std::pair<Float_t, Float_t>> pts;
122 pts.reserve(std::min(n, kMaxPoints));
123 for (Long64_t j = 0; j < n; j++) {
124 tree->GetEntry(j);
125 const Double_t tx =
126 StripTotalAdc(leftdE_adc, rightdE_adc, strip0_adc, strip17_adc, sx);
127 const Double_t ty =
128 StripTotalAdc(leftdE_adc, rightdE_adc, strip0_adc, strip17_adc, sy);
129 if (tx > 0.0 && ty > 0.0) {
130 h->Fill(tx, ty);
131 if (Long64_t(pts.size()) < kMaxPoints)
132 pts.push_back(std::make_pair(Float_t(tx), Float_t(ty)));
133 }
134 }
135 sf->Close();
136 delete sf;
137
138 if (h->GetEntries() < 100) {
139 Constants::DetailErr() << " " << run_label
140 << ": too few events for the strip " << sy
141 << " beam gate (strips " << sx << " vs " << sy << ")"
142 << std::endl;
143 delete h;
144 return out;
145 }
146
150 const Int_t kSeedHalfBins = 40;
151 Moments2D m = BeamFitUtils::SeedSpotMoments(h, kSeedHalfBins);
152 if (m.weight <= 0) {
153 Constants::DetailErr() << " " << run_label
154 << ": no bins above beam seed threshold"
155 << std::endl;
156 delete h;
157 return out;
158 }
159 // Re-center iteratively (moments inside ±2.5σ of the centroid); at high
160 // rate (run84: 45 kHz) the ~2x-beam pileup blob passes the seed cut.
161 const Int_t kMomentRefineIters = 4;
162 const Double_t kMomentRefineNSigma = 2.5;
163 const Double_t bw_x = h->GetXaxis()->GetBinWidth(1);
164 const Double_t bw_y = h->GetYaxis()->GetBinWidth(1);
165 Int_t bx, by, bz;
166 h->GetMaximumBin(bx, by, bz);
167 const Double_t peak_val = h->GetBinContent(bx, by);
168 for (Int_t iter = 0; iter < kMomentRefineIters; iter++) {
169 Int_t wlo_bx = std::max(
170 1, h->GetXaxis()->FindBin(m.mu_x - kMomentRefineNSigma * m.sigma_x));
171 Int_t whi_bx = std::min(
172 h->GetNbinsX(),
173 h->GetXaxis()->FindBin(m.mu_x + kMomentRefineNSigma * m.sigma_x));
174 Int_t wlo_by = std::max(
175 1, h->GetYaxis()->FindBin(m.mu_y - kMomentRefineNSigma * m.sigma_y));
176 Int_t whi_by = std::min(
177 h->GetNbinsY(),
178 h->GetYaxis()->FindBin(m.mu_y + kMomentRefineNSigma * m.sigma_y));
180 h, wlo_bx, whi_bx, wlo_by, whi_by, 0.30 * peak_val, bw_x, bw_y);
181 if (m_ref.weight <= 0)
182 break;
183 m = m_ref;
184 }
185 // The width BEAM_GATE_NSIGMA is in is a fitted sigma, finely binned
186 // around the seed; over the full ADC range the spot is a few bins wide.
188 out = spot.fit;
189 Constants::Detail() << " beam gate strip " << sy << " (strips " << sx
190 << " vs " << sy << "): "
191 << (spot.fit_used ? "fit"
192 : "CLIPPED MOMENTS (fit failed)")
193 << " mu=(" << out.mu_x << "," << out.mu_y << ") sigma=("
194 << out.sigma_x << "," << out.sigma_y
195 << ") rho=" << out.rho << " chi2/ndf=" << spot.chi2_ndf
196 << std::endl;
197
198 if (save_plot) {
199 TCanvas *cv = PlottingUtils::GetConfiguredCanvas(kFALSE);
200 PlottingUtils::ConfigureAndDraw2DHistogram(h, cv);
201 BeamFitUtils::DrawEllipse(out, Constants::cfg.BEAM_GATE_NSIGMA,
202 kViolet + 2);
204 PlottingUtils::SaveFigure(cv, Form("beam_gate_s%02d", sy),
205 plot_subdir + "/beam_gate",
206 PlotSaveOptions::kLINEAR);
207 delete cv;
208 }
209 delete h;
210 return out;
211}
212
213// Median by partial sort; v need not be sorted, but is only partially
214// ordered afterwards, so a sort is still needed for other order statistics.
215template <class T> inline Double_t Median(std::vector<T> &v) {
216 if (v.empty())
217 return 0.0;
218 Int_t n = Int_t(v.size());
219 std::nth_element(v.begin(), v.begin() + n / 2, v.end());
220 Double_t med = Double_t(v[n / 2]);
221 if (n % 2 == 0) {
222 std::nth_element(v.begin(), v.begin() + n / 2 - 1, v.end());
223 med = 0.5 * (med + Double_t(v[n / 2 - 1]));
224 }
225 return med;
226}
227
234void RobustPeakSeed(const std::vector<Float_t> &v, Double_t &mode,
235 Double_t &sigma) {
236 mode = 0.0;
237 sigma = 0.0;
238 Int_t n = Int_t(v.size());
239 if (n < 4)
240 return;
241 std::vector<Float_t> s(v);
242 std::sort(s.begin(), s.end());
243 Int_t i_lo = Int_t(0.05 * n);
244 Int_t i_hi = Int_t(0.95 * n);
245 Int_t i_q1 = Int_t(0.25 * n);
246 Int_t i_q3 = Int_t(0.75 * n);
247 if (i_hi >= n)
248 i_hi = n - 1;
249 Double_t p_lo = Double_t(s[i_lo]);
250 Double_t p_hi = Double_t(s[i_hi]);
251 Double_t med = Double_t(s[n / 2]);
252 sigma = (Double_t(s[i_q3]) - Double_t(s[i_q1])) / 1.349;
253 if (!(sigma > 0.0))
254 sigma = 0.05 * (med > 0.0 ? med : 1.0);
255 if (!(p_hi > p_lo)) {
256 mode = med;
257 return;
258 }
259 const Int_t nbins = 100;
260 TH1F h("robust_peak_seed_h", "", nbins, p_lo, p_hi);
261 h.SetDirectory(nullptr);
262 for (Int_t j = 0; j < n; j++)
263 if (Double_t(s[j]) >= p_lo && Double_t(s[j]) <= p_hi)
264 h.Fill(Double_t(s[j]));
265 mode = h.GetBinCenter(h.GetMaximumBin());
266 if (!(mode > 0.0))
267 mode = med;
268}
269
277 std::vector<Float_t> l;
278 std::vector<Float_t> r;
279 // Beam-gated, long-triggered pairs: keep events with silent short end
280 // (recorded 0) -- the population the decode sums; `l`/`r` need both ends.
281 std::vector<Float_t> gated_long;
282 std::vector<Float_t> gated_short;
283};
284
285// check_LR gain-match slice constants (37Cl_an_check_LR.ipynb); defined
286// before CollectAnchorSamplesOneSubfile, which picks the shoulder slice.
287
293 const std::vector<ChannelCal> &chans,
294 const BeamFit2D gate[18],
295 std::vector<std::vector<Float_t>> &samples,
296 StripPairSamples pairs[18]) {
297 Int_t n_chans = Int_t(chans.size());
298 samples.assign(n_chans, std::vector<Float_t>());
299 Bool_t any = kFALSE;
300 for (Int_t s = 0; s <= 17; s++)
301 any = any || gate[s].ok;
302 if (!any)
303 return;
304
305 TString sub = FileSet::EventsName(spec) + ".root";
306 TFile *sf = IO::OpenForReading(sub);
307 if (!sf || sf->IsZombie()) {
308 if (sf)
309 sf->Close();
310 return;
311 }
312 TTree *tree = static_cast<TTree *>(sf->Get("events"));
313 if (!tree) {
314 sf->Close();
315 delete sf;
316 return;
317 }
318 // Raw ADC, pre-calibration: the two ends of strips 1-16 and the
319 // unsegmented strips; strip totals are L+R.
320 UShort_t leftdE_adc[16], rightdE_adc[16], strip0_adc = 0, strip17_adc = 0;
321 Short_t cathode_adc = 0, grid_adc = 0;
322 tree->SetBranchAddress("LeftdE", leftdE_adc);
323 tree->SetBranchAddress("RightdE", rightdE_adc);
324 tree->SetBranchAddress("Strip0dE", &strip0_adc);
325 tree->SetBranchAddress("Strip17dE", &strip17_adc);
326 tree->SetBranchAddress("Cathode", &cathode_adc);
327 tree->SetBranchAddress("Grid", &grid_adc);
328
329 Long64_t n = tree->GetEntries();
330 for (Long64_t j = 0; j < n; j++) {
331 tree->GetEntry(j);
332 // (L, R) pairs for strips 1–16, UNGATED (check_LR runs on all events):
333 // a beam gate would remove the shoulder events (long side ~300-350 ADC).
334 if (pairs) {
335 for (Int_t s = 1; s <= 16; s++) {
336 Int_t lv = Int_t(leftdE_adc[s - 1]);
337 Int_t rv = Int_t(rightdE_adc[s - 1]);
338 if (lv > 0 && rv > 0 && Long64_t(pairs[s].l.size()) < kPairCap) {
339 pairs[s].l.push_back(Float_t(lv));
340 pairs[s].r.push_back(Float_t(rv));
341 }
342 }
343 }
344 // Which strips this event is beam-like in, each judged by its own gate.
345 Bool_t pass[18] = {kFALSE};
346 for (Int_t s = 1; s <= 17; s++) {
347 if (!gate[s].ok)
348 continue;
349 const Double_t x = StripTotalAdc(leftdE_adc, rightdE_adc, strip0_adc,
350 strip17_adc, GatePartner(s));
351 const Double_t y =
352 StripTotalAdc(leftdE_adc, rightdE_adc, strip0_adc, strip17_adc, s);
353 if (x <= 0.0 || y <= 0.0)
354 continue;
355 pass[s] = BeamFitUtils::InEllipseXY(gate[s], x, y,
356 Constants::cfg.BEAM_GATE_NSIGMA);
357 }
358 // Strip 0 shares strip 1's gate, so share its verdict: GatePartner(0) is
359 // 0, and a self-test lands every event outside, zeroing its gain.
360 pass[0] = pass[1];
361 if (pairs) {
362 for (Int_t s = 1; s <= 16; s++) {
363 if (!pass[s] || Long64_t(pairs[s].gated_long.size()) >= kPairCap)
364 continue;
365 Bool_t l_is_long = (LongSide(s) == 'L');
366 Int_t long_v =
367 l_is_long ? Int_t(leftdE_adc[s - 1]) : Int_t(rightdE_adc[s - 1]);
368 Int_t short_v =
369 l_is_long ? Int_t(rightdE_adc[s - 1]) : Int_t(leftdE_adc[s - 1]);
370 if (long_v > 0) {
371 pairs[s].gated_long.push_back(Float_t(long_v));
372 pairs[s].gated_short.push_back(Float_t(short_v > 0 ? short_v : 0));
373 }
374 }
375 }
376 for (Int_t i = 0; i < n_chans; i++) {
377 if (Long64_t(samples[i].size()) >= kSampleCap)
378 continue;
379 const ChannelCal &c = chans[i];
380 // The cathode and the grid have no strip of their own, so they ride on
381 // strip 1's gate.
382 const Int_t s = (c.side == 'C' || c.side == 'G') ? 1 : c.strip;
383 if (s < 0 || s > 17 || !pass[s])
384 continue;
385 Int_t v = 0;
386 if (c.side == 'S')
387 v = Int_t(c.strip == 0 ? strip0_adc : strip17_adc);
388 else if (c.side == 'L')
389 v = Int_t(leftdE_adc[c.strip - 1]);
390 else if (c.side == 'R')
391 v = Int_t(rightdE_adc[c.strip - 1]);
392 else if (c.side == 'C')
393 v = Int_t(cathode_adc);
394 else if (c.side == 'G')
395 v = Int_t(grid_adc);
396 if (v > 0)
397 samples[i].push_back(Float_t(v));
398 }
399 }
400 sf->Close();
401 delete sf;
402}
403
453const Int_t kGmBins = 512;
475const Double_t kRidgeShortMinFrac =
476 0.15; // start of the short window, as a fraction of C_long
477const Double_t kRidgeShortMaxFrac =
478 0.5; // cap on short, as a fraction of C_long
479const Double_t kRidgeBandLo = 0.30; // single-particle band, x C_long
480const Double_t kRidgeBandHi = 1.45;
481const Int_t kRidgeSlices = 60; // at most this many slices of the window
482const Int_t kRidgeMinSlices = 6; // at least this many, for sparse ridges
483const Long64_t kRidgeMinPerSlice = 200;
484// Absolute noise floor per slice when the per-slice minimum is relaxed for
485// thinly-spread ridges: below this a slice median is dominated by shot noise.
486const Long64_t kRidgeNoiseFloor = 20;
487const Int_t kRidgeMinPts = 6;
488// C_short/C_long is a preamp gain ratio, order unity; outside this range
489// the ridge fit failed in a way the intercept test cannot see.
490const Double_t kRidgeRatioLo = 0.50;
491const Double_t kRidgeRatioHi = 2.00;
492const Double_t kGmEsumLo = 0.8; // a.u. eSum peak search window (pass 2)
493const Double_t kGmEsumHi = 2.5;
494
498struct RidgeFit {
499 std::vector<Double_t> x, y, ey; // slice centres, medians, median errors
500 Double_t lo = 0.0, hi = 0.0; // short-axis fit window
501 Double_t slope = 0.0, intercept = 0.0;
502 Double_t c_long = 0.0, c_short = 0.0;
503 Bool_t fitted = kFALSE; // a line was fitted (it may still be rejected)
504 // Diagnostic: how far a strip got before the fit, so a "not measurable"
505 // log line can say which check rejected it instead of only slope/intercept.
506 Long64_t n_gated = 0; // pairs handed to RidgeShortAnchor
507 Long64_t n_short_window = 0; // stayed inside the short window
508 Long64_t n_long_band = 0; // also inside the long band
509 Int_t n_slices = 0; // slices with >= kRidgeMinPerSlice
510 Long64_t min_per_slice = 0; // adaptive per-slice minimum used this strip
511 const char *fail = ""; // the check that returned 0, if any
512};
513
519Bool_t TheilSenLine(const std::vector<Double_t> &x,
520 const std::vector<Double_t> &y, Double_t &slope,
521 Double_t &intercept) {
522 const Int_t n = Int_t(x.size());
523 if (n < 2)
524 return kFALSE;
525 std::vector<Double_t> slopes;
526 slopes.reserve(n * (n - 1) / 2);
527 for (Int_t i = 0; i < n; i++)
528 for (Int_t j = i + 1; j < n; j++) {
529 Double_t dx = x[j] - x[i];
530 if (dx == 0.0)
531 continue;
532 slopes.push_back((y[j] - y[i]) / dx);
533 }
534 if (slopes.empty())
535 return kFALSE;
536 slope = Median(slopes);
537 std::vector<Double_t> resid;
538 resid.reserve(n);
539 for (Int_t i = 0; i < n; i++)
540 resid.push_back(y[i] - slope * x[i]);
541 intercept = Median(resid);
542 return kTRUE;
543}
544
553Double_t RidgeShortAnchor(const std::vector<Float_t> &v_short,
554 const std::vector<Float_t> &v_long, Double_t c_long,
555 Double_t &slope_out, Double_t &intercept_out,
556 RidgeFit &dbg) {
557 slope_out = 0.0;
558 intercept_out = 0.0;
559 dbg.c_long = c_long;
560 dbg.n_gated = Long64_t(v_short.size());
561 if (c_long <= 0 || v_short.size() != v_long.size() || v_short.size() < 500) {
562 dbg.fail = c_long <= 0 ? "c_long<=0"
563 : v_short.size() != v_long.size() ? "size mismatch"
564 : "fewer than 500 gated pairs";
565 return 0.0;
566 }
567 const Double_t hi = kRidgeShortMaxFrac * c_long;
568 const Double_t lo = kRidgeShortMinFrac * c_long;
569 // Pairs inside the short window and the long band, kept so the slice count
570 // can be chosen from how many there are before they are binned.
571 std::vector<std::pair<Double_t, Double_t>> in_window;
572 for (Int_t j = 0; j < Int_t(v_short.size()); j++) {
573 Double_t sh = Double_t(v_short[j]), lg = Double_t(v_long[j]);
574 if (sh < lo || sh >= hi)
575 continue;
576 dbg.n_short_window++;
577 if (lg <= kRidgeBandLo * c_long || lg >= kRidgeBandHi * c_long)
578 continue;
579 dbg.n_long_band++;
580 in_window.push_back(std::make_pair(sh, lg));
581 }
582 // Slice count from the population: kRidgeMinPerSlice per slice on average,
583 // between kRidgeMinSlices (sparse censored ends) and kRidgeSlices.
584 Int_t n_slices = Int_t(Long64_t(in_window.size()) / kRidgeMinPerSlice);
585 n_slices = TMath::Max(kRidgeMinSlices, TMath::Min(kRidgeSlices, n_slices));
586 std::vector<std::vector<Double_t>> slice(n_slices);
587 for (Int_t j = 0; j < Int_t(in_window.size()); j++) {
588 Int_t b = Int_t((in_window[j].first - lo) / (hi - lo) * n_slices);
589 if (b >= 0 && b < n_slices)
590 slice[b].push_back(in_window[j].second);
591 }
592 std::vector<Double_t> x, y, ey;
593 {
594 // Per-slice min: strict floor first; if kRidgeMinPts slices don't fill,
595 // halve & retry down to the noise floor (the censored ends are sparse).
596 Long64_t min_per_slice = kRidgeMinPerSlice;
597 for (;;) {
598 x.clear();
599 y.clear();
600 ey.clear();
601 for (Int_t b = 0; b < n_slices; b++) {
602 if (Long64_t(slice[b].size()) < min_per_slice)
603 continue;
604 std::sort(slice[b].begin(), slice[b].end());
605 const Int_t nb = Int_t(slice[b].size());
606 // The IQR reads the sorted slice, so it comes before the median,
607 // which only partially orders it.
608 const Double_t iqr = slice[b][nb * 3 / 4] - slice[b][nb / 4];
609 const Double_t med = Median(slice[b]);
610 x.push_back(lo + (b + 0.5) * (hi - lo) / n_slices);
611 y.push_back(med);
612 ey.push_back(1.253 * (iqr / 1.349) / TMath::Sqrt(Double_t(nb)));
613 }
614 if (Int_t(x.size()) >= kRidgeMinPts || min_per_slice <= kRidgeNoiseFloor)
615 break;
616 // Halve the floor and retry.
617 min_per_slice = TMath::Max(kRidgeNoiseFloor, min_per_slice / 2);
618 }
619 dbg.min_per_slice = min_per_slice;
620 }
621 dbg.x = x;
622 dbg.y = y;
623 dbg.ey = ey;
624 dbg.lo = lo;
625 dbg.hi = hi;
626 dbg.n_slices = Int_t(x.size());
627 if (Int_t(x.size()) < kRidgeMinPts) {
628 dbg.fail = "fewer than kRidgeMinPts filled slices";
629 return 0.0;
630 }
631 // TheilSen: median of pairwise slopes; outlying slice medians (2/3-particle
632 // bands, low-long background) can't bend the slope as a chi-square pol1.
633 Double_t slope = 0.0, inter = 0.0;
634 if (!TheilSenLine(x, y, slope, inter)) {
635 dbg.fail = "degenerate slice set (TheilSenLine)";
636 return 0.0;
637 }
638 slope_out = slope;
639 intercept_out = inter;
640 dbg.slope = slope;
641 dbg.intercept = inter;
642 dbg.fitted = kTRUE;
643 if (slope < 0)
644 dbg.c_short = -inter / slope;
645 if (slope >= 0) {
646 dbg.fail = "slope >= 0";
647 return 0.0;
648 }
649 // The line must cross the long axis near the modal seed (the short-end
650 // offset adds up to ~20% on the R-chain); a large departure means a miss.
651 if (inter < 0.80 * c_long || inter > 1.35 * c_long) {
652 dbg.fail = "intercept outside [0.80, 1.35]*C_long";
653 return 0.0;
654 }
655 // The line's short-axis crossing, as the "measurable" flag; the caller
656 // works from slope and intercept directly.
657 return -inter / slope;
658}
659
665void SaveRidgeFitPlots(const StripPairSamples pairs[18], const RidgeFit dbg[18],
666 const TString &plot_subdir) {
667 TString subdir = plot_subdir + "/ridge";
668 for (Int_t s = 1; s <= 16; s++) {
669 const RidgeFit &d = dbg[s];
670 const StripPairSamples &p = pairs[s];
671 if (d.c_long <= 0 || p.gated_short.empty())
672 continue;
673 Bool_t l_is_long = (LongSide(s) == 'L');
674 // A little past the fit window, so the cap the fit stops at is visible.
675 Double_t xhi = (d.hi > 0 ? d.hi : kRidgeShortMaxFrac * d.c_long) * 1.35;
676 TH2F *h = new TH2F(Form("h_ridge_s%d", s),
677 Form(";Strip %d Short (%c) #DeltaE [ADC];Strip %d Long "
678 "(%c) #DeltaE [ADC]",
679 s, l_is_long ? 'R' : 'L', s, l_is_long ? 'L' : 'R'),
680 200, 0.0, xhi, 200, 0.0, 1.60 * d.c_long);
681 for (Int_t j = 0; j < Int_t(p.gated_short.size()); j++)
682 h->Fill(Double_t(p.gated_short[j]), Double_t(p.gated_long[j]));
683 TCanvas *cv = PlottingUtils::GetConfiguredCanvas(kFALSE);
684 PlottingUtils::Configure2DHistogram(h, cv);
685 h->Draw("COLZ");
686 TGraphErrors *g = nullptr;
687 if (!d.x.empty()) {
688 g = new TGraphErrors(Int_t(d.x.size()), &d.x[0], &d.y[0], nullptr,
689 &d.ey[0]);
690 g->SetMarkerStyle(20);
691 g->SetMarkerSize(0.8);
692 g->SetMarkerColor(kBlack);
693 g->SetLineColor(kBlack);
694 g->Draw("P SAME");
695 }
696 // Extended to short = 0 on purpose: where the line lands above the
697 // long-only peak is the short-end offset times the slope.
698 TLine *lf = nullptr;
699 if (d.fitted) {
700 lf = new TLine(0.0, d.intercept, xhi, d.intercept + d.slope * xhi);
701 lf->SetLineColor(kViolet + 2);
702 lf->SetLineWidth(2);
703 lf->Draw();
704 }
705 // The short window the slices were taken from, so the threshold blob left
706 // of it can be seen to be excluded.
707 TLine *lw[2] = {nullptr, nullptr};
708 if (d.hi > 0) {
709 const Double_t ymax = 1.60 * d.c_long;
710 lw[0] = new TLine(d.lo, 0.0, d.lo, ymax);
711 lw[1] = new TLine(d.hi, 0.0, d.hi, ymax);
712 for (Int_t k = 0; k < 2; k++) {
713 lw[k]->SetLineColor(kGray + 2);
714 lw[k]->SetLineStyle(2);
715 lw[k]->Draw();
716 }
717 }
719 PlottingUtils::SaveFigure(cv, Form("ridge_s%02d", s), subdir,
720 PlotSaveOptions::kLINEAR);
721 delete cv;
722 delete g;
723 delete lf;
724 delete lw[0];
725 delete lw[1];
726 delete h;
727 }
728}
729
733Double_t GmFindPeak(const std::vector<Float_t> &v, Double_t lo, Double_t hi,
734 Double_t skip_frac) {
735 if (v.empty() || hi <= lo)
736 return 0.0;
737 std::vector<Long64_t> h(kGmBins, 0);
738 Double_t bw = (hi - lo) / kGmBins;
739 for (Int_t j = 0; j < Int_t(v.size()); j++) {
740 Double_t x = Double_t(v[j]);
741 if (x < lo || x >= hi)
742 continue;
743 Int_t b = Int_t((x - lo) / bw);
744 if (b >= 0 && b < kGmBins)
745 h[b]++;
746 }
747 Int_t skip = Int_t(kGmBins * skip_frac);
748 Int_t bmax = -1;
749 Long64_t vmax = 0;
750 for (Int_t b = skip; b < kGmBins; b++) {
751 if (h[b] > vmax) {
752 vmax = h[b];
753 bmax = b;
754 }
755 }
756 if (bmax < 0 || vmax <= 0)
757 return 0.0;
758 return lo + (bmax + 0.5) * bw;
759}
760
761// The L/R gain match state per strip: the anchors and short offset, the
762// measured flags, and the found and median ratios and offsets.
764 Bool_t matched[18] = {kFALSE};
765 Bool_t ridge_ok[18] = {kFALSE};
766 Double_t long_anchor_adc[18] = {0};
767 Double_t short_anchor_adc[18] = {0};
768 Double_t short_offset_adc[18] = {0};
769 std::vector<Double_t> ratios_found[2];
770 std::vector<Double_t> offsets_found[2];
771 Double_t median_ratio[2] = {0.0, 0.0};
772 Double_t median_offset[2] = {0.0, 0.0};
773 Double_t global_ratio = 0.0;
774};
775
776// Each strip's L/R channel indices in the cal vector, -1 where absent.
777static void IndexStripEnds(const std::vector<ChannelCal> &chans,
778 Int_t idx_l[18], Int_t idx_r[18]) {
779 for (Int_t s = 0; s < 18; s++) {
780 idx_l[s] = -1;
781 idx_r[s] = -1;
782 }
783 for (Int_t i = 0; i < Int_t(chans.size()); i++) {
784 if (chans[i].strip >= 1 && chans[i].strip <= 16) {
785 if (chans[i].side == 'L')
786 idx_l[chans[i].strip] = i;
787 else if (chans[i].side == 'R')
788 idx_r[chans[i].strip] = i;
789 }
790 }
791}
792
793// One strip's beam-gated ridge fit: the gate removes pile-up/junk while
794// keeping off-centre crossings; the line is long = a - k short.
795static void FitStripRidge(Int_t s, const StripPairSamples &p,
796 const ChannelCal &c_long, RidgeFit &dbg,
797 Double_t &peak_short, Double_t &peak_long,
798 Double_t &offset) {
799 const std::vector<Float_t> &v_short = p.gated_short;
800 const std::vector<Float_t> &v_long = p.gated_long;
801 Double_t slope = 0.0, inter = 0.0;
802 const Double_t crossing =
803 RidgeShortAnchor(v_short, v_long, c_long.fit_adc, slope, inter, dbg);
804 if (crossing > 0) {
805 const Double_t k = -slope;
806 peak_long = c_long.fit_adc;
807 peak_short = peak_long / k;
808 offset = (inter - peak_long) / k;
809 Constants::Detail() << " strip " << s
810 << " ridge slope=" << Form("%.3f", slope)
811 << " intercept=" << Form("%.1f", inter)
812 << " ADC; long anchor=" << Form("%.1f", peak_long)
813 << " (modal) short anchor=" << Form("%.1f", peak_short)
814 << " ADC short offset=" << Form("%.1f", offset)
815 << " ADC (" << Form("%.3f", offset / peak_long)
816 << " x C_long)" << std::endl;
817 } else
818 Constants::DetailErr() << " strip " << s << ": ridge not measurable "
819 << "(slope=" << Form("%.3f", slope)
820 << ", intercept=" << Form("%.1f", inter) << ")"
821 << (dbg.fail[0] ? Form(" [%s]", dbg.fail) : "")
822 << " gated=" << Form("%lld", dbg.n_gated)
823 << " short_win=" << Form("%lld", dbg.n_short_window)
824 << " long_band=" << Form("%lld", dbg.n_long_band)
825 << " slices=" << dbg.n_slices
826 << " min_per_slice=" << dbg.min_per_slice
827 << std::endl;
828}
829
830// Pass 1: per strip the LONG anchor = the modal peak, the SHORT anchor
831// C_long/k and the offset from the ridge line; the fallback fills gaps.
832static void MeasureRidges(std::vector<ChannelCal> &chans,
833 const StripPairSamples pairs[18],
834 const Int_t idx_l[18], const Int_t idx_r[18],
835 RidgeFit *ridge_dbg, GainMatchState &m) {
836 for (Int_t s = 1; s <= 16; s++) {
837 if (idx_l[s] < 0 || idx_r[s] < 0)
838 continue;
839 Bool_t l_is_long = (LongSide(s) == 'L');
840 ChannelCal &c_long = chans[l_is_long ? idx_l[s] : idx_r[s]];
841 ChannelCal &c_short = chans[l_is_long ? idx_r[s] : idx_l[s]];
842 if (!IsCalibrated(c_long)) {
844 << " strip " << s
845 << ": long side uncalibrated; skipping L/R gain match" << std::endl;
846 continue;
847 }
848 Double_t peak_short = 0.0, peak_long = 0.0, offset = 0.0;
849 FitStripRidge(s, pairs[s], c_long, ridge_dbg[s], peak_short, peak_long,
850 offset);
851 if (peak_short <= 0 || peak_long <= 0)
852 continue;
853 // C_short/C_long = -1/slope is a preamp-gain ratio, so order unity.
854 // Catches ridges too flat for the intercept test: ratio absurd.
855 Double_t ratio = peak_short / peak_long;
856 if (ratio < kRidgeRatioLo || ratio > kRidgeRatioHi) {
858 << " strip " << s << ": ridge ratio " << Form("%.2f", ratio)
859 << " outside [" << kRidgeRatioLo << ", " << kRidgeRatioHi
860 << "]; rejecting anchor " << Form("%.1f", peak_short) << " ADC"
861 << std::endl;
862 continue;
863 }
864 m.long_anchor_adc[s] = peak_long;
865 m.short_anchor_adc[s] = peak_short;
866 m.short_offset_adc[s] = offset;
867 m.ridge_ok[s] = kTRUE;
868 m.ratios_found[s % 2].push_back(ratio);
869 m.offsets_found[s % 2].push_back(offset / peak_long);
870 c_short.ridge_ratio = ratio;
871 c_short.ridge_offset = offset / peak_long;
872 }
873}
874
875// The fallback medians, per parity (the L/R preamps differ, odd ~1.2 and
876// even ~0.9) and globally over the ratios.
877static void RidgeFallbackMedians(GainMatchState &m) {
878 // Fall back on the median RATIO (preamp property, transfers across
879 // strips; ADC anchors don't).
880 std::vector<Double_t> all_ratios;
881 for (Int_t par = 0; par < 2; par++) {
882 std::vector<Double_t> &v = m.ratios_found[par];
883 all_ratios.insert(all_ratios.end(), v.begin(), v.end());
884 if (v.size() < 2)
885 continue;
886 m.median_ratio[par] = Median(v);
887 }
888 if (!all_ratios.empty())
889 m.global_ratio = Median(all_ratios);
890 Constants::Detail() << " ridge ratio medians: odd="
891 << Form("%.3f", m.median_ratio[1])
892 << " even=" << Form("%.3f", m.median_ratio[0])
893 << " all=" << Form("%.3f", m.global_ratio) << std::endl;
894 // The offset fallback, likewise per parity (it is a chain property), in
895 // units of C_long; 0 where the parity measured none.
896 for (Int_t par = 0; par < 2; par++) {
897 std::vector<Double_t> &v = m.offsets_found[par];
898 if (v.size() < 2)
899 continue;
900 m.median_offset[par] = Median(v);
901 }
902 Constants::Detail() << " ridge offset medians (x C_long): odd="
903 << Form("%.3f", m.median_offset[1])
904 << " even=" << Form("%.3f", m.median_offset[0])
905 << std::endl;
906}
907
908// Apply the anchors: the parity-median fallback where the ridge was not
909// measurable, then the gains and the short offset, with the detail lines.
910static void ApplyGainMatch(std::vector<ChannelCal> &chans,
911 const Int_t idx_l[18], const Int_t idx_r[18],
912 GainMatchState &m) {
913 for (Int_t s = 1; s <= 16; s++) {
914 if (idx_l[s] < 0 || idx_r[s] < 0)
915 continue;
916 Bool_t l_is_long = (LongSide(s) == 'L');
917 ChannelCal &c_long = chans[l_is_long ? idx_l[s] : idx_r[s]];
918 ChannelCal &c_short = chans[l_is_long ? idx_r[s] : idx_l[s]];
919 if (!IsCalibrated(c_long))
920 continue;
921 if (m.short_anchor_adc[s] <= 0) {
922 // No ridge: the modal peak is the best long anchor available.
923 m.long_anchor_adc[s] = c_long.fit_adc;
924 Double_t r =
925 m.median_ratio[s % 2] > 0 ? m.median_ratio[s % 2] : m.global_ratio;
926 if (r <= 0) {
927 Constants::DetailErr() << " strip " << s
928 << ": no ridge and no ratio fallback; keeping "
929 "independent gains"
930 << std::endl;
931 continue;
932 }
933 m.short_anchor_adc[s] = r * m.long_anchor_adc[s];
934 m.short_offset_adc[s] = m.median_offset[s % 2] * m.long_anchor_adc[s];
936 << " strip " << s << " short_anchor=ratio fallback "
937 << Form("%.3f", r)
938 << " x C_long = " << Form("%.1f", m.short_anchor_adc[s]) << " ADC"
939 << (m.median_ratio[s % 2] > 0 ? "" : " (global, parity had none)")
940 << " short offset=parity median "
941 << Form("%.3f", m.median_offset[s % 2])
942 << " x C_long = " << Form("%.1f", m.short_offset_adc[s]) << " ADC"
943 << std::endl;
944 }
945 c_long.gain = 1.0 / m.long_anchor_adc[s];
946 c_short.gain = 1.0 / m.short_anchor_adc[s];
947 c_short.offset_adc = m.short_offset_adc[s];
948 m.matched[s] = kTRUE;
949 Constants::Detail() << " strip " << s << " L/R match: long anchor="
950 << Form("%.1f", m.long_anchor_adc[s])
951 << " ADC short anchor="
952 << Form("%.1f", m.short_anchor_adc[s])
953 << " ADC short offset="
954 << Form("%.1f", m.short_offset_adc[s]) << " ADC"
955 << (m.ridge_ok[s] ? "" : " (fallback)") << std::endl;
956 }
957}
958
959// Pass 2: check, do not correct. On the pairs on the ridge, eSum with the
960// short offset removed peaks at 1.0 a.u.; rescaling would hide a failure.
961static void CheckSummedBeamPeaks(std::vector<ChannelCal> &chans,
962 const StripPairSamples pairs[18],
963 const Int_t idx_l[18], const Int_t idx_r[18],
964 const GainMatchState &m) {
965 Double_t esum_peak[18] = {0};
966 for (Int_t s = 1; s <= 16; s++) {
967 if (!m.matched[s])
968 continue;
969 const StripPairSamples &p = pairs[s];
970 Bool_t l_is_long = (LongSide(s) == 'L');
971 Double_t g_long = Gain(chans[l_is_long ? idx_l[s] : idx_r[s]]);
972 Double_t g_short = Gain(chans[l_is_long ? idx_r[s] : idx_l[s]]);
973 std::vector<Float_t> esum;
974 esum.reserve(p.gated_long.size());
975 const Double_t sh_lo = kRidgeShortMinFrac * m.long_anchor_adc[s];
976 const Double_t sh_hi = kRidgeShortMaxFrac * m.long_anchor_adc[s];
977 for (Int_t j = 0; j < Int_t(p.gated_long.size()); j++) {
978 if (p.gated_short[j] < sh_lo || p.gated_short[j] >= sh_hi)
979 continue;
980 esum.push_back(Float_t(
981 g_long * Double_t(p.gated_long[j]) +
982 g_short * (Double_t(p.gated_short[j]) - m.short_offset_adc[s])));
983 }
984 esum_peak[s] = GmFindPeak(esum, kGmEsumLo, kGmEsumHi, 0.0);
985 }
986 for (Int_t s = 1; s <= 16; s++) {
987 if (!m.matched[s])
988 continue;
989 if (esum_peak[s] <= 0) {
991 << " strip " << s << ": no summed beam peak in (" << kGmEsumLo
992 << ", " << kGmEsumHi << ") a.u." << std::endl;
993 continue;
994 }
995 Double_t dev = esum_peak[s] - 1.0;
996 Constants::Detail() << " strip " << s
997 << " summed beam peak=" << Form("%.4f", esum_peak[s])
998 << " a.u. (" << Form("%+.1f%%", 100.0 * dev) << ")"
999 << (TMath::Abs(dev) > 0.05 ? " <-- check ridge fit"
1000 : "")
1001 << std::endl;
1002 }
1003}
1004
1005void ComputeLRGainMatch(std::vector<ChannelCal> &chans,
1006 const StripPairSamples pairs[18],
1007 const TString &plot_subdir) {
1008 RidgeFit ridge_dbg[18];
1009 Int_t idx_l[18], idx_r[18];
1010 IndexStripEnds(chans, idx_l, idx_r);
1011
1013 MeasureRidges(chans, pairs, idx_l, idx_r, ridge_dbg, m);
1014
1015 // Between measure and fallback: the detail lines keep their order.
1016 SaveRidgeFitPlots(pairs, ridge_dbg, plot_subdir);
1017
1018 RidgeFallbackMedians(m);
1019 ApplyGainMatch(chans, idx_l, idx_r, m);
1020 CheckSummedBeamPeaks(chans, pairs, idx_l, idx_r, m);
1021}
1022
1027void ReduceToAnchors(std::vector<ChannelCal> &chans,
1028 std::vector<std::vector<Float_t>> &samples,
1029 const StripPairSamples pairs[18],
1030 const TString &plot_subdir) {
1031 Int_t n_chans = Int_t(chans.size());
1032
1033 for (Int_t i = 0; i < n_chans; i++) {
1034 ChannelCal &c = chans[i];
1035 std::vector<Float_t> &v = samples[i];
1036 c.n_samples = Long64_t(v.size());
1037
1038 if (Long64_t(v.size()) < kMinSamples) {
1039 c.fit_adc = 0;
1040 continue;
1041 }
1042 if (c.side == 'C') {
1043 // Cathode: the median (asymmetric tail, no clean peak).
1044 c.fit_adc = Median(v);
1045 } else {
1046 // The modal bin, never the tail-biased mean.
1047 Double_t mode = 0.0, rsigma = 0.0;
1048 RobustPeakSeed(v, mode, rsigma);
1049 c.fit_adc = mode;
1050 }
1051 Constants::Detail() << " " << c.name << " anchor[ADC]=" << c.fit_adc
1052 << " (n=" << c.n_samples << ")" << std::endl;
1053 }
1054
1055 // L/R match (strips 1-16): both anchors from the charge-sharing ridge, the
1056 // modal peak above being the seed and the fallback.
1057 if (pairs)
1058 ComputeLRGainMatch(chans, pairs, plot_subdir);
1059}
1060
1064void WriteCalibrationTree(TFile *dst, const std::vector<ChannelCal> &chans,
1065 const StripAlignmentResult *align) {
1066 dst->cd();
1067 if (TObject *old = dst->Get("calibration"))
1068 old->Delete();
1069 TTree *cal = new TTree("calibration", "Per-channel normMUSIC calibration");
1070 Float_t gain[kMaxChannels] = {0};
1071 Float_t fit_adc[kMaxChannels] = {0};
1072 Long64_t fit_n[kMaxChannels] = {0};
1073 Bool_t ok[kMaxChannels] = {0};
1074 // GainLeft[k]/GainRight[k] scale the two ends of strip k+1;
1075 // GainStrip0/GainStrip17 the unsegmented strips.
1076 Float_t gain_left[16] = {0}, gain_right[16] = {0};
1077 Float_t gain_strip0 = 0.0f, gain_strip17 = 0.0f;
1078 Float_t gain_cathode = 0.0f;
1079 Float_t gain_grid = 0.0f;
1080 // OffsetLeft[k]/OffsetRight[k] are subtracted before the gain, only when
1081 // that end fired (the short-end offset); zero on the long ends.
1082 Float_t offset_left[16] = {0}, offset_right[16] = {0};
1083 // Per-strip ridge ratio and offset (x C_long) measured in this subfile,
1084 // 0 where not measurable; AggregateRidgeRatiosForRun medians them.
1085 Float_t ridge_ratio[18] = {0};
1086 Float_t ridge_offset[18] = {0};
1087 Float_t long_anchor[18] = {0};
1088 Int_t n_actual = TMath::Min(Int_t(chans.size()), kMaxChannels);
1089 for (Int_t k = 0; k < n_actual; k++) {
1090 const ChannelCal &c = chans[k];
1091 ok[k] = IsCalibrated(c) || c.gain > 0;
1092 gain[k] = ok[k] ? Float_t(Gain(c)) : 0.0f;
1093 fit_adc[k] = Float_t(c.fit_adc);
1094 fit_n[k] = c.n_samples;
1095 if (c.strip >= 1 && c.strip <= 16 && (c.side == 'L' || c.side == 'R')) {
1096 if (c.ridge_ratio > 0) {
1097 ridge_ratio[c.strip] = Float_t(c.ridge_ratio);
1098 ridge_offset[c.strip] = Float_t(c.ridge_offset);
1099 }
1100 // The long anchor in use (the modal peak): AggregateRidgeRatiosForRun
1101 // rebuilds the short gain and offset from it.
1102 if (c.side == LongSide(c.strip) && ok[k] && gain[k] > 0)
1103 long_anchor[c.strip] = Float_t(1.0 / gain[k]);
1104 }
1105 if (c.side == 'S' && c.strip == 0)
1106 gain_strip0 = gain[k];
1107 else if (c.side == 'S' && c.strip == 17)
1108 gain_strip17 = gain[k];
1109 else if (c.side == 'L' && c.strip >= 1 && c.strip <= 16) {
1110 gain_left[c.strip - 1] = gain[k];
1111 offset_left[c.strip - 1] = Float_t(c.offset_adc);
1112 } else if (c.side == 'R' && c.strip >= 1 && c.strip <= 16) {
1113 gain_right[c.strip - 1] = gain[k];
1114 offset_right[c.strip - 1] = Float_t(c.offset_adc);
1115 } else if (c.side == 'C')
1116 gain_cathode = gain[k];
1117 else if (c.side == 'G')
1118 gain_grid = gain[k];
1119 }
1120 cal->Branch("Gain", gain, Form("Gain[%d]/F", kMaxChannels));
1121 cal->Branch("Ok", ok, Form("Ok[%d]/O", kMaxChannels));
1122 cal->Branch("FitADC", fit_adc, Form("FitADC[%d]/F", kMaxChannels));
1123 cal->Branch("FitN", fit_n, Form("FitN[%d]/L", kMaxChannels));
1124 cal->Branch("GainLeft", gain_left, "GainLeft[16]/F");
1125 cal->Branch("GainRight", gain_right, "GainRight[16]/F");
1126 cal->Branch("GainStrip0", &gain_strip0, "GainStrip0/F");
1127 cal->Branch("GainStrip17", &gain_strip17, "GainStrip17/F");
1128 cal->Branch("GainCathode", &gain_cathode, "GainCathode/F");
1129 cal->Branch("GainGrid", &gain_grid, "GainGrid/F");
1130 cal->Branch("OffsetLeft", offset_left, "OffsetLeft[16]/F");
1131 cal->Branch("OffsetRight", offset_right, "OffsetRight[16]/F");
1132 cal->Branch("RidgeRatio", ridge_ratio, "RidgeRatio[18]/F");
1133 cal->Branch("RidgeOffset", ridge_offset, "RidgeOffset[18]/F");
1134 cal->Branch("LongAnchor", long_anchor, "LongAnchor[18]/F");
1135
1136 // Per-strip two-point alignment: StripFactor on every end, StripOffset on
1137 // the long end, so beam = 1 and pile-up = 2 on the strip total.
1138 Float_t strip_factor[18], strip_offset[18];
1139 for (Int_t s = 0; s < 18; s++) {
1140 strip_factor[s] = 1.0f;
1141 strip_offset[s] = 0.0f;
1142 }
1143 if (align && align->ok)
1144 for (Int_t s = 0; s < 18; s++) {
1145 strip_factor[s] = Float_t(align->factors[s]);
1146 strip_offset[s] = Float_t(align->offsets[s]);
1147 }
1148 cal->Branch("StripFactor", strip_factor, "StripFactor[18]/F");
1149 cal->Branch("StripOffset", strip_offset, "StripOffset[18]/F");
1150 cal->Fill();
1151 cal->Write("calibration", TObject::kOverwrite);
1152}
1153
1160 const std::vector<ChannelCal> &chans,
1161 const StripAlignmentResult *align) {
1162 TString events_subpath = FileSet::EventsName(spec) + ".root";
1163 TFile *f = IO::OpenForWriting(events_subpath, "UPDATE");
1164 if (!f || f->IsZombie()) {
1165 std::cerr << "Cannot open " << events_subpath << " to write calibration"
1166 << std::endl;
1167 if (f)
1168 delete f;
1169 return;
1170 }
1171 WriteCalibrationTree(f, chans, align);
1172 Constants::Detail() << " wrote calibration into " << events_subpath
1173 << std::endl;
1174 f->Close();
1175 delete f;
1176}
1177
1178// Per-channel calibrated overlay for one subfile, via AttachCalSidecar (the
1179// same path downstream macros use). The sidecar must already be on disk.
1180void SaveDynamicRangeOverlay(const FileSpec &spec, const TString &plot_subdir,
1181 const TString &file_label) {
1182 const Int_t kNStrips = 18;
1183 const Double_t emin = Constants::cfg.STRIP_DE_MIN_NORMED;
1184 const Double_t emax = Constants::cfg.STRIP_DE_MAX_NORMED;
1185 // 0.025 a.u. bins: a few per beam-peak width, so eighteen overlaid curves
1186 // read as curves rather than as noise.
1187 const Int_t nbins = TMath::Max(20, Int_t((emax - emin) / 0.025 + 0.5));
1188 TH1D *h[kNStrips];
1189 for (Int_t s = 0; s < kNStrips; s++) {
1190 h[s] = new TH1D(Form("h_dynrange_%s_S%d", file_label.Data(), s),
1191 ";#DeltaE [a.u.];Counts", nbins, emin, emax);
1192 h[s]->SetDirectory(nullptr);
1193 }
1194 TString sub = FileSet::EventsName(spec) + ".root";
1195 TFile *sf = IO::OpenForReading(sub);
1196 if (!sf || sf->IsZombie()) {
1197 if (sf)
1198 sf->Close();
1199 for (Int_t s = 0; s < kNStrips; s++)
1200 delete h[s];
1201 return;
1202 }
1203 TTree *tree = static_cast<TTree *>(sf->Get("events"));
1204 if (!tree) {
1205 sf->Close();
1206 delete sf;
1207 for (Int_t s = 0; s < kNStrips; s++)
1208 delete h[s];
1209 return;
1210 }
1211 EnergyView ev;
1212 ev.Attach(tree);
1213 if (!ev.is_normed) {
1214 sf->Close();
1215 delete sf;
1216 for (Int_t s = 0; s < kNStrips; s++)
1217 delete h[s];
1218 return;
1219 }
1220 // The grid alone on the same axis, so its beam classes read against the
1221 // strips' range.
1222 TH1D *hg = new TH1D(Form("h_dynrange_%s_grid", file_label.Data()),
1223 ";Grid #DeltaE [a.u.];Counts", nbins, emin, emax);
1224 hg->SetDirectory(nullptr);
1225 Long64_t n = tree->GetEntries();
1226 for (Long64_t j = 0; j < n; j++) {
1227 tree->GetEntry(j);
1228 ev.Decode();
1229 for (Int_t s = 0; s < kNStrips; s++) {
1230 Double_t v = ev.Total(s);
1231 if (v > 0)
1232 h[s]->Fill(v);
1233 }
1234 if (ev.grid > 0.0)
1235 hg->Fill(ev.grid);
1236 }
1237 sf->Close();
1238 delete sf;
1239 std::vector<Int_t> colors = PlottingUtils::GetDefaultColors();
1240 Double_t y_top = 0;
1241 for (Int_t s = 0; s < kNStrips; s++) {
1242 Double_t m = h[s]->GetMaximum();
1243 if (m > y_top)
1244 y_top = m;
1245 }
1246 TCanvas *cv = PlottingUtils::GetConfiguredCanvas(kFALSE);
1247 cv->SetRightMargin(0.20);
1248 Bool_t first = kTRUE;
1249 for (Int_t s = 0; s < kNStrips; s++) {
1250 Int_t color = colors[s % Int_t(colors.size())];
1251 h[s]->SetLineColor(color);
1252 h[s]->SetLineWidth(2);
1253 h[s]->SetMaximum(1.15 * y_top);
1254 h[s]->Draw(first ? "HIST" : "HIST SAME");
1255 first = kFALSE;
1256 }
1257 TLegend *leg = PlottingUtils::AddLegend(0.81, 0.99, 0.10, 0.95);
1258 for (Int_t s = 0; s < kNStrips; s++)
1259 leg->AddEntry(h[s], Form("S%d", s), "l");
1260 leg->Draw();
1262 PlottingUtils::SaveFigure(cv, "dynamic_range_check", plot_subdir,
1263 PlotSaveOptions::kLOG);
1264 delete cv;
1265 delete leg;
1266 TCanvas *cg = PlottingUtils::GetConfiguredCanvas(kTRUE);
1267 PlottingUtils::ConfigureAndDrawHistogram(hg, kBlue + 1);
1269 PlottingUtils::SaveFigure(cg, "grid_spectrum", plot_subdir,
1270 PlotSaveOptions::kLOG);
1271 delete cg;
1272 delete hg;
1273 for (Int_t s = 0; s < kNStrips; s++)
1274 delete h[s];
1275}
1276
1285Double_t SmoothedPeakIn(TH1D *proj, Double_t lo, Double_t hi) {
1286 Int_t b_lo = proj->FindBin(lo);
1287 Int_t b_hi = proj->FindBin(hi);
1288 Int_t b_max = -1;
1289 Double_t val_max = 0;
1290 for (Int_t b = b_lo; b <= b_hi; b++) {
1291 Double_t v = proj->GetBinContent(b);
1292 if (v > val_max) {
1293 val_max = v;
1294 b_max = b;
1295 }
1296 }
1297 if (b_max < 0 || b_max == b_lo || b_max == b_hi)
1298 return 0.0;
1299 Double_t peak = proj->GetBinCenter(b_max);
1300 if (b_max > 1 && b_max < proj->GetNbinsX()) {
1301 const Double_t ym = proj->GetBinContent(b_max - 1);
1302 const Double_t y0 = proj->GetBinContent(b_max);
1303 const Double_t yp = proj->GetBinContent(b_max + 1);
1304 const Double_t denom = ym - 2.0 * y0 + yp;
1305 if (denom < 0.0) {
1306 Double_t shift = 0.5 * (ym - yp) / denom;
1307 shift = TMath::Max(-0.5, TMath::Min(0.5, shift));
1308 peak += shift * proj->GetBinWidth(b_max);
1309 }
1310 }
1311 return peak;
1312}
1313
1342 const TString &plot_subdir,
1343 const TString &file_label) {
1344 const Int_t kNStrips = 18;
1345 const Int_t kNHistBins = 2000; // 0.005 a.u. bins
1346 const Double_t kHistMin = 0.0;
1347 const Double_t kHistMax = 10.0;
1348 const Int_t kSmoothTimes = 5;
1349 const Double_t kBeamLo = 0.6, kBeamHi = 1.5; // beam peak window
1350 const Double_t kPileLoX = 1.7, kPileHiX = 2.3; // 2-particle window, x beam
1351 StripAlignmentResult result;
1352 for (Int_t s = 0; s < kNStrips; s++) {
1353 result.factors[s] = 1.0;
1354 result.offsets[s] = 0.0;
1355 result.centroids[s] = 0.0;
1356 result.pileups[s] = 0.0;
1357 }
1358
1359 TString sub = FileSet::EventsName(spec) + ".root";
1360 TFile *sf = IO::OpenForReading(sub);
1361 if (!sf || sf->IsZombie()) {
1362 if (sf)
1363 sf->Close();
1364 delete sf;
1365 return result;
1366 }
1367 TTree *tree = static_cast<TTree *>(sf->Get("events"));
1368 if (!tree) {
1369 sf->Close();
1370 delete sf;
1371 return result;
1372 }
1373 EnergyView ev;
1374 ev.Attach(tree);
1375 if (!ev.is_normed) {
1376 std::cerr << " " << file_label
1377 << ": calibration tree not found -- skipping alignment"
1378 << std::endl;
1379 sf->Close();
1380 delete sf;
1381 return result;
1382 }
1383
1384 TH2D *h2 = new TH2D(Form("h2_strip_align_%s", file_label.Data()),
1385 ";Strip number;#DeltaE [a.u.]", kNStrips, -0.5,
1386 kNStrips - 0.5, kNHistBins, kHistMin, kHistMax);
1387 h2->SetDirectory(nullptr);
1388
1389 Long64_t n = tree->GetEntries();
1390 Long64_t n_used = 0;
1391 for (Long64_t j = 0; j < n; j++) {
1392 tree->GetEntry(j);
1393 ev.Decode();
1394 Bool_t any = kFALSE;
1395 for (Int_t s = 0; s < kNStrips; s++) {
1396 Double_t v = ev.Total(s);
1397 if (v <= 0)
1398 continue;
1399 h2->Fill(Double_t(s), v);
1400 any = kTRUE;
1401 }
1402 if (any)
1403 n_used++;
1404 }
1405 sf->Close();
1406 delete sf;
1407
1408 Constants::Detail() << " strip alignment: " << n_used << " events decoded"
1409 << std::endl;
1410
1411 Bool_t beam_ok[kNStrips] = {kFALSE};
1412 Bool_t pile_ok[kNStrips] = {kFALSE};
1413 Int_t valid_strips = 0;
1414
1415 for (Int_t s = 0; s < kNStrips; s++) {
1416 Int_t bin_ix = s + 1;
1417 TH1D *proj = h2->ProjectionY(
1418 Form("hproj_align_%s_s%d", file_label.Data(), s), bin_ix, bin_ix);
1419 proj->SetDirectory(nullptr);
1420 Long64_t n_entries = Long64_t(proj->GetEntries());
1421 if (n_entries < kMinSamples) {
1423 << " strip " << s << ": too few entries for alignment (" << n_entries
1424 << ")" << std::endl;
1425 delete proj;
1426 continue;
1427 }
1428 proj->Smooth(kSmoothTimes);
1429
1430 Double_t beam = SmoothedPeakIn(proj, kBeamLo, kBeamHi);
1431 if (beam <= 0) {
1433 << " strip " << s << ": no beam peak in [" << kBeamLo << ", "
1434 << kBeamHi << "] a.u.; left unaligned" << std::endl;
1435 delete proj;
1436 continue;
1437 }
1438 // The 2-particle peak sits near twice the beam; a peak on the window
1439 // edge is rejected by SmoothedPeakIn.
1440 Double_t pile = SmoothedPeakIn(proj, kPileLoX * beam, kPileHiX * beam);
1441 delete proj;
1442
1443 result.centroids[s] = beam;
1444 beam_ok[s] = kTRUE;
1445 if (pile > 0) {
1446 result.pileups[s] = pile;
1447 pile_ok[s] = kTRUE;
1448 result.factors[s] = 1.0 / (pile - beam);
1449 result.offsets[s] = 1.0 - result.factors[s] * beam;
1450 } else {
1451 result.factors[s] = 1.0 / beam;
1452 result.offsets[s] = 0.0;
1453 }
1454 valid_strips++;
1455 Constants::Detail() << " strip " << s << " beam=" << Form("%.4f", beam)
1456 << " pileup="
1457 << (pile > 0 ? Form("%.4f", pile)
1458 : "none (factor only)")
1459 << " a.u. factor=" << Form("%.4f", result.factors[s])
1460 << " offset=" << Form("%+.4f", result.offsets[s])
1461 << " (n=" << n_entries << ")" << std::endl;
1462 }
1463 result.ok = (valid_strips >= 4) ? kTRUE : kFALSE;
1464
1465 // Diagnostic plot: the pre-alignment totals with the two peaks each strip
1466 // was aligned on (beam orange, pile-up violet).
1467 {
1468 TCanvas *cv = PlottingUtils::GetConfiguredCanvas(kFALSE);
1469 PlottingUtils::ConfigureAndDraw2DHistogram(h2, cv);
1470 TGraph *g_beam_plot = new TGraph(kNStrips);
1471 TGraph *g_pile_plot = new TGraph(kNStrips);
1472 Int_t nb = 0, npk = 0;
1473 for (Int_t s = 0; s < kNStrips; s++) {
1474 if (beam_ok[s])
1475 g_beam_plot->SetPoint(nb++, Double_t(s), result.centroids[s]);
1476 if (pile_ok[s])
1477 g_pile_plot->SetPoint(npk++, Double_t(s), result.pileups[s]);
1478 }
1479 g_beam_plot->Set(nb);
1480 g_pile_plot->Set(npk);
1481 if (nb > 0) {
1482 g_beam_plot->SetMarkerStyle(20);
1483 g_beam_plot->SetMarkerColor(kOrange);
1484 g_beam_plot->Draw("P SAME");
1485 }
1486 if (npk > 0) {
1487 g_pile_plot->SetMarkerStyle(21);
1488 g_pile_plot->SetMarkerColor(kViolet + 2);
1489 g_pile_plot->Draw("P SAME");
1490 }
1492 PlottingUtils::SaveFigure(cv, "strip_alignment_check", plot_subdir,
1493 PlotSaveOptions::kLINEAR);
1494 delete cv;
1495 delete g_beam_plot;
1496 delete g_pile_plot;
1497 }
1498
1499 delete h2;
1500
1501 return result;
1502}
1503
1505 const FileSpec &spec, const std::vector<ChannelCal> &chans_template) {
1506 TString file_label = FileSet::FileLabel(spec);
1507 TString plot_subdir = "beam_calibration/" + file_label;
1508 Constants::Detail() << "Beam calibration: " << file_label << std::endl;
1509
1510 // One gate per strip: strip total vs its neighbour's. A strip whose gate
1511 // cannot be fitted is excluded from calibration, not the whole subfile.
1512 BeamFit2D gate[18];
1513 Int_t n_gates = 0;
1514 {
1515 std::lock_guard<std::mutex> lock(g_plot_mutex);
1516 for (Int_t s = 1; s <= 17; s++) {
1517 gate[s] =
1518 FindBeamGateStrips(spec, GatePartner(s), s, file_label, plot_subdir);
1519 if (gate[s].ok)
1520 n_gates++;
1521 else
1523 << " " << file_label << ": strip " << s
1524 << " beam gate failed; that strip is not calibrated here"
1525 << std::endl;
1526 }
1527 // Strip 0 shares strip 1's gate: there is no strip before it, so the
1528 // (0, 1) pair is the only one available to either.
1529 gate[0] = gate[1];
1530 }
1531 if (n_gates == 0) {
1532 std::cerr << " " << file_label << ": every per-strip beam gate failed"
1533 << std::endl;
1534 return;
1535 }
1536
1537 std::vector<ChannelCal> chans = chans_template;
1538 std::vector<std::vector<Float_t>> samples;
1539 StripPairSamples pairs[18];
1540 CollectAnchorSamplesOneSubfile(spec, chans, gate, samples, pairs);
1541 {
1542 std::lock_guard<std::mutex> lock(g_plot_mutex);
1543 ReduceToAnchors(chans, samples, pairs, plot_subdir);
1544 }
1545
1546 // Uncalibrated channels silently get gain 0 (drop out of strip totals), so
1547 // log which/why; grep "[uncalibrated long]" — long side should never starve.
1548 for (Int_t i = 0; i < Int_t(chans.size()); i++) {
1549 const ChannelCal &c = chans[i];
1550 if (IsCalibrated(c))
1551 continue;
1552 TString kind;
1553 if (c.side == 'C')
1554 kind = "cathode";
1555 else if (c.side == 'G')
1556 kind = "grid";
1557 else if (c.side == 'S')
1558 kind = "unsegmented";
1559 else
1560 kind = (c.side == LongSide(c.strip)) ? "long" : "short";
1561 TString why;
1562 if (c.n_samples < kMinSamples)
1563 why =
1564 Form("too few beam samples (%lld < %lld)", c.n_samples, kMinSamples);
1565 else
1566 why = Form("bad exp anchor (fit_adc=%.1f)", c.fit_adc);
1567 Constants::DetailErr() << " [uncalibrated " << kind << "] " << c.name
1568 << " -> gain 0; " << why
1569 << " (beam n=" << c.n_samples << ")" << std::endl;
1570 }
1571
1572 for (Int_t i = 0; i < Int_t(chans.size()); i++)
1573 if (IsCalibrated(chans[i]))
1574 Constants::Detail() << " " << chans[i].name << " gain=" << Gain(chans[i])
1575 << " a.u./ADC" << std::endl;
1576
1578
1579 // Write initial calibration tree: per-channel gains. The alignment step
1580 // needs this on disk so EnergyView can decode events in a.u.
1581 WriteCalibrationToEvents(spec, chans, &align);
1582
1583 // Per-strip two-point alignment on the strip total: beam to 1.0, pile-up
1584 // to 2.0 (factor on every end, offset on the long end).
1585 {
1586 std::lock_guard<std::mutex> lock(g_plot_mutex);
1587 align = FindStripCentroidAlignment(spec, plot_subdir, file_label);
1588 }
1589 if (align.ok) {
1590 WriteCalibrationToEvents(spec, chans, &align);
1591 }
1592
1593 {
1594 std::lock_guard<std::mutex> lock(g_plot_mutex);
1595 SaveDynamicRangeOverlay(spec, plot_subdir, file_label);
1596 }
1597
1598 // One line per subfile whatever the sample: what was measured and what fell
1599 // back. The per-strip and per-channel lines above are detail.
1600 Int_t n_cal = 0, n_uncal = 0, n_ridge = 0, n_aligned = 0;
1601 for (Int_t i = 0; i < Int_t(chans.size()); i++) {
1602 if (IsCalibrated(chans[i]))
1603 n_cal++;
1604 else
1605 n_uncal++;
1606 if (chans[i].ridge_ratio > 0.0)
1607 n_ridge++;
1608 }
1609 for (Int_t s = 1; s <= 16; s++)
1610 if (align.ok && align.factors[s] != 1.0)
1611 n_aligned++;
1612 // One string, one write, so concurrent workers' lines do not interleave.
1613 const TString line =
1614 Form("[calibration] %s: gates %d/17, channels %d calibrated (%d not), "
1615 "ridge measured on %d/16 strips, alignment on %d/16",
1616 file_label.Data(), n_gates, n_cal, n_uncal, n_ridge, n_aligned);
1617 std::cout << line << std::endl;
1618}
1619
1638 Int_t run, const std::vector<FileSpec> &specs) {
1639 std::vector<Double_t> per_strip[18], per_strip_off[18];
1640 for (Int_t k = 0; k < Int_t(specs.size()); k++) {
1641 TString sub = FileSet::EventsName(specs[k]) + ".root";
1642 TFile *cf = IO::OpenForReading(sub);
1643 if (!cf || cf->IsZombie()) {
1644 if (cf)
1645 delete cf;
1646 continue;
1647 }
1648 TTree *t = static_cast<TTree *>(cf->Get("calibration"));
1649 if (!t || t->GetEntries() < 1 || !t->GetBranch("RidgeRatio") ||
1650 !t->GetBranch("RidgeOffset")) {
1651 cf->Close();
1652 delete cf;
1653 continue;
1654 }
1655 Float_t rr[18] = {0}, ro[18] = {0};
1656 t->SetBranchAddress("RidgeRatio", rr);
1657 t->SetBranchAddress("RidgeOffset", ro);
1658 t->GetEntry(0);
1659 for (Int_t s = 1; s <= 16; s++)
1660 if (rr[s] > 0) {
1661 per_strip[s].push_back(Double_t(rr[s]));
1662 per_strip_off[s].push_back(Double_t(ro[s]));
1663 }
1664 cf->Close();
1665 delete cf;
1666 }
1667
1668 // The ratio and the offset (both in units of C_long) travel together: a
1669 // subfile that measured the ridge measured both.
1670 Double_t med[18] = {0}, med_off[18] = {0};
1671 std::cout << "Run " << run << " ridge-ratio aggregation:" << std::endl;
1672 for (Int_t s = 1; s <= 16; s++) {
1673 std::vector<Double_t> &v = per_strip[s];
1674 std::vector<Double_t> &vo = per_strip_off[s];
1675 if (v.size() < 3) {
1676 std::cerr << " strip " << s << ": only " << v.size()
1677 << " subfiles measured the ridge; leaving per-subfile gains"
1678 << std::endl;
1679 continue;
1680 }
1681 std::sort(v.begin(), v.end());
1682 std::sort(vo.begin(), vo.end());
1683 Int_t m = Int_t(v.size());
1684 // The IQR reads the sorted vectors, so it comes before the medians,
1685 // which only partially order them.
1686 Double_t lo = v[m / 4], hi = v[(3 * m) / 4];
1687 Double_t lo_off = vo[m / 4], hi_off = vo[(3 * m) / 4];
1688 med[s] = Median(v);
1689 med_off[s] = Median(vo);
1690 std::cout << " strip " << s << " ratio=" << Form("%.4f", med[s])
1691 << " IQR " << Form("%.4f", lo) << "-" << Form("%.4f", hi)
1692 << " offset=" << Form("%.4f", med_off[s]) << " x C_long IQR "
1693 << Form("%.4f", lo_off) << "-" << Form("%.4f", hi_off)
1694 << " from " << m << " subfiles" << std::endl;
1695 }
1696
1697 Int_t n_rewritten = 0;
1698 for (Int_t k = 0; k < Int_t(specs.size()); k++) {
1699 TString sub = FileSet::EventsName(specs[k]) + ".root";
1700 TFile *cf = IO::OpenForWriting(sub, "UPDATE");
1701 if (!cf || cf->IsZombie()) {
1702 if (cf)
1703 delete cf;
1704 continue;
1705 }
1706 TTree *t = static_cast<TTree *>(cf->Get("calibration"));
1707 if (!t || t->GetEntries() < 1 || !t->GetBranch("LongAnchor") ||
1708 !t->GetBranch("OffsetLeft")) {
1709 cf->Close();
1710 delete cf;
1711 continue;
1712 }
1713 Float_t gl[16] = {0}, gr[16] = {0}, ol[16] = {0}, orr[16] = {0},
1714 la[18] = {0}, rr[18] = {0}, ro[18] = {0};
1715 t->SetBranchAddress("GainLeft", gl);
1716 t->SetBranchAddress("GainRight", gr);
1717 t->SetBranchAddress("OffsetLeft", ol);
1718 t->SetBranchAddress("OffsetRight", orr);
1719 t->SetBranchAddress("LongAnchor", la);
1720 t->SetBranchAddress("RidgeRatio", rr);
1721 t->SetBranchAddress("RidgeOffset", ro);
1722 t->GetEntry(0);
1723 Bool_t changed = kFALSE;
1724 for (Int_t s = 1; s <= 16; s++) {
1725 if (med[s] <= 0 || la[s] <= 0)
1726 continue;
1727 // Short anchor and offset from the run medians and this subfile's own
1728 // long anchor, so per-subfile gain drift is preserved.
1729 Double_t anchor = med[s] * Double_t(la[s]);
1730 if (anchor <= 0)
1731 continue;
1732 const Float_t g = Float_t(1.0 / anchor);
1733 const Float_t off = Float_t(med_off[s] * Double_t(la[s]));
1734 if (LongSide(s) == 'L') {
1735 gr[s - 1] = g;
1736 orr[s - 1] = off;
1737 } else {
1738 gl[s - 1] = g;
1739 ol[s - 1] = off;
1740 }
1741 rr[s] = Float_t(med[s]);
1742 ro[s] = Float_t(med_off[s]);
1743 changed = kTRUE;
1744 }
1745 if (changed) {
1746 TTree *nt = t->CloneTree(0);
1747 nt->Fill();
1748 cf->cd();
1749 nt->Write("calibration", TObject::kOverwrite);
1750 n_rewritten++;
1751 }
1752 cf->Close();
1753 delete cf;
1754 }
1755 std::cout << " rewrote short gains and offsets in " << n_rewritten
1756 << " subfiles" << std::endl;
1757}
1758
1759void CalibrateBeam::Run(const TString &file_label) {
1760 const TString project_root = Paths::DatasetDir();
1761 InitUtils::SetROOTPreferences(PlotSaveFormat::kPNG,
1762 Paths::ResultsDir() + "/plots",
1763 Paths::ResultsDir() + "/root_files");
1764 gROOT->SetBatch(kTRUE);
1765
1766 std::vector<FileSpec> specs;
1767 if (file_label.IsNull()) {
1769 if (specs.empty()) {
1770 std::cerr << "No file specs from FileSet::BuildProcessedFileSpecs()"
1771 << std::endl;
1772 return;
1773 }
1774 } else {
1775 FileSpec s = FileSet::ResolveFileSpec(file_label);
1776 if (s.run < 0) {
1777 std::cerr << "Could not resolve file label '" << file_label << "'"
1778 << std::endl;
1779 return;
1780 }
1781 specs.push_back(s);
1782 }
1783
1784 std::vector<ChannelCal> chans = CalibrateBeam::BuildChannels();
1785
1786 Int_t n_specs = Int_t(specs.size());
1787
1788 std::set<Int_t> runs;
1789 for (Int_t k = 0; k < n_specs; k++)
1790 runs.insert(specs[k].run);
1791
1792 Int_t n_workers =
1793 TMath::Min(Int_t(std::thread::hardware_concurrency()), n_specs);
1794 n_workers = TMath::Min(n_workers, Constants::cfg.MAX_FUSED_WORKERS);
1795 if (n_workers < 1)
1796 n_workers = 1;
1797 std::cout << "calibrate-beam: " << n_specs << " subfiles on " << n_workers
1798 << " workers" << std::endl;
1799
1800 std::queue<Int_t> work;
1801 for (Int_t k = 0; k < n_specs; k++)
1802 work.push(k);
1803 std::mutex work_mutex;
1804
1805 std::vector<std::thread> workers;
1806 for (Int_t w = 0; w < n_workers; w++) {
1807 workers.emplace_back([&]() {
1808 while (true) {
1809 Int_t k;
1810 {
1811 std::lock_guard<std::mutex> lk(work_mutex);
1812 if (work.empty())
1813 return;
1814 k = work.front();
1815 work.pop();
1816 }
1817 // The first files of the epoch draw (a single named file always).
1819 CalibrateBeamOneSubfile(specs[k], chans);
1820 }
1821 });
1822 }
1823 for (Int_t w = 0; w < Int_t(workers.size()); w++)
1824 workers[w].join();
1825}
void ComputeLRGainMatch(std::vector< ChannelCal > &chans, const StripPairSamples pairs[18], const TString &plot_subdir)
const Long64_t kRidgeMinPerSlice
Double_t StripTotalAdc(const UShort_t *l, const UShort_t *r, UShort_t s0, UShort_t s17, Int_t s)
Double_t SmoothedPeakIn(TH1D *proj, Double_t lo, Double_t hi)
Peak of a smoothed projection in [lo, hi]: the highest bin of the window, refined to sub-bin precisio...
const Int_t kGmBins
L/R gain matching, after the check_LR notebook's recipe (37Cl_an_check_LR.ipynb, "Save 2-pass calibra...
const Double_t kRidgeBandHi
void SaveDynamicRangeOverlay(const FileSpec &spec, const TString &plot_subdir, const TString &file_label)
void RobustPeakSeed(const std::vector< Float_t > &v, Double_t &mode, Double_t &sigma)
Robust peak/width estimate used to seed the beam-peak Gaussian fit, and as the fallback anchor when t...
const Double_t kRidgeRatioLo
const Int_t kMaxChannels
BeamFit2D FindBeamGateStrips(const FileSpec &spec, Int_t sx, Int_t sy, const TString &run_label, const TString &plot_subdir, Bool_t save_plot=kTRUE)
The beam gate for one strip: a 2D Gaussian on the (strip sx, strip sy) raw totals.
Bool_t TheilSenLine(const std::vector< Double_t > &x, const std::vector< Double_t > &y, Double_t &slope, Double_t &intercept)
Theil-Sen robust line: slope = median of pairwise slopes, intercept = median residual.
const Double_t kRidgeBandLo
Double_t RidgeShortAnchor(const std::vector< Float_t > &v_short, const std::vector< Float_t > &v_long, Double_t c_long, Double_t &slope_out, Double_t &intercept_out, RidgeFit &dbg)
Robust line through the single-particle charge-sharing ridge, returning the fitted line's short-axis ...
const Long64_t kMinSamples
Double_t GmFindPeak(const std::vector< Float_t > &v, Double_t lo, Double_t hi, Double_t skip_frac)
Histogram-mode peak finder, mirroring the notebook's find_peak(): histogram v over [lo,...
Char_t LongSide(Int_t strip)
const Long64_t kSampleCap
void ReduceToAnchors(std::vector< ChannelCal > &chans, std::vector< std::vector< Float_t > > &samples, const StripPairSamples pairs[18], const TString &plot_subdir)
Cathode uses median + IQR/1.349 (asymmetric tail, no clean peak).
void WriteCalibrationTree(TFile *dst, const std::vector< ChannelCal > &chans, const StripAlignmentResult *align)
Writes a one-row calibration tree into the open file dst (typically a per-subfile ....
Int_t GatePartner(Int_t s)
The strip whose total is paired with strip s to gate it: the one before it, except strips 0 and 1 whi...
void SaveRidgeFitPlots(const StripPairSamples pairs[18], const RidgeFit dbg[18], const TString &plot_subdir)
One plot per strip under <plot_subdir>/ridge, named ridge_s<NN>: the beam-gated long-vs-short plane,...
const Double_t kGmEsumLo
void CollectAnchorSamplesOneSubfile(const FileSpec &spec, const std::vector< ChannelCal > &chans, const BeamFit2D gate[18], std::vector< std::vector< Float_t > > &samples, StripPairSamples pairs[18])
Every strip carries its own beam gate, on its own total and its neighbour's (GatePartner).
Double_t Median(std::vector< T > &v)
StripAlignmentResult FindStripCentroidAlignment(const FileSpec &spec, const TString &plot_subdir, const TString &file_label)
Per-strip two-point alignment of the strip total.
const Double_t kRidgeShortMaxFrac
const Int_t kRidgeSlices
const Double_t kRidgeShortMinFrac
Ridge-slope short anchor.
Bool_t IsCalibrated(const ChannelCal &c)
const Int_t kRidgeMinPts
Double_t Gain(const ChannelCal &c)
const Double_t kRidgeRatioHi
const Long64_t kRidgeNoiseFloor
void WriteCalibrationToEvents(const FileSpec &spec, const std::vector< ChannelCal > &chans, const StripAlignmentResult *align)
Writes the per-channel gain table (tree "calibration") into the subfile's own events file.
const Double_t kGmEsumHi
const Int_t kRidgeMinSlices
const Long64_t kPairCap
(L, R) pair cap for the L/R gain-matching passes.
std::mutex g_plot_mutex
Serialises all plotting and canvas work.
Definition FileSet.cpp:3
static SpotFit FitSpotFromPoints(TH2F *h, std::vector< std::pair< Float_t, Float_t > > &pts, const Moments2D &seed)
A beam spot from a coarse histogram and the same events unbinned: the sample's sigma-clipped moments ...
static void DrawEllipse(const BeamFit2D &b, Double_t n, Color_t color)
Draw the n-sigma contour of a fitted spot on the current pad.
Definition BeamFit2D.cpp:24
static Bool_t InEllipseXY(const BeamFit2D &b, Double_t x, Double_t y, Double_t n)
Whether a point lies inside the fitted ellipse at n sigma: the correlation-aware Mahalanobis chi2 bel...
Definition BeamFit2D.cpp:9
static Moments2D ComputeMoments(TH2F *h, Int_t lo_bx, Int_t hi_bx, Int_t lo_by, Int_t hi_by, Double_t thresh, Double_t bw_x, Double_t bw_y)
Second moments of a histogram region above a threshold.
Definition BeamFit2D.cpp:42
static Moments2D SeedSpotMoments(TH2F *h, Int_t seed_half_bins)
Second moments of a coarse spot histogram's seed window: the maximum bin, ± seed_half_bins around it,...
static void Run(const TString &file_label="")
Calibrate every configured subfile, then aggregate per run.
static void CalibrateBeamOneSubfile(const FileSpec &spec, const std::vector< ChannelCal > &chans_template)
Calibrate one subfile end to end.
static std::vector< ChannelCal > BuildChannels()
One ChannelCal per readout channel in the active channel map.
static void AggregateRidgeRatiosForRun(Int_t run, const std::vector< FileSpec > &specs)
Replace each subfile's ridge ratio and short-end offset with the run-level medians.
static TString EventsName(const FileSpec &s)
Filename of the built-events ROOT file for a subfile.
Definition FileSet.cpp:292
static TString FileLabel(const FileSpec &s)
Human-readable label identifying a subfile.
Definition FileSet.cpp:299
static FileSpec ResolveFileSpec(const TString &file_label)
Recover the FileSpec behind a label from FileLabel().
Definition FileSet.cpp:342
static std::vector< FileSpec > BuildProcessedFileSpecs()
Every subfile that already has processed output.
Definition FileSet.cpp:270
static TString DatasetDir()
Absolute path to the active dataset directory, analysis/<iso>.
Definition Paths.cpp:63
static TString ResultsDir()
Absolute path to the directory receiving generated output.
Definition Paths.cpp:24
Double_t ActiveStripEMaxAdc()
Upper bound of the per-strip energy range, in ADC.
Bool_t SavePlots()
FileInSample(), by the name the plot call sites use.
Bool_t InPlotSample(Int_t k)
Whether the k-th file (0-based, in file order) of an epoch's list is in the plot sample.
std::ostream & Detail()
std::cout for a file in the sample, a stream that discards otherwise: the per-file detail lines (per ...
const DatasetConfig & cfg
The active dataset's configuration, flat block.
void SetPlotsThisFile(Bool_t on)
Mark the file this thread is about to process as in (or out of) the plot sample.
std::ostream & DetailErr()
A fitted 2-D Gaussian beam spot.
Definition BeamFit2D.hpp:19
Double_t rho
Correlation coefficient, in [-1, 1].
Definition BeamFit2D.hpp:23
Double_t sigma_y
Widths along each axis.
Definition BeamFit2D.hpp:22
Double_t mu_x
Definition BeamFit2D.hpp:21
Double_t mu_y
Centroid.
Definition BeamFit2D.hpp:21
Double_t sigma_x
Definition BeamFit2D.hpp:22
Result of a beam spot fit: the spot to use, and whether the Gaussian fit (rather than the clipped-mom...
Double_t chi2_ndf
The fit's reduced chi2; -1 when it did not run.
Bool_t fit_used
kTRUE when fit is the fit, not the fallback.
BeamFit2D fit
The spot to use; ok when usable.
One readout channel's beam-peak calibration.
Double_t ridge_offset
Short channels only: offset_adc in units of C_long, from this subfile's own ridge fit,...
Double_t offset_adc
ADC subtracted from this channel before its gain, only when the channel fired (a reading of 0 stays 0...
Char_t side
Readout end: 'L' or 'R' for the split strips 1-16, 'S' for the unsplit end strips 0 and 17,...
TString name
Channel name from the active channel map.
Double_t gain
Left/right gain-match override.
Double_t fit_adc
Modal beam peak, in ADC: the modal bin of the channel's beam-peak histogram (100 bins over the 5th-95...
Int_t strip
Anode strip index this channel reads; -1 for the cathode.
Long64_t n_samples
Events the anchor was taken from.
Double_t ridge_ratio
Short channels only: C_short / C_long = -1 / slope from this subfile's own ridge fit,...
A view over one event's energies, decoding raw ADC into calibrated units.
void Decode()
Decode the currently loaded entry into the value members.
Bool_t Attach(TTree *t)
Bind to an events tree and set up the branch addresses.
Double_t Total(Int_t strip) const
A strip's deposit: the sum of its ends, or the unsegmented value.
Bool_t is_normed
Whether a calibration tree was found.
Double_t grid
Grid energy.
One input file: a run number and the subfile suffix within it.
Definition FileSet.hpp:40
Int_t run
Run number.
Definition FileSet.hpp:41
Bool_t ridge_ok[18]
std::vector< Double_t > offsets_found[2]
Double_t short_anchor_adc[18]
Double_t median_ratio[2]
Double_t long_anchor_adc[18]
Double_t median_offset[2]
std::vector< Double_t > ratios_found[2]
Double_t short_offset_adc[18]
Second moments of a 2-D distribution over a bin range.
Definition BeamFit2D.hpp:33
Double_t sigma_y
Weighted RMS widths.
Definition BeamFit2D.hpp:35
Double_t sigma_x
Definition BeamFit2D.hpp:35
Double_t mu_x
Definition BeamFit2D.hpp:34
Double_t mu_y
Weighted centroid.
Definition BeamFit2D.hpp:34
Double_t weight
Total weight included; zero means the range held nothing above threshold.
Definition BeamFit2D.hpp:37
Everything SaveRidgeFitPlots needs to redraw a fit: the slice medians it was fitted to,...
Double_t slope
Long64_t n_gated
Double_t hi
Double_t lo
std::vector< Double_t > y
Long64_t min_per_slice
Double_t c_short
const char * fail
std::vector< Double_t > ey
Double_t intercept
Long64_t n_long_band
Double_t c_long
Long64_t n_short_window
std::vector< Double_t > x
Output of the strip centroid alignment, the post-gain calibration step.
Double_t centroids[18]
Measured beam peak per strip before alignment, in calibrated units.
Double_t pileups[18]
Measured two-particle pile-up peak per strip before alignment, in calibrated units; 0 where none was ...
Double_t factors[18]
Per-strip multiplicative alignment factors, applied to every end.
Double_t offsets[18]
Per-strip additive offsets in calibrated units, applied after the factor to the strip total; EnergyVi...
Bool_t ok
Whether the step produced usable results.
Paired (L, R) raw-ADC samples for one strip, collected UNGATED from events where both ends fire,...
std::vector< Float_t > gated_long
std::vector< Float_t > gated_short
std::vector< Float_t > r
std::vector< Float_t > l