mist-hep 0.1.0
ROOT-backed analysis helpers built on mist
Loading...
Searching...
No Matches
histo.h
Go to the documentation of this file.
1// SPDX-License-Identifier: MIT
2//
3// mist/hep/histo/histo.h — histogram introspection and basic manipulation.
4//
5// Foundational subset of the BLU histogram engine port (mist-hep:F-20..F-27).
6// Header-only. ROOT-typed; depends on mist::logger for diagnostics and
7// mist::hep::stats for error propagation.
8//
9// The heavier BLU histogram items — rebin (F-24), randomize_points (F-28),
10// the N-dimensional TFile loader (F-29), add_sum (F-30), transpose (F-31) —
11// are deferred to a follow-up; see DISCUSSION.md. Each needs either careful
12// re-derivation (the originals never compiled) or TFile test fixtures.
13//
14#pragma once
15
16#include <cmath>
17#include <string>
18#include <type_traits>
19#include <vector>
20
21#include <TH1.h>
22#include <TH2.h>
23#include <TH3.h>
24#include <TH1F.h>
25
26#include <mist/logger/logger.h>
27#include <mist/hep/owned.h>
28#include <mist/hep/stats.h>
29
30namespace mist::hep::histo {
31
32namespace owned = ::mist::hep::owned;
33
34// ===========================================================================
35// Introspection (F-20 / F-21 / F-22)
36// ===========================================================================
37
38// ---------------------------------------------------------------------------
39// dimension: runtime axis count of a histogram (1, 2, or 3), or -1 on error
40// (null, or not a histogram). Replaces BLU uGetTHDimension; routes the error
41// path through mist::logger instead of raw cout.
42// ---------------------------------------------------------------------------
43template <typename TH>
44[[nodiscard]] int dimension(const TH* h)
45{
46 if (!h) {
47 mist::logger::error("(histo::dimension) target is null");
48 return -1;
49 }
50 const auto* h1 = dynamic_cast<const TH1*>(h);
51 if (!h1) {
52 mist::logger::error("(histo::dimension) target is not a TH1-derived histogram");
53 return -1;
54 }
55 if (dynamic_cast<const TH3*>(h)) return 3;
56 if (dynamic_cast<const TH2*>(h)) return 2;
57 return 1;
58}
59
60// ---------------------------------------------------------------------------
61// pair_dimension: common dimension of two histograms, or -1 if they disagree
62// or either is invalid.
63// ---------------------------------------------------------------------------
64template <typename TH1Type, typename TH2Type>
65[[nodiscard]] int pair_dimension(const TH1Type* a, const TH2Type* b)
66{
67 const int da = dimension(a);
68 const int db = dimension(b);
69 if (da < 0 || db < 0) return -1;
70 if (da != db) {
71 mist::logger::error("(histo::pair_dimension) dimensions disagree");
72 return -1;
73 }
74 return da;
75}
76
77// ---------------------------------------------------------------------------
78// is_consistent: true when two histograms share the same dimension, the same
79// per-axis bin counts, and the same per-bin low edges.
80// ---------------------------------------------------------------------------
81template <typename TH1Type, typename TH2Type>
82[[nodiscard]] bool is_consistent(const TH1Type* a, const TH2Type* b)
83{
84 if (pair_dimension(a, b) < 0) return false;
85 if (a->GetNbinsX() != b->GetNbinsX()) return false;
86 if (a->GetNbinsY() != b->GetNbinsY()) return false;
87 if (a->GetNbinsZ() != b->GetNbinsZ()) return false;
88 for (int i = 1; i <= a->GetNbinsX(); ++i)
89 if (a->GetXaxis()->GetBinLowEdge(i) != b->GetXaxis()->GetBinLowEdge(i))
90 return false;
91 for (int j = 1; j <= a->GetNbinsY(); ++j)
92 if (a->GetYaxis()->GetBinLowEdge(j) != b->GetYaxis()->GetBinLowEdge(j))
93 return false;
94 for (int k = 1; k <= a->GetNbinsZ(); ++k)
95 if (a->GetZaxis()->GetBinLowEdge(k) != b->GetZaxis()->GetBinLowEdge(k))
96 return false;
97 return true;
98}
99
100// ===========================================================================
101// Binning helpers (F-25)
102// ===========================================================================
103
104// ---------------------------------------------------------------------------
105// uniform_binning: bin edges for [low, high] split into bins of width `width`.
106// Returns the edge array as a std::vector<double> (the BLU original leaked a
107// raw new[]). If `high` is not an integer number of widths above `low`, the
108// last edge is rounded up and a warning is logged.
109//
110// ROOT-free arithmetic; lives in mist::hep only because its callers (TH1
111// constructors) do. Could migrate to mist::algo if a ROOT-free caller appears.
112// ---------------------------------------------------------------------------
113[[nodiscard]] inline std::vector<double>
114uniform_binning(double width, double low, double high)
115{
116 std::vector<double> edges;
117 if (width <= 0.0 || high <= low) {
118 mist::logger::error("(histo::uniform_binning) require width > 0 and high > low");
119 return edges;
120 }
121 int n = static_cast<int>((high - low) / width);
122 if ((high - low) - n * width > 0.0) ++n; // round up a partial last bin
123 edges.reserve(n + 1);
124 for (int i = 0; i <= n; ++i) edges.push_back(low + i * width);
125 if (edges.back() != high)
126 mist::logger::warning("(histo::uniform_binning) high edge adjusted to fit width");
127 return edges;
128}
129
130// ---------------------------------------------------------------------------
131// log_binning: `n_bins + 1` logarithmically-spaced bin edges spanning
132// [low, high], i.e. edges equally spaced in log10. Common for pT / energy
133// spectra where a constant *ratio* between adjacent edges is wanted rather
134// than a constant difference.
135//
136// Both bounds must be strictly positive (log is undefined at and below zero)
137// and high > low; otherwise an empty vector is returned and an error logged.
138//
139// The result is suitable for the variable-binning TH1 constructor:
140// auto e = mist::hep::histo::log_binning(50, 0.1, 100.0);
141// TH1F h("h", "h", e.size() - 1, e.data());
142// ---------------------------------------------------------------------------
143[[nodiscard]] inline std::vector<double>
144log_binning(int n_bins, double low, double high)
145{
146 std::vector<double> edges;
147 if (n_bins <= 0 || low <= 0.0 || high <= low) {
148 mist::logger::error("(histo::log_binning) require n_bins > 0 and 0 < low < high");
149 return edges;
150 }
151 const double log_lo = std::log10(low);
152 const double log_hi = std::log10(high);
153 const double step = (log_hi - log_lo) / static_cast<double>(n_bins);
154 edges.reserve(n_bins + 1);
155 for (int i = 0; i <= n_bins; ++i)
156 edges.push_back(std::pow(10.0, log_lo + i * step));
157 // Pin the endpoints exactly — pow(10, log10(x)) can drift by an ULP.
158 edges.front() = low;
159 edges.back() = high;
160 return edges;
161}
162
163// ===========================================================================
164// Construction (F-23)
165// ===========================================================================
166
167namespace detail {
168// Function-local counter for default unique names — one instance per process,
169// no header-global. Not thread-safe; pass an explicit name from concurrent code.
170inline int& build_counter() { static int c = 0; return c; }
171} // namespace detail
172
173// ---------------------------------------------------------------------------
174// make_th1_from_vector: build a 1-D histogram from a vector of values, with an
175// automatic binning heuristic when n_bins <= 0. Replaces BLU uBuildTH1; fixes
176// the duplicate `>= 1e3` branch that made the original always end at 216 bins.
177//
178// Returned via owned::root_ptr and born detached from gDirectory (owned::make):
179// safe to own even if a TFile is open at call time — the BLU original and the
180// pre-owned mist-hep version both risked a double-free there.
181//
182// Heuristic (n_bins <= 0):
183// default -> 12 bins
184// size >= 1e2 -> size/3 + 2
185// size >= 1e3 -> size/5 + 2
186// size >= 1e4 -> 216 (cap)
187// ---------------------------------------------------------------------------
188template <typename TH1Type = TH1F, typename T>
189 requires std::is_arithmetic_v<T>
190[[nodiscard]] owned::root_ptr<TH1Type>
191make_th1_from_vector(const std::vector<T>& data,
192 int n_bins = -1,
193 double offset = 0.0,
194 double low = 0.0,
195 double high = 0.0)
196{
197 const std::string name = "histo_from_vector_" +
198 std::to_string(detail::build_counter()++);
199
200 if (data.empty())
201 return owned::make<TH1Type>(name.c_str(), name.c_str(), 1, 0.0, 1.0);
202
203 const auto max_it = *std::max_element(data.begin(), data.end());
204 const auto min_it = *std::min_element(data.begin(), data.end());
205 const double span = static_cast<double>(max_it) - static_cast<double>(min_it);
206
207 if (low == high) {
208 low = static_cast<double>(min_it) - 0.2 * span + offset;
209 high = static_cast<double>(max_it) + 0.2 * span + offset;
210 }
211
212 if (n_bins <= 0) {
213 const std::size_t sz = data.size();
214 n_bins = 12;
215 if (sz >= static_cast<std::size_t>(1e2)) n_bins = static_cast<int>(sz / 3) + 2;
216 if (sz >= static_cast<std::size_t>(1e3)) n_bins = static_cast<int>(sz / 5) + 2;
217 if (sz >= static_cast<std::size_t>(1e4)) n_bins = 216;
218 }
219
220 auto h = owned::make<TH1Type>(name.c_str(), name.c_str(), n_bins, low, high);
221 for (auto v : data) h->Fill(static_cast<double>(v) + offset);
222 return h;
223}
224
225// ===========================================================================
226// Manipulation (F-26 / F-27)
227// ===========================================================================
228
229namespace detail {
230// Clone a histogram into an owned, gDirectory-detached copy of the same
231// concrete type. Delegates to owned::clone so the copy carries ROOT-correct
232// ownership (the previous raw-Clone() form attached the copy to gDirectory).
233template <typename TH>
234[[nodiscard]] owned::root_ptr<TH> clone_as(const TH* h)
235{
236 return owned::clone(*h);
237}
238} // namespace detail
239
240// ---------------------------------------------------------------------------
241// offset: add a constant to every bin. With absolute = true, stores the
242// magnitude |content + value|. Operates on all cells (including under/overflow),
243// matching ROOT's own Add/Scale convention. Replaces BLU uOffset.
244// ---------------------------------------------------------------------------
245template <typename TH>
246[[nodiscard]] owned::root_ptr<TH>
247offset(const TH* h, double value, bool absolute = false)
248{
249 auto out = detail::clone_as(h);
250 if (!out) return out;
251 const int n_cells = out->GetNcells();
252 for (int i = 0; i < n_cells; ++i) {
253 const double shifted = out->GetBinContent(i) + value;
254 out->SetBinContent(i, absolute ? std::fabs(shifted) : shifted);
255 }
256 return out;
257}
258
259// ---------------------------------------------------------------------------
260// absolute: store the magnitude of every bin content. Replaces BLU uAbsolute.
261// ---------------------------------------------------------------------------
262template <typename TH>
263[[nodiscard]] owned::root_ptr<TH> absolute(const TH* h)
264{
265 return offset(h, 0.0, /*absolute=*/true);
266}
267
268// ---------------------------------------------------------------------------
269// scale: multiply every bin content by `factor`. The error is propagated:
270// - factor_error == 0 (default): error scales linearly (error *= |factor|).
271// - factor_error > 0: relative errors add in quadrature, i.e.
272// e_new = |content_new| * sqrt( (e/content)^2 + (factor_error/factor)^2 ).
273//
274// Replaces BLU uScale (TH). The BLU original carried -1/-2 magic sentinels
275// and read GetBinContent(iBin) instead of the global bin in the 3-D loop;
276// both are dropped here. Operates on all cells.
277// ---------------------------------------------------------------------------
278template <typename TH>
279[[nodiscard]] owned::root_ptr<TH>
280scale(const TH* h, double factor, double factor_error = 0.0)
281{
282 auto out = detail::clone_as(h);
283 if (!out) return out;
284 const int n_cells = out->GetNcells();
285 for (int i = 0; i < n_cells; ++i) {
286 const double content = out->GetBinContent(i);
287 const double error = out->GetBinError(i);
288 const double scaled = factor * content;
289 out->SetBinContent(i, scaled);
290 if (factor_error == 0.0 || content == 0.0 || factor == 0.0) {
291 out->SetBinError(i, std::fabs(factor) * error);
292 } else {
293 const double rel = mist::hep::stats::quadrature_sum(
294 {error / content, factor_error / factor});
295 out->SetBinError(i, std::fabs(scaled) * rel);
296 }
297 }
298 return out;
299}
300
301} // namespace mist::hep::histo
owned::root_ptr< TH > clone_as(const TH *h)
Definition histo.h:234
int & build_counter()
Definition histo.h:170
Definition fill.h:23
owned::root_ptr< TH > scale(const TH *h, double factor, double factor_error=0.0)
Definition histo.h:280
std::vector< double > uniform_binning(double width, double low, double high)
Definition histo.h:114
std::vector< double > log_binning(int n_bins, double low, double high)
Definition histo.h:144
owned::root_ptr< TH > offset(const TH *h, double value, bool absolute=false)
Definition histo.h:247
owned::root_ptr< TH > absolute(const TH *h)
Definition histo.h:263
owned::root_ptr< TH1Type > make_th1_from_vector(const std::vector< T > &data, int n_bins=-1, double offset=0.0, double low=0.0, double high=0.0)
Definition histo.h:191
bool is_consistent(const TH1Type *a, const TH2Type *b)
Definition histo.h:82
int pair_dimension(const TH1Type *a, const TH2Type *b)
Definition histo.h:65
int dimension(const TH *h)
Definition histo.h:44
Definition owned.h:33
root_ptr< T > clone(const T &source)
Definition owned.h:81
std::unique_ptr< T, root_deleter > root_ptr
Definition owned.h:60
root_ptr< T > make(Args &&... args)
Definition owned.h:67
double quadrature_sum(R &&xs)
Definition stats.h:36