mist-hep 0.1.0
ROOT-backed analysis helpers built on mist
Loading...
Searching...
No Matches
algebra.h
Go to the documentation of this file.
1// SPDX-License-Identifier: MIT
2//
3// mist/hep/graph/algebra.h — error-propagating algebra on TGraphErrors.
4//
5// Binary and unary operations on TGraphErrors that carry the uncertainty
6// through correctly. Header-only; every operation returns an owning
7// mist::hep::owned::root_ptr and leaves its inputs untouched.
8//
9// Salvaged and corrected from the ePIC SiPM-characterisation graphutils
10// (mist-hep:F-35..F-44). Notable fix: the original log10 propagated the
11// error as ey/y (the natural-log rule); the correct factor is ey/(y*ln10),
12// applied here.
13//
14// Where a second graph is combined with the first (ratio/product/
15// difference), it is linearly interpolated at the first graph's x values via
16// eval(); points outside the second graph's x-range are skipped. Inputs are
17// assumed sorted ascending in x.
18//
19#pragma once
20
21#include <array>
22#include <cmath>
23#include <utility>
24#include <vector>
25
26#include <TF1.h>
27#include <TGraphErrors.h>
28#include <TProfile.h>
29
30#include <mist/hep/owned.h>
31
33
34namespace owned = ::mist::hep::owned;
35
36// ---------------------------------------------------------------------------
37// eval: linear interpolation of a TGraphErrors at x, WITH error propagation.
38// Returns {value, error}. ROOT's TGraph::Eval gives no error; this does.
39//
40// Out of range (x below the first point or above the last) returns
41// {NaN, NaN}. Assumes the graph is sorted ascending in x.
42// ---------------------------------------------------------------------------
43[[nodiscard]] inline std::pair<double, double>
44eval(const TGraphErrors& g, double x)
45{
46 const int n = g.GetN();
47 const double nan = std::nan("");
48 if (n == 0) return {nan, nan};
49 if (x < g.GetPointX(0) || x > g.GetPointX(n - 1)) return {nan, nan};
50
51 for (int i = 1; i < n; ++i) {
52 const double x0 = g.GetPointX(i - 1);
53 const double x1 = g.GetPointX(i);
54 if (x >= x0 && x <= x1) {
55 if (x1 == x0) return {g.GetPointY(i), g.GetErrorY(i)};
56 const double w1 = (x - x0) / (x1 - x0);
57 const double w0 = (x1 - x) / (x1 - x0);
58 const double y = w1 * g.GetPointY(i) + w0 * g.GetPointY(i - 1);
59 const double e1 = w1 * g.GetErrorY(i);
60 const double e0 = w0 * g.GetErrorY(i - 1);
61 return {y, std::sqrt(e0 * e0 + e1 * e1)};
62 }
63 }
64 return {nan, nan};
65}
66
67// ---------------------------------------------------------------------------
68// eval_with_errors: like eval(), but returns {value, error} as a std::array
69// (the form some callers and the conventions doc expect).
70// ---------------------------------------------------------------------------
71[[nodiscard]] inline std::array<double, 2>
72eval_with_errors(const TGraphErrors& g, double x_target)
73{
74 const auto [y, ey] = eval(g, x_target);
75 return {y, ey};
76}
77
78// ---------------------------------------------------------------------------
79// add: shift every y by a constant (with optional uncertainty on it).
80// y' = y + addend ; ey' = sqrt(ey^2 + addend_error^2)
81// ---------------------------------------------------------------------------
82[[nodiscard]] inline owned::root_ptr<TGraphErrors>
83add(const TGraphErrors& g, double addend, double addend_error = 0.0)
84{
85 auto out = owned::make<TGraphErrors>();
86 for (int i = 0; i < g.GetN(); ++i) {
87 const double ey = g.GetErrorY(i);
88 out->SetPoint(i, g.GetPointX(i), g.GetPointY(i) + addend);
89 out->SetPointError(i, g.GetErrorX(i),
90 std::sqrt(ey * ey + addend_error * addend_error));
91 }
92 return out;
93}
94
95// ---------------------------------------------------------------------------
96// offset: shift every y by add[0] with uncertainty add[1] — the {value,error}
97// pair form of add().
98// ---------------------------------------------------------------------------
99[[nodiscard]] inline owned::root_ptr<TGraphErrors>
100offset(const TGraphErrors& g, std::array<double, 2> add_value)
101{
102 return add(g, add_value[0], add_value[1]);
103}
104
105// ---------------------------------------------------------------------------
106// power: raise every y to an exponent.
107// y' = y^p ; ey' = |p| * |y|^(p-1) * ey
108// Points with y == 0 and p < 0 are skipped (the value would diverge).
109// ---------------------------------------------------------------------------
110[[nodiscard]] inline owned::root_ptr<TGraphErrors>
111power(const TGraphErrors& g, double exponent)
112{
113 auto out = owned::make<TGraphErrors>();
114 for (int i = 0; i < g.GetN(); ++i) {
115 const double y = g.GetPointY(i);
116 if (exponent < 0.0 && y == 0.0) continue;
117 const double yp = std::pow(y, exponent);
118 const double ey = (y == 0.0) ? 0.0
119 : std::fabs(exponent) * std::fabs(yp / y) * g.GetErrorY(i);
120 const int n = out->GetN();
121 out->SetPoint(n, g.GetPointX(i), yp);
122 out->SetPointError(n, g.GetErrorX(i), ey);
123 }
124 return out;
125}
126
127// ---------------------------------------------------------------------------
128// scale_values: multiply every y by a scalar factor, optionally with an
129// uncertainty on the factor (relative errors added in quadrature). Distinct
130// from graph::scale (which scales the x/y *axes*); this scales the values by
131// a measured constant.
132// y' = factor * y
133// ey' = |y'| * sqrt((ey/y)^2 + (factor_error/factor)^2) if applicable
134// = |factor| * ey otherwise
135// ---------------------------------------------------------------------------
136[[nodiscard]] inline owned::root_ptr<TGraphErrors>
137scale_values(const TGraphErrors& g, double factor, double factor_error = 0.0)
138{
139 auto out = owned::make<TGraphErrors>();
140 for (int i = 0; i < g.GetN(); ++i) {
141 const double y = g.GetPointY(i);
142 const double ey = g.GetErrorY(i);
143 const double y_new = factor * y;
144 double ey_new;
145 if (factor_error == 0.0 || y == 0.0 || factor == 0.0) {
146 ey_new = std::fabs(factor) * ey;
147 } else {
148 const double rel_y = ey / y;
149 const double rel_f = factor_error / factor;
150 ey_new = std::fabs(y_new) * std::sqrt(rel_y * rel_y + rel_f * rel_f);
151 }
152 out->SetPoint(i, g.GetPointX(i), y_new);
153 out->SetPointError(i, g.GetErrorX(i), ey_new);
154 }
155 return out;
156}
157
158// ---------------------------------------------------------------------------
159// log / log10: natural and base-10 logarithm of every y (y > 0 required;
160// non-positive points are skipped).
161// ln: y' = ln(y) ; ey' = ey / y
162// log10: y' = log10(y) ; ey' = ey / (y * ln 10)
163// ---------------------------------------------------------------------------
164[[nodiscard]] inline owned::root_ptr<TGraphErrors>
165log(const TGraphErrors& g)
166{
167 auto out = owned::make<TGraphErrors>();
168 for (int i = 0; i < g.GetN(); ++i) {
169 const double y = g.GetPointY(i);
170 if (y <= 0.0) continue;
171 const int n = out->GetN();
172 out->SetPoint(n, g.GetPointX(i), std::log(y));
173 out->SetPointError(n, g.GetErrorX(i), g.GetErrorY(i) / y);
174 }
175 return out;
176}
177
178[[nodiscard]] inline owned::root_ptr<TGraphErrors>
179log10(const TGraphErrors& g)
180{
181 static const double ln10 = std::log(10.0);
182 auto out = owned::make<TGraphErrors>();
183 for (int i = 0; i < g.GetN(); ++i) {
184 const double y = g.GetPointY(i);
185 if (y <= 0.0) continue;
186 const int n = out->GetN();
187 out->SetPoint(n, g.GetPointX(i), std::log10(y));
188 out->SetPointError(n, g.GetErrorX(i), g.GetErrorY(i) / (y * ln10));
189 }
190 return out;
191}
192
193// ---------------------------------------------------------------------------
194// ratio: numerator / denominator, the denominator interpolated at the
195// numerator's x values. Points outside the denominator's range, or where
196// either value is zero, are skipped.
197// r = yn / yd
198// er = |r| * sqrt((eyn/yn)^2 + (eyd/yd)^2) if propagate_error
199// = eyn / yd otherwise
200// ---------------------------------------------------------------------------
201[[nodiscard]] inline owned::root_ptr<TGraphErrors>
202ratio(const TGraphErrors& numerator, const TGraphErrors& denominator,
203 bool propagate_error = true)
204{
205 auto out = owned::make<TGraphErrors>();
206 for (int i = 0; i < numerator.GetN(); ++i) {
207 const double x = numerator.GetPointX(i);
208 const double yn = numerator.GetPointY(i);
209 const auto [yd, eyd] = eval(denominator, x);
210 if (std::isnan(yd) || yd == 0.0 || yn == 0.0) continue;
211 const double eyn = numerator.GetErrorY(i);
212 const double r = yn / yd;
213 double er;
214 if (propagate_error) {
215 const double rel_n = eyn / yn;
216 const double rel_d = eyd / yd;
217 er = std::fabs(r) * std::sqrt(rel_n * rel_n + rel_d * rel_d);
218 } else {
219 er = eyn / yd;
220 }
221 const int n = out->GetN();
222 out->SetPoint(n, x, r);
223 out->SetPointError(n, numerator.GetErrorX(i), er);
224 }
225 return out;
226}
227
228// ---------------------------------------------------------------------------
229// product: a * b, the second interpolated at the first's x values.
230// p = ya * yb ; ep = |p| * sqrt((eya/ya)^2 + (eyb/yb)^2)
231// ---------------------------------------------------------------------------
232[[nodiscard]] inline owned::root_ptr<TGraphErrors>
233product(const TGraphErrors& a, const TGraphErrors& b,
234 bool propagate_error = true)
235{
236 auto out = owned::make<TGraphErrors>();
237 for (int i = 0; i < a.GetN(); ++i) {
238 const double x = a.GetPointX(i);
239 const double ya = a.GetPointY(i);
240 const auto [yb, eyb] = eval(b, x);
241 if (std::isnan(yb) || ya == 0.0 || yb == 0.0) continue;
242 const double eya = a.GetErrorY(i);
243 const double p = ya * yb;
244 double ep;
245 if (propagate_error) {
246 const double rel_a = eya / ya;
247 const double rel_b = eyb / yb;
248 ep = std::fabs(p) * std::sqrt(rel_a * rel_a + rel_b * rel_b);
249 } else {
250 ep = eya * yb;
251 }
252 const int n = out->GetN();
253 out->SetPoint(n, x, p);
254 out->SetPointError(n, a.GetErrorX(i), ep);
255 }
256 return out;
257}
258
259// ---------------------------------------------------------------------------
260// difference: a - b, the second interpolated at the first's x values.
261// d = ya - yb ; ed = sqrt(eya^2 + eyb^2) if propagate_error, else eya
262// ---------------------------------------------------------------------------
263[[nodiscard]] inline owned::root_ptr<TGraphErrors>
264difference(const TGraphErrors& a, const TGraphErrors& b,
265 bool propagate_error = true)
266{
267 auto out = owned::make<TGraphErrors>();
268 for (int i = 0; i < a.GetN(); ++i) {
269 const double x = a.GetPointX(i);
270 const auto [yb, eyb] = eval(b, x);
271 if (std::isnan(yb)) continue;
272 const double eya = a.GetErrorY(i);
273 const double ed = propagate_error ? std::sqrt(eya * eya + eyb * eyb) : eya;
274 const int n = out->GetN();
275 out->SetPoint(n, x, a.GetPointY(i) - yb);
276 out->SetPointError(n, a.GetErrorX(i), ed);
277 }
278 return out;
279}
280
281// ---------------------------------------------------------------------------
282// difference (vs a function): subtract f(x) from every y. The function is
283// treated as exact, so the y-errors are carried through unchanged.
284// ---------------------------------------------------------------------------
285[[nodiscard]] inline owned::root_ptr<TGraphErrors>
286difference(const TGraphErrors& g, const TF1& f)
287{
288 auto out = owned::make<TGraphErrors>();
289 // TF1::Eval is non-const in older ROOT; take a mutable copy of the pointer.
290 auto& fn = const_cast<TF1&>(f);
291 for (int i = 0; i < g.GetN(); ++i) {
292 const double x = g.GetPointX(i);
293 out->SetPoint(i, x, g.GetPointY(i) - fn.Eval(x));
294 out->SetPointError(i, g.GetErrorX(i), g.GetErrorY(i));
295 }
296 return out;
297}
298
299// ---------------------------------------------------------------------------
300// trim: copy keeping only the points whose x lies in [min_x, max_x].
301// (Non-mutating rewrite of the SiPM remove_points, which edited in place.)
302// ---------------------------------------------------------------------------
303[[nodiscard]] inline owned::root_ptr<TGraphErrors>
304trim(const TGraphErrors& g, double min_x, double max_x)
305{
306 auto out = owned::make<TGraphErrors>();
307 for (int i = 0; i < g.GetN(); ++i) {
308 const double x = g.GetPointX(i);
309 if (x < min_x || x > max_x) continue;
310 const int n = out->GetN();
311 out->SetPoint(n, x, g.GetPointY(i));
312 out->SetPointError(n, g.GetErrorX(i), g.GetErrorY(i));
313 }
314 return out;
315}
316
317// ---------------------------------------------------------------------------
318// mean_of: point-wise mean across several graphs that share a point layout.
319// Point i of the result is the mean of point i over all input graphs, with
320// the error set to the standard error of the mean (population stdev / sqrt N).
321// Uses the shortest input graph's length and the first graph's x values.
322// ---------------------------------------------------------------------------
323[[nodiscard]] inline owned::root_ptr<TGraphErrors>
324mean_of(const std::vector<TGraphErrors*>& graphs)
325{
326 auto out = owned::make<TGraphErrors>();
327 if (graphs.empty() || !graphs.front()) return out;
328
329 int n_points = graphs.front()->GetN();
330 for (auto* g : graphs) {
331 if (!g) return out;
332 n_points = std::min(n_points, g->GetN());
333 }
334 const double count = static_cast<double>(graphs.size());
335
336 for (int i = 0; i < n_points; ++i) {
337 double sum = 0.0;
338 for (auto* g : graphs) sum += g->GetPointY(i);
339 const double mean = sum / count;
340
341 double var = 0.0;
342 for (auto* g : graphs) {
343 const double d = g->GetPointY(i) - mean;
344 var += d * d;
345 }
346 var /= count;
347 const double sem = std::sqrt(var / count); // standard error of the mean
348
349 out->SetPoint(i, graphs.front()->GetPointX(i), mean);
350 out->SetPointError(i, 0.0, sem);
351 }
352 return out;
353}
354
355// ---------------------------------------------------------------------------
356// average_rms: bin the scattered (x, y) points in x and report, per populated
357// bin, the mean and RMS (spread) of y. Backed by a TProfile in "S" mode, so
358// the bin error is the standard deviation of the points in the bin. Output:
359// one point per populated bin at (bin centre, mean) with y-error = RMS.
360//
361// Out-of-range x lands in the TProfile under/overflow bins, which are not
362// iterated here (only physical bins 1..N), so they are skipped naturally —
363// no -1 bin is ever indexed.
364// ---------------------------------------------------------------------------
365[[nodiscard]] inline owned::root_ptr<TGraphErrors>
366average_rms(const TGraphErrors& g, int n_bins, double x_min, double x_max)
367{
368 auto out = owned::make<TGraphErrors>();
369 if (n_bins <= 0 || !(x_max > x_min)) return out;
370
371 TProfile profile("", "", n_bins, x_min, x_max, "S");
372 profile.SetDirectory(nullptr); // do not let gDirectory own this scratch object
373 for (int i = 0; i < g.GetN(); ++i)
374 profile.Fill(g.GetPointX(i), g.GetPointY(i));
375
376 for (int b = 1; b <= profile.GetNbinsX(); ++b)
377 {
378 if (profile.GetBinEntries(b) <= 0.0) continue; // skip empty bins
379 const int n = out->GetN();
380 out->SetPoint(n, profile.GetBinCenter(b), profile.GetBinContent(b));
381 out->SetPointError(n, 0.0, profile.GetBinError(b)); // "S" -> RMS spread
382 }
383 return out;
384}
385
386// ---------------------------------------------------------------------------
387// derivative: discrete derivative of a graph (F-19). Each consecutive pair
388// (x0,y0),(x1,y1) yields one point at the midpoint with slope
389// `factor * (y1 - y0) / (x1 - x0)`. `factor` defaults to 1 (set -1 to negate,
390// e.g. for inverse-log-derivative conventions). Pairs with x1 == x0 are
391// skipped. Resolves the long-deferred central/forward-difference item; the
392// SiPM-original error-bar variant was inconsistent, so this is the clean
393// value-only TGraph form.
394// ---------------------------------------------------------------------------
395[[nodiscard]] inline owned::root_ptr<TGraph>
396derivative(const TGraph& g, double factor = 1.0)
397{
398 auto out = owned::make<TGraph>();
399 for (int i = 0; i + 1 < g.GetN(); ++i) {
400 const double x0 = g.GetPointX(i);
401 const double x1 = g.GetPointX(i + 1);
402 if (x1 == x0) continue;
403 const double slope = factor * (g.GetPointY(i + 1) - g.GetPointY(i)) / (x1 - x0);
404 out->SetPoint(out->GetN(), 0.5 * (x0 + x1), slope);
405 }
406 return out;
407}
408
409} // namespace mist::hep::graph
Definition algebra.h:32
owned::root_ptr< TGraphErrors > add(const TGraphErrors &g, double addend, double addend_error=0.0)
Definition algebra.h:83
owned::root_ptr< TGraphErrors > log10(const TGraphErrors &g)
Definition algebra.h:179
owned::root_ptr< TGraphErrors > power(const TGraphErrors &g, double exponent)
Definition algebra.h:111
std::pair< double, double > eval(const TGraphErrors &g, double x)
Definition algebra.h:44
owned::root_ptr< TGraphErrors > offset(const TGraphErrors &g, std::array< double, 2 > add_value)
Definition algebra.h:100
@ nan
Emit the point with a NaN y-value.
owned::root_ptr< TGraphErrors > difference(const TGraphErrors &a, const TGraphErrors &b, bool propagate_error=true)
Definition algebra.h:264
owned::root_ptr< TGraph > derivative(const TGraph &g, double factor=1.0)
Definition algebra.h:396
owned::root_ptr< TGraphErrors > log(const TGraphErrors &g)
Definition algebra.h:165
owned::root_ptr< TGraphErrors > product(const TGraphErrors &a, const TGraphErrors &b, bool propagate_error=true)
Definition algebra.h:233
owned::root_ptr< TGraphErrors > mean_of(const std::vector< TGraphErrors * > &graphs)
Definition algebra.h:324
owned::root_ptr< TGraphErrors > scale_values(const TGraphErrors &g, double factor, double factor_error=0.0)
Definition algebra.h:137
owned::root_ptr< TGraphErrors > ratio(const TGraphErrors &numerator, const TGraphErrors &denominator, bool propagate_error=true)
Definition algebra.h:202
owned::root_ptr< TGraphErrors > average_rms(const TGraphErrors &g, int n_bins, double x_min, double x_max)
Definition algebra.h:366
std::array< double, 2 > eval_with_errors(const TGraphErrors &g, double x_target)
Definition algebra.h:72
owned::root_ptr< TGraphErrors > trim(const TGraphErrors &g, double min_x, double max_x)
Definition algebra.h:304
Definition owned.h:33
std::unique_ptr< T, root_deleter > root_ptr
Definition owned.h:60
root_ptr< T > make(Args &&... args)
Definition owned.h:67