use crate::cubature::CubatureRule;
use crate::error::QuadratureError;
use crate::rule::QuadratureRule;
#[cfg(not(feature = "std"))]
use alloc::{vec, vec::Vec};
#[derive(Debug, Clone)]
pub struct TensorProductRule {
rule: CubatureRule,
}
impl TensorProductRule {
pub fn new(rules_1d: &[&QuadratureRule<f64>]) -> Result<Self, QuadratureError> {
if rules_1d.is_empty() {
return Err(QuadratureError::InvalidInput(
"at least one dimension required",
));
}
let dim = rules_1d.len();
let orders: Vec<usize> = rules_1d.iter().map(|r| r.order()).collect();
let total_points: usize = orders.iter().product();
let mut nodes_flat = Vec::with_capacity(total_points * dim);
let mut weights = Vec::with_capacity(total_points);
let mut indices = vec![0usize; dim];
for _ in 0..total_points {
let mut w = 1.0;
for j in 0..dim {
nodes_flat.push(rules_1d[j].nodes[indices[j]]);
w *= rules_1d[j].weights[indices[j]];
}
weights.push(w);
for j in 0..dim {
indices[j] += 1;
if indices[j] < orders[j] {
break;
}
indices[j] = 0;
}
}
Ok(Self {
rule: CubatureRule::new(nodes_flat, weights, dim),
})
}
pub fn isotropic(rule_1d: &QuadratureRule<f64>, dim: usize) -> Result<Self, QuadratureError> {
let refs: Vec<&QuadratureRule<f64>> = (0..dim).map(|_| rule_1d).collect();
Self::new(&refs)
}
#[inline]
#[must_use]
pub fn rule(&self) -> &CubatureRule {
&self.rule
}
#[inline]
#[must_use]
pub fn num_points(&self) -> usize {
self.rule.num_points()
}
#[inline]
#[must_use]
pub fn dim(&self) -> usize {
self.rule.dim()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::GaussLegendre;
#[test]
fn point_count() {
let gl5 = GaussLegendre::new(5).unwrap();
let gl3 = GaussLegendre::new(3).unwrap();
let tp = TensorProductRule::new(&[gl5.rule(), gl3.rule()]).unwrap();
assert_eq!(tp.num_points(), 15);
assert_eq!(tp.dim(), 2);
}
#[test]
fn isotropic_point_count() {
let gl = GaussLegendre::new(4).unwrap();
let tp = TensorProductRule::isotropic(gl.rule(), 3).unwrap();
assert_eq!(tp.num_points(), 64); assert_eq!(tp.dim(), 3);
}
#[test]
fn weight_sum() {
let gl = GaussLegendre::new(5).unwrap();
for d in 1..=4 {
let tp = TensorProductRule::isotropic(gl.rule(), d).unwrap();
let sum: f64 = tp.rule().weights().iter().sum();
let expected = 2.0_f64.powi(d as i32);
assert!(
(sum - expected).abs() < 1e-12,
"d={d}: sum={sum}, expected={expected}"
);
}
}
#[test]
fn separable_2d() {
let gl = GaussLegendre::new(5).unwrap();
let tp = TensorProductRule::isotropic(gl.rule(), 2).unwrap();
let result = tp
.rule()
.integrate_box(&[0.0, 0.0], &[1.0, 1.0], |x| x[0] * x[1]);
assert!((result - 0.25).abs() < 1e-14, "result={result}");
}
#[test]
fn polynomial_exactness_2d() {
let gl = GaussLegendre::new(5).unwrap();
let tp = TensorProductRule::isotropic(gl.rule(), 2).unwrap();
let result = tp.rule().integrate(|x| x[0].powi(4) * x[1].powi(4));
let expected = (2.0 / 5.0) * (2.0 / 5.0);
assert!((result - expected).abs() < 1e-13, "result={result}");
}
#[test]
fn gaussian_3d() {
let gl = GaussLegendre::new(10).unwrap();
let tp = TensorProductRule::isotropic(gl.rule(), 3).unwrap();
let result = tp
.rule()
.integrate_box(&[0.0, 0.0, 0.0], &[1.0, 1.0, 1.0], |x| {
(-(x[0] * x[0] + x[1] * x[1] + x[2] * x[2])).exp()
});
let one_d = 0.7468241328124271; let expected = one_d * one_d * one_d;
assert!(
(result - expected).abs() < 1e-10,
"result={result}, expected={expected}"
);
}
#[test]
fn mixed_rules() {
let gl5 = GaussLegendre::new(5).unwrap();
let gl3 = GaussLegendre::new(3).unwrap();
let tp = TensorProductRule::new(&[gl5.rule(), gl3.rule()]).unwrap();
let result = tp
.rule()
.integrate_box(&[0.0, 0.0], &[1.0, 1.0], |x| x[0] * x[0] + x[1] * x[1]);
assert!((result - 2.0 / 3.0).abs() < 1e-13, "result={result}");
}
#[test]
fn empty_rules() {
let result = TensorProductRule::new(&[]);
assert!(result.is_err());
}
}