Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-09-11 09:31:34

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