Skip to main content

sparse_ir/
interpolation1d.rs

1//! 1D interpolation functionality for SparseIR
2//!
3//! This module provides efficient 1D interpolation using Legendre polynomials
4//! with pre-computed collocation matrices.
5
6use crate::gauss::{Rule, legendre_vandermonde};
7use crate::numeric::CustomNumeric;
8use mdarray::DTensor;
9use std::fmt::Debug;
10
11/// 1D interpolator with pre-computed Legendre polynomial coefficients
12///
13/// This struct stores pre-computed interpolation coefficients and domain information
14/// for efficient 1D interpolation using Legendre polynomials.
15#[derive(Debug, Clone)]
16pub struct Interpolate1D<T> {
17    /// Left boundary of interpolation domain
18    pub x_min: T,
19    /// Right boundary of interpolation domain  
20    pub x_max: T,
21    /// Pre-computed Legendre polynomial coefficients
22    pub coeffs: Vec<T>,
23    /// Gauss quadrature rule used for interpolation
24    pub gauss_rule: Rule<T>,
25}
26
27impl<T: CustomNumeric + Debug + Clone + 'static> Interpolate1D<T> {
28    /// Create a new 1D interpolator from function values and Gauss rule
29    ///
30    /// # Arguments
31    /// * `values` - Function values at Gauss points
32    /// * `gauss_rule` - Gauss quadrature rule
33    ///
34    /// # Returns
35    /// New Interpolate1D instance with pre-computed coefficients
36    pub fn new(values: &[T], gauss_rule: &Rule<T>) -> Self {
37        assert!(
38            !values.is_empty(),
39            "Cannot create interpolation from empty values"
40        );
41        assert_eq!(
42            values.len(),
43            gauss_rule.x.len(),
44            "Values length must match Gauss points"
45        );
46
47        let coeffs = interpolate_1d_legendre(values, gauss_rule);
48
49        Interpolate1D {
50            x_min: gauss_rule.a,
51            x_max: gauss_rule.b,
52            coeffs,
53            gauss_rule: gauss_rule.clone(),
54        }
55    }
56
57    /// Evaluate the interpolated function at a given point
58    ///
59    /// # Arguments
60    /// * `x` - Point to evaluate at (must be within [x_min, x_max])
61    ///
62    /// # Returns
63    /// Interpolated value at x
64    ///
65    /// # Panics
66    /// Panics if x is outside the interpolation domain [x_min, x_max]
67    pub fn evaluate(&self, x: T) -> T {
68        assert!(
69            x >= self.x_min && x <= self.x_max,
70            "Point x={:?} is outside interpolation domain [{:?}, {:?}]",
71            x,
72            self.x_min,
73            self.x_max
74        );
75
76        evaluate_interpolated_polynomial(x, &self.coeffs)
77    }
78
79    /// Get the domain boundaries
80    pub fn domain(&self) -> (T, T) {
81        (self.x_min, self.x_max)
82    }
83
84    /// Get the number of interpolation points
85    pub fn n_points(&self) -> usize {
86        self.coeffs.len()
87    }
88}
89
90/// Create Legendre collocation matrix (inverse of Vandermonde matrix)
91///
92/// This function creates a matrix C such that V * C ≈ I, where V is the
93/// Legendre Vandermonde matrix. This avoids solving linear systems during
94/// interpolation by pre-computing the inverse.
95///
96/// # Arguments
97/// * `gauss_rule` - Gauss quadrature rule containing the grid points
98///
99/// # Returns
100/// Collocation matrix C where V * C ≈ I
101pub fn legendre_collocation_matrix<T: CustomNumeric>(gauss_rule: &Rule<T>) -> DTensor<T, 2> {
102    let n = gauss_rule.x.len();
103
104    // Create Legendre Vandermonde matrix
105    let v = legendre_vandermonde(&gauss_rule.x, n - 1);
106
107    // Create normalization factors: range(0.5; length=n) in Julia
108    let invnorm: Vec<T> = (0..n)
109        .map(|i| T::from_f64_unchecked(0.5 + i as f64))
110        .collect();
111
112    // Compute: res = permutedims(V .* w) .* invnorm
113    // This is equivalent to: result[i,j] = V[j,i] * w[j] * invnorm[i]
114    DTensor::<T, 2>::from_fn([n, n], |idx| {
115        let (i, j) = (idx[0], idx[1]);
116        v[[j, i]] * gauss_rule.w[j] * invnorm[i]
117    })
118}
119
120/// 1D polynomial interpolation using Legendre collocation matrix
121///
122/// This function uses the pre-computed collocation matrix to avoid
123/// solving linear systems during interpolation.
124///
125/// # Arguments
126/// * `values` - Function values at grid points
127/// * `gauss_rule` - Gauss quadrature rule containing the grid points
128///
129/// # Returns
130/// Coefficient vector for the interpolating polynomial
131pub fn interpolate_1d_legendre<T: CustomNumeric>(values: &[T], gauss_rule: &Rule<T>) -> Vec<T> {
132    let n = values.len();
133    assert_eq!(
134        n,
135        gauss_rule.x.len(),
136        "Values length must match grid points"
137    );
138
139    // Get collocation matrix (pre-computed inverse of Vandermonde matrix)
140    let collocation_matrix = legendre_collocation_matrix(gauss_rule);
141
142    // Compute coefficients: coeffs = C * values
143    let mut coeffs = vec![T::zero(); n];
144    for i in 0..n {
145        for j in 0..n {
146            coeffs[i] = coeffs[i] + collocation_matrix[[i, j]] * values[j];
147        }
148    }
149
150    coeffs
151}
152
153/// Evaluate interpolated polynomial at point x using coefficient vector
154///
155/// # Arguments
156/// * `x` - Point to evaluate at
157/// * `coeffs` - Coefficient vector from interpolate_1d_legendre
158///
159/// # Returns
160/// Interpolated value at x
161pub fn evaluate_interpolated_polynomial<T: CustomNumeric>(x: T, coeffs: &[T]) -> T {
162    let n = coeffs.len();
163    let mut result = T::zero();
164
165    for i in 0..n {
166        result = result + coeffs[i] * evaluate_legendre_polynomial(x, i);
167    }
168
169    result
170}
171
172/// Evaluate Legendre polynomial P_n(x) using recurrence relation
173///
174/// # Arguments
175/// * `x` - Point to evaluate at
176/// * `n` - Polynomial degree
177///
178/// # Returns
179/// P_n(x)
180fn evaluate_legendre_polynomial<T: CustomNumeric>(x: T, n: usize) -> T {
181    evaluate_legendre_basis(x, n + 1)[n]
182}
183
184/// Evaluate Legendre polynomial basis functions at point x
185///
186/// Returns a vector of Legendre polynomial values [P_0(x), P_1(x), ..., P_{n-1}(x)]
187pub fn evaluate_legendre_basis<T: CustomNumeric>(x: T, n: usize) -> Vec<T> {
188    if n == 0 {
189        return Vec::new();
190    }
191
192    let mut p = Vec::with_capacity(n);
193
194    // P_0(x) = 1
195    p.push(T::from_f64_unchecked(1.0));
196
197    if n > 1 {
198        // P_1(x) = x
199        p.push(x);
200    }
201
202    // Recurrence relation: (n+1) * P_{n+1}(x) = (2n+1) * x * P_n(x) - n * P_{n-1}(x)
203    for i in 1..n - 1 {
204        let i_f64 = i as f64;
205        let next_p = (T::from_f64_unchecked(2.0 * i_f64 + 1.0) * x * p[i]
206            - T::from_f64_unchecked(i_f64) * p[i - 1])
207            / T::from_f64_unchecked(i_f64 + 1.0);
208        p.push(next_p);
209    }
210
211    p
212}
213
214#[cfg(test)]
215#[path = "interpolation1d_tests.rs"]
216mod tests;