2
0
mirror of https://github.com/boostorg/math.git synced 2026-01-19 04:22:09 +00:00

Change skew normal quantile to use bracket_and_solve_root.

Rather than Newton iterations.
Add test case.
Fixes https://github.com/boostorg/math/issues/1120
This commit is contained in:
jzmaddock
2024-04-21 17:19:16 +01:00
parent 15c40fae16
commit db90dfd709
3 changed files with 102 additions and 5 deletions

View File

@@ -27,6 +27,10 @@
#include <utility>
#include <algorithm> // std::lower_bound, std::distance
#ifdef BOOST_MATH_INSTRUMENT_SKEW_NORMAL_ITERATIONS
extern std::uintmax_t global_iter_count;
#endif
namespace boost{ namespace math{
namespace detail
@@ -681,14 +685,46 @@ namespace boost{ namespace math{
// refine the result by numerically searching the root of (p-cdf)
const RealType search_min = support(dist).first;
const RealType search_max = support(dist).second;
const int get_digits = policies::digits<RealType, Policy>();// get digits from policy,
std::uintmax_t max_iter = policies::get_max_root_iterations<Policy>(); // and max iterations.
result = tools::newton_raphson_iterate(detail::skew_normal_quantile_functor<RealType, Policy>(dist, p), result,
search_min, search_max, get_digits, max_iter);
if (result == 0)
result = tools::min_value<RealType>(); // we need to be one side of zero or the other for the root finder to work.
auto fun = [&, dist, p](const RealType& x)->RealType { return cdf(dist, x) - p; };
RealType f_result = fun(result);
if (f_result == 0)
return result;
if (f_result * result > 0)
{
// If the root is in the direction of zero, we need to check that we're the correct side of it:
RealType f_zero = fun(static_cast<RealType>(0));
if (f_zero * f_result > 0)
{
// we're the wrong side of zero:
result = -result;
f_result = fun(result);
}
}
RealType scaling_factor = 1.25;
if (f_result * result > 0)
{
// We're heading towards zero... it's a long way down so use a larger scaling factor:
scaling_factor = 16;
}
auto p_result = tools::bracket_and_solve_root(fun, result, scaling_factor, true, tools::eps_tolerance<RealType>(get_digits), max_iter, Policy());
#ifdef BOOST_MATH_INSTRUMENT_SKEW_NORMAL_ITERATIONS
global_iter_count += max_iter;
#endif
result = (p_result.first + p_result.second) / 2;
if (max_iter >= policies::get_max_root_iterations<Policy>())
{
return policies::raise_evaluation_error<RealType>(function, "Unable to locate solution in a reasonable time: either there is no answer to quantile" // LCOV_EXCL_LINE

View File

@@ -873,6 +873,7 @@ test-suite distribution_tests :
[ run scipy_issue_18302.cpp ../../test/build//boost_unit_test_framework ]
[ run scipy_issue_18511.cpp ../../test/build//boost_unit_test_framework ]
[ compile scipy_issue_19762.cpp ]
[ run git_issue_1120.cpp ]
;
test-suite new_floats :

60
test/git_issue_1120.cpp Normal file
View File

@@ -0,0 +1,60 @@
// Copyright John Maddock 2024.
// Use, modification and distribution are subject to the
// Boost Software License, Version 1.0. (See accompanying file
// LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
// The purpose of this test case is to probe the skew normal quantiles
// for extreme values of skewness and ensure that our root finders don't
// blow up, see https://github.com/boostorg/math/issues/1120 for original
// test case. We test both the maximum number of iterations taken, and the
// overall total (ie average). Any changes to the skew normal should
// ideally NOT cause this test to fail, as this indicates that our root
// finding has been made worse by the change!!
//
// Note that defining BOOST_MATH_INSTRUMENT_SKEW_NORMAL_ITERATIONS
// causes the skew normal quantile to save the number of iterations
// to a global variable "global_iter_count".
//
#define BOOST_MATH_INSTRUMENT_SKEW_NORMAL_ITERATIONS
#include <random>
#include <boost/math/distributions/skew_normal.hpp>
#include "math_unit_test.hpp"
std::uintmax_t global_iter_count;
std::uintmax_t total_iter_count = 0;
int main()
{
using scipy_policy = boost::math::policies::policy<boost::math::policies::promote_double<false>>;
std::mt19937 gen;
std::uniform_real<> location(-3, 3);
std::uniform_real<> scale(0.001, 3);
for (unsigned skew = 50; skew < 2000; skew += 43)
{
constexpr double pn[] = { 0.0001, 0.001, 0.01, 0.1, 0.2, 0.3, 0.4 };
boost::math::skew_normal_distribution<double, scipy_policy> dist(location(gen), scale(gen), skew);
for (unsigned i = 0; i < 7; ++i)
{
global_iter_count = 0;
double x = quantile(dist, pn[i]);
total_iter_count += global_iter_count;
CHECK_LE(global_iter_count, static_cast<std::uintmax_t>(55));
double p = cdf(dist, x);
CHECK_ULP_CLOSE(p, pn[i], 45000);
global_iter_count = 0;
x = quantile(complement(dist, pn[i]));
total_iter_count += global_iter_count;
CHECK_LE(global_iter_count, static_cast<std::uintmax_t>(55));
p = cdf(complement(dist, x));
CHECK_ULP_CLOSE(p, pn[i], 45000);
}
}
CHECK_LE(total_iter_count, static_cast<std::uintmax_t>(10000));
return boost::math::test::report_errors();
}