MUSIC unknown
Analysis for the MUSIC active-target ionization chamber
Loading...
Searching...
No Matches
CrossSection.cpp
Go to the documentation of this file.
1#include "CrossSection.hpp"
2#include "BeamEnergies.hpp"
3#include "Constants.hpp"
4#include "InitUtils.hpp"
5#include "Paths.hpp"
6#include "PlottingUtils.hpp"
7#include "RegionCuts.hpp"
8#include "StripSumScatter.hpp"
9#include "TagEfficiency.hpp"
10#include <TAxis.h>
11#include <TCanvas.h>
12#include <TCutG.h>
13#include <TFile.h>
14#include <TGraph.h>
15#include <TGraphAsymmErrors.h>
16#include <TH1F.h>
17#include <TH2F.h>
18#include <TKey.h>
19#include <TLegend.h>
20#include <TMath.h>
21#include <TNamed.h>
22#include <TParameter.h>
23#include <TSystem.h>
24#include <cmath>
25#include <cstdio>
26#include <iostream>
27
28namespace {
29
30// Boltzmann constant [J/K] and Torr in Pa, for the ideal-gas number density.
31const Double_t kBoltzmann = 1.380649e-23;
32const Double_t kPaPerTorr = 133.322368;
33// MUSIC's geometry and fill temperature, the same for every dataset: the
34// active length of one anode strip along the beam, and room temperature.
35const Double_t kStripLengthCm = 1.578;
36const Double_t kGasTemperatureK = 293.0;
37// A barn is 1e-24 cm^2, so a millibarn is 1e-27 cm^2.
38const Double_t kCm2PerMb = 1.0e-27;
39
40// Marker styles and colours for the channels on a combined figure.
41const Int_t kChannelMarker[4] = {20, 21, 22, 23};
42const Int_t kChannelColor[4] = {kBlack, kGreen + 2, kMagenta + 2, kOrange + 7};
43
44} // namespace
45
46Double_t CrossSection::Point::Err() const {
47 return std::sqrt(stat * stat + sys * sys);
48}
49
50// "n", "2n", "pn", "a", "g": light particles leaving the compound nucleus,
51// each with an optional multiplicity digit in front. The residue is the
52// compound (beam + alpha) minus what left.
53Bool_t CrossSection::ExitResidue(const TString &exit, Int_t z_beam,
54 Int_t a_beam, Int_t &z, Int_t &a) {
55 Int_t dz = 0, da = 0, mult = 0;
56 for (Int_t i = 0; i < exit.Length(); i++) {
57 const char c = exit[i];
58 if (c >= '0' && c <= '9') {
59 mult = mult * 10 + (c - '0');
60 continue;
61 }
62 Int_t pz = 0, pa = 0;
63 switch (c) {
64 case 'n':
65 pz = 0;
66 pa = 1;
67 break;
68 case 'p':
69 pz = 1;
70 pa = 1;
71 break;
72 case 'd':
73 pz = 1;
74 pa = 2;
75 break;
76 case 't':
77 pz = 1;
78 pa = 3;
79 break;
80 case 'h':
81 pz = 2;
82 pa = 3;
83 break;
84 case 'a':
85 pz = 2;
86 pa = 4;
87 break;
88 case 'g':
89 pz = 0;
90 pa = 0;
91 break;
92 default:
93 return kFALSE;
94 }
95 const Int_t m = mult > 0 ? mult : 1;
96 dz += m * pz;
97 da += m * pa;
98 mult = 0;
99 }
100 if (mult > 0 || exit.Length() == 0)
101 return kFALSE;
102 z = z_beam + 2 - dz;
103 a = a_beam + 4 - da;
104 return z > 0 && a > z;
105}
106
108 if (ch.label.Length() > 0)
109 return ch.label;
110 if (ch.talys_exits.empty())
111 return "";
112 Bool_t all_neutron = kTRUE;
113 for (Int_t k = 0; k < Int_t(ch.talys_exits.size()); k++) {
114 TString e = ch.talys_exits[k];
115 e.ReplaceAll("n", "");
116 for (Int_t i = 0; i < e.Length(); i++)
117 if (e[i] < '0' || e[i] > '9')
118 all_neutron = kFALSE;
119 }
120 if (ch.talys_exits.size() == 1)
121 return "(#alpha, " + ch.talys_exits[0] + ")";
122 if (all_neutron)
123 return "(#alpha, xn)";
124 TString joined;
125 for (Int_t k = 0; k < Int_t(ch.talys_exits.size()); k++)
126 joined += (k ? ", " : "") + ch.talys_exits[k];
127 return "(#alpha, " + joined + ")";
128}
129
130Long64_t CrossSection::ReadCount(TFile &f, const char *name, Bool_t &ok) {
131 TParameter<Long64_t> *p = static_cast<TParameter<Long64_t> *>(f.Get(name));
132 if (!p) {
133 ok = kFALSE;
134 return 0;
135 }
136 return p->GetVal();
137}
138
139// The curve clipped to an energy window, for drawing.
140TGraph *CrossSection::Clipped(TGraph *g, Double_t e_lo, Double_t e_hi) {
141 std::vector<Double_t> x, y;
142 for (Int_t p = 0; p < g->GetN(); p++)
143 if (g->GetX()[p] >= e_lo && g->GetX()[p] <= e_hi) {
144 x.push_back(g->GetX()[p]);
145 y.push_back(g->GetY()[p]);
146 }
147 return x.empty() ? nullptr : new TGraph(Int_t(x.size()), &x[0], &y[0]);
148}
149
150// The effective energy of a strip spanning e_out..e_in: the energy at which
151// the model's cross section equals its average over the strip, so a thin
152// target at e_eff would give what the strip gives (Szegedi et al. 2021).
153// The beam's dE/dx varies by a few percent within one strip, so the average
154// is taken uniform in energy. Only the model's shape enters: a constant
155// factor on sigma cancels. Bisection on a rising curve; the midpoint when
156// there is no curve or the strip lies outside it.
157Double_t CrossSection::EffectiveEnergy(TGraph *axn, Double_t e_out,
158 Double_t e_in) {
159 const Double_t mid = 0.5 * (e_out + e_in);
160 if (!axn || axn->GetN() < 2)
161 return mid;
162 const Double_t lo = TMath::Min(e_out, e_in), hi = TMath::Max(e_out, e_in);
163 if (lo < axn->GetX()[0] || hi > axn->GetX()[axn->GetN() - 1])
164 return mid;
165 const Int_t kSteps = 200;
166 Double_t mean = 0.0;
167 for (Int_t k = 0; k < kSteps; k++)
168 mean += axn->Eval(lo + (k + 0.5) * (hi - lo) / kSteps);
169 mean /= Double_t(kSteps);
170 Double_t a = lo, b = hi;
171 if ((axn->Eval(a) - mean) * (axn->Eval(b) - mean) > 0.0)
172 return mid;
173 for (Int_t k = 0; k < 40; k++) {
174 const Double_t c = 0.5 * (a + b);
175 if ((axn->Eval(a) - mean) * (axn->Eval(c) - mean) <= 0.0)
176 b = c;
177 else
178 a = c;
179 }
180 return 0.5 * (a + b);
181}
182
183// The cut scaled about its own centroid; caller owns the copy. Scaling a
184// region drawn at N sigma by k puts it at kN sigma.
185TCutG *CrossSection::ScaledCut(TCutG *cut, Double_t scale) {
186 Double_t cx = 0.0, cy = 0.0;
187 for (Int_t p = 0; p < cut->GetN(); p++) {
188 cx += cut->GetX()[p];
189 cy += cut->GetY()[p];
190 }
191 cx /= Double_t(cut->GetN());
192 cy /= Double_t(cut->GetN());
193 TCutG *scaled = new TCutG(*cut);
194 for (Int_t p = 0; p < scaled->GetN(); p++)
195 scaled->SetPoint(p, cx + scale * (cut->GetX()[p] - cx),
196 cy + scale * (cut->GetY()[p] - cy));
197 return scaled;
198}
199
200// Events in the scatter whose bin centre falls inside the cut scaled by
201// `scale`. Counting at a few scales and correcting each by the fraction of a
202// Gaussian it should enclose measures how much of what the region holds is
203// not the component: a clean peak gives the same number at every scale,
204// contamination grows with area.
205Double_t CrossSection::CountInCut(TH2F *scatter, TCutG *cut, Double_t scale) {
206 TCutG *scaled = ScaledCut(cut, scale);
207 TAxis *ax = scatter->GetXaxis();
208 TAxis *ay = scatter->GetYaxis();
209 Double_t n = 0.0;
210 for (Int_t i = 1; i <= scatter->GetNbinsX(); i++)
211 for (Int_t j = 1; j <= scatter->GetNbinsY(); j++)
212 if (scaled->IsInside(ax->GetBinCenter(i), ay->GetBinCenter(j)))
213 n += scatter->GetBinContent(i, j);
214 delete scaled;
215 return n;
216}
217
218// Fraction of a bivariate Gaussian inside its n-sigma Mahalanobis ellipse.
219Double_t CrossSection::Enclosed(Double_t nsigma) {
220 return 1.0 - std::exp(-0.5 * nsigma * nsigma);
221}
222
223Bool_t CrossSection::LoadCache() {
224 const CrossSectionConfig &X = Constants::cfg.CROSS_SECTION_CONFIG;
225 TString path = IO::GetRootFilesBaseDir() + "/" + StripSumScatter::CacheName();
226 cache_ = new TFile(path, "READ");
227 if (cache_->IsZombie()) {
228 std::cerr << "cross-section: no scatter cache at " << path
229 << "; run strip-sum-scatter first" << std::endl;
230 return kFALSE;
231 }
232 Bool_t have = kTRUE;
233 n_seen_ = ReadCount(*cache_, "n_seen", have);
234 n_beam_ = ReadCount(*cache_, "n_normed", have);
235 if (!have || n_beam_ <= 0) {
236 std::cerr << "cross-section: the cache carries no normalization counts, so "
237 "it predates them; re-run strip-sum-scatter to refill"
238 << std::endl;
239 return kFALSE;
240 }
241 // Areal density of one strip: ideal gas at the fill conditions.
242 n_gas_ = X.GAS_PRESSURE_TORR * kPaPerTorr / (kBoltzmann * kGasTemperatureK) *
244 areal_ = n_gas_ * kStripLengthCm;
245 return kTRUE;
246}
247
248// The simulated beam's loss per strip and the energy entering strip 0, and
249// from them the centre-of-mass energy at the middle of every strip, so the
250// alignment to a published table can be found over the whole detector rather
251// than only the strips a cross section is reported for.
252Bool_t CrossSection::LoadBeam() {
253 if (!BeamEnergies::Profile(BeamEnergies::SimPath(), dE_, e_strip0_)) {
254 std::cerr << "cross-section: cannot read the simulated beam at "
255 << BeamEnergies::SimPath() << std::endl;
256 return kFALSE;
257 }
258 cm_frac_ = BeamEnergies::CmFraction();
259 Double_t e = e_strip0_;
260 for (Int_t s = 0; s <= 16; s++) {
261 e_mid_[s] = (e - 0.5 * dE_[s]) * cm_frac_;
262 e -= dE_[s];
263 }
264 return kTRUE;
265}
266
267// Every model in root_files/talys/talys_xs.root, in the config's order, with
268// every residual-production graph talys-xs wrote for it. Empty when there is
269// no file; the plot goes on without them. The label is presentation, so the
270// config's current one wins over the one stamped in the file; relabelling
271// never needs a TALYS rerun.
272void CrossSection::LoadTalys() {
273 const CrossSectionConfig &X = Constants::cfg.CROSS_SECTION_CONFIG;
274 const TString path = Paths::ResultsDir() + "/root_files/talys/talys_xs.root";
275 if (gSystem->AccessPathName(path))
276 return;
277 TFile f(path, "READ");
278 if (f.IsZombie())
279 return;
280 for (Int_t m = 0;; m++) {
281 TDirectory *d = f.GetDirectory(Form("m%d", m));
282 if (!d)
283 break;
284 std::map<std::pair<Int_t, Int_t>, TGraph *> graphs;
285 for (TIter it(d->GetListOfKeys()); TObject *k = it();) {
286 Int_t z = 0, a = 0;
287 if (sscanf(k->GetName(), "rp%3d%3d", &z, &a) != 2)
288 continue;
289 TGraph *g = dynamic_cast<TGraph *>(d->Get(k->GetName()));
290 if (g)
291 graphs[std::make_pair(z, a)] = static_cast<TGraph *>(g->Clone());
292 }
293 TNamed *label = dynamic_cast<TNamed *>(d->Get("label"));
294 talys_labels_.push_back(m < Int_t(X.TALYS_MODELS.size())
295 ? X.TALYS_MODELS[m].label
296 : label ? TString(label->GetTitle())
297 : TString(Form("TALYS model %d", m)));
298 talys_raw_.push_back(graphs);
299 }
300}
301
302std::vector<CrossSection::TalysCurve>
303CrossSection::ChannelCurves(const CrossSectionChannel &ch) const {
304 const CrossSectionConfig &X = Constants::cfg.CROSS_SECTION_CONFIG;
305 std::vector<TalysCurve> curves;
306 if (talys_raw_.empty())
307 return curves;
308 for (Int_t m = 0; m < Int_t(talys_raw_.size()); m++) {
309 std::map<Double_t, Double_t> sum;
310 TString used, missing;
311 for (Int_t k = 0; k < Int_t(ch.talys_exits.size()); k++) {
312 Int_t z = 0, a = 0;
313 ExitResidue(ch.talys_exits[k], X.BEAM_Z, X.BEAM_A, z, a);
314 std::map<std::pair<Int_t, Int_t>, TGraph *>::const_iterator it =
315 talys_raw_[m].find(std::make_pair(z, a));
316 if (it == talys_raw_[m].end()) {
317 missing += Form(" %s(Z=%d,A=%d)", ch.talys_exits[k].Data(), z, a);
318 continue;
319 }
320 for (Int_t p = 0; p < it->second->GetN(); p++)
321 sum[it->second->GetX()[p]] += it->second->GetY()[p];
322 used += Form(" %s(Z=%d,A=%d)", ch.talys_exits[k].Data(), z, a);
323 }
324 std::vector<Double_t> tx, ty;
325 for (std::map<Double_t, Double_t>::const_iterator it = sum.begin();
326 it != sum.end(); ++it)
327 if (it->second > 0.0) {
328 tx.push_back(it->first);
329 ty.push_back(it->second);
330 }
331 std::cout << "cross-section: " << ch.name << ": " << talys_labels_[m]
332 << ": summed" << (used.Length() ? used : " nothing")
333 << (missing.Length() ? "; not in the file:" + missing : "")
334 << std::endl;
335 if (tx.empty())
336 continue;
337 TalysCurve c;
338 c.label = talys_labels_[m];
339 c.axn = new TGraph(Int_t(tx.size()), &tx[0], &ty[0]);
340 curves.push_back(c);
341 }
342 return curves;
343}
344
345Bool_t CrossSection::Strip(const CrossSectionChannel &ch,
346 const std::vector<TalysCurve> &talys, Int_t reac,
347 Point &pt) {
348 const StripSumScatterConfig &C = Constants::cfg.STRIP_SUM_SCATTER_CONFIG;
349 const TString region = "region_" + ch.name;
350 TH2F *h = static_cast<TH2F *>(cache_->Get(Form("scatter_r%d", reac)));
351 TCutG *cut = RegionCutStore::Load(region, reac);
352 if (!h || !cut) {
353 std::cout << Form(" %2d no %s", reac,
354 h ? "region cut" : "scatter in the cache")
355 << std::endl;
356 delete cut;
357 return kFALSE;
358 }
359 pt.reac = reac;
360 // Beam particles that reached this strip under the same conditions a
361 // reaction here had to satisfy. Falls back to the flat count only if the
362 // cache predates the per-strip one.
363 Bool_t at_ok = kTRUE;
364 const Long64_t n_at = ReadCount(*cache_, Form("n_normed_r%d", reac), at_ok);
365 pt.n_denom = Double_t(at_ok && n_at > 0 ? n_at : n_beam_);
366
367 // Beam energy at the strip's entrance and exit: what entered strip 0, less
368 // each earlier strip's mean loss. Effective energy from the first model's
369 // shape; the others say how much it depends on the shape.
370 Double_t e_lab = e_strip0_;
371 for (Int_t s = 0; s < reac; s++)
372 e_lab -= dE_[s];
373 pt.e_in = e_lab * cm_frac_;
374 pt.e_out = (e_lab - dE_[reac]) * cm_frac_;
376 pt.e_eff = use_eff ? EffectiveEnergy(talys.empty() ? nullptr : talys[0].axn,
377 pt.e_out, pt.e_in)
378 : 0.5 * (pt.e_in + pt.e_out);
379 pt.e_eff_lo = pt.e_eff_hi = pt.e_eff;
380 for (Int_t m = 1; use_eff && m < Int_t(talys.size()); m++) {
381 const Double_t e = EffectiveEnergy(talys[m].axn, pt.e_out, pt.e_in);
382 pt.e_eff_lo = TMath::Min(pt.e_eff_lo, e);
383 pt.e_eff_hi = TMath::Max(pt.e_eff_hi, e);
384 }
385 if (use_eff && talys.size() > 1)
386 std::cout << Form(" E_cm,eff %.3f (midpoint %.3f); across the %zu "
387 "model shapes %.3f..%.3f -> energy systematic "
388 "+%.3f/-%.3f",
389 pt.e_eff, 0.5 * (pt.e_in + pt.e_out), talys.size(),
390 pt.e_eff_lo, pt.e_eff_hi, pt.e_eff_hi - pt.e_eff,
391 pt.e_eff - pt.e_eff_lo)
392 << std::endl;
393
394 const Double_t norm = pt.n_denom * areal_ * kCm2PerMb;
395 TagEfficiencyRecord eff;
396 if (TagEfficiencyStore::Load(ch.name, reac, eff) && eff.eff > 0.0) {
397 // The efficiency side's own count, unfolded: what the strip before fed
398 // into it by migration is removed first, then the loss is undone. The
399 // feed is only known when the strip before was itself unfolded.
400 const Double_t feed =
401 prev_reac_ == reac - 1 ? prev_migrate_ * prev_true_ : 0.0;
402 pt.n_reac = (eff.n_counted - feed) / eff.eff;
403 pt.sigma = pt.n_reac / norm;
404 pt.stat = eff.n_counted > 0.0 ? pt.sigma / std::sqrt(eff.n_counted) : 0.0;
405 pt.sys = pt.sigma * eff.eff_err / eff.eff;
406 prev_reac_ = reac;
407 prev_true_ = pt.n_reac;
408 prev_migrate_ = eff.migrate;
409 std::cout << Form(" %2d [%5.2f, %5.2f] %6.2f %6.0f %9.0f "
410 "%7.1f +- %.1f (%.1f stat, %.1f eff; counted %.0f, "
411 "eff %.3f, fed %.0f from strip %d)",
412 reac, pt.e_in, pt.e_out, pt.e_eff, pt.n_reac, pt.n_denom,
413 pt.sigma, pt.Err(), pt.stat, pt.sys, eff.n_counted,
414 eff.eff, feed, reac - 1)
415 << std::endl;
416 delete cut;
417 return kTRUE;
418 }
419 prev_reac_ = -1;
420
421 // No efficiency record: two estimates of the reaction count. The geometric
422 // one is everything inside the region, corrected for the fraction of a
423 // Gaussian it should enclose; the attributed one is what the mixture fit
424 // assigned to the reaction component (trimmed at 3 sigma, so corrected for
425 // that). Where the island is well off the beam ridge they agree; where the
426 // region also covers beam tail the geometric count runs away, since the
427 // beam's real tail is far heavier than any Gaussian and cannot be
428 // subtracted by model. The attributed count is the value; the two
429 // estimates' disagreement is the region systematic, because that
430 // disagreement is exactly the overlap ambiguity.
431 const Double_t n_raw = CountInCut(h, cut, 1.0);
432 const Bool_t band =
434 RegionFit band_fit;
435 if (band && RegionCutStore::LoadFit(reac, band_fit) && band_fit.ok) {
436 // A ridge band is not a Gaussian's ellipse: its count is what it holds,
437 // with no enclosed-fraction correction. The region systematic is the
438 // change in count when the band's lower edge moves by one conditional
439 // sigma either way, since that edge is where the beam tail ends.
440 pt.n_reac = n_raw;
441 pt.sigma = pt.n_reac / norm;
442 pt.stat = pt.n_reac > 0.0 ? pt.sigma / std::sqrt(pt.n_reac) : 0.0;
443 Double_t lo = pt.sigma, hi = pt.sigma;
444 for (Int_t k = -1; k <= 1; k += 2) {
445 TCutG *alt = RegionCutFinder::RidgeBandCut(
446 "region_an_alt", band_fit.beam, C.AN_RIDGE_NSIGMA_LO + k,
447 C.AN_RIDGE_NSIGMA_HI, band_fit.x_lo, band_fit.x_hi, band_fit.y_lo,
448 band_fit.y_hi);
449 const Double_t s = CountInCut(h, alt, 1.0) / norm;
450 delete alt;
451 lo = TMath::Min(lo, s);
452 hi = TMath::Max(hi, s);
453 }
454 pt.sys = 0.5 * (hi - lo);
455 std::cout << Form(" %2d [%5.2f, %5.2f] %6.2f %6.0f %9.0f "
456 "%7.1f +- %.1f (%.1f stat, %.1f region; band %.1f..%.1f "
457 "sigma above the ridge, edge +-1 sigma)",
458 reac, pt.e_in, pt.e_out, pt.e_eff, pt.n_reac, pt.n_denom,
459 pt.sigma, pt.Err(), pt.stat, pt.sys, C.AN_RIDGE_NSIGMA_LO,
461 << std::endl;
462 delete cut;
463 return kTRUE;
464 }
465 const Double_t est_geo = n_raw / Enclosed(C.AN_REGION_NSIGMA);
466 const Double_t n_assigned = RegionCutStore::LoadAssigned(region, reac);
467 const Bool_t have_fit = n_assigned >= 0.0;
468 const Double_t est_fit = have_fit ? n_assigned / Enclosed(3.0) : est_geo;
469 pt.n_reac = est_fit;
470 pt.sigma = pt.n_reac / norm;
471 pt.stat = pt.n_reac > 0.0 ? pt.sigma / std::sqrt(pt.n_reac) : 0.0;
472 if (have_fit) {
473 // Against the region's core: the 1-sigma ellipse (half the drawn region),
474 // corrected for the 39% it encloses. The full region's count is no check
475 // where it also covers tail, but the core is where the island dominates,
476 // so core and attributed counts should agree, and their disagreement is
477 // the honest size of the overlap ambiguity.
478 const Double_t est_core =
479 CountInCut(h, cut, 1.0 / C.AN_REGION_NSIGMA) / Enclosed(1.0);
480 pt.sys = std::fabs(est_core - est_fit) / norm;
481 } else {
482 // A drawn region without an attributed count: scale it by 0.75x and
483 // 1.25x, each corrected for its enclosed fraction.
484 Double_t lo = pt.sigma, hi = pt.sigma;
485 const Double_t kScale[2] = {0.75, 1.25};
486 for (Int_t k = 0; k < 2; k++) {
487 const Double_t s = CountInCut(h, cut, kScale[k]) /
488 Enclosed(kScale[k] * C.AN_REGION_NSIGMA) / norm;
489 lo = TMath::Min(lo, s);
490 hi = TMath::Max(hi, s);
491 }
492 pt.sys = 0.5 * (hi - lo);
493 }
494 std::cout << Form(" %2d [%5.2f, %5.2f] %6.2f %6.0f %9.0f "
495 "%7.1f +- %.1f (%.1f stat, %.1f region; in-region %.0f, "
496 "attributed %s)",
497 reac, pt.e_in, pt.e_out, pt.e_eff, pt.n_reac, pt.n_denom,
498 pt.sigma, pt.Err(), pt.stat, pt.sys, est_geo,
499 have_fit ? Form("%.0f", est_fit) : "none")
500 << std::endl;
501 delete cut;
502 return kTRUE;
503}
504
505// Against the published values. A published table is a list of strips, so
506// the two are aligned strip to row by the single integer offset that best
507// matches the energies over the whole table, not row by row: pairing each
508// strip with its nearest published energy silently pairs two strips with one
509// row when a dataset reaches energies the reference never reported.
510void CrossSection::CompareReference(const ChannelResult &r) const {
511 const std::vector<std::vector<Double_t>> &ref = r.ch->reference_xs;
512 if (ref.empty())
513 return;
514 const Int_t n_ref = Int_t(ref.size());
515 Int_t best_off = 0;
516 Double_t best_rms = 1.0e9;
517 for (Int_t off = 0; off + n_ref - 1 <= 16; off++) {
518 Double_t s = 0.0;
519 for (Int_t k = 0; k < n_ref; k++) {
520 const Double_t d = e_mid_[off + k] - ref[k][0];
521 s += d * d;
522 }
523 const Double_t rms = std::sqrt(s / Double_t(n_ref));
524 if (rms < best_rms) {
525 best_rms = rms;
526 best_off = off;
527 }
528 }
529 std::cout << std::endl;
530 std::cout << Form(" %s vs %s: first row is strip %d from the energies, rms "
531 "%.3f MeV",
532 r.ch->name.Data(), r.ch->reference_label.Data(), best_off,
533 best_rms)
534 << std::endl;
535 std::cout << " strip E_cm,eff published E this work [mb] "
536 "published [mb] ratio"
537 << std::endl;
538 for (Int_t i = 0; i < Int_t(r.points.size()); i++) {
539 const Point &pt = r.points[i];
540 const Int_t row = pt.reac - best_off;
541 if (row < 0 || row >= n_ref) {
542 std::cout << Form(" %2d %6.2f -- %7.1f "
543 " -- (upstream of the published range)",
544 pt.reac, pt.e_eff, pt.sigma)
545 << std::endl;
546 continue;
547 }
548 const Double_t v = ref[row][3];
549 std::cout << Form(" %2d %6.2f %6.2f %7.1f +- %-5.1f "
550 "%7.1f (%.1f) %5.2f",
551 pt.reac, pt.e_eff, ref[row][0], pt.sigma, pt.Err(), v,
552 ref[row][4], v > 0.0 ? pt.sigma / v : 0.0)
553 << std::endl;
554 }
555}
556
557// Excitation function(s), this work against the published points. The frame
558// spans every set drawn so the measured strips are seen in the context of the
559// whole published curve, not just the rows they happen to sit beside. One
560// channel: its own title. Several: the dataset alone, every channel labelled.
561void CrossSection::Draw(const std::vector<const ChannelResult *> &rs,
562 const TString &name) const {
563 Double_t fx_lo = 1.0e9, fx_hi = -1.0e9, fy_lo = 1.0e9, fy_hi = -1.0e9;
564 struct Series {
565 TGraphAsymmErrors *g;
566 TString label;
567 };
568 std::vector<Series> measured, published;
569 std::vector<std::pair<TGraph *, TString>> curves;
570 for (Int_t c = 0; c < Int_t(rs.size()); c++) {
571 const ChannelResult &r = *rs[c];
572 std::vector<Double_t> vx, vy, vexl, vexh, vey;
573 for (Int_t i = 0; i < Int_t(r.points.size()); i++) {
574 const Point &pt = r.points[i];
575 // The energy error is the strip's extent about the effective energy,
576 // asymmetric since that energy sits above the midpoint on a rising
577 // curve (the reference table's convention), with the effective
578 // energy's dependence on the model shape added in quadrature on each
579 // side.
580 vx.push_back(pt.e_eff);
581 vy.push_back(pt.sigma);
582 vexl.push_back(std::hypot(pt.e_eff - TMath::Min(pt.e_in, pt.e_out),
583 pt.e_eff - pt.e_eff_lo));
584 vexh.push_back(std::hypot(TMath::Max(pt.e_in, pt.e_out) - pt.e_eff,
585 pt.e_eff_hi - pt.e_eff));
586 vey.push_back(pt.Err());
587 fx_lo = TMath::Min(fx_lo, vx.back() - vexl.back());
588 fx_hi = TMath::Max(fx_hi, vx.back() + vexh.back());
589 fy_lo = TMath::Min(fy_lo, vy.back());
590 fy_hi = TMath::Max(fy_hi, vy.back());
591 }
592 if (vx.empty())
593 continue;
594 Series s;
595 s.g = new TGraphAsymmErrors(Int_t(vx.size()), &vx[0], &vy[0], &vexl[0],
596 &vexh[0], &vey[0], &vey[0]);
597 s.g->SetMarkerStyle(kChannelMarker[c % 4]);
598 s.g->SetMarkerColor(kChannelColor[c % 4]);
599 s.g->SetLineColor(kChannelColor[c % 4]);
600 s.label = rs.size() > 1 ? "Present Work " + Label(*r.ch) : "Present Work";
601 measured.push_back(s);
602 const std::vector<std::vector<Double_t>> &ref = r.ch->reference_xs;
603 if (!ref.empty()) {
604 std::vector<Double_t> rx, ry, rexl, rexh, rey;
605 for (Int_t k = 0; k < Int_t(ref.size()); k++) {
606 rx.push_back(ref[k][0]);
607 rexh.push_back(ref[k][1]);
608 rexl.push_back(ref[k][2]);
609 ry.push_back(ref[k][3]);
610 rey.push_back(ref[k][4]);
611 fx_lo = TMath::Min(fx_lo, ref[k][0]);
612 fx_hi = TMath::Max(fx_hi, ref[k][0]);
613 fy_lo = TMath::Min(fy_lo, ref[k][3]);
614 fy_hi = TMath::Max(fy_hi, ref[k][3]);
615 }
616 Series p;
617 p.g = new TGraphAsymmErrors(Int_t(rx.size()), &rx[0], &ry[0], &rexl[0],
618 &rexh[0], &rey[0], &rey[0]);
619 p.g->SetMarkerStyle(24 + (c % 4));
620 p.g->SetMarkerColor(kRed + 1);
621 p.g->SetLineColor(kRed + 1);
622 p.label = rs.size() > 1 ? r.ch->reference_label + " " + Label(*r.ch)
623 : r.ch->reference_label;
624 published.push_back(p);
625 }
626 // Hauser-Feshbach prediction, if talys-xs has written one. The first
627 // model in full, the shape checks dashed; on a combined figure only the
628 // first model per channel, in the channel's colour.
629 for (Int_t m = 0; m < Int_t(r.talys.size()); m++) {
630 if (rs.size() > 1 && m > 0)
631 break;
632 curves.push_back(std::make_pair(
633 r.talys[m].axn, rs.size() > 1 ? r.talys[m].label + " " + Label(*r.ch)
634 : r.talys[m].label));
635 }
636 }
637 if (measured.empty())
638 return;
639 TCanvas *c = PlottingUtils::GetConfiguredCanvas(kTRUE);
640 TH1F *frame =
641 c->DrawFrame(fx_lo - 0.4, 0.5 * fy_lo, fx_hi + 0.4, 3.0 * fy_hi);
642 frame->SetTitle(Form("%s%s;%s [MeV];#sigma [mb]", Paths::DatasetName().Data(),
643 rs.size() == 1 ? Label(*rs[0]->ch).Data() : "",
644 Constants::cfg.CROSS_SECTION_CONFIG.EFFECTIVE_ENERGY
645 ? "E_{c.m.,eff}"
646 : "E_{c.m.}"));
647 // Curves first so the points sit on top, and only over the frame's
648 // energies.
649 std::vector<std::pair<TGraph *, TString>> drawn;
650 for (Int_t k = 0; k < Int_t(curves.size()); k++) {
651 TGraph *g = Clipped(curves[k].first, fx_lo - 0.4, fx_hi + 0.4);
652 if (!g)
653 continue;
654 const Int_t ch = rs.size() > 1 ? k : 0;
655 g->SetLineColor(rs.size() > 1 ? kChannelColor[ch % 4] : kAzure + 1);
656 g->SetLineWidth(k == 0 || rs.size() > 1 ? 2 : 1);
657 g->SetLineStyle(k == 0 || rs.size() > 1 ? 1 : 2);
658 g->Draw("L SAME");
659 drawn.push_back(std::make_pair(g, curves[k].second));
660 }
661 for (Int_t k = 0; k < Int_t(published.size()); k++)
662 published[k].g->Draw("P SAME");
663 for (Int_t k = 0; k < Int_t(measured.size()); k++)
664 measured[k].g->Draw("P SAME");
665 // Bottom right is the one empty corner: the excitation function climbs to
666 // the upper right and the reference table starts at the lower left.
667 const Int_t n_entries =
668 Int_t(measured.size() + published.size() + drawn.size());
669 TLegend *leg =
670 PlottingUtils::AddLegend(0.42, 0.89, 0.16, 0.16 + 0.07 * n_entries);
671 for (Int_t k = 0; k < Int_t(measured.size()); k++)
672 leg->AddEntry(measured[k].g, measured[k].label, "pe");
673 for (Int_t k = 0; k < Int_t(published.size()); k++)
674 leg->AddEntry(published[k].g, published[k].label, "pe");
675 for (Int_t k = 0; k < Int_t(drawn.size()); k++)
676 leg->AddEntry(drawn[k].first, drawn[k].second, "l");
677 leg->Draw();
678 PlottingUtils::SaveFigure(c, name, "cross_section", PlotSaveOptions::kLOG);
679 delete c;
680}
681
682Bool_t CrossSection::RunChannel(const CrossSectionChannel &ch,
683 ChannelResult &out) {
684 const CrossSectionConfig &X = Constants::cfg.CROSS_SECTION_CONFIG;
685 out.ch = &ch;
686 for (Int_t k = 0; k < Int_t(ch.talys_exits.size()); k++) {
687 Int_t z = 0, a = 0;
688 if (!ExitResidue(ch.talys_exits[k], X.BEAM_Z, X.BEAM_A, z, a)) {
689 std::cerr << "cross-section: channel " << ch.name << ": exit \""
690 << ch.talys_exits[k]
691 << "\" does not parse (light particles n p d t h a g with an "
692 "optional multiplicity digit)"
693 << std::endl;
694 return kFALSE;
695 }
696 }
697 if (!talys_raw_.empty() && ch.talys_exits.empty()) {
698 std::cerr << "cross-section: channel " << ch.name
699 << ": TALYS models are declared but the channel names no exits"
700 << std::endl;
701 return kFALSE;
702 }
703 out.talys = ChannelCurves(ch);
704 std::cout << std::endl
705 << " " << ch.name << " " << Label(ch) << ", region_" << ch.name
706 << (out.talys.empty() ? " (no TALYS curve: midpoint energies)" : "")
707 << std::endl;
708 std::cout << " strip E_cm range [MeV] "
709 << (X.EFFECTIVE_ENERGY ? "E_cm,eff" : "E_cm,mid")
710 << " N_reac N_beam sigma [mb]" << std::endl;
711 prev_reac_ = -1;
712 for (Int_t reac = X.XS_STRIP_MIN; reac <= X.XS_STRIP_MAX; reac++) {
713 Point pt;
714 if (Strip(ch, out.talys, reac, pt))
715 out.points.push_back(pt);
716 }
717 if (out.points.empty()) {
718 std::cerr << "cross-section: channel " << ch.name
719 << ": no strip produced a cross section" << std::endl;
720 return kFALSE;
721 }
722 CompareReference(out);
723 std::vector<const ChannelResult *> one(1, &out);
724 Draw(one, "cross_section_" + ch.name);
725 return kTRUE;
726}
727
729 const CrossSectionConfig &X = Constants::cfg.CROSS_SECTION_CONFIG;
730 const StripSumScatterConfig &C = Constants::cfg.STRIP_SUM_SCATTER_CONFIG;
731 if (!(X.GAS_PRESSURE_TORR > 0.0) || X.BEAM_A <= 0) {
732 std::cerr << "cross-section: this dataset has no CROSS_SECTION_CONFIG "
733 "(gas pressure and beam are both required)"
734 << std::endl;
735 return kFALSE;
736 }
737 if (X.CHANNELS.empty()) {
738 std::cerr << "cross-section: this dataset declares no "
739 "CROSS_SECTION_CONFIG.CHANNELS"
740 << std::endl;
741 return kFALSE;
742 }
743 if (!LoadCache() || !LoadBeam())
744 return kFALSE;
745
746 std::cout << "cross-section: " << n_beam_
747 << " beam events past every pre-tag "
748 << "cut, of " << n_seen_ << " seen" << std::endl;
749 std::cout << Form(" gas %.0f Torr at %.0f K -> %.4e atoms/cm^3; strip "
750 "%.3f cm -> %.4e atoms/cm^2",
751 X.GAS_PRESSURE_TORR, kGasTemperatureK, n_gas_,
752 kStripLengthCm, areal_)
753 << std::endl;
754 const TString method = TagEfficiencyStore::Method();
755 if (method.Length() > 0)
756 std::cout << " tag efficiency: " << method
757 << " (strips with a record are unfolded by it)" << std::endl;
758 else
759 std::cout << Form(" no tag-efficiency store; regions at %.1f sigma "
760 "enclose %.1f%% of the fitted component and counts "
761 "are corrected for that only",
762 C.AN_REGION_NSIGMA, 100.0 * Enclosed(C.AN_REGION_NSIGMA))
763 << std::endl;
764 LoadTalys();
765 if (talys_raw_.empty())
766 std::cout << "cross-section: no TALYS graphs in root_files (run talys-xs); "
767 "midpoint energies, no overlay"
768 << std::endl;
769
770 std::vector<ChannelResult> results(X.CHANNELS.size());
771 std::vector<const ChannelResult *> done;
772 for (Int_t c = 0; c < Int_t(X.CHANNELS.size()); c++)
773 if (RunChannel(X.CHANNELS[c], results[c]))
774 done.push_back(&results[c]);
775 if (done.empty())
776 return kFALSE;
777 if (done.size() > 1)
778 Draw(done, "cross_section");
779 cache_->Close();
780 return kTRUE;
781}
The dataset configuration, and how it is layered.
Double_t TargetGasAtomsPerMolecule(TargetGas gas)
Atoms of the reacting species per molecule of fill gas.
Definition Constants.cpp:11
Absolute cross section per reaction strip, for every declared channel.
The reaction search: strip-sum scatters, beam gating and tagging.
The tag-efficiency correction, and its contract with the cross section.
static TString Label(const CrossSectionChannel &ch)
Display label for a channel.
Bool_t Run()
Run every channel and write the tables and figures.
static Bool_t ExitResidue(const TString &exit, Int_t z_beam, Int_t a_beam, Int_t &z, Int_t &a)
Residual nucleus left by a named exit channel.
StripSumScatterConfig STRIP_SUM_SCATTER_CONFIG
CrossSectionConfig CROSS_SECTION_CONFIG
static TString DatasetName()
The dataset's isotope name, e.g.
Definition Paths.cpp:22
static TString ResultsDir()
Absolute path to the directory receiving generated output.
Definition Paths.cpp:24
static TString CacheName()
Filename of the scatter cache for this configuration.
Bool_t Profile(const TString &path, Double_t *dE, Double_t &e_strip0)
Read the per-strip energy loss profile from a beam simulation.
Double_t CmFraction()
Centre-of-mass energy fraction for this dataset's reaction.
TString SimPath()
Path to the dataset's beam simulation file.
const DatasetConfig & cfg
The active dataset's configuration, flat block.
Bool_t LoadFit(Int_t reac, RegionFit &fit)
Read back a stored fit.
TCutG * Load(const char *name, Int_t reac)
Load a cut, from either storage generation.
Double_t LoadAssigned(const char *name, Int_t reac)
The attributed count stored with a cut.
Bool_t Load(const TString &channel, Int_t reac, TagEfficiencyRecord &record)
Read one record.
TString Method()
The method label stamped into the store.
One reaction channel the cross section is extracted for.
std::vector< TString > talys_exits
Which of the residual channels TALYS wrote make up this channel's curve, as exit channels by name: wh...
TString label
The reaction as it appears in the plot title after the dataset name, e.g.
TString name
Names the region cut (region_<name>), the tag-efficiency records and the output figure.
What the cross section needs about the experiment rather than the analysis.
Int_t XS_STRIP_MIN
Reaction strips to report a cross section for.
Double_t GAS_PRESSURE_TORR
At the pressure the gas was actually at rather than the nominal one.
Int_t BEAM_A
Beam mass number, for the lab-to-centre-of-mass conversion; its Z and element symbol name it to a rea...
Bool_t EFFECTIVE_ENERGY
Report each strip at its effective centre-of-mass energy (Szegedi et al.
std::vector< TalysModel > TALYS_MODELS
Hauser-Feshbach predictions from TALYS.
std::vector< CrossSectionChannel > CHANNELS
The reaction channels measured on this dataset.
Bool_t ok
Whether the fit succeeded. Check this first.
Everything governing the reaction search in the strip-sum scatters.
Definition Constants.hpp:53
AnRegionMode AN_REGION_MODE
Double_t AN_REGION_NSIGMA
compute-regions: the (a,n) region is the reaction component's AN_REGION_NSIGMA Mahalanobis ellipse fr...