Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-07-18 09:11:46

0001 // -*- C++ -*-
0002 //
0003 // This file is part of YODA -- Yet more Objects for Data Analysis
0004 // Copyright (C) 2008-2025 The YODA collaboration (see AUTHORS for details)
0005 //
0006 #ifndef YODA_BinnedDbn_h
0007 #define YODA_BinnedDbn_h
0008 
0009 #include "YODA/AnalysisObject.h"
0010 #include "YODA/Fillable.h"
0011 #include "YODA/FillableStorage.h"
0012 #include "YODA/Dbn.h"
0013 #include "YODA/BinnedEstimate.h"
0014 #include "YODA/Scatter.h"
0015 
0016 #ifdef HAVE_HDF5
0017 #include "YODA/Utils/H5Utils.h"
0018 #endif
0019 
0020 #include <memory>
0021 #include <utility>
0022 #include <iostream>
0023 #include <iomanip>
0024 
0025 
0026 namespace YODA {
0027 
0028   /// All histograms can be instantiated through this alias.
0029   /*
0030   ///               BinnedStorage                : Introduces binning backend.
0031   ///                     |
0032   ///              FillableStorage               : Introduces FillAdapterT
0033   ///                     |
0034   ///                 DbnStorage                 : Hooks up with AnalysisObject
0035   ///                     /\
0036   ///                    /  \
0037   ///    BinnedDbn<1>___/    \___ BinnedDbn<2>   : Introduces dimension specific
0038   ///          \                     /           : utility aliases
0039   ///           \_______     _______/            : (xMin(), yMax(), etc.)
0040   ///                   \   /
0041   ///                    \ /
0042   ///                     |
0043   ///         BinnedHisto/BinnedProfile          : Convenience alias
0044   */
0045   /// Since objects with continuous axes are by far the most commonly used type
0046   /// in practice, we define convenient short-hand aliases HistoND/ProfileND for
0047   /// with only continuous axes, along with the familar types Histo1D, Profile2D, etc.
0048 
0049 
0050   template <size_t DbnN, typename... AxisT>
0051   class DbnStorage;
0052 
0053   /// @brief User-facing BinnedDbn class in arbitrary dimension
0054   template <size_t DbnN, typename... AxisT>
0055   class BinnedDbn : public DbnStorage<DbnN, AxisT...> {
0056   public:
0057     using HistoT = BinnedDbn<DbnN, AxisT...>;
0058     using BaseT = DbnStorage<DbnN, AxisT...>;
0059     using FillType = typename BaseT::FillType;
0060     using BinType = typename BaseT::BinT;
0061     using Ptr = std::shared_ptr<HistoT>;
0062 
0063     /// @brief Inherit constructors.
0064     using BaseT::BaseT;
0065 
0066     BinnedDbn() = default;
0067     BinnedDbn(const HistoT&) = default;
0068     BinnedDbn(HistoT&&) = default;
0069     BinnedDbn& operator =(const HistoT&) = default;
0070     BinnedDbn& operator =(HistoT&&) = default;
0071     using AnalysisObject::operator =;
0072 
0073     /// @brief Copy constructor (needed for clone functions).
0074     ///
0075     /// @note Compiler won't generate this constructor automatically.
0076     BinnedDbn(const BaseT& other) : BaseT(other) {}
0077     //
0078     BinnedDbn(const HistoT& other, const std::string& path) : BaseT(other, path) {}
0079 
0080     /// @brief Move constructor
0081     BinnedDbn(BaseT&& other) : BaseT(std::move(other)) {}
0082     //
0083     BinnedDbn(HistoT&& other, const std::string& path) : BaseT(std::move(other), path) {}
0084 
0085     /// @brief Make a copy on the stack
0086     HistoT clone() const noexcept {
0087       return HistoT(*this);
0088     }
0089 
0090     /// @brief Make a copy on the heap
0091     HistoT* newclone() const noexcept {
0092       return new HistoT(*this);
0093     }
0094 
0095   };
0096 
0097 
0098   /// @name Define dimension-specific short-hands
0099   /// @{
0100 
0101   template <typename... AxisTypes>
0102   using BinnedHisto = BinnedDbn<sizeof...(AxisTypes), AxisTypes...>;
0103 
0104   template <typename... AxisTypes>
0105   using BinnedProfile = BinnedDbn<sizeof...(AxisTypes)+1, AxisTypes...>;
0106 
0107   /// @}
0108 
0109 
0110   /// @brief Histogram convenience class based on FillableStorage.
0111   ///
0112   /// @note We use this abstraction layer to implement the bulk once and only once.
0113   /// The user-facing BinnedDbn type(s) will inherit all their methods from this
0114   /// base class along with a few axis-specifc mixins.
0115   ///
0116   /// @note Alias generates index sequence later used to create
0117   /// a parameter pack consisting of axis types to instantiate
0118   /// the Binning template.
0119   template <size_t DbnN, typename... AxisT>
0120   class DbnStorage : public FillableStorage<DbnN, Dbn<DbnN>, AxisT...>,
0121                      public AnalysisObject, public Fillable {
0122   public:
0123 
0124     using BaseT = FillableStorage<DbnN, Dbn<DbnN>, AxisT...>;
0125     using BinningT = typename BaseT::BinningT;
0126     using BinT = typename BaseT::BinT;
0127     using BinType = typename BaseT::BinT;
0128     using FillType = typename BaseT::FillType;
0129     using AnalysisObject::operator =;
0130 
0131     /// @name Constructors
0132     /// @{
0133 
0134     /// @brief Nullary constructor for unique pointers etc.
0135     ///
0136     /// @note The setting of optional path/title is not possible here in order
0137     /// to avoid overload ambiguity for brace-initialised constructors.
0138     DbnStorage() : BaseT(), AnalysisObject(mkTypeString<DbnN, AxisT...>(), "") { }
0139 
0140     /// @brief Constructor giving explicit bin edges as rvalue reference.
0141     ///
0142     /// Discrete axes have as many edges as bins.
0143     /// Continuous axes have number of edges = number of bins + 1,
0144     /// the last one being the upper bound of the last bin.
0145     DbnStorage(std::vector<AxisT>&&... binsEdges,
0146                const std::string& path = "", const std::string& title = "")
0147          : BaseT(Axis<AxisT>(std::move(binsEdges))...),
0148            AnalysisObject(mkTypeString<DbnN, AxisT...>(), path, title) { }
0149 
0150     /// @brief Constructor giving explicit bin edges as lvalue const reference.
0151     ///
0152     /// Discrete axes have as many edges as bins.
0153     /// Continuous axes have bins.size()+1 edges, the last one
0154     /// being the upper bound of the last bin.
0155     DbnStorage(const std::vector<AxisT>&... binsEdges,
0156                const std::string& path = "", const std::string& title = "")
0157          : BaseT(Axis<AxisT>(binsEdges)...),
0158            AnalysisObject(mkTypeString<DbnN, AxisT...>(), path, title) { }
0159 
0160     /// @brief Constructor giving explicit bin edges as initializer list
0161     ///
0162     /// Discrete axes have as many edges as bins.
0163     /// Continuous axes have number of edges = number of bins + 1,
0164     /// the last one being the upper bound of the last bin.
0165     DbnStorage(std::initializer_list<AxisT>&&... binsEdges,
0166                const std::string& path = "", const std::string& title = "")
0167          : BaseT(Axis<AxisT>(std::move(binsEdges))...),
0168            AnalysisObject(mkTypeString<DbnN, AxisT...>(), path, title) { }
0169 
0170     /// @brief Constructor giving range and number of bins.
0171     ///
0172     /// @note This constructor is only supported for objects with purely continous axes.
0173     /// It is also the only place where the index sequence sequence is actually needed.
0174     template <typename EdgeT = double, typename = enable_if_all_CAxisT<EdgeT, AxisT...>>
0175     DbnStorage(const std::vector<size_t>& nBins, const std::vector<std::pair<EdgeT, EdgeT>>& limitsLowUp,
0176                const std::string& path = "", const std::string& title = "")
0177          : BaseT( _mkBinning(nBins, limitsLowUp, std::make_index_sequence<sizeof...(AxisT)>{}) ),
0178            AnalysisObject(mkTypeString<DbnN, AxisT...>(), path, title) { }
0179 
0180     /// @brief Constructor given a sequence of axes
0181     DbnStorage(const Axis<AxisT>&... axes, const std::string& path = "", const std::string& title = "")
0182          : BaseT(axes...), AnalysisObject(mkTypeString<DbnN, AxisT...>(), path, title) { }
0183 
0184     /// @brief Constructor given a sequence of rvalue axes
0185     DbnStorage(Axis<AxisT>&&... axes, const std::string& path = "", const std::string& title = "")
0186          : BaseT(std::move(axes)...), AnalysisObject(mkTypeString<DbnN, AxisT...>(), path, title) { }
0187 
0188     /// @brief Constructor given a BinningT (needed for type reductions)
0189     DbnStorage(const BinningT& binning, const std::string& path = "", const std::string& title = "")
0190          : BaseT(binning), AnalysisObject(mkTypeString<DbnN, AxisT...>(), path, title) { }
0191 
0192     /// @brief Constructor given an rvalue BinningT
0193     DbnStorage(BinningT&& binning, const std::string& path = "", const std::string& title = "")
0194          : BaseT(std::move(binning)), AnalysisObject(mkTypeString<DbnN, AxisT...>(), path, title) { }
0195 
0196     /// @brief Constructor given a scatter
0197     template <typename EdgeT = double, typename = enable_if_all_CAxisT<EdgeT, AxisT...>>
0198     DbnStorage(const ScatterND<sizeof...(AxisT)+1>& s, const std::string& path = "", const std::string& title = "")
0199          : BaseT(_mkBinning(s, std::make_index_sequence<sizeof...(AxisT)>{})),
0200            AnalysisObject(mkTypeString<DbnN, AxisT...>(), path, title) { }
0201 
0202     /// @brief Copy constructor
0203     ///
0204     /// @todo Also allow title setting from the constructor?
0205     DbnStorage(const DbnStorage& other, const std::string& path = "") : BaseT(other),
0206          AnalysisObject(mkTypeString<DbnN, AxisT...>(), path!=""? path : other.path(), other, other.title()) { }
0207 
0208     /// @brief Move constructor
0209     ///
0210     /// @todo Also allow title setting from the constructor?
0211     DbnStorage(DbnStorage&& other, const std::string& path = "") : BaseT(std::move(other)),
0212          AnalysisObject(mkTypeString<DbnN, AxisT...>(), path!=""? path : other.path(), other, other.title()) {  }
0213 
0214     /// @brief Make a copy on the stack
0215     DbnStorage clone() const noexcept {
0216       return DbnStorage(*this);
0217     }
0218 
0219     /// @brief Make a copy on the heap
0220     DbnStorage* newclone() const noexcept {
0221       return new DbnStorage(*this);
0222     }
0223 
0224     /// @}
0225 
0226 
0227     /// @name Transformations
0228     /// @{
0229 
0230     /// @brief Triggers fill adapter on the bin corresponding to coords.
0231     ///
0232     /// @note Accepts coordinates only as rvalue tuple. The tuple members
0233     /// are then moved (bringing tuple member to unspecified state) later in adapters.
0234     virtual int fill(FillType&& coords, const double weight = 1.0, const double fraction = 1.0) {
0235       return BaseT::fill(std::move(coords), std::make_index_sequence<sizeof...(AxisT)>{}, weight, fraction);
0236     }
0237 
0238     /// @brief Rescale as if all fill weights had been different by factor @a scalefactor.
0239     void scaleW(const double scalefactor) noexcept {
0240       setAnnotation("ScaledBy", annotation<double>("ScaledBy", 1.0) * scalefactor);
0241       for (auto& bin : BaseT::_bins) {
0242         bin.scaleW(scalefactor);
0243       }
0244     }
0245 
0246     /// @brief Rescale as if all fill weights had been different by factor @a scalefactor along dimension @a i.
0247     void scale(const size_t i, const double scalefactor) noexcept {
0248       setAnnotation("ScaledBy", annotation<double>("ScaledBy", 1.0) * scalefactor);
0249       for (auto& bin : BaseT::_bins) {
0250         bin.scale(i, scalefactor);
0251       }
0252     }
0253 
0254 
0255     /// @brief Normalize the (visible) histo "volume" to the @a normto value.
0256     ///
0257     /// If @a includeoverflows is true, the original normalisation is computed with
0258     /// the overflow bins included, so that the resulting visible normalisation can
0259     /// be less than @a normto. This is probably what you want.
0260     void normalize(const double normto=1.0, const bool includeOverflows=true) {
0261       const double oldintegral = integral(includeOverflows);
0262       if (oldintegral == 0) throw WeightError("Attempted to normalize a histogram with null area");
0263       scaleW(normto / oldintegral);
0264     }
0265 
0266 
0267     /// @brief Merge every group of @a n bins, from start to end inclusive
0268     ///
0269     /// If the number of bins is not a multiple of @a n, the last @a m < @a n
0270     /// bins on the RHS will also be merged, as the closest possible approach to
0271     /// factor @n rebinning everywhere.
0272     ///
0273     /// @note Only visible bins are being rebinned. Underflow (index = 0) and
0274     /// overflow (index = numBins() + 1) are not included.
0275     template <size_t axisN>
0276     void rebinBy(unsigned int n, size_t begin=1, size_t end=UINT_MAX) {
0277       if (n < 1)  throw UserError("Rebinning requested in groups of 0!");
0278       if (!begin) throw UserError("Visible bins start with index 1!");
0279       if (end > BaseT::numBinsAt(axisN)+1)  end = BaseT::numBinsAt(axisN) + 1;
0280       for (size_t m = begin; m < end; ++m) {
0281         if (m > BaseT::numBinsAt(axisN)+1) break; // nothing to be done
0282         const size_t myend = (m+n-1 < BaseT::numBinsAt(axisN)+1) ? m+n-1 : BaseT::numBinsAt(axisN);
0283         if (myend > m) {
0284           BaseT::template mergeBins<axisN>({m, myend});
0285           end -= myend-m; //< reduce upper index by the number of removed bins
0286         }
0287       }
0288     }
0289 
0290     /// @brief Overloaded alias for rebinBy
0291     template <size_t axisN>
0292     void rebin(unsigned int n, size_t begin=1, size_t end=UINT_MAX) {
0293       rebinBy<axisN>(n, begin, end);
0294     }
0295 
0296     /// @brief Rebin to the given list of bin edges
0297     template <size_t axisN>
0298     void rebinTo(const std::vector<typename BinningT::template getAxisT<axisN>::EdgeT>& newedges) {
0299       if (newedges.size() < 2)
0300         throw UserError("Requested rebinning to an edge list which defines no bins");
0301       using thisAxisT = typename BinningT::template getAxisT<axisN>;
0302       using thisEdgeT = typename thisAxisT::EdgeT;
0303       // get list of shared edges
0304       thisAxisT& oldAxis = BaseT::_binning.template axis<axisN>();
0305       const thisAxisT newAxis(newedges);
0306       const std::vector<thisEdgeT> eshared = oldAxis.sharedEdges(newAxis);
0307       if (eshared.size() != newAxis.edges().size())
0308         throw BinningError("Requested rebinning to incompatible edges");
0309       // loop over new lower bin edges (= first bin index of merge range)
0310       for (size_t begin = 0; begin < eshared.size() - 1; ++begin) {
0311         // find index of upper edge along old axis
0312         // (subtracting 1 gives index of last bin to be merged)
0313         size_t end = oldAxis.index(eshared[begin+1]) - 1;
0314         // if the current edge is the last visible edge before the overflow
0315         // merge the remaining bins into the overflow
0316         if (begin == newAxis.numBins(true)-1)  end = oldAxis.numBins(true)-1;
0317         // merge this range
0318         if (end > begin)  BaseT::template mergeBins<axisN>({begin, end});
0319         if (eshared.size() == oldAxis.edges().size())  break; // we're done
0320       }
0321     }
0322 
0323     /// @brief Overloaded alias for rebinTo
0324     template <size_t axisN>
0325     void rebin(const std::vector<typename BinningT::template getAxisT<axisN>::EdgeT>& newedges) {
0326       rebinTo<axisN>(newedges);
0327     }
0328 
0329     /// Copy assignment
0330     DbnStorage& operator = (const DbnStorage& dbn) noexcept {
0331       if (this != &dbn) {
0332         AnalysisObject::operator = (dbn);
0333         BaseT::operator = (dbn);
0334       }
0335       return *this;
0336     }
0337 
0338     /// Move assignment
0339     DbnStorage& operator = (DbnStorage&& dbn) noexcept {
0340       if (this != &dbn) {
0341         AnalysisObject::operator = (dbn);
0342         BaseT::operator = (std::move(dbn));
0343       }
0344       return *this;
0345     }
0346 
0347 
0348     /// @brief Add two DbnStorages
0349     ///
0350     /// @note Adding DbnStorages will unset any ScaledBy
0351     /// attribute from previous calls to scale or normalize.
0352     ///
0353     /// @todo What happens if two storages disagree on masked bins?
0354     DbnStorage& operator += (const DbnStorage& dbn) {
0355       if (*this != dbn)
0356         throw BinningError("Arithmetic operation requires compatible binning!");
0357       if (AO::hasAnnotation("ScaledBy")) AO::rmAnnotation("ScaledBy");
0358       for (size_t i = 0; i < BaseT::numBins(true, true); ++i) {
0359         BaseT::bin(i) += dbn.bin(i);
0360       }
0361       BaseT::maskBins(dbn.maskedBins(), true);
0362       return *this;
0363     }
0364     //
0365     DbnStorage& operator += (DbnStorage&& dbn) {
0366       if (*this != dbn)
0367         throw BinningError("Arithmetic operation requires compatible binning!");
0368       if (AO::hasAnnotation("ScaledBy")) AO::rmAnnotation("ScaledBy");
0369       for (size_t i = 0; i < BaseT::numBins(true, true); ++i) {
0370         BaseT::bin(i) += std::move(dbn.bin(i));
0371       }
0372       BaseT::maskBins(dbn.maskedBins(), true);
0373       return *this;
0374     }
0375 
0376 
0377     /// @brief Subtract one DbnStorages from another one
0378     ///
0379     /// @note Subtracting DbnStorages will unset any ScaledBy
0380     /// attribute from previous calls to scale or normalize.
0381     DbnStorage& operator -= (const DbnStorage& dbn) {
0382       if (*this != dbn)
0383         throw BinningError("Arithmetic operation requires compatible binning!");
0384       if (AO::hasAnnotation("ScaledBy")) AO::rmAnnotation("ScaledBy");
0385       for (size_t i = 0; i < BaseT::numBins(true, true); ++i) {
0386         BaseT::bin(i) -= dbn.bin(i);
0387       }
0388       BaseT::maskBins(dbn.maskedBins(), true);
0389       return *this;
0390     }
0391     //
0392     DbnStorage& operator -= (DbnStorage&& dbn) {
0393       if (*this != dbn)
0394         throw BinningError("Arithmetic operation requires compatible binning!");
0395       if (AO::hasAnnotation("ScaledBy")) AO::rmAnnotation("ScaledBy");
0396       for (size_t i = 0; i < BaseT::numBins(true, true); ++i) {
0397         BaseT::bin(i) -= std::move(dbn.bin(i));
0398       }
0399       BaseT::maskBins(dbn.maskedBins(), true);
0400       return *this;
0401     }
0402 
0403     /// @}
0404 
0405     /// @name Reset methods
0406     /// @{
0407 
0408     /// @brief Reset the histogram.
0409     ///
0410     /// Keep the binning but set all bin contents and related quantities to zero
0411     void reset() noexcept { BaseT::reset(); }
0412 
0413     /// @}
0414 
0415 
0416     /// @name Binning info.
0417     /// @{
0418 
0419     size_t fillDim() const noexcept { return BaseT::fillDim(); }
0420 
0421     /// @brief Total dimension of the object (number of axes + filled value)
0422     size_t dim() const noexcept { return sizeof...(AxisT) + 1; }
0423 
0424     /// @brief Returns the axis configuration
0425     std::string _config() const noexcept { return mkAxisConfig<AxisT...>(); }
0426 
0427     /// @brief Returns the edges of axis N by value.
0428     template <size_t I, typename E = typename BinningT::template getEdgeT<I>>
0429     std::vector<E> edges(const bool includeOverflows = false) const noexcept {
0430       return BaseT::_binning.template edges<I>(includeOverflows);
0431     }
0432 
0433     /// @brief Templated version to get bin widths of axis N by reference.
0434     ///
0435     /// Overflows are included depending on @a includeOverflows
0436     /// Needed by axis-specific version from AxisMixin
0437     ///
0438     /// @note This version is only supported for continuous axes.
0439     template <size_t I, typename E = typename BinningT::template getEdgeT<I>>
0440     std::enable_if_t<std::is_floating_point<E>::value, std::vector<E>>
0441     widths(const bool includeOverflows = false) const noexcept {
0442       return BaseT::_binning.template widths<I>(includeOverflows);
0443     }
0444 
0445     /// @brief Get the lowest non-overflow edge of the axis
0446     ///
0447     /// @note This method is only supported for continuous axes
0448     template <size_t I, typename E = typename BinningT::template getEdgeT<I>>
0449     std::enable_if_t<std::is_floating_point<E>::value, E> min() const noexcept {
0450       return BaseT::_binning.template min<I>();
0451     }
0452 
0453     /// @brief Get the highest non-overflow edge of the axis
0454     ///
0455     /// @note This method is only supported for continuous axes
0456     template <size_t I, typename E = typename BinningT::template getEdgeT<I>>
0457     std::enable_if_t<std::is_floating_point<E>::value, E> max() const noexcept {
0458       return BaseT::_binning.template max<I>();
0459     }
0460 
0461     /// @}
0462 
0463 
0464     /// @name Whole histo data
0465     /// @{
0466 
0467     /// @brief Get the total volume of the histogram.
0468     double integral(const bool includeOverflows=true) const noexcept {
0469       return sumW(includeOverflows);
0470     }
0471 
0472     /// @brief Get the total volume error of the histogram.
0473     double integralError(const bool includeOverflows=true) const noexcept {
0474       return sqrt(sumW2(includeOverflows));
0475     }
0476 
0477     /// @brief Get the total volume of the histogram.
0478     double integralTo(const size_t binIndex) const noexcept {
0479       return integralRange(0, binIndex);
0480     }
0481 
0482     /// @brief Calculates the integrated volume of the histogram between
0483     /// global bins @a binindex1 and @a binIndex2.
0484     double integralRange(const size_t binIndex1, size_t binIndex2) const {
0485       assert(binIndex2 >= binIndex1);
0486       if (binIndex1 >= BaseT::numBins(true)) throw RangeError("binindex1 is out of range");
0487       if (binIndex2 >= BaseT::numBins(true)) throw RangeError("binindex2 is out of range");
0488       double sumw = 0;
0489       for (size_t idx = binIndex1; idx <= binIndex2; ++idx) {
0490         if (BaseT::bin(idx).isMasked())  continue;
0491         sumw += BaseT::bin(idx).sumW();
0492       }
0493       return sumw;
0494     }
0495 
0496     /// @brief Calculates the integrated volume error of the histogram between
0497     /// global bins @a binindex1 and @a binIndex2.
0498     double integralRangeError(const size_t binIndex1, size_t binIndex2) const {
0499       assert(binIndex2 >= binIndex1);
0500       if (binIndex1 >= BaseT::numBins(true)) throw RangeError("binindex1 is out of range");
0501       if (binIndex2 >= BaseT::numBins(true)) throw RangeError("binindex2 is out of range");
0502       double sumw2 = 0;
0503       for (size_t idx = binIndex1; idx <= binIndex2; ++idx) {
0504         if (BaseT::bin(idx).isMasked())  continue;
0505         sumw2 += BaseT::bin(idx).sumW2();
0506       }
0507       return sumw2;
0508     }
0509 
0510     /// @brief Get the number of fills (fractional fills are possible).
0511     double numEntries(const bool includeOverflows=true) const noexcept {
0512       double n = 0;
0513       for (const auto& b : BaseT::bins(includeOverflows))
0514         n += b.numEntries();
0515       return n;
0516     }
0517 
0518     /// @brief Get the effective number of fills.
0519     double effNumEntries(const bool includeOverflows=true) const noexcept {
0520       double n = 0;
0521       for (const auto& b : BaseT::bins(includeOverflows))
0522         n += b.effNumEntries();
0523       return n;
0524     }
0525 
0526     /// @brief Calculates sum of weights in histo.
0527     double sumW(const bool includeOverflows=true) const noexcept {
0528       double sumw = 0;
0529       for (const auto& b : BaseT::bins(includeOverflows))
0530         sumw += b.sumW();
0531       return sumw;
0532     }
0533 
0534     /// @brief Calculates sum of squared weights in histo.
0535     double sumW2(const bool includeOverflows=true) const noexcept {
0536       double sumw2 = 0;
0537       for (const auto& b : BaseT::bins(includeOverflows))
0538         sumw2 += b.sumW2();
0539       return sumw2;
0540     }
0541 
0542     /// @brief Calculates first moment along axis @a dim in histo.
0543     double sumWA(const size_t dim, const bool includeOverflows=true) const {
0544       if (dim >= DbnN)  throw RangeError("Invalid axis int, must be in range 0..dim-1");
0545       double sumwa = 0;
0546       for (const auto& b : BaseT::bins(includeOverflows))
0547         sumwa += b.sumW(dim+1);
0548       return sumwa;
0549     }
0550 
0551     /// @brief Calculates second moment along axis @a dim in histo.
0552     double sumWA2(const size_t dim, const bool includeOverflows=true) const {
0553       if (dim >= DbnN)  throw RangeError("Invalid axis int, must be in range 0..dim-1");
0554       double sumwa2 = 0;
0555       for (const auto& b : BaseT::bins(includeOverflows))
0556         sumwa2 += b.sumW2(dim+1);
0557       return sumwa2;
0558     }
0559 
0560     /// @brief Calculates cross-term along axes @a A1 and @a A2 in histo.
0561     template<size_t dim = DbnN, typename = std::enable_if_t<(dim >= 2)>>
0562     double crossTerm(const size_t A1, const size_t A2, const bool includeOverflows=true) const {
0563       if (A1 >= DbnN || A2 >= DbnN)  throw RangeError("Invalid axis int, must be in range 0..dim-1");
0564       if (A1 >= A2)  throw RangeError("Indices need to be different for cross term");
0565       double sumw = 0;
0566       for (const auto& b : BaseT::bins(includeOverflows))
0567         sumw += b.crossTerm(A1, A2);
0568       return sumw;
0569     }
0570 
0571     /// @brief Calculates the mean value at axis.
0572     double mean(size_t axisN, const bool includeOverflows=true) const noexcept {
0573       Dbn<DbnN> dbn;
0574       for (const auto& b : BaseT::bins(includeOverflows))
0575         dbn += b;
0576       return dbn.mean(axisN+1);
0577     }
0578 
0579     /// @brief Calculates the variance at axis.
0580     double variance(size_t axisN, const bool includeOverflows=true) const noexcept {
0581       Dbn<DbnN> dbn;
0582       for (const auto& b : BaseT::bins(includeOverflows))
0583         dbn += b;
0584       return dbn.variance(axisN+1);
0585     }
0586 
0587     /// @brief Calculates the standard deviation at axis.
0588     double stdDev(size_t axisN, const bool includeOverflows=true) const noexcept {
0589       return std::sqrt(variance(axisN, includeOverflows));
0590     }
0591 
0592     /// @brief Calculates the standard error at axis.
0593     double stdErr(size_t axisN, const bool includeOverflows=true) const noexcept {
0594       Dbn<DbnN> dbn;
0595       for (const auto& b : BaseT::bins(includeOverflows))
0596         dbn += b;
0597       return dbn.stdErr(axisN+1);
0598     }
0599 
0600     /// @brief Calculates the RMS at axis.
0601     double rms(size_t axisN, const bool includeOverflows=true) const noexcept {
0602       Dbn<DbnN> dbn;
0603       for (const auto& b : BaseT::bins(includeOverflows))
0604         dbn += b;
0605       return dbn.RMS(axisN+1);
0606     }
0607 
0608     double dVol(const bool includeOverflows=true) const noexcept {
0609       double vol = 0.0;
0610       for (const auto& b : BaseT::bins(includeOverflows))
0611         vol += b.dVol();
0612       return vol;
0613     }
0614 
0615     /// @brief Get the total density of the histogram.
0616     double density(const bool includeOverflows=true) const noexcept {
0617       const double vol = dVol(includeOverflows);
0618       if (vol)  return integral(includeOverflows) / vol;
0619       return std::numeric_limits<double>::quiet_NaN();
0620     }
0621 
0622     /// @brief Get the total density error of the histogram.
0623     double densityError(const bool includeOverflows=true) const noexcept {
0624       const double vol = dVol(includeOverflows);
0625       if (vol)  return integralError(includeOverflows) / vol;
0626       return std::numeric_limits<double>::quiet_NaN();
0627     }
0628 
0629     /// @brief Returns the sum of the bin densities
0630     double densitySum(const bool includeOverflows=true) const noexcept {
0631       double rho = 0.0;
0632       for (const auto& b : BaseT::bins(includeOverflows))
0633         rho += b.sumW() / b.dVol();
0634       return rho;
0635     }
0636 
0637     /// @brief Returns the largest density in any of the bins
0638     double maxDensity(const bool includeOverflows=true) const noexcept {
0639       std::vector<double> vals;
0640       for (auto& b : BaseT::bins(includeOverflows))
0641         vals.emplace_back(b.sumW() / b.dVol());
0642       return *max_element(vals.begin(), vals.end());
0643     }
0644 
0645     /// @}
0646 
0647     /// @name I/O
0648     /// @{
0649 
0650   private:
0651 
0652     // @brief Render information about this AO (private implementation)
0653     template<size_t... Is>
0654     void _renderYODA_aux(std::ostream& os, const int width, std::index_sequence<Is...>) const noexcept {
0655 
0656       // YODA1-style metadata
0657       if ( effNumEntries(true) > 0 ) {
0658         os << "# Mean: ";
0659         if (DbnN > 1) {  os << "("; }
0660         (( os <<  std::string(Is? ", " : "") << mean(Is, true)), ...);
0661         if (DbnN > 1) {  os << ")"; }
0662         os << "\n# Integral: " << integral(true) << "\n";
0663       }
0664 
0665       // render bin edges
0666       BaseT::_binning._renderYODA(os);
0667 
0668       // column header: content types
0669       os << std::setw(width) << std::left << "# sumW" << "\t";
0670       os << std::setw(width) << std::left << "sumW2" << "\t";
0671       (( os << std::setw(width) << std::left << ("sumW(A"  + std::to_string(Is+1) + ")") << "\t"
0672             << std::setw(width) << std::left << ("sumW2(A" + std::to_string(Is+1) + ")") << "\t"), ...);
0673       if constexpr (DbnN >= 2) {
0674         for (size_t i = 0; i < (DbnN-1); ++i) {
0675           for (size_t j = i+1; j < DbnN; ++j) {
0676             const std::string scross = "sumW(A" + std::to_string(i+1) + ",A" + std::to_string(j+1) + ")";
0677             os << std::setw(width) << std::left << scross << "\t";
0678           }
0679         }
0680       }
0681       os << "numEntries\n";
0682       // now write one row per bin
0683       for (const auto& b : BaseT::bins(true, true)) {
0684         os << std::setw(width) << std::left << b.sumW() << "\t"; // renders sumW
0685         os << std::setw(width) << std::left << b.sumW2() << "\t"; // renders sumW2
0686         ((os << std::setw(width) << std::left << b.sumW( Is+1) << "\t"
0687              << std::setw(width) << std::left << b.sumW2(Is+1) << "\t"), ...); // renders first moments
0688         if constexpr (DbnN >= 2) {
0689           for (size_t i = 0; i < (DbnN-1); ++i) {
0690             for (size_t j = i+1; j < DbnN; ++j) {
0691               os << std::setw(width) << std::left << b.crossTerm(i,j) << "\t";
0692             }
0693           }
0694         }
0695         os << std::setw(width) << std::left << b.numEntries() << "\n"; // renders raw event count
0696       }
0697     }
0698 
0699   public:
0700 
0701     // @brief Render information about this AO (public API)
0702     void _renderYODA(std::ostream& os, const int width = 13) const noexcept {
0703       _renderYODA_aux(os, width, std::make_index_sequence<DbnN>{});
0704     }
0705 
0706     // @brief Render scatter-like information about this AO
0707     void _renderFLAT(std::ostream& os, const int width = 13) const noexcept {
0708       const ScatterND<sizeof...(AxisT)+1> tmp = mkScatter();
0709       tmp._renderYODA(os, width);
0710     }
0711 
0712     #ifdef HAVE_HDF5
0713     // @brief Extract axes edges of this AO into map of @a edges
0714     void _extractEdges(std::map<std::string, EdgeHandlerBasePtr>& edges,
0715                        const std::vector<std::string>&) const noexcept {
0716 
0717       using lenT = EdgeHandler<size_t>;
0718       using lenPtr = EdgeHandlerPtr<size_t>;
0719       const std::string lengthID("sizeinfo");
0720       lenPtr nedges = std::static_pointer_cast<lenT>(edges.find(lengthID)->second);
0721 
0722       auto extractEdges = [&edges, &binning = BaseT::_binning, &nedges](auto I) {
0723 
0724         using thisEdgeT = typename BinningT::template getEdgeT<I>;
0725         using thisHandlerT = EdgeHandler<thisEdgeT>;
0726         using thisHandlerPtr = EdgeHandlerPtr<thisEdgeT>;
0727 
0728         const std::string edgeID = std::string("edges_") + TypeID<thisEdgeT>::name();
0729         std::vector<thisEdgeT> tmp = binning.template edges<I>();
0730         nedges->extend({ tmp.size() });
0731 
0732         auto itr = edges.find(edgeID);
0733         if (itr != edges.cend()) {
0734           thisHandlerPtr edgeset = std::static_pointer_cast<thisHandlerT>(itr->second);
0735           edgeset->extend(std::move(tmp));
0736         }
0737         else {
0738           edges[edgeID] = std::make_shared<thisHandlerT>(std::move(tmp));
0739         }
0740       };
0741       MetaUtils::staticFor<sizeof...(AxisT)>(extractEdges);
0742 
0743       std::vector<size_t> masks = BaseT::_binning.maskedBins();
0744       nedges->extend({ masks.size() });
0745       nedges->extend(std::move(masks));
0746 
0747     };
0748     #endif
0749 
0750     /// @}
0751 
0752     /// @name MPI (de-)serialisation
0753     //@{
0754 
0755     size_t lengthContent(bool = false) const noexcept {
0756       return BaseT::numBins(true, true) * Dbn<DbnN>::DataSize::value;
0757     }
0758 
0759     std::vector<double> serializeContent(bool = false) const noexcept {
0760       std::vector<double> rtn;
0761       const size_t nBins = BaseT::numBins(true, true);
0762       rtn.reserve(nBins * Dbn<DbnN>::DataSize::value);
0763       for (size_t i = 0; i < nBins; ++i) {
0764         std::vector<double> bdata = BaseT::bin(i)._serializeContent();
0765         rtn.insert(std::end(rtn),
0766                    std::make_move_iterator(std::begin(bdata)),
0767                    std::make_move_iterator(std::end(bdata)));
0768       }
0769       return rtn;
0770     }
0771 
0772     void deserializeContent(const std::vector<double>& data) {
0773 
0774       constexpr size_t dbnSize = Dbn<DbnN>::DataSize::value;
0775       const size_t nBins = BaseT::numBins(true, true);
0776       if (data.size() != nBins * dbnSize)
0777         throw UserError("Length of serialized data should be "
0778                         + std::to_string(nBins * dbnSize)+"!");
0779 
0780       const auto itr = data.cbegin();
0781       for (size_t i = 0; i < nBins; ++i) {
0782         auto first = itr + i*dbnSize;
0783         auto last = first + dbnSize;
0784         BaseT::bin(i)._deserializeContent(std::vector<double>{first, last});
0785       }
0786 
0787     }
0788 
0789     // @}
0790 
0791     /// @name Type reductions
0792     /// @{
0793 
0794     /// @brief Produce a BinnedEstimate from a DbnStorage
0795     ///
0796     /// The binning remains unchanged.
0797     ///
0798     /// @note The @a overflowsWidth argument will be applied
0799     /// to all bins outside the visible bin range.
0800     BinnedEstimate<AxisT...> mkEstimate(const std::string& path = "",
0801                                         const std::string& source = "",
0802                        [[maybe_unused]] const bool divbyvol = true,
0803                                         const double overflowsWidth = -1.0) const {
0804 
0805       // @todo Should we check BaseT::nanCount() and report?
0806       BinnedEstimate<AxisT...> rtn(BaseT::_binning);
0807       for (const std::string& a : annotations()) {
0808         if (a != "Type")  rtn.setAnnotation(a, annotation(a));
0809       }
0810       rtn.setAnnotation("Path", path);
0811 
0812       if (BaseT::nanCount()) {
0813         const double nanc = BaseT::nanCount();
0814         const double nanw = BaseT::nanSumW();
0815         const double frac = nanc / (nanc + numEntries());
0816         const double wtot = nanw + effNumEntries();
0817         rtn.setAnnotation("NanFraction", frac);
0818         if (wtot)  rtn.setAnnotation("WeightedNanFraction", nanw/wtot);
0819       }
0820 
0821       for (const auto& b : BaseT::bins(true, true)) {
0822         if (overflowsWidth <= 0. && !b.isVisible())  continue;
0823         if constexpr(DbnN > sizeof...(AxisT)) {
0824           rtn.bin(b.index()).setVal(b.mean(DbnN));
0825           if (b.numEntries()) { // only set uncertainty for filled Dbns
0826             rtn.bin(b.index()).setErr(b.stdErr(DbnN), source);
0827           }
0828         }
0829         else {
0830           const double scale = divbyvol? (b.isVisible()? b.dVol() : overflowsWidth) : 1.0;
0831           rtn.bin(b.index()).setVal(b.sumW() / scale);
0832           if (b.numEntries()) { // only set uncertainty for filled Dbns
0833             rtn.bin(b.index()).setErr(b.errW() / scale, source);
0834           }
0835         }
0836       }
0837 
0838       return rtn;
0839     }
0840 
0841     /// @brief Produce a BinnedEstimate for each bin along axis @a axisN
0842     /// and return as a vector.
0843     ///
0844     /// The binning dimension is reduced by one unit.
0845     ///
0846     /// @note The @a overflowsWidth argument will be applied
0847     /// to all bins outside the visible bin range.
0848     template<size_t axisN, typename = std::enable_if_t< (axisN < sizeof...(AxisT)) >>
0849     auto mkEstimates(const std::string& path = "", const std::string source = "",
0850                      const bool divbyvol = true, const bool includeOverflows = false,
0851                      const double overflowsWidth = -1.0) {
0852 
0853       BinnedEstimate<AxisT...> est = mkEstimate(path, source, divbyvol, overflowsWidth);
0854       return est.template mkEstimates<axisN>(path, includeOverflows);
0855     }
0856 
0857 
0858     /// @brief Produce a ScatterND from a DbnStorage
0859     ///
0860     /// @note The @a overflowsWidth argument will be applied
0861     /// to all bins outside the visible bin range.
0862     auto mkScatter(const std::string& path="", const bool divbyvol = true,
0863                                                const bool usefocus = false,
0864                                                const bool includeOverflows = false,
0865                                                const bool includeMaskedBins = false,
0866                                                const double overflowsWidth = -1.0) const {
0867       const BinnedEstimate<AxisT...> est = mkEstimate("", "", divbyvol, overflowsWidth);
0868       ScatterND<sizeof...(AxisT)+1> rtn = est.mkScatter(path, "", includeOverflows, includeMaskedBins);
0869       if (usefocus) {
0870         size_t idx = 0;
0871         for (const auto& b : BaseT::bins(includeOverflows, includeMaskedBins)) {
0872           auto shiftIfContinuous = [&rtn, &b, &idx](auto I) {
0873             using isContinuous = typename BinningT::template is_CAxis<I>;
0874             if constexpr (isContinuous::value) {
0875               const double oldMax = rtn.point(idx).max(I);
0876               const double oldMin = rtn.point(idx).min(I);
0877               const double newVal = b.mean(I+1);
0878               rtn.point(idx).set(I, newVal, newVal - oldMin, oldMax - newVal);
0879             }
0880           };
0881           MetaUtils::staticFor<BinningT::Dimension::value>(shiftIfContinuous);
0882           ++idx;
0883         }
0884       }
0885       return rtn;
0886     }
0887 
0888     /// @brief Produce a BinnedHisto from BinnedProfile.
0889     ///
0890     /// The binning remains unchanged, but the fill
0891     /// dimension is reduced by one unit.
0892     template<size_t N = DbnN, typename = std::enable_if_t< (N == sizeof...(AxisT)+1) >>
0893     BinnedHisto<AxisT...> mkHisto(const std::string& path="") const {
0894 
0895       BinnedHisto<AxisT...> rtn(BaseT::_binning);
0896       rtn.setNanLog(BaseT::nanCount(), BaseT::nanSumW(), BaseT::nanSumW2());
0897       for (const std::string& a : annotations()) {
0898         if (a != "Type")  rtn.setAnnotation(a, annotation(a));
0899       }
0900       rtn.setAnnotation("Path", path);
0901 
0902       for (const auto& b : BaseT::bins(true)) {
0903         rtn.bin(b.index()) += b.template reduce<N-1>();
0904       }
0905 
0906       return rtn;
0907     }
0908 
0909     /// @brief Produce a BinnedProfile from a DbnStorage
0910     ///
0911     /// Case 1: BinnedHisto(N+1)D -> BinnedProfileND
0912     /// The fill dimension remains the same, but the
0913     /// binning is reduced by one dimension.
0914     ///
0915     /// Case 2: BinnedProfile(N+1)D -> BinnedProfileND
0916     /// Both fill and binning dimmensions are reduced
0917     /// by one unit.
0918     ///
0919     /// @todo use a parameter pack and allow marginalising over multiple axes?
0920     template<size_t axisN, typename = std::enable_if_t< (axisN < sizeof...(AxisT)) >>
0921     auto mkMarginalProfile(const std::string& path="") const {
0922 
0923       auto rtn = BaseT::template _mkBinnedT<BinnedProfile>(BaseT::_binning.template _getAxesExcept<axisN>());
0924       rtn.setNanLog(BaseT::nanCount(), BaseT::nanSumW(), BaseT::nanSumW2());
0925       for (const std::string& a : annotations()) {
0926         if (a != "Type")  rtn.setAnnotation(a, annotation(a));
0927       }
0928       rtn.setAnnotation("Path", path);
0929 
0930       auto collapseStorageBins =
0931         [&oldBinning = BaseT::_binning, &oldBins = BaseT::_bins, &rtn](auto I, auto dbnRed) {
0932 
0933         auto collapse = [&oldBins, &rtn](const auto& binsIndicesToMerge, auto axis) {
0934           assert(rtn.numBins(true) == binsIndicesToMerge.size());
0935 
0936           // for any given pivot, add the content
0937           // from the old slice to the new slice
0938           for (size_t i = 0; i < rtn.numBins(true); ++i) {
0939             auto& pivotBin = rtn.bin(i);
0940             auto& binToAppend = oldBins[binsIndicesToMerge[i]];
0941             pivotBin += binToAppend.template reduce<axis>();
0942           }
0943         };
0944 
0945         // get bin slice for any given bin along the axis that is to be
0946         // collapsed, then copy the values into the new binning
0947         ssize_t nBinRowsToBeMerged = oldBinning.numBinsAt(I);
0948         while (nBinRowsToBeMerged--) {
0949           /// @note Binning iteratively shrinks, so the next bin slice
0950           /// to merge will always be the next.
0951           collapse(oldBinning.sliceIndices(I, nBinRowsToBeMerged), dbnRed);
0952         }
0953       };
0954       /// If the calling object is a histogram, we can just copy the Dbn<N>,
0955       /// otherwise we need to collapse an axis first in order to produce a Dbn<N-1>.
0956       /// @note Dbn axes are 0-indexed, so asking to reduce DbnN doesn't reduce anything.
0957       auto dbnRed = std::integral_constant<size_t, (sizeof...(AxisT) == DbnN)? DbnN : axisN>();
0958       (void)collapseStorageBins(std::integral_constant<std::size_t, axisN>(), dbnRed);
0959 
0960       return rtn;
0961     }
0962 
0963     /// @brief Produce a BinnedHisto from a DbnStorage
0964     ///
0965     /// Case 1: BinnedProfile(N+1)D -> BinnedHistoND
0966     /// The binning dimension is reduced by one unit,
0967     /// and the fill dimension is reduced by two units.
0968     ///
0969     /// Case 2: BinnedHisto(N+1)D -> BinnedHisto
0970     /// Both fill and binning dimension are reduced
0971     /// by one unit.
0972     ///
0973     /// @todo use a parameter pack and allow marginalising over multiple axes?
0974     template<size_t axisN, typename = std::enable_if_t< (axisN < sizeof...(AxisT)) >>
0975     auto mkMarginalHisto(const std::string& path="") const {
0976 
0977       if constexpr (DbnN != sizeof...(AxisT)) {
0978         // Case 1: BP(N+1) -> BH(N+1) -> BHN
0979         return mkHisto().template mkMarginalHisto<axisN>(path);
0980       }
0981       else {
0982         // Case 2: BH(N+1) -> BHN
0983 
0984         auto rtn = BaseT::template _mkBinnedT<BinnedHisto>(BaseT::_binning.template _getAxesExcept<axisN>());
0985         rtn.setNanLog(BaseT::nanCount(), BaseT::nanSumW(), BaseT::nanSumW2());
0986         for (const std::string& a : annotations()) {
0987           if (a != "Type")  rtn.setAnnotation(a, annotation(a));
0988         }
0989         rtn.setAnnotation("Path", path);
0990 
0991         auto collapseStorageBins =
0992           [&oldBinning = BaseT::_binning, &oldBins = BaseT::_bins, &rtn](auto I, auto dbnRed) {
0993 
0994           auto collapse = [&oldBins, &rtn](const auto& binsIndicesToMerge, auto axis) {
0995             assert(rtn.numBins(true) == binsIndicesToMerge.size());
0996 
0997             // for any given pivot, add the content
0998             // from the old slice to the new slice
0999             for (size_t i = 0; i < rtn.numBins(true); ++i) {
1000               auto& pivotBin = rtn.bin(i);
1001               auto& binToAppend = oldBins[binsIndicesToMerge[i]];
1002               pivotBin += binToAppend.template reduce<axis>();
1003             }
1004           };
1005 
1006           // get bin slice for any given bin along the axis that is to be
1007           // collapsed, then copy the values into the new binning
1008           ssize_t nBinRowsToBeMerged = oldBinning.numBinsAt(I);
1009           while (nBinRowsToBeMerged--) {
1010             /// @note Binning iteratively shrinks, so the next bin slice
1011             /// to merge will always be the next.
1012             collapse(oldBinning.sliceIndices(I, nBinRowsToBeMerged), dbnRed);
1013           }
1014         };
1015         // collapse Dbn along axisN
1016         auto dbnRed = std::integral_constant<size_t, axisN>();
1017         (void)collapseStorageBins(std::integral_constant<std::size_t, axisN>(), dbnRed);
1018 
1019         return rtn;
1020       }
1021     }
1022 
1023 
1024     /// @brief Split into vector of BinnedProfile along axis @a axisN
1025     ///
1026     /// The binning dimension of the returned objects are reduced by one unit.
1027     /// @note Requires at least two binning dimensions.
1028     template<size_t axisN, typename = std::enable_if_t< (axisN < sizeof...(AxisT) &&
1029                                                          sizeof...(AxisT)>=2 &&
1030                                                          DbnN > sizeof...(AxisT)) >>
1031     auto mkProfiles(const std::string& path="", const bool includeOverflows=false) const {
1032 
1033       // Need to provide a prescription for how to add the two bin contents
1034       auto how2add = [](auto& pivot, const BinType& toCopy) { pivot = toCopy.template reduce<axisN>(); };
1035       auto rtn = BaseT::template mkBinnedSlices<axisN, BinnedProfile>(how2add, includeOverflows);
1036       for (const std::string& a : annotations()) {
1037         if (a == "Type")  continue;
1038         for (size_t i = 0; i < rtn.size(); ++i) {
1039           rtn[i].setAnnotation(a, annotation(a));
1040         }
1041       }
1042       for (size_t i = 0; i < rtn.size(); ++i) {
1043         rtn[i].setAnnotation("Path", path);
1044       }
1045       return rtn;
1046     }
1047 
1048 
1049     /// @brief Split into vector of BinnedHisto along axis @a axisN
1050     ///
1051     /// The binning dimension of the returned ojects are reduced by one unit.
1052     /// @note Requires at least two binning dimensions.
1053     template<size_t axisN, typename = std::enable_if_t< (axisN < sizeof...(AxisT) && sizeof...(AxisT)>=2) >>
1054     auto mkHistos(const std::string& path="", const bool includeOverflows=false) const {
1055 
1056       if constexpr (DbnN != sizeof...(AxisT)) {
1057         // Case 1: BP(N+1) -> BH(N+1) -> BHN
1058         return mkHisto().template mkHistos<axisN>(path, includeOverflows);
1059       }
1060       else {
1061         // Case 2: BH(N+1) -> BHN
1062 
1063         // Need to provide a prescription for how to add the two bin contents
1064         auto how2add = [](auto& pivot, const BinType& toCopy) { pivot = toCopy.template reduce<axisN>(); };
1065         auto rtn = BaseT::template mkBinnedSlices<axisN,BinnedHisto>(how2add, includeOverflows);
1066         for (const std::string& a : annotations()) {
1067           if (a == "Type")  continue;
1068           for (size_t i = 0; i < rtn.size(); ++i) {
1069             rtn[i].setAnnotation(a, annotation(a));
1070           }
1071         }
1072         for (size_t i = 0; i < rtn.size(); ++i) {
1073           rtn[i].setAnnotation("Path", path);
1074         }
1075         return rtn;
1076       }
1077     }
1078 
1079 
1080     /// @brief Convert the BinnedDbn to a BinnedEstimate representing
1081     /// the effective number of entries in each bin
1082     ///
1083     /// @note The @a overflowsWidth argument will be applied
1084     /// to all bins outside the visible bin range.
1085     BinnedEstimate<AxisT...> mkBinnedEffNumEntries(const std::string& path="",
1086                                                    const std::string& source = "",
1087                                                    const bool includeOverflows = true,
1088                                                    const bool divbyvol = true,
1089                                                    const double overflowsWidth = -1.0) {
1090 
1091       BinnedEstimate<AxisT...> rtn = mkEstimate(path);
1092 
1093       for (const auto& b : BaseT::bins(includeOverflows)) {
1094         double scale = 1.0;
1095         if (divbyvol) {
1096           scale = (overflowsWidth > 0. && !b.isVisible())? overflowsWidth : b.dVol();
1097         }
1098         const double effN = b.effNumEntries() / scale;
1099         const double err = effN * b.relErrW() / scale;
1100         rtn.bin(b.index()).set(effN, {-err, err}, source);
1101       }
1102 
1103       return rtn;
1104     }
1105 
1106 
1107     /// @brief Return an inert version of the analysis object (e.g. scatter, estimate)
1108     AnalysisObject* mkInert(const std::string& path = "",
1109                             const std::string& source = "") const noexcept {
1110       return mkEstimate(path, source).newclone();
1111     }
1112 
1113     /// @}
1114 
1115     private:
1116 
1117     /// @brief Helper function to create a BinningT from
1118     /// a given set @a nBins within a range @a limitsLowUp
1119     template<size_t... Is>
1120     BinningT _mkBinning(const std::vector<size_t>& nBins,
1121                         const std::vector<std::pair<double, double>>& limitsLowUp,
1122                         std::index_sequence<Is...>) const {
1123       return BinningT({((void)Is, Axis<AxisT>(nBins[Is], limitsLowUp[Is].first, limitsLowUp[Is].second))...});
1124     }
1125 
1126     /// @brief Helper function to create a BinningT from a scatter @a s
1127     template<size_t... Is>
1128     BinningT _mkBinning(const ScatterND<sizeof...(AxisT)+1>& s, std::index_sequence<Is...>) const {
1129       return BinningT(Axis<AxisT>(s.edges(Is))...);
1130     }
1131 
1132   };
1133 
1134 
1135 
1136   /// @name Combining BinnedDbn objects: global operators
1137   /// @{
1138 
1139   /// @brief Add two BinnedDbn objects
1140   template<size_t DbnN, typename... AxisT>
1141   inline BinnedDbn<DbnN, AxisT...>
1142   operator + (BinnedDbn<DbnN, AxisT...> first, BinnedDbn<DbnN, AxisT...>&& second) {
1143     first += std::move(second);
1144     return first;
1145   }
1146   //
1147   template <size_t DbnN, typename... AxisT>
1148   inline BinnedDbn<DbnN, AxisT...>
1149   operator + (BinnedDbn<DbnN, AxisT...> first, const BinnedDbn<DbnN, AxisT...>& second) {
1150     first += second;
1151     return first;
1152   }
1153 
1154 
1155   /// @brief Subtract one BinnedDbn object from another
1156   template <size_t DbnN, typename... AxisT>
1157   inline BinnedDbn<DbnN, AxisT...>
1158   operator - (BinnedDbn<DbnN, AxisT...> first, BinnedDbn<DbnN, AxisT...>&& second) {
1159     first -= std::move(second);
1160     return first;
1161   }
1162   //
1163   template <size_t DbnN, typename... AxisT>
1164   inline BinnedDbn<DbnN, AxisT...>
1165   operator - (BinnedDbn<DbnN, AxisT...> first, const BinnedDbn<DbnN, AxisT...>& second) {
1166     first -= second;
1167     return first;
1168   }
1169 
1170 
1171   /// @brief Divide two BinnedDbn objects
1172   template <size_t DbnN, typename... AxisT>
1173   inline BinnedEstimate<AxisT...>
1174   divide(const BinnedDbn<DbnN, AxisT...>& numer, const BinnedDbn<DbnN, AxisT...>& denom) {
1175 
1176     if (numer != denom) {
1177       throw BinningError("Arithmetic operation requires compatible binning!");
1178     }
1179 
1180     BinnedEstimate<AxisT...> rtn = numer.mkEstimate();
1181     if (numer.path() == denom.path())  rtn.setPath(numer.path());
1182     if (rtn.hasAnnotation("ScaledBy")) rtn.rmAnnotation("ScaledBy");
1183 
1184     for (const auto& b_num : numer.bins(true, true)) {
1185       const size_t idx = b_num.index();
1186       const auto& b_den = denom.bin(idx);
1187       double v, e;
1188       if (isZero(b_den.effNumEntries())) {
1189         v = std::numeric_limits<double>::quiet_NaN();
1190         e = std::numeric_limits<double>::quiet_NaN();
1191       }
1192       else {
1193         if constexpr(DbnN > sizeof...(AxisT)) {
1194           v = b_num.mean(DbnN) / b_den.mean(DbnN);
1195           const double e_num = isZero(b_num.effNumEntries())? 0 : b_num.relStdErr(DbnN);
1196           const double e_den = isZero(b_den.effNumEntries())? 0 : b_den.relStdErr(DbnN);
1197           e = fabs(v) * sqrt(sqr(e_num) + sqr(e_den));
1198         }
1199         else {
1200           v = b_num.sumW() / b_den.sumW();
1201           const double e_num = isZero(b_num.effNumEntries())? 0 : b_num.relErrW();
1202           const double e_den = isZero(b_den.effNumEntries())? 0 : b_den.relErrW();
1203           e = fabs(v) * sqrt(sqr(e_num) + sqr(e_den));
1204         }
1205       }
1206       rtn.bin(idx).set(v, {-e, e}); // @todo put "stats" as source?
1207     }
1208     rtn.maskBins(denom.maskedBins(), true);
1209 
1210     return rtn;
1211   }
1212   //
1213   template <size_t DbnN, typename... AxisT>
1214   inline BinnedEstimate<AxisT...>
1215   operator / (const BinnedDbn<DbnN, AxisT...>& numer, const BinnedDbn<DbnN, AxisT...>& denom) {
1216     return divide(numer, denom);
1217   }
1218   //
1219   template <size_t DbnN, typename... AxisT>
1220   inline BinnedEstimate<AxisT...>
1221   operator / (const BinnedDbn<DbnN, AxisT...>& numer, BinnedDbn<DbnN, AxisT...>&& denom) {
1222     return divide(numer, std::move(denom));
1223   }
1224   //
1225   template <size_t DbnN, typename... AxisT>
1226   inline BinnedEstimate<AxisT...>
1227   operator / (BinnedDbn<DbnN, AxisT...>&& numer, const BinnedDbn<DbnN, AxisT...>& denom) {
1228     return divide(std::move(numer), denom);
1229   }
1230   //
1231   template <size_t DbnN, typename... AxisT>
1232   inline BinnedEstimate<AxisT...>
1233   operator / (BinnedDbn<DbnN, AxisT...>&& numer, BinnedDbn<DbnN, AxisT...>&& denom) {
1234     return divide(std::move(numer), std::move(denom));
1235   }
1236 
1237 
1238   /// @brief Calculate a binned efficiency ratio of two BinnedDbn objects
1239   ///
1240   /// @note An efficiency is not the same thing as a standard division of two
1241   /// BinnedDbn objects: the errors are treated as correlated via binomial statistics.
1242   template <size_t DbnN, typename... AxisT>
1243   inline BinnedEstimate<AxisT...>
1244   efficiency(const BinnedDbn<DbnN, AxisT...>& accepted, const BinnedDbn<DbnN, AxisT...>& total) {
1245 
1246     if (accepted != total) {
1247       throw BinningError("Arithmetic operation requires compatible binning!");
1248     }
1249 
1250     BinnedEstimate<AxisT...> rtn = divide(accepted, total);
1251 
1252     for (const auto& b_acc : accepted.bins(true, true)) {
1253       const auto& b_tot = total.bin(b_acc.index());
1254       auto& b_rtn = rtn.bin(b_acc.index());
1255 
1256       // Check that the numerator is consistent with being a subset of the denominator
1257       /// @note Neither effNumEntries nor sumW are guaranteed to satisfy num <= den for general weights!
1258       if (b_acc.numEntries() > b_tot.numEntries())
1259         throw UserError("Attempt to calculate an efficiency when the numerator is not a subset of the denominator: "
1260                         + Utils::toStr(b_acc.numEntries()) + " entries / " + Utils::toStr(b_tot.numEntries()) + " entries");
1261 
1262       // If no entries on the denominator, set eff = err = 0 and move to the next bin
1263       double eff = std::numeric_limits<double>::quiet_NaN();
1264       double err = std::numeric_limits<double>::quiet_NaN();
1265       if (!isZero(b_tot.effNumEntries())) {
1266         eff = b_rtn.val();
1267         err = sqrt(fabs( add((1.0-2.0*eff)*b_acc.sumW2(), sqr(eff)*b_tot.sumW2()) / sqr(b_tot.sumW()) ));
1268       }
1269       b_rtn.setErr({-err, err}); // @todo put "stats" as source?
1270     }
1271     return rtn;
1272   }
1273 
1274 
1275   /// @brief Calculate the asymmetry (a-b)/(a+b) of two BinnedDbn objects
1276   template <size_t DbnN, typename... AxisT>
1277   inline BinnedEstimate<AxisT...>
1278   asymm(const BinnedDbn<DbnN, AxisT...>& a, const BinnedDbn<DbnN, AxisT...>& b) {
1279     return (a-b) / (a+b);
1280   }
1281 
1282 
1283   /// @brief Convert a BinnedDbn to a BinnedEstimate representing the integral of the histogram
1284   ///
1285   /// @note The integral histo errors are calculated as sqrt(binvalue), as if they
1286   /// are uncorrelated. This is not in general true for integral histograms, so if you
1287   /// need accurate errors you should explicitly monitor bin-to-bin correlations.
1288   ///
1289   /// The includeunderflow param chooses whether the underflow bin is included
1290   /// in the integral numbers as an offset.
1291   template <size_t DbnN, typename... AxisT>
1292   inline BinnedEstimate<AxisT...>
1293   mkIntegral(const BinnedDbn<DbnN, AxisT...>& histo, const bool includeOverflows = true) {
1294 
1295     BinnedEstimate<AxisT...> rtn = histo.mkEstimate();
1296 
1297     double sumW = 0.0, sumW2 = 0.0;
1298     for (const auto& b : histo.bins(includeOverflows)) {
1299       sumW  += b.sumW();
1300       sumW2 += b.sumW2();
1301       const double e = sqrt(sumW2);
1302       rtn.bin(b.index()).set(sumW, {-e, e});
1303     }
1304 
1305     return rtn;
1306   }
1307 
1308 
1309   /// @brief Convert a BinnedDbn to a BinnedEstimate where each bin is a fraction of the total
1310   ///
1311   /// @note This sounds weird: let's explain a bit more! Sometimes we want to
1312   /// take a histo h, make an integral histogram H from it, and then divide H by
1313   /// the total integral of h, such that every bin in H represents the
1314   /// cumulative efficiency of that bin as a fraction of the total. I.e. an
1315   /// integral histo, scaled by 1/total_integral and with binomial errors.
1316   ///
1317   /// The includeunderflow param behaves as for toIntegral, and applies to both
1318   /// the initial integration and the integral used for the scaling. The
1319   /// includeoverflow param applies only to obtaining the scaling factor.
1320   template <size_t DbnN, typename... AxisT>
1321   inline BinnedEstimate<AxisT...>
1322   mkIntegralEff(const BinnedDbn<DbnN, AxisT...>& histo, const bool includeOverflows = true) {
1323 
1324     BinnedEstimate<AxisT...> rtn = mkIntegral(histo, includeOverflows);
1325     const double integral = histo.integral(includeOverflows);
1326 
1327     // If the integral is empty, the (integrated) efficiency values may as well all be zero, so return here
1328     /// @todo Or throw a LowStatsError exception if h.effNumEntries() == 0?
1329     /// @todo Provide optional alt behaviours
1330     /// @todo Need to check that bins are all positive? Integral could be zero due to large +ve/-ve in different bins :O
1331     if (!integral) return rtn;
1332 
1333     const double integral_err = histo.integralError(includeOverflows);
1334     for (const auto& b : rtn.bins(includeOverflows)) {
1335       const double eff = b.val() / integral;
1336       const double err = sqrt(std::abs( ((1-2*eff)*sqr(b.relTotalErrAvg()) + sqr(eff)*sqr(integral_err)) / sqr(integral) ));
1337       b.set(eff, {-err,err});
1338     }
1339 
1340     return rtn;
1341   }
1342 
1343 
1344   /// @brief Calculate the addition of a BinnedDbn with a BinnedEstimate
1345   template <size_t DbnN, typename... AxisT>
1346   inline BinnedEstimate<AxisT...>
1347   add(const BinnedDbn<DbnN, AxisT...>& dbn, const BinnedEstimate<AxisT...>& est) {
1348     return dbn.mkEstimate() + est;
1349   }
1350   //
1351   template <size_t DbnN, typename... AxisT>
1352   inline BinnedEstimate<AxisT...>
1353   operator + (const BinnedDbn<DbnN, AxisT...>& dbn, const BinnedEstimate<AxisT...>& est) {
1354     return add(dbn, est);
1355   }
1356   //
1357   template <size_t DbnN, typename... AxisT>
1358   inline BinnedEstimate<AxisT...>
1359   operator + (BinnedDbn<DbnN, AxisT...>&& dbn, const BinnedEstimate<AxisT...>& est) {
1360     return add(std::move(dbn), est);
1361   }
1362   //
1363   template <size_t DbnN, typename... AxisT>
1364   inline BinnedEstimate<AxisT...>
1365   operator + (const BinnedDbn<DbnN, AxisT...>& dbn, BinnedEstimate<AxisT...>&& est) {
1366     return add(dbn, std::move(est));
1367   }
1368   //
1369   template <size_t DbnN, typename... AxisT>
1370   inline BinnedEstimate<AxisT...>
1371   operator + (BinnedDbn<DbnN, AxisT...>&& dbn, BinnedEstimate<AxisT...>&& est) {
1372     return add(std::move(dbn), std::move(est));
1373   }
1374 
1375 
1376   /// @brief Calculate the subtraction of a BinnedEstimate from a BinnedDbn
1377   template <size_t DbnN, typename... AxisT>
1378   inline BinnedEstimate<AxisT...>
1379   subtract(const BinnedDbn<DbnN, AxisT...>& dbn, const BinnedEstimate<AxisT...>& est) {
1380     return dbn.mkEstimate() - est;
1381   }
1382   //
1383   template <size_t DbnN, typename... AxisT>
1384   inline BinnedEstimate<AxisT...>
1385   operator - (const BinnedDbn<DbnN, AxisT...>& dbn, const BinnedEstimate<AxisT...>& est) {
1386     return subtract(dbn, est);
1387   }
1388   //
1389   template <size_t DbnN, typename... AxisT>
1390   inline BinnedEstimate<AxisT...>
1391   operator - (BinnedDbn<DbnN, AxisT...>&& dbn, const BinnedEstimate<AxisT...>& est) {
1392     return subtract(std::move(dbn), est);
1393   }
1394   //
1395   template <size_t DbnN, typename... AxisT>
1396   inline BinnedEstimate<AxisT...>
1397   operator - (const BinnedDbn<DbnN, AxisT...>& dbn, BinnedEstimate<AxisT...>&& est) {
1398     return subtract(dbn, std::move(est));
1399   }
1400   //
1401   template <size_t DbnN, typename... AxisT>
1402   inline BinnedEstimate<AxisT...>
1403   operator - (BinnedDbn<DbnN, AxisT...>&& dbn, BinnedEstimate<AxisT...>&& est) {
1404     return subtract(std::move(dbn), std::move(est));
1405   }
1406 
1407 
1408   /// @brief Calculate the division of a BinnedDbn and a BinnedEstimate
1409   template <size_t DbnN, typename... AxisT>
1410   inline BinnedEstimate<AxisT...>
1411   divide(const BinnedDbn<DbnN, AxisT...>& dbn, const BinnedEstimate<AxisT...>& est) {
1412     return dbn.mkEstimate() / est;
1413   }
1414   //
1415   template <size_t DbnN, typename... AxisT>
1416   inline BinnedEstimate<AxisT...>
1417   operator / (const BinnedDbn<DbnN, AxisT...>& dbn, const BinnedEstimate<AxisT...>& est) {
1418     return divide(dbn, est);
1419   }
1420   //
1421   template <size_t DbnN, typename... AxisT>
1422   inline BinnedEstimate<AxisT...>
1423   operator / (BinnedDbn<DbnN, AxisT...>&& dbn, const BinnedEstimate<AxisT...>& est) {
1424     return divide(std::move(dbn), est);
1425   }
1426   //
1427   template <size_t DbnN, typename... AxisT>
1428   inline BinnedEstimate<AxisT...>
1429   operator / (const BinnedDbn<DbnN, AxisT...>& dbn, BinnedEstimate<AxisT...>&& est) {
1430     return divide(dbn, std::move(est));
1431   }
1432   //
1433   template <size_t DbnN, typename... AxisT>
1434   inline BinnedEstimate<AxisT...>
1435   operator / (BinnedDbn<DbnN, AxisT...>&& dbn, BinnedEstimate<AxisT...>&& est) {
1436     return divide(std::move(dbn), std::move(est));
1437   }
1438 
1439 
1440   /// @brief Zip profile objects of the same type into a combined scatter object
1441   ///
1442   /// The resulting object has as many points as profile bins and whose central
1443   /// values are given by the means along the unbinned profile axes with means
1444   /// of first profile corresponding to x-coordinate, means of second profile
1445   /// corresponding to y-coordinate etc.
1446   ///
1447   /// @note The BinnedDbn objects must be profiles and have the same axis config.
1448   template<size_t DbnN, typename... AxisT, typename... Args,
1449            typename = std::enable_if_t<(DbnN == sizeof...(AxisT)+1 &&
1450                                        (std::is_same_v<BinnedDbn<DbnN, AxisT...>, Args> && ...))>>
1451   ScatterND<sizeof...(Args)+1> zipProfiles(const BinnedDbn<DbnN, AxisT...>& p1, Args&&... others,
1452                                            const std::string& path = "") {
1453 
1454     // Check profiles have the same binning
1455     if ( !((p1 == others) && ...) )
1456       throw BinningError("Requested zipping of profiles with incompatible binning!");
1457 
1458     // Construct resulting Scatter whose coordinates
1459     // are given by the unbinned means
1460     constexpr size_t N = sizeof...(Args)+1;
1461     ScatterND<N> rtn;
1462     rtn.setAnnotation("Path", path);
1463     for (const auto& b1 : p1.bins()) {
1464       typename ScatterND<N>::NdVal vals = { b1.mean(DbnN), others.bin(b1.binIndex()).mean(DbnN) ... };
1465       typename ScatterND<N>::NdVal errs = { b1.stdErr(DbnN), others.bin(b1.binIndex()).stdErr(DbnN) ... };
1466       rtn.addPoint(vals, errs);
1467     }
1468     return rtn;
1469   }
1470 
1471   /// @}
1472 
1473 }
1474 
1475 #endif