Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-10-31 09:20:42

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 <functional>
0016 #include <numeric>
0017 #include <cassert>
0018 #include <cfloat>
0019 #include <climits>
0020 #include <cmath>
0021 #include <functional>
0022 #include <iostream>
0023 #include <limits>
0024 #include <map>
0025 #include <numeric>
0026 #include <ostream>
0027 #include <sstream>
0028 #include <stdexcept>
0029 #include <string>
0030 #include <utility>
0031 #include <vector>
0032 
0033 namespace YODA {
0034 
0035 
0036   /// Pre-defined numeric type limits
0037   /// @deprecated Prefer the standard DBL/INT_MAX
0038   const static double MAXDOUBLE = DBL_MAX; // was std::numeric_limits<double>::max(); -- warns in GCC5
0039   const static double MAXINT = INT_MAX; // was std::numeric_limits<int>::max(); -- warns in GCC5
0040 
0041   /// A pre-defined value of \f$ \pi \f$.
0042   static const double PI = M_PI;
0043 
0044   /// A pre-defined value of \f$ 2\pi \f$.
0045   static const double TWOPI = 2*M_PI;
0046 
0047   /// A pre-defined value of \f$ \pi/2 \f$.
0048   static const double HALFPI = M_PI_2;
0049 
0050   /// Enum for signs of numbers.
0051   enum Sign { MINUS = -1, ZERO = 0, PLUS = 1 };
0052 
0053 
0054   /// @name Comparison functions for safe (floating point) equality tests
0055   //@{
0056 
0057   /// @brief Compare a number to zero
0058   ///
0059   /// This version for floating point types has a degree of fuzziness expressed
0060   /// by the absolute @a tolerance parameter, for floating point safety.
0061   template <typename NUM>
0062   inline typename std::enable_if_t<std::is_floating_point_v<NUM>, bool>
0063   isZero(NUM val, double tolerance=1e-5) {
0064     return fabs(val) < tolerance;
0065   }
0066 
0067   /// @brief Compare a number to zero
0068   ///
0069   /// SFINAE template specialisation for integers, since there is no FP
0070   /// precision issue.
0071   template <typename NUM>
0072   inline typename std::enable_if_t<std::is_integral_v<NUM>, bool>
0073   isZero(NUM val, double = 1e-5) {
0074     return val==0;
0075   }
0076 
0077   /// @brief Check if a number is NaN
0078   template <typename NUM>
0079   inline typename std::enable_if_t<std::is_floating_point_v<NUM>, bool>
0080   isNaN(NUM val) { return std::isnan(val); }
0081 
0082   /// @brief Check if a number is non-NaN
0083   template <typename NUM>
0084   inline typename std::enable_if_t<std::is_floating_point_v<NUM>, bool>
0085   notNaN(NUM val) { return !std::isnan(val); }
0086 
0087   /// @brief Compare two numbers for equality with a degree of fuzziness
0088   ///
0089   /// This version for floating point types (if any argument is FP) has a degree
0090   /// of fuzziness expressed by the fractional @a tolerance parameter, for
0091   /// floating point safety.
0092   template <typename N1, typename N2>
0093   inline typename std::enable_if_t<
0094     std::is_arithmetic_v<N1> && std::is_arithmetic_v<N2> &&
0095    (std::is_floating_point_v<N1> || std::is_floating_point_v<N2>), bool>
0096   fuzzyEquals(N1 a, N2 b, double tolerance=1e-5) {
0097     const double absavg = (std::abs(a) + std::abs(b))/2.0;
0098     const double absdiff = std::abs(a - b);
0099     const bool rtn = (isZero(a) && isZero(b)) || absdiff < tolerance*absavg;
0100     return rtn;
0101   }
0102 
0103   /// @brief Compare two numbers for equality with a degree of fuzziness
0104   ///
0105   /// Simpler SFINAE template specialisation for integers, since there is no FP
0106   /// precision issue.
0107   template <typename N1, typename N2>
0108   inline typename std::enable_if_t<
0109     std::is_integral_v<N1> && std::is_integral_v<N2>, bool>
0110     fuzzyEquals(N1 a, N2 b, double) { //< NB. unused tolerance parameter for ints, still needs a default value!
0111     return a == b;
0112   }
0113 
0114   /// @brief Comparator wrapper to use with STL algorithms, e.g. std::equal etc.
0115   static std::function<bool(const double, const double)> fuzzyEqComp =
0116     [](const double& lhs, const double& rhs) { return fuzzyEquals(lhs, rhs); };
0117 
0118 
0119   /// @brief Compare two numbers for >= with a degree of fuzziness
0120   ///
0121   /// The @a tolerance parameter on the equality test is as for @c fuzzyEquals.
0122   template <typename N1, typename N2>
0123   inline typename std::enable_if_t<
0124     std::is_arithmetic_v<N1> && std::is_arithmetic_v<N2>, bool>
0125   fuzzyGtrEquals(N1 a, N2 b, double tolerance=1e-5) {
0126     return a > b || fuzzyEquals(a, b, tolerance);
0127   }
0128 
0129   /// @brief Compare two floating point numbers for <= with a degree of fuzziness
0130   ///
0131   /// The @a tolerance parameter on the equality test is as for @c fuzzyEquals.
0132   template <typename N1, typename N2>
0133   inline typename std::enable_if_t<
0134     std::is_arithmetic_v<N1> && std::is_arithmetic_v<N2>, bool>
0135   fuzzyLessEquals(N1 a, N2 b, double tolerance=1e-5) {
0136     return a < b || fuzzyEquals(a, b, tolerance);
0137   }
0138 
0139   /// Returns a number floored at the nth decimal place.
0140   inline double approx(double a, int n = 5) {
0141     double roundTo = pow(10.0,n);
0142     a *= roundTo;
0143     a = floor(a);
0144     return a/roundTo;
0145   }
0146   //@}
0147 
0148 
0149   /// @name Ranges and intervals
0150   //@{
0151 
0152   /// Represents whether an interval is open (non-inclusive) or closed (inclusive).
0153   ///
0154   /// For example, the interval \f$ [0, \pi) \f$ is closed (an inclusive
0155   /// boundary) at 0, and open (a non-inclusive boundary) at \f$ \pi \f$.
0156   enum RangeBoundary { OPEN=0, SOFT=0, CLOSED=1, HARD=1 };
0157 
0158 
0159   /// @brief Determine if @a value is in the range @a low to @a high, for floating point numbers
0160   ///
0161   /// Interval boundary types are defined by @a lowbound and @a highbound.
0162   /// @todo Optimise to one-line at compile time?
0163   template<typename NUM>
0164   inline bool inRange(NUM value, NUM low, NUM high,
0165                       RangeBoundary lowbound=CLOSED, RangeBoundary highbound=OPEN) {
0166     if (lowbound == OPEN && highbound == OPEN) {
0167       return (value > low && value < high);
0168     } else if (lowbound == OPEN && highbound == CLOSED) {
0169       return (value > low && value <= high);
0170     } else if (lowbound == CLOSED && highbound == OPEN) {
0171       return (value >= low && value < high);
0172     } else { // if (lowbound == CLOSED && highbound == CLOSED) {
0173       return (value >= low && value <= high);
0174     }
0175   }
0176 
0177   /// Alternative version of inRange for doubles, which accepts a pair for the range arguments.
0178   template<typename NUM>
0179   inline bool inRange(NUM value, std::pair<NUM, NUM> lowhigh,
0180                       RangeBoundary lowbound=CLOSED, RangeBoundary highbound=OPEN) {
0181     return inRange(value, lowhigh.first, lowhigh.second, lowbound, highbound);
0182   }
0183 
0184 
0185   /// @brief Determine if @a value is in the range @a low to @a high, for integer types
0186   ///
0187   /// Interval boundary types are defined by @a lowbound and @a highbound.
0188   /// @todo Optimise to one-line at compile time?
0189   inline bool inRange(int value, int low, int high,
0190                       RangeBoundary lowbound=CLOSED, RangeBoundary highbound=CLOSED) {
0191     if (lowbound == OPEN && highbound == OPEN) {
0192       return (value > low && value < high);
0193     } else if (lowbound == OPEN && highbound == CLOSED) {
0194       return (value > low && value <= high);
0195     } else if (lowbound == CLOSED && highbound == OPEN) {
0196       return (value >= low && value < high);
0197     } else { // if (lowbound == CLOSED && highbound == CLOSED) {
0198       return (value >= low && value <= high);
0199     }
0200   }
0201 
0202   /// Alternative version of @c inRange for ints, which accepts a pair for the range arguments.
0203   inline bool inRange(int value, std::pair<int, int> lowhigh,
0204                       RangeBoundary lowbound=CLOSED, RangeBoundary highbound=OPEN) {
0205     return inRange(value, lowhigh.first, lowhigh.second, lowbound, highbound);
0206   }
0207 
0208   //@}
0209 
0210 
0211   /// @name Miscellaneous numerical helpers
0212   //@{
0213 
0214   /// Named number-type squaring operation.
0215   template <typename NUM>
0216   inline NUM sqr(NUM a) {
0217     return a*a;
0218   }
0219 
0220   /// Named number-type addition in quadrature operation.
0221   template <typename... Num>
0222   inline std::enable_if_t<std::conjunction_v<std::is_arithmetic<Num>...>, double>
0223   add_quad(Num ... vals) {
0224     return sqrt((0.0 + ... + sqr(vals)));
0225   }
0226 
0227   /// Find the sign of a number
0228   inline int sign(double val) {
0229     if (isZero(val)) return ZERO;
0230     const int valsign = (val > 0) ? PLUS : MINUS;
0231     return valsign;
0232   }
0233 
0234   /// Find the sign of a number
0235   inline int sign(int val) {
0236     if (val == 0) return ZERO;
0237     return (val > 0) ? PLUS : MINUS;
0238   }
0239 
0240   /// Find the sign of a number
0241   inline int sign(long val) {
0242     if (val == 0) return ZERO;
0243     return (val > 0) ? PLUS : MINUS;
0244   }
0245 
0246   /// Subtract two numbers with FP fuzziness
0247   inline double subtract(double a, double b, double tolerance = 1e-5) {
0248     if (fuzzyEquals(a,b,tolerance))  return 0.;
0249     return a - b;
0250   }
0251 
0252   /// Add two numbers with FP fuzziness
0253   inline double add(double a, double b, double tolerance = 1e-5) {
0254     return subtract(a,-b,tolerance);
0255   }
0256 
0257   //@}
0258 
0259 
0260   /// @name Binning helper functions
0261   //@{
0262 
0263   /// @brief Make a list of @a nbins + 1 values uniformly spaced between @a xmin and @a xmax inclusive.
0264   ///
0265   /// @note The arg ordering and the meaning of the nbins variable is "histogram-like",
0266   /// as opposed to the Numpy/Matlab version.
0267   inline std::vector<double> linspace(size_t nbins, double xmin, double xmax, bool include_end=true) {
0268     if (xmax < xmin)  throw RangeError("xmax should not be smaller than xmin!");
0269     if (nbins == 0)   throw RangeError("Requested number of bins is 0!");
0270     std::vector<double> rtn;
0271     const double interval = (xmax-xmin)/static_cast<double>(nbins);
0272     for (size_t i = 0; i < nbins; ++i) {
0273       rtn.push_back(xmin + i*interval);
0274     }
0275     assert(rtn.size() == nbins);
0276     if (include_end) rtn.push_back(xmax); // exact xmax, not result of n * interval
0277     return rtn;
0278   }
0279 
0280 
0281   /// @brief Make a list of @a nbins + 1 values uniformly spaced in log(x) between @a xmin and @a xmax inclusive.
0282   ///
0283   /// @note The arg ordering and the meaning of the nbins variable is "histogram-like",
0284   /// as opposed to the Numpy/Matlab version, and the xmin and xmax arguments are expressed
0285   /// in "normal" space, rather than as the logarithms of the xmin/xmax values as in Numpy/Matlab.
0286   inline std::vector<double> logspace(size_t nbins, double xmin, double xmax, bool include_end=true) {
0287     if (xmax < xmin)  throw RangeError("xmax should not be smaller than xmin!");
0288     if (xmin < 0)     throw RangeError("xmin should not be negative!");
0289     if (nbins == 0)   throw RangeError("Requested number of bins is 0!");
0290     const double logxmin = std::log(xmin);
0291     const double logxmax = std::log(xmax);
0292     const std::vector<double> logvals = linspace(nbins, logxmin, logxmax);
0293     assert(logvals.size() == nbins+1);
0294     std::vector<double> rtn; rtn.reserve(logvals.size());
0295     rtn.push_back(xmin);
0296     for (size_t i = 1; i < logvals.size()-1; ++i) {
0297       rtn.push_back(std::exp(logvals[i]));
0298     }
0299     assert(rtn.size() == nbins);
0300     if (include_end) rtn.push_back(xmax);
0301     return rtn;
0302   }
0303 
0304 
0305   /// @todo fspace() for uniform sampling from f(x); requires ability to invert fn... how, in general?
0306   //inline std::vector<double> fspace(size_t nbins, double xmin, double xmax, std::function<double(double)>& fn) {
0307 
0308 
0309   /// @brief Make a list of @a nbins + 1 values spaced with *density* ~ f(x) between @a xmin and @a end inclusive.
0310   ///
0311   /// The density function @a fn will be evaluated at @a nsample uniformly
0312   /// distributed points between @a xmin and @a xmax, its integral approximated
0313   /// via the Trapezium Rule and used to normalize the distribution, and @a
0314   /// nbins + 1 edges then selected to (approximately) divide into bins each
0315   /// containing fraction 1/@a nbins of the integral.
0316   ///
0317   /// @note The function @a fn does not need to be a normalized pdf, but it should be non-negative.
0318   /// Any negative values returned by @a fn(x) will be truncated to zero and contribute nothing to
0319   /// the estimated integral and binning density.
0320   ///
0321   /// @note The arg ordering and the meaning of the nbins variable is "histogram-like",
0322   /// as opposed to the Numpy/Matlab version, and the xmin and xmax arguments are expressed
0323   /// in "normal" space, rather than in the function-mapped space as with Numpy/Matlab.
0324   ///
0325   /// @note The naming of this function differs from the other, Matlab-inspired ones: the bin spacing is
0326   /// uniform in the CDF of the density function given, rather than in the function itself. For
0327   /// most use-cases this is more intuitive.
0328   inline std::vector<double> pdfspace(size_t nbins, double xmin, double xmax, std::function<double(double)>& fn, size_t nsample=10000) {
0329     const double dx = (xmax-xmin)/(double)nsample;
0330     const std::vector<double> xs = linspace(nsample, xmin, xmax);
0331     std::vector<double> ys(0, nsample);
0332     auto posfn = [&](double x){return std::max(fn(x), 0.0);};
0333     std::transform(xs.begin(), xs.end(), ys.begin(), posfn);
0334     std::vector<double> areas; areas.reserve(nsample);
0335     double areasum = 0;
0336     for (size_t i = 0; i < ys.size()-1; ++i) {
0337       const double area = (ys[i] + ys[i+1])*dx/2.0;
0338       areas[i] = area;
0339       areasum += area;
0340     }
0341     const double df = areasum/(double)nbins;
0342     std::vector<double> xedges{xmin}; xedges.reserve(nbins+1);
0343     double fsum = 0;
0344     for (size_t i = 0; i < nsample-1; ++i) {
0345       fsum += areas[i];
0346       if (fsum > df) {
0347         fsum = 0;
0348         xedges.push_back(xs[i+1]);
0349       }
0350     }
0351     xedges.push_back(xmax);
0352     assert(xedges.size() == nbins+1);
0353     return xedges;
0354   }
0355 
0356 
0357   /// @brief Return the bin index of the given value, @a val, given a vector of bin edges
0358   ///
0359   /// NB. The @a binedges vector must be sorted
0360   template <typename NUM>
0361   inline int index_between(const NUM& val, const std::vector<NUM>& binedges) {
0362     if (!inRange(val, binedges.front(), binedges.back())) return -1; //< Out of histo range
0363     int index = -1;
0364     for (size_t i = 1; i < binedges.size(); ++i) {
0365       if (val < binedges[i]) {
0366         index = i-1;
0367         break;
0368       }
0369     }
0370     assert(inRange(index, -1, binedges.size()-1));
0371     return index;
0372   }
0373 
0374   //@}
0375 
0376 
0377   /// @name Statistics functions
0378   //@{
0379 
0380   /// @brief Calculate the effective number of entries of a sample
0381   inline double effNumEntries(const double sumW, const double sumW2) {
0382     if (isZero(sumW2))  return 0;
0383     return sqr(sumW) / sumW2;
0384   }
0385 
0386   /// @brief Calculate the effective number of entries of a sample
0387   inline double effNumEntries(const std::vector<double>& weights) {
0388     double sumW = 0.0, sumW2 = 0.0;
0389     for (size_t i = 0; i < weights.size(); ++i) {
0390       sumW += weights[i];
0391       sumW2 += sqr(weights[i]);
0392     }
0393     return effNumEntries(sumW, sumW2);
0394   }
0395 
0396   /// @brief Calculate the mean of a sample
0397   inline double mean(const std::vector<int>& sample) {
0398     double mean = 0.0;
0399     for (size_t i=0; i<sample.size(); ++i) {
0400       mean += sample[i];
0401     }
0402     return mean/sample.size();
0403   }
0404 
0405   /// @brief Calculate the weighted mean of a sample
0406   inline double mean(const double sumWX, const double sumW) {
0407     return sumW? sumWX / sumW : std::numeric_limits<double>::quiet_NaN();
0408   }
0409 
0410   /// @brief Calculate the weighted mean of a sample
0411   inline double mean(const std::vector<double>& sample,
0412                      const std::vector<double>& weights) {
0413     if (sample.size() != weights.size())  throw RangeError("Inputs should have equal length!");
0414     double sumWX = 0., sumW = 0.;
0415     for (size_t i = 0; i < sample.size(); ++i) {
0416       sumW  += weights[i];
0417       sumWX += weights[i]*sample[i];
0418     }
0419     return mean(sumWX, sumW);
0420   }
0421 
0422   /// @brief Calculate the weighted variance of a sample
0423   ///
0424   /// Weighted variance defined as
0425   /// sig2 = ( sum(wx**2) * sum(w) - sum(wx)**2 ) / ( sum(w)**2 - sum(w**2) )
0426   /// see http://en.wikipedia.org/wiki/Weighted_mean
0427   inline double variance(const double sumWX, const double sumW,
0428                          const double sumWX2, const double sumW2) {
0429     const double num = subtract(sumWX2*sumW, sqr(sumWX));
0430     const double den = subtract(sqr(sumW), sumW2);
0431     /// @todo Isn't this sensitive to the overall scale of the weights?
0432     /// Shouldn't it check if den is bigger then num by a set number of
0433     /// orders of magnitude and vice versa?
0434     // if (fabs(num) < 1e-10 && fabs(den) < 1e-10) {
0435     //   return std::numeric_limits<double>::quiet_NaN();
0436     // }
0437     /// We take the modulus of the weighted variance
0438     /// since the ratio can be negative with weighted means
0439     /// @todo Is this the correct approach? There is no information
0440     /// online other than "weights are non-negative"...
0441     return den? fabs(num/den): std::numeric_limits<double>::quiet_NaN();
0442   }
0443 
0444   /// @brief Calculate the weighted variance of a sample
0445   inline double variance(const std::vector<double>& sample,
0446                          const std::vector<double>& weights) {
0447     if (sample.size() != weights.size())  throw RangeError("Inputs should have equal length!");
0448     if (fuzzyLessEquals(effNumEntries(weights), 1.0)) {
0449        //throw LowStatsError("Requested variance of a distribution with only one effective entry");
0450        return std::numeric_limits<double>::quiet_NaN();
0451     }
0452     double sumWX = 0., sumW = 0.;
0453     double sumWX2 = 0., sumW2 = 0.;
0454     for (size_t i = 0; i < sample.size(); ++i) {
0455       sumW   += weights[i];
0456       sumWX  += weights[i]*sample[i];
0457       sumW2  += sqr(weights[i]);
0458       sumWX2 += weights[i]*sqr(sample[i]);
0459     }
0460     return variance(sumWX, sumW, sumWX2, sumW2);
0461   }
0462 
0463   /// @brief Calculate the weighted standard deviation of a sample
0464   inline double stdDev(const double sumWX, const double sumW,
0465                        const double sumWX2, const double sumW2) {
0466     return std::sqrt(variance(sumWX, sumW, sumWX2, sumW2));
0467   }
0468 
0469   /// @brief Calculate the weighted variance of a sample
0470   inline double stdDev(const std::vector<double>& sample,
0471                        const std::vector<double>& weights) {
0472     return std::sqrt(variance(sample, weights));
0473   }
0474 
0475   /// @brief Calculate the weighted standard error of a sample
0476   inline double stdErr(const double sumWX, const double sumW,
0477                        const double sumWX2, const double sumW2) {
0478     const double effN = effNumEntries(sumW, sumW2);
0479     if (effN == 0)  return std::numeric_limits<double>::quiet_NaN();
0480     const double var = variance(sumWX, sumW, sumWX2, sumW2);
0481     return std::sqrt(var / effN);
0482   }
0483 
0484   /// @brief Calculate the weighted variance of a sample
0485   inline double stdErr(const std::vector<double>& sample,
0486                         const std::vector<double>& weights) {
0487     if (sample.size() != weights.size())  throw RangeError("Inputs should have equal length!");
0488     const double effN = effNumEntries(weights);
0489     if (effN == 0)  return std::numeric_limits<double>::quiet_NaN();
0490     const double var = variance(sample, weights);
0491     return std::sqrt(var / effN);
0492   }
0493 
0494   /// @brief Calculate the weighted RMS of a sample
0495   inline double RMS(const double sumWX2, const double sumW, const double sumW2) {
0496     // Weighted RMS defined as
0497     // rms = sqrt(sum{w x^2} / sum{w})
0498     const double effN = effNumEntries(sumW, sumW2);
0499     if (effN == 0)  return std::numeric_limits<double>::quiet_NaN();
0500     const double meanSq = sumWX2 / sumW;
0501     return std::sqrt(meanSq);
0502   }
0503 
0504   /// @brief Calculate the weighted RMS of a sample
0505   inline double RMS(const std::vector<double>& sample,
0506                     const std::vector<double>& weights) {
0507     if (sample.size() != weights.size())  throw RangeError("Inputs should have equal length!");
0508     double sumWX2 = 0., sumW = 0., sumW2 = 0.;
0509     for (size_t i = 0; i < sample.size(); ++i) {
0510       sumW   += weights[i];
0511       sumW2  += sqr(weights[i]);
0512       sumWX2 += weights[i]*sqr(sample[i]);
0513     }
0514     return RMS(sumWX2, sumW, sumW2);
0515   }
0516 
0517   /// @brief Calculate the covariance (variance) between two samples
0518   inline double covariance(const std::vector<int>& sample1, const std::vector<int>& sample2) {
0519     const double mean1 = mean(sample1);
0520     const double mean2 = mean(sample2);
0521     const size_t N = sample1.size();
0522     double cov = 0.0;
0523     for (size_t i = 0; i < N; i++) {
0524       const double cov_i = (sample1[i] - mean1)*(sample2[i] - mean2);
0525       cov += cov_i;
0526     }
0527     if (N > 1) return cov/(N-1);
0528     else return 0.0;
0529   }
0530 
0531 
0532   /// @brief Calculate the correlation strength between two samples
0533   inline double correlation(const std::vector<int>& sample1, const std::vector<int>& sample2) {
0534     const double cov = covariance(sample1, sample2);
0535     const double var1 = covariance(sample1, sample1);
0536     const double var2 = covariance(sample2, sample2);
0537     const double correlation = cov/sqrt(var1*var2);
0538     const double corr_strength = correlation*sqrt(var2/var1);
0539     return corr_strength;
0540   }
0541 
0542 
0543   /// @brief Calculate the error-weighted chi2 statistic between two samples
0544   ///
0545   /// @note This calculation is rather naive as it neglects the error breakdowns that
0546   /// may be available for the curves being compared. More sophisticated comparisons
0547   /// will require a separate analysis based on the Estimate objects utilising the full
0548   /// covariance information.
0549   inline double naiveChi2(const std::vector<double>& sample1, const std::vector<double>& sample2,
0550                      const std::vector<double>& s1errors = std::vector<double>{},
0551                      const std::vector<double>& s2errors = std::vector<double>{}) {
0552     if (sample1.size() != sample2.size()) {
0553       throw RangeError("Inputs should have equal length!");
0554     }
0555     if (s1errors.size() && sample1.size() != s1errors.size()) {
0556       throw RangeError("Inputs should have equal length!");
0557     }
0558     if (s2errors.size() && sample2.size() != s2errors.size()) {
0559       throw RangeError("Inputs should have equal length!");
0560     }
0561     const size_t N = sample1.size();
0562     double chi2 = 0.0;
0563     for (size_t i = 0; i < N; ++i) {
0564       double temp = sqr(sample1[i] - sample2[i]);
0565       if (s1errors.size()) {
0566         temp /= sqr(s1errors[i]) + sqr(s2errors[i]);
0567       }
0568       chi2 += temp;
0569     }
0570     return chi2;
0571   }
0572 
0573   /// @brief Calculate the error-weighted reduced chi2 statistic between two samples
0574   ///
0575   /// @note This calculation is rather naive as it neglects the error breakdowns that
0576   /// may be available for the curves being compared. More sophisticated comparisons
0577   /// will require a separate analysis based on the Estimate objects utilising the full
0578   /// covariance information.
0579   inline double naiveChi2reduced(const std::vector<double>& sample1, const std::vector<double>& sample2,
0580                         const std::vector<double>& s1errors = std::vector<double>{},
0581                         const std::vector<double>& s2errors = std::vector<double>{}) {
0582     if (sample1.empty()) throw RangeError("Inputs should not have 0 length!");
0583     return naiveChi2(sample1, sample2, s1errors, s2errors)/sample1.size();
0584   }
0585 
0586   //@}
0587 
0588 
0589 }
0590 
0591 #endif