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;