Repository navigation
Add nested Leja quadrature and a dimension-adaptive sparse grid - #1589
NAThompson wants to merge 1 commit into
Conversation
|
@JacobHass8 : Check it out at your convenience. |
950bfb6 to
b071d87
Compare
| // The weights solve sum_i w_i p_k(x_i) = sqrt(mu0) delta_k0, k < n. Scaling column i by sqrt(w(x_i)) makes the | ||
| // matrix Q_ki = sqrt(w(x_i)) p_k(x_i) well conditioned where the Vandermonde matrix p_k(x_i) is not. Each n is | ||
| // solved by LU with partial pivoting over the degrees k, in O(N^4) for all n; reusing one factorization of Q^T in | ||
| // Leja order would take O(N^3), but for Legendre its weights are up to 5 times less accurate. | ||
| void compute_weights(unsigned N) |
There was a problem hiding this comment.
I wonder if there is a way to generalize this for any weight function if you don't know the recurrence relation. I think there is a way if you know the moments of the weight function but I'm not exactly sure.
There was a problem hiding this comment.
Yes. The two halves of the construction need different things. The points only use √w through log √w and its derivatives (the greedy argmax), so any weight we can evaluate works as is. The orthonormal recurrence (beta(k), mu0) is only used for the weights, to keep the moment system well conditioned.
Recovering the recurrence from raw moments ∫xᵏw is badly conditioned: the Hankel matrix's condition number grows exponentially. The stable routes are Gautschi's:
- the modified Chebyshev algorithm, from modified moments ∫pₖ(x)w(x)dx against a known basis such as Legendre or Chebyshev, which is stable on a bounded interval;
- discretized Stieltjes or Lanczos, which only needs w, discretized by a fine Gauss or tanh-sinh rule.
Either would give a general leja<Real>(w, support). I kept this PR to the two closed-form weights, but a Jacobi weight (also closed form) or a general weight via discretized Stieltjes would be a natural follow-up.
-claude
b071d87 to
291a854
Compare
3365f91 to
d5d2ac5
Compare
|
@JacobHass8 : I believe that performance-wise, API wise, feature-wise, and otherwise this is good. Please try these commands against the wheel at your convenience: import boost.math as bm, numpy as np
# The rule is nested: adding a point keeps all the earlier ones,
# so the adaptive loop never evaluates f at the same point twice.
r = bm.leja_legendre(max_points=64)
r.abscissa[:5] # [0, -1, 1, -0.577, 0.659]
r.integrate_adaptive(np.cos, tolerance=1e-12)
# (1.682941969615793, 6e-13, ...) exact 2 sin 1 = 1.682941969615793
# Chebyshev weight 1/sqrt(1-x^2) via Jacobi(-1/2, -1/2)
cheb = bm.leja_jacobi(max_points=64, alpha=-0.5, beta=-0.5)
cheb.integrate_adaptive(np.cos, tolerance=1e-12)
# (2.403939430634413, 1e-12, ...) exact π J0(1) = 2.403939430634413
# Singular weight on a half-line: ∫₀^∞ cos(x) e^{-x} / sqrt(x) dx
bm.leja_laguerre(max_points=128, alpha=-0.5).integrate_adaptive(np.cos, tolerance=1e-10)
# (1.3769963318531544, 6e-11, ...) exact 1.376996331853153
# An 8-dimensional Gaussian integral on a sparse grid
c = 1 / np.arange(1, 9)
bm.sparse_grid(lambda x: np.cos(c @ x), rule=bm.leja_hermite(max_points=64),
dimension=8, max_calls=200_000, tolerance=1e-8)
# (66.49082912155218, 3.3e-7, ...) exact π⁴ exp(-Σcᵢ²/4) = 66.49082913179191
# A product of exponential weights on [0, ∞)³
bm.sparse_grid(lambda x: np.exp(-x[0]/2 - x[1]/3 - x[2]/4),
rule=bm.leja_laguerre(max_points=64), dimension=3,
max_calls=100_000, tolerance=1e-12)
# (0.3999999999999982, 1e-13, ...) exact 2/5 |
Legendre, Hermite, Laguerre and Jacobi Leja rules reuse earlier evaluations; the sparse grid refines where contributions are largest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
d5d2ac5 to
857423b
Compare
|
Wow, this is excellent! I had tried to implement sparse grids but it was quite slow and inaccurate. I'm impressed it's so accurate! Thank you! |
Legendre, Hermite, Laguerre and Jacobi Leja rules reuse earlier evaluations; the sparse grid refines where contributions are largest.
leja_legendre,leja_hermite,leja_laguerre(max_points, alpha = 0)(x^α e^{−x} on [0, ∞)) andleja_jacobi(max_points, alpha, beta)((1−x)^α (1+x)^β on [−1, 1]) are nested weighted Leja rules (one new point per rule, starting at the mean of the weight, weights from a scaled moment system), with an adaptiveintegrate.sparse_grid(f, rule, dimension, max_calls, tolerance)builds a dimension-adaptive sparse grid (Gerstner–Griebel) from them, against a product of any of these weights. Both are in the nanobind layer.One dimension
Error vs function calls: Leja (nested, one new call per rule), its free estimate, one-shot Gauss, and Gauss re-run at each doubling.
Where Leja loses, Hermite with a pole at ±i:
Sparse grid
Genz integrands, relative error vs calls, against Tasmanian 8.2 (Leja rules, surplus and anisotropic refinement), one Monte Carlo run and Boost.Random's unshifted Sobol' sequence, as a user would run them:
Where QMC wins, a peaked integrand in eight dimensions:
max_calls(tolerance 1e-14) so every point is a budget-limited result; the honesty numbers below come from a tolerance sweep.Laguerre and Jacobi, exponents from −0.9 to 10: stable to 1024 points (cos x, 1/(2+x) within 1.2e-14), points within 8 ulps and weights within 160 ulps of 50-digit builds up to 100 points (α = −0.9: 112 / 1340). Laguerre moments lose n²ε·L1 in the LU solve rather than nε.
Evidence (H5Web, VS Code extension): leja_correctness.h5.zip (exactness for every n, weights vs 100 digits, greedy choices, Lebesgue constants), sparse_grid_evidence.h5.zip (all convergence plots, honesty of the estimate, chosen index sets).
Benchmark (
reporting/performance/leja_quadrature_performance.cpp, Apple M-series, -O3, CPU time)🤖 Generated with Claude Code