Ticket #9104 (closed Bugs: fixed)
boost::math::ellint_2 bug in x86_64 double precision
|Reported by:||julian.panetta@…||Owned by:||johnmaddock|
|Milestone:||To Be Determined||Component:||math|
The way ellint_e_imp reduces its input angle into the range |phi| <= pi/2 has a bug when executing in double precision mode on intel chips (as opposed to the default double extended-precision mode) that results in errors as great as 100%.
The relevant lines are at boost/math/special_functions/ellint_2.hpp:
T rphi = boost::math::tools::fmod_workaround(phi, T(constants::pi<T>() / 2)); T m = floor((2 * phi) / constants::pi<T>());
Compiled with optimizations on both g++ and clang++, fmod_workaround() essentially becomes a "fprem" instruction, and (2 * phi) / pi() becomes an "fdivr" instruction. "fprem" ignores the precision mode and always produces exact results. "fdivr," however, gives less accurate results in plain double precision mode. This leads to the following error when, for example, phi = M_PI (slightly less than boost's more accurate pi()):
In plain double precision, rphi is computed as slightly less than pi()/2 (correct since M_PI < pi()). However, m = 2 (incorrect, rounded result). The end result is m * pi()/2 + rphi is close to 3pi/2 instead of M_PI, causing the computed integral to be 50% too large. In extended precision, the "fdivr" is accurate enough to yield m = 1 correctly.
There are two ways I can see of avoiding this:
T rphi = boost::math::tools::fmod_workaround(phi, T(constants::pi<T>() / 2)); T m = (phi - rphi) / T(constants::pi<T>() / 2);
and the less verbose:
T m = floor(phi / T(constants::pi<T>() / 2)); T rphi = phi - m * T(constants::pi<T>() / 2);
Both would ensure rphi and m are consistent with the original phi. Note: in the second formula for m, T(constants::pi<T>() / 2) is folded into a constant that is exactly pi()/2, whereas the floor() argument in the original version generated extra code to double phi.