MUSIC unknown
Analysis for the MUSIC active-target ionization chamber
Loading...
Searching...
No Matches
StripSumScatter.cpp
Go to the documentation of this file.
1#include "StripSumScatter.hpp"
2#include "RegionCuts.hpp"
3#include <TParameter.h>
4
6 for (Int_t i = 0; i < 64; i++) {
7 m_yLo[i] = 0.0;
8 m_yHi[i] = 0.0;
9 }
10 m_nSeen = 0;
11 m_nNormed = 0;
12 m_normedAt.assign(
13 Constants::cfg.STRIP_SUM_SCATTER_CONFIG.REACTION_STRIP_MAX -
14 Constants::cfg.STRIP_SUM_SCATTER_CONFIG.REACTION_STRIP_MIN + 1,
15 0);
16 m_tagged.assign(
17 Constants::cfg.STRIP_SUM_SCATTER_CONFIG.REACTION_STRIP_MAX -
18 Constants::cfg.STRIP_SUM_SCATTER_CONFIG.REACTION_STRIP_MIN + 1,
19 0);
20}
21
23 std::map<Int_t, TH2F *>::iterator it;
24 for (it = m_scatter.begin(); it != m_scatter.end(); ++it)
25 delete it->second;
26 m_scatter.clear();
27}
28
29Int_t StripSumScatter::ReacIndex(Int_t reac) {
30 return reac - Constants::cfg.STRIP_SUM_SCATTER_CONFIG.REACTION_STRIP_MIN;
31}
32
33Int_t StripSumScatter::YLoOf(Int_t reac) { return reac + 1; }
34
35Int_t StripSumScatter::YHiOf(Int_t reac) {
36 const StripSumScatterConfig &C = Constants::cfg.STRIP_SUM_SCATTER_CONFIG;
37 std::map<Int_t, Int_t>::const_iterator it = C.POST_WINDOW_STRIPS.find(reac);
38 if (it != C.POST_WINDOW_STRIPS.end())
39 return TMath::Min(reac + TMath::Max(1, it->second), 17);
40 // The window shrinks where it would otherwise run past the last strip it
41 // may reach, and is never shorter than one strip.
42 const Int_t hi = TMath::Min(reac + C.POST_TRIGGER_SUM_STRIPS,
43 TMath::Min(C.POST_WINDOW_LAST_STRIP, 17));
44 return TMath::Max(hi, reac + 1);
45}
46
47Double_t StripSumScatter::s_jumpSigma[18] = {0.0};
48Double_t StripSumScatter::s_stripSigma[18] = {0.0};
49
50Double_t StripSumScatter::JumpSigma(Int_t strip) {
51 return (strip >= 0 && strip < 18) ? s_jumpSigma[strip] : 0.0;
52}
53
54Double_t StripSumScatter::StripSigma(Int_t strip) {
55 return (strip >= 0 && strip < 18) ? s_stripSigma[strip] : 0.0;
56}
57
58void StripSumScatter::SetJumpSigma(const Double_t *sigma) {
59 for (Int_t s = 0; s < 18; s++)
60 s_jumpSigma[s] = sigma[s];
61}
62
63void StripSumScatter::SetStripSigma(const Double_t *sigma) {
64 for (Int_t s = 0; s < 18; s++)
65 s_stripSigma[s] = sigma[s];
66}
67
68Double_t StripSumScatter::JumpMin(Int_t reac) {
69 return Constants::cfg.STRIP_SUM_SCATTER_CONFIG.REAC_JUMP_NSIGMA *
70 JumpSigma(reac);
71}
72
73Bool_t StripSumScatter::BeamUpstreamOf(const EnergyView &ev, Int_t reac) {
74 const StripSumScatterConfig &C = Constants::cfg.STRIP_SUM_SCATTER_CONFIG;
76 return kTRUE;
77 for (Int_t s = 1; s < reac; s++)
78 if (TMath::Abs(ev.total[s] - 1.0) > C.BEAM_UPSTREAM_NSIGMA * StripSigma(s))
79 return kFALSE;
80 return kTRUE;
81}
82
83// Sigma-clipped width of a sample.
84static Double_t ClippedWidth(const std::vector<Double_t> &values) {
85 const Int_t kClipPasses = 3;
86 const Double_t kClipNSigma = 3.0;
87 Double_t mean = 0.0, width = 0.0;
88 for (Int_t pass = 0; pass < kClipPasses; pass++) {
89 Double_t sum = 0.0, sum2 = 0.0;
90 Long64_t kept = 0;
91 for (Int_t k = 0; k < Int_t(values.size()); k++) {
92 const Double_t v = values[k];
93 if (pass > 0 && TMath::Abs(v - mean) > kClipNSigma * width)
94 continue;
95 sum += v;
96 sum2 += v * v;
97 kept++;
98 }
99 if (kept < 2)
100 break;
101 mean = sum / Double_t(kept);
102 width = TMath::Sqrt(TMath::Max(0.0, sum2 / Double_t(kept) - mean * mean));
103 }
104 return width;
105}
106
107Bool_t StripSumScatter::MeasureBeamNoise(TChain *chain, Double_t *jump_sigma,
108 Double_t *strip_sigma) {
109 const Long64_t kMaxEvents = 200000;
110 const Long64_t kMinEvents = 1000;
111 for (Int_t s = 0; s < 18; s++) {
112 jump_sigma[s] = 0.0;
113 strip_sigma[s] = 0.0;
114 }
115 if (!chain)
116 return kFALSE;
117
118 EnergyView ev;
119 ev.Attach(chain);
120 EnableEventBranches(chain);
121 const Long64_t n = TMath::Min(chain->GetEntries(), kMaxEvents);
122 std::vector<std::vector<Double_t>> diff(18), deposit(18);
123 for (Long64_t j = 0; j < n; j++) {
124 chain->GetEntry(j);
125 ev.Decode();
126 if (!AllStripsFired(ev) || IsPileup(ev) || IsNoise(ev))
127 continue;
128 if (Constants::cfg.STRIP_SUM_SCATTER_CONFIG.REJECT_OFFBEAM && IsOffbeam(ev))
129 continue;
130 if (IsParityAsymmetric(ev))
131 continue;
132 deposit[0].push_back(ev.total[0]);
133 for (Int_t s = 1; s < 18; s++) {
134 diff[s].push_back(ev.total[s] - ev.total[s - 1]);
135 deposit[s].push_back(ev.total[s]);
136 }
137 }
138 chain->ResetBranchAddresses();
139 if (Long64_t(deposit[0].size()) < kMinEvents)
140 return kFALSE;
141
142 // Reactions are a few 1e-4 of the sample, so the clipped widths are the
143 // beam's noise.
144 for (Int_t s = 0; s < 18; s++)
145 strip_sigma[s] = ClippedWidth(deposit[s]);
146 for (Int_t s = 1; s < 18; s++)
147 jump_sigma[s] = ClippedWidth(diff[s]);
148 for (Int_t s = 1; s <= 16; s++)
149 if (!(jump_sigma[s] > 0.0) || !(strip_sigma[s] > 0.0))
150 return kFALSE;
151 return kTRUE;
152}
153
154void StripSumScatter::EnableEventBranches(TChain *chain) {
155 chain->SetBranchStatus("*", 0);
156 chain->SetBranchStatus("Left_0_17_dE", 1);
157 chain->SetBranchStatus("RightdE", 1);
158 chain->SetBranchStatus("Cathode", 1);
159 // Absent in files built before the branch existed; harmless to enable.
160 if (chain->GetBranch("SeedTs"))
161 chain->SetBranchStatus("SeedTs", 1);
162 // The parity-rejected grid diagnostic reads the Grid trigger channel. Enable
163 // it only when that plot is requested so the common update path stays
164 // branch-light (the grid is a Short_t; reading it otherwise is wasted I/O).
165 if (Constants::cfg.STRIP_SUM_SCATTER_CONFIG.PLOT_PARITY_REJECTED_GRID &&
166 chain->GetBranch("Grid"))
167 chain->SetBranchStatus("Grid", 1);
168}
169
170Bool_t StripSumScatter::AllStripsFired(const EnergyView &ev) {
171 if (!Constants::cfg.IGNORE_STRIP_0 && !(ev.total[0] > 0.0))
172 return kFALSE;
173 if (!Constants::cfg.IGNORE_STRIP_17 && !(ev.total[17] > 0.0))
174 return kFALSE;
175 for (Int_t s = 1; s <= 16; s++)
176 if (!(ev.total[s] > 0.0))
177 return kFALSE;
178 return kTRUE;
179}
180
181Bool_t StripSumScatter::PassesReaction(const EnergyView &ev, Int_t reac) {
182 const Double_t kReacJumpMin = JumpMin(reac);
183 const Double_t kReacJumpMax =
184 Constants::cfg.STRIP_SUM_SCATTER_CONFIG.REAC_JUMP_MAX;
185 const Double_t kSmoothMaxStep =
186 Constants::cfg.STRIP_SUM_SCATTER_CONFIG.REQUIRE_SMOOTHNESS_MAX_STEP;
187 const Int_t kSmoothHiStrip =
188 Constants::cfg.STRIP_SUM_SCATTER_CONFIG.REQUIRE_SMOOTHNESS_END_STRIP;
189 const Double_t kEndStripMax =
190 Constants::cfg.STRIP_SUM_SCATTER_CONFIG.END_STRIP_MAX;
191
192 if (!AllStripsFired(ev))
193 return kFALSE;
194 // Otherwise a reaction at an earlier strip can pass this strip's jump gate
195 // on a noise fluctuation and be counted here as well.
196 if (!BeamUpstreamOf(ev, reac))
197 return kFALSE;
198 Double_t reac_jump = ev.total[reac] - ev.total[reac - 1];
199 if (!(reac_jump > kReacJumpMin && reac_jump < kReacJumpMax))
200 return kFALSE;
201 if (!(ev.total[reac] > 1.0 + kReacJumpMin &&
202 ev.total[reac] < 1.0 + kReacJumpMax))
203 return kFALSE;
204 if (Constants::cfg.STRIP_SUM_SCATTER_CONFIG.REQUIRE_SMOOTHNESS)
205 for (Int_t s = reac + 1; s <= kSmoothHiStrip; s++)
206 if (TMath::Abs(ev.total[s] - ev.total[s - 1]) > kSmoothMaxStep)
207 return kFALSE;
208 if (Constants::cfg.IGNORE_STRIP_17)
209 return ev.total[16] < kEndStripMax;
210 Int_t end_strip =
211 Constants::cfg.STRIP_SUM_SCATTER_CONFIG.REQUIRE_STRIP_16_BELOW_BEAM ? 16
212 : 17;
213 return ev.total[end_strip] < kEndStripMax;
214}
215
216Bool_t StripSumScatter::IsPureBeam(const EnergyView &ev,
217 const BeamEllipses &be) {
218 if (!be.ok)
219 return kFALSE;
220 if (!AllStripsFired(ev))
221 return kFALSE;
222 // Must pass BOTH the entrance AND exit ellipses.
223 if (Constants::cfg.STRIP_SUM_SCATTER_CONFIG.PURE_BEAM_GATE ==
225 if (!PassesGate(be.s1_s2, ev, 1, 2))
226 return kFALSE;
227 } else {
228 if (!PassesGate(be.s0_s1, ev, 0, 1))
229 return kFALSE;
230 }
231 if (be.use_s15_s16) {
232 if (!PassesGate(be.s15_s16, ev, 15, 16))
233 return kFALSE;
234 } else {
235 if (!PassesGate(be.s16_s17, ev, 16, 17))
236 return kFALSE;
237 }
238 return kTRUE;
239}
240
241Bool_t StripSumScatter::IsPileup(const EnergyView &ev) {
242 if (Constants::cfg.STRIP_SUM_SCATTER_CONFIG.REJECT_PILEUP) {
243 const Double_t kThresh =
245 const Int_t kMinStrips =
247 Int_t n = 0;
248 for (Int_t s = 1; s <= 16; s++)
249 if (ev.total[s] >= kThresh && ++n >= kMinStrips)
250 return kTRUE;
251 return kFALSE;
252 }
253 const Double_t kThresh =
255 const Int_t kMinStrips =
257 Int_t n = 0;
258 for (Int_t s = 1; s <= 16; s++)
259 if (ev.total[s] >= kThresh && ++n >= kMinStrips)
260 return kTRUE;
261 return kFALSE;
262}
263
264Bool_t StripSumScatter::IsNoise(const EnergyView &ev) {
265 if (Constants::cfg.STRIP_SUM_SCATTER_CONFIG.REJECT_NOISE) {
266 const Double_t kThresh =
268 const Int_t kMinStrips =
270 Int_t n = 0;
271 for (Int_t s = 1; s <= 16; s++)
272 if (ev.total[s] <= kThresh && ++n >= kMinStrips)
273 return kTRUE;
274 return kFALSE;
275 }
276 const Double_t kThresh =
278 const Int_t kMinStrips =
280 Int_t n = 0;
281 for (Int_t s = 1; s <= 16; s++)
282 if (ev.total[s] <= kThresh && ++n >= kMinStrips)
283 return kTRUE;
284 return kFALSE;
285}
286
287Bool_t StripSumScatter::IsOffbeam(const EnergyView &ev) {
289 const Int_t kMinStrips =
291 // Use flat normed beam level (1.0) as reference, matching Python which
292 // computes beam reference after these pre-filters.
293 const Double_t kBeamRef = 1.0;
294 Int_t n = 0;
295 for (Int_t s = 1; s <= 16; s++)
296 if (TMath::Abs(ev.total[s] - kBeamRef) >= kDist && ++n >= kMinStrips)
297 return kTRUE;
298 return kFALSE;
299}
300
301// Even strips against odd strips over the whole trace, before the tag; see
302// PARITY_ASYM_MAX. Strip 16 is even and strip 1 odd, so both means run over
303// eight strips; a residue's excess spreads over both parities alike and moves
304// the ratio by a few percent at most.
305Bool_t StripSumScatter::IsParityAsymmetric(const EnergyView &ev) {
307 if (kMax <= 0.0)
308 return kFALSE;
309 Double_t odd = 0.0, even = 0.0;
310 for (Int_t s = 1; s <= 16; s++)
311 ((s % 2) ? odd : even) += ev.total[s];
312 if (odd <= 0.0)
313 return kTRUE;
314 return TMath::Abs(even / odd - 1.0) > kMax;
315}
316
317Double_t StripSumScatter::SumRange(const Double_t *total, Int_t lo, Int_t hi) {
318 Double_t sum = 0.0;
319 for (Int_t s = lo; s <= hi; s++)
320 sum += total[s];
321 return sum;
322}
323
324void StripSumScatter::PlaneXY(const Double_t *total, Int_t reac, Double_t &x,
325 Double_t &y) {
326 x = SumRange(total, Constants::cfg.STRIP_SUM_SCATTER_CONFIG.X_LO,
327 Constants::cfg.STRIP_SUM_SCATTER_CONFIG.X_HI);
328 y = SumRange(total, YLoOf(reac), YHiOf(reac));
329}
330
331std::vector<GateSpec> StripSumScatter::ActiveGates() {
332 std::vector<GateSpec> gates;
333 GateSpec g;
334 g.sx = Constants::cfg.STRIP_SUM_SCATTER_CONFIG.GATE_STRIP_X;
335 g.sy = Constants::cfg.STRIP_SUM_SCATTER_CONFIG.GATE_STRIP_Y;
336 gates.push_back(g);
337 if (Constants::cfg.STRIP_SUM_SCATTER_CONFIG.REQUIRE_GATE_S3_S4) {
338 g.sx = 3;
339 g.sy = 4;
340 gates.push_back(g);
341 }
342 if (Constants::cfg.STRIP_SUM_SCATTER_CONFIG.REQUIRE_GATE_S5_S6) {
343 g.sx = 5;
344 g.sy = 6;
345 gates.push_back(g);
346 }
347 return gates;
348}
349
351 TString name = "StripSumScatter_cache";
352 if (Constants::cfg.STRIP_SUM_SCATTER_CONFIG.REQUIRE_GATE_S3_S4)
353 name += "_g34";
354 if (Constants::cfg.STRIP_SUM_SCATTER_CONFIG.REQUIRE_GATE_S5_S6)
355 name += "_g56";
356 name += ".root";
357 return name;
358}
359
360Bool_t StripSumScatter::PassesGate(const BeamFit2D &gate, const EnergyView &ev,
361 Int_t sx, Int_t sy) {
362 Double_t g0 = ev.total[sx];
363 Double_t g1 = ev.total[sy];
364 if (!(g0 > 0.0 && g1 > 0.0))
365 return kFALSE;
366 const Double_t kGateNSigmaX =
367 Constants::cfg.STRIP_SUM_SCATTER_CONFIG.GATE_NSIGMA_X;
368 const Double_t kGateNSigmaY =
369 Constants::cfg.STRIP_SUM_SCATTER_CONFIG.GATE_NSIGMA_Y;
370 return BeamFitUtils::InEllipseXY(gate, g0, g1, kGateNSigmaX, kGateNSigmaY);
371}
372
374StripSumScatter::FindBeamGate(TChain *chain, Int_t sx, Int_t sy,
375 const std::vector<GateSpec> &prior_specs,
376 const std::vector<BeamFit2D> &prior_gates,
377 const TString &tag, const TString &subdir) {
378 const Int_t kGateBins = Constants::cfg.STRIP_SUM_SCATTER_CONFIG.GATE_BINS;
379 const Double_t kGateMin = Constants::cfg.STRIP_SUM_SCATTER_CONFIG.GATE_MIN;
380 const Double_t kGateMax = Constants::cfg.STRIP_SUM_SCATTER_CONFIG.GATE_MAX;
381 const Int_t kSeedHalfBins =
383 const Double_t kSeedFrac = 0.3;
384 const Long64_t kSampleMaxPoints =
386
387 BeamFit2D out;
388 EnergyView ev;
389 ev.Attach(chain);
390 EnableEventBranches(chain);
391 TH2F *h =
392 new TH2F(Form("h2_beamgate_s%d_s%d_%s", sx, sy, tag.Data()),
393 Form(";#DeltaE strip %d [a.u.];#DeltaE strip %d [a.u.]", sx, sy),
394 kGateBins, kGateMin, kGateMax, kGateBins, kGateMin, kGateMax);
395 h->SetDirectory(nullptr);
396 Long64_t n = chain->GetEntries();
397 Long64_t stride = FileSet::SampleStride(n, kSampleMaxPoints);
398 for (Long64_t j = 0; j < n; j += stride) {
399 chain->GetEntry(j);
400 ev.Decode();
401 // Series gating: only events passing every prior gate feed this fit.
402 Bool_t prior_ok = kTRUE;
403 for (Int_t gi = 0; gi < Int_t(prior_specs.size()); gi++)
404 if (!PassesGate(prior_gates[gi], ev, prior_specs[gi].sx,
405 prior_specs[gi].sy)) {
406 prior_ok = kFALSE;
407 break;
408 }
409 if (!prior_ok)
410 continue;
411 Double_t x = ev.total[sx];
412 Double_t y = ev.total[sy];
413 if (x > 0.0 && y > 0.0)
414 h->Fill(x, y);
415 }
416 if (h->GetEntries() < 100) {
417 delete h;
418 return out;
419 }
420 Double_t bw_x = h->GetXaxis()->GetBinWidth(1);
421 Double_t bw_y = h->GetYaxis()->GetBinWidth(1);
422 Int_t bx = 0, by = 0, bz = 0;
423 h->GetMaximumBin(bx, by, bz);
424 Double_t peak_val = h->GetBinContent(bx, by);
425 Int_t lo_bx = std::max(1, bx - kSeedHalfBins);
426 Int_t hi_bx = std::min(h->GetNbinsX(), bx + kSeedHalfBins);
427 Int_t lo_by = std::max(1, by - kSeedHalfBins);
428 Int_t hi_by = std::min(h->GetNbinsY(), by + kSeedHalfBins);
429 Moments2D m = BeamFitUtils::ComputeMoments(h, lo_bx, hi_bx, lo_by, hi_by,
430 kSeedFrac * peak_val, bw_x, bw_y);
431 if (m.weight <= 0) {
432 delete h;
433 return out;
434 }
435 out.amp = peak_val;
436 out.mu_x = m.mu_x;
437 out.mu_y = m.mu_y;
438 out.sigma_x = m.sigma_x;
439 out.sigma_y = m.sigma_y;
440 out.rho = m.rho;
441 out.ok = kTRUE;
442
443 const Double_t kGateNSigmaX =
445 const Double_t kGateNSigmaY =
447
448 std::lock_guard<std::mutex> lock(g_plot_mutex);
449 TCanvas *c = PlottingUtils::GetConfiguredCanvas(kFALSE);
450 PlottingUtils::ConfigureAndDraw2DHistogram(h, c);
451 // Correlated 2D Gaussian ellipse matching the InEllipseXY gate.
452 Double_t sxx = out.sigma_x * out.sigma_x;
453 Double_t syy = out.sigma_y * out.sigma_y;
454 Double_t sxy = out.rho * out.sigma_x * out.sigma_y;
455 Double_t sum = sxx + syy;
456 Double_t diff = sxx - syy;
457 Double_t det = TMath::Sqrt(diff * diff + 4.0 * sxy * sxy);
458 Double_t lambda1 = 0.5 * (sum + det);
459 Double_t lambda2 = 0.5 * (sum - det);
460 Double_t theta = 0.5 * TMath::ATan2(2.0 * sxy, diff) * 180.0 / TMath::Pi();
461 TEllipse *e =
462 new TEllipse(out.mu_x, out.mu_y, kGateNSigmaX * TMath::Sqrt(lambda1),
463 kGateNSigmaX * TMath::Sqrt(lambda2), 0, 360, theta);
464 e->SetFillStyle(0);
465 e->SetLineColor(kRed + 1);
466 e->SetLineWidth(2);
467 e->Draw();
468 PlottingUtils::SaveFigure(c, Form("beam_gate_s%d_s%d", sx, sy), subdir,
469 PlotSaveOptions::kLINEAR);
470 delete c;
471
472 delete h;
473 return out;
474}
475
476void StripSumScatter::DrawTraceSet(const std::vector<TGraph *> &traces,
477 Int_t color) {
478 for (Int_t i = 0; i < Int_t(traces.size()); i++) {
479 traces[i]->SetLineColor(color);
480 traces[i]->SetLineWidth(1);
481 traces[i]->Draw("L SAME");
482 }
483}
484
485// Re-render the SAME selected events under the other decode.
486// IGNORE_SHORT_STRIPS is a decode-time switch, so both renderings come from one
487// calibration: the long-only trace is long_au, and the summed trace is (long_au
488// + short_au) rescaled per strip so that the summed BEAM peak sits at 1.0 a.u.
489// Without that rescale the two are not on a common scale -- pass 1 anchors the
490// long side's own beam peak at 1.0, so the sum reads 1+f with f the short-side
491// fraction.
492void StripSumScatter::DrawAltDecodeRegionTraces(Int_t reac, TCutG *cutAn,
493 TCutG *cutAa) {
494 const Bool_t long_only_is_current = Constants::cfg.IGNORE_SHORT_STRIPS;
497 const Int_t kTracesPerRegion =
499 UInt_t bit = (1u << ReacIndex(reac));
500
501 std::vector<const TraceEvt *> ev_beam, ev_aa, ev_an;
502 for (Int_t k = 0; k < Int_t(m_reservoir.size()); k++) {
503 const TraceEvt &e = m_reservoir[k];
504 if (e.beam_flat) {
505 if (Int_t(ev_beam.size()) < kTracesPerRegion)
506 ev_beam.push_back(&e);
507 continue;
508 }
509 if (!(e.reac_mask & bit))
510 continue;
511 Double_t td[18];
512 for (Int_t s = 0; s < 18; s++)
513 td[s] = Double_t(e.total[s]);
514 Double_t x = 0.0, y = 0.0;
515 PlaneXY(td, reac, x, y);
516 if (cutAn && Int_t(ev_an.size()) < kTracesPerRegion &&
517 cutAn->IsInside(x, y))
518 ev_an.push_back(&e);
519 else if (cutAa && Int_t(ev_aa.size()) < kTracesPerRegion &&
520 cutAa->IsInside(x, y))
521 ev_aa.push_back(&e);
522 }
523
524 Double_t norm[18];
525 for (Int_t s = 0; s < 18; s++)
526 norm[s] = 1.0;
527 for (Int_t s = 1; s <= 16; s++) {
528 std::vector<Double_t> v;
529 for (Int_t k = 0; k < Int_t(ev_beam.size()); k++) {
530 Double_t sum =
531 Double_t(ev_beam[k]->long_au[s]) + Double_t(ev_beam[k]->short_au[s]);
532 if (sum > 0)
533 v.push_back(sum);
534 }
535 if (v.size() < 20)
536 continue;
537 std::sort(v.begin(), v.end());
538 Double_t med = v[v.size() / 2];
539 if (med > 0)
540 norm[s] = 1.0 / med;
541 }
542
543 std::vector<TGraph *> g_beam, g_aa, g_an;
544 std::vector<const std::vector<const TraceEvt *> *> srcs;
545 srcs.push_back(&ev_beam);
546 srcs.push_back(&ev_aa);
547 srcs.push_back(&ev_an);
548 std::vector<std::vector<TGraph *> *> dsts;
549 dsts.push_back(&g_beam);
550 dsts.push_back(&g_aa);
551 dsts.push_back(&g_an);
552 for (Int_t c = 0; c < 3; c++) {
553 for (Int_t k = 0; k < Int_t(srcs[c]->size()); k++) {
554 const TraceEvt &e = *(*srcs[c])[k];
555 Float_t alt[18];
556 for (Int_t s = 0; s < 18; s++)
557 alt[s] = e.long_au[s];
558 for (Int_t s = 1; s <= 16; s++)
559 alt[s] =
560 long_only_is_current
561 ? Float_t((Double_t(e.long_au[s]) + Double_t(e.short_au[s])) *
562 norm[s])
563 : e.long_au[s];
564 dsts[c]->push_back(TraceFromTotal(alt));
565 }
566 }
567
568 const char *tag = long_only_is_current ? "sum" : "longonly";
569 DrawRegionTraces(Form("region_traces_reac%d_altdecode_%s", reac, tag),
570 "strip_sum_scatter", g_beam, g_aa, g_an, 0.6, 1.6,
571 "#DeltaE [a.u.]");
572 DrawRegionMeanTraces(
573 Form("region_mean_traces_reac%d_altdecode_%s", reac, tag),
574 "strip_sum_scatter", g_beam, g_aa, g_an, 0.6, 1.6, "#DeltaE [a.u.]");
575
576 TString txt =
578 Form("/plots/strip_sum_scatter/region_traces_reac%d_altdecode_%s.txt",
579 reac, tag);
580 std::ofstream out(txt.Data());
581 if (out) {
582 out << "# selected under "
583 << (long_only_is_current ? "long-side-only" : "L+R sum")
584 << " decode; alternate rendering is " << tag << std::endl;
585 out << "# per-strip normalisation applied to the summed decode so that the"
586 " summed BEAM median is 1.0 a.u."
587 << std::endl;
588 out << "# strip norm then per class: <long_only> <sum> ratio "
589 "short_frac"
590 << std::endl;
591 const char *cls[3] = {"beam", "aa", "an"};
592 out << "strip norm";
593 for (Int_t c = 0; c < 3; c++)
594 out << " " << cls[c] << "_long " << cls[c] << "_sum " << cls[c]
595 << "_ratio " << cls[c] << "_shortfrac";
596 out << std::endl;
597 for (Int_t s = 1; s <= 16; s++) {
598 out << s << " " << Form("%.6f", norm[s]);
599 for (Int_t c = 0; c < 3; c++) {
600 Double_t sl = 0, ss = 0;
601 Int_t nn = Int_t(srcs[c]->size());
602 for (Int_t k = 0; k < nn; k++) {
603 sl += Double_t((*srcs[c])[k]->long_au[s]);
604 ss += Double_t((*srcs[c])[k]->short_au[s]);
605 }
606 if (nn > 0) {
607 sl /= nn;
608 ss /= nn;
609 }
610 Double_t sum = (sl + ss) * norm[s];
611 out << " " << Form("%.5f", sl) << " " << Form("%.5f", sum) << " "
612 << Form("%.5f", sl > 0 ? sum / sl : 0.0) << " "
613 << Form("%.5f", (sl + ss) > 0 ? ss / (sl + ss) : 0.0);
614 }
615 out << std::endl;
616 }
617 out.close();
618 std::cout << "Wrote " << txt << std::endl;
619 }
620
621 for (Int_t c = 0; c < 3; c++)
622 for (Int_t k = 0; k < Int_t(dsts[c]->size()); k++)
623 delete (*dsts[c])[k];
624}
625
626TGraph *StripSumScatter::TraceFromTotal(const Float_t *total) {
627
628 Double_t td[18];
629 for (Int_t s = 0; s < 18; s++)
630 td[s] = Double_t(total[s]);
632}
633
634void StripSumScatter::DrawRegionTraces(const TString &save_name,
635 const TString &subdir,
636 const std::vector<TGraph *> &beam,
637 const std::vector<TGraph *> &aa,
638 const std::vector<TGraph *> &an,
639 Double_t y_min, Double_t y_max,
640 const char *y_title) {
641 std::lock_guard<std::mutex> lock(g_plot_mutex);
642 Int_t s_lo = Constants::cfg.IGNORE_STRIP_0 ? 1 : 0;
643 Int_t s_hi = Constants::cfg.IGNORE_STRIP_17 ? 16 : 17;
644 TH2F *frame =
645 new TH2F("h_region_trace_frame", Form(";Strip;%s", y_title),
646 s_hi - s_lo + 1, s_lo - 0.5, s_hi + 0.5, 100, y_min, y_max);
647 frame->SetStats(0);
648 TCanvas *c = PlottingUtils::GetConfiguredCanvas(kFALSE);
649 frame->Draw();
650 DrawTraceSet(beam, kGray + 2);
651 DrawTraceSet(aa, kAzure + 2);
652 DrawTraceSet(an, kRed + 1);
653
654 TGraph *p_beam = new TGraph(1);
655 TGraph *p_aa = new TGraph(1);
656 TGraph *p_an = new TGraph(1);
657 TGraph *proxies[3] = {p_beam, p_aa, p_an};
658 Int_t pcol[3] = {kGray + 2, kAzure + 2, kRed + 1};
659 for (Int_t i = 0; i < 3; i++) {
660 proxies[i]->SetPoint(0, -1e9, -1e9);
661 proxies[i]->SetLineColor(pcol[i]);
662 proxies[i]->SetLineWidth(3);
663 }
664 TLegend *leg = PlottingUtils::AddLegend(0.725, 0.875, 0.70, 0.86);
665 leg->AddEntry(p_beam, "Beam", "l");
666 leg->AddEntry(p_aa, "(#alpha,#alpha')", "l");
667 leg->AddEntry(p_an, "(#alpha,n)", "l");
668 leg->Draw();
669
670 PlottingUtils::SaveFigure(c, save_name, subdir, PlotSaveOptions::kLINEAR);
671 delete c;
672}
673
674void StripSumScatter::DrawRegionMeanTraces(const TString &save_name,
675 const TString &subdir,
676 const std::vector<TGraph *> &beam,
677 const std::vector<TGraph *> &aa,
678 const std::vector<TGraph *> &an,
679 Double_t y_min, Double_t y_max,
680 const char *y_title) {
681 std::lock_guard<std::mutex> lock(g_plot_mutex);
682 TH2F *frame = new TH2F("h_region_mean_frame", Form(";Strip;%s", y_title), 18,
683 -0.5, 17.5, 100, y_min, y_max);
684 frame->SetStats(0);
685 frame->SetDirectory(nullptr);
686 TCanvas *c = PlottingUtils::GetConfiguredCanvas(kFALSE);
687 frame->Draw();
688
689 const std::vector<TGraph *> *regions[3] = {&beam, &aa, &an};
690 Int_t colors[3] = {kGray + 2, kAzure + 2, kRed + 1};
691 const char *labels[3] = {"Beam", "(#alpha,#alpha')", "(#alpha,n)"};
692 std::vector<TGraphErrors *> means;
693 TLegend *leg = PlottingUtils::AddLegend(0.725, 0.875, 0.70, 0.86);
694
695 for (Int_t r = 0; r < 3; r++) {
696 const std::vector<TGraph *> &tr = *regions[r];
697 if (tr.empty())
698 continue;
699 Int_t npts = tr[0]->GetN();
700 std::vector<Double_t> mean(npts, 0.0), m2(npts, 0.0);
701 for (Int_t t = 0; t < Int_t(tr.size()); t++) {
702 Double_t *yv = tr[t]->GetY();
703 for (Int_t p = 0; p < npts; p++) {
704 mean[p] += yv[p];
705 m2[p] += yv[p] * yv[p];
706 }
707 }
708 Double_t nt = Double_t(tr.size());
709 TGraphErrors *ge = new TGraphErrors(npts);
710 Double_t *xv = tr[0]->GetX();
711 for (Int_t p = 0; p < npts; p++) {
712 mean[p] /= nt;
713 Double_t var = m2[p] / nt - mean[p] * mean[p];
714 ge->SetPoint(p, xv[p], mean[p]);
715 ge->SetPointError(p, 0.0, var > 0.0 ? TMath::Sqrt(var) : 0.0);
716 }
717 ge->SetLineColor(colors[r]);
718 ge->SetLineWidth(3);
719 ge->SetFillColorAlpha(colors[r], 0.15);
720 ge->Draw("3 SAME"); // +-1 RMS band (all bands first, behind the lines)
721 means.push_back(ge);
722 leg->AddEntry(ge, labels[r], "l");
723 }
724 // Mean lines on top of every band.
725 for (Int_t i = 0; i < Int_t(means.size()); i++)
726 means[i]->Draw("LX SAME"); // mean line, no end caps
727 leg->Draw();
728
729 PlottingUtils::SaveFigure(c, save_name, subdir, PlotSaveOptions::kLINEAR);
730 for (Int_t i = 0; i < Int_t(means.size()); i++)
731 delete means[i];
732 delete leg;
733 delete c;
734 delete frame;
735}
736
737void StripSumScatter::TraceYRange(const std::vector<TGraph *> &beam,
738 const std::vector<TGraph *> &aa,
739 const std::vector<TGraph *> &an,
740 Double_t &y_min, Double_t &y_max) {
741 y_min = std::numeric_limits<Double_t>::max();
742 y_max = -std::numeric_limits<Double_t>::max();
743 const std::vector<TGraph *> *sets[3] = {&beam, &aa, &an};
744 for (Int_t si = 0; si < 3; si++) {
745 const std::vector<TGraph *> &v = *sets[si];
746 for (Int_t i = 0; i < Int_t(v.size()); i++) {
747 Double_t x = 0.0, y = 0.0;
748 for (Int_t k = 0; k < v[i]->GetN(); k++) {
749 v[i]->GetPoint(k, x, y);
750 if (x < 0.5 || x > 16.5)
751 continue;
752 if (y < y_min)
753 y_min = y;
754 if (y > y_max)
755 y_max = y;
756 }
757 }
758 }
759 if (y_min > y_max) { // no in-range points sampled
760 y_min = 0.0;
761 y_max = 1.0;
762 }
763 Double_t pad = 0.05 * (y_max - y_min);
764 if (pad <= 0.0)
765 pad = 1.0;
766 y_min -= pad;
767 y_max += pad;
768}
769
770TCutG *StripSumScatter::PromptCut(TCanvas *c, const char *name,
771 const char *label) {
772 std::cout << " >>> draw the " << label
773 << " region: left-click vertices, double-click to close"
774 << std::endl;
775 c->cd();
776 TCutG *cut = static_cast<TCutG *>(c->WaitPrimitive("CUTG", "CutG"));
777 if (!cut) {
778 std::cerr << " no " << label << " cut drawn" << std::endl;
779 return nullptr;
780 }
781 cut->SetName(name);
782 cut->SetLineColor(kBlack);
783 cut->SetLineWidth(2);
784 return cut;
785}
786
787// Saved region cuts.
788//
789// Drawing the regions by hand is the only way to place them when the reaction
790// population's location is not yet known, but a hand-drawn cut that is not
791// stored makes the run unreproducible: the next pass gets a different polygon
792// and no two results are comparable. Persisting them separates "decide where
793// the region is", which needs a person once, from "apply it", which should be
794// automatic from then on -- and without a DISPLAY.
795void StripSumScatter::SaveRegionCuts(Int_t reac, TCutG *cut_an, TCutG *cut_aa) {
796 RegionCutStore::Save(reac, cut_an, cut_aa);
797}
798
799TCutG *StripSumScatter::LoadRegionCut(const char *name, Int_t reac) {
800 return RegionCutStore::Load(name, reac);
801}
802
803void StripSumScatter::SmoothTrace(const Double_t *in, Double_t *out,
804 Int_t width) {
805 Int_t half = width / 2;
806 for (Int_t s = 0; s < 18; s++) {
807 Int_t lo = TMath::Max(0, s - half);
808 Int_t hi = TMath::Min(17, s + half);
809 Double_t sum = 0.0;
810 for (Int_t t = lo; t <= hi; t++)
811 sum += in[t];
812 out[s] = sum / Double_t(hi - lo + 1);
813 }
814}
815
816// Savitzky-Golay smoothing: 3rd-degree polynomial, half-window of 2
817// (5-point convolution). Uses standard SG coefficients that sum to 1.
818// For a 5-point window with 3rd-degree polynomial, the smoothed value at
819// the center uses coefficients: [-3, 12, 17, 12, -3] / 35.
820// At edges, the window shrinks and coefficients are renormalised.
821void StripSumScatter::SavitzkyGolay(const Double_t *in, Double_t *out) {
822 static const Int_t K = 2; // half-width (5-point window)
823
824 // Standard SG coefficients for 5-point, 3rd-degree polynomial (smoothed
825 // value): These are translation-invariant - same for all center positions.
826 static const Double_t sg_coeff[2 * K + 1] = {
827 -3.0 / 35.0, // coefficient for t = s - 2
828 12.0 / 35.0, // coefficient for t = s - 1
829 17.0 / 35.0, // coefficient for t = s (center)
830 12.0 / 35.0, // coefficient for t = s + 1
831 -3.0 / 35.0 // coefficient for t = s + 2
832 };
833
834 for (Int_t s = 0; s < 18; s++) {
835 Int_t lo = TMath::Max(0, s - K);
836 Int_t hi = TMath::Min(17, s + K);
837 Double_t val = 0.0;
838 Double_t wsum = 0.0;
839
840 // Apply SG coefficients for the positions within the clipped window
841 for (Int_t t = lo; t <= hi; t++) {
842 Int_t offset = t - s + K; // 0..4, position within 5-point window
843 val += sg_coeff[offset] * in[t];
844 wsum += sg_coeff[offset];
845 }
846
847 // Renormalise at edges where window shrinks
848 out[s] = (wsum != 0.0) ? val / wsum : in[s];
849 }
850}
851
852// CFD-style trigger finder: scan left-to-right for the first strip whose
853// beam-subtracted signal exceeds both a fraction of the trace peak and a
854// multiple of the beam sigma. Returns the strip index, or -1 if none fires.
855
856Int_t StripSumScatter::FindTrigger(const Double_t *td, const Double_t *base,
857 Double_t beam_sigma) {
858 const Int_t s_lo = 2;
859 const Int_t s_hi = 16;
860
861 const Double_t frac =
863
864 const Double_t nsigma =
866
867 Double_t peak_signal = -1.0e30;
868
869 for (Int_t s = s_lo; s <= s_hi; s++) {
870 Double_t signal = td[s] - base[s];
871 if (signal > peak_signal)
872 peak_signal = signal;
873 }
874
875 if (peak_signal < nsigma)
876 return -1;
877
878 Double_t thresh = frac * peak_signal;
879
880 for (Int_t s = s_lo; s <= s_hi; s++) {
881 Double_t signal = td[s] - base[s];
882
883 if (signal >= thresh && signal >= nsigma)
884 return s;
885 }
886
887 return -1;
888}
889
890// Build a TGraph trace from a Savitzky-Golay-smoothed set of per-strip totals.
891TGraph *StripSumScatter::SmoothedTraceFromTotal(const Float_t *total) {
892 Double_t td[18], sgd[18];
893 for (Int_t s = 0; s < 18; s++)
894 td[s] = Double_t(total[s]);
895 SavitzkyGolay(td, sgd);
897}
898
899void StripSumScatter::ClusterVarHists(Int_t reac, TCutG *cut_aa, TCutG *cut_an,
900 const TString &subdir) {
901 const Int_t NV = 9;
902 const Int_t NC = 3;
903 const char *vkey[NV] = {"energy", "peak3", "plateau",
904 "tail", "reacstrip", "mult",
905 "trigtaildev", "reacslope3", "beamdev"};
906 const char *vtitle[NV] = {
907 "#Sigma_{all strips}(#DeltaE#minus1) [a.u.]",
908 "#Sigma_{trig#pm1}#DeltaE (0 if no trigger) [a.u.]",
909 "Plateau Excess #Sigma_{trig+1..trig+POST}(#DeltaE#minus1) [a.u.]",
910 "#DeltaE(s17) [a.u.]",
911 "Trigger Strip",
912 "Both-side Multiplicity (strips 1-16)",
913 "|#DeltaE#minusbeam| at trigger + at s17 [a.u.]",
914 "#DeltaE(reac+3) #minus #DeltaE(reac#minus3) [a.u.]",
915 "RMS_{8-17}(#DeltaE#minusbeam) [a.u.]"};
916 const char *clabel[NC] = {"beam", "(a,a')", "(a,n)"};
917
920 const Int_t kClusterSmoothWindow =
922
923 // Per (class, variable) value lists for raw traces.
924 std::vector<Double_t> vals_raw[NC][NV];
925 // Same structure, but cluster variables computed on Savitzky-Golay-smoothed
926 // traces (SG kernel removes the L_odd/R_even sawtooth so peak/onset features
927 // land on the real physical peak rather than an odd strip).
928 std::vector<Double_t> vals_sg[NC][NV];
929 UInt_t bit = (1u << ReacIndex(reac));
930
931 // Per-strip beam baseline = mean over the beam-flat reservoir events. It
932 // carries the L_odd/R_even sawtooth, so subtracting it removes that
933 // systematic exactly before the reaction-onset jump search (better than
934 // blurring it with smoothing).
935 Double_t base[18];
936 for (Int_t s = 0; s < 18; s++)
937 base[s] = 0.0;
938 Long64_t nbeam = 0;
939 for (Int_t k = 0; k < Int_t(m_reservoir.size()); k++)
940 if (m_reservoir[k].beam_flat) {
941 for (Int_t s = 0; s < 18; s++)
942 base[s] += Double_t(m_reservoir[k].total[s]);
943 nbeam++;
944 }
945 for (Int_t s = 0; s < 18; s++)
946 base[s] = (nbeam > 0) ? base[s] / Double_t(nbeam) : 1.0;
947
948 // Pooled beam-noise RMS = sqrt(mean over beam-flat events and all strips of
949 // (total - base)^2). The reaction-onset threshold is this many sigma (an
950 // N-sigma discriminator), matching the Python pipeline -- so on flat beam
951 // the excess does NOT cross on average and the event gets NO trigger.
952 Double_t beam_sumsq = 0.0;
953 Long64_t beam_npt = 0;
954 for (Int_t k = 0; k < Int_t(m_reservoir.size()); k++)
955 if (m_reservoir[k].beam_flat)
956 for (Int_t s = 0; s < 18; s++) {
957 Double_t d = Double_t(m_reservoir[k].total[s]) - base[s];
958 beam_sumsq += d * d;
959 beam_npt++;
960 }
961 Double_t beam_sigma =
962 (beam_npt > 0) ? TMath::Sqrt(beam_sumsq / beam_npt) : 0.0;
963
964 // Count triggers over ALL reservoir events (not just classified subset) so
965 // the numbers are directly comparable to the Python pipeline.
966 Long64_t triggered = 0, no_trigger = 0;
967 Double_t td_all[18];
968 for (Int_t k = 0; k < Int_t(m_reservoir.size()); k++) {
969 for (Int_t s = 0; s < 18; s++)
970 td_all[s] = Double_t(m_reservoir[k].total[s]);
971 if (FindTrigger(td_all, base, beam_sigma) >= 0)
972 triggered++;
973 else
974 no_trigger++;
975 }
976 const Double_t reac_onset_gate =
978 const Double_t cf_frac =
980 std::cout << " beam reference: mean+RMS of " << nbeam
981 << " pure-beam events (fitted s0,s1 & s16,s17 ellipses); "
982 << Form("noise sigma=%.4f", beam_sigma) << std::endl;
983 std::cout << Form(" reaction onset: gate %g-sigma = %.4f, CF fraction %g; "
984 "triggered %lld of %lld (no trigger: %lld)",
985 Constants::cfg.STRIP_SUM_SCATTER_CONFIG.TRIGGER_NSIGMA,
986 reac_onset_gate, cf_frac, Long64_t(triggered),
987 Long64_t(m_reservoir.size()), Long64_t(no_trigger))
988 << std::endl;
989
990 for (Int_t k = 0; k < Int_t(m_reservoir.size()); k++) {
991 const TraceEvt &e = m_reservoir[k];
992 Double_t td[18];
993 for (Int_t s = 0; s < 18; s++)
994 td[s] = Double_t(e.total[s]);
995 Int_t cls = -1;
996 if (e.beam_flat)
997 cls = 0;
998 else if (e.reac_mask & bit) {
999 Double_t x = 0.0, y = 0.0;
1000 PlaneXY(td, reac, x, y);
1001 if (cut_aa && cut_aa->IsInside(x, y))
1002 cls = 1;
1003 else if (cut_an && cut_an->IsInside(x, y))
1004 cls = 2;
1005 }
1006 if (cls < 0)
1007 continue;
1008 // The same five variables the blind clustering uses (normed total, beam
1009 // at 1 per strip; guards 0/17 included).
1010 Double_t energy = 0.0;
1011 for (Int_t s = 0; s < 18; s++) {
1012 energy += td[s] - 1.0;
1013 }
1014
1015 Int_t trigger_strip = FindTrigger(td, base, beam_sigma);
1016 Bool_t has_trig = (trigger_strip >= 0);
1017 // Plateau excess: beam-subtracted sum over a sliding window that tracks
1018 // the trigger (reacstrip+1 .. reacstrip+PLATEAU_POST). 0 when no trigger.
1019 // Out-of-range strips are dropped from the sum.
1020 const Int_t kPlateauPost =
1022 Double_t plateau = 0.0;
1023 if (has_trig)
1024 for (Int_t d = 1; d <= kPlateauPost; d++) {
1025 Int_t s = trigger_strip + d;
1026 if (s >= 0 && s < 18)
1027 plateau += td[s] - 1.0;
1028 }
1029 // peak-3 sum centered on the TRIGGER strip (not the argmax). 0 when there
1030 // is no trigger, which is the discriminator: flat beam scores 0.
1031 Double_t peak3 = 0.0;
1032 if (has_trig)
1033 for (Int_t s = trigger_strip - 1; s <= trigger_strip + 1; s++)
1034 if (s >= 0 && s < 18)
1035 peak3 += td[s];
1036 // |deviation from beam| at the TRIGGER strip plus at the end strip s17 --
1037 // the (a,n) signature is a rise at the trigger AND a collapse at s17.
1038 Double_t trigtaildev =
1039 has_trig ? TMath::Abs(td[trigger_strip] - base[trigger_strip]) +
1040 TMath::Abs(td[17] - base[17])
1041 : 0.0;
1042 // Slopes ACROSS the trigger: dE(reac+n) - dE(reac-n). reac+-n share parity
1043 // so the L_odd/R_even sawtooth cancels. Only valid (and filled) when there
1044 // is a trigger AND both endpoints are in range (symmetric, no clamping).
1045 Bool_t ok3 =
1046 has_trig && (trigger_strip - 3 >= 0) && (trigger_strip + 3 <= 17);
1047 Double_t reacslope3 =
1048 ok3 ? td[trigger_strip + 3] - td[trigger_strip - 3] : 0.0;
1049
1050 // How beam-like the BACK HALF (strips 8-17) is: RMS deviation of the trace
1051 // from the beam baseline over those strips. Subtracting base[] removes the
1052 // L/R sawtooth, so this is clean; LOW = beam-like (flat at beam level),
1053 // high for a reaction's plateau/collapse or the elevation of pileup.
1054 // Amplitude-aware (NOT max-normalized like the template-prune residual):
1055 // the beam has a fixed level, so a flat-but-elevated trace must not read as
1056 // beam. No trigger needed (fixed window), so always filled.
1057 Double_t beamdev = 0.0;
1058 Int_t n_bl = 0;
1059 for (Int_t s = 8; s <= 17; s++) {
1060 Double_t d = td[s] - base[s];
1061 beamdev += d * d;
1062 n_bl++;
1063 }
1064 beamdev = TMath::Sqrt(beamdev / Double_t(n_bl));
1065 Double_t v[NV] = {energy,
1066 peak3,
1067 plateau,
1068 td[17], // raw end strip (was td[17] - 1.0)
1069 Double_t(trigger_strip),
1070 Double_t(e.both_mult),
1071 trigtaildev,
1072 reacslope3,
1073 beamdev};
1074 // peak3 is filled even with no trigger (it scores 0 -- the discriminator).
1075 // The other trigger-centered vars are skipped when there is no trigger
1076 // (reacstrip) or the symmetric window runs off an edge (slopes /
1077 // jaggedness).
1078 Bool_t vok[NV] = {kTRUE, kTRUE, kTRUE, kTRUE, kTRUE,
1079 kTRUE, has_trig, ok3, kTRUE};
1080 for (Int_t iv = 0; iv < NV; iv++)
1081 if (vok[iv])
1082 vals_raw[cls][iv].push_back(v[iv]);
1083
1084 // Savitzky-Golay-smoothed trace: recompute trigger and cluster variables on
1085 // the SG-filtered copy. This removes the L_odd/R_even sawtooth so onset /
1086 // peak / slope features land on the real physical structure.
1087 Double_t sgd[18];
1088 SavitzkyGolay(td, sgd);
1089 Double_t energy_sg = 0.0;
1090 for (Int_t s = 0; s < 18; s++)
1091 energy_sg += sgd[s] - 1.0;
1092 Double_t ex_sg[18], sm_ex_sg[18];
1093 for (Int_t s = 0; s < 18; s++)
1094 ex_sg[s] = sgd[s] - base[s];
1095 SmoothTrace(ex_sg, sm_ex_sg, kClusterSmoothWindow);
1096 Int_t reacstrip_sg = FindTrigger(sgd, base, beam_sigma);
1097 Bool_t has_trig_sg = (reacstrip_sg >= 0);
1098 // Plateau excess on SG trace: sliding window over reacstrip_sg+1 ..
1099 // reacstrip_sg+PLATEAU_POST. 0 when no trigger; out-of-range strips
1100 // dropped.
1101 Double_t plateau_sg = 0.0;
1102 if (has_trig_sg)
1103 for (Int_t d = 1; d <= kPlateauPost; d++) {
1104 Int_t s = reacstrip_sg + d;
1105 if (s >= 0 && s < 18)
1106 plateau_sg += sgd[s] - 1.0;
1107 }
1108 Double_t peak3_sg = 0.0;
1109 if (has_trig_sg)
1110 for (Int_t s = reacstrip_sg - 1; s <= reacstrip_sg + 1; s++)
1111 if (s >= 0 && s < 18)
1112 peak3_sg += sgd[s];
1113 Double_t trigtaildev_sg =
1114 has_trig_sg ? TMath::Abs(sgd[reacstrip_sg] - base[reacstrip_sg]) +
1115 TMath::Abs(sgd[17] - base[17])
1116 : 0.0;
1117 Bool_t ok3_sg =
1118 has_trig_sg && (reacstrip_sg - 3 >= 0) && (reacstrip_sg + 3 <= 17);
1119 Double_t reacslope3_sg =
1120 ok3_sg ? sgd[reacstrip_sg + 3] - sgd[reacstrip_sg - 3] : 0.0;
1121 Double_t beamdev_sg = 0.0;
1122 Int_t n_bl_sg = 0;
1123 for (Int_t s = 8; s <= 17; s++) {
1124 Double_t d = sgd[s] - base[s];
1125 beamdev_sg += d * d;
1126 n_bl_sg++;
1127 }
1128 beamdev_sg = TMath::Sqrt(beamdev_sg / Double_t(n_bl_sg));
1129 Double_t v_sg[NV] = {energy_sg,
1130 peak3_sg,
1131 plateau_sg,
1132 sgd[17],
1133 Double_t(reacstrip_sg),
1134 Double_t(e.both_mult),
1135 trigtaildev_sg,
1136 reacslope3_sg,
1137 beamdev_sg};
1138 Bool_t vok_sg[NV] = {kTRUE, kTRUE, kTRUE, kTRUE, kTRUE,
1139 kTRUE, has_trig_sg, ok3_sg, kTRUE};
1140 for (Int_t iv = 0; iv < NV; iv++)
1141 if (vok_sg[iv])
1142 vals_sg[cls][iv].push_back(v_sg[iv]);
1143 }
1144
1145 std::cout << "cluster-var hists (reac " << reac
1146 << "): beam=" << vals_raw[0][0].size()
1147 << " (a,a')=" << vals_raw[1][0].size()
1148 << " (a,n)=" << vals_raw[2][0].size() << std::endl;
1149
1150 std::vector<Int_t> colors = PlottingUtils::GetDefaultColors();
1151 // Two smoothing passes: raw traces, then Savitzky-Golay smoothed.
1152 const Int_t kNP = 2;
1153 const char *pass_label[kNP] = {"raw", "sg"};
1154
1155 for (Int_t ip = 0; ip < kNP; ip++) {
1156 if (ip == 1 && Constants::cfg.STRIP_SUM_SCATTER_CONFIG.SKIP_SAVGOL_PLOTS)
1157 continue;
1158 std::vector<Double_t>(*vals)[NC][NV] = (ip == 0) ? &vals_raw : &vals_sg;
1159 for (Int_t iv = 0; iv < NV; iv++) {
1160 Double_t lo = 1.0e30, hi = -1.0e30;
1161 Double_t mean[NC] = {0.0, 0.0, 0.0};
1162 for (Int_t ic = 0; ic < NC; ic++) {
1163 for (Int_t m = 0; m < Int_t((*vals)[ic][iv].size()); m++) {
1164 lo = TMath::Min(lo, (*vals)[ic][iv][m]);
1165 hi = TMath::Max(hi, (*vals)[ic][iv][m]);
1166 mean[ic] += (*vals)[ic][iv][m];
1167 }
1168 if (!(*vals)[ic][iv].empty())
1169 mean[ic] /= Double_t((*vals)[ic][iv].size());
1170 }
1171 if (ip == 0) {
1172 std::cout << " [" << pass_label[ip] << "] " << vkey[iv]
1173 << ": mean beam=" << mean[0] << " (a,a')=" << mean[1]
1174 << " (a,n)=" << mean[2] << " [range " << lo << ".." << hi
1175 << "]" << std::endl;
1176 }
1177 Int_t nbins = 80;
1178 if (iv == 4) { // reaction strip: integer bins 0..17
1179 lo = -1.5;
1180 hi = 17.5;
1181 nbins = 19;
1182 } else if (iv == 5) { // both-channel multiplicity: integer bins 0..16
1183 lo = -0.5;
1184 hi = 16.5;
1185 nbins = 17;
1186 } else {
1187 if (!(hi > lo)) { // constant -> give it a drawable range
1188 lo -= 0.5;
1189 hi += 0.5;
1190 }
1191 Double_t pad = 0.05 * (hi - lo);
1192 lo -= pad;
1193 hi += pad;
1194 }
1195
1196 TCanvas *c = PlottingUtils::GetConfiguredCanvas(kTRUE);
1197 TString axis = Form(";%s;Counts", vtitle[iv]);
1198 std::vector<TH1F *> hs;
1199 Double_t ymax = 0.0;
1200 for (Int_t ic = 0; ic < NC; ic++) {
1201 TH1F *h = new TH1F(
1202 Form("h_cv_%s_%s_c%d_r%d", vkey[iv], pass_label[ip], ic, reac),
1203 axis, nbins, lo, hi);
1204 h->SetDirectory(nullptr);
1205 for (Int_t m = 0; m < Int_t((*vals)[ic][iv].size()); m++)
1206 h->Fill((*vals)[ic][iv][m]);
1207
1208 PlottingUtils::ConfigureHistogram(h, colors[ic % Int_t(colors.size())],
1209 axis);
1210 h->SetStats(0);
1211 ymax = TMath::Max(ymax, h->GetMaximum());
1212 hs.push_back(h);
1213 }
1214 if (ymax <= 0.0)
1215 ymax = 1.0;
1216 TLegend *leg = PlottingUtils::AddLegend(0.775, 0.875, 0.70, 0.86);
1217
1218 for (Int_t ic = 0; ic < NC; ic++) {
1219 if (ic == 0) {
1220 hs[0]->SetMaximum(3.0 * ymax);
1221 hs[0]->SetMinimum(1.0e-1);
1222 hs[0]->Draw("HIST");
1223 } else {
1224 hs[ic]->Draw("HIST SAME");
1225 }
1226 leg->AddEntry(hs[ic], clabel[ic], "l");
1227 }
1228 leg->Draw();
1229
1230 TString sub_subdir = subdir + "/clusters_" + pass_label[ip];
1231
1232 PlottingUtils::SaveFigure(
1233 c, Form("cluster_var_%s_%s_reac%d", vkey[iv], pass_label[ip], reac),
1234 sub_subdir, PlotSaveOptions::kLOG);
1235 for (Int_t m = 0; m < Int_t(hs.size()); m++)
1236 delete hs[m];
1237 delete c;
1238 }
1239 }
1240}
1241
1242TString StripSumScatter::BuildFingerprint(const std::vector<Int_t> &run_order,
1243 std::map<Int_t, TChain *> &chains) {
1244 const Int_t kReacMin =
1246 const Int_t kReacMax =
1248 const Double_t kReacJumpNSigma =
1250 const Double_t kReacJumpMax =
1252 const Int_t kSmoothHiStrip =
1254 const Double_t kSmoothMaxStep =
1256 const Double_t kEndStripMax =
1258
1259 const Int_t kGateStripX =
1261 const Int_t kGateStripY =
1263 const Double_t kGateNSigmaX =
1265 const Double_t kGateNSigmaY =
1267 const Int_t kGateBins = Constants::cfg.STRIP_SUM_SCATTER_CONFIG.GATE_BINS;
1268 const Double_t kGateMin = Constants::cfg.STRIP_SUM_SCATTER_CONFIG.GATE_MIN;
1269 const Double_t kGateMax = Constants::cfg.STRIP_SUM_SCATTER_CONFIG.GATE_MAX;
1270
1271 const Int_t kXBins = Constants::cfg.STRIP_SUM_SCATTER_CONFIG.XBINS;
1272 const Int_t kYBins = Constants::cfg.STRIP_SUM_SCATTER_CONFIG.YBINS;
1273
1274 // Two parts. Before the bar: everything that decides which events are
1275 // tagged and kept, so a change there means a pass over the events files.
1276 // After it: only what is built from the tagged events, all of which the
1277 // reservoir keeps, so a cache whose tagging matches but whose plane does
1278 // not is re-projected from its reservoir in a minute rather than refilled.
1279 TString s = Form(
1280 "v19 reac[%d,%d] bmult[%d,%d] jump[%.2fsig,%.3f] smooth=%d,%d "
1281 "step=%.3f s17=%.3f gate[s%d,s%d,%.2f,%.2f,%d,%.3f,%.3f] par=%.3f",
1282 kReacMin, kReacMax, Constants::cfg.STRIP_SUM_SCATTER_CONFIG.BOTH_MULT_MAX,
1284 kReacJumpNSigma, kReacJumpMax,
1285 Int_t(Constants::cfg.STRIP_SUM_SCATTER_CONFIG.REQUIRE_SMOOTHNESS),
1286 kSmoothHiStrip, kSmoothMaxStep, kEndStripMax, kGateStripX, kGateStripY,
1287 kGateNSigmaX, kGateNSigmaY, kGateBins, kGateMin, kGateMax,
1289 // The gate resolves through the measured noise, so the thresholds actually
1290 // applied are stamped too: a drift in the noise refills.
1291 s += " jmin[";
1292 for (Int_t reac = kReacMin; reac <= kReacMax; reac++)
1293 s += Form("%s%.4f", reac == kReacMin ? "" : ",", JumpMin(reac));
1294 s += "]";
1295 // The upstream-beam precondition changes which events are tagged; like the
1296 // jump gate it resolves through measured noise, so the tolerances are
1297 // stamped.
1298 s += Form(" up=%d,%.2fsig[",
1299 Int_t(Constants::cfg.STRIP_SUM_SCATTER_CONFIG
1300 .REQUIRE_BEAM_UPSTREAM_OF_REAC),
1301 Constants::cfg.STRIP_SUM_SCATTER_CONFIG.BEAM_UPSTREAM_NSIGMA);
1302 for (Int_t strip = 1; strip < kReacMax; strip++)
1303 s += Form("%s%.4f", strip == 1 ? "" : ",",
1304 Constants::cfg.STRIP_SUM_SCATTER_CONFIG.BEAM_UPSTREAM_NSIGMA *
1305 StripSigma(strip));
1306 s += "]";
1307 // Active beam gates (also keyed by cache filename, but folded in here too so
1308 // a mismatch never silently reuses a stale same-named cache).
1309 std::vector<GateSpec> gates = ActiveGates();
1310 for (Int_t i = 0; i < Int_t(gates.size()); i++)
1311 s += Form(" g[s%d,s%d]", gates[i].sx, gates[i].sy);
1312
1313 // Display windows are deliberately absent: they no longer change what is
1314 // built, so retuning them must not invalidate the cache.
1315 for (Int_t i = 0; i < Int_t(run_order.size()); i++) {
1316 Int_t run = run_order[i];
1317 s += Form(" r%d:%lld", run, chains[run]->GetEntries());
1318 }
1319
1320 // The built quantity: build range, binning, the x range and each strip's
1321 // y window, so a change to the window rule re-projects.
1322 TString plane =
1323 Form("buildx[%.3f,%.3f] buildy[%.3f,%.3f] bins[%d,%d] x[%d,%d]",
1326 Constants::cfg.STRIP_SUM_SCATTER_CONFIG.X_LO,
1328 plane += " y";
1329 for (Int_t reac = kReacMin; reac <= kReacMax; reac++)
1330 plane +=
1331 Form("%s%d-%d", reac == kReacMin ? "[" : ",", YLoOf(reac), YHiOf(reac));
1332 plane += "]";
1333 return s + " | " + plane;
1334}
1335
1336// The tagging half of a fingerprint; a cache from before the split has no
1337// bar and never matches.
1338static TString TagPart(const TString &fingerprint) {
1339 const Ssiz_t bar = fingerprint.Index(" | ");
1340 return bar < 0 ? TString("") : TString(fingerprint(0, bar));
1341}
1342
1343// Per-reaction-strip y-axis bounds straight from
1344// Constants::cfg.STRIP_SUM_SCATTER_CONFIG.Y_DISPLAY_RANGE (tunable per dataset,
1345// per strip); strips absent from the map fall back to
1346// Y_DISPLAY_MIN/Y_DISPLAY_MAX. x stays fixed (strip-independent).
1347void StripSumScatter::YBounds(Double_t *y_lo, Double_t *y_hi) {
1348 const Int_t kReacMin =
1350 const Int_t kReacMax =
1352 for (Int_t reac = kReacMin; reac <= kReacMax; reac++) {
1353 Int_t ri = reac - kReacMin;
1354 std::map<Int_t, std::pair<Double_t, Double_t>>::const_iterator it =
1356 if (it != Constants::cfg.STRIP_SUM_SCATTER_CONFIG.Y_DISPLAY_RANGE.end()) {
1357 y_lo[ri] = it->second.first;
1358 y_hi[ri] = it->second.second;
1359 } else {
1362 }
1363 }
1364}
1365
1366TString StripSumScatter::PrettyLabel(const TString &tag) {
1367 TString base = RemixSim::TagWithoutStrip(tag);
1368 base.ReplaceAll("_eres", "");
1369 if (base == "aa")
1370 return "(#alpha,#alpha')";
1371 if (base == "an")
1372 return "(#alpha,n)";
1373 if (base == "beam")
1374 return "Beam";
1375 return base;
1376}
1377
1378// Per-strip sim normalization gains: read the sim beam file and, for each
1379// strip, average the (unit-gain) per-strip beam total, then set gain[s] = 1 /
1380// mean[s] so every strip's sim beam lands on 1 a.u. This flattens the sim beam
1381// the SAME way the per-channel data normalization flattens the experimental
1382// beam
1383// -- a single global factor would not, since it preserves the sim's per-strip
1384// structure. Strips with no beam signal keep gain 0 (drop out like an
1385// uncalibrated channel).
1386Bool_t StripSumScatter::SimBeamGains(Double_t *gain) {
1387 const Long64_t kSampleMaxPoints =
1389
1390 for (Int_t s = 0; s < 18; s++)
1391 gain[s] = 0.0;
1392 // Reference the ERES beam file -- the same file type as the eres populations
1393 // plotted in SimOverlay/SimTraceOverlay -- so the normalized eres beam lands
1394 // exactly on 1. Falls back to the non-eres SIM_BEAM_FILE if no eres beam
1395 // control file is present.
1396 TString file;
1397 std::vector<RemixSim::SimFileSpec> specs = RemixSim::BuildFileSpecs();
1398 for (Int_t i = 0; i < Int_t(specs.size()); i++) {
1399 if (!RemixSim::IsEresTag(specs[i].tag))
1400 std::cout << "No eres sim file for " << specs[i].tag << ", using standard"
1401 << std::endl;
1402
1403 TString base = RemixSim::TagWithoutStrip(specs[i].tag);
1404 base.ReplaceAll("_eres", "");
1405 if (base == "beam") {
1406 file = RemixSim::SimRootPath(specs[i]);
1407 break;
1408 }
1409 }
1410 if (file.Length() == 0)
1411 file =
1412 Paths::DatasetDir() + "/sim_root_files/" + Constants::cfg.SIM_BEAM_FILE;
1413 TFile *f = IO::OpenForReading(file);
1414 if (!f || f->IsZombie()) {
1415 std::cerr << "strip-sum-scatter: cannot open sim beam file " << file
1416 << "; sim overlay stays in raw sim units." << std::endl;
1417 if (f)
1418 delete f;
1419 return kFALSE;
1420 }
1421 TTree *t = static_cast<TTree *>(f->Get("events_MeV"));
1422 if (!t) {
1423 std::cerr << "strip-sum-scatter: no events_MeV tree in sim beam file "
1424 << file << "; sim overlay stays in raw sim units." << std::endl;
1425 f->Close();
1426 delete f;
1427 return kFALSE;
1428 }
1429 Float_t left[18] = {0}, right[18] = {0};
1430 t->SetBranchAddress("Left_0_17_dE", left);
1431 t->SetBranchAddress("RightdE", right);
1432 Long64_t n = t->GetEntries();
1433 Long64_t stride = FileSet::SampleStride(n, kSampleMaxPoints);
1434 Double_t sum[18] = {0};
1435 Long64_t cnt[18] = {0};
1436 // Unit gains so SimTotal yields the raw per-strip beam total (IGNORE_SHORT
1437 // aware), which is exactly the quantity these gains will later normalize.
1438 Double_t unit[18];
1439 for (Int_t s = 0; s < 18; s++)
1440 unit[s] = 1.0;
1441 for (Long64_t j = 0; j < n; j += stride) {
1442 t->GetEntry(j);
1443 Double_t total[18];
1444 SimTotal(left, right, unit, total);
1445 for (Int_t s = 0; s < 18; s++)
1446 if (total[s] > 0.0) {
1447 sum[s] += total[s];
1448 cnt[s]++;
1449 }
1450 }
1451 f->Close();
1452 delete f;
1453 Int_t n_set = 0;
1454 for (Int_t s = 0; s < 18; s++) {
1455 if (cnt[s] > 0 && sum[s] > 0.0) {
1456 gain[s] = 1.0 / (sum[s] / Double_t(cnt[s]));
1457 n_set++;
1458 }
1459 }
1460 if (n_set == 0)
1461 return kFALSE;
1462 std::cout << "strip-sum-scatter: sim per-strip beam normalization to 1 a.u. ("
1463 << n_set << " strips)." << std::endl;
1464 return kTRUE;
1465}
1466
1467void StripSumScatter::SimTotal(const Float_t *left, const Float_t *right,
1468 const Double_t *gain, Double_t *total) {
1469 for (Int_t s = 0; s < 18; s++)
1470 total[s] = gain[s] * (Double_t(left[s]) + Double_t(right[s]));
1471 if (Constants::cfg.IGNORE_SHORT_STRIPS)
1472 for (Int_t s = 1; s <= 16; s++)
1473 total[s] =
1474 gain[s] * ((s % 2) != 0 ? Double_t(left[s]) : Double_t(right[s]));
1475}
1476
1477TGraph *StripSumScatter::SimPopScatter(const TString &file, Int_t reac,
1478 const Double_t *gain,
1479 Long64_t max_points) {
1480 const Int_t kXLo = Constants::cfg.STRIP_SUM_SCATTER_CONFIG.X_LO;
1481 const Int_t kXHi = Constants::cfg.STRIP_SUM_SCATTER_CONFIG.X_HI;
1482
1483 TFile *f = IO::OpenForReading(file);
1484 if (!f || f->IsZombie()) {
1485 if (f)
1486 delete f;
1487 return nullptr;
1488 }
1489 TTree *t = static_cast<TTree *>(f->Get("events_MeV"));
1490 if (!t) {
1491 std::cerr << " no events_MeV tree in " << file << std::endl;
1492 f->Close();
1493 delete f;
1494 return nullptr;
1495 }
1496 Int_t y_lo = reac + 1;
1497 Int_t y_hi = TMath::Min(reac + 6, 17);
1498 Float_t left[18] = {0}, right[18] = {0};
1499 t->SetBranchAddress("Left_0_17_dE", left);
1500 t->SetBranchAddress("RightdE", right);
1501 Long64_t n = t->GetEntries();
1502 Long64_t stride = FileSet::SampleStride(n, max_points);
1503 TGraph *g = new TGraph();
1504 Long64_t k = 0;
1505 for (Long64_t j = 0; j < n; j += stride) {
1506 t->GetEntry(j);
1507 Double_t total[18];
1508 SimTotal(left, right, gain, total);
1509 Double_t x = SumRange(total, kXLo, kXHi);
1510 Double_t y = SumRange(total, y_lo, y_hi);
1511 if (x > 0.0)
1512 g->SetPoint(k++, x, y);
1513 }
1514 g->Set(k);
1515 f->Close();
1516 delete f;
1517 return g;
1518}
1519
1520// Up to max_traces per-strip dE-profile traces, stride-sampled across one sim
1521// file. Sim energies are arbitrary-unit floats; total[s] is the per-strip
1522// normalized gain[s]*(left[s] + right[s]) so the traces share the data's axis
1523// (and the sim beam is flat at NORM, like the data).
1524std::vector<TGraph *> StripSumScatter::SimPopTraces(const TString &file,
1525 const Double_t *gain,
1526 Long64_t max_traces) {
1527 std::vector<TGraph *> traces;
1528 TFile *f = IO::OpenForReading(file);
1529 if (!f || f->IsZombie()) {
1530 if (f)
1531 delete f;
1532 return traces;
1533 }
1534 TTree *t = static_cast<TTree *>(f->Get("events_MeV"));
1535 if (!t) {
1536 f->Close();
1537 delete f;
1538 return traces;
1539 }
1540 Float_t left[18] = {0}, right[18] = {0};
1541 t->SetBranchAddress("Left_0_17_dE", left);
1542 t->SetBranchAddress("RightdE", right);
1543 Long64_t n = t->GetEntries();
1544 Long64_t stride = FileSet::SampleStride(n, max_traces);
1545 for (Long64_t j = 0; j < n && Int_t(traces.size()) < max_traces;
1546 j += stride) {
1547 t->GetEntry(j);
1548 Double_t total[18];
1549 SimTotal(left, right, gain, total);
1550 traces.push_back(EventsSummary::BuildTraceFromTotals(total));
1551 }
1552 f->Close();
1553 delete f;
1554 return traces;
1555}
1556
1557// Per reaction strip, overlay TRACES_PER_CLASS sampled per-strip traces of each
1558// sim population in the experimental DrawRegionTraces style (beam grey, (a,a')
1559// azure, (a,n) red). The beam reference is the same for every strip. Sampled
1560// fresh each run (40 traces/file is trivial).
1561void StripSumScatter::SimTraceOverlay() {
1562 const Int_t kReacMin =
1564 const Int_t kReacMax =
1566 const Int_t kTracesPerRegion =
1568
1569 std::vector<RemixSim::SimFileSpec> specs = RemixSim::BuildFileSpecs();
1570 if (specs.empty())
1571 return;
1572 // One file per class and strip, the _eres twin preferred where it exists
1573 // (alphabetical order would otherwise leave the plain file in the map).
1574 std::map<Int_t, TString> aa_file, an_file; // reaction strip -> sim file
1575 std::map<Int_t, Bool_t> aa_eres, an_eres;
1576 std::vector<TString> beam_files;
1577 Bool_t beam_eres = kFALSE;
1578 for (Int_t i = 0; i < Int_t(specs.size()); i++) {
1579 TString base = RemixSim::TagWithoutStrip(specs[i].tag);
1580 base.ReplaceAll("_eres", "");
1581 TString file = RemixSim::SimRootPath(specs[i]);
1582 Int_t strip = RemixSim::ReactionStripOf(specs[i].tag);
1583 const Bool_t eres = RemixSim::IsEresTag(specs[i].tag);
1584 if (base == "beam") {
1585 if (beam_eres && !eres)
1586 continue;
1587 if (eres && !beam_eres)
1588 beam_files.clear();
1589 beam_files.push_back(file);
1590 beam_eres = beam_eres || eres;
1591 } else if (strip >= kReacMin && strip <= kReacMax) {
1592 std::map<Int_t, TString> &files = base == "aa" ? aa_file : an_file;
1593 std::map<Int_t, Bool_t> &have_eres = base == "aa" ? aa_eres : an_eres;
1594 if (base != "aa" && base != "an")
1595 continue;
1596 if (files.count(strip) && (have_eres[strip] || !eres))
1597 continue;
1598 files[strip] = file;
1599 have_eres[strip] = eres;
1600 }
1601 }
1602
1603 Double_t gain[18];
1604 if (!SimBeamGains(gain))
1605 for (Int_t s = 0; s < 18; s++)
1606 gain[s] = 1.0;
1607
1608 std::vector<TGraph *> beam_traces;
1609 for (Int_t i = 0; i < Int_t(beam_files.size()) &&
1610 Int_t(beam_traces.size()) < kTracesPerRegion;
1611 i++) {
1612 std::vector<TGraph *> t = SimPopTraces(
1613 beam_files[i], gain, kTracesPerRegion - Int_t(beam_traces.size()));
1614 for (Int_t k = 0; k < Int_t(t.size()); k++)
1615 beam_traces.push_back(t[k]);
1616 }
1617
1618 for (Int_t r = kReacMin; r <= kReacMax; r++) {
1619 std::vector<TGraph *> aa_traces, an_traces;
1620 if (aa_file.find(r) != aa_file.end())
1621 aa_traces = SimPopTraces(aa_file[r], gain, kTracesPerRegion);
1622 if (an_file.find(r) != an_file.end())
1623 an_traces = SimPopTraces(an_file[r], gain, kTracesPerRegion);
1624 if (aa_traces.empty() && an_traces.empty())
1625 continue;
1626 DrawRegionTraces(Form("sim_region_traces_reac%d", r), "sim_scatter",
1627 beam_traces, aa_traces, an_traces, 0.6, 1.6,
1628 "#DeltaE [a.u.]");
1629 for (Int_t i = 0; i < Int_t(aa_traces.size()); i++)
1630 delete aa_traces[i];
1631 for (Int_t i = 0; i < Int_t(an_traces.size()); i++)
1632 delete an_traces[i];
1633 }
1634 for (Int_t i = 0; i < Int_t(beam_traces.size()); i++)
1635 delete beam_traces[i];
1636}
1637
1638// Fingerprint of the sim inputs + window geometry: each eres file's size+mtime
1639// (cheap, no open) plus the reaction-strip range and x window. Regenerating the
1640// sim (new mtimes) or changing the windows invalidates the cached overlay.
1641TString StripSumScatter::SimFingerprint(
1642 const std::vector<RemixSim::SimFileSpec> &specs) {
1643 // v2: sim is per-strip normalized via SimBeamGains(); the gain source (the
1644 // eres beam file) is stamped by the per-spec loop below (it covers every
1645 // eres file, including beam_eres).
1646 // v3: normalization hardcoded to 1 a.u. (NORM_MUSIC_MEV removed); bump so
1647 // overlays cached at other norms rebuild.
1648 const Int_t kReacMin =
1650 const Int_t kReacMax =
1652 const Int_t kXLo = Constants::cfg.STRIP_SUM_SCATTER_CONFIG.X_LO;
1653 const Int_t kXHi = Constants::cfg.STRIP_SUM_SCATTER_CONFIG.X_HI;
1654
1655 // v4: one population per class and strip (the eres twin preferred), so
1656 // caches that held every population twice are rebuilt.
1657 TString s = Form("v4 reac[%d,%d] x[%d,%d]", kReacMin, kReacMax, kXLo, kXHi);
1658 for (Int_t i = 0; i < Int_t(specs.size()); i++) {
1659 TString f = RemixSim::SimRootPath(specs[i]);
1660 Long_t id = 0, flags = 0, mtime = 0;
1661 Long64_t size = -1;
1662 if (gSystem->GetPathInfo(f, &id, &size, &flags, &mtime) != 0) {
1663 size = -1;
1664 mtime = 0;
1665 }
1666 s += Form(" %s:%lld:%ld", specs[i].tag.Data(), size, mtime);
1667 }
1668 return s;
1669}
1670
1671// Reload cached sim scatter graphs (grouped by reaction strip; each graph's
1672// title holds its population label) if the fingerprint matches. Caller owns the
1673// returned graphs.
1674Bool_t StripSumScatter::LoadSimCache(
1675 const TString &fp, std::map<Int_t, std::vector<TGraph *>> &by_strip) {
1676 TString full = IO::GetRootFilesBaseDir() + TString("/") +
1677 "StripSumScatter_simcache.root";
1678 if (gSystem->AccessPathName(full))
1679 return kFALSE;
1680 TFile *f = IO::OpenForReading("StripSumScatter_simcache.root");
1681 if (!f || f->IsZombie()) {
1682 if (f)
1683 delete f;
1684 return kFALSE;
1685 }
1686 TNamed *cfp = static_cast<TNamed *>(f->Get("sim_fingerprint"));
1687 if (!cfp || fp != cfp->GetTitle()) {
1688 f->Close();
1689 delete f;
1690 return kFALSE;
1691 }
1692 TIter next(f->GetListOfKeys());
1693 TKey *key;
1694 while ((key = static_cast<TKey *>(next()))) {
1695 TString name = key->GetName();
1696 if (!name.BeginsWith("simg_r"))
1697 continue;
1698 TString rest = name(6, name.Length() - 6); // after "simg_r": <strip>_p<idx>
1699 Int_t us = rest.Index("_p");
1700 if (us < 0)
1701 continue;
1702 Int_t r = TString(rest(0, us)).Atoi();
1703 TGraph *g = static_cast<TGraph *>(f->Get(name));
1704 if (!g)
1705 continue;
1706 by_strip[r].push_back(static_cast<TGraph *>(g->Clone()));
1707 }
1708 f->Close();
1709 delete f;
1710 return kTRUE;
1711}
1712
1713void StripSumScatter::WriteSimCache(
1714 const TString &fp, const std::map<Int_t, std::vector<TGraph *>> &by_strip) {
1715 TFile *out = IO::OpenForWriting("StripSumScatter_simcache.root", "RECREATE");
1716 if (!out || out->IsZombie()) {
1717 if (out)
1718 delete out;
1719 return;
1720 }
1721 out->cd();
1722 TNamed cfp("sim_fingerprint", fp.Data());
1723 cfp.Write();
1724 std::map<Int_t, std::vector<TGraph *>>::const_iterator it;
1725 for (it = by_strip.begin(); it != by_strip.end(); ++it)
1726 for (Int_t i = 0; i < Int_t(it->second.size()); i++)
1727 it->second[i]->Write(Form("simg_r%d_p%d", it->first, Int_t(i)));
1728 out->Close();
1729 delete out;
1730}
1731
1732// Sim-only comparison plots: one per reaction strip, each sim population a
1733// coloured+labelled point cloud on the same axes as that strip's data scatter,
1734// for side-by-side comparison with the data PID scatters. Beam (no reaction
1735// strip) overlays on every strip. The scatter graphs are fingerprint-cached
1736// (sim file sizes/mtimes + window geometry), so re-runs reload them instead of
1737// rescanning the sim files.
1738void StripSumScatter::SimOverlay() {
1739 const Int_t kReacMin =
1741 const Int_t kReacMax =
1743 const Int_t kXLo = Constants::cfg.STRIP_SUM_SCATTER_CONFIG.X_LO;
1744 const Int_t kXHi = Constants::cfg.STRIP_SUM_SCATTER_CONFIG.X_HI;
1745
1746 std::vector<RemixSim::SimFileSpec> specs = RemixSim::BuildFileSpecs();
1747 if (specs.empty()) {
1748 std::cerr
1749 << "strip-sum-scatter: no sim control files; skipping sim overlay."
1750 << std::endl;
1751 return;
1752 }
1753 TString fp = SimFingerprint(specs);
1754
1755 std::map<Int_t, std::vector<TGraph *>>
1756 by_strip; // strip -> graphs (title=label)
1757 Bool_t loaded = LoadSimCache(fp, by_strip);
1758 if (!loaded) {
1759 // One population per class and strip. Every population has two control
1760 // files, the plain one and its _eres twin with the measured widths, and
1761 // both carry the same label; taking each file as a population drew every
1762 // class twice. The eres file wins where it exists.
1763 std::map<std::pair<TString, Int_t>, std::pair<SimPop, Bool_t>> chosen;
1764 for (Int_t i = 0; i < Int_t(specs.size()); i++) {
1765 TString base = RemixSim::TagWithoutStrip(specs[i].tag);
1766 base.ReplaceAll("_eres", "");
1767 const Int_t strip = RemixSim::ReactionStripOf(specs[i].tag);
1768 const Bool_t eres = RemixSim::IsEresTag(specs[i].tag);
1769 std::pair<TString, Int_t> key(base, strip);
1770 if (chosen.count(key) && (chosen[key].second || !eres))
1771 continue;
1772 SimPop p;
1773 p.file = RemixSim::SimRootPath(specs[i]);
1774 p.label = PrettyLabel(specs[i].tag);
1775 chosen[key] = std::make_pair(p, eres);
1776 }
1777 std::map<Int_t, std::vector<SimPop>> reacted;
1778 std::vector<SimPop> refs;
1779 for (std::map<std::pair<TString, Int_t>,
1780 std::pair<SimPop, Bool_t>>::const_iterator it =
1781 chosen.begin();
1782 it != chosen.end(); ++it) {
1783 if (!it->second.second)
1784 std::cout << "No eres sim file for " << it->first.first
1785 << (it->first.second >= 0 ? Form("_s%d", it->first.second)
1786 : "")
1787 << ", using standard" << std::endl;
1788 if (it->first.second < 0)
1789 refs.push_back(it->second.first);
1790 else
1791 reacted[it->first.second].push_back(it->second.first);
1792 }
1793 Double_t gain[18];
1794 if (!SimBeamGains(gain))
1795 for (Int_t s = 0; s < 18; s++)
1796 gain[s] = 1.0;
1797 const Long64_t kSimMaxPoints = 25000;
1798 for (Int_t r = kReacMin; r <= kReacMax; r++) {
1799 std::vector<SimPop> group = reacted[r];
1800 for (Int_t i = 0; i < Int_t(refs.size()); i++)
1801 group.push_back(refs[i]);
1802 for (Int_t i = 0; i < Int_t(group.size()); i++) {
1803 TGraph *g = SimPopScatter(group[i].file, r, gain, kSimMaxPoints);
1804 if (!g || g->GetN() == 0) {
1805 if (g)
1806 delete g;
1807 continue;
1808 }
1809 g->SetTitle(group[i].label);
1810 by_strip[r].push_back(g);
1811 }
1812 }
1813 Int_t n_graphs = 0;
1814 std::map<Int_t, std::vector<TGraph *>>::const_iterator cit;
1815 for (cit = by_strip.begin(); cit != by_strip.end(); ++cit)
1816 n_graphs += Int_t(cit->second.size());
1817 if (n_graphs == 0) {
1818 std::cerr << "strip-sum-scatter: no sim data found (regenerate "
1819 "sim_root_files); skipping sim overlay."
1820 << std::endl;
1821 return;
1822 }
1823 WriteSimCache(fp, by_strip);
1824 std::cout << "strip-sum-scatter: built + cached sim overlay (" << n_graphs
1825 << " population graphs)." << std::endl;
1826 } else {
1827 std::cout
1828 << "strip-sum-scatter: loaded cached sim overlay (fingerprint match)."
1829 << std::endl;
1830 }
1831
1832 std::map<Int_t, std::vector<TGraph *>>::iterator it;
1833 for (it = by_strip.begin(); it != by_strip.end(); ++it) {
1834 Int_t r = it->first;
1835 std::map<Int_t, TH2F *>::const_iterator sit = m_scatter.find(r);
1836 if (sit == m_scatter.end())
1837 continue;
1838 std::lock_guard<std::mutex> lock(g_plot_mutex);
1839 TH2F *ref = sit->second;
1840 TH2F *frame =
1841 new TH2F(Form("sim_frame_r%d", r), "", 10, ref->GetXaxis()->GetXmin(),
1842 ref->GetXaxis()->GetXmax(), 10, ref->GetYaxis()->GetXmin(),
1843 ref->GetYaxis()->GetXmax());
1844 frame->SetStats(0);
1845 frame->GetXaxis()->SetTitle(ref->GetXaxis()->GetTitle());
1846 frame->GetYaxis()->SetTitle(ref->GetYaxis()->GetTitle());
1847 TCanvas *c = PlottingUtils::GetConfiguredCanvas(kFALSE);
1848 c->SetLeftMargin(0.18);
1849 frame->Draw();
1850 // Match the experimental region-traces legend placement (top-right).
1851 TLegend *leg = PlottingUtils::AddLegend(0.725, 0.875, 0.70, 0.86);
1852 for (Int_t i = 0; i < Int_t(it->second.size()); i++) {
1853 TGraph *g = it->second[i];
1854 // Match the experimental region-trace colours (DrawRegionTraces): beam
1855 // grey, (a,a') azure, (a,n) red -- keyed off the population label.
1856 TString lab = g->GetTitle();
1857 Int_t color = kBlack;
1858 if (lab == "Beam")
1859 color = kGray + 2;
1860 else if (lab == "(#alpha,#alpha')")
1861 color = kAzure + 2;
1862 else if (lab == "(#alpha,n)")
1863 color = kRed + 1;
1864 g->SetMarkerStyle(20);
1865 g->SetMarkerSize(0.3);
1866 g->SetMarkerColorAlpha(color, 0.35);
1867 g->SetLineColor(color);
1868 g->Draw("P SAME");
1869 leg->AddEntry(g, g->GetTitle(), "p");
1870 }
1871 leg->Draw();
1872 PlottingUtils::SaveFigure(c,
1873 Form("sim_normsumE_reac%d_s%d_%d_vs_s%d_%d", r,
1874 YLoOf(r), YHiOf(r), kXLo, kXHi),
1875 "sim_scatter", PlotSaveOptions::kLINEAR);
1876 delete leg;
1877 delete c;
1878 delete frame;
1879 }
1880
1881 std::map<Int_t, std::vector<TGraph *>>::iterator dit;
1882 for (dit = by_strip.begin(); dit != by_strip.end(); ++dit)
1883 for (Int_t i = 0; i < Int_t(dit->second.size()); i++)
1884 delete dit->second[i];
1885}
1886
1887Bool_t StripSumScatter::TryLoadCache(const TString &cacheName,
1888 const TString &fingerprint) {
1889 const Int_t kReacMin =
1891 const Int_t kReacMax =
1893
1894 TString cache_full = IO::GetRootFilesBaseDir() + TString("/") + cacheName;
1895 if (gSystem->AccessPathName(cache_full)) {
1896 std::cout << "strip-sum-scatter: no cache file found; will rebuild."
1897 << std::endl;
1898 return kFALSE;
1899 }
1900
1901 TFile *cf = IO::OpenForReading(cacheName);
1902 if (!cf || cf->IsZombie()) {
1903 if (cf)
1904 delete cf;
1905 std::cout << "strip-sum-scatter: cache file unreadable; rebuilding."
1906 << std::endl;
1907 return kFALSE;
1908 }
1909
1910 TNamed *fp = static_cast<TNamed *>(cf->Get("fingerprint"));
1911 const Bool_t exact = fp && fingerprint == fp->GetTitle();
1912 // Same tagging, different plane: the reservoir has every tagged event, so
1913 // the scatters are rebuilt from it below and the cache rewritten.
1914 const Bool_t reproject = !exact && fp && !TagPart(fingerprint).IsNull() &&
1915 TagPart(fingerprint) == TagPart(fp->GetTitle());
1916 if (!exact && !reproject) {
1917 std::cout << "strip-sum-scatter: cache present but stale; rebuilding."
1918 << std::endl;
1919 std::cout << " cached: " << (fp ? fp->GetTitle() : "(none)") << std::endl;
1920 std::cout << " wanted: " << fingerprint << std::endl;
1921 cf->Close();
1922 delete cf;
1923 return kFALSE;
1924 }
1925
1926 Bool_t ok = kTRUE;
1927 for (Int_t reac = kReacMin; reac <= kReacMax && ok && exact; reac++) {
1928 TH2F *h = static_cast<TH2F *>(cf->Get(Form("scatter_r%d", reac)));
1929 if (!h) {
1930 ok = kFALSE;
1931 break;
1932 }
1933 TH2F *hc = static_cast<TH2F *>(h->Clone());
1934 hc->SetDirectory(nullptr);
1935 m_scatter[reac] = hc;
1936 }
1937
1938 // The normalization counts ride along, so a re-projected cache keeps them.
1939 if (TParameter<Long64_t> *p =
1940 dynamic_cast<TParameter<Long64_t> *>(cf->Get("n_seen")))
1941 m_nSeen = p->GetVal();
1942 if (TParameter<Long64_t> *p =
1943 dynamic_cast<TParameter<Long64_t> *>(cf->Get("n_normed")))
1944 m_nNormed = p->GetVal();
1945 for (Int_t reac = kReacMin; reac <= kReacMax; reac++) {
1946 if (TParameter<Long64_t> *p = dynamic_cast<TParameter<Long64_t> *>(
1947 cf->Get(Form("n_normed_r%d", reac))))
1948 m_normedAt[ReacIndex(reac)] = p->GetVal();
1949 if (TParameter<Long64_t> *p = dynamic_cast<TParameter<Long64_t> *>(
1950 cf->Get(Form("n_tagged_r%d", reac))))
1951 m_tagged[ReacIndex(reac)] = p->GetVal();
1952 }
1953
1954 TTree *tt = static_cast<TTree *>(cf->Get("traces"));
1955 if (reproject && !tt)
1956 ok = kFALSE;
1957 if (ok && tt) {
1958 TraceEvt e;
1959 tt->SetBranchAddress("total", e.total);
1960 tt->SetBranchAddress("total_adc", e.total_adc);
1961 tt->SetBranchAddress("reac_mask", &e.reac_mask);
1962 tt->SetBranchAddress("beam_flat", &e.beam_flat);
1963 tt->SetBranchAddress("both_mult", &e.both_mult);
1964 e.seed_ts = 0;
1965 if (tt->GetBranch("seed_ts"))
1966 tt->SetBranchAddress("seed_ts", &e.seed_ts);
1967 Long64_t nt = tt->GetEntries();
1968 m_reservoir.reserve(nt);
1969 for (Long64_t j = 0; j < nt; j++) {
1970 tt->GetEntry(j);
1971 m_reservoir.push_back(e);
1972 }
1973 }
1974
1975 cf->Close();
1976 delete cf;
1977
1978 if (!ok) {
1979 std::cout << "strip-sum-scatter: cache partially corrupt; rebuilding."
1980 << std::endl;
1981 m_reservoir.clear();
1982 return kFALSE;
1983 }
1984 if (reproject) {
1985 std::cout << "strip-sum-scatter: cache tagging matches but the plane "
1986 "changed; re-projecting "
1987 << m_reservoir.size() << " reservoir events." << std::endl;
1988 AllocateScatters();
1989 ReprojectFromReservoir();
1990 WriteCache(cacheName, fingerprint);
1991 } else {
1992 std::cout << "strip-sum-scatter: loaded cached scatters + "
1993 << m_reservoir.size() << " reservoir events (fingerprint match)."
1994 << std::endl;
1995 }
1996 return kTRUE;
1997}
1998
1999void StripSumScatter::WriteCache(const TString &cacheName,
2000 const TString &fingerprint) {
2001 const Int_t kReacMin =
2003 const Int_t kReacMax =
2005
2006 TFile *out = IO::OpenForWriting(cacheName, "RECREATE");
2007 if (!out || out->IsZombie()) {
2008 if (out)
2009 delete out;
2010 return;
2011 }
2012 out->cd();
2013 TNamed fp("fingerprint", fingerprint.Data());
2014 fp.Write();
2015 // Normalization counts travel with the scatters they describe, so a cross
2016 // section never has to re-derive them from a different pass over the data.
2017 TParameter<Long64_t>("n_seen", m_nSeen).Write();
2018 TParameter<Long64_t>("n_normed", m_nNormed).Write();
2019 // The noise the jump gate and the upstream tolerance were resolved against.
2020 for (Int_t s = 1; s < 18; s++)
2021 TParameter<Double_t>(Form("jump_sigma_s%d", s), s_jumpSigma[s]).Write();
2022 for (Int_t s = 0; s < 18; s++)
2023 TParameter<Double_t>(Form("strip_sigma_s%d", s), s_stripSigma[s]).Write();
2024 for (Int_t reac = kReacMin; reac <= kReacMax; reac++)
2025 TParameter<Long64_t>(Form("n_normed_r%d", reac),
2026 m_normedAt[ReacIndex(reac)])
2027 .Write();
2028 for (Int_t reac = kReacMin; reac <= kReacMax; reac++)
2029 TParameter<Long64_t>(Form("n_tagged_r%d", reac), m_tagged[ReacIndex(reac)])
2030 .Write();
2031 for (Int_t reac = kReacMin; reac <= kReacMax; reac++)
2032 m_scatter[reac]->Write(Form("scatter_r%d", reac));
2033
2034 TTree *tt = new TTree("traces", "strip-sum trace reservoir");
2035 TraceEvt e;
2036 tt->Branch("total", e.total, "total[18]/F");
2037 tt->Branch("total_adc", e.total_adc, "total_adc[18]/F");
2038 tt->Branch("reac_mask", &e.reac_mask, "reac_mask/i");
2039 tt->Branch("beam_flat", &e.beam_flat, "beam_flat/O");
2040 tt->Branch("both_mult", &e.both_mult, "both_mult/I");
2041 tt->Branch("seed_ts", &e.seed_ts, "seed_ts/l");
2042 for (Int_t k = 0; k < Int_t(m_reservoir.size()); k++) {
2043 e = m_reservoir[k];
2044 tt->Fill();
2045 }
2046 tt->Write();
2047 out->Close();
2048 delete out;
2049 std::cout << "strip-sum-scatter: wrote cache " << cacheName << std::endl;
2050}
2051
2052// One run's beam ellipses and series gates. Split out so runs can be fitted in
2053// parallel: each call touches only its own chain and returns its own result,
2054// with no shared state to guard.
2056StripSumScatter::FitRunGates(Int_t run, TChain *chain,
2057 const std::vector<GateSpec> &activeGates) {
2058 SingleRunFitResult res;
2059 if (!chain || chain->GetEntries() == 0)
2060 return res;
2061
2062 // --- Beam classification ellipses (no gating) ---
2063 BeamEllipses be;
2064 be.ok = kFALSE;
2065 {
2066 std::vector<GateSpec> emptyPrior;
2067 std::vector<BeamFit2D> emptyGates;
2068 const TString tag = Form("run%d", run);
2069 const TString subdir = Form("strip_sum_scatter/run%d", run);
2070 Int_t ent_sx = 0, ent_sy = 1;
2071 const Char_t *ent_tag = "s0/s1";
2072 if (Constants::cfg.STRIP_SUM_SCATTER_CONFIG.PURE_BEAM_GATE ==
2074 ent_sx = 1;
2075 ent_sy = 2;
2076 ent_tag = "s1/s2";
2077 }
2078 BeamFit2D ent_ell = FindBeamGate(chain, ent_sx, ent_sy, emptyPrior,
2079 emptyGates, tag, subdir);
2080 if (ent_sx == 0)
2081 be.s0_s1 = ent_ell;
2082 else
2083 be.s1_s2 = ent_ell;
2084 if (ent_ell.ok) {
2085 std::lock_guard<std::mutex> lk(g_log_mutex);
2086 std::cout << " run " << run << " beam ellipse " << ent_tag << ": mu=("
2087 << ent_ell.mu_x << "," << ent_ell.mu_y << ")" << std::endl;
2088 } else {
2089 std::lock_guard<std::mutex> lk(g_log_mutex);
2090 std::cerr << " run " << run << " beam ellipse " << ent_tag
2091 << " failed; skipping run" << std::endl;
2092 return res;
2093 }
2094 if (Constants::cfg.IGNORE_STRIP_17) {
2095 be.use_s15_s16 = kTRUE;
2096 be.s15_s16 =
2097 FindBeamGate(chain, 15, 16, emptyPrior, emptyGates, tag, subdir);
2098 if (be.s15_s16.ok) {
2099 std::lock_guard<std::mutex> lk(g_log_mutex);
2100 std::cout << " run " << run << " beam ellipse s15/s16: mu=("
2101 << be.s15_s16.mu_x << "," << be.s15_s16.mu_y << ")"
2102 << std::endl;
2103 } else {
2104 std::lock_guard<std::mutex> lk(g_log_mutex);
2105 std::cerr << " run " << run
2106 << " beam ellipse s15/s16 failed; skipping run" << std::endl;
2107 return res;
2108 }
2109 } else {
2110 be.use_s15_s16 = kFALSE;
2111 be.s16_s17 =
2112 FindBeamGate(chain, 16, 17, emptyPrior, emptyGates, tag, subdir);
2113 if (be.s16_s17.ok) {
2114 std::lock_guard<std::mutex> lk(g_log_mutex);
2115 std::cout << " run " << run << " beam ellipse s16/s17: mu=("
2116 << be.s16_s17.mu_x << "," << be.s16_s17.mu_y << ")"
2117 << std::endl;
2118 } else {
2119 std::lock_guard<std::mutex> lk(g_log_mutex);
2120 std::cerr << " run " << run
2121 << " beam ellipse s16/s17 failed; skipping run" << std::endl;
2122 return res;
2123 }
2124 }
2125 be.ok = kTRUE;
2126 }
2127 res.pure_beam = be;
2128
2129 // --- Scatter filter gates (series gating) ---
2130 std::vector<BeamFit2D> runGates;
2131 std::vector<GateSpec> priorSpecs;
2132 Bool_t allOk = kTRUE;
2133 for (Int_t gi = 0; gi < Int_t(activeGates.size()); gi++) {
2134 BeamFit2D g = FindBeamGate(chain, activeGates[gi].sx, activeGates[gi].sy,
2135 priorSpecs, runGates, Form("run%d", run),
2136 Form("strip_sum_scatter/run%d", run));
2137 if (g.ok) {
2138 std::lock_guard<std::mutex> lk(g_log_mutex);
2139 std::cout << " run " << run << " beam gate s" << activeGates[gi].sx
2140 << "/s" << activeGates[gi].sy << ": mu=(" << g.mu_x << ","
2141 << g.mu_y << ")" << std::endl;
2142 } else {
2143 std::lock_guard<std::mutex> lk(g_log_mutex);
2144 std::cerr << " run " << run << " beam gate s" << activeGates[gi].sx
2145 << "/s" << activeGates[gi].sy << " failed; skipping run"
2146 << std::endl;
2147 allOk = kFALSE;
2148 }
2149 runGates.push_back(g);
2150 priorSpecs.push_back(activeGates[gi]);
2151 }
2152 res.series_gates = runGates;
2153 res.ok = allOk;
2154 return res;
2155}
2156
2157// One run's scatter fill. Each call builds PRIVATE scatter histograms and its
2158// own reservoir slice, so the runs never touch shared state; the caller merges
2159// them in run order, which makes the threaded result identical to sequential.
2160SingleRunFillResult StripSumScatter::FillRunScatters(
2161 Int_t run, TChain *chain, const std::vector<GateSpec> &activeGates,
2162 const std::vector<BeamFit2D> &runGates, const BeamEllipses &runBeam) {
2163 const Int_t kReacMin =
2165 const Int_t kReacMax =
2167 const Int_t kXLo = Constants::cfg.STRIP_SUM_SCATTER_CONFIG.X_LO;
2168 const Int_t kXHi = Constants::cfg.STRIP_SUM_SCATTER_CONFIG.X_HI;
2169 const Int_t kXBins = Constants::cfg.STRIP_SUM_SCATTER_CONFIG.XBINS;
2170 const Int_t kYBins = Constants::cfg.STRIP_SUM_SCATTER_CONFIG.YBINS;
2171 const Int_t kBeamReservoirCap =
2173 const Int_t nReacStrips = kReacMax - kReacMin + 1;
2174
2175 SingleRunFillResult res;
2176 res.scatters.assign(nReacStrips, nullptr);
2177 if (!chain)
2178 return res;
2179 // Private, directory-less clones over the same fixed build range as the
2180 // merged ones.
2181 for (Int_t reac = kReacMin; reac <= kReacMax; reac++) {
2182 TH2F *h =
2183 new TH2F(Form("scatter_r%d_run%d", reac, run), "", kXBins,
2186 h->SetDirectory(nullptr);
2187 res.scatters[ReacIndex(reac)] = h;
2188 }
2189 Long64_t totalGated = 0, totalSeen = 0, totalNormed = 0;
2190 res.tagged.assign(nReacStrips, 0);
2191 res.normed_at.assign(nReacStrips, 0);
2192 Int_t nBeamKept = 0;
2193 EnergyView ev;
2194 ev.Attach(chain);
2195 EnableEventBranches(chain);
2196 ULong64_t seed_ts_in = 0;
2197 if (chain->GetBranch("SeedTs"))
2198 chain->SetBranchAddress("SeedTs", &seed_ts_in);
2199 Long64_t n = chain->GetEntries();
2200 Int_t nReac = kReacMax - kReacMin + 1;
2201 {
2202 std::lock_guard<std::mutex> lk(g_log_mutex);
2203 std::cout << "Run " << run << ": filling " << nReac
2204 << " reaction-strip scatters over " << n << " events..."
2205 << std::endl;
2206 }
2207
2208 for (Long64_t j = 0; j < n; j++) {
2209 chain->GetEntry(j);
2210 ev.Decode();
2211 totalSeen++;
2212
2213 Bool_t passesAll = kTRUE;
2214 for (Int_t gi = 0; gi < Int_t(activeGates.size()); gi++)
2215 if (!PassesGate(runGates[gi], ev, activeGates[gi].sx,
2216 activeGates[gi].sy)) {
2217 passesAll = kFALSE;
2218 break;
2219 }
2220 if (!passesAll)
2221 continue;
2222 if (IsPileup(ev)) // reject overlapping-beam pileup
2223 continue;
2224 if (IsNoise(ev))
2225 continue;
2226 if (Constants::cfg.STRIP_SUM_SCATTER_CONFIG.REJECT_OFFBEAM && IsOffbeam(ev))
2227 continue;
2228 if (IsParityAsymmetric(ev))
2229 continue;
2230 // Both-ends multiplicity: counted on raw ADC so it sees the short end
2231 // even when IGNORE_SHORT_STRIPS zeroes it in the decode.
2232 if (Constants::cfg.STRIP_SUM_SCATTER_CONFIG.BOTH_MULT_MAX >= 0) {
2233 const Int_t hi = TMath::Min(
2234 16, Constants::cfg.STRIP_SUM_SCATTER_CONFIG.BOTH_MULT_COUNT_TO);
2235 Int_t nboth = 0;
2236 for (Int_t s = 1; s <= hi; s++)
2237 if (ev.left_0_17_adc[s] > 0.0 && ev.rightdE_adc[s] > 0.0)
2238 nboth++;
2239 if (nboth > Constants::cfg.STRIP_SUM_SCATTER_CONFIG.BOTH_MULT_MAX)
2240 continue;
2241 }
2242 // Last point at which nothing about a reaction has been asked. Counting
2243 // here, rather than at `seen`, is what makes the ratio to a tag count a
2244 // cross section: both sides carry the same gate and quality efficiencies.
2245 totalNormed++;
2246 // Per-strip denominator. A beam particle counts toward strip `reac` only
2247 // if it met the conditions a reaction there would also have had to meet,
2248 // so those efficiencies cancel in the ratio rather than tilting the
2249 // excitation function. Everything after this in PassesReaction is about
2250 // the reaction itself and belongs only to the numerator.
2251 if (AllStripsFired(ev)) {
2252 for (Int_t reac = kReacMin; reac <= kReacMax; reac++)
2253 if (BeamUpstreamOf(ev, reac))
2254 res.normed_at[ReacIndex(reac)]++;
2255 }
2256
2257 UInt_t mask = 0;
2258 for (Int_t reac = kReacMin; reac <= kReacMax; reac++) {
2259 if (!PassesReaction(ev, reac))
2260 continue;
2261 mask |= (1u << ReacIndex(reac));
2262 res.tagged[ReacIndex(reac)]++;
2263 Double_t x = 0.0, y = 0.0;
2264 PlaneXY(ev.total, reac, x, y);
2265 res.scatters[ReacIndex(reac)]->Fill(x, y);
2266 }
2267
2268 // Keep reaction-passing events for traces; cap pure-beam events (only
2269 // ~TRACES_PER_CLASS are ever drawn). The two are mutually exclusive -- a
2270 // pure-beam event has no reaction jump. This cap is only a per-task
2271 // memory bound; FillScatters re-applies it across all tasks when it
2272 // merges, which is what actually fixes the kept beam population.
2273 Bool_t beam = (mask == 0) && IsPureBeam(ev, runBeam);
2274 if (mask == 0 && !(beam && nBeamKept < kBeamReservoirCap))
2275 continue;
2276 if (beam)
2277 nBeamKept++;
2278
2279 TraceEvt e;
2280 for (Int_t s = 0; s < 18; s++) {
2281 e.total[s] = Float_t(ev.total[s]);
2282 e.total_adc[s] =
2283 Float_t(ev.left_0_17_adc[s]) + Float_t(ev.rightdE_adc[s]);
2284 e.long_au[s] = Float_t(ev.total[s]);
2285 e.short_au[s] = 0.0f;
2286 }
2287 for (Int_t s = 1; s <= 16; s++) {
2288 Double_t lv = Double_t(ev.gain_left[s]) * Double_t(ev.left_0_17_adc[s]) *
2289 Double_t(ev.strip_factor[s]);
2290 Double_t rv = Double_t(ev.gain_right[s]) * Double_t(ev.rightdE_adc[s]) *
2291 Double_t(ev.strip_factor[s]);
2292 Bool_t l_is_long = ((s % 2) != 0);
2293 e.long_au[s] = Float_t(l_is_long ? lv : rv);
2294 e.short_au[s] = Float_t(l_is_long ? rv : lv);
2295 }
2296 // Mirror IGNORE_SHORT_STRIPS: the normed total keeps only the long side
2297 // of a split strip, so the raw trace must drop the same side to stay
2298 // comparable.
2299 if (Constants::cfg.IGNORE_SHORT_STRIPS)
2300 for (Int_t s = 1; s <= 16; s++)
2301 e.total_adc[s] = ((s % 2) != 0) ? Float_t(ev.left_0_17_adc[s])
2302 : Float_t(ev.rightdE_adc[s]);
2303 // Both-channel multiplicity: split strips (1-16) where both ends
2304 // FIRED. Read off the RAW ADC, not the calibrated ends -- the
2305 // short-end gains are 0 (uncalibrated, no sim anchor), so the
2306 // calibrated short ends are always zero; the raw ADC still carries
2307 // whether the channel fired.
2308 Int_t both = 0;
2309 for (Int_t s = 1; s <= 16; s++)
2310 if (ev.left_0_17_adc[s] > 0.0 && ev.rightdE_adc[s] > 0.0)
2311 both++;
2312 e.both_mult = both;
2313 e.seed_ts = seed_ts_in;
2314 e.reac_mask = mask;
2315 e.beam_flat = beam;
2316 res.reservoir.push_back(e);
2317 if (mask != 0)
2318 totalGated++;
2319 }
2320 res.gated = totalGated;
2321 res.seen = totalSeen;
2322 res.normed = totalNormed;
2323 return res;
2324}
2325
2326// Run `n` indexed tasks on `workers` threads, pulling from a shared queue.
2327static void RunIndexedParallel(Int_t n, Int_t workers,
2328 const std::function<void(Int_t)> &task) {
2329 std::queue<Int_t> work;
2330 for (Int_t i = 0; i < n; i++)
2331 work.push(i);
2332 std::mutex work_mutex;
2333 std::vector<std::thread> pool;
2334 for (Int_t w = 0; w < workers; w++) {
2335 pool.emplace_back([&]() {
2336 while (true) {
2337 Int_t i;
2338 {
2339 std::lock_guard<std::mutex> lk(work_mutex);
2340 if (work.empty())
2341 return;
2342 i = work.front();
2343 work.pop();
2344 }
2345 task(i);
2346 }
2347 });
2348 }
2349 for (Int_t w = 0; w < Int_t(pool.size()); w++)
2350 pool[w].join();
2351}
2352
2353void StripSumScatter::AllocateScatters() {
2354 const Int_t kReacMin =
2356 const Int_t kReacMax =
2358 const Int_t kXLo = Constants::cfg.STRIP_SUM_SCATTER_CONFIG.X_LO;
2359 const Int_t kXHi = Constants::cfg.STRIP_SUM_SCATTER_CONFIG.X_HI;
2360 const Int_t kXBins = Constants::cfg.STRIP_SUM_SCATTER_CONFIG.XBINS;
2361 const Int_t kYBins = Constants::cfg.STRIP_SUM_SCATTER_CONFIG.YBINS;
2362 for (Int_t reac = kReacMin; reac <= kReacMax; reac++) {
2363 TH2F *h =
2364 new TH2F(Form("scatter_r%d", reac),
2365 Form(";norm. #DeltaE strips %d#rightarrow%d [a.u.];norm. "
2366 "#DeltaE strips %d#rightarrow%d [a.u.]",
2367 kXLo, kXHi, YLoOf(reac), YHiOf(reac)),
2370 h->SetDirectory(nullptr);
2371 h->SetStats(0);
2372 m_scatter[reac] = h;
2373 }
2374}
2375
2376void StripSumScatter::ReprojectFromReservoir() {
2377 const Int_t kReacMin =
2379 const Int_t kReacMax =
2381 Long64_t nFilled = 0;
2382 for (Int_t k = 0; k < Int_t(m_reservoir.size()); k++) {
2383 const TraceEvt &e = m_reservoir[k];
2384 if (e.reac_mask == 0)
2385 continue;
2386 Double_t total[18];
2387 for (Int_t s = 0; s < 18; s++)
2388 total[s] = e.total[s];
2389 for (Int_t reac = kReacMin; reac <= kReacMax; reac++) {
2390 if (!(e.reac_mask & (1u << ReacIndex(reac))))
2391 continue;
2392 Double_t x = 0.0, y = 0.0;
2393 PlaneXY(total, reac, x, y);
2394 m_scatter[reac]->Fill(x, y);
2395 nFilled++;
2396 }
2397 }
2398 std::cout << "strip-sum-scatter: re-projected " << nFilled
2399 << " tagged entries." << std::endl;
2400}
2401
2402void StripSumScatter::FillScatters(const std::vector<Int_t> &runOrder,
2403 std::map<Int_t, TChain *> &chains) {
2404 const Int_t kReacMin =
2406 const Int_t kReacMax =
2408 const Int_t kXBins = Constants::cfg.STRIP_SUM_SCATTER_CONFIG.XBINS;
2409 const Int_t kYBins = Constants::cfg.STRIP_SUM_SCATTER_CONFIG.YBINS;
2410
2411 AllocateScatters();
2412
2413 std::vector<GateSpec> activeGates = ActiveGates();
2414
2415 // Pre-index the chains so worker threads only touch their own run.
2416 const Int_t nRuns = Int_t(runOrder.size());
2417 std::vector<TChain *> chainVec(nRuns);
2418 for (Int_t i = 0; i < nRuns; i++)
2419 chainVec[i] = chains[runOrder[i]];
2420
2421 Int_t n_workers =
2422 TMath::Min(Int_t(std::thread::hardware_concurrency()), nRuns);
2423 n_workers = TMath::Min(
2424 n_workers, Constants::cfg.STRIP_SUM_SCATTER_CONFIG.MAX_STRIP_SUM_WORKERS);
2425 if (n_workers < 1)
2426 n_workers = 1;
2427 std::cout << "strip-sum-scatter: " << nRuns << " runs on " << n_workers
2428 << " workers" << std::endl;
2429
2430 // Phase 1: beam ellipses + series gates, one task per run. The gates within
2431 // a run stay sequential -- each only sees events passing the prior ones.
2432 std::vector<SingleRunFitResult> fits(nRuns);
2433 RunIndexedParallel(nRuns, n_workers, [&](Int_t i) {
2434 fits[i] = FitRunGates(runOrder[i], chainVec[i], activeGates);
2435 });
2436
2437 // Phase 2: event filling. Work is split per EVENTS FILE, not per run: a
2438 // CoMPASS run is hundreds of subfiles collapsed into one chain by
2439 // GroupEventsByRun, so per-run tasks would leave 87Rb at two-way parallelism
2440 // while a SOLARIS dataset with one file per run already gets full width.
2441 // Gates stay per-run above (they need whole-run statistics); the fill does
2442 // not, so each file is its own task carrying its run's gates.
2443 struct FillTask {
2444 Int_t run_idx;
2445 TString path;
2446 };
2447 std::vector<FillTask> tasks;
2448 {
2449 std::map<Int_t, Int_t> idx_of_run;
2450 for (Int_t i = 0; i < nRuns; i++)
2451 idx_of_run[runOrder[i]] = i;
2452 std::vector<FileSpec> specs = FileSet::BuildProcessedFileSpecs();
2453 for (Int_t k = 0; k < Int_t(specs.size()); k++) {
2454 std::map<Int_t, Int_t>::const_iterator it = idx_of_run.find(specs[k].run);
2455 if (it == idx_of_run.end() || !fits[it->second].ok)
2456 continue;
2457 TString full = IO::GetRootFilesBaseDir() + "/" +
2458 FileSet::EventsName(specs[k]) + ".root";
2459 if (gSystem->AccessPathName(full))
2460 continue;
2461 FillTask t;
2462 t.run_idx = it->second;
2463 t.path = full;
2464 tasks.push_back(t);
2465 }
2466 }
2467 Int_t nTasks = Int_t(tasks.size());
2468 Int_t fill_workers =
2469 TMath::Min(Int_t(std::thread::hardware_concurrency()), nTasks);
2470 fill_workers =
2471 TMath::Min(fill_workers,
2472 Constants::cfg.STRIP_SUM_SCATTER_CONFIG.MAX_STRIP_SUM_WORKERS);
2473 if (fill_workers < 1)
2474 fill_workers = 1;
2475 std::cout << "strip-sum-scatter: filling " << nTasks << " files on "
2476 << fill_workers << " workers" << std::endl;
2477
2478 // Every task's result is merged into the totals in TASK ORDER as soon as
2479 // every task before it has finished, and freed on the spot. Holding all
2480 // results until the end and merging afterwards kept fourteen scatters of
2481 // XBINS x YBINS bins and a reservoir slice per file alive at once, then
2482 // copied every slice into the total while the slices still existed: for a
2483 // SOLARIS dataset of seventy files that is tens of GB at the moment of the
2484 // merge, and the process was killed there. The order is kept so the pure-
2485 // beam budget below is spent the same way whatever order the workers
2486 // finish in, which makes the threaded result identical to sequential.
2487 std::vector<SingleRunFillResult> fills(nTasks);
2488 std::vector<Bool_t> filled(nTasks, kFALSE);
2489 Int_t next_merge = 0;
2490 std::mutex merge_mutex;
2491 Long64_t totalGated = 0, totalSeen = 0;
2492 const Int_t kBeamReservoirCap =
2494 Int_t nBeamKept = 0;
2495 auto merge = [&](Int_t t) {
2496 for (Int_t reac = kReacMin; reac <= kReacMax; reac++) {
2497 Int_t ri = ReacIndex(reac);
2498 if (ri < Int_t(fills[t].scatters.size()) && fills[t].scatters[ri])
2499 m_scatter[reac]->Add(fills[t].scatters[ri]);
2500 }
2501 // Pure-beam events are capped GLOBALLY, not per task. Each task applies
2502 // the same cap to its own slice, so without this the kept beam population
2503 // would scale with the number of events files -- hundreds of them for a
2504 // CoMPASS run. Reaction-tagged events are never dropped.
2505 for (Int_t k = 0; k < Int_t(fills[t].reservoir.size()); k++) {
2506 const TraceEvt &e = fills[t].reservoir[k];
2507 if (e.beam_flat) {
2508 if (nBeamKept >= kBeamReservoirCap)
2509 continue;
2510 nBeamKept++;
2511 }
2512 m_reservoir.push_back(e);
2513 }
2514 totalGated += fills[t].gated;
2515 totalSeen += fills[t].seen;
2516 m_nNormed += fills[t].normed;
2517 for (Int_t k = 0; k < Int_t(fills[t].normed_at.size()); k++)
2518 m_normedAt[k] += fills[t].normed_at[k];
2519 for (Int_t k = 0; k < Int_t(fills[t].tagged.size()); k++)
2520 m_tagged[k] += fills[t].tagged[k];
2521 for (Int_t k = 0; k < Int_t(fills[t].scatters.size()); k++)
2522 delete fills[t].scatters[k];
2523 fills[t].scatters.clear();
2524 std::vector<TraceEvt>().swap(fills[t].reservoir);
2525 };
2526 RunIndexedParallel(nTasks, fill_workers, [&](Int_t t) {
2527 Int_t i = tasks[t].run_idx;
2528 {
2529 TChain ch("events");
2530 ch.Add(tasks[t].path);
2531 if (ch.GetEntries() > 0)
2532 fills[t] = FillRunScatters(runOrder[i], &ch, activeGates,
2533 fits[i].series_gates, fits[i].pure_beam);
2534 }
2535 std::lock_guard<std::mutex> lk(merge_mutex);
2536 filled[t] = kTRUE;
2537 while (next_merge < nTasks && filled[next_merge]) {
2538 merge(next_merge);
2539 next_merge++;
2540 }
2541 });
2542 m_nSeen = totalSeen;
2543 std::cout << "strip-sum-scatter: " << totalGated << " reaction-tagged of "
2544 << totalSeen << " events (" << m_nNormed
2545 << " past every pre-tag cut); reservoir " << m_reservoir.size()
2546 << std::endl;
2547}
2548
2549void StripSumScatter::PlotScatters() {
2550 // Display windows, recomputed here rather than in FillScatters: a cached run
2551 // skips the fill entirely, and leaving these at their constructor zeros made
2552 // SetRangeUser(0, 0) throw the zoom away on every run after the first.
2553 YBounds(m_yLo, m_yHi);
2554 const Int_t kXLo = Constants::cfg.STRIP_SUM_SCATTER_CONFIG.X_LO;
2555 const Int_t kXHi = Constants::cfg.STRIP_SUM_SCATTER_CONFIG.X_HI;
2556 const Int_t kReacMin =
2558 const Int_t kReacMax =
2560
2561 std::lock_guard<std::mutex> lock(g_plot_mutex);
2562 for (Int_t reac = kReacMin; reac <= kReacMax; reac++) {
2563 TCanvas *c = PlottingUtils::GetConfiguredCanvas(kFALSE);
2564 // Display-only zoom: the histogram is built over the fixed build range, so
2565 // retuning these windows never forces a refill.
2566 Int_t ri = ReacIndex(reac);
2567 m_scatter[reac]->GetXaxis()->SetRangeUser(
2568 Constants::cfg.STRIP_SUM_SCATTER_CONFIG.X_DISPLAY_MIN,
2570 m_scatter[reac]->GetYaxis()->SetRangeUser(m_yLo[ri], m_yHi[ri]);
2571 PlottingUtils::ConfigureAndDraw2DHistogram(m_scatter[reac], c);
2572 m_scatter[reac]->GetYaxis()->SetTitleOffset(1.3);
2573 c->SetLeftMargin(0.18);
2574 PlottingUtils::SaveFigure(c,
2575 Form("normsumE_reac%d_s%d_%d_vs_s%d_%d", reac,
2576 YLoOf(reac), YHiOf(reac), kXLo, kXHi),
2577 "strip_sum_scatter", PlotSaveOptions::kLINEAR);
2578 delete c;
2579 }
2580}
2581
2582// Diagnostic: the Grid #DeltaE of events that pass the cheap pre-tag cuts and
2583// are then rejected by the parity cut. Self-contained pass over the chains
2584// (not the fill's cuts), so it works on a cached dataset too. Two views of the
2585// same population are saved: the decoded Grid signal in a.u. ([0,1]) and the
2586// raw Grid branch in ADC. Purely visual: does not change what is tagged or the
2587// cache. Both are drawn on a log-y axis (PlotSaveOptions::kLOG).
2588void StripSumScatter::PlotParityRejectedGrid(
2589 const std::vector<Int_t> &run_order, std::map<Int_t, TChain *> &chains) {
2590 const StripSumScatterConfig &C = Constants::cfg.STRIP_SUM_SCATTER_CONFIG;
2592 return;
2593 if (!(C.PARITY_ASYM_MAX > 0.0)) {
2594 std::cout << "strip-sum-scatter: PLOT_PARITY_REJECTED_GRID is set but "
2595 "PARITY_ASYM_MAX <= 0; no parity cut to reject events, "
2596 "skipping the grid diagnostic."
2597 << std::endl;
2598 return;
2599 }
2600
2601 const Double_t grid_max_adc = Constants::ActiveGridMaxAdc();
2602
2603 // a.u. view: the decoded grid (grid_adc / 16384 when calibrated), which is
2604 // what the rest of the analysis works in. ADC view: the raw trigger channel.
2605 TH1F *h_au = new TH1F("grid_parity_rejected_au",
2606 ";Grid #DeltaE [a.u.];Counts", 400, 0.0, 1.0);
2607 TH1F *h_adc =
2608 new TH1F("grid_parity_rejected_adc", ";Grid #DeltaE [ADC];Counts", 400,
2609 0.0, grid_max_adc > 0.0 ? grid_max_adc : 16384.0);
2610 h_au->SetDirectory(nullptr);
2611 h_adc->SetDirectory(nullptr);
2612
2613 Long64_t nRejected = 0;
2614 for (Int_t i = 0; i < Int_t(run_order.size()); i++) {
2615 TChain *chain = chains[run_order[i]];
2616 if (!chain)
2617 continue;
2618 EnergyView ev;
2619 ev.Attach(chain);
2620 EnableEventBranches(chain);
2621 const Long64_t n = chain->GetEntries();
2622 for (Long64_t j = 0; j < n; j++) {
2623 chain->GetEntry(j);
2624 ev.Decode();
2625 if (!AllStripsFired(ev) || IsPileup(ev) || IsNoise(ev))
2626 continue;
2627 if (C.REJECT_OFFBEAM && IsOffbeam(ev))
2628 continue;
2629 if (IsParityAsymmetric(ev)) {
2630 h_au->Fill(ev.grid);
2631 h_adc->Fill(ev.grid_adc);
2632 nRejected++;
2633 }
2634 }
2635 chain->ResetBranchAddresses();
2636 }
2637
2638 std::cout << "strip-sum-scatter: parity-rejected grid diagnostic: "
2639 << nRejected << " events rejected by the parity cut." << std::endl;
2640 if (nRejected == 0) {
2641 delete h_au;
2642 delete h_adc;
2643 return;
2644 }
2645
2646 {
2647 std::lock_guard<std::mutex> lock(g_plot_mutex);
2648 TCanvas *c_au = PlottingUtils::GetConfiguredCanvas(kFALSE);
2649 PlottingUtils::ConfigureAndDrawHistogram(h_au, kBlue + 1);
2650 PlottingUtils::SaveFigure(c_au, "grid_parity_rejected_au",
2651 "strip_sum_scatter", PlotSaveOptions::kLOG);
2652 delete c_au;
2653 TCanvas *c_adc = PlottingUtils::GetConfiguredCanvas(kFALSE);
2654 PlottingUtils::ConfigureAndDrawHistogram(h_adc, kBlue + 1);
2655 PlottingUtils::SaveFigure(c_adc, "grid_parity_rejected_adc",
2656 "strip_sum_scatter", PlotSaveOptions::kLOG);
2657 delete c_adc;
2658 }
2659 delete h_au;
2660 delete h_adc;
2661}
2662
2663void StripSumScatter::InteractiveOverlay(Int_t reac) {
2664 const Int_t kReacMin =
2666 const Int_t kReacMax =
2668 const Int_t kXLo = Constants::cfg.STRIP_SUM_SCATTER_CONFIG.X_LO;
2669 const Int_t kXHi = Constants::cfg.STRIP_SUM_SCATTER_CONFIG.X_HI;
2670 const Int_t kTracesPerRegion =
2672
2673 if (reac < kReacMin || reac > kReacMax) {
2674 std::cerr << "strip-sum-scatter: candidate reaction strip " << reac
2675 << " outside [" << kReacMin << "," << kReacMax
2676 << "]; skipping interactive overlay." << std::endl;
2677 return;
2678 }
2679 // A cut saved from a previous pass wins over prompting, so a run that has
2680 // already been decided once repeats without a person in the loop -- and
2681 // without a DISPLAY, since nothing interactive has to open.
2682 TCutG *cutAn = nullptr;
2683 TCutG *cutAa = nullptr;
2684 // Saved cuts -- drawn here earlier, or fitted by compute-regions -- win over
2685 // prompting, so a decided run repeats without a person in the loop.
2686 if (!Constants::cfg.STRIP_SUM_SCATTER_CONFIG.REGION_CUT_REDRAW) {
2687 cutAn = LoadRegionCut("region_an", reac);
2688 cutAa = LoadRegionCut("region_aa", reac);
2689 }
2690 TCanvas *cutCanvas = nullptr;
2691
2692 // The TApplication and its argv must outlive every GUI canvas made below,
2693 // which includes the trace canvases drawn after this block -- tearing it down
2694 // while those still exist faults in the ROOT paint path. So it is never
2695 // deleted: the process exits shortly after this function returns, and letting
2696 // it leak is the only way to keep it alive past the last canvas. The argv
2697 // storage is static for the same reason -- TApplication keeps the pointers we
2698 // hand it, and they must not dangle into a dead stack frame.
2699 static Int_t app_argc = 1;
2700 static char app_arg0[] = "strip-sum-scatter";
2701 static char *app_argv[] = {app_arg0};
2702
2703 if (cutAn && cutAa) {
2704 std::cout << " [region] loaded saved cuts for reac " << reac << std::endl;
2705 } else {
2706 delete cutAn;
2707 cutAn = nullptr;
2708 delete cutAa;
2709 cutAa = nullptr;
2710 if (!gSystem->Getenv("DISPLAY")) {
2711 std::cerr << "strip-sum-scatter: no saved region cuts for reac " << reac
2712 << " and no DISPLAY to draw them; skipping interactive "
2713 "region-trace overlay (scatters already saved)."
2714 << std::endl;
2715 return;
2716 }
2717 // Intentionally not stored and never deleted -- see the note above.
2718 new TApplication("strip-sum-scatter", &app_argc, app_argv);
2719 gROOT->SetBatch(kFALSE);
2720
2721 cutCanvas = new TCanvas("c_strip_sum_regions",
2722 "Draw (a,n) then (a,a') regions", 900, 700);
2723 cutCanvas->SetLogz(kTRUE); // match the saved scatter's z-scale
2724 m_scatter[reac]->Draw("COLZ");
2725 cutCanvas->Update();
2726 cutAn = PromptCut(cutCanvas, "region_an", "(a,n)");
2727 cutAa = PromptCut(cutCanvas, "region_aa", "(a,a')");
2728 SaveRegionCuts(reac, cutAn, cutAa);
2729
2730 cutCanvas->GetListOfPrimitives()->Remove(cutAn);
2731 cutCanvas->GetListOfPrimitives()->Remove(cutAa);
2732 gROOT->SetEditorMode();
2733 gSystem->ProcessEvents();
2734 // Stay in GUI mode for the rest of the run, and keep this canvas alive.
2735 //
2736 // PlotScatters() builds batch canvases earlier in this same process without
2737 // trouble, so batch canvas creation is fine on its own; what faults is
2738 // going back to batch once a TApplication and a GUI canvas exist.
2739 // TCanvas::Build() paints its border while constructing, and that paint
2740 // path is what dies in TPad::PaintBox. Deleting this canvas or nulling gPad
2741 // does not help -- only not re-entering batch does. The remaining canvases
2742 // open windows, which is fine for a command that already requires DISPLAY;
2743 // SaveFigure still writes them to disk, and the process exits shortly
2744 // after.
2745 cutCanvas->Clear();
2746 gSystem->ProcessEvents();
2747 }
2748
2749 // ClusterVarHists(reac, cutAa, cutAn, "strip_sum_scatter");
2750
2751 std::vector<TGraph *> tr_an, tr_aa, tr_beam;
2752 // Same selected events, raw (un-normalized) ADC -- one entry per normed
2753 // trace, kept in lock-step so the two overlays show the identical events.
2754 std::vector<TGraph *> tr_an_adc, tr_aa_adc, tr_beam_adc;
2755 // Same selected events again, Savitzky-Golay smoothed (normed a.u. space),
2756 // for the with-smoothing overlay -- also in lock-step with the raw normed
2757 // traces, so the two a.u. overlays show the identical events.
2758 const Bool_t kSkipSg =
2760 std::vector<TGraph *> tr_an_sg, tr_aa_sg, tr_beam_sg;
2761 UInt_t bit = (1u << ReacIndex(reac));
2762
2763 for (Int_t k = 0; k < Int_t(m_reservoir.size()); k++) {
2764 if (Int_t(tr_an.size()) >= kTracesPerRegion &&
2765 Int_t(tr_aa.size()) >= kTracesPerRegion &&
2766 Int_t(tr_beam.size()) >= kTracesPerRegion)
2767 break;
2768 const TraceEvt &e = m_reservoir[k];
2769 if (e.beam_flat && Int_t(tr_beam.size()) < kTracesPerRegion) {
2770 tr_beam.push_back(TraceFromTotal(e.total));
2771 tr_beam_adc.push_back(TraceFromTotal(e.total_adc));
2772 if (!kSkipSg)
2773 tr_beam_sg.push_back(SmoothedTraceFromTotal(e.total));
2774 continue;
2775 }
2776 if (!(e.reac_mask & bit))
2777 continue;
2778 Double_t td[18];
2779 for (Int_t s = 0; s < 18; s++)
2780 td[s] = Double_t(e.total[s]);
2781 Double_t x = 0.0, y = 0.0;
2782 PlaneXY(td, reac, x, y);
2783 if (cutAn && Int_t(tr_an.size()) < kTracesPerRegion &&
2784 cutAn->IsInside(x, y)) {
2785 tr_an.push_back(TraceFromTotal(e.total));
2786 tr_an_adc.push_back(TraceFromTotal(e.total_adc));
2787 if (!kSkipSg)
2788 tr_an_sg.push_back(SmoothedTraceFromTotal(e.total));
2789 } else if (cutAa && Int_t(tr_aa.size()) < kTracesPerRegion &&
2790 cutAa->IsInside(x, y)) {
2791 tr_aa.push_back(TraceFromTotal(e.total));
2792 tr_aa_adc.push_back(TraceFromTotal(e.total_adc));
2793 if (!kSkipSg)
2794 tr_aa_sg.push_back(SmoothedTraceFromTotal(e.total));
2795 }
2796 }
2797
2798 std::cout << "Sampled traces: beam=" << tr_beam.size()
2799 << " (a,a')=" << tr_aa.size() << " (a,n)=" << tr_an.size()
2800 << std::endl;
2801 if (Constants::cfg.STRIP_SUM_SCATTER_CONFIG.ALT_DECODE_REGION_TRACES)
2802 DrawAltDecodeRegionTraces(reac, cutAn, cutAa);
2803 DrawRegionTraces(Form("region_traces_reac%d", reac), "strip_sum_scatter",
2804 tr_beam, tr_aa, tr_an, 0.6, 1.6, "#DeltaE [a.u.]");
2805 DrawRegionMeanTraces(Form("region_mean_traces_reac%d", reac),
2806 "strip_sum_scatter", tr_beam, tr_aa, tr_an, 0.6, 1.6,
2807 "#DeltaE [a.u.]");
2808 Double_t adc_y_lo = 0.0, adc_y_hi = 0.0;
2809 TraceYRange(tr_beam_adc, tr_aa_adc, tr_an_adc, adc_y_lo, adc_y_hi);
2810 DrawRegionTraces(Form("region_traces_reac%d_adc", reac), "strip_sum_scatter",
2811 tr_beam_adc, tr_aa_adc, tr_an_adc, adc_y_lo, adc_y_hi,
2812 "#DeltaE [ADC]");
2813 DrawRegionMeanTraces(Form("region_mean_traces_reac%d_adc", reac),
2814 "strip_sum_scatter", tr_beam_adc, tr_aa_adc, tr_an_adc,
2815 adc_y_lo, adc_y_hi, "#DeltaE [ADC]");
2816 if (!kSkipSg) {
2817 DrawRegionTraces(Form("region_traces_reac%d_sg", reac), "strip_sum_scatter",
2818 tr_beam_sg, tr_aa_sg, tr_an_sg, 0.6, 1.6,
2819 "#DeltaE [a.u.]");
2820 DrawRegionMeanTraces(Form("region_mean_traces_reac%d_sg", reac),
2821 "strip_sum_scatter", tr_beam_sg, tr_aa_sg, tr_an_sg,
2822 0.7, 1.3, "#DeltaE [a.u.]");
2823 }
2824
2825 for (Int_t i = 0; i < Int_t(tr_an.size()); i++)
2826 delete tr_an[i];
2827 for (Int_t i = 0; i < Int_t(tr_aa.size()); i++)
2828 delete tr_aa[i];
2829 for (Int_t i = 0; i < Int_t(tr_beam.size()); i++)
2830 delete tr_beam[i];
2831 for (Int_t i = 0; i < Int_t(tr_an_adc.size()); i++)
2832 delete tr_an_adc[i];
2833 for (Int_t i = 0; i < Int_t(tr_aa_adc.size()); i++)
2834 delete tr_aa_adc[i];
2835 for (Int_t i = 0; i < Int_t(tr_beam_adc.size()); i++)
2836 delete tr_beam_adc[i];
2837 for (Int_t i = 0; i < Int_t(tr_an_sg.size()); i++)
2838 delete tr_an_sg[i];
2839 for (Int_t i = 0; i < Int_t(tr_aa_sg.size()); i++)
2840 delete tr_aa_sg[i];
2841 for (Int_t i = 0; i < Int_t(tr_beam_sg.size()); i++)
2842 delete tr_beam_sg[i];
2843
2844 delete cutAn;
2845 delete cutAa;
2846}
2847
2849 // Required before the threaded fill touches TChains from worker threads.
2850 ROOT::EnableThreadSafety();
2851 InitUtils::SetROOTPreferences(PlotSaveFormat::kPNG,
2852 Paths::ResultsDir() + "/plots",
2853 Paths::ResultsDir() + "/root_files");
2854 gROOT->SetBatch(kTRUE);
2855
2856 std::vector<Int_t> run_order;
2857 std::map<Int_t, TChain *> chain_by_run = FileSet::GroupEventsByRun(run_order);
2858 if (run_order.empty()) {
2859 std::cerr << "strip-sum-scatter: no runs found" << std::endl;
2860 return;
2861 }
2862
2863 // The jump gate and the upstream-beam tolerance are set in sigma of the
2864 // beam's noise, so the noise comes first: the fingerprint stamps the
2865 // thresholds they resolve to.
2866 Double_t jump_sigma[18], strip_sigma[18];
2867 if (!MeasureBeamNoise(chain_by_run[run_order[0]], jump_sigma, strip_sigma)) {
2868 std::cerr << "strip-sum-scatter: cannot measure the beam noise on run "
2869 << run_order[0] << std::endl;
2870 return;
2871 }
2872 SetJumpSigma(jump_sigma);
2873 SetStripSigma(strip_sigma);
2874 {
2875 const Int_t kReacMin =
2876 Constants::cfg.STRIP_SUM_SCATTER_CONFIG.REACTION_STRIP_MIN;
2877 const Int_t kReacMax =
2878 Constants::cfg.STRIP_SUM_SCATTER_CONFIG.REACTION_STRIP_MAX;
2879 TString line =
2880 Form("strip-sum-scatter: jump noise sigma from run %d:", run_order[0]);
2881 for (Int_t reac = kReacMin; reac <= kReacMax; reac++)
2882 line += Form(" s%d %.4f", reac, jump_sigma[reac]);
2883 std::cout << line << std::endl;
2884 line =
2885 Form("strip-sum-scatter: strip noise sigma from run %d:", run_order[0]);
2886 for (Int_t strip = 1; strip < kReacMax; strip++)
2887 line += Form(" s%d %.4f", strip, strip_sigma[strip]);
2888 std::cout << line << std::endl;
2889 std::cout << Form("strip-sum-scatter: jump gate %.2f sigma -> %.4f at "
2890 "strip %d, %.4f at strip %d",
2891 Constants::cfg.STRIP_SUM_SCATTER_CONFIG.REAC_JUMP_NSIGMA,
2892 JumpMin(kReacMin), kReacMin, JumpMin(kReacMax), kReacMax)
2893 << std::endl;
2894 }
2895
2896 // Build fingerprint and try cache.
2897 TString fingerprint = BuildFingerprint(run_order, chain_by_run);
2898 TString cache_name = CacheName();
2899
2900 Bool_t loaded = TryLoadCache(cache_name, fingerprint);
2901
2902 if (!loaded) {
2903 FillScatters(run_order, chain_by_run);
2904 WriteCache(cache_name, fingerprint);
2905 }
2906
2907 // Batch plotting (always done).
2908 PlotScatters();
2909
2910 // Optional diagnostic: grid of events rejected by the parity cut. Reads the
2911 // Grid branch directly, so it works whether or not the scatter cache was
2912 // freshly filled. Self-gated on PLOT_PARITY_REJECTED_GRID.
2913 PlotParityRejectedGrid(run_order, chain_by_run);
2914
2915 // Optional sim overlays.
2916 if (Constants::cfg.STRIP_SUM_SCATTER_CONFIG.RERUN_SIM) {
2917 SimOverlay();
2918 SimTraceOverlay();
2919 }
2920
2921 // Interactive region-trace overlay (requires DISPLAY).
2922 Int_t reac = Constants::cfg.STRIP_SUM_SCATTER_CONFIG.CANDIDATE_REAC_STRIP;
2923 InteractiveOverlay(reac);
2924
2925 // Cleanup chains.
2926 for (Int_t i = 0; i < Int_t(run_order.size()); i++)
2927 delete chain_by_run[run_order[i]];
2928}
std::mutex g_plot_mutex
Serialises all plotting and canvas work.
Definition FileSet.cpp:3
std::mutex g_log_mutex
Serialises multi-line progress logging from worker threads.
Definition FileSet.cpp:4
The reaction search: strip-sum scatters, beam gating and tagging.
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:20
static Bool_t InEllipseXY(const BeamFit2D &b, Double_t x, Double_t y, Double_t nx, Double_t ny)
Whether a point lies inside the fitted beam ellipse.
Definition BeamFit2D.cpp:3
StripSumScatterConfig STRIP_SUM_SCATTER_CONFIG
Bool_t IGNORE_STRIP_0
TString SIM_BEAM_FILE
Bool_t IGNORE_STRIP_17
Bool_t IGNORE_SHORT_STRIPS
static TGraph * BuildTraceFromTotals(const Double_t *total)
Build a trace graph from one event's per-strip totals.
static TString EventsName(const FileSpec &s)
Filename of the built-events ROOT file for a subfile.
Definition FileSet.cpp:289
static std::map< Int_t, TChain * > GroupEventsByRun(std::vector< Int_t > &run_order)
Chain every run's events files, grouped by run.
Definition FileSet.cpp:304
static std::vector< FileSpec > BuildProcessedFileSpecs()
Every subfile that already has processed output.
Definition FileSet.cpp:259
static Long64_t SampleStride(Long64_t n_total, Long64_t max_points)
Stride that visits at most max_points of n_total entries.
Definition FileSet.cpp:323
static TString DatasetDir()
Absolute path to the active dataset directory, analysis/<iso>.
Definition Paths.cpp:60
static TString ResultsDir()
Absolute path to the directory receiving generated output.
Definition Paths.cpp:24
static Bool_t IsEresTag(const TString &tag)
Whether a tag denotes an energy-resolution simulation.
Definition RemixSim.cpp:91
static TString SimRootPath(const SimFileSpec &s)
Absolute path to a simulation's ROOT file.
Definition RemixSim.cpp:67
static Int_t ReactionStripOf(const TString &tag)
Which strip a simulated reaction occurs on.
Definition RemixSim.cpp:71
static TString TagWithoutStrip(const TString &tag)
The tag with any trailing _s<N> reaction-strip token removed.
Definition RemixSim.cpp:84
static std::vector< SimFileSpec > BuildFileSpecs()
Every simulation this dataset defines.
Definition RemixSim.cpp:35
static TString CacheName()
Filename of the scatter cache for this configuration.
static Int_t YLoOf(Int_t reac)
First strip of the post-trigger window summed onto y.
static void SetStripSigma(const Double_t *sigma)
Install the per-strip sigmas.
static Int_t YHiOf(Int_t reac)
Last strip of the post-trigger window, inclusive.
static Bool_t PassesReaction(const EnergyView &ev, Int_t reac)
Whether an event is tagged as a reaction at a given strip.
static Double_t JumpMin(Int_t reac)
Minimum jump for a tag: REAC_JUMP_NSIGMA * JumpSigma(reac).
static Double_t StripSigma(Int_t strip)
Sigma of a strip's own deposit.
void Run()
Build or load the scatters, tag reactions, and draw everything.
static Double_t JumpSigma(Int_t strip)
Sigma of the strip-to-strip difference total[s] - total[s-1].
StripSumScatter()
Construct with empty scatters and no cache loaded.
~StripSumScatter()
Frees the scatters and the reservoir.
static void PlaneXY(const Double_t *total, Int_t reac, Double_t &x, Double_t &y)
Where an event sits in the scatter plane for a given reaction strip.
static void SetJumpSigma(const Double_t *sigma)
Install the jump sigmas.
Double_t ActiveGridMaxAdc()
Grid full scale, in ADC.
const DatasetConfig & cfg
The active dataset's configuration, flat block.
void Save(Int_t reac, TCutG *cut_an, TCutG *cut_aa, Double_t n_an_assigned=-1.0)
Write a strip's two region cuts.
TCutG * Load(const char *name, Int_t reac)
Load a cut, from either storage generation.
const Double_t kXMin
Lower x bound of the build window.
const Double_t kXMax
Upper x bound; x sums 16 strips.
const Double_t kYMin
Lower y bound.
const Double_t kYMax
Upper y bound; y sums the post-trigger strips only.
void Write(const TString &channel, const std::vector< TagEfficiencyRecord > &records, const TString &method)
Replace one channel's records, keeping every other channel's.
The classification ellipses defining a pure-beam event.
BeamFit2D s15_s16
Alternative exit ellipse.
BeamFit2D s0_s1
Entrance ellipse on strips 0 and 1.
Bool_t ok
Whether the fits succeeded.
Bool_t use_s15_s16
Which exit ellipse is in force.
BeamFit2D s16_s17
Exit ellipse on strips 16 and 17.
BeamFit2D s1_s2
Alternative entrance ellipse, per PURE_BEAM_GATE.
A fitted 2-D Gaussian beam spot.
Definition BeamFit2D.hpp:16
Double_t rho
Correlation coefficient, in [-1, 1].
Definition BeamFit2D.hpp:20
Double_t sigma_y
Widths along each axis.
Definition BeamFit2D.hpp:19
Double_t mu_x
Definition BeamFit2D.hpp:18
Double_t amp
Peak amplitude.
Definition BeamFit2D.hpp:17
Double_t mu_y
Centroid.
Definition BeamFit2D.hpp:18
Bool_t ok
Whether the fit converged.
Definition BeamFit2D.hpp:21
Double_t sigma_x
Definition BeamFit2D.hpp:19
A view over one event's energies, decoding raw ADC into calibrated units.
void Decode()
Decode the currently loaded entry into the value members.
UShort_t rightdE_adc[18]
Raw right-side strip ADC values.
Float_t gain_left[18]
Per-strip left-side gain.
Bool_t Attach(TTree *t)
Bind to an events tree and set up the branch addresses.
Double_t total[18]
Summed energy per strip, after strip_factor.
Float_t strip_factor[18]
Per-strip multiplicative alignment, pol3_reference / centroid, applied to total after the per-channel...
UShort_t left_0_17_adc[18]
Raw left-side strip ADC values.
Float_t gain_right[18]
Per-strip right-side gain.
Short_t grid_adc
Raw Frisch grid ADC value.
Double_t grid
Grid energy.
A pair of strips whose sums form one classification plane.
Int_t sx
Strip whose sum forms the x axis.
Int_t sy
Strip whose sum forms the y axis.
Double_t rho
Correlation coefficient, in [-1, 1].
Definition BeamFit2D.hpp:33
Double_t sigma_y
Weighted RMS widths.
Definition BeamFit2D.hpp:32
Double_t sigma_x
Definition BeamFit2D.hpp:32
Double_t mu_x
Definition BeamFit2D.hpp:31
Double_t mu_y
Weighted centroid.
Definition BeamFit2D.hpp:31
Double_t weight
Total weight included; zero means the range held nothing above threshold.
Definition BeamFit2D.hpp:34
TString label
Legend label.
TString file
Simulation ROOT file.
One run's filled scatters, reservoir and normalisation counts.
std::vector< Long64_t > normed_at
Per-strip denominator: beam particles that reached that strip under exactly the conditions a reaction...
Long64_t seen
Events examined.
Long64_t normed
Events surviving every cut applied before reaction tagging: the beam gates, the pileup,...
Long64_t gated
Events passing the beam gates.
std::vector< Long64_t > tagged
Events tagged at each reaction strip, indexed the same way.
std::vector< TH2F * > scatters
Private clones, one per reaction strip.
std::vector< TraceEvt > reservoir
Tagged events from this run.
One run's fitted beam gates.
std::vector< BeamFit2D > series_gates
One gate per active GateSpec.
Bool_t ok
Whether the fits succeeded.
BeamEllipses pure_beam
Entrance and exit ellipses.
Everything governing the reaction search in the strip-sum scatters.
Definition Constants.hpp:53
Double_t PARITY_ASYM_MAX
Reject events whose even strips and odd strips disagree by more than this fraction,...
std::map< Int_t, Int_t > POST_WINDOW_STRIPS
Definition Constants.hpp:67
Int_t POST_TRIGGER_SUM_STRIPS
Strips summed onto the scatter y-axis after the trigger strip: y spans reac+1 .
Definition Constants.hpp:65
Double_t REAC_JUMP_NSIGMA
Minimum jump at the reaction strip for a tag, in sigma of the measured strip-to-strip beam noise (Str...
Definition Constants.hpp:80
Bool_t PLOT_PARITY_REJECTED_GRID
Diagnostic only: when set, run an extra pass over the events (gated by PARITY_ASYM_MAX > 0) that fill...
Double_t REQUIRE_SMOOTHNESS_MAX_STEP
Definition Constants.hpp:75
Bool_t REQUIRE_BEAM_UPSTREAM_OF_REAC
Tolerance in sigma of each strip's measured beam spread (StripSumScatter::StripSigma).
Double_t BEAM_UPSTREAM_NSIGMA
Int_t REQUIRE_SMOOTHNESS_END_STRIP
Definition Constants.hpp:74
std::map< Int_t, std::pair< Double_t, Double_t > > Y_DISPLAY_RANGE
Per-reaction-strip y-axis display windows, overriding Y_DISPLAY_MIN/MAX for individual strips (displa...
Float_t total[18]
Calibrated per-strip totals.
Int_t both_mult
Split strips (1-16) with both ends above threshold.
Float_t total_adc[18]
Raw, un-normalised ADC sum per strip.
Float_t long_au[18]
Long end of each split strip.
ULong64_t seed_ts
Timestamp of the grid hit that seeded the event, from the events tree.
Bool_t beam_flat
Whether the trace looked flat, i.e. beam-like.
Float_t short_au[18]
Short end.
UInt_t reac_mask
Bit per reaction strip this event was tagged at.