lib/maths/common/CTools.cc (1,417 lines of code) (raw):

/* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License * 2.0 and the following additional limitation. Functionality enabled by the * files subject to the Elastic License 2.0 may only be used in production when * invoked by an Elasticsearch process with a license key installed that permits * use of machine learning features. You may not use this file except in * compliance with the Elastic License 2.0 and the foregoing additional * limitation. */ #include <maths/common/CTools.h> #include <core/CLogger.h> #include <core/CStringUtils.h> #include <core/Constants.h> #include <maths/common/CChecksum.h> #include <maths/common/CEqualWithTolerance.h> #include <maths/common/CIntegration.h> #include <maths/common/CLogTDistribution.h> #include <maths/common/CMathsFuncs.h> #include <maths/common/CNormalMeanPrecConjugate.h> #include <maths/common/CPrior.h> #include <maths/common/CSolvers.h> #include <maths/common/CToolsDetail.h> #include <maths/common/Constants.h> #include <boost/math/distributions/beta.hpp> #include <boost/math/distributions/binomial.hpp> #include <boost/math/distributions/chi_squared.hpp> #include <boost/math/distributions/gamma.hpp> #include <boost/math/distributions/lognormal.hpp> #include <boost/math/distributions/negative_binomial.hpp> #include <boost/math/distributions/normal.hpp> #include <boost/math/distributions/poisson.hpp> #include <boost/math/distributions/students_t.hpp> #include <boost/math/special_functions/digamma.hpp> #include <boost/math/tools/precision.hpp> #include <algorithm> #include <cmath> #include <optional> #include <ostream> namespace boost { namespace math { namespace policies { template<class T> T user_overflow_error(const char* /*function*/, const char* /*message*/, const T& /*val*/) { return std::numeric_limits<T>::max(); } } } } namespace ml { namespace maths { namespace common { namespace { using TDoubleBoolPr = std::pair<double, bool>; using TDoubleDoublePr = std::pair<double, double>; using TOptionalDoubleDoublePr = std::optional<TDoubleDoublePr>; namespace adapters { template<typename DISTRIBUTION> inline double pdf(const DISTRIBUTION& distribution, double x) { return CTools::safePdf(distribution, x); } inline double pdf(const CLogTDistribution& distribution, double x) { return maths::common::pdf(distribution, x); } } // adapters:: inline TDoubleBoolPr stationaryPoint(const boost::math::beta_distribution<>& beta) { if (beta.alpha() < 1.0 && beta.beta() < 1.0) { // This is the unique minimum of the p.d.f. return {(beta.alpha() - 1.0) / (beta.alpha() + beta.beta() - 2.0), false}; } return {boost::math::mode(beta), true}; } //! \brief p.d.f function adapter. //! //! DESCRIPTION:\n //! Wrapper around a distribution object which evaluates the safe version //! of the p.d.f. function. This is used to adapt the function for use //! with the boost::math solvers. template<typename DISTRIBUTION> class CPdf { public: CPdf(const DISTRIBUTION& distribution, double target) : m_Distribution(distribution), m_Target(target) {} double operator()(double x) const { return adapters::pdf(m_Distribution, x) - m_Target; } private: DISTRIBUTION m_Distribution; double m_Target; }; //! Convenience factory method for the CPdf object for \p distribution. template<typename DISTRIBUTION> inline CPdf<DISTRIBUTION> makePdf(const DISTRIBUTION& distribution, double target) { return CPdf<DISTRIBUTION>(distribution, target); } template<typename Distribution> inline double continuousSafePdf(const Distribution& distribution, double x) { TDoubleDoublePr support = boost::math::support(distribution); if (x <= support.first || x >= support.second) { return 0.0; } else if (CMathsFuncs::isNan(x)) { LOG_ERROR(<< "x = NaN, distribution = " << typeid(Distribution).name()); return 0.0; } return boost::math::pdf(distribution, x); } template<typename Distribution> inline double discreteSafePdf(const Distribution& distribution, double x) { // Note that the inequalities are strict this is needed because // the distribution is discrete and can have mass at the support // end points. TDoubleDoublePr support = boost::math::support(distribution); if (x < support.first || x > support.second) { return 0.0; } else if (CMathsFuncs::isNan(x)) { LOG_ERROR(<< "x = NaN, distribution = " << typeid(Distribution).name()); return 0.0; } return boost::math::pdf(distribution, x); } template<typename Distribution> inline double continuousSafeCdf(const Distribution& distribution, double x) { TDoubleDoublePr support = boost::math::support(distribution); if (x <= support.first) { return 0.0; } else if (x >= support.second) { return 1.0; } else if (CMathsFuncs::isNan(x)) { LOG_ERROR(<< "x = NaN, distribution = " << typeid(Distribution).name()); return 0.0; } return boost::math::cdf(distribution, x); } template<typename Distribution> inline double discreteSafeCdf(const Distribution& distribution, double x) { // Note that the inequalities are strict this is needed because // the distribution is discrete and can have mass at the support // end points. TDoubleDoublePr support = boost::math::support(distribution); if (x < support.first) { return 0.0; } else if (x > support.second) { return 1.0; } else if (CMathsFuncs::isNan(x)) { LOG_ERROR(<< "x = NaN, distribution = " << typeid(Distribution).name()); return 0.0; } return boost::math::cdf(distribution, x); } template<typename Distribution> inline double continuousSafeCdfComplement(const Distribution& distribution, double x) { TDoubleDoublePr support = boost::math::support(distribution); if (x <= support.first) { return 1.0; } else if (x >= support.second) { return 0.0; } else if (CMathsFuncs::isNan(x)) { LOG_ERROR(<< "x = NaN, distribution = " << typeid(Distribution).name()); return 0.0; } return boost::math::cdf(boost::math::complement(distribution, x)); } template<typename Distribution> inline double discreteSafeCdfComplement(const Distribution& distribution, double x) { // Note that the inequalities are strict this is needed because // the distribution is discrete and can have mass at the support // end points. TDoubleDoublePr support = boost::math::support(distribution); if (x < support.first) { return 1.0; } else if (x > support.second) { return 0.0; } else if (CMathsFuncs::isNan(x)) { LOG_ERROR(<< "x = NaN distribution = " << typeid(Distribution).name()); return 0.0; } return boost::math::cdf(boost::math::complement(distribution, x)); } const double EPSILON = std::numeric_limits<double>::epsilon(); const double MIN_DOUBLE = std::numeric_limits<double>::min(); const double NEG_INF = std::numeric_limits<double>::lowest(); const double POS_INF = std::numeric_limits<double>::max(); } // unnamed:: //////// SMinusLogCdf Implementation //////// namespace { //! Computes -log(\p cdf) enforces limits and avoids underflow. inline double safeMinusLogCdf(double cdf) { if (cdf == 0.0) { // log(0.0) == -HUGE_VALUE, which is too big for our purposes // and causes problems on Windows. In fact, we want to avoid // underflow since this will pollute the floating point // environment and *may* cause problems for some library // function implementations (see fe*exceptflags for more details). // The log of the minimum double should be small enough for // our purposes. return -core::constants::LOG_MIN_DOUBLE; } return std::max(-std::log(cdf), 0.0); } } const double CTools::IMPROPER_CDF(0.5); double CTools::SMinusLogCdf::operator()(const SImproperDistribution&, double) const { return -std::log(IMPROPER_CDF); } double CTools::SMinusLogCdf::operator()(const normal& normal_, double x) const { return safeMinusLogCdf(safeCdf(normal_, x)); } double CTools::SMinusLogCdf::operator()(const students_t& students, double x) const { return safeMinusLogCdf(safeCdf(students, x)); } double CTools::SMinusLogCdf::operator()(const negative_binomial& negativeBinomial, double x) const { return safeMinusLogCdf(safeCdf(negativeBinomial, x)); } double CTools::SMinusLogCdf::operator()(const lognormal& logNormal, double x) const { return safeMinusLogCdf(safeCdf(logNormal, x)); } double CTools::SMinusLogCdf::operator()(const CLogTDistribution& logt, double x) const { return safeMinusLogCdf(maths::common::cdf(logt, x)); } double CTools::SMinusLogCdf::operator()(const gamma& gamma_, double x) const { return safeMinusLogCdf(safeCdf(gamma_, x)); } double CTools::SMinusLogCdf::operator()(const beta& beta_, double x) const { return safeMinusLogCdf(safeCdf(beta_, x)); } //////// SMinusLogCdfComplement Implementation //////// double CTools::SMinusLogCdfComplement::operator()(const SImproperDistribution&, double) const { return -std::log(1.0 - IMPROPER_CDF); } double CTools::SMinusLogCdfComplement::operator()(const normal& normal_, double x) const { return safeMinusLogCdf(safeCdfComplement(normal_, x)); } double CTools::SMinusLogCdfComplement::operator()(const students_t& students, double x) const { return safeMinusLogCdf(safeCdfComplement(students, x)); } double CTools::SMinusLogCdfComplement:: operator()(const negative_binomial& negativeBinomial, double x) const { return safeMinusLogCdf(safeCdfComplement(negativeBinomial, x)); } double CTools::SMinusLogCdfComplement::operator()(const lognormal& logNormal, double x) const { return safeMinusLogCdf(safeCdfComplement(logNormal, x)); } double CTools::SMinusLogCdfComplement::operator()(const CLogTDistribution& logt, double x) const { return safeMinusLogCdf(maths::common::cdfComplement(logt, x)); } double CTools::SMinusLogCdfComplement::operator()(const gamma& gamma_, double x) const { return safeMinusLogCdf(safeCdfComplement(gamma_, x)); } double CTools::SMinusLogCdfComplement::operator()(const beta& beta_, double x) const { return safeMinusLogCdf(safeCdfComplement(beta_, x)); } //////// SProbabilityLessLikelySample Implementation //////// CTools::CProbabilityOfLessLikelySample::CProbabilityOfLessLikelySample(maths_t::EProbabilityCalculation calculation) : m_Calculation(calculation) { } double CTools::CProbabilityOfLessLikelySample:: operator()(const SImproperDistribution&, double, maths_t::ETail& tail) const { // For any finite sample this is one. tail = maths_t::E_MixedOrNeitherTail; return 1.0; } double CTools::CProbabilityOfLessLikelySample:: operator()(const normal& normal_, double x, maths_t::ETail& tail) const { double px = 0.0; TDoubleDoublePr support = boost::math::support(normal_); if (!this->check(support, x, px, tail)) { return px; } switch (m_Calculation) { case maths_t::E_OneSidedBelow: px = truncate(2.0 * safeCdf(normal_, x), 0.0, 1.0); tail = static_cast<maths_t::ETail>(tail | maths_t::E_LeftTail); break; case maths_t::E_TwoSided: { // The normal distribution is symmetric and single mode so the // probability of less likely events than x is: // 2 * std::min(cdf(x), 1 - cdf(x)). // // Note, we use the complement function to compute the 1 - cdf(x) // so that we aren't restricted to epsilon precision. double m = boost::math::mode(normal_); if (x < m) { px = truncate(2.0 * safeCdf(normal_, x), 0.0, 1.0); } else { px = truncate(2.0 * safeCdfComplement(normal_, x), 0.0, 1.0); } this->tail(x, m, tail); break; } case maths_t::E_OneSidedAbove: px = truncate(2.0 * safeCdfComplement(normal_, x), 0.0, 1.0); tail = static_cast<maths_t::ETail>(tail | maths_t::E_RightTail); break; } return px; } double CTools::CProbabilityOfLessLikelySample:: operator()(const students_t& students, double x, maths_t::ETail& tail) const { double px = 0.0; TDoubleDoublePr support = boost::math::support(students); if (!this->check(support, x, px, tail)) { return px; } switch (m_Calculation) { case maths_t::E_OneSidedBelow: px = truncate(2.0 * safeCdf(students, x), 0.0, 1.0); tail = static_cast<maths_t::ETail>(tail | maths_t::E_LeftTail); break; case maths_t::E_TwoSided: { // Student's t distribution is symmetric and single mode so the // probability of less likely events than x is: // 2 * std::min(cdf(x), 1 - cdf(x)). // // Note, we use the complement function to compute the 1 - cdf(x) // so that we aren't restricted to epsilon precision. double m = boost::math::mode(students); if (x < m) { px = truncate(2.0 * safeCdf(students, x), 0.0, 1.0); } else { px = truncate(2.0 * safeCdfComplement(students, x), 0.0, 1.0); } this->tail(x, m, tail); break; } case maths_t::E_OneSidedAbove: px = truncate(2.0 * safeCdfComplement(students, x), 0.0, 1.0); tail = static_cast<maths_t::ETail>(tail | maths_t::E_RightTail); break; } return px; } double CTools::CProbabilityOfLessLikelySample:: operator()(const negative_binomial& negativeBinomial, double x, maths_t::ETail& tail) const { x = std::floor(x); double px = 0.0; TDoubleDoublePr support = boost::math::support(negativeBinomial); if (!this->check(support, x, px, tail)) { return px; } switch (m_Calculation) { case maths_t::E_OneSidedBelow: tail = static_cast<maths_t::ETail>(tail | maths_t::E_LeftTail); return truncate(2.0 * safeCdf(negativeBinomial, x), 0.0, 1.0); case maths_t::E_TwoSided: // Fall through. break; case maths_t::E_OneSidedAbove: tail = static_cast<maths_t::ETail>(tail | maths_t::E_RightTail); return truncate(2.0 * (safeCdfComplement(negativeBinomial, x) + safePdf(negativeBinomial, x)), 0.0, 1.0); } double fx = safePdf(negativeBinomial, x); double r = negativeBinomial.successes(); double p = negativeBinomial.success_fraction(); double m = boost::math::mode(negativeBinomial); LOG_TRACE(<< "x = " << x << ", f(x) = " << fx); // If the number of successes <= 1 the distribution is single sided. if (r <= 1.0) { tail = maths_t::E_RightTail; return truncate(safeCdfComplement(negativeBinomial, x) + fx, 0.0, 1.0); } this->tail(x, m, tail); // If the f(x) <= f(0) and x is greater than the mode the probability // is just P(y > x). if (x > m) { double f0 = safePdf(negativeBinomial, 0.0); LOG_TRACE(<< "f(0) = " << f0); if (fx <= f0) { return truncate(safeCdfComplement(negativeBinomial, x) + fx, 0.0, 1.0); } } // Trap the case f(x) = f(m). double fm = safePdf(negativeBinomial, m); LOG_TRACE(<< "m = " << m << ", f(m) = " << fm); if (fx >= fm) { return 1.0; } const unsigned int MAX_ITERATIONS = 20; std::size_t maxIterations = MAX_ITERATIONS; double b1, b2, f1, f2; if (x > m) { b1 = b2 = m; f1 = f2 = fm; double shrinkFactor = 1.5; double step = (1.0 / shrinkFactor - 1.0) * b1; for (;;) { if (maxIterations == 0 || f1 <= fx) { break; } b2 = b1; f2 = f1; b1 += step; f1 = safePdf(negativeBinomial, b1); --maxIterations; if (maxIterations <= 3 * MAX_ITERATIONS / 4) { shrinkFactor *= 2.0; } step = (maxIterations == MAX_ITERATIONS / 2 ? b1 : (1.0 / shrinkFactor - 1.0) * b1); } } else { // Noting that the binomial coefficient (k + r - 1)! / k! / (r - 1)! // is a monotonic increasing function of k, we have for any k': // f(k') * (1 - p)^(k - k') < f(k) for k > k' // // Solving for k and using the fact that log(1 - p) is negative: // k > k' + log(f(x)/f(k')) / log(1 - p) double logOneMinusP = std::log(1 - p); b1 = std::floor(m + std::log(std::max(fx, MIN_DOUBLE) / std::max(fm, MIN_DOUBLE)) / logOneMinusP); f1 = safePdf(negativeBinomial, b1); b2 = b1; f2 = f1; LOG_TRACE(<< "b1 = " << b1 << ", f(b1) = " << f1); double growthFactor = 0.25; double step = growthFactor * b2; for (;;) { if (maxIterations == 0 || f2 <= fx) { break; } b1 = b2; f1 = f2; b2 += step; f2 = safePdf(negativeBinomial, b2); --maxIterations; // We compute successively tighter lower bounds on the // bracket point. double lowerBound = b2 + std::log(std::max(fx, MIN_DOUBLE) / std::max(f2, MIN_DOUBLE)) / logOneMinusP; LOG_TRACE(<< "b2 = " << b2 << ", f2 = " << f2 << ", bound = " << lowerBound); if (maxIterations <= 3 * MAX_ITERATIONS / 4) { growthFactor *= 4.0; } step = std::max(growthFactor * b2, lowerBound - b2); } } LOG_TRACE(<< "Initial bracket = (" << b1 << "," << b2 << ")" << ", f(bracket) = (" << f1 - fx << "," << f2 - fx << ")"); px = x < m ? safeCdf(negativeBinomial, x) : safeCdfComplement(negativeBinomial, x); double y = POS_INF; try { // Note that this form of epsilon controls the maximum // relative error in the probability since p > px and // the error will be order eps * f(x) so we require that // eps * f(x) <= 0.05 * px. double eps = 0.05 * px / std::max(fx, MIN_DOUBLE); eps = std::max(eps, EPSILON * std::min(b1, b2)); CEqualWithTolerance<double> equal(CToleranceTypes::E_AbsoluteTolerance, eps); CSolvers::solve(b1, b2, f1 - fx, f2 - fx, makePdf(negativeBinomial, fx), maxIterations, equal, y); LOG_TRACE(<< "bracket = (" << b1 << "," << b2 << ")" << ", iterations = " << maxIterations << ", f(y) = " << safePdf(negativeBinomial, y) - fx << ", eps = " << eps); } catch (const std::exception& e) { if (std::fabs(f1 - fx) < 10.0 * EPSILON * fx) { y = b1; } else if (std::fabs(f2 - fx) < 10.0 * EPSILON * fx) { y = b2; } else { LOG_ERROR(<< "Failed in root finding: " << e.what() << ", x = " << x << ", bracket = (" << b1 << "," << b2 << ")" << ", f(bracket) = (" << f1 - fx << "," << f2 - fx << ")"); return truncate(px, 0.0, 1.0); } } if ((x < m && y < m) || (x > m && y > m) || !(x >= support.first && x <= support.second)) { LOG_ERROR(<< "Bad root " << y << " (x = " << x << ")"); } double py = x < m ? safeCdfComplement(negativeBinomial, y) : safeCdf(negativeBinomial, y); return truncate(px + py + fx, 0.0, 1.0); } double CTools::CProbabilityOfLessLikelySample:: operator()(const lognormal& logNormal, double x, maths_t::ETail& tail) const { double px = 0.0; TDoubleDoublePr support = boost::math::support(logNormal); if (!this->check(support, x, px, tail)) { return px; } switch (m_Calculation) { case maths_t::E_OneSidedBelow: px = truncate(2.0 * safeCdf(logNormal, x), 0.0, 1.0); tail = static_cast<maths_t::ETail>(tail | maths_t::E_LeftTail); break; case maths_t::E_TwoSided: { // Changing variables to x = exp(m) * exp(x') where m is the location // of the log normal distribution it is possible to show that the // equal point on the p.d.f. is at: // exp(m) * exp(-s^2 + (s^4 + (log(x) - m)^2 // + 2 * s^2 * (log(x) - m))^(1/2)) if x < mode // // and // exp(m) * exp(-s^2 - (s^4 + (log(x) - m)^2 // + 2 * s^2 * (log(x) - m))^(1/2)) if x > mode double logx = std::log(x); double squareScale = pow2(logNormal.scale()); double discriminant = std::sqrt( pow2(squareScale) + (logx - logNormal.location() + 2.0 * squareScale) * (logx - logNormal.location())); double m = boost::math::mode(logNormal); this->tail(x, m, tail); double y = m * std::exp(x > m ? -discriminant : discriminant); if (x > y) { std::swap(x, y); } px = truncate(safeCdf(logNormal, x) + safeCdfComplement(logNormal, y), 0.0, 1.0); break; } case maths_t::E_OneSidedAbove: px = truncate(2.0 * safeCdfComplement(logNormal, x), 0.0, 1.0); tail = static_cast<maths_t::ETail>(tail | maths_t::E_RightTail); break; } return px; } double CTools::CProbabilityOfLessLikelySample:: operator()(const CLogTDistribution& logt, double x, maths_t::ETail& tail) const { double px = 0.0; TDoubleDoublePr support = maths::common::support(logt); if (!this->check(support, x, px, tail)) { return px; } switch (m_Calculation) { case maths_t::E_OneSidedBelow: tail = static_cast<maths_t::ETail>(tail | maths_t::E_LeftTail); return truncate(2.0 * cdf(logt, x), 0.0, 1.0); case maths_t::E_TwoSided: // Fall through. break; case maths_t::E_OneSidedAbove: tail = static_cast<maths_t::ETail>(tail | maths_t::E_RightTail); return truncate(2.0 * cdfComplement(logt, x), 0.0, 1.0); } const unsigned int MAX_ITERATIONS = 20; double fx = pdf(logt, x); double m = mode(logt); LOG_TRACE(<< "x = " << x << ", f(x) = " << fx); this->tail(x, m, tail); // Handle the case that x than the mode of the distribution. // Note that the p.d.f. can have a local minimum between zero // and the mode of the distribution. CLogTDistribution::TOptionalDouble localMinimum = maths::common::localMinimum(logt); if (!localMinimum) { // If there is no local minimum the distribution is single sided. return truncate(cdfComplement(logt, x), 0.0, 1.0); } else { double b1 = *localMinimum; double f1 = pdf(logt, b1); LOG_TRACE(<< "b1 = " << b1 << ", f(b1) = " << f1); if (f1 > fx) { return truncate(cdfComplement(logt, x), 0.0, 1.0); } else if (x > m) { px = cdfComplement(logt, x); double b2 = m; double f2 = pdf(logt, m); LOG_TRACE(<< "Initial bracket = (" << b1 << "," << b2 << ")" << ", f(bracket) = (" << f1 - fx << "," << f2 - fx << ")"); double y = 0.0; try { // The gradient of the log normal p.d.f. can be very // large near the origin so we use the maximum of f1 and // f2 to be safe here rather that the value of f at the // root, i.e. f(x). Note that this form of epsilon controls // the maximum relative error in the probability since // p > px and the error will be order eps * f(x) so we // require that eps * f(x) <= 0.05 * px. double eps = 0.05 * px / std::max(std::max(f1, f2), MIN_DOUBLE); eps = std::max(eps, EPSILON * std::min(b1, b2)); CEqualWithTolerance<double> equal(CToleranceTypes::E_AbsoluteTolerance, eps); std::size_t maxIterations = MAX_ITERATIONS; CSolvers::solve(b1, b2, f1 - fx, f2 - fx, makePdf(logt, fx), maxIterations, equal, y); LOG_TRACE(<< "bracket = (" << b1 << "," << b2 << ")" << ", iterations = " << maxIterations << ", f(y) = " << pdf(logt, y) - fx << ", eps = " << eps); } catch (const std::exception& e) { if (std::fabs(f1 - fx) < 10.0 * EPSILON * fx) { y = b1; } else if (std::fabs(f2 - fx) < 10.0 * EPSILON * fx) { y = b2; } else { LOG_ERROR(<< "Failed in root finding: " << e.what() << ", x = " << x << ", bracket = (" << b1 << "," << b2 << ")" << ", f(bracket) = (" << f1 - fx << "," << f2 - fx << ")"); return truncate(px, 0.0, 1.0); } } return truncate(cdf(logt, y) + px, 0.0, 1.0); } } // Handle the case that x is less than the distribution mode. // For small x the density can be greater than the local mode. double fm = pdf(logt, m); LOG_TRACE(<< "f(m) = " << fm); if (fx > fm) { return truncate(cdfComplement(logt, x), 0.0, 1.0); } // We use the fact that 1/3 log(x) < x^(1/8) for x > 0 to derive // the following lower bound for the density function if x > exp(l): // // f(x|v,l,s) > f(exp(l)) * exp(l) * (s' / (3 * (exp(s'-l) * x)^(1/8)))^(v+1) // // where, // v is the number of degrees freedom of the log-t distribution, // l is the location of the log-t distribution, // s is the scale of the log-t distribution // s' = v^(1/2) * s. // // We can use this to solve for a lower bound for the value of x // denoted x* which satisfies: // f(x*|v,l,s) = f // // The bound is relatively tight (for smallish v) since: // g(x) = (1 - log(x) / 3) / x^(1/8) // // is a monotonic increasing function of x for large x and equals // 0.18 at x = 1000000. double v = logt.degreesFreedom(); double l = logt.location(); double s = logt.scale(); // Initialize the bracket. double fl = pdf(logt, std::exp(l)); double scale = std::sqrt(v) * s; double bound = 0.0; double fBound = POS_INF; if (fl < fx) { bound = std::exp(l); fBound = fl; } else { double t1 = l + std::log(fl / fx); double t2 = (l - scale) / 8.0 + std::log(scale / 3.0); double k0 = 8.0 * (t1 + (v + 1.0) * t2) / (v + 9.0); bound = std::max(std::exp(l), std::exp(k0)); fBound = pdf(logt, bound); } double b1 = fBound < fx ? m : bound; double f1 = fBound < fx ? fm : fBound; double b2 = bound; double f2 = fBound; LOG_TRACE(<< "b1 = " << b1 << ", f(b1) = " << f1 << ", b2 = " << b2 << ", f(b2) = " << f2); std::size_t maxIterations = MAX_ITERATIONS; // Doubling search for the upper bracket with the possibility // of early exit based on an upper bound for the bracket. Note // that we accelerate the growth if we don't bracket the root // quickly and fallback to the bound if we haven't bracketed double step = std::max(b2, std::exp(l) - b2); double growthFactor = 1.0; for (;;) { if (maxIterations == 0 || f2 <= fx) { break; } b1 = b2; f1 = f2; b2 += step; f2 = pdf(logt, b2); --maxIterations; // We compute successively tighter upper bounds on the // bracket point. double upperBound = b2 * f2 / fx; LOG_TRACE(<< "Bound = " << upperBound); if (maxIterations <= 3 * MAX_ITERATIONS / 4) { growthFactor *= 3.0; } if (maxIterations <= MAX_ITERATIONS / 2 || upperBound - b2 < 2.0 * growthFactor * b2) { step = upperBound - b2; } else { step = growthFactor * b2; } } LOG_TRACE(<< "Initial bracket = (" << b1 << "," << b2 << ")" << ", f(bracket) = (" << f1 - fx << "," << f2 - fx << ")"); px = cdf(logt, x); double y = POS_INF; try { // Note that this form of epsilon controls the maximum // relative error in the probability since p > px and // the error will be order eps * f(x) so we require that // eps * f(x) <= 0.05 * px. double eps = 0.05 * px / std::max(fx, MIN_DOUBLE); eps = std::max(eps, EPSILON * std::min(b1, b2)); CEqualWithTolerance<double> equal(CToleranceTypes::E_AbsoluteTolerance, eps); CSolvers::solve(b1, b2, f1 - fx, f2 - fx, makePdf(logt, fx), maxIterations, equal, y); LOG_TRACE(<< "bracket = (" << b1 << "," << b2 << ")" << ", iterations = " << maxIterations << ", f(y) = " << pdf(logt, y) - fx << ", eps = " << eps); } catch (const std::exception& e) { if (std::fabs(f1 - fx) < 10.0 * EPSILON * fx) { y = b1; } else if (std::fabs(f2 - fx) < 10.0 * EPSILON * fx) { y = b2; } else { LOG_ERROR(<< "Failed in root finding: " << e.what() << ", x = " << x << ", bracket = (" << b1 << "," << b2 << ")" << ", f(bracket) = (" << f1 - fx << "," << f2 - fx << ")"); return truncate(px, 0.0, 1.0); } } return truncate(px + cdfComplement(logt, y), 0.0, 1.0); } double CTools::CProbabilityOfLessLikelySample:: operator()(const gamma& gamma_, double x, maths_t::ETail& tail) const { double px = 0.0; TDoubleDoublePr support = boost::math::support(gamma_); if (!this->check(support, x, px, tail)) { return px; } switch (m_Calculation) { case maths_t::E_OneSidedBelow: tail = static_cast<maths_t::ETail>(tail | maths_t::E_LeftTail); return truncate(2.0 * safeCdf(gamma_, x), 0.0, 1.0); case maths_t::E_TwoSided: // Fall through. break; case maths_t::E_OneSidedAbove: tail = static_cast<maths_t::ETail>(tail | maths_t::E_RightTail); return truncate(2.0 * safeCdfComplement(gamma_, x), 0.0, 1.0); } // For alpha <= 1 the distribution is single sided. if (gamma_.shape() <= 1.0) { tail = static_cast<maths_t::ETail>(tail | maths_t::E_RightTail); return truncate(safeCdfComplement(gamma_, x), 0.0, 1.0); } const double CONVERGENCE_TOLERANCE = 1e-4; const double PDF_TOLERANCE = 5e-2; const unsigned int MAX_ITERATIONS = 20; double m = boost::math::mode(gamma_); LOG_TRACE(<< "x = " << x << ", mode = " << m); this->tail(x, m, tail); double y[] = {2.0 * m - x, 0.0}; unsigned int i = 0; if (x == m) { return 1.0; } else if (x < m) { // For x < m we use the recurrence relation: // y(n+1) = x + m * log(y(n) / x) // // Note it is relatively straightforward to show that: // g(y) = x + m * log(y(n) / x) // // is a contractive mapping for y > m and x < m by showing // that the derivative is less than 1. This guarantees that // the following iteration (eventually) converges to the // unique fixed point (Banach fixed point theorem). Note that // this has poor convergence for x near the stationary point; // however, reflecting x in the stationary point gives a starting // point very near root in this case, i.e. it is equivalent to // initializing with a second order Taylor expansion about the // mode. for (;;) { y[(i + 1) % 2] = x + m * std::log(y[i % 2] / x); LOG_TRACE(<< "y = " << y[(i + 1) % 2]); if (++i == MAX_ITERATIONS || std::fabs(y[1] - y[0]) < CONVERGENCE_TOLERANCE * std::max(y[0], y[1])) { break; } } } else { // For x > m we use the recurrence relation: // y(n+1) = m - x * exp(-(x - y(n)) / m) // // Note it is relatively straightforward to show that: // g(y) = x * exp(-(x - y) / m) // // is a contractive mapping for y > x and x < (a-1)/(a+b-1) by // showing that the derivative is less than 1. This guarantees // that the following iteration (eventually) converges to the // unique fixed point (Banach fixed point theorem). Note that // this has poor convergence for x near the stationary point; // however, reflecting x in the stationary point gives a starting // point very near root in this case, i.e. it is equivalent to // initializing with a second order Taylor expansion about the // mode. y[0] = std::max(y[0], m / 2.0); for (;;) { y[(i + 1) % 2] = x * std::exp(-(x - y[i % 2]) / m); LOG_TRACE(<< "y = " << y[(i + 1) % 2]); if (++i == MAX_ITERATIONS || std::fabs(y[1] - y[0]) < CONVERGENCE_TOLERANCE * std::max(y[0], y[1])) { break; } } } double fx = safePdf(gamma_, x); double fy = safePdf(gamma_, y[i % 2]); LOG_TRACE(<< "f(x) = " << fx << ", f(y) = " << fy); if (std::fabs(fx - fy) <= PDF_TOLERANCE * std::max(fx, fy)) { if (x > y[i % 2]) { std::swap(x, y[i % 2]); } return truncate(safeCdf(gamma_, x) + safeCdfComplement(gamma_, y[i % 2]), 0.0, 1.0); } // The gamma density function can be rewritten as: // f(y) = f(u) / u^(a - 1) / exp(-b * u) * exp(-b * (y - m * log(y))) // // for any u. Noting that log(u) is a concave function and expanding // about u: // log(y) <= log(x) + (y - u) / u // // This implies that: // f(y) < f(u) / u^(a - 1) / exp(-b * u) * exp(-b * (y - m * (log(u) - (y - u) / u))) // // From this it follows that if the initial f(y) > f(x) then a bracket // f(y') <= f(x) can be found using: // y' = (1 + log(f(y) / f(x)) / b / (y - m)) * y double a = y[i % 2]; double b = a; double fa = fy; double fb = fa; if (x > m && fy < fx) { b = m; fb = safePdf(gamma_, m); } else if (x > m && fy > fx) { b = (1.0 + gamma_.scale() / (a - m) * std::log(fa / fx)) * a; fb = safePdf(gamma_, b); std::swap(a, b); std::swap(fa, fb); } else if (fy < fx) { b = m; fb = safePdf(gamma_, m); std::swap(a, b); std::swap(fa, fb); } else { b = (1.0 + gamma_.scale() / (a - m) * std::log(fa / fx)) * a; fb = safePdf(gamma_, b); } LOG_TRACE(<< "Initial bracket = (" << a << ", " << b << ")" << ", f(bracket) = (" << fa - fx << "," << fb - fx << ")"); px = x > m ? safeCdfComplement(gamma_, x) : safeCdf(gamma_, x); try { // The gradient of the gamma p.d.f. can be very large // near the origin so we use the maximum of fa and // fb to be safe here rather that the value of f at the // root, i.e. f(x). Note that this form of epsilon controls // the maximum relative error in the probability since // p > px and the error will be order eps * f(x) so we // require that eps * f(x) <= 0.05 * px. double eps = 0.05 * px / std::max(std::max(fa, fb), MIN_DOUBLE); eps = std::max(eps, EPSILON * std::min(a, b)); CEqualWithTolerance<double> equal(CToleranceTypes::E_AbsoluteTolerance, eps); std::size_t maxIterations = MAX_ITERATIONS / 2; double candidate; CSolvers::solve(a, b, fa - fx, fb - fx, makePdf(gamma_, fx), maxIterations, equal, candidate); LOG_TRACE(<< "bracket = (" << a << "," << b << ")" << ", iterations = " << maxIterations << ", f(candidate) = " << safePdf(gamma_, candidate) - fx); if (std::fabs(safePdf(gamma_, candidate) - fx) < std::fabs(fy - fx)) { y[i % 2] = candidate; } } catch (const std::exception& e) { if (std::fabs(fa - fx) < 10.0 * EPSILON * fx) { y[i % 2] = a; } else if (std::fabs(fb - fx) < 10.0 * EPSILON * fx) { y[i % 2] = b; } else { LOG_ERROR(<< "Failed in bracketed solver: " << e.what() << ", x = " << x << ", bracket = (" << a << ", " << b << ")" << ", f(bracket) = (" << fa - fx << "," << fb - fx << ")"); return truncate(px, 0.0, 1.0); } } LOG_TRACE(<< "f(x) = " << fx << ", f(y) = " << safePdf(gamma_, y[i % 2])); double py = x > y[i % 2] ? safeCdf(gamma_, y[i % 2]) : safeCdfComplement(gamma_, y[i % 2]); return truncate(px + py, 0.0, 1.0); } double CTools::CProbabilityOfLessLikelySample:: operator()(const beta& beta_, double x, maths_t::ETail& tail) const { double px = 0.0; TDoubleDoublePr support(0.0, 1.0); if (!this->check(support, x, px, tail)) { return px; } switch (m_Calculation) { case maths_t::E_OneSidedBelow: tail = static_cast<maths_t::ETail>(tail | maths_t::E_LeftTail); return truncate(2.0 * safeCdf(beta_, x), 0.0, 1.0); case maths_t::E_TwoSided: // Fall through. break; case maths_t::E_OneSidedAbove: tail = static_cast<maths_t::ETail>(tail | maths_t::E_RightTail); return truncate(2.0 * safeCdfComplement(beta_, x), 0.0, 1.0); } if (beta_.alpha() < 1.0 && beta_.beta() < 1.0) { // The probability density function tends to infinity at x = 0 // and x = 1 and has a unique minimum in the interval (0,1). // // We can't evaluate the function at 0 and 1 so can't use these // as a bracket values. The p.d.f. is monotonic increasing towards // 0 and 1 so we choose very nearby values. tail = maths_t::E_MixedOrNeitherTail; double eps = boost::math::tools::epsilon<double>(); if (x <= eps || x >= 1.0 - eps) { return 1.0; } support = std::make_pair(eps, 1.0 - eps); } else if (beta_.alpha() == 1.0 && beta_.beta() == 1.0) { // The distribution is flat. tail = maths_t::E_MixedOrNeitherTail; return 1.0; } else if (beta_.alpha() <= 1.0 && beta_.beta() >= 1.0) { // The distribution is monotone decreasing. tail = static_cast<maths_t::ETail>(tail | maths_t::E_RightTail); return truncate(safeCdfComplement(beta_, x), 0.0, 1.0); } else if (beta_.alpha() >= 1.0 && beta_.beta() <= 1.0) { // The distribution is monotone increasing. tail = static_cast<maths_t::ETail>(tail | maths_t::E_LeftTail); return truncate(safeCdf(beta_, x), 0.0, 1.0); } else { // If alpha > 1 and beta > 1 the probability density function // tends to zero at x = 0 and x = 1 and has a unique maximum in // the interval (0,1). } // The following uses an iterative scheme to find the roots of: // y^(a-1) * (1 - y)^(b-1) = x^(a-1) * (1 - x)^(b-1) // // This means that we can generally avoid evaluating the p.d.f. except // to check reasonable convergence of expansion and typically reduces // the runtime by an order of magnitude over root finding. const double CONVERGENCE_TOLERANCE = 1e-4; const double PDF_TOLERANCE = 5e-2; const unsigned int MAX_ITERATIONS = 6; TDoubleBoolPr sp = stationaryPoint(beta_); double y[] = {2.0 * sp.first - x, 0.0}; unsigned int i = 0; this->tail(x, sp.first, tail); if (x < sp.first) { // For x < mode we use the recurrence relation: // y(n+1) = 1 - (x / y(n))^((a-1)/(b-1)) * (1 - x) // // Note it is relatively straightforward to show that: // g(y) = 1 - (x / y)^((a-1)/(b-1)) * (1 - x) // // is a contractive mapping for y > x and x < (a-1)/(a+b-1) by // showing that the derivative is less than 1. This guarantees // that the following iteration (eventually) converges to the // unique fixed point (Banach fixed point theorem). Note that // this has poor convergence for x near the stationary point; // however, reflecting x in the stationary point gives a starting // point very near root in this case, i.e. it is equivalent to // initializing with a second order Taylor expansion about the // mode. y[0] = std::min(y[0], (1.0 + sp.first) / 2.0); double k = (beta_.alpha() - 1.0) / (beta_.beta() - 1.0); for (;;) { y[(i + 1) % 2] = 1.0 - std::exp(k * std::log(x / y[i % 2])) * (1.0 - x); if (++i == MAX_ITERATIONS || std::fabs(y[1] - y[0]) < CONVERGENCE_TOLERANCE) { break; } } // Max sure y is supported by the p.d.f. if (y[i % 2] > support.second) { return truncate(sp.second ? safeCdf(beta_, x) : safeCdfComplement(beta_, x), 0.0, 1.0); } y[i % 2] = std::max(y[i % 2], sp.first); } else { // For x > mode we use the recurrence relation: // y(n+1) = ((1 - x) / (1 - y(n)))^((b-1)/(a-1)) * x // // Note it is relatively straightforward to show that: // g(y) = ((1 - x) / (1 - y)^((b-1)/(a-1)) * x // // is a contractive mapping for y > x and x < (a-1)/(a+b-1) by // showing that the derivative is less than 1. This guarantees // that the following iteration (eventually) converges to the // unique fixed point (Banach fixed point theorem). Note that // this has poor convergence for x near the stationary point; // however, reflecting x in the stationary point gives a starting // point very near the root in this case, i.e. it is equivalent to // initializing with a second order Taylor expansion about the // mode. y[0] = std::max(y[0], sp.first / 2.0); double k = (beta_.beta() - 1.0) / (beta_.alpha() - 1.0); for (;;) { y[(i + 1) % 2] = std::exp(k * std::log((1.0 - x) / (1.0 - y[i % 2]))) * x; if (++i == MAX_ITERATIONS || std::fabs(y[1] - y[0]) < CONVERGENCE_TOLERANCE) { break; } } // Max sure y is supported by the p.d.f. if (y[i % 2] < support.first) { return truncate(sp.second ? safeCdfComplement(beta_, x) : safeCdf(beta_, x), 0.0, 1.0); } y[i % 2] = std::min(y[i % 2], sp.first); } double fx = safePdf(beta_, x); double fy = safePdf(beta_, y[i % 2]); LOG_TRACE(<< "f(x) = " << fx << ", f(y) = " << fy); TDoubleDoublePr bracket(support); TDoubleDoublePr fBracket(0.0, 0.0); try { double error = sp.second ? fy - fx : fx - fy; if (std::fabs(error) <= PDF_TOLERANCE * std::max(fx, fy)) { if (x > y[i % 2]) { std::swap(x, y[i % 2]); } return truncate(sp.second ? safeCdf(beta_, x) + safeCdfComplement(beta_, y[i % 2]) : safeCdf(beta_, y[i % 2]) - safeCdf(beta_, x), 0.0, 1.0); } else if (error > 0.0) { if (x < sp.first) { bracket = std::make_pair(y[i % 2], bracket.second); double fa = fy - fx; double fb = safePdf(beta_, bracket.second) - fx; fBracket = std::make_pair(fa, fb); } else { bracket = std::make_pair(bracket.first, y[i % 2]); double fa = safePdf(beta_, bracket.first) - fx; double fb = fy - fx; fBracket = std::make_pair(fa, fb); } } else { bracket = std::make_pair(sp.first, y[i % 2]); double fa = safePdf(beta_, sp.first) - fx; double fb = fy - fx; fBracket = std::make_pair(fa, fb); } } catch (const std::exception& e) { LOG_ERROR(<< "Failed to evaluate p.d.f.: " << e.what() << ", alpha = " << beta_.alpha() << ", beta = " << beta_.beta() << ", x = " << x << ", y = " << y[i % 2]); return 1.0; } if (bracket.first > bracket.second) { std::swap(bracket.first, bracket.second); std::swap(fBracket.first, fBracket.second); } LOG_TRACE(<< "Initial bracket = " << bracket << ", f(bracket) = " << fBracket); try { double eps = 0.05 / fx; eps = std::max(eps, EPSILON * std::min(bracket.first, bracket.second)); CEqualWithTolerance<double> equal(CToleranceTypes::E_AbsoluteTolerance, eps); std::size_t maxIterations = MAX_ITERATIONS; double candidate; CSolvers::solve(bracket.first, bracket.second, fBracket.first, fBracket.second, makePdf(beta_, fx), maxIterations, equal, candidate); LOG_TRACE(<< "bracket = " << bracket << ", iterations = " << maxIterations << ", f(candidate) = " << safePdf(beta_, candidate) - fx << ", eps = " << eps); if (std::fabs(safePdf(beta_, candidate) - fx) < std::fabs(fy - fx)) { y[i % 2] = candidate; } } catch (const std::exception& e) { if (std::fabs(fBracket.first - fx) < 10.0 * EPSILON * fx) { y[i % 2] = bracket.first; } else if (std::fabs(fBracket.second - fx) < 10.0 * EPSILON * fx) { y[i % 2] = bracket.second; } else { LOG_ERROR(<< "Failed in bracketed solver: " << e.what() << ", x = " << x << ", bracket " << bracket << ", f(bracket) = " << fBracket); return 1.0; } } if (x > y[i % 2]) { std::swap(x, y[i % 2]); } return truncate(sp.second ? safeCdf(beta_, x) + safeCdfComplement(beta_, y[i % 2]) : safeCdf(beta_, y[i % 2]) - safeCdf(beta_, x), 0.0, 1.0); } bool CTools::CProbabilityOfLessLikelySample::check(const TDoubleDoublePr& support, double x, double& px, maths_t::ETail& tail) const { if (CMathsFuncs::isNan(x)) { LOG_ERROR(<< "Bad argument x = " << x); tail = maths_t::E_MixedOrNeitherTail; return false; } else if (x < support.first) { switch (m_Calculation) { case maths_t::E_OneSidedBelow: case maths_t::E_TwoSided: px = 0.0; break; case maths_t::E_OneSidedAbove: px = 1.0; break; } tail = static_cast<maths_t::ETail>(tail | maths_t::E_LeftTail); return false; } else if (x > support.second) { switch (m_Calculation) { case maths_t::E_OneSidedBelow: px = 1.0; break; case maths_t::E_TwoSided: case maths_t::E_OneSidedAbove: px = 0.0; break; } tail = static_cast<maths_t::ETail>(tail | maths_t::E_RightTail); return false; } return true; } void CTools::CProbabilityOfLessLikelySample::tail(double x, double mode, maths_t::ETail& tail) const { if (x <= mode) { tail = static_cast<maths_t::ETail>(tail | maths_t::E_LeftTail); } if (x >= mode) { tail = static_cast<maths_t::ETail>(tail | maths_t::E_RightTail); } } //////// CMixtureProbabilityOfLessLikelySample Implementation //////// CTools::CMixtureProbabilityOfLessLikelySample::CMixtureProbabilityOfLessLikelySample( std::size_t n, double x, double logFx, double a, double b) : m_X(x), m_LogFx(logFx), m_A(a), m_B(b) { m_Endpoints.reserve(4 * n + 2); m_Endpoints.push_back(a); m_Endpoints.push_back(b); } void CTools::CMixtureProbabilityOfLessLikelySample::reinitialize(double x, double logFx) { m_X = x; m_LogFx = logFx; m_Endpoints.clear(); m_Endpoints.push_back(m_A); m_Endpoints.push_back(m_B); } void CTools::CMixtureProbabilityOfLessLikelySample::addMode(double weight, double modeMean, double modeSd) { double deviation = m_LogFx - fastLog(weight) + LOG_ROOT_TWO_PI + fastLog(modeSd); if (deviation >= 0.0) { deviation = 0.0; m_Endpoints.push_back(truncate(modeMean - 2.0 * modeSd, m_A, m_B)); m_Endpoints.push_back(truncate(modeMean + 2.0 * modeSd, m_A, m_B)); } else if (deviation >= -0.5) { deviation = std::sqrt(-2.0 * deviation); m_Endpoints.push_back(truncate(modeMean - (deviation + 2.0) * modeSd, m_A, m_B)); m_Endpoints.push_back(truncate(modeMean, m_A, m_B)); m_Endpoints.push_back(truncate(modeMean + (deviation + 2.0) * modeSd, m_A, m_B)); } else { deviation = std::sqrt(-2.0 * deviation); m_Endpoints.push_back(truncate(modeMean - (deviation + 2.0) * modeSd, m_A, m_B)); m_Endpoints.push_back(truncate(modeMean - (deviation - 1.0) * modeSd, m_A, m_B)); m_Endpoints.push_back(truncate(modeMean + (deviation - 1.0) * modeSd, m_A, m_B)); m_Endpoints.push_back(truncate(modeMean + (deviation + 2.0) * modeSd, m_A, m_B)); } m_MaxDeviation.add((2.0 + deviation) * modeSd); } void CTools::CMixtureProbabilityOfLessLikelySample::intervals(TDoubleDoublePrVec& intervals) { std::sort(m_Endpoints.begin(), m_Endpoints.end()); m_Endpoints.erase(std::unique(m_Endpoints.begin(), m_Endpoints.end()), m_Endpoints.end()); intervals.reserve(m_Endpoints.size() - 1); for (std::size_t i = 1; i < m_Endpoints.size(); ++i) { intervals.emplace_back(m_Endpoints[i - 1], m_Endpoints[i]); } LOG_TRACE(<< "intervals = " << intervals); } const double CTools::CMixtureProbabilityOfLessLikelySample::LOG_ROOT_TWO_PI = 0.5 * std::log(boost::math::double_constants::two_pi); //////// SIntervalExpectation Implementation //////// double CTools::SIntervalExpectation::operator()(const normal& normal_, double a, double b) const { if (a > b) { std::swap(a, b); } if (a >= POS_INF) { return POS_INF; } if (a == b) { return a; } if (a == b) { return a; } double mean = normal_.mean(); double sd = normal_.standard_deviation(); double s = std::sqrt(2.0) * sd; double a_ = a <= NEG_INF ? a : (a - mean) / s; double b_ = b >= POS_INF ? b : (b - mean) / s; double expa = a_ <= NEG_INF ? 0.0 : std::exp(-a_ * a_); double expb = b_ >= POS_INF ? 0.0 : std::exp(-b_ * b_); double erfa = a_ <= NEG_INF ? -1.0 : std::erf(a_); double erfb = b_ >= POS_INF ? 1.0 : std::erf(b_); if (erfb - erfa < std::sqrt(EPSILON)) { return expa == expb ? (a + b) / 2.0 : (a * expa + b * expb) / (expa + expb); } return truncate(mean + 2.0 * sd * (expa - expb) / boost::math::double_constants::root_two_pi / (erfb - erfa), -POS_INF, POS_INF); } double CTools::SIntervalExpectation:: operator()(const lognormal& logNormal, double a, double b) const { if (a > b) { std::swap(a, b); } if (a >= POS_INF) { return POS_INF; } if (b <= 0.0) { return 0.0; } if (a == b) { return a; } double location = logNormal.location(); double scale = logNormal.scale(); if (a <= 1e-8) { lognormal logNormalShift{location + pow2(scale), scale}; // If F(b) underflows we're in the extreme left tail. We can fallback to // computing an asymptotic expansion of the ratio. The leading term (by // L'Hospital) turns out to just be b. This intuitively makes sense since // the distribution is concentrated at the right end of the interval. double cdf = boost::math::cdf(logNormal, b); return (cdf == 0.0 ? b : boost::math::mean(logNormal) * boost::math::cdf(logNormalShift, b) / cdf); } double mean = boost::math::mean(logNormal); double loga = std::log(a); double logb = std::log(b); double c = location + pow2(scale); double s = std::sqrt(2.0) * scale; double a_ = (loga - location) / s; double b_ = (logb - location) / s; double erfa = std::erf((loga - c) / s); double erfb = std::erf((logb - c) / s); if (erfb - erfa < std::sqrt(EPSILON)) { double expa = std::exp(-a_ * a_); double expb = std::exp(-b_ * b_); return expa == expb ? (2.0 * a / (a + b)) * b : (expa + expb) / (expa / a + expb / b); } double erfa_ = std::erf(a_); double erfb_ = std::erf(b_); return truncate( (erfb - erfa) == (erfb_ - erfa_) ? mean : mean * (erfb - erfa) / (erfb_ - erfa_), -POS_INF, POS_INF); } double CTools::SIntervalExpectation::operator()(const gamma& gamma_, double a, double b) const { if (a > b) { std::swap(a, b); } if (a >= POS_INF) { return POS_INF; } if (b <= 0.0) { return 0.0; } if (a == b) { return a; } double shape = gamma_.shape(); double scale = gamma_.scale(); if (a <= 1e-8) { boost::math::gamma_distribution<> gammaShift_{shape + 1.0, scale}; return boost::math::mean(gamma_) * boost::math::cdf(gammaShift_, b) / boost::math::cdf(gamma_, b); } double rate = 1.0 / scale; double mean = boost::math::mean(gamma_); double gama = boost::math::gamma_p(shape + 1.0, rate * a); double gamb = b >= POS_INF ? 1.0 : boost::math::gamma_p(shape + 1.0, rate * b); if (gamb - gama < std::sqrt(EPSILON)) { double expa = std::exp((shape - 1.0) * std::log(a) - rate * a); double expb = std::exp((shape - 1.0) * std::log(b) - rate * b); return expa == expb ? (a + b) / 2.0 : (a * expa + b * expb) / (expa + expb); } double gama_ = boost::math::gamma_p(shape, rate * a); double gamb_ = boost::math::gamma_p(shape, rate * b); return truncate( (gamb - gama) == (gamb_ - gama_) ? mean : mean * (gamb - gama) / (gamb_ - gama_), -POS_INF, POS_INF); } //////// smallestProbability Implementation //////// double CTools::smallestProbability() { return MIN_DOUBLE; } //////// safePdf Implementation //////// namespace { namespace math_policy { using namespace boost::math::policies; using AllowOverflow = policy<overflow_error<user_error>>; } inline boost::math::normal_distribution<double, math_policy::AllowOverflow> allowOverflow(const boost::math::normal_distribution<>& normal) { return boost::math::normal_distribution<double, math_policy::AllowOverflow>( normal.mean(), normal.standard_deviation()); } inline boost::math::students_t_distribution<double, math_policy::AllowOverflow> allowOverflow(const boost::math::students_t_distribution<>& students) { return boost::math::students_t_distribution<double, math_policy::AllowOverflow>( students.degrees_of_freedom()); } inline boost::math::poisson_distribution<double, math_policy::AllowOverflow> allowOverflow(const boost::math::poisson_distribution<>& poisson) { return boost::math::poisson_distribution<double, math_policy::AllowOverflow>( poisson.mean()); } inline boost::math::negative_binomial_distribution<double, math_policy::AllowOverflow> allowOverflow(const boost::math::negative_binomial_distribution<>& negativeBinomial) { return boost::math::negative_binomial_distribution<double, math_policy::AllowOverflow>( negativeBinomial.successes(), negativeBinomial.success_fraction()); } inline boost::math::lognormal_distribution<double, math_policy::AllowOverflow> allowOverflow(const boost::math::lognormal_distribution<>& logNormal) { return boost::math::lognormal_distribution<double, math_policy::AllowOverflow>( logNormal.location(), logNormal.scale()); } inline boost::math::gamma_distribution<double, math_policy::AllowOverflow> allowOverflow(const boost::math::gamma_distribution<>& gamma) { return boost::math::gamma_distribution<double, math_policy::AllowOverflow>( gamma.shape(), gamma.scale()); } inline boost::math::beta_distribution<double, math_policy::AllowOverflow> allowOverflow(const boost::math::beta_distribution<>& beta) { return boost::math::beta_distribution<double, math_policy::AllowOverflow>( beta.alpha(), beta.beta()); } inline boost::math::binomial_distribution<double, math_policy::AllowOverflow> allowOverflow(const boost::math::binomial_distribution<>& binomial) { return boost::math::binomial_distribution<double, math_policy::AllowOverflow>( binomial.trials(), binomial.success_fraction()); } inline boost::math::chi_squared_distribution<double, math_policy::AllowOverflow> allowOverflow(const boost::math::chi_squared_distribution<>& chi2) { return boost::math::chi_squared_distribution<double, math_policy::AllowOverflow>( chi2.degrees_of_freedom()); } } double CTools::safePdf(const normal& normal_, double x) { return continuousSafePdf(allowOverflow(normal_), x); } double CTools::safePdf(const students_t& students, double x) { return continuousSafePdf(allowOverflow(students), x); } double CTools::safePdf(const poisson& poisson_, double x) { return discreteSafePdf(allowOverflow(poisson_), x); } double CTools::safePdf(const negative_binomial& negativeBinomial, double x) { return discreteSafePdf(allowOverflow(negativeBinomial), x); } double CTools::safePdf(const lognormal& logNormal, double x) { return continuousSafePdf(allowOverflow(logNormal), x); } double CTools::safePdf(const gamma& gamma_, double x) { TDoubleDoublePr support = boost::math::support(gamma_); // The distribution at the 0 is either: // 0, // b ^ a / (a - 1)!, // infinite // // depending on the shape parameter. if (x == support.first) { if (gamma_.shape() < 1.0) { return POS_INF; } else if (gamma_.shape() == 1.0) { return 1.0 / gamma_.scale(); } return 0.0; } return continuousSafePdf(allowOverflow(gamma_), x); } double CTools::safePdf(const beta& beta_, double x) { TDoubleDoublePr support = boost::math::support(beta_); // The distribution is either: // 0, // 1, // 1 / B(a, b) or // infinity // // at the end points depending on the end point in question // and the values of the parameters. We explicitly handle all // cases because for some combinations of a and b the error // introduced by using a constant continuation of the function // from eps and 1 - eps can be very large. if (x == support.first) { if (beta_.alpha() < 1.0) { return POS_INF; } else if (beta_.alpha() == 1.0) { return 1.0 / boost::math::beta(beta_.alpha(), beta_.beta()); } else { return 0.0; } } else if (x == support.second) { if (beta_.beta() < 1.0) { return POS_INF; } else if (beta_.beta() == 1.0) { return 1.0 / boost::math::beta(beta_.alpha(), beta_.beta()); } else { return 0.0; } } return continuousSafePdf(allowOverflow(beta_), x); } double CTools::safePdf(const binomial& binomial_, double x) { return discreteSafePdf(allowOverflow(binomial_), x); } double CTools::safePdf(const chi_squared& chi2, double x) { TDoubleDoublePr support = boost::math::support(chi2); // Depending on the degrees of freedom the pdf at zero is either: // 0, // 1/2, // infinity // // at zero. double df = chi2.degrees_of_freedom(); if (x == support.first) { if (df < 2.0) { return POS_INF; } else if (df == 2.0) { return 0.5; } else { return 0.0; } } return continuousSafePdf(allowOverflow(chi2), x); } //////// safeCdf Implementation //////// double CTools::safeCdf(const normal& normal_, double x) { return continuousSafeCdf(allowOverflow(normal_), x); } double CTools::safeCdf(const students_t& students, double x) { return continuousSafeCdf(allowOverflow(students), x); } double CTools::safeCdf(const poisson& poisson_, double x) { return discreteSafeCdf(allowOverflow(poisson_), x); } double CTools::safeCdf(const negative_binomial& negativeBinomial, double x) { return discreteSafeCdf(allowOverflow(negativeBinomial), x); } double CTools::safeCdf(const lognormal& logNormal, double x) { return continuousSafeCdf(allowOverflow(logNormal), x); } double CTools::safeCdf(const gamma& gamma_, double x) { return continuousSafeCdf(allowOverflow(gamma_), x); } double CTools::safeCdf(const beta& beta_, double x) { return continuousSafeCdf(allowOverflow(beta_), x); } double CTools::safeCdf(const binomial& binomial_, double x) { return discreteSafeCdf(allowOverflow(binomial_), x); } double CTools::safeCdf(const chi_squared& chi2, double x) { return continuousSafeCdf(allowOverflow(chi2), x); } //////// safeCdfComplement Implementation //////// double CTools::safeCdfComplement(const normal& normal_, double x) { return continuousSafeCdfComplement(allowOverflow(normal_), x); } double CTools::safeCdfComplement(const students_t& students, double x) { return continuousSafeCdfComplement(allowOverflow(students), x); } double CTools::safeCdfComplement(const poisson& poisson_, double x) { return discreteSafeCdfComplement(allowOverflow(poisson_), x); } double CTools::safeCdfComplement(const negative_binomial& negativeBinomial, double x) { return discreteSafeCdfComplement(allowOverflow(negativeBinomial), x); } double CTools::safeCdfComplement(const lognormal& logNormal, double x) { return continuousSafeCdfComplement(allowOverflow(logNormal), x); } double CTools::safeCdfComplement(const gamma& gamma_, double x) { return continuousSafeCdfComplement(allowOverflow(gamma_), x); } double CTools::safeCdfComplement(const beta& beta_, double x) { return continuousSafeCdfComplement(allowOverflow(beta_), x); } double CTools::safeCdfComplement(const binomial& binomial_, double x) { return discreteSafeCdfComplement(allowOverflow(binomial_), x); } double CTools::safeCdfComplement(const chi_squared& chi2, double x) { return continuousSafeCdfComplement(allowOverflow(chi2), x); } //////// anomalyScore Implementation //////// namespace { const double SMALL_PROBABILITY_ANOMALY_SCORE = 1.0; const double MINUSCULE_PROBABILITY_ANOMALY_SCORE = 50.0; const double MAX_ANOMALY_SCORE = 100.0; const double INV_LARGEST_SIGNIFICANT_PROBABILITY = 1.0 / LARGEST_SIGNIFICANT_PROBABILITY; const double INV_SMALL_PROBABILITY = 1.0 / SMALL_PROBABILITY; const double MINUS_LOG_SMALL_PROBABILITY = -std::log(SMALL_PROBABILITY); const double MINUS_LOG_MINUSCULE_PROBABILITY = -std::log(MINUSCULE_PROBABILITY); } double CTools::anomalyScore(double p) { const double MINUS_LOG_SMALLEST_PROBABILITY = -std::log(smallestProbability()); double result = 0.0; double adjP = std::max(p, smallestProbability()); if (adjP < LARGEST_SIGNIFICANT_PROBABILITY) { if (adjP >= SMALL_PROBABILITY) { // We use a linear scaling based on the inverse probability // into the range (0.0, 1.0]. result = SMALL_PROBABILITY_ANOMALY_SCORE * (1.0 / adjP - INV_LARGEST_SIGNIFICANT_PROBABILITY) / (INV_SMALL_PROBABILITY - INV_LARGEST_SIGNIFICANT_PROBABILITY); } else if (adjP >= MINUSCULE_PROBABILITY) { // We use a linear scaling based on the log probability into // the range (1.0, 50.0]. result = SMALL_PROBABILITY_ANOMALY_SCORE + (MINUSCULE_PROBABILITY_ANOMALY_SCORE - SMALL_PROBABILITY_ANOMALY_SCORE) * (-std::log(adjP) - MINUS_LOG_SMALL_PROBABILITY) / (MINUS_LOG_MINUSCULE_PROBABILITY - MINUS_LOG_SMALL_PROBABILITY); } else { // We use a linear scaling based on the log probability into // the range (50.0, 100.0]. result = MINUSCULE_PROBABILITY_ANOMALY_SCORE + (MAX_ANOMALY_SCORE - MINUSCULE_PROBABILITY_ANOMALY_SCORE) * (-std::log(adjP) - MINUS_LOG_MINUSCULE_PROBABILITY) / (MINUS_LOG_SMALLEST_PROBABILITY - MINUS_LOG_MINUSCULE_PROBABILITY); } } if (!(result >= 0.0 && result <= MAX_ANOMALY_SCORE)) { LOG_ERROR(<< "Score " << result << " out of range, p = " << p); } return result; } double CTools::inverseAnomalyScore(double score) { const double MINUS_LOG_SMALLEST_PROBABILITY = -std::log(smallestProbability()); double result = 0.0; double adjScore = truncate(score, 0.0, MAX_ANOMALY_SCORE); if (adjScore == 0.0) { result = (1.0 + LARGEST_SIGNIFICANT_PROBABILITY) / 2.0; } else if (adjScore <= SMALL_PROBABILITY_ANOMALY_SCORE) { // We invert the linear scaling of the inverse probability // into the range (0.0, 1.0]. result = 1.0 / (INV_LARGEST_SIGNIFICANT_PROBABILITY + (INV_SMALL_PROBABILITY - INV_LARGEST_SIGNIFICANT_PROBABILITY) * score / SMALL_PROBABILITY_ANOMALY_SCORE); } else if (adjScore <= MINUSCULE_PROBABILITY_ANOMALY_SCORE) { // We invert the linear scaling of the log probability // into the range (1.0, 50.0]. result = std::exp( -(MINUS_LOG_SMALL_PROBABILITY + (MINUS_LOG_MINUSCULE_PROBABILITY - MINUS_LOG_SMALL_PROBABILITY) * (score - SMALL_PROBABILITY_ANOMALY_SCORE) / (MINUSCULE_PROBABILITY_ANOMALY_SCORE - SMALL_PROBABILITY_ANOMALY_SCORE))); } else { // We invert the linear scaling of the log probability // into the range (50.0, 100.0]. result = std::exp( -(MINUS_LOG_MINUSCULE_PROBABILITY + (MINUS_LOG_SMALLEST_PROBABILITY - MINUS_LOG_MINUSCULE_PROBABILITY) * (score - MINUSCULE_PROBABILITY_ANOMALY_SCORE) / (MAX_ANOMALY_SCORE - MINUSCULE_PROBABILITY_ANOMALY_SCORE))); } if (!(result >= 0.0 && result <= 1.0)) { LOG_ERROR(<< "Probability " << result << " out of range, score = " << score); } return result; } //////// differentialEntropy Implementation //////// double CTools::differentialEntropy(const poisson& poisson_) { // Approximate as sum over [mean - 5 * std, mean + 5 * std]. double mean = boost::math::mean(poisson_); double deviation = boost::math::standard_deviation(poisson_); unsigned int a = static_cast<unsigned int>(std::max(mean - 5.0 * deviation, 0.0)); unsigned int b = static_cast<unsigned int>(std::max(mean + 5.0 * deviation, 5.0)); double result = 0.0; for (unsigned int x = a; x <= b; ++x) { double pdf = safePdf(poisson_, x); result -= log(pdf) * pdf; } return result; } double CTools::differentialEntropy(const normal& normal_) { // Equals log(2 * pi * e * v) / 2 // // where, // m is the mean and variance of the normal distribution. double variance = boost::math::variance(normal_); return 0.5 * std::log(boost::math::double_constants::two_pi * boost::math::double_constants::e * variance); } double CTools::differentialEntropy(const lognormal& logNormal) { // Equals log(2 * pi * e * v) / 2 + m. // // where, // m and v are the mean and variance of the exponentiated normal // distribution, respectively. double location = logNormal.location(); double scale = logNormal.scale(); return 0.5 * std::log(boost::math::double_constants::two_pi * boost::math::double_constants::e * pow2(scale)) + location; } double CTools::differentialEntropy(const gamma& gamma_) { // Equals k + log(t) + log(g(k)) + (1 - k) * f(k) // // where, // k and t are the shape and scale of the gamma distribution, respectively. // g(.) is the gamma function and // f(.) is the digamma function, i.e. the derivative of log gamma. double shape = gamma_.shape(); double scale = gamma_.scale(); return shape + std::log(scale) + std::lgamma(shape) + (1 - shape) * boost::math::digamma(shape); } //////// Miscellaneous Implementations //////// namespace { const double EPS{0.1}; const double COEFFS[]{-1.0, +1.0 / 2.0, -1.0 / 6.0, +1.0 / 24.0, -1.0 / 120.0, +1.0 / 720.0}; const std::size_t N{std::size(COEFFS)}; } double CTools::shiftLeft(double x, double eps) { if (x == NEG_INF) { return x; } return (x < 0.0 ? 1.0 + eps : 1.0 - eps) * x; } double CTools::shiftRight(double x, double eps) { if (x == POS_INF) { return x; } return (x < 0.0 ? 1.0 - eps : 1.0 + eps) * x; } double CTools::linearlyInterpolate(double a, double b, double fa, double fb, double x) { if (a > b) { std::swap(a, b); std::swap(fa, fb); } if (x <= a) { return fa; } if (x >= b) { return fb; } if (b == a) { return 0.5 * (fa + fb); } return ((b - x) * fa + (x - a) * fb) / (b - a); } double CTools::powOneMinusX(double x, double p) { // For large p, // (1 - x) ^ p ~= exp(-p * x). // // and this doesn't suffer from cancellation errors in the limit // p -> inf and x -> 0. For p * x << 1 we get much better precision // using the Taylor expansion: // (1 - x) ^ p = 1 - p * x + p * (p - 1) * x^2 / 2! + ... // // and canceling the leading terms. if (x == 1.0) { return 0.0; } if (p == 1.0) { return 1.0 - x; } double y = p * x; if (std::fabs(y) < EPS) { double remainder = 0.0; double ti = 1.0; for (std::size_t i = 0; i < N && p != 0.0; ++i, p -= 1.0) { ti *= p * x; remainder += COEFFS[i] * ti; } return 1.0 + remainder; } else if (p > 1000.0) { return std::exp(-y); } if (x > 1.0) { double sign = static_cast<int>(p) % 2 ? -1.0 : 1.0; return sign * std::exp(p * std::log(x - 1.0)); } return std::exp(p * std::log(1.0 - x)); } double CTools::oneMinusPowOneMinusX(double x, double p) { // For large p, // (1 - x) ^ p ~= exp(-p * x). // // and this doesn't suffer from cancellation errors in the limit // p -> inf and x -> 0. For p * x << 1 we get much better precision // using the Taylor expansion: // (1 - x) ^ p = 1 - p * x + p * (p - 1) * x^2 / 2! + ... // // Note that this doesn't make use of powOneMinusX because we can // avoid the cancellation errors by using: // 1 - (1 - x) ^ p = p * x - p * (p - 1) * x^2 / 2 + ... // // when p * x is small. if (x == 1.0) { return 1.0; } if (p == 1.0) { return x; } double y = p * x; if (std::fabs(y) < EPS) { double result = 0.0; double ti = 1.0; for (std::size_t i = 0; i < N && p != 0.0; ++i, p -= 1.0) { ti *= p * x; result -= COEFFS[i] * ti; } return result; } else if (p > 1000.0) { return 1.0 - std::exp(-y); } if (x > 1.0) { double sign = static_cast<int>(p) % 2 ? -1.0 : 1.0; return 1.0 - sign * std::exp(p * std::log(x - 1.0)); } return 1.0 - std::exp(p * std::log(1.0 - x)); } double CTools::logOneMinusX(double x) { double result = 0.0; if (std::fabs(x) < EPS) { double xi = -x; for (std::size_t i = 0; i < 6; ++i, xi *= -x) { result += xi / static_cast<double>(i + 1); } } else { result = std::log(1.0 - x); } return result; } bool CTools::lgamma(double value, double& result, bool checkForFinite) { result = std::lgamma(value); return checkForFinite ? std::isfinite(result) : true; } const CTools::CLookupTableForFastLog<CTools::FAST_LOG_PRECISION> CTools::FAST_LOG_TABLE; } } }