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_BINNEDAXIS_H
0007 #define YODA_BINNEDAXIS_H
0008 
0009 #include "YODA/Utils/BinEstimators.h"
0010 #include "YODA/Utils/BinningUtils.h"
0011 #include "YODA/Utils/MathUtils.h"
0012 #include "YODA/Utils/MetaUtils.h"
0013 #include <cmath>
0014 #include <cstdlib>
0015 #include <limits>
0016 #include <memory>
0017 #include <algorithm>
0018 #include <set>
0019 #include <string>
0020 #include <stdexcept>
0021 #include <type_traits>
0022 #include <vector>
0023 #include <iostream>
0024 #include <iomanip>
0025 
0026 namespace YODA {
0027 
0028   const size_t SEARCH_SIZElc = 16;
0029   const size_t BISECT_LINEAR_THRESHOLDlc = 32;
0030 
0031   namespace YODAConcepts {
0032 
0033       using MetaUtils::conjunction;
0034 
0035       /// @brief Axis concept
0036       template <typename T>
0037       struct AxisImpl {
0038 
0039         /// @note const/volatile function parameter qualifiers are not enforced:
0040         ///  C++ Standard (C++ 17, 16.1 Overloadable declarations):
0041         ///   (3.4) — Parameter declarations that differ only in the presence or
0042         ///   absence of const and/or volatile are equivalent. That is, the const and
0043         ///   volatile type-specifiers for each parameter type are ignored when
0044         ///   determining which function is being declared, defined, or called.
0045         ///
0046         /// noexcept qualifier is not enforced too since it's not part of type.
0047 
0048         /// @brief Function signatures
0049         using index_sig        = size_t (T::*)(const typename T::EdgeT&) const;
0050         using edge_sig         = typename T::EdgeT (T::*)(const size_t) const;
0051         using edges_sig        = const std::vector<std::reference_wrapper<const typename T::EdgeT>> (T::*)() const noexcept;
0052         using size_sig         = size_t (T::*)() const noexcept;
0053         using same_edges_sig   = bool (T::*)(const T&) const noexcept;
0054         using shared_edges_sig = std::vector<typename T::EdgeT> (T::*)(const T&) const noexcept;
0055 
0056         using checkResult = conjunction<
0057             std::is_same<index_sig,         decltype(&T::index)>,
0058             std::is_same<edge_sig,          decltype(&T::edge)>,
0059             //std::is_same<edges_sig,         decltype(&T::edges)>,
0060             std::is_same<size_sig,          decltype(&T::size)>,
0061             std::is_same<same_edges_sig,    decltype(&T::hasSameEdges)>,
0062             std::is_same<shared_edges_sig,  decltype(&T::sharedEdges)>
0063             >;
0064       };
0065   }
0066 
0067 
0068   /// @note Anonymous namespace to limit visibility to this file
0069   namespace {
0070     /// Checks if edge types is continuous
0071     template <typename EdgeT>
0072     using isCAxis = std::enable_if_t<std::is_floating_point<EdgeT>::value>;
0073 
0074     /// Checks if edge type has width measure
0075     template <typename T>
0076     struct hasWidth : std::false_type {};
0077 
0078     template <>
0079     struct hasWidth<std::string> : std::true_type {};
0080   }
0081 
0082 
0083   template <typename T, typename>
0084   class Axis;
0085 
0086 
0087   /// @brief Discrete axis with edges of non-floating-point-type
0088   ///
0089   /// @note Based on *unsorted* std::vector<T>
0090   template <typename T, typename = void>
0091   class Axis {
0092     public:
0093     using EdgeT = T;
0094     using ContainerT = std::vector<T>;
0095     using const_iterator = typename ContainerT::const_iterator;
0096 
0097     /// @name Constructors
0098     //@{
0099 
0100     /// @brief Nullary constructor for unique pointers etc.
0101     Axis() { }
0102 
0103     /// @brief Constructs discrete Axis from edges vector.
0104     ///
0105     /// @note Vector is not sorted by constructor.
0106     Axis(const std::vector<T>& edges);
0107 
0108     /// @brief Constructs discrete Axis from edges vector (rvalue).
0109     ///
0110     /// @note Vector is not sorted by constructor.
0111     Axis(std::vector<T>&& edges);
0112 
0113     /// @brief Constructs discrete Axis from an initializer list.
0114     ///
0115     /// @note Vector is not sorted by constructor.
0116     Axis(std::initializer_list<T>&& edges);
0117 
0118     /// @brief Move constructs Axis
0119     Axis(Axis<T>&& other) : _edges(std::move(other._edges)) {}
0120 
0121     /// @brief Copy constructs Axis
0122     Axis(const Axis<T>& other) : _edges(other._edges) {}
0123 
0124     Axis& operator=(const Axis& other) {
0125       if (this != &other)  _edges = other._edges;
0126       return *this;
0127     }
0128 
0129     Axis& operator=(Axis&& other) {
0130       if (this != &other)  _edges = std::move(other._edges);
0131       return *this;
0132     }
0133 
0134     //@}
0135 
0136     /// @name I/O
0137     // @{
0138 
0139     void _renderYODA(std::ostream& os) const noexcept {
0140       os << "[";
0141       for (size_t i = 0; i < _edges.size(); ++i) {
0142         if (i)  os << ", ";
0143         if constexpr(std::is_same<T, std::string>::value) {
0144           os << std::quoted(_edges[i]);
0145         }
0146         else {
0147           os << _edges[i];
0148         }
0149       }
0150       os << "]";
0151     }
0152 
0153     /// @brief Returns a string representation of EdgeT
0154     std::string type() const noexcept { return TypeID<EdgeT>::name(); }
0155 
0156     int maxEdgeWidth() const noexcept {
0157       int maxwidth = 0;
0158       if constexpr (hasWidth<EdgeT>::value) {
0159         auto it = std::max_element(_edges.begin(), _edges.end(),
0160                                    [](const auto& a, const auto& b) {
0161                                       return a.size() < b.size();
0162         });
0163         maxwidth = (*it).size();
0164       }
0165       return maxwidth;
0166     }
0167 
0168     //@}
0169 
0170     /// @brief Returns index of edge x
0171     ///
0172     /// @note Returns 0 if there is no @a x on this axis.
0173     size_t index(const T& x) const;
0174 
0175     /// @brief Returns edge corresponding to index @a i
0176     EdgeT edge(const size_t i) const;
0177 
0178     /// @brief Returns a copy of the container of edges.
0179     std::vector<EdgeT> edges() const noexcept;
0180 
0181     /// @brief Returns the const begin iterator for the edges container
0182     const_iterator begin() const { return _edges.cbegin(); }
0183 
0184     /// @brief Returns the const end iterator for the edges container
0185     const_iterator end() const { return _edges.cend(); }
0186 
0187     /// @brief Returns number of edges + 1 for this Axis
0188     size_t size() const noexcept;
0189 
0190     /// @brief Returns number of bins of this axis
0191     size_t numBins(const bool includeOverflows = false) const noexcept;
0192 
0193     /// @brief Checks if two axes have exactly the same edges
0194     bool hasSameEdges(const Axis<T>& other) const noexcept;
0195 
0196     /// @brief Finds shared between Axes edges.
0197     ///
0198     /// @note Not sorted, order similar to initial (in constructor) not guaranteed.
0199     std::vector<T> sharedEdges(const Axis<T>& other) const noexcept;
0200 
0201     /// @brief Check if other axis edges are a subset of edges of this one
0202     /// @remark Should it be symmetrical? E.g. ax1.subsetEdges(ax2) == ax2.subsetEdges(ax1)?
0203     bool isSubsetEdges(const Axis<T>& other) const noexcept;
0204 
0205     protected:
0206 
0207     /// @name Utility
0208     // @{
0209 
0210     /// @brief Fills edge storage. Used in constructors.
0211     void fillEdges(std::vector<EdgeT>&& edges) noexcept;
0212 
0213     // @}
0214 
0215     /// @brief Axis edges
0216     std::vector<T> _edges;
0217 
0218   };
0219 
0220 
0221   // @todo Document!
0222   template <typename T, typename U>
0223   Axis<T, U>::Axis(const std::vector<T>& edges) : Axis(std::vector<T>(edges)) {
0224       /// @brief Concept check shall appear inside body of type's member function
0225       /// or outside of type, since otherwise type is considered incomplete.
0226       static_assert(MetaUtils::checkConcept<Axis<EdgeT>, YODAConcepts::AxisImpl>(),
0227         "Axis<T> should implement Axis concept.");
0228   }
0229 
0230   template <typename T, typename U>
0231   Axis<T, U>::Axis(std::vector<T>&& edges) {
0232     fillEdges(std::move(edges));
0233   }
0234 
0235   template <typename T, typename U>
0236   Axis<T, U>::Axis(std::initializer_list<T>&& edges) {
0237     fillEdges(std::vector<T>{edges});
0238   }
0239 
0240   template <typename T, typename U>
0241   size_t Axis<T, U>::index(const T& x) const {
0242     auto it = std::find(this->_edges.begin(), this->_edges.end(), x);
0243     if (it == this->_edges.end())  return 0; // otherflow
0244     return it - this->_edges.begin() + 1;
0245   }
0246 
0247   template <typename T, typename U>
0248   typename Axis<T, U>::EdgeT Axis<T, U>::edge(const size_t i) const {
0249     if (this->_edges.empty()) {
0250       throw RangeError("Axis has no edges!");
0251      }
0252     if (!i || i > this->_edges.size()) {
0253       throw RangeError("Invalid index, must be in range 1.." + std::to_string(this->_edges.size()));
0254     }
0255     return this->_edges.at(i-1);
0256   }
0257 
0258   template <typename T, typename U>
0259   std::vector<typename Axis<T, U>::EdgeT> Axis<T, U>::edges() const noexcept {
0260     return this->_edges;
0261   }
0262 
0263   /// Includes +1 for the otherflow bin
0264   template <typename T, typename U>
0265   size_t Axis<T, U>::size() const noexcept {
0266     return numBins(true);
0267   }
0268 
0269   /// Includes +1 for the otherflow bin
0270   template <typename T, typename U>
0271   size_t Axis<T, U>::numBins(const bool includeOverflows) const noexcept {
0272     return _edges.size() + (includeOverflows? 1 : 0);
0273   }
0274 
0275   template <typename T, typename U>
0276   bool Axis<T, U>::hasSameEdges(const Axis<T>& other) const noexcept {
0277     return _edges.size() == other._edges.size() &&
0278            std::equal(_edges.begin(), _edges.end(), other._edges.begin());
0279   }
0280 
0281   template <typename T, typename U>
0282   std::vector<T> Axis<T, U>::sharedEdges(const Axis<T>& other) const noexcept {
0283     std::vector<EdgeT> res;
0284     const auto& otherBegin = other._edges.begin();
0285     const auto& otherEnd = other._edges.end();
0286     for (const T& edge : this->_edges) {
0287       if (std::find(otherBegin, otherEnd, edge) != otherEnd)
0288         res.emplace_back(std::move(edge));
0289     }
0290     return res;
0291   }
0292 
0293   template <typename T, typename U>
0294   bool Axis<T, U>::isSubsetEdges(const Axis<T>& other) const noexcept {
0295     if (_edges.size() == other._edges.size()) return hasSameEdges(other);
0296 
0297     std::vector<T> edgesLhs(edges());
0298     std::vector<T> edgesRhs(other.edges());
0299 
0300     std::sort(edgesLhs.begin(), edgesLhs.end());
0301     std::sort(edgesRhs.begin(), edgesRhs.end());
0302 
0303     /// @note std::includes demands sorted ranges
0304     return std::includes(edgesLhs.begin(), edgesLhs.end(),
0305                          edgesRhs.begin(), edgesRhs.end());
0306   }
0307 
0308   template <typename T, typename U>
0309   void Axis<T, U>::fillEdges(std::vector<EdgeT>&& edges) noexcept {
0310     for (auto& edge : edges) {
0311       if (std::find(this->_edges.begin(),
0312                     this->_edges.end(), edge) == this->_edges.end()) // no duplicate edges allowed
0313         _edges.emplace_back(std::move(edge));
0314     }
0315   }
0316 
0317 
0318 
0319 
0320 
0321 
0322 
0323 
0324   /// @brief Continuous axis with floating-point-type edges
0325   template <typename T>
0326   class Axis<T, isCAxis<T>> {
0327     public:
0328     using EdgeT = T;
0329     using ContainerT = std::vector<T>;
0330     using const_iterator = typename ContainerT::const_iterator;
0331     using CAxisT = isCAxis<T>;
0332 
0333     /// @brief Nullary constructor for unique pointers etc.
0334     Axis() {
0335       _updateEdges({});
0336       _setEstimator();
0337     }
0338 
0339     Axis(const Axis<T, CAxisT>& other);
0340 
0341     Axis(Axis<T, CAxisT>&& other)
0342       : _est(other._est),
0343         _maskedBins(std::move(other._maskedBins)),
0344         _edges(std::move(other._edges)) {}
0345 
0346     /// @brief Constructs continuous Axis from a vector.
0347     ///
0348     /// @note Edges are sorted on construction stage.
0349     Axis(std::vector<EdgeT> edges);
0350 
0351     /// @brief Constructs continuous Axis from an initializer list.
0352     ///
0353     /// @note Edges are sorted on construction stage
0354     Axis(std::initializer_list<T>&& edges);
0355 
0356     /// @note Vector shouldn't contain any intersecting pairs,
0357     /// e.g. pair1.second > pair2.first. Order of pairs does not
0358     /// matter. BinsEdges is sorted on construction stage.
0359     Axis(std::vector<std::pair<EdgeT, EdgeT>> binsEdges);
0360 
0361     Axis(size_t nBins, EdgeT lower, EdgeT upper);
0362 
0363     Axis& operator=(const Axis& other) {
0364       if (this != &other) {
0365         _est = other._est;
0366         _maskedBins = other._maskedBins;
0367         _edges = other._edges;
0368       }
0369       return *this;
0370     }
0371 
0372     Axis& operator=(Axis&& other) {
0373       if (this != &other) {
0374         _est = std::move(other._est);
0375         _maskedBins = std::move(other._maskedBins);
0376         _edges = std::move(other._edges);
0377       }
0378       return *this;
0379     }
0380 
0381     // /// Explicit constructor, specifying the edges and estimation strategy
0382     // Axis(const std::vector<double>& edges, bool log) {
0383     //   _updateEdges(edges);
0384     //   // Internally use a log or linear estimator as requested
0385     //   if (log) {
0386     //     _est.reset(new LogBinEstimator(edges.size()-1, edges.front(), edges.back()));
0387     //   } else {
0388     //     _est.reset(new LinBinEstimator(edges.size()-1, edges.front(), edges.back()));
0389     //   }
0390     // }
0391 
0392 
0393     /// @brief Bin searcher
0394     ///
0395     /// @author David Mallows
0396     /// @author Andy Buckley
0397     ///
0398     /// Handles low-level bin lookups using a hybrid algorithm that is
0399     /// considerably faster for regular (logarithmic or linear) and near-regular
0400     /// binnings. Comparable performance for irregular binnings.
0401     ///
0402     /// The reason this works is that linear search is faster than bisection
0403     /// search up to about 32-64 elements. So we make a guess, and we then do a
0404     /// linear search. If that fails, then we bisect on the remainder,
0405     /// terminating once bisection search has got the range down to about 32. So
0406     /// we actually pay for the fanciness of predicting the bin out of speeding
0407     /// up the bisection search by finishing it with a linear search. So in most
0408     /// cases, we get constant-time lookups regardless of the space.
0409     ///
0410     size_t index(const EdgeT& x) const;
0411 
0412     /// @brief Returns amount of bins in axis
0413     size_t size() const noexcept;
0414 
0415     /// @brief Returns amount of bins in axis
0416     size_t numBins(const bool includeOverflows = false) const noexcept;
0417 
0418     /// @brief Returns edge corresponding to index
0419     EdgeT edge(const size_t i) const;
0420 
0421     /// @brief Returns a copy of all axis edges. Includes -inf and +inf edges.
0422     std::vector<EdgeT> edges() const noexcept;
0423 
0424     /// @brief Returns the const begin iterator for the edges container
0425     const_iterator begin() const { return _edges.cbegin(); }
0426 
0427     /// @brief Returns the const end iterator for the edges container
0428     const_iterator end() const { return _edges.cend(); }
0429 
0430     /// @brief Check if two BinnedAxis objects have the same edges
0431     bool hasSameEdges(const Axis<EdgeT, CAxisT>& other) const noexcept;
0432 
0433     /// @brief Find edges which are shared between BinnedAxis objects, within numeric tolerance
0434     /// @note The return vector is sorted and includes -inf and inf
0435     std::vector<T> sharedEdges(const Axis<EdgeT, CAxisT>& other) const noexcept;
0436 
0437     /// @brief Check if other axis edges are a subset of edges of this one
0438     /// @remark Should it be symmetrical? E.g. ax1.subsetEdges(ax2) == ax2.subsetEdges(ax1)?
0439     bool isSubsetEdges(const Axis<EdgeT, CAxisT>& other) const noexcept;
0440 
0441     /// @brief Returns the masked indices
0442     std::vector<size_t> maskedBins() const noexcept {  return _maskedBins; }
0443 
0444     // ssize_t index_inrange(double x) const {
0445     //   const size_t i = index(x);
0446     //   /// Change to <= and >=
0447     //   if (i == 0 || i == _edges.size()-1) return -1;
0448     //   return i;
0449     // }
0450 
0451     /// @name I/O
0452     // @{
0453 
0454     void _renderYODA(std::ostream& os) const noexcept {
0455       os << "[";
0456       size_t nEdges = _edges.size() - 2; // exclude under-/overflow
0457       for (size_t i = 0; i < nEdges; ++i) {
0458         if (i) {
0459           os << ", ";
0460         }
0461         os << _edges[i+1];
0462       }
0463       os << "]";
0464     }
0465 
0466     /// @brief Returns a string representation of EdgeT
0467     std::string type() const noexcept { return TypeID<EdgeT>::name(); }
0468 
0469     // @}
0470 
0471     /// @name Transformations
0472     // @{
0473 
0474     /// @brief Merges bins, e.g. removes edges in between.
0475     ///
0476     /// @note At least 1 non-overflow bin must exist after merging.
0477     void mergeBins(std::pair<size_t, size_t>);
0478 
0479     // @}
0480 
0481     /// @name Space characteristics
0482     // @{
0483 
0484     std::vector<T> widths(const bool includeOverflows = false) const noexcept {
0485       // number of widths = number of edges - 1
0486       // unless you exclude under-/overflow
0487       size_t offset = includeOverflows? 1 : 3;
0488       std::vector<T> ret(_edges.size()-offset);
0489       size_t start = 1 + !includeOverflows;
0490       size_t end = _edges.size() - !includeOverflows;
0491       for (size_t i = start; i < end; ++i) {
0492         ret[i-start] = _edges[i] - _edges[i-1];
0493       }
0494       return ret;
0495     }
0496 
0497     /// @brief Width of bin side on this axis.
0498     EdgeT width(size_t binNum) const noexcept {
0499       return _edges[binNum+1] - _edges[binNum];
0500     }
0501 
0502     /// @brief Max of bin side on this axis.
0503     EdgeT max(size_t binNum) const noexcept {
0504       return _edges[binNum+1];
0505     }
0506 
0507 
0508     /// @brief Min of bin side on this axis.
0509     EdgeT min(size_t binNum) const noexcept {
0510       return _edges[binNum];
0511     }
0512 
0513     /// @brief Mid of bin side on this axis.
0514     EdgeT mid(size_t binNum) const noexcept {
0515       /// @note Corner bins are overflow bins, thus infinite limit values.
0516       if(binNum == 0)
0517         return std::numeric_limits<EdgeT>::min();
0518       if (binNum == (numBins(true) - 1))
0519         return std::numeric_limits<EdgeT>::max();
0520 
0521       EdgeT minVal = min(binNum);
0522       return (max(binNum) - minVal)/2 + minVal;
0523     }
0524     // @}
0525 
0526     protected:
0527 
0528     /// @brief Set the edges array and related member variables
0529     void _updateEdges(std::vector<EdgeT>&& edges) noexcept;
0530 
0531     /// @brief Set the estimator.
0532     /// @note Used in constructors.
0533     void _setEstimator() noexcept;
0534 
0535     /// @brief Linear search in the forward direction
0536     ///
0537     /// Return bin index or -1 if not found within linear search range. Assumes that edges[istart] <= x
0538     ssize_t _linsearch_forward(size_t istart, double x, size_t nmax) const noexcept;
0539 
0540     /// @brief Linear search in the backward direction
0541     ///
0542     /// Return bin index or -1 if not found within linear search range. Assumes that edges[istart] > x
0543     ssize_t _linsearch_backward(size_t istart, double x, size_t nmax) const noexcept;
0544 
0545     /// Truncated bisection search, adapted from C++ std lib implementation
0546     size_t _bisect(double x, size_t imin, size_t imax) const noexcept;
0547 
0548     /// BinEstimator object to be used for making fast bin index guesses
0549     std::shared_ptr<BinEstimator> _est;
0550 
0551     /// @brief masked bins indices.
0552     std::vector<size_t> _maskedBins;
0553 
0554     /// @brief Axis edges
0555     std::vector<T> _edges;
0556 
0557   };
0558 
0559   template <typename T>
0560   Axis<T, isCAxis<T>>::Axis(const Axis<T, CAxisT>& other) {
0561     /// @brief Concept check shall appear inside body of type's member function
0562     /// or outside of type, since otherwise type is considered incomplete.
0563     /// @note Concept check appears once to check whether the type Axis satisfies
0564     /// the concept.
0565     static_assert(MetaUtils::checkConcept<Axis<EdgeT>, YODAConcepts::AxisImpl>(),
0566       "Axis<T> should implement Axis concept.");
0567 
0568     _est = other._est;
0569     _edges = other._edges;
0570     _maskedBins = other._maskedBins;
0571   }
0572 
0573   template <typename T>
0574   Axis<T, isCAxis<T>>::Axis(const size_t nBins, const EdgeT lower, const EdgeT upper) {
0575     if(upper <= lower)
0576       throw(std::logic_error("Upper bound should be larger than lower."));
0577     _edges.resize(nBins+1+2);
0578     double step = (upper - lower) / nBins;
0579 
0580     _edges[0] = -std::numeric_limits<double>::infinity();
0581 
0582     _edges[1] = lower;
0583 
0584     for(size_t i = 2; i < _edges.size()-1; i++) {
0585       _edges[i] = _edges[i-1] + step;
0586     }
0587 
0588     _edges[_edges.size()-1] = std::numeric_limits<double>::infinity();
0589 
0590     _setEstimator();
0591   }
0592 
0593   template <typename T>
0594   Axis<T, isCAxis<T>>::Axis(std::vector<std::pair<EdgeT, EdgeT>> binsEdges) {
0595     if (binsEdges.empty()) throw BinningError("Expected at least one discrete edge");
0596 
0597     std::sort(binsEdges.begin(), binsEdges.end(), [](auto &left, auto &right) {
0598       return left.first < right.first;
0599     });
0600 
0601     _edges.resize(binsEdges.size()*2+2);
0602 
0603     _edges[0] = -std::numeric_limits<double>::infinity();
0604 
0605 
0606     /*             Edges pairs from binsEdges vector
0607     ///                      ____|____
0608     ///            __{1,3}__/         \_{5,6}__
0609     ///           /        |    GAP    |       \
0610     ///   -inf    1        3   MASKED  5       6   +inf
0611     ///     | BIN |  BIN   |    BIN    |  BIN  | BIN |
0612     ///    i=0   i=1      i=2         i=3     i=4   i=5
0613     */
0614 
0615     size_t i = 1;
0616     for (const auto& pair : binsEdges) {
0617       if (!fuzzyGtrEquals(pair.first, _edges[i-1])) throw BinningError("Bin edges shouldn't intersect");
0618       if (i == 1 && std::isinf(pair.first) && pair.first < 0) {
0619         _edges[i++] = pair.second;
0620         continue;
0621       }
0622       if (fuzzyEquals(pair.first, _edges[i-1])) {
0623         _edges[i++] = pair.second; /// Merge if equal
0624         continue;
0625       }
0626       if (i != 1 && pair.first > _edges[i-1]) {
0627         /// @note If there is a gap, mark bin as masked.
0628         _maskedBins.emplace_back(i-1);
0629       }
0630       _edges[i++] = pair.first;
0631       _edges[i++] = pair.second;
0632     }
0633 
0634     _edges[i] = std::numeric_limits<double>::infinity();
0635 
0636     _edges.resize(i+1); /// In case there have been merges. +1 to account for +inf.
0637 
0638     _setEstimator();
0639   }
0640 
0641   template <typename T>
0642   Axis<T, isCAxis<T>>::Axis(std::vector<EdgeT> edges) {
0643     std::sort(edges.begin(), edges.end());
0644     edges.erase( std::unique(edges.begin(), edges.end()), edges.end() );
0645 
0646     _updateEdges(std::move(edges));
0647 
0648     _setEstimator();
0649   }
0650 
0651   template <typename T>
0652   Axis<T, isCAxis<T>>::Axis(std::initializer_list<T>&& edgeList) {
0653     std::vector<T> edges{edgeList};
0654     std::sort(edges.begin(), edges.end());
0655     edges.erase( std::unique(edges.begin(), edges.end()), edges.end() );
0656 
0657     _updateEdges(std::move(edges));
0658 
0659     _setEstimator();
0660   }
0661 
0662   template <typename T>
0663   size_t Axis<T, isCAxis<T>>::index(const EdgeT& x) const {
0664       if (_edges.size() <= 2) throw BinningError("Axis initialised without specifying edges");
0665       // Check overflows
0666       if (std::isinf(x)) { return x < 0? 0 : _edges.size() - 2; }
0667       // Get initial estimate
0668       size_t index = std::min(_est->estindex(x), _edges.size()-2);
0669       // Return now if this is the correct bin
0670       if (x >= this->_edges[index] && x < this->_edges[index+1]) return index;
0671 
0672       // Otherwise refine the estimate, if x is not exactly on a bin edge
0673       if (x > this->_edges[index]) {
0674         const ssize_t newindex = _linsearch_forward(index, x, SEARCH_SIZElc);
0675         index = (newindex > 0) ? newindex : _bisect(x, index, this->_edges.size()-1);
0676       } else if (x < this->_edges[index]) {
0677         const ssize_t newindex = _linsearch_backward(index, x, SEARCH_SIZElc);
0678         index = (newindex > 0) ? newindex : _bisect(x, 0, index+1);
0679       }
0680 
0681       assert(x >= this->_edges[index] && (x < this->_edges[index+1] || std::isinf(x)));
0682       return index;
0683   }
0684 
0685   template <typename T>
0686   size_t Axis<T, isCAxis<T>>::size() const noexcept {
0687     return numBins(true); // number of visible + 2 for +-inf
0688   }
0689 
0690   template <typename T>
0691   size_t Axis<T, isCAxis<T>>::numBins(const bool includeOverflows) const noexcept {
0692     if (_edges.size() < 3)  return includeOverflows? 1 : 0; // no visible bin edge
0693     return this->_edges.size() - (includeOverflows? 1 : 3); // has 2 extra edges for +-inf
0694   }
0695 
0696   template <typename T>
0697   typename Axis<T, isCAxis<T>>::EdgeT Axis<T, isCAxis<T>>::edge(const size_t i) const {
0698     return this->_edges.at(i);
0699   }
0700 
0701   template <typename T>
0702   std::vector<typename Axis<T, isCAxis<T>>::EdgeT> Axis<T, isCAxis<T>>::edges() const noexcept {
0703     return this->_edges;
0704   }
0705 
0706   template <typename T>
0707   bool Axis<T, isCAxis<T>>::hasSameEdges(const Axis<T, CAxisT>& other) const noexcept{
0708     if (this->numBins(true) != other.numBins(true)) return false;
0709     for (size_t i = 1; i < this->numBins(true) - 1; i++) {
0710       /// @todo Be careful about using fuzzyEquals... should be an exact comparison?
0711       if (!fuzzyEquals(edge(i), other.edge(i))) return false;
0712     }
0713     return true;
0714   }
0715 
0716   template <typename T>
0717   std::vector<T> Axis<T, isCAxis<T>>::sharedEdges(const Axis<T, CAxisT>& other) const noexcept {
0718     std::vector<T> intersection;
0719 
0720     /// Skip if any of axes only has two limit edges
0721     if (_edges.size() > 2 && other._edges.size() > 2) {
0722       std::set_intersection(std::next(_edges.begin()), std::prev(_edges.end()),
0723                             std::next(other._edges.begin()), std::prev(other._edges.end()),
0724                             std::back_inserter(intersection));
0725     }
0726 
0727     std::vector<T> rtn;
0728     rtn.reserve(intersection.size()+2);
0729 
0730     rtn.emplace_back(-std::numeric_limits<Axis<T, isCAxis<T>>::EdgeT>::infinity());
0731     rtn.insert(std::next(rtn.begin()),
0732                   std::make_move_iterator(intersection.begin()),
0733                   std::make_move_iterator(intersection.end()));
0734     rtn.emplace_back(std::numeric_limits<Axis<T, isCAxis<T>>::EdgeT>::infinity());
0735 
0736     return rtn;
0737   }
0738 
0739   template <typename T>
0740   bool Axis<T, isCAxis<T>>::isSubsetEdges(const Axis<T, CAxisT>& other) const noexcept {
0741     if (_edges.size() == other._edges.size()) return hasSameEdges(other);
0742 
0743     /// Skip if any of axes only has two limit edges
0744     if (_edges.size() > 2 && other._edges.size() > 2) {
0745       /// @note std::includes demands sorted ranges
0746       return std::includes(std::next(_edges.begin()), std::prev(_edges.end()),
0747                             std::next(other._edges.begin()), std::prev(other._edges.end()));
0748     }
0749 
0750     /// Since one of the axes consists only of limits (-inf, +inf), it's a
0751     /// subset of the other one
0752     return true;
0753   }
0754 
0755   template <typename T>
0756   void Axis<T, isCAxis<T>>::mergeBins(std::pair<size_t, size_t> mergeRange) {
0757     if (_edges.size() <= 2) throw BinningError("Axis initialised without specifying edges");
0758     if (mergeRange.second < mergeRange.first) throw RangeError("Upper index comes before lower index");
0759     if (mergeRange.second >= numBins(true)) throw RangeError("Upper index exceeds last visible bin");
0760     _edges.erase(_edges.begin()+mergeRange.first+1, _edges.begin() + mergeRange.second + 1);
0761     // masked bins above the merge range need to be re-indexed
0762     // masked bins within the merge range need to be unmasked
0763     std::vector<size_t> toRemove;
0764     size_t mrange = mergeRange.second - mergeRange.first;
0765     for (size_t i = 0; i < _maskedBins.size(); ++i) {
0766       if (_maskedBins[i] > mergeRange.second)  _maskedBins[i] -= mrange;
0767       else if (_maskedBins[i] >= mergeRange.first) toRemove.push_back(_maskedBins[i]);
0768     }
0769     if (toRemove.size()) {
0770       _maskedBins.erase(
0771         std::remove_if(_maskedBins.begin(), _maskedBins.end(), [&](const auto& idx) {
0772           return std::find(toRemove.begin(), toRemove.end(), idx) != toRemove.end();
0773       }), _maskedBins.end());
0774     }
0775   }
0776 
0777 
0778   template <typename T>
0779   void Axis<T, isCAxis<T>>::_updateEdges(std::vector<EdgeT>&& edges) noexcept {
0780     // Array of in-range edges, plus underflow and overflow sentinels
0781     _edges.clear();
0782 
0783     // Move vector and insert -+inf at ends
0784     _edges.emplace_back(-std::numeric_limits<Axis<T, isCAxis<T>>::EdgeT>::infinity());
0785     _edges.insert(std::next(_edges.begin()),
0786                   std::make_move_iterator(edges.begin()),
0787                   std::make_move_iterator(edges.end()));
0788     _edges.emplace_back(std::numeric_limits<Axis<T, isCAxis<T>>::EdgeT>::infinity());
0789   }
0790 
0791   template <typename T>
0792   void Axis<T, isCAxis<T>>::_setEstimator() noexcept {
0793     if (_edges.empty()) {
0794         _est = std::make_shared<LinBinEstimator>(0, 0, 1);
0795       } else if (_edges.front() <= 0.0) {
0796         _est = std::make_shared<LinBinEstimator>(_edges.size()-1, _edges.front(), _edges.back());
0797       } else {
0798         LinBinEstimator linEst(_edges.size()-1, _edges.front(), _edges.back());
0799         LogBinEstimator logEst(_edges.size()-1, _edges.front(), _edges.back());
0800 
0801         // Calculate mean index estimate deviations from the correct answers (for bin edges)
0802         double logsum = 0, linsum = 0;
0803         for (size_t i = 0; i < _edges.size(); i++) {
0804           logsum += logEst(_edges[i]) - i;
0805           linsum += linEst(_edges[i]) - i;
0806         }
0807         const double log_avg = logsum / _edges.size();
0808         const double lin_avg = linsum / _edges.size();
0809 
0810         // This also implicitly works for NaN returned from the log There is a
0811         // subtle bug here if the if statement is the other way around, as
0812         // (nan < linsum) -> false always.  But (nan > linsum) -> false also.
0813         if (log_avg < lin_avg) { //< Use log estimator if its avg performance is better than lin
0814           _est = std::make_shared<LogBinEstimator>(logEst);
0815         } else { // Else use linear estimation
0816           _est = std::make_shared<LinBinEstimator>(linEst);
0817         }
0818     }
0819   }
0820 
0821 
0822   template <typename T>
0823   ssize_t Axis<T, isCAxis<T>>::_linsearch_forward(size_t istart, double x, size_t nmax) const noexcept {
0824     assert(x >= this->_edges[istart]); // assumption that x >= start is wrong
0825     for (size_t i = 0; i < nmax; i++) {
0826       const size_t j = istart + i + 1; // index of _next_ edge
0827       if (j > this->_edges.size()-1) return -1;
0828       if (x < this->_edges[j]) {
0829         assert(x >= this->_edges[j-1] && (x < this->_edges[j] || std::isinf(x)));
0830         return j-1; // note one more iteration needed if x is on an edge
0831       }
0832     }
0833     return -1;
0834   }
0835 
0836   template <typename T>
0837   ssize_t Axis<T, isCAxis<T>>::_linsearch_backward(size_t istart, double x, size_t nmax) const noexcept {
0838     assert(x < this->_edges[istart]); // assumption that x < start is wrong
0839     for (size_t i = 0; i < nmax; i++) {
0840       const int j = istart - i - 1; // index of _next_ edge (working backwards)
0841       if (j < 0) return -1;
0842       if (x >= this->_edges[j]) {
0843         assert(x >= this->_edges[j] && (x < this->_edges[j+1] || std::isinf(x)));
0844         return (ssize_t) j; // note one more iteration needed if x is on an edge
0845       }
0846     }
0847     return -1;
0848   }
0849 
0850   template <typename T>
0851   size_t Axis<T, isCAxis<T>>::_bisect(double x, size_t imin, size_t imax) const noexcept {
0852     size_t len = imax - imin;
0853     while (len >= BISECT_LINEAR_THRESHOLDlc) {
0854       const size_t half = len >> 1;
0855       const size_t imid = imin + half;
0856       if (x >= this->_edges[imid]) {
0857         if (x < this->_edges[imid+1]) return imid; // Might as well return directly if we get lucky!
0858         imin = imid;
0859       } else {
0860         imax = imid;
0861       }
0862       len = imax - imin;
0863     }
0864     assert(x >= this->_edges[imin] && (x < this->_edges[imax] || std::isinf(x)));
0865     return _linsearch_forward(imin, x, BISECT_LINEAR_THRESHOLDlc);
0866   }
0867 
0868 }
0869 
0870 #endif