use crate::scalar::{DoubleDouble as Dd, Real};
use crate::{Error, Result};
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct GaussLegendre<T> {
pub nodes: Vec<T>,
pub weights: Vec<T>,
}
pub const MAX_GAUSS_LEGENDRE_POINTS: usize = 1 << 17;
pub(super) const ASYMPTOTIC_THRESHOLD: usize = 100;
const F64_PRECISION_FROM: f64 = f64::from_bits((1023 - 60) << 52);
const CONVERGED: f64 = f64::from_bits((1023 - 90) << 52); const MAX_ITERATIONS: usize = 100;
pub(super) struct Recurrence {
a: Vec<Dd>,
b: Vec<Dd>,
}
impl Recurrence {
pub(super) fn new(n: usize) -> Self {
let a = (0..=n)
.map(|j| {
if j == 0 {
Dd::ZERO
} else {
Dd::from_f64((2 * j - 1) as f64) / j as f64
}
})
.collect();
let b = (0..=n)
.map(|j| {
if j == 0 {
Dd::ZERO
} else {
Dd::from_f64((j - 1) as f64) / j as f64
}
})
.collect();
Self { a, b }
}
fn legendre_and_derivative(&self, n: usize, x: Dd) -> (Dd, Dd) {
let mut p_prev = Dd::ONE;
let mut p = x;
for j in 2..=n {
let next = self.a[j] * x * p - self.b[j] * p_prev;
p_prev = p;
p = next;
}
let dp = (p_prev - x * p) * n as f64 / ((Dd::ONE - x) * (Dd::ONE + x));
(p, dp)
}
fn newton(&self, n: usize, guess: f64) -> Result<(Dd, Dd)> {
let mut x = Dd::from_f64(guess);
let mut last = f64::INFINITY;
let mut polish = false;
for _ in 0..MAX_ITERATIONS {
let (p, dp) = self.legendre_and_derivative(n, x);
let dx = p / dp;
x -= dx;
last = dx.hi().abs();
let small = last <= CONVERGED;
if polish && small {
let (_, dp) = self.legendre_and_derivative(n, x);
return Ok((x, dp));
}
polish = small;
}
Err(Error::NotConverged {
achieved: if last.is_nan() { f64::INFINITY } else { last },
requested: CONVERGED,
})
}
pub(super) fn root_and_weight(&self, n: usize, i: usize) -> Result<(Dd, Dd)> {
let (x, dp) = if 2 * i + 1 == n {
(Dd::ZERO, self.legendre_and_derivative(n, Dd::ZERO).1)
} else {
let nf = n as f64;
let k = (i + 1) as f64;
let theta = core::f64::consts::PI * (4.0 * k - 1.0) / (4.0 * nf + 2.0);
let cos_theta = Dd::from_f64(theta).cos().hi();
let guess = (1.0 - (nf - 1.0) / (8.0 * nf * nf * nf)) * cos_theta;
self.newton(n, guess)?
};
let w = Dd::from_f64(2.0) / ((Dd::ONE - x) * (Dd::ONE + x) * dp * dp);
Ok((x, w))
}
}
pub fn gauss_legendre<T: Real>(n: usize) -> Result<GaussLegendre<T>> {
check_n(n)?;
let mut nodes = vec![T::zero(); n];
let mut weights = vec![T::zero(); n];
fill(&mut nodes, &mut weights)?;
Ok(GaussLegendre { nodes, weights })
}
pub fn gauss_legendre_into<T: Real>(nodes: &mut [T], weights: &mut [T]) -> Result<()> {
check_n(nodes.len())?;
if weights.len() != nodes.len() {
return Err(Error::InvalidInput(format!(
"Gauss-Legendre buffers must have the same length, got {} nodes and {} weights",
nodes.len(),
weights.len()
)));
}
fill(nodes, weights)
}
fn check_n(n: usize) -> Result<()> {
if n == 0 {
return Err(Error::InvalidInput(
"Gauss-Legendre rule needs n >= 1".into(),
));
}
if n > MAX_GAUSS_LEGENDRE_POINTS {
return Err(Error::InvalidInput(format!(
"Gauss-Legendre rule needs n <= {MAX_GAUSS_LEGENDRE_POINTS}, got {n}"
)));
}
Ok(())
}
fn fill<T: Real>(nodes: &mut [T], weights: &mut [T]) -> Result<()> {
let n = nodes.len();
if n > ASYMPTOTIC_THRESHOLD && T::epsilon().approx_f64() >= F64_PRECISION_FROM {
super::gauss_legendre_asymptotic::asymptotic_rule(nodes, weights);
return Ok(());
}
let rec = Recurrence::new(n);
for i in 0..n.div_ceil(2) {
let (x, w) = rec.root_and_weight(n, i)?;
let w = T::from_f64_pair(w.hi(), w.lo());
nodes[i] = T::from_f64_pair(-x.hi(), -x.lo());
nodes[n - 1 - i] = T::from_f64_pair(x.hi(), x.lo());
weights[i] = w;
weights[n - 1 - i] = w;
}
Ok(())
}
#[cfg(test)]
mod tests {
use core::f64::consts::PI;
use std::collections::BTreeSet;
use super::*;
type RefValue = [f64; 4];
type RefRule = (usize, Vec<usize>, Vec<RefValue>, Vec<RefValue>);
fn reference(case: &str) -> Option<RefRule> {
let text = crate::test_data::reference_file("gauss_legendre.jsonl")?;
let rec = text
.lines()
.map(|l| serde_json::from_str::<serde_json::Value>(l).unwrap())
.find(|r| r["case"] == case)
.unwrap_or_else(|| panic!("no reference case {case}: regenerate the data"));
let size = |v: &serde_json::Value| v.as_u64().expect("integer") as usize;
let values = |name: &str| -> Vec<RefValue> {
let column = |k: &str| -> Vec<f64> {
let out = &rec["outputs"][name][k];
out.as_array()
.expect("array")
.iter()
.map(|v| v.as_f64().unwrap())
.collect()
};
let (hi, lo, lo2, err) = (column("hi"), column("lo"), column("lo2"), column("err"));
(0..hi.len())
.map(|i| [hi[i], lo[i], lo2[i], err[i]])
.collect()
};
let indices = rec["inputs"]["indices"].as_array().expect("array");
Some((
size(&rec["inputs"]["n"]),
indices.iter().map(size).collect(),
values("nodes"),
values("weights"),
))
}
fn error(got: Dd, [hi, lo, lo2, err]: RefValue) -> f64 {
((got - Dd::new(hi, lo)) - lo2).abs().hi() + err
}
#[test]
fn roots_at_the_cap_match_arb() {
let Some((n, indices, nodes, weights)) =
reference(&format!("n={MAX_GAUSS_LEGENDRE_POINTS},roots"))
else {
return;
};
assert_eq!(n, MAX_GAUSS_LEGENDRE_POINTS);
let rec = Recurrence::new(n);
let rule = gauss_legendre::<f64>(n).unwrap();
let u2 = 2f64.powi(-106);
let (mut worst_x, mut worst_w) = (0.0_f64, 0.0_f64);
for (k, &idx) in indices.iter().enumerate() {
let (x, w) = rec.root_and_weight(n, idx.min(n - 1 - idx)).unwrap();
let x = if idx < n / 2 { -x } else { x };
let ex = error(x, nodes[k]);
let ew = error(w, weights[k]) / weights[k][0];
worst_x = worst_x.max(ex / u2);
worst_w = worst_w.max(ew / u2);
assert!(ex <= 2f64.powi(-98), "node {idx}: error {ex:e}");
let wtol = 2f64.powi(-100) * (4.0 + (n * n) as f64);
assert!(ew <= wtol, "weight {idx}: relative error {ew:e}");
let ex64 = error(Dd::from_f64(rule.nodes[idx]), nodes[k]);
let ew64 = error(Dd::from_f64(rule.weights[idx]), weights[k]) / weights[k][0];
assert!(
ex64 <= 32.0 * f64::EPSILON,
"node {idx}: f64 error {ex64:e}"
);
assert!(
ew64 <= 32.0 * f64::EPSILON,
"weight {idx}: f64 error {ew64:e}"
);
}
eprintln!(
"n = {n}: worst node error {worst_x:.3} u², worst weight error {worst_w:.3e} u² \
(u² = 2^-106)"
);
}
#[test]
fn roots_next_to_the_ends_and_the_middle_converge_up_to_the_cap() {
let cap = MAX_GAUSS_LEGENDRE_POINTS;
let mut ns: BTreeSet<usize> = (1..=64).collect();
let mut m = 64.0_f64;
while (m as usize) < cap {
ns.insert(m as usize);
m *= 1.1;
}
for e in 6..=cap.trailing_zeros() {
ns.extend(
[(1 << e) - 1, 1 << e, (1 << e) + 1]
.into_iter()
.filter(|&n| n <= cap),
);
}
for &n in &ns {
let rec = Recurrence::new(n);
let half = n.div_ceil(2);
let h = n as f64 + 0.5;
let roots: BTreeSet<usize> = (0..half.min(6))
.chain(half.saturating_sub(3)..half)
.collect();
for i in roots {
let (x, w) = rec
.root_and_weight(n, i)
.unwrap_or_else(|e| panic!("n={n} root {i}: {e}"));
let j = (i + 1) as f64; let (below, above) = ((j * PI / h).cos(), ((j - 0.5) * PI / h).cos());
assert!(
below < x.hi() && x.hi() < above,
"n={n} root {i}: {x:?} outside ({below}, {above})"
);
assert!(w.hi() > 0.0, "n={n} root {i}: weight {w:?}");
}
}
eprintln!("{} values of n up to {cap}: ok", ns.len());
}
}