Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-04-19 09:13:38

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