Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-11-12 11:07:52

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_MathUtils_H
0007 #define YODA_MathUtils_H
0008 
0009 /// @todo Add SFINAE math type stuff (see Rivet) and add inrange() and inrange_closed_closed() etc. aliases cf. MCUtils
0010 
0011 #include "YODA/Exceptions.h"
0012 #include "YODA/Config/BuildConfig.h"
0013 
0014 #include <algorithm>
0015 #include <cassert>
0016 #include <cfloat>
0017 #include <climits>
0018 #include <cmath>
0019 #include <functional>
0020 #include <iostream>
0021 #include <limits>
0022 #include <map>
0023 #include <numeric>
0024 #include <numeric>
0025 #include <ostream>
0026 #include <sstream>
0027 #include <stdexcept>
0028 #include <string>
0029 #include <utility>
0030 #include <vector>
0031 
0032 namespace YODA {
0033 
0034 
0035   /// Pre-defined numeric type limits
0036   /// @deprecated Prefer the standard DBL/INT_MAX
0037   const static double MAXDOUBLE = DBL_MAX; // was std::numeric_limits<double>::max(); -- warns in GCC5
0038   const static double MAXINT = INT_MAX; // was std::numeric_limits<int>::max(); -- warns in GCC5
0039 
0040   /// A pre-defined value of \f$ \pi \f$.
0041   static const double PI = M_PI;
0042 
0043   /// A pre-defined value of \f$ 2\pi \f$.
0044   static const double TWOPI = 2*M_PI;
0045 
0046   /// A pre-defined value of \f$ \pi/2 \f$.
0047   static const double HALFPI = M_PI_2;
0048 
0049   /// Enum for signs of numbers.
0050   enum Sign { MINUS = -1, ZERO = 0, PLUS = 1 };
0051 
0052 
0053   /// @name Comparison functions for safe (floating point) equality tests
0054   /// @{
0055 
0056   /// @brief Compare a number to zero
0057   template <typename NUM, typename = std::enable_if_t<std::is_arithmetic_v<NUM>>>
0058   inline bool isZero(NUM val, double tolerance=1e-5) {
0059     if constexpr(std::is_integral_v<NUM>) {
0060       return val==0;
0061     }
0062     else {
0063       return fabs(val) < tolerance;
0064     }
0065   }
0066 
0067   /// @brief Check if a number is NaN
0068   template <typename NUM, typename = std::enable_if_t<std::is_floating_point_v<NUM>>>
0069   inline bool isNaN(NUM val) { return std::isnan(val); }
0070 
0071   /// @brief Check if a number is non-NaN
0072   template <typename NUM, typename = std::enable_if_t<std::is_floating_point_v<NUM>>>
0073   inline bool notNaN(NUM val) { return !std::isnan(val); }
0074 
0075   /// @brief Compare two numbers for equality with a degree of fuzziness
0076   template <typename N1,
0077             typename N2,
0078             typename = std::enable_if_t<std::is_arithmetic_v<N1> && std::is_arithmetic_v<N2>>>
0079   inline bool fuzzyEquals(N1 a, N2 b, double tolerance=1e-5) {
0080     if constexpr(std::is_integral_v<N1> && std::is_integral_v<N2>) {
0081       return a == b;
0082     }
0083     else {
0084       const double absavg = (std::abs(a) + std::abs(b))/2.0;
0085       const double absdiff = std::abs(a - b);
0086       const bool rtn = (isZero(a) && isZero(b)) || absdiff < tolerance*absavg;
0087       return rtn;
0088     }
0089   }
0090 
0091   /// @brief Comparator wrapper to use with STL algorithms, e.g. std::equal etc.
0092   static std::function<bool(const double, const double)> fuzzyEqComp =
0093     [](const double& lhs, const double& rhs) { return fuzzyEquals(lhs, rhs); };
0094 
0095 
0096   /// @brief Compare two numbers for >= with a degree of fuzziness
0097   ///
0098   /// The @a tolerance parameter on the equality test is as for @c fuzzyEquals.
0099   template <typename N1,
0100             typename N2,
0101             typename = std::enable_if_t<std::is_arithmetic_v<N1> && std::is_arithmetic_v<N2>>>
0102   inline bool fuzzyGtrEquals(N1 a, N2 b, double tolerance=1e-5) {
0103     return a > b || fuzzyEquals(a, b, tolerance);
0104   }
0105 
0106   /// @brief Compare two floating point numbers for <= with a degree of fuzziness
0107   ///
0108   /// The @a tolerance parameter on the equality test is as for @c fuzzyEquals.
0109   template <typename N1,
0110             typename N2,
0111             typename = std::enable_if_t<std::is_arithmetic_v<N1> && std::is_arithmetic_v<N2>>>
0112   inline bool fuzzyLessEquals(N1 a, N2 b, double tolerance=1e-5) {
0113     return a < b || fuzzyEquals(a, b, tolerance);
0114   }
0115 
0116   /// Returns a number floored at the nth decimal place.
0117   inline double approx(double a, int n = 5) {
0118     double roundTo = pow(10.0,n);
0119     a *= roundTo;
0120     a = floor(a);
0121     return a/roundTo;
0122   }
0123   /// @}
0124 
0125 
0126   /// @name Ranges and intervals
0127   /// @{
0128 
0129   /// Represents whether an interval is open (non-inclusive) or closed (inclusive).
0130   ///
0131   /// For example, the interval \f$ [0, \pi) \f$ is closed (an inclusive
0132   /// boundary) at 0, and open (a non-inclusive boundary) at \f$ \pi \f$.
0133   enum RangeBoundary { OPEN=0, SOFT=0, CLOSED=1, HARD=1 };
0134 
0135 
0136   /// @brief Determine if @a value is in the range @a low to @a high, for floating point numbers
0137   ///
0138   /// Interval boundary types are defined by @a lowbound and @a highbound.
0139   /// @todo Optimise to one-line at compile time?
0140   template<typename NUM>
0141   inline bool inRange(NUM value, NUM low, NUM high,
0142                       RangeBoundary lowbound=CLOSED, RangeBoundary highbound=OPEN) {
0143     if (lowbound == OPEN && highbound == OPEN) {
0144       return (value > low && value < high);
0145     } else if (lowbound == OPEN && highbound == CLOSED) {
0146       return (value > low && value <= high);
0147     } else if (lowbound == CLOSED && highbound == OPEN) {
0148       return (value >= low && value < high);
0149     } else { // if (lowbound == CLOSED && highbound == CLOSED)
0150       return (value >= low && value <= high);
0151     }
0152   }
0153 
0154   /// Alternative version of inRange for doubles, which accepts a pair for the range arguments.
0155   template<typename NUM>
0156   inline bool inRange(NUM value, std::pair<NUM, NUM> lowhigh,
0157                       RangeBoundary lowbound=CLOSED, RangeBoundary highbound=OPEN) {
0158     return inRange(value, lowhigh.first, lowhigh.second, lowbound, highbound);
0159   }
0160 
0161 
0162   /// @brief Determine if @a value is in the range @a low to @a high, for integer types
0163   ///
0164   /// Interval boundary types are defined by @a lowbound and @a highbound.
0165   /// @todo Optimise to one-line at compile time?
0166   inline bool inRange(int value, int low, int high,
0167                       RangeBoundary lowbound=CLOSED, RangeBoundary highbound=CLOSED) {
0168     if (lowbound == OPEN && highbound == OPEN) {
0169       return (value > low && value < high);
0170     } else if (lowbound == OPEN && highbound == CLOSED) {
0171       return (value > low && value <= high);
0172     } else if (lowbound == CLOSED && highbound == OPEN) {
0173       return (value >= low && value < high);
0174     } else { // if (lowbound == CLOSED && highbound == CLOSED)
0175       return (value >= low && value <= high);
0176     }
0177   }
0178 
0179   /// Alternative version of @c inRange for ints, which accepts a pair for the range arguments.
0180   inline bool inRange(int value, std::pair<int, int> lowhigh,
0181                       RangeBoundary lowbound=CLOSED, RangeBoundary highbound=OPEN) {
0182     return inRange(value, lowhigh.first, lowhigh.second, lowbound, highbound);
0183   }
0184 
0185   /// @}
0186 
0187 
0188   /// @name Miscellaneous numerical helpers
0189   /// @{
0190 
0191   /// Named number-type squaring operation.
0192   template <typename NUM>
0193   inline NUM sqr(NUM a) {
0194     return a*a;
0195   }
0196 
0197   /// Named number-type addition in quadrature operation.
0198   template <typename... Num, typename = std::enable_if_t<std::conjunction_v<std::is_arithmetic<Num>...>>>
0199   inline double add_quad(Num ... vals) {
0200     return sqrt((0.0 + ... + sqr(vals)));
0201   }
0202 
0203   /// Find the sign of a number.
0204   inline int sign(double val) {
0205     if (isZero(val)) return ZERO;
0206     const int valsign = (val > 0) ? PLUS : MINUS;
0207     return valsign;
0208   }
0209 
0210   /// Find the sign of a number.
0211   inline int sign(int val) {
0212     if (val == 0) return ZERO;
0213     return (val > 0) ? PLUS : MINUS;
0214   }
0215 
0216   /// Find the sign of a number.
0217   inline int sign(long val) {
0218     if (val == 0) return ZERO;
0219     return (val > 0) ? PLUS : MINUS;
0220   }
0221 
0222   /// Subtract two numbers with FP fuzziness.
0223   inline double subtract(double a, double b, double tolerance = 1e-5) {
0224     if (fuzzyEquals(a,b,tolerance))  return 0.;
0225     return a - b;
0226   }
0227 
0228   /// Add two numbers with FP fuzziness.
0229   inline double add(double a, double b, double tolerance = 1e-5) {
0230     return subtract(a,-b,tolerance);
0231   }
0232 
0233   /// @}
0234 
0235 
0236   /// @name Binning helper functions
0237   /// @{
0238 
0239   /// @brief Make a list of @a nbins + 1 values uniformly spaced between @a xmin and @a xmax inclusive.
0240   ///
0241   /// @note The arg ordering and the meaning of the nbins variable is "histogram-like",
0242   /// as opposed to the Numpy/Matlab version.
0243   inline std::vector<double> linspace(size_t nbins, double xmin, double xmax, bool include_end=true) {
0244     if (xmax < xmin)  throw RangeError("xmax should not be smaller than xmin!");
0245     if (nbins == 0)   throw RangeError("Requested number of bins is 0!");
0246     std::vector<double> rtn;
0247     const double interval = (xmax-xmin)/static_cast<double>(nbins);
0248     for (size_t i = 0; i < nbins; ++i) {
0249       rtn.push_back(xmin + i*interval);
0250     }
0251     assert(rtn.size() == nbins);
0252     if (include_end) rtn.push_back(xmax); // exact xmax, not result of n * interval
0253     return rtn;
0254   }
0255 
0256 
0257   /// @brief Make a list of @a nbins + 1 values uniformly spaced in log(x) between @a xmin and @a xmax inclusive.
0258   ///
0259   /// @note The arg ordering and the meaning of the nbins variable is "histogram-like",
0260   /// as opposed to the Numpy/Matlab version, and the xmin and xmax arguments are expressed
0261   /// in "normal" space, rather than as the logarithms of the xmin/xmax values as in Numpy/Matlab.
0262   inline std::vector<double> logspace(size_t nbins, double xmin, double xmax, bool include_end=true) {
0263     if (xmax < xmin)  throw RangeError("xmax should not be smaller than xmin!");
0264     if (xmin < 0)     throw RangeError("xmin should not be negative!");
0265     if (nbins == 0)   throw RangeError("Requested number of bins is 0!");
0266     const double logxmin = std::log(xmin);
0267     const double logxmax = std::log(xmax);
0268     const std::vector<double> logvals = linspace(nbins, logxmin, logxmax);
0269     assert(logvals.size() == nbins+1);
0270     std::vector<double> rtn; rtn.reserve(logvals.size());
0271     rtn.push_back(xmin);
0272     for (size_t i = 1; i < logvals.size()-1; ++i) {
0273       rtn.push_back(std::exp(logvals[i]));
0274     }
0275     assert(rtn.size() == nbins);
0276     if (include_end) rtn.push_back(xmax);
0277     return rtn;
0278   }
0279 
0280 
0281   /// @todo fspace() for uniform sampling from f(x); requires ability to invert fn... how, in general?
0282   //inline std::vector<double> fspace(size_t nbins, double xmin, double xmax, std::function<double(double)>& fn)
0283 
0284 
0285   /// @brief Make a list of @a nbins + 1 values spaced with *density* ~ f(x) between @a xmin and @a end inclusive.
0286   ///
0287   /// The density function @a fn will be evaluated at @a nsample uniformly
0288   /// distributed points between @a xmin and @a xmax, its integral approximated
0289   /// via the Trapezium Rule and used to normalize the distribution, and @a
0290   /// nbins + 1 edges then selected to (approximately) divide into bins each
0291   /// containing fraction 1/@a nbins of the integral.
0292   ///
0293   /// @note The function @a fn does not need to be a normalized pdf, but it should be non-negative.
0294   /// Any negative values returned by @a fn(x) will be truncated to zero and contribute nothing to
0295   /// the estimated integral and binning density.
0296   ///
0297   /// @note The arg ordering and the meaning of the nbins variable is "histogram-like",
0298   /// as opposed to the Numpy/Matlab version, and the xmin and xmax arguments are expressed
0299   /// in "normal" space, rather than in the function-mapped space as with Numpy/Matlab.
0300   ///
0301   /// @note The naming of this function differs from the other, Matlab-inspired ones: the bin spacing is
0302   /// uniform in the CDF of the density function given, rather than in the function itself. For
0303   /// most use-cases this is more intuitive.
0304   inline std::vector<double> pdfspace(size_t nbins, double xmin, double xmax, std::function<double(double)>& fn, size_t nsample=10000) {
0305     const double dx = (xmax-xmin)/(double)nsample;
0306     const std::vector<double> xs = linspace(nsample, xmin, xmax);
0307     std::vector<double> ys(0, nsample);
0308     auto posfn = [&](double x){return std::max(fn(x), 0.0);};
0309     std::transform(xs.begin(), xs.end(), ys.begin(), posfn);
0310     std::vector<double> areas; areas.reserve(nsample);
0311     double areasum = 0;
0312     for (size_t i = 0; i < ys.size()-1; ++i) {
0313       const double area = (ys[i] + ys[i+1])*dx/2.0;
0314       areas[i] = area;
0315       areasum += area;
0316     }
0317     const double df = areasum/(double)nbins;
0318     std::vector<double> xedges{xmin}; xedges.reserve(nbins+1);
0319     double fsum = 0;
0320     for (size_t i = 0; i < nsample-1; ++i) {
0321       fsum += areas[i];
0322       if (fsum > df) {
0323         fsum = 0;
0324         xedges.push_back(xs[i+1]);
0325       }
0326     }
0327     xedges.push_back(xmax);
0328     assert(xedges.size() == nbins+1);
0329     return xedges;
0330   }
0331 
0332 
0333   /// @brief Return the bin index of the given value, @a val, given a vector of bin edges.
0334   ///
0335   /// NB. The @a binedges vector must be sorted
0336   template <typename NUM>
0337   inline int index_between(const NUM& val, const std::vector<NUM>& binedges) {
0338     if (!inRange(val, binedges.front(), binedges.back())) return -1; //< Out of histo range
0339     int index = -1;
0340     for (size_t i = 1; i < binedges.size(); ++i) {
0341       if (val < binedges[i]) {
0342         index = i-1;
0343         break;
0344       }
0345     }
0346     assert(inRange(index, -1, binedges.size()-1));
0347     return index;
0348   }
0349 
0350   /// @}
0351 
0352 }
0353 
0354 #endif