use std::sync::OnceLock;
pub(crate) const MAX_ORDER: usize = 64;
#[derive(Debug)]
pub(crate) struct Rule {
pub(crate) nodes: Vec<f64>,
pub(crate) weights: Vec<f64>,
}
impl Rule {
fn new(n: usize) -> Self {
let mut nodes = vec![0.0; n];
let mut weights = vec![0.0; n];
let nf = n as f64;
for k in 0..n.div_ceil(2) {
let mut x = (std::f64::consts::PI * (k as f64 + 0.75) / (nf + 0.5)).cos();
let mut derivative = 1.0;
for _ in 0..100 {
let (mut p_prev, mut p) = (1.0, x);
for j in 1..n {
let jf = j as f64;
let p_next = ((2.0 * jf + 1.0) * x * p - jf * p_prev) / (jf + 1.0);
p_prev = p;
p = p_next;
}
derivative = nf * (x * p - p_prev) / (x * x - 1.0);
let step = p / derivative;
x -= step;
if step.abs() <= 4.0 * f64::EPSILON * x.abs().max(1e-3) {
break;
}
}
if n % 2 == 1 && k == n / 2 {
x = 0.0;
let (mut p_prev, mut p) = (1.0, x);
for j in 1..n {
let jf = j as f64;
let p_next = ((2.0 * jf + 1.0) * x * p - jf * p_prev) / (jf + 1.0);
p_prev = p;
p = p_next;
}
derivative = nf * (x * p - p_prev) / (x * x - 1.0);
}
let weight = 2.0 / ((1.0 - x * x) * derivative * derivative);
nodes[n - 1 - k] = x;
nodes[k] = -x;
weights[n - 1 - k] = weight;
weights[k] = weight;
}
Self { nodes, weights }
}
pub(crate) fn on(&self, a: f64, b: f64) -> impl Iterator<Item = (f64, f64)> + '_ {
let (mid, half) = (0.5 * (a + b), 0.5 * (b - a));
self.nodes
.iter()
.zip(&self.weights)
.map(move |(&x, &w)| (mid + half * x, half * w))
}
}
pub(crate) fn rule(n: usize) -> &'static Rule {
static RULES: OnceLock<Vec<Rule>> = OnceLock::new();
let rules = RULES.get_or_init(|| (1..=MAX_ORDER).map(Rule::new).collect());
&rules[n.clamp(1, MAX_ORDER) - 1]
}
#[derive(Debug)]
pub(crate) struct OrderTable {
thresholds: Vec<f64>,
}
impl OrderTable {
pub(crate) fn new(tolerance: f64, max_order: usize) -> Self {
let log = (1.0 / tolerance).ln();
Self {
thresholds: (1..=max_order)
.map(|n| (log / (2.0 * n as f64)).sinh())
.collect(),
}
}
pub(crate) fn max_order(&self) -> usize {
self.thresholds.len()
}
pub(crate) fn order_for(&self, half_extent: f64, distance: f64) -> Option<usize> {
if half_extent <= 0.0 {
return Some(1);
}
if distance.is_nan() || distance <= 0.0 {
return None;
}
let delta = distance / half_extent;
self.thresholds
.iter()
.position(|&needed| delta >= needed)
.map(|index| index + 1)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn weights_sum_to_two_and_nodes_are_symmetric() {
for n in 1..=MAX_ORDER {
let r = rule(n);
assert_eq!(r.nodes.len(), n);
let sum: f64 = r.weights.iter().sum();
assert!((sum - 2.0).abs() < 1e-13, "n = {n}: sum = {sum}");
for k in 0..n {
assert!((r.nodes[k] + r.nodes[n - 1 - k]).abs() < 1e-15);
assert!(r.nodes[k] > -1.0 && r.nodes[k] < 1.0);
if k > 0 {
assert!(r.nodes[k] > r.nodes[k - 1]);
}
}
}
}
#[test]
fn integrates_polynomials_of_degree_2n_minus_1_exactly() {
for n in [1, 2, 3, 5, 8, 16, 33, 64] {
let r = rule(n);
let degree = (2 * n - 1).min(40);
for p in 0..=degree {
let got: f64 = r.on(0.0, 1.0).map(|(x, w)| w * x.powi(p as i32)).sum();
let want = 1.0 / (p as f64 + 1.0);
assert!((got - want).abs() < 1e-13, "n = {n}, p = {p}: {got}");
}
}
}
#[test]
fn order_estimate_grows_as_the_singularity_approaches() {
let table = OrderTable::new(1e-9, 64);
assert_eq!(table.max_order(), 64);
assert_eq!(table.order_for(0.0, 1.0), Some(1));
assert_eq!(table.order_for(1.0, 0.0), None);
assert_eq!(table.order_for(1.0, f64::NAN), None);
let far = table.order_for(1.0, 1e6).unwrap();
let mid = table.order_for(1.0, 10.0).unwrap();
let near = table.order_for(1.0, 1.0).unwrap();
assert!(far <= mid && mid < near, "{far} {mid} {near}");
assert_eq!(far, 1);
assert_eq!(table.order_for(1.0, 1e-3), None);
for delta in [0.5_f64, 1.0, 3.0, 30.0] {
let n = table.order_for(1.0, delta).unwrap();
let a = 1.0 + delta;
let got: f64 = rule(n).on(-1.0, 1.0).map(|(x, w)| w / (a - x)).sum();
let want = ((a + 1.0) / (a - 1.0)).ln();
assert!(((got - want) / want).abs() < 1e-9, "δ = {delta}, n = {n}");
}
}
}