2 Copyright (C) 2002-2026 CERN for the benefit of the ATLAS collaboration
6#include "HDF5Utils/HistCommon.h"
18#include <boost/histogram.hpp>
21namespace H5Utils::hist::detail {
23 // various stuff to inspect the boost histograms
25 // Concepts to inspect which members the accumulator exposes.
27 concept HasValue = requires(T t) { t.begin()->value(); };
29 concept HasVariance = requires(T t) { t.begin()->variance(); };
31 concept HasCount = requires(T t) { t.begin()->count(); };
33 concept HasSumOfWeights = requires(T t) { t.begin()->sum_of_weights(); };
35 concept HasSumOfWeightsSquared = requires(T t) {
36 t.begin()->sum_of_weights_squared();
39 // figure out the output array type
43 using type = typename T::value_type;
45 // this needs further specialization in some cases
47 using iter_value_t = decltype(
48 std::declval<T>().begin()->value());
52 using type = std::remove_cv_t<std::remove_reference_t<iter_value_t<T>>>;
55 using array_t = typename array_type<T>::type;
58 // function to flatten the histogram array
59 template <typename T, typename F>
60 std::vector<array_t<T>> get_flat_hist_array(const T& hist,
61 const F& value_getter)
63 using out_t = std::vector<array_t<T>>;
64 namespace bh = boost::histogram;
66 // calculate a global index for the flat output array
67 auto global_index = [&hist](const auto& hist_idx)
69 // This is unrolling the index to a c-style array. We calculate
70 // a global index, starting with the last dimension. The last
71 // dimension is just added to the global index. Each previous
72 // dimension needs to multiply the index by the cumulative
73 // product of the maximum index of subsequent dimensions.
76 for (size_t dim_p1 = hist.rank(); dim_p1 != 0; dim_p1--) {
77 size_t dim = dim_p1 - 1;
78 auto ops = hist.axis(dim).options();
79 int underflow = (ops & bh::axis::option::underflow) ? 1 : 0;
80 int overflow = (ops & bh::axis::option::overflow) ? 1 : 0;
81 // ugly check to decide if we have to add underflow offset
82 int h5idx_signed = hist_idx.index(dim) + underflow;
83 if (h5idx_signed < 0) throw std::logic_error("neg signed index");
84 size_t h5idx = static_cast<size_t>(h5idx_signed);
85 global += h5idx * stride;
86 stride *= hist.axis(dim).size() + underflow + overflow;
91 out_t out(hist.size());
92 std::set<size_t> used_indices;
93 for (const auto& x : bh::indexed(hist, bh::coverage::all)) {
94 size_t global = global_index(x);
95 if (!used_indices.insert(global).second) {
96 throw std::logic_error(
97 "repeat global index: " + std::to_string(global));
99 out.at(global) = value_getter(x);
107 // Runtime functions to check axes, this information can't always be
108 // determined statically with the public api.
110 // Returns true when all bin widths are equal and positive (regular
112 inline bool is_regular_axis(const std::vector<double>& edges) {
113 if (edges.size() < 2) return false;
114 const double width0 = edges[1] - edges[0];
115 if (width0 <= 0.0) return false;
116 for (size_t i = 2; i < edges.size(); i++) {
117 const double width = edges[i] - edges[i-1];
118 if (std::abs(width - width0) > 1e-5 * std::abs(width0)) { return false; }
123 // Returns true if bins are contiguous integer sequence
124 inline bool is_contiguous(const std::vector<int64_t>& vals) {
125 if (vals.size() < 2) return true;
126 for (size_t i = 1; i < vals.size(); ++i) {
127 if (vals[i] != vals[i-1] + 1) return false;
132 // Helper: construct an Axis with name/underflow/overflow filled from the
133 // boost axis options. Each overload then only needs to set ax.edges.
134 inline Axis make_axis(const auto& bh_ax, const std::string& name)
136 namespace opts = boost::histogram::axis::option;
139 ax.underflow = (bh_ax.options() & opts::underflow) ? true : false;
140 ax.overflow = (bh_ax.options() & opts::overflow) ? true : false;
145 // Concepts to label descrete axes
147 // A random-access sequence of string labels.
148 // Matches std::vector<string>, std::deque<string>, etc.
150 concept LabelList = requires(const L& lst, size_t i) {
151 { lst[i] } -> std::convertible_to<std::string>;
152 { lst.size() } -> std::convertible_to<size_t>;
155 // A map-like container with key_type and at(key) -> string.
156 // Matches std::map<K, string> but not std::vector<string>.
158 concept LabelMap = requires(const M& m) {
159 { m.at(std::declval<typename M::key_type>()) }
160 -> std::convertible_to<std::string>;
161 { m.size() } -> std::convertible_to<size_t>;
164 // Either a positional label list or a keyed label map.
166 concept LabelOverride = LabelList<T> || LabelMap<T>;
168 // Positional list of per-axis label overrides.
169 // Supports std::vector, std::deque (operator[] + size() on const ref).
171 concept AxisOverrideSeq = requires(const L& l, size_t i) {
173 { l.size() } -> std::convertible_to<size_t>;
176 // String-keyed map of per-axis label overrides.
177 // at(axis_name) returns the label info; find/end used for existence
178 // check; begin/end used to validate every key was matched.
180 concept AxisOverrideMap = requires(const M& m) {
181 m.at(std::declval<const std::string&>());
182 m.find(std::declval<const std::string&>()) != m.end();
187 // General axis override match
188 template <typename Labels>
189 concept AxisOverrides = AxisOverrideSeq<Labels> || AxisOverrideMap<Labels>;
191 // Axis whose bins expose lower()/upper() (regular, variable).
192 // Discrete axes (integer, category) iterate to raw values — no lower().
193 template<typename Ax>
194 concept ContinuousAxis = requires(const Ax& ax) {
195 { ax.begin()->lower() } -> std::convertible_to<double>;
198 // Concept: axis with no lower()/upper() per bin (category, integer).
199 template<typename Ax>
200 concept DiscreteAxis = !ContinuousAxis<Ax>;
203 // Axis reading functions
205 // There are several overloads to provide maximum flexibility, both
206 // in what axes are used from boost::histogram and how the user
207 // decides to provide labels for the discrete axis cases.
209 // discrete axis where the user has provided labels for each value
210 template<LabelList List>
211 Axis read_one_axis(const DiscreteAxis auto& bh_ax,
212 const std::pair<std::string, List>& meta)
214 auto ax = make_axis(bh_ax, meta.first);
216 const auto& labels = meta.second;
217 if (labels.size() != static_cast<size_t>(bh_ax.size())) {
218 throw std::invalid_argument(
220 "axis '{}': label count ({}) != bin count ({})",
221 ax.name, labels.size(), bh_ax.size() ));
224 std::vector<std::string> edge_labels;
225 edge_labels.reserve(labels.size());
226 for (size_t i = 0; i < labels.size(); ++i)
227 edge_labels.push_back(labels[i]);
228 ax.edges = std::move(edge_labels);
232 // Map-based labeled categorical overload: key = category value,
235 // Reorders map entries to axis bin order, then delegates to
236 // LabelList overload.
237 template<LabelMap Map>
238 Axis read_one_axis(const DiscreteAxis auto& bh_ax,
239 const std::pair<std::string, Map>& meta)
241 std::vector<std::string> ordered_labels;
242 for (const auto& bin : bh_ax) {
243 ordered_labels.push_back(meta.second.at(bin));
245 return read_one_axis(bh_ax,
246 std::pair<std::string, std::vector<std::string>>{
247 meta.first, std::move(ordered_labels)});
250 // Continuous-axis overload (regular, variable): bins are interval_view.
251 Axis read_one_axis(const ContinuousAxis auto& bh_ax,
252 const std::string& meta)
254 auto ax = make_axis(bh_ax, meta);
256 std::vector<double> dedges;
257 for (const auto& bin : bh_ax) dedges.push_back(bin.lower());
258 if (bh_ax.size() > 0) dedges.push_back(bh_ax.rbegin()->upper());
259 if (is_regular_axis(dedges)) {
260 ax.edges = regular_axis_t{
261 dedges.front(), dedges.back(), dedges.size() - 1};
263 ax.edges = std::move(dedges);
268 // Continuous axes reject non-empty label overrides; name taken from
270 template<LabelOverride Override>
271 Axis read_one_axis(const ContinuousAxis auto& bh_ax,
272 const std::pair<std::string, Override>& meta)
274 if (meta.second.size() > 0) {
275 throw std::invalid_argument(std::format(
276 "axis '{}' is continuous (regular/variable): "
277 "{} label(s) provided but labels are not valid "
278 "for this axis type",
279 meta.first, meta.second.size()));
281 return read_one_axis(bh_ax, meta.first);
284 // Discrete-axis overload (integer, category): bins are raw values
286 Axis read_one_axis(const auto& bh_ax, const std::string& meta)
288 auto ax = make_axis(bh_ax, meta);
290 std::vector<int64_t> int_vals;
291 for (const auto& bin : bh_ax) int_vals.push_back(static_cast<int64_t>(bin));
292 if (is_contiguous(int_vals)) {
293 ax.edges = std::pair<int64_t,int64_t>{int_vals.front(), int_vals.back()};
295 ax.edges = std::move(int_vals);
300 // Extract axis name from plain-string or pair<string,T> metadata.
301 inline const std::string& get_axis_name(const std::string& m) {
305 inline const std::string& get_axis_name(
306 const std::pair<std::string, T>& m) {
311 // Top-level Axis builder functions
313 // Basic case with no labeling overrides. Labels provided via via
314 // per-axis metadata.
315 template <typename T>
316 std::vector<Axis> get_axes(const T& hist) {
317 std::vector<Axis> axes;
318 hist.for_each_axis([&axes](const auto& bh_ax) {
319 axes.push_back(read_one_axis(bh_ax, bh_ax.metadata()));
324 // Positional-list override: labels[i] is the LabelList or LabelMap
326 template<typename Hist, AxisOverrideSeq Labels>
327 std::vector<Axis> get_axes(const Hist& hist, const Labels& labels)
329 if (labels.size() != hist.rank()) {
330 throw std::invalid_argument(std::format(
331 "label list has {} entries but histogram has {} axes",
332 labels.size(), hist.rank()));
334 std::vector<Axis> axes;
336 hist.for_each_axis([&](const auto& bh_ax) {
337 const auto& lbl = labels[i++];
338 using lbl_t = std::decay_t<decltype(lbl)>;
339 axes.push_back(read_one_axis(
341 std::pair<std::string, lbl_t>{
342 get_axis_name(bh_ax.metadata()), lbl}));
347 // Map-based override: axes absent from the map fall back to metadata.
348 template<typename Hist, AxisOverrideMap Labels>
349 std::vector<Axis> get_axes(const Hist& hist, const Labels& labels)
351 std::vector<Axis> axes;
352 std::set<std::string> remaining_keys;
353 for (const auto& [key, val] : labels) {
354 remaining_keys.insert(key);
356 hist.for_each_axis([&](const auto& bh_ax) {
357 const std::string& name = get_axis_name(bh_ax.metadata());
358 auto it = labels.find(name);
359 if (it != labels.end()) {
360 remaining_keys.erase(name);
361 if constexpr (ContinuousAxis<std::decay_t<decltype(bh_ax)>>) {
362 throw std::invalid_argument(std::format(
363 "axis '{}' is continuous (regular/variable) and cannot "
364 "have string labels; remove it from the label map",
367 const auto& lbl = it->second;
368 using lbl_t = std::decay_t<decltype(lbl)>;
369 axes.push_back(read_one_axis(
371 std::pair<std::string, lbl_t>{name, lbl}));
373 axes.push_back(read_one_axis(bh_ax, bh_ax.metadata()));
376 if (!remaining_keys.empty()) {
377 std::string key_list;
378 for (const auto& key : remaining_keys) {
379 if (!key_list.empty()) key_list += ", ";
382 throw std::invalid_argument(std::format(
383 "label map key(s) do not match any axis in the histogram: "
390 // Main histogram storage writing functions
392 // Simple utility to calculate integer products
393 template <typename T>
394 auto product(const std::vector<T>& v) {
395 return std::accumulate(
396 v.begin(), v.end(), T(1), std::multiplies<hsize_t>{});
399 // Return the UHI storage type string for histogram type T.
400 template <typename T>
401 std::string storage_type_name() {
402 if constexpr (HasSumOfWeightsSquared<T>) {
403 return "weighted_mean";
404 } else if constexpr (HasCount<T>) {
406 } else if constexpr (HasVariance<T>) {
408 } else if constexpr (std::is_integral_v<array_t<T>>) {
415 // internal top level histogram writer
416 template <typename T>
417 void write_hist_impl(
418 H5::Group& parent_group,
420 const std::string& name,
421 const std::vector<Axis>& axes)
423 // build the data space
424 std::vector<hsize_t> ds_dims;
425 for (const auto& axis: axes) {
427 std::visit([](const auto& e) { return n_bins(e); }, axis.edges)
428 + (axis.underflow ? 1u : 0u)
429 + (axis.overflow ? 1u : 0u);
430 ds_dims.push_back(static_cast<hsize_t>(total_size));
432 H5::DataSpace space(ds_dims.size(), ds_dims.data());
434 // Create histogram group and write UHI schema version
435 H5::Group group = parent_group.createGroup(name);
436 write_int_attr(group, "uhi_schema", 1);
437 group.createGroup("metadata");
438 group.createGroup("writer_info");
440 // Dataset creation properties for storage datasets
441 H5::DSetCreatPropList props;
442 props.setChunk(ds_dims.size(), ds_dims.data());
445 H5Pset_dset_no_attrs_hint(props.getId(), true),
446 "no attribute hint");
447 H5::DataType type = hdf5_t<array_t<T>>;
450 write_axes(group, axes);
452 // Write storage group
453 H5::Group storage = group.createGroup("storage");
454 write_str_attr(storage, "type", storage_type_name<T>());
456 // Helper to write a named dataset into the storage group
457 auto add_dataset = [&hist, &type, &space, &props, &storage](
458 const std::string& ds_name,
460 auto flat = get_flat_hist_array<T>(hist, func);
462 space.getSelectNpoints() == static_cast<hssize_t>(flat.size()));
463 storage.createDataSet(ds_name, type, space, props)
464 .write(flat.data(), type);
467 // values (the primary bin contents)
468 if constexpr (HasValue<T>) {
469 add_dataset("values",
470 [](const auto& x) { return x->value(); });
472 add_dataset("values",
473 [](const auto& x) { return *x; });
476 if constexpr (HasVariance<T>) {
477 add_dataset("variances",
478 [](const auto& x) { return x->variance(); });
480 // count (mean / weighted_mean accumulators)
481 if constexpr (HasCount<T>) {
483 [](const auto& x) { return x->count(); });
485 // sum_of_weights (weighted_mean accumulator)
486 if constexpr (HasSumOfWeights<T>) {
487 add_dataset("sum_of_weights",
488 [](const auto& x) { return x->sum_of_weights(); });
490 // sum_of_weights_squared (weighted_mean accumulator)
491 if constexpr (HasSumOfWeightsSquared<T>) {
492 add_dataset("sum_of_weights_squared",
493 [](const auto& x) { return x->sum_of_weights_squared(); });
498// main hist writing function, this should be all anyone needs to
501void H5Utils::hist::detail::write_hist_to_group_fn::operator()(
502 H5::Group& parent_group,
504 const std::string& name) const
507 parent_group, hist, name, get_axes(hist));
510// Overload with external label override (positional list or name map).
511template <typename T, typename Labels>
512void H5Utils::hist::detail::write_hist_to_group_fn::operator()(
513 H5::Group& parent_group,
515 const std::string& name,
516 const Labels& labels) const
519 parent_group, hist, name, get_axes(hist, labels));