You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

137 lines
4.8 KiB
C++

// (C) Copyright Nick Thompson 2021.
// 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)
#ifndef BOOST_MATH_TOOLS_CUBIC_ROOTS_HPP
#define BOOST_MATH_TOOLS_CUBIC_ROOTS_HPP
#include <array>
#include <algorithm>
#include <boost/math/special_functions/sign.hpp>
#include <boost/math/tools/roots.hpp>
namespace boost::math::tools {
// Solves ax^3 + bx^2 + cx + d = 0.
// Only returns the real roots, as types get weird for real coefficients and complex roots.
// Follows Numerical Recipes, Chapter 5, section 6.
// NB: A better algorithm apparently exists:
// Algorithm 954: An Accurate and Efficient Cubic and Quartic Equation Solver for Physical Applications
// However, I don't have access to that paper!
template<typename Real>
std::array<Real, 3> cubic_roots(Real a, Real b, Real c, Real d) {
using std::sqrt;
using std::acos;
using std::cos;
using std::cbrt;
using std::abs;
using std::fma;
std::array<Real, 3> roots = {std::numeric_limits<Real>::quiet_NaN(),
std::numeric_limits<Real>::quiet_NaN(),
std::numeric_limits<Real>::quiet_NaN()};
if (a == 0) {
// bx^2 + cx + d = 0:
if (b == 0) {
// cx + d = 0:
if (c == 0) {
if (d != 0) {
// No solutions:
return roots;
}
roots[0] = 0;
roots[1] = 0;
roots[2] = 0;
return roots;
}
roots[0] = -d/c;
return roots;
}
auto [x0, x1] = quadratic_roots(b, c, d);
roots[0] = x0;
roots[1] = x1;
return roots;
}
if (d == 0) {
auto [x0, x1] = quadratic_roots(a, b, c);
roots[0] = x0;
roots[1] = x1;
roots[2] = 0;
std::sort(roots.begin(), roots.end());
return roots;
}
Real p = b/a;
Real q = c/a;
Real r = d/a;
Real Q = (p*p - 3*q)/9;
Real R = (2*p*p*p - 9*p*q + 27*r)/54;
if (R*R < Q*Q*Q) {
Real rtQ = sqrt(Q);
Real theta = acos(R/(Q*rtQ))/3;
Real st = sin(theta);
Real ct = cos(theta);
roots[0] = -2*rtQ*ct - p/3;
roots[1] = -rtQ*(-ct + sqrt(Real(3))*st) - p/3;
roots[2] = rtQ*(ct + sqrt(Real(3))*st) - p/3;
} else {
// In Numerical Recipes, Chapter 5, Section 6, it is claimed that we only have one real root
// if R^2 >= Q^3. But this isn't true; we can even see this from equation 5.6.18.
// The condition for having three real roots is that A = B.
// It *is* the case that if we're in this branch, and we have 3 real roots, two are a double root.
// Take (x+1)^2(x-2) = x^3 - 3x -2 as an example. This clearly has a double root at x = -1,
// and it gets sent into this branch.
Real arg = R*R - Q*Q*Q;
Real A = -boost::math::sign(R)*cbrt(abs(R) + sqrt(arg));
Real B = 0;
if (A != 0) {
B = Q/A;
}
roots[0] = A + B - p/3;
// Yes, we're comparing floats for equality:
// Any perturbation pushes the roots into the complex plane; out of the bailiwick of this routine.
if (A == B || arg == 0) {
roots[1] = -A - p/3;
roots[2] = -A - p/3;
}
}
// Root polishing:
for (auto & r : roots) {
// Horner's method.
// Here I'll take John Gustaffson's opinion that the fma is a *distinct* operation from a*x +b:
// Make sure to compile these fmas into a single instruction and not a function call!
// (I'm looking at you Windows.)
Real f = fma(a, r, b);
f = fma(f,r,c);
f = fma(f,r,d);
Real df = fma(3*a, r, 2*b);
df = fma(df, r, c);
if (df != 0) {
// No standard library feature for fused-divide add!
r -= f/df;
}
}
std::sort(roots.begin(), roots.end());
return roots;
}
// Computes the empirical residual p(r) (first element) and expected residual eps*|rp'(r)| (second element) for a root.
// Recall that for a numerically computed root r satisfying r = r_0(1+eps) of a function p, |p(r)| <= eps|rp'(r)|.
template<typename Real>
std::array<Real, 2> cubic_root_residual(Real a, Real b, Real c, Real d, Real root) {
using std::fma;
using std::abs;
std::array<Real, 2> out;
Real residual = fma(a, root, b);
residual = fma(residual,root,c);
residual = fma(residual,root,d);
out[0] = residual;
Real expected_residual = fma(3*a, root, 2*b);
expected_residual = fma(expected_residual, root, c);
expected_residual = abs(root*expected_residual)*std::numeric_limits<Real>::epsilon();
out[1] = expected_residual;
return out;
}
}
#endif