Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-09-18 09:34:44

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