use crate::error::{IntegrateError, IntegrateResult};
use std::collections::HashMap;
use std::f64::consts::PI;
#[derive(Debug, Clone, PartialEq, Default)]
#[non_exhaustive]
pub enum OneDimRule {
#[default]
ClenshawCurtis,
GaussLegendre,
GaussPatterson,
}
#[derive(Debug, Clone)]
pub struct SmolyakConfig {
pub level: usize,
pub n_dims: usize,
pub rule: OneDimRule,
}
impl Default for SmolyakConfig {
fn default() -> Self {
Self {
level: 3,
n_dims: 3,
rule: OneDimRule::ClenshawCurtis,
}
}
}
#[derive(Debug, Clone)]
pub struct SmolyakGrid {
pub points: Vec<Vec<f64>>,
pub weights: Vec<f64>,
pub n_dims: usize,
pub level: usize,
}
impl SmolyakGrid {
#[inline]
pub fn n_points(&self) -> usize {
self.points.len()
}
}
#[derive(Debug, Clone)]
pub struct SparseGridResult {
pub value: f64,
pub n_points: usize,
pub error_estimate: f64,
}
pub fn cc_points_weights(n: usize) -> (Vec<f64>, Vec<f64>) {
if n == 1 {
return (vec![0.0], vec![2.0]);
}
let m = n - 1; let mf = m as f64;
let nodes: Vec<f64> = (0..n).map(|j| -(PI * j as f64 / mf).cos()).collect();
let mut weights = vec![0.0_f64; n];
for j in 0..n {
let cj = if j == 0 || j == m { 1.0 } else { 2.0 };
let half = m / 2;
let mut s = 0.0_f64;
for k in 1..=half {
let bk = if k == half && m.is_multiple_of(2) {
1.0
} else {
2.0
};
let denom = 4.0 * (k as f64).powi(2) - 1.0;
s += bk / denom * (2.0 * PI * j as f64 * k as f64 / mf).cos();
}
weights[j] = cj / mf * (1.0 - s);
}
(nodes, weights)
}
pub fn gl_points_weights(n: usize) -> (Vec<f64>, Vec<f64>) {
if n == 1 {
return (vec![0.0], vec![2.0]);
}
let mut nodes = vec![0.0_f64; n];
let mut weights = vec![0.0_f64; n];
let half = n.div_ceil(2);
let nf = n as f64;
for i in 0..half {
let mut x = -(1.0 - (1.0 - 1.0 / nf) / (8.0 * nf * nf))
* (PI * (4 * i + 3) as f64 / (4 * n + 2) as f64).cos();
for _ in 0..100 {
let (p, dp) = legendre_p_and_dp(n, x);
let dx = p / dp;
x -= dx;
if dx.abs() < 1e-15 * (1.0 + x.abs()) {
break;
}
}
let (_, dp) = legendre_p_and_dp(n, x);
let w = 2.0 / ((1.0 - x * x) * dp * dp);
nodes[i] = -x;
nodes[n - 1 - i] = x;
weights[i] = w;
weights[n - 1 - i] = w;
}
(nodes, weights)
}
fn legendre_p_and_dp(n: usize, x: f64) -> (f64, f64) {
if n == 0 {
return (1.0, 0.0);
}
if n == 1 {
return (x, 1.0);
}
let mut p_prev = 1.0_f64;
let mut p_curr = x;
for k in 1..n {
let kf = k as f64;
let p_next = ((2.0 * kf + 1.0) * x * p_curr - kf * p_prev) / (kf + 1.0);
p_prev = p_curr;
p_curr = p_next;
}
let nf = n as f64;
let dp = nf * (x * p_curr - p_prev) / (x * x - 1.0);
(p_curr, dp)
}
fn gp_points_weights(level: usize) -> (Vec<f64>, Vec<f64>) {
let n = match level {
1 => 1,
2 => 3,
3 => 7,
4 => 15,
5 => 31,
k => 2_usize.pow((k as u32).min(10)) + 1, };
gl_points_weights(n)
}
fn one_dim_rule(level: usize, rule: &OneDimRule) -> (Vec<f64>, Vec<f64>) {
match rule {
OneDimRule::ClenshawCurtis => {
let n = if level <= 1 {
1
} else {
(1_usize << (level - 1)) + 1
};
cc_points_weights(n)
}
OneDimRule::GaussLegendre => {
let n = level.max(1);
gl_points_weights(n)
}
OneDimRule::GaussPatterson => gp_points_weights(level),
}
}
fn binom(n: usize, k: usize) -> i64 {
if k > n {
return 0;
}
if k == 0 || k == n {
return 1;
}
let k = k.min(n - k);
let mut result = 1_i64;
for i in 0..k {
result = result * (n - i) as i64 / (i + 1) as i64;
}
result
}
#[allow(clippy::only_used_in_recursion)]
fn multi_indices_sum(
n_dims: usize,
target: usize,
base: usize,
result: &mut Vec<Vec<usize>>,
current: &mut Vec<usize>,
) {
if current.len() == n_dims {
if base == 0 {
result.push(current.clone());
}
return;
}
let remaining_dims = n_dims - current.len();
if base < remaining_dims {
return;
}
let max_val = base - (remaining_dims - 1); for val in 1..=max_val {
current.push(val);
multi_indices_sum(n_dims, target, base - val, result, current);
current.pop();
}
}
pub fn smolyak_grid(
n_dims: usize,
level: usize,
rule: &OneDimRule,
) -> IntegrateResult<SmolyakGrid> {
if n_dims == 0 {
return Err(IntegrateError::InvalidInput(
"n_dims must be at least 1".into(),
));
}
if level == 0 {
return Err(IntegrateError::InvalidInput(
"level must be at least 1".into(),
));
}
let max_sum = level + n_dims - 1;
let mut point_map: HashMap<String, (Vec<f64>, f64)> = HashMap::new();
for s in n_dims..=max_sum {
let diff = max_sum - s;
let sign = if diff.is_multiple_of(2) {
1_i64
} else {
-1_i64
};
let coeff = sign * binom(n_dims - 1, diff);
if coeff == 0 {
continue;
}
let coeff_f = coeff as f64;
let mut indices: Vec<Vec<usize>> = Vec::new();
multi_indices_sum(n_dims, s, s, &mut indices, &mut Vec::new());
for idx in indices {
let (mut pts, mut wts): (Vec<Vec<f64>>, Vec<f64>) = {
let (x0, w0) = one_dim_rule(idx[0], rule);
let pts: Vec<Vec<f64>> = x0.iter().map(|&xi| vec![xi]).collect();
let wts: Vec<f64> = w0;
(pts, wts)
};
for d in 1..n_dims {
let (xd, wd) = one_dim_rule(idx[d], rule);
let mut new_pts = Vec::with_capacity(pts.len() * xd.len());
let mut new_wts = Vec::with_capacity(wts.len() * xd.len());
for (p, &wp) in pts.iter().zip(wts.iter()) {
for (&xj, &wj) in xd.iter().zip(wd.iter()) {
let mut new_p = p.clone();
new_p.push(xj);
new_pts.push(new_p);
new_wts.push(wp * wj);
}
}
pts = new_pts;
wts = new_wts;
}
for (p, w) in pts.iter().zip(wts.iter()) {
let key = point_key(p);
let entry = point_map.entry(key).or_insert_with(|| (p.clone(), 0.0));
entry.1 += coeff_f * w;
}
}
}
let mut points = Vec::with_capacity(point_map.len());
let mut weights = Vec::with_capacity(point_map.len());
for (_, (p, w)) in point_map {
if w.abs() > 1e-16 {
points.push(p);
weights.push(w);
}
}
Ok(SmolyakGrid {
points,
weights,
n_dims,
level,
})
}
fn point_key(p: &[f64]) -> String {
p.iter()
.map(|&v| format!("{:.15e}", v))
.collect::<Vec<_>>()
.join(",")
}
pub fn smolyak_integrate(f: impl Fn(&[f64]) -> f64, grid: &SmolyakGrid) -> f64 {
grid.points
.iter()
.zip(grid.weights.iter())
.map(|(p, &w)| w * f(p))
.sum()
}
pub fn smolyak_integrate_with_error(
f: impl Fn(&[f64]) -> f64 + Clone,
n_dims: usize,
level: usize,
rule: OneDimRule,
) -> IntegrateResult<SparseGridResult> {
if level < 1 {
return Err(IntegrateError::InvalidInput(
"level must be at least 1".into(),
));
}
let grid_l = smolyak_grid(n_dims, level, &rule)?;
let val_l = smolyak_integrate(f.clone(), &grid_l);
let n_points = grid_l.n_points();
let error_estimate = if level >= 2 {
let grid_l1 = smolyak_grid(n_dims, level - 1, &rule)?;
let val_l1 = smolyak_integrate(f, &grid_l1);
(val_l - val_l1).abs()
} else {
val_l.abs() };
Ok(SparseGridResult {
value: val_l,
n_points,
error_estimate,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_smolyak_x2_plus_y2_2d() {
let result = smolyak_integrate_with_error(
|p| p[0] * p[0] + p[1] * p[1],
2,
3,
OneDimRule::ClenshawCurtis,
)
.expect("smolyak integration should succeed");
let exact = 8.0 / 3.0;
assert!(
(result.value - exact).abs() < 1e-10,
"value = {}, exact = {}, error = {}",
result.value,
exact,
(result.value - exact).abs()
);
}
#[test]
fn test_sparse_grid_point_count() {
let grid = smolyak_grid(4, 3, &OneDimRule::ClenshawCurtis)
.expect("grid construction should succeed");
assert!(
grid.n_points() < 200,
"sparse grid should have fewer points than full tensor product; got {}",
grid.n_points()
);
assert!(grid.n_points() > 0, "grid should be non-empty");
}
#[test]
fn test_level_improvement() {
let f = |p: &[f64]| p[0] * p[0] + p[1] * p[1];
let exact = 8.0 / 3.0;
let r1 = smolyak_integrate_with_error(f, 2, 1, OneDimRule::ClenshawCurtis)
.expect("level-1 integration should succeed");
let r3 = smolyak_integrate_with_error(f, 2, 3, OneDimRule::ClenshawCurtis)
.expect("level-3 integration should succeed");
let err1 = (r1.value - exact).abs();
let err3 = (r3.value - exact).abs();
assert!(
err3 <= err1 + 1e-12,
"level-3 error {} should be ≤ level-1 error {}",
err3,
err1
);
}
#[test]
fn test_gauss_legendre_rule() {
let result =
smolyak_integrate_with_error(|p| p[0].powi(4), 1, 3, OneDimRule::GaussLegendre)
.expect("GL integration should succeed");
let exact = 2.0 / 5.0;
assert!(
(result.value - exact).abs() < 1e-10,
"GL result = {}, exact = {}",
result.value,
exact
);
}
#[test]
fn test_cc_points_weights_3() {
let (x, w) = cc_points_weights(3);
assert_eq!(x.len(), 3);
assert_eq!(w.len(), 3);
let sum_w: f64 = w.iter().sum();
assert!((sum_w - 2.0).abs() < 1e-12, "sum of CC weights = {}", sum_w);
}
#[test]
fn test_gl_points_weights_5() {
let (x, w) = gl_points_weights(5);
assert_eq!(x.len(), 5);
let sum_w: f64 = w.iter().sum();
assert!((sum_w - 2.0).abs() < 1e-12, "sum of GL weights = {}", sum_w);
let val: f64 = x
.iter()
.zip(w.iter())
.map(|(&xi, &wi)| wi * xi.powi(4))
.sum();
assert!((val - 0.4).abs() < 1e-12, "GL integral of x^4 = {}", val);
}
}