1use crate::error::{IntegrateError, IntegrateResult};
23use crate::IntegrateFloat;
24use scirs2_core::ndarray::Array1;
25use std::collections::HashMap;
26
27#[inline(always)]
29fn to_f<F: IntegrateFloat>(value: f64) -> F {
30 F::from_f64(value).unwrap_or_else(|| F::zero())
31}
32
33#[derive(Debug, Clone)]
39pub struct OneDRule {
40 pub nodes: Vec<f64>,
42 pub weights: Vec<f64>,
44}
45
46pub trait OneDRuleFamily: Send + Sync {
48 fn rule(&self, level: usize) -> IntegrateResult<OneDRule>;
51}
52
53#[derive(Debug, Clone, Copy)]
55pub struct ClenshawCurtisFamily;
56
57impl OneDRuleFamily for ClenshawCurtisFamily {
58 fn rule(&self, level: usize) -> IntegrateResult<OneDRule> {
59 if level == 0 {
60 return Err(IntegrateError::ValueError("Level must be >= 1".into()));
61 }
62 let n = if level == 1 { 1 } else { 1 << (level - 1) };
64 cc_rule_f64(n)
65 }
66}
67
68#[derive(Debug, Clone, Copy)]
70pub struct GaussLegendreFamily;
71
72impl OneDRuleFamily for GaussLegendreFamily {
73 fn rule(&self, level: usize) -> IntegrateResult<OneDRule> {
74 if level == 0 {
75 return Err(IntegrateError::ValueError("Level must be >= 1".into()));
76 }
77 gl_rule_f64(level)
79 }
80}
81
82fn cc_rule_f64(n: usize) -> IntegrateResult<OneDRule> {
88 if n == 0 {
89 return Ok(OneDRule {
91 nodes: vec![0.0],
92 weights: vec![2.0],
93 });
94 }
95 if n == 1 {
96 return Ok(OneDRule {
97 nodes: vec![-1.0, 0.0, 1.0],
98 weights: vec![1.0 / 3.0, 4.0 / 3.0, 1.0 / 3.0],
99 });
100 }
101
102 let nf = n as f64;
103 let pi = std::f64::consts::PI;
104 let mut nodes = Vec::with_capacity(n + 1);
105 let mut weights = Vec::with_capacity(n + 1);
106
107 for j in 0..=n {
108 let theta = j as f64 * pi / nf;
109 nodes.push(theta.cos());
110 }
111
112 let half_n = n / 2;
113 for j in 0..=n {
114 let c_j: f64 = if j == 0 || j == n { 1.0 } else { 2.0 };
115 let theta_j = j as f64 * pi / nf;
116 let mut s = 0.0_f64;
117 for k in 1..=half_n {
118 let b_k: f64 = if k < half_n || (!n.is_multiple_of(2) && k == half_n) {
119 2.0
120 } else {
121 1.0
122 };
123 let denom = (4 * k * k) as f64 - 1.0;
124 s += b_k / denom * (2.0 * k as f64 * theta_j).cos();
125 }
126 weights.push(c_j / nf * (1.0 - s));
127 }
128
129 Ok(OneDRule { nodes, weights })
130}
131
132fn gl_rule_f64(n: usize) -> IntegrateResult<OneDRule> {
134 if n == 0 {
135 return Err(IntegrateError::ValueError(
136 "Number of GL points must be >= 1".into(),
137 ));
138 }
139 if n == 1 {
140 return Ok(OneDRule {
141 nodes: vec![0.0],
142 weights: vec![2.0],
143 });
144 }
145
146 let pi = std::f64::consts::PI;
147 let mut nodes = vec![0.0_f64; n];
148 let mut weights = vec![0.0_f64; n];
149
150 let m = n.div_ceil(2);
152 for i in 0..m {
153 let mut z = ((i as f64 + 0.75) / (n as f64 + 0.5) * pi).cos();
155
156 for _ in 0..100 {
157 let mut p0 = 1.0_f64;
158 let mut p1 = z;
159 for k in 2..=n {
160 let kf = k as f64;
161 let p2 = ((2.0 * kf - 1.0) * z * p1 - (kf - 1.0) * p0) / kf;
162 p0 = p1;
163 p1 = p2;
164 }
165 let nf = n as f64;
167 let dp = nf * (z * p1 - p0) / (z * z - 1.0);
168 let delta = p1 / dp;
169 z -= delta;
170 if delta.abs() < 1e-15 {
171 break;
172 }
173 }
174
175 nodes[i] = -z;
176 nodes[n - 1 - i] = z;
177
178 let mut p0 = 1.0_f64;
180 let mut p1 = z;
181 for k in 2..=n {
182 let kf = k as f64;
183 let p2 = ((2.0 * kf - 1.0) * z * p1 - (kf - 1.0) * p0) / kf;
184 p0 = p1;
185 p1 = p2;
186 }
187 let nf = n as f64;
188 let dp = nf * (z * p1 - p0) / (z * z - 1.0);
189 let w = 2.0 / ((1.0 - z * z) * dp * dp);
190 weights[i] = w;
191 weights[n - 1 - i] = w;
192 }
193
194 Ok(OneDRule { nodes, weights })
195}
196
197#[derive(Debug, Clone)]
203pub struct SparseGridResult<F: IntegrateFloat> {
204 pub value: F,
206 pub n_points: usize,
208 pub level: usize,
210 pub dim: usize,
212}
213
214#[derive(Debug, Clone)]
216pub struct SparseGridOptions {
217 pub level: usize,
219 pub rule_family: SparseGridRuleFamily,
221}
222
223#[derive(Debug, Clone, Copy, PartialEq)]
225pub enum SparseGridRuleFamily {
226 ClenshawCurtis,
228 GaussLegendre,
230}
231
232impl Default for SparseGridOptions {
233 fn default() -> Self {
234 Self {
235 level: 4,
236 rule_family: SparseGridRuleFamily::ClenshawCurtis,
237 }
238 }
239}
240
241fn enumerate_multi_indices(d: usize, q: usize) -> Vec<Vec<usize>> {
243 if d == 0 {
244 return if q == 0 { vec![vec![]] } else { vec![] };
245 }
246 if d == 1 {
247 return if q >= 1 { vec![vec![q]] } else { vec![] };
248 }
249 let mut result = Vec::new();
250 let max_first = if q >= d { q - d + 1 } else { return result };
252 for a0 in 1..=max_first {
253 let sub = enumerate_multi_indices(d - 1, q - a0);
254 for mut tail in sub {
255 let mut row = Vec::with_capacity(d);
256 row.push(a0);
257 row.append(&mut tail);
258 result.push(row);
259 }
260 }
261 result
262}
263
264pub fn sparse_grid_quad<F, Func>(
288 f: Func,
289 ranges: &[(F, F)],
290 options: Option<SparseGridOptions>,
291) -> IntegrateResult<SparseGridResult<F>>
292where
293 F: IntegrateFloat,
294 Func: Fn(&Array1<F>) -> F,
295{
296 let opts = options.unwrap_or_default();
297 let d = ranges.len();
298 if d == 0 {
299 return Err(IntegrateError::ValueError(
300 "At least one dimension required".into(),
301 ));
302 }
303 let level = opts.level;
304 if level == 0 {
305 return Err(IntegrateError::ValueError("Level must be >= 1".into()));
306 }
307
308 let max_level_1d = level; let family: Box<dyn OneDRuleFamily> = match opts.rule_family {
311 SparseGridRuleFamily::ClenshawCurtis => Box::new(ClenshawCurtisFamily),
312 SparseGridRuleFamily::GaussLegendre => Box::new(GaussLegendreFamily),
313 };
314
315 let mut rules_1d = Vec::with_capacity(max_level_1d + 1);
316 rules_1d.push(OneDRule {
317 nodes: vec![],
318 weights: vec![],
319 }); for lv in 1..=max_level_1d {
321 rules_1d.push(family.rule(lv)?);
322 }
323
324 let mut point_weights: HashMap<Vec<i64>, (Array1<F>, F)> = HashMap::new();
333 let mut total_points_used: usize = 0;
334
335 let q_min = if level >= d { level - d + 1 } else { 1 };
336 let q_max = level;
337
338 for q in q_min..=q_max {
339 let multi_indices = enumerate_multi_indices(d, q);
340 let sign: f64 = if (level - q).is_multiple_of(2) {
341 1.0
342 } else {
343 -1.0
344 };
345 let binom = binomial_coeff(d - 1, level - q);
346 let coeff = sign * binom as f64;
347
348 for alpha in &multi_indices {
349 add_tensor_product(
352 &rules_1d,
353 alpha,
354 ranges,
355 to_f::<F>(coeff),
356 &mut point_weights,
357 &mut total_points_used,
358 )?;
359 }
360 }
361
362 let mut integral = F::zero();
364 for (pt, w) in point_weights.values() {
365 integral += *w * f(pt);
366 }
367
368 Ok(SparseGridResult {
369 value: integral,
370 n_points: point_weights.len(),
371 level,
372 dim: d,
373 })
374}
375
376fn add_tensor_product<F: IntegrateFloat>(
378 rules: &[OneDRule],
379 alpha: &[usize],
380 ranges: &[(F, F)],
381 coeff: F,
382 map: &mut HashMap<Vec<i64>, (Array1<F>, F)>,
383 total: &mut usize,
384) -> IntegrateResult<()> {
385 let d = alpha.len();
386 let half = 0.5_f64;
387
388 let mut dim_nodes: Vec<Vec<f64>> = Vec::with_capacity(d);
390 let mut dim_weights: Vec<Vec<f64>> = Vec::with_capacity(d);
391
392 for (i, &lv) in alpha.iter().enumerate() {
393 let rule = &rules[lv];
394 let (a_f64, b_f64) = (
395 ranges[i]
396 .0
397 .to_f64()
398 .ok_or_else(|| IntegrateError::ComputationError("f64 conversion".into()))?,
399 ranges[i]
400 .1
401 .to_f64()
402 .ok_or_else(|| IntegrateError::ComputationError("f64 conversion".into()))?,
403 );
404 let mid = (a_f64 + b_f64) * half;
405 let half_len = (b_f64 - a_f64) * half;
406
407 let mapped_nodes: Vec<f64> = rule.nodes.iter().map(|&x| mid + half_len * x).collect();
408 let mapped_weights: Vec<f64> = rule.weights.iter().map(|&w| w * half_len).collect();
409 dim_nodes.push(mapped_nodes);
410 dim_weights.push(mapped_weights);
411 }
412
413 let sizes: Vec<usize> = dim_nodes.iter().map(|v| v.len()).collect();
415 let total_size: usize = sizes.iter().product();
416 *total += total_size;
417
418 let mut indices = vec![0_usize; d];
419 for _ in 0..total_size {
420 let mut w_prod = 1.0_f64;
422 let mut point_f64 = vec![0.0_f64; d];
423 for k in 0..d {
424 point_f64[k] = dim_nodes[k][indices[k]];
425 w_prod *= dim_weights[k][indices[k]];
426 }
427
428 let key: Vec<i64> = point_f64
430 .iter()
431 .map(|&x| (x * 1e14).round() as i64)
432 .collect();
433
434 let w_contrib: F = coeff * F::from_f64(w_prod).unwrap_or_else(|| F::zero());
435
436 let entry = map.entry(key).or_insert_with(|| {
437 let pt_arr = Array1::from_vec(
438 point_f64
439 .iter()
440 .map(|&v| F::from_f64(v).unwrap_or_else(|| F::zero()))
441 .collect(),
442 );
443 (pt_arr, F::zero())
444 });
445 entry.1 += w_contrib;
446
447 let mut carry = true;
449 for k in (0..d).rev() {
450 if carry {
451 indices[k] += 1;
452 if indices[k] >= sizes[k] {
453 indices[k] = 0;
454 } else {
455 carry = false;
456 }
457 }
458 }
459 }
460
461 Ok(())
462}
463
464fn binomial_coeff(n: usize, k: usize) -> usize {
466 if k > n {
467 return 0;
468 }
469 if k == 0 || k == n {
470 return 1;
471 }
472 let k = k.min(n - k); let mut result: usize = 1;
474 for i in 0..k {
475 result = result * (n - i) / (i + 1);
476 }
477 result
478}
479
480#[cfg(test)]
485mod tests {
486 use super::*;
487 use scirs2_core::ndarray::Array1;
488
489 #[test]
490 fn test_1d_constant() {
491 let res = sparse_grid_quad(
493 |_x: &Array1<f64>| 1.0,
494 &[(0.0, 1.0)],
495 Some(SparseGridOptions {
496 level: 2,
497 ..Default::default()
498 }),
499 )
500 .expect("sparse_grid_quad");
501 assert!(
502 (res.value - 1.0).abs() < 1e-14,
503 "1D constant: got {}",
504 res.value
505 );
506 }
507
508 #[test]
509 fn test_2d_polynomial() {
510 let res = sparse_grid_quad(
512 |x: &Array1<f64>| x[0] * x[1],
513 &[(0.0, 1.0), (0.0, 1.0)],
514 Some(SparseGridOptions {
515 level: 3,
516 ..Default::default()
517 }),
518 )
519 .expect("sparse_grid_quad");
520 assert!(
521 (res.value - 0.25).abs() < 1e-10,
522 "2D x*y: got {}",
523 res.value
524 );
525 }
526
527 #[test]
528 fn test_3d_polynomial() {
529 let res = sparse_grid_quad(
531 |x: &Array1<f64>| x[0] * x[1] * x[2],
532 &[(0.0, 1.0), (0.0, 1.0), (0.0, 1.0)],
533 Some(SparseGridOptions {
534 level: 3,
535 ..Default::default()
536 }),
537 )
538 .expect("sparse_grid_quad");
539 assert!(
540 (res.value - 0.125).abs() < 1e-10,
541 "3D x*y*z: got {}",
542 res.value
543 );
544 }
545
546 #[test]
547 fn test_2d_gaussian() {
548 let res = sparse_grid_quad(
552 |x: &Array1<f64>| (-x[0] * x[0] - x[1] * x[1]).exp(),
553 &[(-3.0, 3.0), (-3.0, 3.0)],
554 Some(SparseGridOptions {
555 level: 8,
556 ..Default::default()
557 }),
558 )
559 .expect("sparse_grid_quad");
560 assert!(
561 (res.value - std::f64::consts::PI).abs() < 0.02,
562 "2D Gaussian: got {}, expected pi",
563 res.value
564 );
565 }
566
567 #[test]
568 fn test_gauss_legendre_family() {
569 let res = sparse_grid_quad(
571 |x: &Array1<f64>| x[0] * x[1],
572 &[(0.0, 1.0), (0.0, 1.0)],
573 Some(SparseGridOptions {
574 level: 3,
575 rule_family: SparseGridRuleFamily::GaussLegendre,
576 }),
577 )
578 .expect("sparse_grid_quad");
579 assert!(
580 (res.value - 0.25).abs() < 1e-10,
581 "GL 2D x*y: got {}",
582 res.value
583 );
584 }
585
586 #[test]
587 fn test_enumerate_multi_indices() {
588 let mi = enumerate_multi_indices(2, 3);
590 assert_eq!(mi.len(), 2);
591 }
592
593 #[test]
594 fn test_binomial() {
595 assert_eq!(binomial_coeff(5, 2), 10);
596 assert_eq!(binomial_coeff(4, 0), 1);
597 assert_eq!(binomial_coeff(4, 4), 1);
598 assert_eq!(binomial_coeff(0, 0), 1);
599 assert_eq!(binomial_coeff(3, 5), 0);
600 }
601
602 #[test]
603 fn test_high_dim_constant() {
604 let ranges: Vec<(f64, f64)> = (0..5).map(|_| (0.0, 1.0)).collect();
607 let res = sparse_grid_quad(
608 |_x: &Array1<f64>| 1.0,
609 &ranges,
610 Some(SparseGridOptions {
611 level: 6,
612 ..Default::default()
613 }),
614 )
615 .expect("sparse_grid_quad");
616 assert!(
617 (res.value - 1.0).abs() < 1e-10,
618 "5D constant: got {}",
619 res.value
620 );
621 }
622
623 #[test]
624 fn test_non_unit_domain() {
625 let res = sparse_grid_quad(
627 |_x: &Array1<f64>| 1.0,
628 &[(2.0, 5.0), (2.0, 5.0)],
629 Some(SparseGridOptions {
630 level: 2,
631 ..Default::default()
632 }),
633 )
634 .expect("sparse_grid_quad");
635 assert!(
636 (res.value - 9.0).abs() < 1e-10,
637 "non-unit domain: got {}",
638 res.value
639 );
640 }
641
642 #[test]
643 fn test_invalid_level() {
644 let res = sparse_grid_quad(
645 |_x: &Array1<f64>| 1.0,
646 &[(0.0, 1.0)],
647 Some(SparseGridOptions {
648 level: 0,
649 ..Default::default()
650 }),
651 );
652 assert!(res.is_err(), "level 0 should be invalid");
653 }
654}