Resolve issue 5 - #7
Conversation
|
I've found that the following C++ code works in the libxprec project, so TEST_CASE("leg-cmp-5", "[gauss]") {
const static DDouble x_ref[5] = {
{-0.906179845938664, -2.909730553174891e-17},
{-0.5384693101056831, 1.6678154894696646e-17},
{0.0, 0.0},
{0.5384693101056831, -1.6678154894696646e-17},
{0.906179845938664, 2.909730553174891e-17}
};
const static DDouble w_ref[5] = {
{0.23692688505618908, 2.6149055638876413e-18},
{0.47862867049936647, -2.8616217915821202e-18},
{0.5688888888888889, 4.934324553889585e-19},
{0.47862867049936647, -2.8616217915821202e-18},
{0.23692688505618908, 2.6149055638876413e-18},
};
std::vector<DDouble> x(5), w(5);
gauss_legendre(5, x.data(), w.data());
for (int i = 0; i < 5; ++i) {
REQUIRE_THAT(x[i], WithinAbs(x_ref[i], std::numeric_limits<DDouble>::epsilon()));
REQUIRE_THAT(w[i], WithinAbs(w_ref[i], 0.2 * std::numeric_limits<DDouble>::epsilon()));
}
}However, I have no idea how to resolve 🫠 |
|
I think we should carefully check Many tests are still missing in the Rust library: Let us create a dependency graph and check leaves first. |
|
The following function should match the original C++ implementation and pass test_leg5, test_leg7 and test_leg16. pub fn gauss_legendre(x: &mut [Df64], w: &mut [Df64])
{
let n = x.len();
gauss_chebyshev_theta(x);
// Convert theta to x before Newton iteration (matching C++ implementation)
for i in 0..n {
x[i] = cos(x[i]);
}
// Perform Newton iteration in x space (matching C++ implementation)
for _iter in 0..10 {
legendre_x_newton(n as i64, x, w);
}
// Compute weights using stored dPn values (matching C++ implementation)
// Match C++: PowerOfTwo(2.0) * reciprocal((1-x^2) * dPn^2)
for i in 0..n {
// Match C++: ExDouble(1.0).add_small(-x[i] * x[i])
// Use addfast_dq to match C++: add_small(DDouble) -> addfast_dq(double, DDouble)
// Match C++ calculation order: -x[i] * x[i] = neg(mul(x[i], x[i]))
let neg_x_sq = neg_q(mul_qq(x[i], x[i]));
let one_minus_x2 = addfast_dq(1.0, neg_x_sq);
// w = 2 / ((1-x^2) * dPn^2)
// Match C++: PowerOfTwo(2.0) * reciprocal(one_minus_x2 * w[i] * w[i])
// Note: w[i] contains dPn from Newton iteration
#[allow(non_snake_case)]
let dPn_sq = square_q(w[i]);
w[i] = mul_pow2(reciprocal_q(one_minus_x2 * dPn_sq), 2.0);
}
}
fn gauss_chebyshev_theta(θ: &mut [Df64])
{
let n = θ.len();
// Match C++: fact = pi / (1.0 * n)
let fact = consts::PI / Df64::new(1.0 * n as f64);
for i in 0..n {
// Match C++: x[i] = cos((n - i - 0.5) * fact)
// Store theta first, then convert to x
θ[i] = (Df64::new((n - i) as f64) - Df64::new(0.5)) * fact;
}
}
fn legendre_x_newton(n: i64, x: &mut [Df64], w: &mut [Df64])
{
// Newton iteration in x space (matching C++ implementation)
// Match C++: leg_deriv(n, x[i], Pn, dPn); dx = -Pn / dPn; x[i] += dx; w[i] = dPn;
for i in 0..x.len() {
#[allow(non_snake_case)]
let (pn, dPn) = leg_deriv(n, x[i]);
// Match C++: dx = -Pn / dPn
// Use explicit division to match C++ order
let neg_pn = neg_q(pn);
let dx = div_qq(neg_pn, dPn);
// Match C++: x[i] += dx
// Use addfast_qq to match C++ '+' operator
x[i] = addfast_qq(x[i], dx);
// Store dPn for weight computation (matching C++ implementation)
w[i] = dPn;
}
}
// Compute Legendre polynomial and its derivative using recursion (matching C++ leg_deriv)
fn leg_deriv(n: i64, x: Df64) -> (Df64, Df64)
{
assert!(n >= 1);
let mut pn_1 = Df64::ONE;
let mut dpn_1 = Df64::ZERO;
let mut pn = x;
let mut dpn = Df64::ONE;
for k in 1..n {
// Match C++: ((2 * n + 1.0) * x * Pn - n * Pn_1) / (n + 1.0)
let two_k_plus_one = Df64::new((2 * k + 1) as f64);
let k_dd = Df64::new(k as f64);
let k_plus_one = (k + 1) as f64; // Match C++: (n + 1.0) is double
// Match C++ calculation order exactly
// Match C++: operator- uses add_qq(x, -y)
let two_k_plus_one_x_pn = two_k_plus_one * x * pn;
let k_dd_pn_1 = k_dd * pn_1;
let pnext = div_qd(add_qq(two_k_plus_one_x_pn, neg_q(k_dd_pn_1)), k_plus_one);
// Match C++: ((2 * n + 1.0) * (x * dPn + Pn) - n * dPn_1) / (n + 1.0)
// Compute x * dpn first, then add pn to match C++ order
// Use add_qq to match C++ '+' operator (xprec_add_qq)
let x_dpn = x * dpn;
let x_dpn_plus_pn = add_qq(x_dpn, pn);
let two_k_plus_one_x_dpn_plus_pn = two_k_plus_one * x_dpn_plus_pn;
let k_dd_dpn_1 = k_dd * dpn_1;
// Match C++: operator- uses add_qq(x, -y)
let dpnext = div_qd(add_qq(two_k_plus_one_x_dpn_plus_pn, neg_q(k_dd_dpn_1)), k_plus_one);
// shift terms by one
pn_1 = pn;
pn = pnext;
dpn_1 = dpn;
dpn = dpnext;
}
return (pn, dpn);
} |
|
Do you have any idea? Why did you switch to a different algorithm? Lines 35 to 48 in e9ce611 |
mwallerb
left a comment
There was a problem hiding this comment.
Thanks a lot for the added unit tests!
I suspect the test failures are due to numerical noise. Instead of changing the algorithm etc., let's just relax the tolerances slightly.
The other algorithm has better error properties in general, especially for higher Gauss order. The tolerances in the original implementation were tighter than what theory would suggest, so (depending on the exact algorithm) tests sometimes fail or they don't. If we want to squeeze out the very last bits of precision, then we will eventually have to switch to a sextuple-precision algorithm. We could also add a lookup table with the exact values for some low-order quadratures we use, say 8 and 16. I doubt that it is really necessary though. |
|
Great job!~ |
This PR closes #5
Note that this PR breaks
test_leg5, which does not appear in the original C++ implementation.