Skip to content

Commit febd4e2

Browse files
committed
FIX: Correct Gauss sum for even-order Gauss-Kronrod rules (#93)
The evaluation-reusing path (compute_gauss_kronrod_sums_stored, introduced in faeaf2b) reconstructed the Gauss sum assuming an odd-order Gauss rule: a center node at 0.0 plus pairs at Kronrod offsets +/- 2j. Even-order rules (G10/G20/G30) have no node at 0.0; their nodes sit at odd Kronrod offsets +/- (2j - 1). The old code therefore added a spurious center term (a Kronrod-only node times an ordinary pair weight), dropped one node pair, and read every pair from Kronrod-only nodes, so the "Gauss" sum contained no Gauss node at all. |G - K| then decayed only linearly with interval width, so G10K21 / G20K41 / G30K61 (and R variants) never early-exited and always subdivided to max_iter, even for a constant integrand. Branch on the parity of g: odd keeps the previous mapping, even skips the center term, sums all g/2 pairs at offsets +/- (2j - 1), and starts pair weights at index g/2 - 1. Tests: * unit: reconstructed G matches direct Gauss-Legendre and |G - K| is round-off for a cubic, for all six rules (failed on G10K21 before) * integration: a cubic finishes in exactly k evaluations for all twelve method variants, and the optimized path agrees with the reference gauss_kronrod_quadrature on a generic smooth integrand
1 parent 087fd4f commit febd4e2

2 files changed

Lines changed: 139 additions & 10 deletions

File tree

src/numerical/integral.rs

Lines changed: 80 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -500,29 +500,46 @@ where
500500
let K = kronrod_sum * xh;
501501

502502
// 6. Calculate Gauss sum (G) using stored evaluations and Gauss weights
503-
// Assumes G(g) nodes are subset of K(k), g is odd, standard mapping: +/- xg_j maps to +/- xk_{2j}.
503+
// The Gauss nodes are a subset of the Kronrod nodes, but their position
504+
// relative to the Kronrod center node (0.0) depends on the parity of g:
505+
// - odd g: 0.0 is a Gauss node; +/- xg_j maps to Kronrod indices center +/- 2j
506+
// - even g: 0.0 is a Kronrod-only node; +/- xg_j maps to center +/- (2j - 1)
504507
let kronrod_center_idx = k / 2; // Index of 0.0 node in Kronrod/f_evals
505-
let gauss_center_idx = g / 2; // Index of 0.0 weight in Gauss weights
506-
let num_gauss_pairs = (g - 1) / 2;
507508

508-
// Contribution from center node (0.0)
509-
let mut gauss_sum = f_evals[kronrod_center_idx].clone() * gauss_weights[gauss_center_idx];
509+
let mut gauss_sum;
510+
let num_gauss_pairs;
511+
let node_offset_parity; // Kronrod offset of pair j is 2j - node_offset_parity
512+
let gauss_weight_base; // Gauss weight index of pair j is gauss_weight_base + j
513+
if g % 2 == 1 {
514+
// Center node contribution with the true center Gauss weight
515+
gauss_sum = f_evals[kronrod_center_idx].clone() * gauss_weights[g / 2];
516+
num_gauss_pairs = (g - 1) / 2;
517+
node_offset_parity = 0;
518+
gauss_weight_base = g / 2;
519+
} else {
520+
// Even-order Gauss rules have no node at 0.0: every node is paired
521+
gauss_sum = Y::ZERO;
522+
num_gauss_pairs = g / 2;
523+
node_offset_parity = 1;
524+
gauss_weight_base = g / 2 - 1;
525+
}
510526

511527
// Contribution from paired Gauss nodes (+/- xg_j)
512528
for j in 1..=num_gauss_pairs {
513-
if 2 * j > kronrod_center_idx {
529+
let offset = 2 * j - node_offset_parity;
530+
if offset > kronrod_center_idx {
514531
panic!(
515532
"Kronrod node index underflow. k={}, center_idx={}, j={}",
516533
k, kronrod_center_idx, j
517534
);
518535
}
519536

520-
// Indices in f_evals corresponding to Kronrod nodes +/- xk_{2j}
521-
let kronrod_node_pos_idx = kronrod_center_idx + 2 * j;
522-
let kronrod_node_neg_idx = kronrod_center_idx - 2 * j;
537+
// Indices in f_evals corresponding to the Kronrod nodes hosting +/- xg_j
538+
let kronrod_node_pos_idx = kronrod_center_idx + offset;
539+
let kronrod_node_neg_idx = kronrod_center_idx - offset;
523540

524541
// Index in gauss_weights for the symmetric weight of the pair +/- xg_j
525-
let gauss_weight_pair_idx = gauss_center_idx + j;
542+
let gauss_weight_pair_idx = gauss_weight_base + j;
526543

527544
// Safety checks
528545
if kronrod_node_pos_idx >= k || kronrod_node_neg_idx >= k {
@@ -2192,3 +2209,56 @@ const KRONROD_WEIGHTS_61: [f64; 61] = [
21922209
0.003890461127099884,
21932210
0.001389013698677008,
21942211
];
2212+
2213+
#[cfg(test)]
2214+
mod tests {
2215+
use super::*;
2216+
2217+
const GK_ORDERS: [(usize, usize); 6] =
2218+
[(7, 15), (10, 21), (15, 31), (20, 41), (25, 51), (30, 61)];
2219+
2220+
#[test]
2221+
fn gauss_sum_reconstruction_is_exact_for_low_degree_polynomials() {
2222+
// Both G and K integrate a cubic exactly, so the reconstructed Gauss
2223+
// sum must match the Kronrod sum to round-off. Regression test for #93:
2224+
// the even-order reconstruction added a spurious center term and used
2225+
// wrong node offsets, so |G - K| stayed O(1) instead of vanishing.
2226+
let f = |x: f64| 1.0 + x + x * x + x * x * x;
2227+
let exact = 1.0 + 0.5 + 1.0 / 3.0 + 0.25;
2228+
for (g, k) in GK_ORDERS {
2229+
let (g_sum, k_sum, err): (f64, f64, f64) =
2230+
compute_gauss_kronrod_sums_stored(f, (0.0, 1.0), g, k);
2231+
assert!(
2232+
(g_sum - k_sum).abs() < 1e-13,
2233+
"G{}K{}: G = {:.17e}, K = {:.17e}",
2234+
g,
2235+
k,
2236+
g_sum,
2237+
k_sum
2238+
);
2239+
assert!((k_sum - exact).abs() < 1e-13);
2240+
assert!(err.abs() < 1e-13);
2241+
}
2242+
}
2243+
2244+
#[test]
2245+
fn gauss_sum_reconstruction_matches_direct_gauss_legendre() {
2246+
// For an arbitrary smooth integrand the Gauss sum reconstructed from
2247+
// the stored Kronrod evaluations must equal the directly computed
2248+
// Gauss-Legendre quadrature of the same order.
2249+
let f = |x: f64| (1.5 * x).sin() + (-x).exp();
2250+
for (g, k) in GK_ORDERS {
2251+
let (g_sum, _, _): (f64, f64, f64) =
2252+
compute_gauss_kronrod_sums_stored(f, (0.3, 2.1), g, k);
2253+
let direct: f64 = gauss_legendre_quadrature(f, g, (0.3, 2.1));
2254+
assert!(
2255+
(g_sum - direct).abs() < 1e-13,
2256+
"G{}K{}: reconstructed = {:.17e}, direct = {:.17e}",
2257+
g,
2258+
k,
2259+
g_sum,
2260+
direct
2261+
);
2262+
}
2263+
}
2264+
}

tests/integral.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,65 @@ fn test_oscillatory() {
9595
}
9696
}
9797

98+
#[test]
99+
fn test_early_exit_on_low_degree_polynomials() {
100+
// A G_gK_k rule integrates polynomials of degree <= 2g - 1 exactly, so the
101+
// very first |G - K| check must pass and the whole integration must finish
102+
// with exactly k function evaluations (one interval, no subdivision).
103+
// Regression test for #93: the even-order rules (G10K21, G20K41, G30K61
104+
// and their R variants) never early-exited and subdivided to max_iter.
105+
use std::cell::Cell;
106+
let methods = [
107+
(G7K15(1e-8, 20), 15),
108+
(G10K21(1e-8, 20), 21),
109+
(G15K31(1e-8, 20), 31),
110+
(G20K41(1e-8, 20), 41),
111+
(G25K51(1e-8, 20), 51),
112+
(G30K61(1e-8, 20), 61),
113+
(G7K15R(1e-8, 20), 15),
114+
(G10K21R(1e-8, 20), 21),
115+
(G15K31R(1e-8, 20), 31),
116+
(G20K41R(1e-8, 20), 41),
117+
(G25K51R(1e-8, 20), 51),
118+
(G30K61R(1e-8, 20), 61),
119+
];
120+
for (m, k) in methods {
121+
let count = Cell::new(0u32);
122+
let f = |x: f64| {
123+
count.set(count.get() + 1);
124+
x.powi(3)
125+
};
126+
let r: f64 = integrate(f, (0.0, 1.0), m);
127+
assert_close(r, 0.25);
128+
assert_eq!(
129+
count.get(),
130+
k,
131+
"{:?} did not early-exit on a cubic: {} evaluations instead of {}",
132+
m,
133+
count.get(),
134+
k
135+
);
136+
}
137+
}
138+
139+
#[test]
140+
fn test_optimized_matches_reference_gauss_kronrod() {
141+
// The evaluation-reusing path must agree with the straightforward
142+
// independent-evaluation path on a generic smooth integrand.
143+
let f = |x: f64| (2.0 * x).sin() * (-x).exp();
144+
for m in gk_methods(1e-10) {
145+
let opt: f64 = gauss_kronrod_quadrature_optimized(f, (0.0, 2.0), m);
146+
let reference: f64 = gauss_kronrod_quadrature(f, (0.0, 2.0), m);
147+
assert!(
148+
(opt - reference).abs() < 1e-9,
149+
"{:?}: optimized = {:.17e}, reference = {:.17e}",
150+
m,
151+
opt,
152+
reference
153+
);
154+
}
155+
}
156+
98157
#[test]
99158
fn test_endpoint_independence() {
100159
let m = || G10K21R(1e-10, 20);

0 commit comments

Comments
 (0)