MUSIC unknown
Analysis for the MUSIC active-target ionization chamber
Loading...
Searching...
No Matches
PulseHistory.cpp
Go to the documentation of this file.
1#include "PulseHistory.hpp"
2#include "Constants.hpp"
3#include "FileSet.hpp"
4#include "IOUtils.hpp"
5#include "PlottingUtils.hpp"
6#include <TCanvas.h>
7#include <TDecompSVD.h>
8#include <TFile.h>
9#include <TGraph.h>
10#include <TH1D.h>
11#include <TH2D.h>
12#include <TLegend.h>
13#include <TLine.h>
14#include <TMath.h>
15#include <TMatrixD.h>
16#include <TProfile.h>
17#include <TTree.h>
18#include <TVectorD.h>
19#include <algorithm>
20#include <cmath>
21#include <deque>
22#include <functional>
23#include <iostream>
24
25namespace PulseHistory {
26
27const char *GroupName(Int_t g) {
28 switch (g) {
29 case kLongLeft:
30 return "long L (odd strips)";
31 case kLongRight:
32 return "long R (even strips)";
33 case kShortLeft:
34 return "short L (even strips)";
35 case kShortRight:
36 return "short R (odd strips)";
37 case kGuard0:
38 return "guard strip 0";
39 case kGuard17:
40 return "guard strip 17";
41 }
42 return "none";
43}
44
45const char *GroupTag(Int_t g) {
46 switch (g) {
47 case kLongLeft:
48 return "L";
49 case kLongRight:
50 return "R";
51 case kShortLeft:
52 return "Ls";
53 case kShortRight:
54 return "Rs";
55 case kGuard0:
56 return "S0";
57 case kGuard17:
58 return "S17";
59 }
60 return "none";
61}
62
63Int_t ChainOf(Int_t g) {
64 switch (g) {
65 case kLongLeft:
66 case kShortLeft:
67 return 0;
68 case kLongRight:
69 case kShortRight:
70 return 1;
71 }
72 return -1;
73}
74
75Bool_t IsLongGroup(Int_t g) { return g == kLongLeft || g == kLongRight; }
76
78 for (Int_t a = 0; a < kMaxAmpBins; a++)
79 for (Int_t b = 0; b < kNBins; b++)
80 k[a][b] = 0.0;
81}
82
84 for (Int_t g = 0; g < kNGroups; g++) {
85 mean_shift[g] = 0.0;
86 n_clamped_group[g] = 0;
87 dev_vs_pred[g] = nullptr;
88 dev_before[g] = dev_after[g] = shift[g] = nullptr;
89 dtprev_before[g] = dtprev_after[g] = nullptr;
90 }
91}
92
94 for (Int_t g = 0; g < kNGroups; g++) {
95 delete dev_vs_pred[g];
96 delete dev_before[g];
97 delete dev_after[g];
98 delete shift[g];
99 delete dtprev_before[g];
100 delete dtprev_after[g];
101 }
102}
103
104Int_t BinOf(Double_t dt_s) {
105 if (!(dt_s > 0.0))
106 return -1;
107 const Double_t l = TMath::Log10(dt_s);
108 if (l < kLogLo || l >= kLogHi)
109 return -1;
110 return Int_t((l - kLogLo) / (kLogHi - kLogLo) * kNBins);
111}
112
113Double_t BinCentreUs(Int_t b) {
114 return TMath::Power(10.0, kLogLo + (b + 0.5) * (kLogHi - kLogLo) / kNBins) *
115 1.0e6;
116}
117
118Int_t AmpBinOf(Double_t e_prev, Double_t mode, Int_t n_amp) {
119 if (n_amp <= 1 || !(mode > 0.0))
120 return 0;
121 // Bands centred on multiples of the beam pulse: [0, 0.5), [0.5, 1.5), ...
122 const Int_t a = Int_t(e_prev / mode + 0.5);
123 return TMath::Min(a, n_amp - 1);
124}
125
126std::vector<Int_t> BuildGroupMap() {
127 const Int_t nch = Constants::ActiveNChannels();
128 std::vector<Int_t> gm(Constants::ActiveNBoards() * nch, kNone);
129 const std::map<std::pair<Int_t, Int_t>, TString> &cm =
131 for (std::map<std::pair<Int_t, Int_t>, TString>::const_iterator it =
132 cm.begin();
133 it != cm.end(); ++it) {
134 const TString &name = it->second;
135 const Int_t idx = it->first.first * nch + it->first.second;
136 if (idx < 0 || idx >= Int_t(gm.size()))
137 continue;
138 if (name == "Strip0") {
139 gm[idx] = kGuard0;
140 continue;
141 }
142 if (name == "Strip17") {
143 gm[idx] = kGuard17;
144 continue;
145 }
146 if (name.Length() < 2 || (name[0] != 'L' && name[0] != 'R'))
147 continue;
148 TString num = name(1, name.Length() - 1);
149 if (!num.IsDigit())
150 continue;
151 const Int_t s = num.Atoi();
152 if (s < 1 || s > 16)
153 continue;
154 const Bool_t odd = (s % 2) != 0;
155 const Bool_t is_long = (name[0] == 'L') ? odd : !odd;
156 if (name[0] == 'L')
157 gm[idx] = is_long ? kLongLeft : kShortLeft;
158 else
159 gm[idx] = is_long ? kLongRight : kShortRight;
160 }
161 return gm;
162}
163
164namespace {
165
166Int_t IndexOfName(const TString &want) {
167 const Int_t nch = Constants::ActiveNChannels();
168 const std::map<std::pair<Int_t, Int_t>, TString> &cm =
170 for (std::map<std::pair<Int_t, Int_t>, TString>::const_iterator it =
171 cm.begin();
172 it != cm.end(); ++it)
173 if (it->second == want)
174 return it->first.first * nch + it->first.second;
175 return -1;
176}
177
178inline Int_t HitIndex(const RawHit &h) {
179 return Int_t(h.board) * Constants::ActiveNChannels() + Int_t(h.channel);
180}
181
182struct Past {
183 Double_t t; // ps
184 Double_t e; // raw ADC
185};
186
187// Feature index of (amplitude band a, dt bin b); 0 is the intercept.
188inline Int_t Feat(Int_t a, Int_t b) { return 1 + a * kNBins + b; }
189
190// Predicted shift from a channel's past, with the group's kernel.
191Double_t Shift(const std::deque<Past> &d, Double_t t, const Kernel &K,
192 Double_t mode) {
193 Double_t s = 0.0;
194 for (const Past &p : d) {
195 const Int_t b = BinOf((t - p.t) * 1.0e-12);
196 if (b < 0)
197 continue;
198 s += K.k[AmpBinOf(p.e, mode, K.n_amp)][b] * p.e;
199 }
200 return s;
201}
202
203} // namespace
204
205Bool_t Measure(std::vector<RawHit> &hits, const std::vector<Int_t> &group_of,
206 Result &res, const TString &file_label) {
207 res.n_hits = Long64_t(hits.size());
208 if (hits.empty())
209 return kFALSE;
210 Bool_t sorted = kTRUE;
211 for (size_t j = 1; j < hits.size() && sorted; j++)
212 sorted = hits[j].timestamp >= hits[j - 1].timestamp;
213 res.sorted_input = sorted;
214 if (!sorted)
215 std::stable_sort(hits.begin(), hits.end(),
216 [](const RawHit &a, const RawHit &b) {
217 return a.timestamp < b.timestamp;
218 });
219
220 const Int_t ref = IndexOfName(Constants::ActiveReferenceChannel());
221 // The entrance guard has to have fired too, or the event is not a beam
222 // particle through the whole chamber (same requirement as the beam-only
223 // selection downstream).
224 const Int_t guard0 = IndexOfName("Strip0");
225 if (ref < 0) {
226 std::cerr << " " << file_label
227 << ": pulse history: no reference channel in the map"
228 << std::endl;
229 return kFALSE;
230 }
231 const Double_t ref_lo = Constants::ActiveReferenceChannelMinAdc();
232 const Double_t ref_hi = Constants::ActiveReferenceChannelMaxAdc();
233 const Double_t window_ps = Constants::ActiveEventTimeWindowUs() * 1.0e6;
234 const Int_t nidx = Int_t(group_of.size());
235 const Int_t n_amp = TMath::Max(
236 1, TMath::Min(kMaxAmpBins, Constants::cfg.PULSE_HISTORY_AMP_BINS));
237 // Every channel with a kernel, and the long ends alone, which are what the
238 // beam-like selection is made of.
239 std::vector<Int_t> fit_ch, long_ch;
240 for (Int_t i = 0; i < nidx; i++) {
241 if (group_of[i] == kNone)
242 continue;
243 fit_ch.push_back(i);
244 if (IsLongGroup(group_of[i]))
245 long_ch.push_back(i);
246 }
247 if (long_ch.empty())
248 return kFALSE;
249
250 // Beam peak per channel: the mode of its raw spectrum above the noise pile.
251 // Beam-like means every long end inside [lo, hi] x its mode. (For a short
252 // end most of the spectrum sits under that pile, so its mode is only a
253 // rough scale, used for the amplitude bands and nothing else.)
254 const Double_t emax = Constants::ActiveStripEMaxAdc();
255 const Int_t nb = 128;
256 std::vector<std::vector<Long64_t>> spec(nidx, std::vector<Long64_t>(nb, 0));
257 for (const RawHit &h : hits) {
258 const Int_t i = HitIndex(h);
259 if (i < 0 || i >= nidx || group_of[i] == kNone)
260 continue;
261 const Int_t b = Int_t(Double_t(h.energy) / emax * nb);
262 if (b >= 0 && b < nb)
263 spec[i][b]++;
264 }
265 res.mode.assign(nidx, 0.0);
266 for (Int_t i : fit_ch) {
267 Int_t best = -1;
268 for (Int_t b = nb / 16; b < nb; b++) // skip the lowest 1/16 of the range
269 if (best < 0 || spec[i][b] > spec[i][best])
270 best = b;
271 res.mode[i] = best >= 0 ? (best + 0.5) * emax / nb : 0.0;
272 }
273 const std::vector<Double_t> &mode = res.mode;
274 const Double_t blo = Constants::cfg.PULSE_HISTORY_BEAM_LO;
275 const Double_t bhi = Constants::cfg.PULSE_HISTORY_BEAM_HI;
276
277 // Pass A: seeds that make a beam-like event for each group, and the channel
278 // means. A beam-like event for group g has the guard fired, every long end
279 // of the OTHER chain inside the tight window, and every long end of g's own
280 // chain inside the loose one: the tight window on the fitted chain would
281 // cut off exactly the large undershoots the kernel has to fit. The guards
282 // belong to neither chain, so both are held tight for them. A short end or
283 // guard enters the fit only in the events where it fired.
284 std::vector<size_t> seeds[kNGroups];
285 std::vector<Double_t> mean(nidx, 0.0);
286 std::vector<Double_t> ev_e(nidx, 0.0);
287 const Double_t olo = Constants::cfg.PULSE_HISTORY_OWN_LO;
288 const Double_t ohi = Constants::cfg.PULSE_HISTORY_OWN_HI;
289 auto lookahead = [&](size_t i0) {
290 for (Int_t c : fit_ch)
291 ev_e[c] = 0.0;
292 Bool_t guard_fired = guard0 < 0;
293 const ULong64_t t0 = hits[i0].timestamp;
294 for (size_t j = i0 + 1;
295 j < hits.size() && hits[j].timestamp - t0 < window_ps; j++) {
296 const Int_t i = HitIndex(hits[j]);
297 if (i == guard0 && hits[j].energy > 0)
298 guard_fired = kTRUE;
299 if (i < 0 || i >= nidx || group_of[i] == kNone)
300 continue;
301 if (Double_t(hits[j].energy) > ev_e[i])
302 ev_e[i] = Double_t(hits[j].energy);
303 }
304 return guard_fired;
305 };
306 auto beam_for = [&](Int_t g) {
307 const Int_t chain = ChainOf(g);
308 for (Int_t c : long_ch) {
309 if (!(mode[c] > 0.0))
310 return kFALSE;
311 const Double_t r = ev_e[c] / mode[c];
312 const Bool_t own = chain >= 0 && ChainOf(group_of[c]) == chain;
313 if (own ? (r < olo || r > ohi) : (r < blo || r > bhi))
314 return kFALSE;
315 }
316 return kTRUE;
317 };
318 std::vector<Long64_t> nmean(nidx, 0);
319 for (size_t j = 0; j < hits.size(); j++) {
320 const RawHit &h = hits[j];
321 if (HitIndex(h) != ref)
322 continue;
323 if (Double_t(h.energy) < ref_lo || Double_t(h.energy) > ref_hi)
324 continue;
325 res.n_seeds++;
326 if (!lookahead(j))
327 continue;
328 for (Int_t g = 1; g < kNGroups; g++) {
329 if (!beam_for(g))
330 continue;
331 seeds[g].push_back(j);
332 for (Int_t c : fit_ch)
333 if (group_of[c] == g && ev_e[c] > 0.0) {
334 mean[c] += ev_e[c];
335 nmean[c]++;
336 }
337 }
338 }
339 res.n_beam_events = 0;
340 for (Int_t g = 1; g < kNGroups; g++)
341 res.n_beam_events =
342 TMath::Max(res.n_beam_events, Long64_t(seeds[g].size()));
343 if (res.n_beam_events < Constants::cfg.PULSE_HISTORY_MIN_EVENTS) {
344 std::cerr << " " << file_label << ": pulse history: only "
345 << res.n_beam_events << " beam-like events; not measured"
346 << std::endl;
347 return kFALSE;
348 }
349 for (Int_t c : fit_ch)
350 mean[c] = nmean[c] ? mean[c] / Double_t(nmean[c]) : 0.0;
351
352 // One walk over the hits for group g, calling fn(channel, features x,
353 // deviation y, dt to the previous pulse) for every channel of g at every
354 // beam-like seed of g. The per-channel deque holds that channel's earlier
355 // pulses only, so the event's own hit (which comes after the seed) never
356 // enters.
357 const Int_t np = 1 + n_amp * kNBins;
358 const Double_t keep_ps = TMath::Power(10.0, kLogHi) * 1.0e12;
359 std::vector<Double_t> x(np, 0.0);
360 auto scan = [&](Int_t g,
361 const std::function<void(Int_t, const std::vector<Double_t> &,
362 Double_t, Double_t)> &fn) {
363 std::vector<std::deque<Past>> past(nidx);
364 size_t next_seed = 0;
365 for (size_t j = 0; j < hits.size(); j++) {
366 if (next_seed < seeds[g].size() && seeds[g][next_seed] == j) {
367 next_seed++;
368 const Double_t tg = Double_t(hits[j].timestamp);
369 lookahead(j);
370 for (Int_t c : fit_ch) {
371 // A short end or guard that did not fire has no height to fit.
372 if (group_of[c] != g || !(ev_e[c] > 0.0))
373 continue;
374 std::deque<Past> &d = past[c];
375 while (!d.empty() && d.front().t < tg - keep_ps)
376 d.pop_front();
377 std::fill(x.begin(), x.end(), 0.0);
378 x[0] = 1.0;
379 for (const Past &p : d) {
380 const Int_t b = BinOf((tg - p.t) * 1.0e-12);
381 if (b >= 0)
382 x[Feat(AmpBinOf(p.e, mode[c], n_amp), b)] += p.e;
383 }
384 const Double_t dt_prev =
385 d.empty() ? -1.0 : (tg - d.back().t) * 1.0e-12;
386 fn(c, x, ev_e[c] - mean[c], dt_prev);
387 }
388 }
389 const Int_t i = HitIndex(hits[j]);
390 if (i >= 0 && i < nidx && group_of[i] == g)
391 past[i].push_back(
392 {Double_t(hits[j].timestamp), Double_t(hits[j].energy)});
393 }
394 };
395
396 // Pass B: normal equations per group.
397 TMatrixD A[kNGroups];
398 TVectorD bv[kNGroups];
399 Double_t syy[kNGroups] = {0}, sy[kNGroups] = {0};
400 Long64_t ng[kNGroups] = {0};
401 for (Int_t g = 0; g < kNGroups; g++) {
402 A[g].ResizeTo(np, np);
403 bv[g].ResizeTo(np);
404 A[g].Zero();
405 bv[g].Zero();
406 }
407 for (Int_t g = 1; g < kNGroups; g++)
408 scan(g, [&](Int_t, const std::vector<Double_t> &xx, Double_t y, Double_t) {
409 for (Int_t a = 0; a < np; a++) {
410 if (xx[a] == 0.0)
411 continue;
412 bv[g][a] += xx[a] * y;
413 for (Int_t b = 0; b < np; b++)
414 A[g](a, b) += xx[a] * xx[b];
415 }
416 syy[g] += y * y;
417 sy[g] += y;
418 ng[g]++;
419 });
420 Bool_t any = kFALSE;
421 for (Int_t g = 1; g < kNGroups; g++) {
422 Kernel &K = res.kernel[g];
423 K.n = ng[g];
424 K.n_amp = n_amp;
425 K.n_beam = Long64_t(seeds[g].size());
426 if (ng[g] < 100)
427 continue;
428 // Drop features nobody populated (an amplitude band with no pulses).
429 std::vector<Int_t> keep;
430 for (Int_t a = 0; a < np; a++)
431 if (a == 0 || A[g](a, a) > 0.0)
432 keep.push_back(a);
433 const Int_t nk = Int_t(keep.size());
434 TMatrixD Ak(nk, nk);
435 TVectorD bk(nk);
436 for (Int_t a = 0; a < nk; a++) {
437 bk[a] = bv[g][keep[a]];
438 for (Int_t b = 0; b < nk; b++)
439 Ak(a, b) = A[g](keep[a], keep[b]);
440 }
441 // SVD rather than LU: the far-dt bins of the rarer amplitude bands are
442 // nearly collinear with the intercept, and a plain inversion gives up on
443 // them. Column scaling first, so the tolerance means the same for every
444 // feature.
445 TVectorD scale(nk);
446 for (Int_t a = 0; a < nk; a++)
447 scale[a] = Ak(a, a) > 0.0 ? 1.0 / TMath::Sqrt(Ak(a, a)) : 1.0;
448 TMatrixD As(nk, nk);
449 TVectorD bs(nk);
450 for (Int_t a = 0; a < nk; a++) {
451 bs[a] = bk[a] * scale[a];
452 for (Int_t b = 0; b < nk; b++)
453 As(a, b) = Ak(a, b) * scale[a] * scale[b];
454 }
455 TDecompSVD svd(As);
456 svd.SetTol(1.0e-10);
457 Bool_t solved = kFALSE;
458 TVectorD ps = svd.Solve(bs, solved);
459 if (!solved)
460 continue;
461 TVectorD p(nk);
462 for (Int_t a = 0; a < nk; a++)
463 p[a] = ps[a] * scale[a];
464 Double_t ss_res = syy[g];
465 for (Int_t a = 0; a < nk; a++)
466 ss_res -= p[a] * bk[a];
467 const Double_t ybar = sy[g] / ng[g];
468 const Double_t ss_tot = syy[g] - ng[g] * ybar * ybar;
469 if (!(ss_tot > 0.0) || !std::isfinite(ss_res))
470 continue;
471 for (Int_t a = 0; a < nk; a++) {
472 if (keep[a] == 0)
473 K.intercept = p[a];
474 else
475 K.k[(keep[a] - 1) / kNBins][(keep[a] - 1) % kNBins] = p[a];
476 }
477 K.r2 = 1.0 - ss_res / ss_tot;
478 K.rms_before = TMath::Sqrt(ss_tot / ng[g]);
479 K.rms_after = TMath::Sqrt(TMath::Max(0.0, ss_res) / ng[g]);
480 K.ok = K.r2 > 0.0;
481 any = any || K.ok;
482 }
483 if (!any)
484 return kFALSE;
485
486 // Pass C: diagnostics, with the fitted kernels. Log-time axes are
487 // log10(dt) where dt is the time difference in microseconds
488 if (Constants::cfg.SAVE_PLOTS) {
489 const TString tag = file_label;
490 const Double_t xlo = kLogLo + 6.0, xhi = kLogHi + 6.0;
491 for (Int_t g = 1; g < kNGroups; g++) {
492 const char *gn = GroupTag(g);
493 res.dev_vs_pred[g] = new TH2D(
494 Form("h_ph_dev_vs_pred_%s_%s", gn, tag.Data()),
495 ";Predicted Deviation from Mean [ADC];#splitline{Measured}{Deviation from Mean [ADC]}",
496 120, -600.0, 600.0, 120, -600.0, 600.0);
497 res.dev_before[g] = new TH1D(
498 Form("h_ph_dev_before_%s_%s", gn, tag.Data()),
499 Form(";%s #minus Mean %s [ADC];Events", gn, gn), 240, -600.0, 600.0);
500 res.dev_after[g] = new TH1D(
501 Form("h_ph_dev_after_%s_%s", gn, tag.Data()),
502 Form(";%s #minus Mean %s [ADC];Events", gn, gn), 240, -600.0, 600.0);
503 res.dtprev_before[g] = new TProfile(
504 Form("p_ph_dtprev_before_%s_%s", gn, tag.Data()),
505 Form(";log_{10}(#Deltat [#mus]);%s #minus Mean %s [ADC]", gn, gn), 48,
506 xlo, xhi);
507 res.dtprev_after[g] = new TProfile(
508 Form("p_ph_dtprev_after_%s_%s", gn, tag.Data()),
509 Form(";log_{10}(#Deltat [#mus]);%s #minus Mean %s [ADC]", gn, gn), 48,
510 xlo, xhi);
511 res.shift[g] = new TH1D(Form("h_ph_shift_%s_%s", gn, tag.Data()),
512 ";Applied Shift [ADC];Hits", 240, -600.0, 600.0);
513 for (TH1 *h : {static_cast<TH1 *>(res.dev_vs_pred[g]),
514 static_cast<TH1 *>(res.dev_before[g]),
515 static_cast<TH1 *>(res.dev_after[g]),
516 static_cast<TH1 *>(res.dtprev_before[g]),
517 static_cast<TH1 *>(res.dtprev_after[g]),
518 static_cast<TH1 *>(res.shift[g])})
519 h->SetDirectory(nullptr);
520 }
521 for (Int_t g = 1; g < kNGroups; g++) {
522 const Kernel &K = res.kernel[g];
523 if (!K.ok)
524 continue;
525 scan(g, [&](Int_t, const std::vector<Double_t> &xx, Double_t y,
526 Double_t dt_prev) {
527 Double_t pred = K.intercept;
528 for (Int_t a = 0; a < n_amp; a++)
529 for (Int_t b = 0; b < kNBins; b++)
530 pred += K.k[a][b] * xx[Feat(a, b)];
531 const Double_t after = y - pred;
532 res.dev_vs_pred[g]->Fill(pred, y);
533 res.dev_before[g]->Fill(y);
534 res.dev_after[g]->Fill(after);
535 if (dt_prev > 0.0) {
536 const Double_t l = TMath::Log10(dt_prev * 1.0e6);
537 res.dtprev_before[g]->Fill(l, y);
538 res.dtprev_after[g]->Fill(l, after);
539 }
540 });
541 }
542 }
543 return kTRUE;
544}
545
546void Apply(std::vector<RawHit> &hits, const std::vector<Int_t> &group_of,
547 Result &res) {
548 const Int_t nidx = Int_t(group_of.size());
549 const Double_t keep_ps = Constants::cfg.PULSE_HISTORY_APPLY_MAX_US * 1.0e6;
550 std::vector<std::deque<Past>> past(nidx);
551 Double_t shift_sum[kNGroups] = {0};
552 Long64_t shift_n[kNGroups] = {0};
553 for (size_t j = 0; j < hits.size(); j++) {
554 RawHit &h = hits[j];
555 const Int_t i = HitIndex(h);
556 if (i < 0 || i >= nidx || group_of[i] == kNone)
557 continue;
558 const Int_t g = group_of[i];
559 const Double_t t = Double_t(h.timestamp);
560 const Double_t e_raw = Double_t(h.energy);
561 std::deque<Past> &d = past[i];
562 while (!d.empty() && d.front().t < t - keep_ps)
563 d.pop_front();
564 if (res.kernel[g].ok) {
565 const Double_t s = Shift(d, t, res.kernel[g],
566 i < Int_t(res.mode.size()) ? res.mode[i] : 0.0);
567 Double_t e = e_raw - s;
568 if (e < 0.0) {
569 e = 0.0;
570 h.flags |= kFlagClamped;
571 res.n_clamped++;
572 res.n_clamped_group[g]++;
573 } else if (e > 65535.0) {
574 e = 65535.0;
575 }
576 h.energy = UShort_t(e + 0.5);
577 res.n_corrected++;
578 shift_sum[g] += s;
579 shift_n[g]++;
580 if (res.shift[g])
581 res.shift[g]->Fill(-s);
582 }
583 // The kernel was fitted on raw amplitudes of the previous pulses.
584 d.push_back({t, e_raw});
585 }
586 for (Int_t g = 0; g < kNGroups; g++)
587 res.mean_shift[g] = shift_n[g] ? shift_sum[g] / shift_n[g] : 0.0;
588}
589
590TString Report(const Result &res, const TString &file_label) {
591 TString s;
592 s += Form(" pulse history %s: %lld hits%s, %lld reference seeds, %lld "
593 "beam-like events; %lld hits corrected, %lld clamped to 0\n",
594 file_label.Data(), res.n_hits,
595 res.sorted_input ? "" : " (SORTED: input was not time ordered)",
596 res.n_seeds, res.n_beam_events, res.n_corrected, res.n_clamped);
597 for (Int_t g = 1; g < kNGroups; g++) {
598 const Kernel &K = res.kernel[g];
599 s += Form(" %-22s %s beam events %lld n %lld R^2 %.3f rms %.1f -> "
600 "%.1f ADC mean shift %.1f ADC clamped %lld\n",
601 GroupName(g), K.ok ? "fitted " : "NOT USED", K.n_beam, K.n, K.r2,
602 K.rms_before, K.rms_after, res.mean_shift[g],
603 res.n_clamped_group[g]);
604 for (Int_t a = 0; a < K.n_amp; a++) {
605 if (K.n_amp == 1)
606 s += " kernel:";
607 else
608 s += Form(" kernel, previous pulse ~%dx beam:", a);
609 for (Int_t b = 0; b < kNBins; b++)
610 s += Form(" %.0fus:%+.3f", BinCentreUs(b), K.k[a][b]);
611 s += "\n";
612 }
613 }
614 return s;
615}
616
617void SavePlots(Result &res, const TString &file_label) {
618 if (!Constants::cfg.SAVE_PLOTS)
619 return;
620 std::lock_guard<std::mutex> lock(g_plot_mutex);
621 const TString subdir = "pulse_history/" + file_label;
622 const Int_t colors[kMaxAmpBins] = {kBlack, kRed + 1, kAzure + 1,
623 kGreen + 2, kOrange + 7, kMagenta + 1};
624 const Double_t xlo = kLogLo + 6.0, xhi = kLogHi + 6.0;
625 // Kernels: one canvas per group, one curve per amplitude band.
626 for (Int_t g = 1; g < kNGroups; g++) {
627 const Kernel &K = res.kernel[g];
628 if (!K.ok)
629 continue;
630 TCanvas *c = PlottingUtils::GetConfiguredCanvas(kFALSE);
631 Double_t ylo = 0.0, yhi = 0.0;
632 for (Int_t a = 0; a < K.n_amp; a++)
633 for (Int_t b = 0; b < kNBins; b++) {
634 ylo = TMath::Min(ylo, K.k[a][b]);
635 yhi = TMath::Max(yhi, K.k[a][b]);
636 }
637 TH1F *frame = c->DrawFrame(xlo, ylo - 0.02, xhi, yhi + 0.02);
638 frame->SetTitle(";log_{10}(#Deltat [#mus]);"
639 "Relative Amplitude Shift");
640 TLegend *leg = nullptr;
641 if (K.n_amp > 1) {
642 leg = PlottingUtils::AddLegend(0.55, 0.89, 0.15, 0.40);
643 }
644 for (Int_t a = 0; a < K.n_amp; a++) {
645 TGraph *gr = new TGraph();
646 for (Int_t b = 0; b < kNBins; b++)
647 gr->SetPoint(b, xlo + (b + 0.5) * (xhi - xlo) / kNBins, K.k[a][b]);
648 gr->SetMarkerStyle(20);
649 gr->SetMarkerColor(colors[a % kMaxAmpBins]);
650 gr->SetLineColor(colors[a % kMaxAmpBins]);
651 gr->SetLineWidth(2);
652 gr->Draw("PL SAME");
653 if (K.n_amp > 1) {
654 leg->AddEntry(gr, Form("Previous pulse ~%d#times beam", a), "pl");
655 }
656 }
657 if (K.n_amp > 1) {
658 leg->Draw();
659 }
660 TLine *zero = new TLine(xlo, 0.0, xhi, 0.0);
661 zero->SetLineStyle(2);
662 zero->Draw();
663 PlottingUtils::SaveFigure(c, Form("kernel_%s", GroupTag(g)), subdir,
664 PlotSaveOptions::kLINEAR);
665 delete c;
666 }
667 for (Int_t g = 1; g < kNGroups; g++) {
668 const char *gn = GroupTag(g);
669 if (res.dev_vs_pred[g] && res.dev_vs_pred[g]->GetEntries() > 0) {
670 TCanvas *c = PlottingUtils::GetConfiguredCanvas(kFALSE);
671 c->SetLogz(kTRUE);
672 PlottingUtils::ConfigureAndDraw2DHistogram(res.dev_vs_pred[g], c);
673 TLine *diag = new TLine(-600.0, -600.0, 600.0, 600.0);
674 diag->SetLineColor(kRed + 1);
675 diag->SetLineStyle(2);
676 diag->Draw();
677 PlottingUtils::SaveFigure(c, Form("deviation_vs_predicted_%s", gn),
678 subdir, PlotSaveOptions::kLINEAR);
679 delete c;
680 }
681 if (res.dev_before[g] && res.dev_before[g]->GetEntries() > 0) {
682 TCanvas *c = PlottingUtils::GetConfiguredCanvas(kTRUE);
683 PlottingUtils::ConfigureAndDrawHistogram(res.dev_before[g], kBlack);
684 res.dev_after[g]->SetLineColor(kRed + 1);
685 res.dev_after[g]->SetLineWidth(2);
686 res.dev_after[g]->Draw("HIST SAME");
687 TLegend *leg = PlottingUtils::AddLegend(0.62, 0.89, 0.72, 0.88);
688 leg->AddEntry(res.dev_before[g],
689 Form("Before, RMS %.1f", res.kernel[g].rms_before), "l");
690 leg->AddEntry(res.dev_after[g],
691 Form("After, RMS %.1f", res.kernel[g].rms_after), "l");
692 leg->Draw();
693 PlottingUtils::SaveFigure(c, Form("deviation_before_after_%s", gn),
694 subdir, PlotSaveOptions::kLOG);
695 delete c;
696 }
697 if (res.dtprev_before[g] && res.dtprev_before[g]->GetEntries() > 0) {
698 TCanvas *c = PlottingUtils::GetConfiguredCanvas(kFALSE);
699 res.dtprev_before[g]->SetMinimum(-300.0);
700 res.dtprev_before[g]->SetMaximum(200.0);
701 PlottingUtils::ConfigureAndDrawHistogram(res.dtprev_before[g], kBlack);
702 res.dtprev_after[g]->SetLineColor(kRed + 1);
703 res.dtprev_after[g]->SetMarkerColor(kRed + 1);
704 res.dtprev_after[g]->SetLineWidth(2);
705 res.dtprev_after[g]->Draw("SAME");
706 TLegend *leg = PlottingUtils::AddLegend(0.62, 0.89, 0.15, 0.30);
707 leg->AddEntry(res.dtprev_before[g], "Before", "l");
708 leg->AddEntry(res.dtprev_after[g], "After", "l");
709 leg->Draw();
710 PlottingUtils::SaveFigure(c, Form("deviation_vs_dt_previous_%s", gn),
711 subdir, PlotSaveOptions::kLINEAR);
712 delete c;
713 }
714 if (res.shift[g] && res.shift[g]->GetEntries() > 0) {
715 TCanvas *c = PlottingUtils::GetConfiguredCanvas(kTRUE);
716 PlottingUtils::ConfigureAndDrawHistogram(res.shift[g], kBlack);
717 PlottingUtils::SaveFigure(c, Form("applied_shift_%s", gn), subdir,
718 PlotSaveOptions::kLOG);
719 delete c;
720 }
721 }
722 for (Int_t g = 0; g < kNGroups; g++) {
723 delete res.dev_vs_pred[g];
724 delete res.dev_before[g];
725 delete res.dev_after[g];
726 delete res.shift[g];
727 delete res.dtprev_before[g];
728 delete res.dtprev_after[g];
729 res.dev_vs_pred[g] = nullptr;
730 res.dev_before[g] = res.dev_after[g] = res.shift[g] = nullptr;
731 res.dtprev_before[g] = res.dtprev_after[g] = nullptr;
732 }
733}
734
735void WriteToEventsFile(const TString &events_subpath, const Result &res) {
736 TFile *f = IO::OpenForWriting(events_subpath, "UPDATE");
737 if (!f || f->IsZombie()) {
738 if (f)
739 delete f;
740 return;
741 }
742 f->cd();
743 if (TObject *old = f->Get("pulse_history"))
744 old->Delete();
745 TTree *t = new TTree("pulse_history", "Pulse-history kernel per group");
746 Int_t group = 0, n_amp = 1;
747 Bool_t ok = kFALSE;
748 Double_t k[kMaxAmpBins * kNBins], centre_us[kNBins],
749 intercept = 0.0, r2 = 0.0, rms_before = 0.0, rms_after = 0.0,
750 mean_shift = 0.0, apply_max_us = 0.0;
751 Long64_t n = 0, n_beam = 0, n_corrected = 0, n_clamped = 0;
752 t->Branch("Group", &group, "Group/I");
753 t->Branch("Ok", &ok, "Ok/O");
754 t->Branch("NAmpBins", &n_amp, "NAmpBins/I");
755 t->Branch("Kernel", k, Form("Kernel[%d]/D", kMaxAmpBins * kNBins));
756 t->Branch("BinCentreUs", centre_us, Form("BinCentreUs[%d]/D", kNBins));
757 t->Branch("Intercept", &intercept, "Intercept/D");
758 t->Branch("R2", &r2, "R2/D");
759 t->Branch("RmsBefore", &rms_before, "RmsBefore/D");
760 t->Branch("RmsAfter", &rms_after, "RmsAfter/D");
761 t->Branch("MeanShift", &mean_shift, "MeanShift/D");
762 t->Branch("ApplyMaxUs", &apply_max_us, "ApplyMaxUs/D");
763 t->Branch("N", &n, "N/L");
764 t->Branch("NBeamEvents", &n_beam, "NBeamEvents/L");
765 t->Branch("NCorrected", &n_corrected, "NCorrected/L");
766 t->Branch("NClamped", &n_clamped, "NClamped/L");
767 for (Int_t g = 1; g < kNGroups; g++) {
768 const Kernel &K = res.kernel[g];
769 group = g;
770 ok = K.ok;
771 n_amp = K.n_amp;
772 for (Int_t a = 0; a < kMaxAmpBins; a++)
773 for (Int_t b = 0; b < kNBins; b++)
774 k[a * kNBins + b] = K.k[a][b];
775 for (Int_t b = 0; b < kNBins; b++)
776 centre_us[b] = BinCentreUs(b);
777 intercept = K.intercept;
778 r2 = K.r2;
779 rms_before = K.rms_before;
780 rms_after = K.rms_after;
781 mean_shift = res.mean_shift[g];
782 apply_max_us = Constants::cfg.PULSE_HISTORY_APPLY_MAX_US;
783 n = K.n;
784 n_beam = res.n_beam_events;
785 n_corrected = res.n_corrected;
786 n_clamped = res.n_clamped;
787 t->Fill();
788 }
789 t->Write("pulse_history", TObject::kOverwrite);
790 f->Close();
791 delete f;
792}
793
794} // namespace PulseHistory
The dataset configuration, and how it is layered.
std::mutex g_plot_mutex
Serialises all plotting and canvas work.
Definition FileSet.cpp:3
Pole-zero pulse-history correction on the raw hit stream.
Double_t ActiveReferenceChannelMaxAdc()
Upper energy gate on the seed channel, in ADC.
const TString & ActiveReferenceChannel()
Channel whose hits seed events.
Double_t ActiveStripEMaxAdc()
Upper bound of the per-strip energy range, in ADC.
Double_t ActiveEventTimeWindowUs()
Coincidence window for event building, in microseconds.
const std::map< std::pair< Int_t, Int_t >, TString > & ActiveChannelMap()
The channel map in force.
Double_t ActiveReferenceChannelMinAdc()
Lower energy gate on the seed channel, in ADC.
Int_t ActiveNBoards()
Boards in the active epoch's setup.
const DatasetConfig & cfg
The active dataset's configuration, flat block.
Int_t ActiveNChannels()
Channels per board.
const Double_t kLogHi
Highest dt bin edge: log10 of 100 us in seconds.
const Int_t kMaxAmpBins
Maximum amplitude bands a kernel may split the previous pulse into.
Int_t ChainOf(Int_t g)
Which readout chain a group belongs to.
const Int_t kNBins
Bins in the kernel, spanning kLogLo to kLogHi in log10 of dt.
TString Report(const Result &res, const TString &file_label)
Format the pass as a human-readable report.
const Double_t kLogLo
Lowest dt bin edge: log10 of 1 us in seconds.
Double_t BinCentreUs(Int_t b)
Centre of a kernel bin, in microseconds, for reports.
const char * GroupTag(Int_t g)
Short tag for filenames: L, R, Ls, Rs, S0, S17.
std::vector< Int_t > BuildGroupMap()
Group lookup for every (board, channel) under the active map.
@ kShortLeft
Short end, left chain.
@ kLongLeft
Long end, left chain.
@ kShortRight
Short end, right chain.
@ kGuard17
Single-pad guard after strip 17.
@ kNone
Not part of any corrected group.
@ kNGroups
Count of groups; not a group itself.
@ kGuard0
Single-pad guard before strip 0.
@ kLongRight
Long end, right chain.
Int_t BinOf(Double_t dt_s)
Kernel bin for a time since the previous pulse.
void Apply(std::vector< RawHit > &hits, const std::vector< Int_t > &group_of, Result &res)
Apply the measured kernels to the hit stream, in place.
Bool_t Measure(std::vector< RawHit > &hits, const std::vector< Int_t > &group_of, Result &res, const TString &file_label)
Measure the kernels on this subfile's beam-like events.
Bool_t IsLongGroup(Int_t g)
Whether a group is a long end rather than a short end or guard.
void SavePlots(Result &res, const TString &file_label)
Draw and save the diagnostics, then free them.
const UInt_t kFlagClamped
Flag set on a hit whose correction was clamped rather than applied in full.
Int_t AmpBinOf(Double_t e_prev, Double_t mode, Int_t n_amp)
Amplitude band of a previous pulse.
void WriteToEventsFile(const TString &events_subpath, const Result &res)
Record the kernels and counters alongside a subfile's events.
const char * GroupName(Int_t g)
Human-readable name of a Group.
The fitted correction for one channel group.
Double_t rms_before
Channel-deviation RMS before correction.
Double_t r2
Coefficient of determination.
Long64_t n_beam
Beam-like events passing this group's selection.
Bool_t ok
Whether this group was successfully fitted.
Double_t k[kMaxAmpBins][kNBins]
Coefficients, [amplitude band][dt bin].
Long64_t n
(event, channel) pairs entering the fit.
Double_t intercept
Fit intercept, in ADC.
Double_t rms_after
And after, so the gain is visible.
Int_t n_amp
Amplitude bands actually used, at most kMaxAmpBins.
Kernel()
Construct unfitted, with zeroed coefficients.
Everything one subfile's pulse-history pass produced.
TH1D * dev_after[kNGroups]
And after.
TH1D * dev_before[kNGroups]
Deviation distribution before correction.
Double_t mean_shift[kNGroups]
Mean subtracted term per group, ADC.
~Result()
Frees any diagnostic histograms still held.
Long64_t n_corrected
Hits that received a correction.
Kernel kernel[kNGroups]
One kernel per Group.
std::vector< Double_t > mode
Beam peak per (board, channel) index, in ADC.
Long64_t n_beam_events
Beam-like events the fit drew on.
TH1D * shift[kNGroups]
Shift applied per hit.
TProfile * dtprev_before[kNGroups]
Deviation against time to previous pulse.
Long64_t n_seeds
Hits usable as a previous pulse.
Bool_t sorted_input
Whether the input arrived time-ordered.
Long64_t n_clamped_group[kNGroups]
Clamps per group.
Long64_t n_clamped
Corrections clamped rather than applied in full.
TProfile * dtprev_after[kNGroups]
And after.
Result()
Construct with zeroed counters and null diagnostics.
TH2D * dev_vs_pred[kNGroups]
Channel deviation against predicted shift.
Long64_t n_hits
Hits examined.