Skip to content

Commit 435d103

Browse files
authored
Merge pull request #1123 from boostorg/issue1120
Change skew normal quantile to use bracket_and_solve_root.
2 parents a243640 + c61c4af commit 435d103

File tree

3 files changed

+114
-31
lines changed

3 files changed

+114
-31
lines changed

include/boost/math/distributions/skew_normal.hpp

+53-31
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@
2727
#include <utility>
2828
#include <algorithm> // std::lower_bound, std::distance
2929

30+
#ifdef BOOST_MATH_INSTRUMENT_SKEW_NORMAL_ITERATIONS
31+
extern std::uintmax_t global_iter_count;
32+
#endif
33+
3034
namespace boost{ namespace math{
3135

3236
namespace detail
@@ -614,32 +618,6 @@ namespace boost{ namespace math{
614618
return factor * y*y / (x*x);
615619
}
616620

617-
namespace detail
618-
{
619-
620-
template <class RealType, class Policy>
621-
struct skew_normal_quantile_functor
622-
{
623-
skew_normal_quantile_functor(const boost::math::skew_normal_distribution<RealType, Policy> dist, RealType const& p)
624-
: distribution(dist), prob(p)
625-
{
626-
}
627-
628-
boost::math::tuple<RealType, RealType> operator()(RealType const& x)
629-
{
630-
RealType c = cdf(distribution, x);
631-
RealType fx = c - prob; // Difference cdf - value - to minimize.
632-
RealType dx = pdf(distribution, x); // pdf is 1st derivative.
633-
// return both function evaluation difference f(x) and 1st derivative f'(x).
634-
return boost::math::make_tuple(fx, dx);
635-
}
636-
private:
637-
const boost::math::skew_normal_distribution<RealType, Policy> distribution;
638-
RealType prob;
639-
};
640-
641-
} // namespace detail
642-
643621
template <class RealType, class Policy>
644622
inline RealType quantile(const skew_normal_distribution<RealType, Policy>& dist, const RealType& p)
645623
{
@@ -681,14 +659,58 @@ namespace boost{ namespace math{
681659

682660
// refine the result by numerically searching the root of (p-cdf)
683661

684-
const RealType search_min = support(dist).first;
685-
const RealType search_max = support(dist).second;
686-
687662
const int get_digits = policies::digits<RealType, Policy>();// get digits from policy,
688663
std::uintmax_t max_iter = policies::get_max_root_iterations<Policy>(); // and max iterations.
689664

690-
result = tools::newton_raphson_iterate(detail::skew_normal_quantile_functor<RealType, Policy>(dist, p), result,
691-
search_min, search_max, get_digits, max_iter);
665+
if (result == 0)
666+
result = tools::min_value<RealType>(); // we need to be one side of zero or the other for the root finder to work.
667+
668+
auto fun = [&, dist, p](const RealType& x)->RealType { return cdf(dist, x) - p; };
669+
670+
RealType f_result = fun(result);
671+
672+
if (f_result == 0)
673+
return result;
674+
675+
if (f_result * result > 0)
676+
{
677+
// If the root is in the direction of zero, we need to check that we're the correct side of it:
678+
RealType f_zero = fun(static_cast<RealType>(0));
679+
if (f_zero * f_result > 0)
680+
{
681+
// we're the wrong side of zero:
682+
result = -result;
683+
f_result = fun(result);
684+
}
685+
}
686+
687+
RealType scaling_factor = 1.25;
688+
if (f_result * result > 0)
689+
{
690+
// We're heading towards zero... it's a long way down so use a larger scaling factor:
691+
scaling_factor = 16;
692+
}
693+
694+
auto p_result = tools::bracket_and_solve_root(fun, result, scaling_factor, true, tools::eps_tolerance<RealType>(get_digits), max_iter, Policy());
695+
696+
#ifdef BOOST_MATH_INSTRUMENT_SKEW_NORMAL_ITERATIONS
697+
global_iter_count += max_iter;
698+
#endif
699+
700+
result = (p_result.first + p_result.second) / 2;
701+
702+
//
703+
// Try one last Newton step, just to close up the interval:
704+
//
705+
RealType step = fun(result) / pdf(dist, result);
706+
707+
if (result - step <= p_result.first)
708+
result = p_result.first;
709+
else if (result - step >= p_result.second)
710+
result = p_result.second;
711+
else
712+
result -= step;
713+
692714
if (max_iter >= policies::get_max_root_iterations<Policy>())
693715
{
694716
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

test/Jamfile.v2

+1
Original file line numberDiff line numberDiff line change
@@ -874,6 +874,7 @@ test-suite distribution_tests :
874874
[ run scipy_issue_18302.cpp ../../test/build//boost_unit_test_framework ]
875875
[ run scipy_issue_18511.cpp ../../test/build//boost_unit_test_framework ]
876876
[ compile scipy_issue_19762.cpp ]
877+
[ run git_issue_1120.cpp ]
877878
;
878879

879880
test-suite new_floats :

test/git_issue_1120.cpp

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright John Maddock 2024.
2+
// Use, modification and distribution are subject to the
3+
// Boost Software License, Version 1.0. (See accompanying file
4+
// LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
5+
6+
//
7+
// The purpose of this test case is to probe the skew normal quantiles
8+
// for extreme values of skewness and ensure that our root finders don't
9+
// blow up, see https://github.com/boostorg/math/issues/1120 for original
10+
// test case. We test both the maximum number of iterations taken, and the
11+
// overall total (ie average). Any changes to the skew normal should
12+
// ideally NOT cause this test to fail, as this indicates that our root
13+
// finding has been made worse by the change!!
14+
//
15+
// Note that defining BOOST_MATH_INSTRUMENT_SKEW_NORMAL_ITERATIONS
16+
// causes the skew normal quantile to save the number of iterations
17+
// to a global variable "global_iter_count".
18+
//
19+
20+
#define BOOST_MATH_INSTRUMENT_SKEW_NORMAL_ITERATIONS
21+
22+
#include <random>
23+
#include <boost/math/distributions/skew_normal.hpp>
24+
#include "math_unit_test.hpp"
25+
26+
std::uintmax_t global_iter_count;
27+
std::uintmax_t total_iter_count = 0;
28+
29+
int main()
30+
{
31+
using scipy_policy = boost::math::policies::policy<boost::math::policies::promote_double<false>>;
32+
33+
std::mt19937 gen;
34+
std::uniform_real_distribution<double> location(-3, 3);
35+
std::uniform_real_distribution<double> scale(0.001, 3);
36+
37+
for (unsigned skew = 50; skew < 2000; skew += 43)
38+
{
39+
constexpr double pn[] = { 0.0001, 0.001, 0.01, 0.1, 0.2, 0.3, 0.4 };
40+
boost::math::skew_normal_distribution<double, scipy_policy> dist(location(gen), scale(gen), skew);
41+
for (unsigned i = 0; i < 7; ++i)
42+
{
43+
global_iter_count = 0;
44+
double x = quantile(dist, pn[i]);
45+
total_iter_count += global_iter_count;
46+
CHECK_LE(global_iter_count, static_cast<std::uintmax_t>(60));
47+
double p = cdf(dist, x);
48+
CHECK_ABSOLUTE_ERROR(p, pn[i], 45 * std::numeric_limits<double>::epsilon());
49+
50+
global_iter_count = 0;
51+
x = quantile(complement(dist, pn[i]));
52+
total_iter_count += global_iter_count;
53+
CHECK_LE(global_iter_count, static_cast<std::uintmax_t>(60));
54+
p = cdf(complement(dist, x));
55+
CHECK_ABSOLUTE_ERROR(p, pn[i], 45 * std::numeric_limits<double>::epsilon());
56+
}
57+
}
58+
CHECK_LE(total_iter_count, static_cast<std::uintmax_t>(10000));
59+
return boost::math::test::report_errors();
60+
}

0 commit comments

Comments
 (0)