scirs2_interpolate/physics_informed/
auto_select.rs1use crate::error::InterpolateError;
19
20#[derive(Debug, Clone)]
26pub struct DataProfile {
27 pub n_points: usize,
29 pub n_dims: usize,
31 pub smoothness_estimate: f64,
34 pub has_noise: bool,
36 pub is_periodic: bool,
38}
39
40#[non_exhaustive]
42#[derive(Debug, Clone, PartialEq)]
43pub enum InterpolationMethod {
44 LinearSpline,
46 CubicSpline,
48 RadialBasis,
50 TensorProduct,
52 SparseGrid,
54 TensorTrain,
56}
57
58impl std::fmt::Display for InterpolationMethod {
59 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 let name = match self {
61 InterpolationMethod::LinearSpline => "LinearSpline",
62 InterpolationMethod::CubicSpline => "CubicSpline",
63 InterpolationMethod::RadialBasis => "RadialBasis",
64 InterpolationMethod::TensorProduct => "TensorProduct",
65 InterpolationMethod::SparseGrid => "SparseGrid",
66 InterpolationMethod::TensorTrain => "TensorTrain",
67 };
68 write!(f, "{}", name)
69 }
70}
71
72pub fn analyze_data(x: &[Vec<f64>], y: &[f64]) -> DataProfile {
99 let n_points = x.len();
100 let n_dims = if n_points > 0 { x[0].len() } else { 0 };
101
102 if n_points < 3 || n_dims == 0 {
103 return DataProfile {
104 n_points,
105 n_dims,
106 smoothness_estimate: 0.0,
107 has_noise: false,
108 is_periodic: false,
109 };
110 }
111
112 let mut order: Vec<usize> = (0..n_points).collect();
116 order.sort_by(|&a, &b| {
117 x[a][0]
118 .partial_cmp(&x[b][0])
119 .unwrap_or(std::cmp::Ordering::Equal)
120 });
121
122 let y_sorted: Vec<f64> = order.iter().map(|&i| y[i]).collect();
123
124 let rms_y = (y_sorted.iter().map(|&v| v * v).sum::<f64>() / n_points as f64)
125 .sqrt()
126 .max(1e-12);
127
128 let second_diff_rms = if n_points >= 3 {
129 let n = y_sorted.len();
130 let ss: f64 = (1..(n - 1))
131 .map(|i| {
132 let d2 = y_sorted[i + 1] - 2.0 * y_sorted[i] + y_sorted[i - 1];
133 d2 * d2
134 })
135 .sum::<f64>();
136 (ss / (n - 2) as f64).sqrt()
137 } else {
138 0.0
139 };
140
141 let smoothness_estimate = second_diff_rms / rms_y;
142
143 let has_noise = smoothness_estimate > 0.3;
145
146 let y_max_abs = y_sorted
148 .iter()
149 .map(|v| v.abs())
150 .fold(0.0_f64, f64::max)
151 .max(1e-12);
152 let endpoint_diff = (y_sorted[0] - y_sorted[n_points - 1]).abs();
153 let is_periodic = endpoint_diff / y_max_abs < 0.05;
154
155 DataProfile {
156 n_points,
157 n_dims,
158 smoothness_estimate,
159 has_noise,
160 is_periodic,
161 }
162}
163
164pub fn recommend_method(profile: &DataProfile) -> InterpolationMethod {
172 let d = profile.n_dims;
173 let n = profile.n_points;
174
175 if d == 1 && !profile.has_noise {
176 return InterpolationMethod::CubicSpline;
177 }
178 if d > 10 {
179 return InterpolationMethod::TensorTrain;
180 }
181 if d <= 4 && n < 500 {
182 return InterpolationMethod::RadialBasis;
183 }
184 if d <= 6 && n < 10_000 {
185 return InterpolationMethod::TensorProduct;
186 }
187 if d > 6 && n > 1_000 {
188 return InterpolationMethod::SparseGrid;
189 }
190 InterpolationMethod::RadialBasis
192}
193
194pub fn recommend_with_rationale(profile: &DataProfile) -> (InterpolationMethod, String) {
197 let d = profile.n_dims;
198 let n = profile.n_points;
199
200 if d == 1 && !profile.has_noise {
201 return (
202 InterpolationMethod::CubicSpline,
203 format!(
204 "1-D data ({n} points) without noise: CubicSpline gives smooth, \
205 C² interpolation at O(n) cost."
206 ),
207 );
208 }
209 if d > 10 {
210 return (
211 InterpolationMethod::TensorTrain,
212 format!(
213 "{d}-D data ({n} points): dimensionality exceeds 10; \
214 TensorTrain (TT-SVD/TT-cross) avoids the curse of dimensionality."
215 ),
216 );
217 }
218 if d <= 4 && n < 500 {
219 return (
220 InterpolationMethod::RadialBasis,
221 format!(
222 "{d}-D scattered data ({n} points): RBF provides flexible \
223 interpolation without a grid structure."
224 ),
225 );
226 }
227 if d <= 6 && n < 10_000 {
228 return (
229 InterpolationMethod::TensorProduct,
230 format!(
231 "{d}-D data ({n} points): a tensor-product grid is feasible \
232 and gives fast O(n) evaluation per dimension."
233 ),
234 );
235 }
236 if d > 6 && n > 1_000 {
237 return (
238 InterpolationMethod::SparseGrid,
239 format!(
240 "{d}-D data ({n} points): Smolyak sparse grid reduces the \
241 exponential cost of tensor-product methods in moderate dimensions."
242 ),
243 );
244 }
245
246 (
247 InterpolationMethod::RadialBasis,
248 format!(
249 "Default choice for {d}-D data ({n} points): RBF interpolation \
250 works well for general scattered data."
251 ),
252 )
253}
254
255#[allow(dead_code)]
257pub(crate) fn validate_input(x: &[Vec<f64>], y: &[f64]) -> Result<(), InterpolateError> {
258 if x.len() != y.len() {
259 return Err(InterpolateError::DimensionMismatch(format!(
260 "x has {} points but y has {} values",
261 x.len(),
262 y.len()
263 )));
264 }
265 Ok(())
266}
267
268#[cfg(test)]
273mod tests {
274 use super::*;
275
276 fn make_1d_data(n: usize) -> (Vec<Vec<f64>>, Vec<f64>) {
277 let x: Vec<Vec<f64>> = (0..n).map(|i| vec![i as f64 / n as f64]).collect();
278 let y: Vec<f64> = x.iter().map(|p| p[0] * p[0]).collect();
279 (x, y)
280 }
281
282 fn make_nd_data(n: usize, d: usize) -> (Vec<Vec<f64>>, Vec<f64>) {
283 let x: Vec<Vec<f64>> = (0..n).map(|i| vec![i as f64 / n as f64; d]).collect();
284 let y: Vec<f64> = x.iter().map(|p| p.iter().sum::<f64>()).collect();
285 (x, y)
286 }
287
288 #[test]
289 fn test_1d_smooth_data_recommends_cubic_spline() {
290 let (x, y) = make_1d_data(50);
291 let profile = analyze_data(&x, &y);
292 assert_eq!(profile.n_dims, 1);
293 let method = recommend_method(&profile);
294 assert_eq!(method, InterpolationMethod::CubicSpline);
295 }
296
297 #[test]
298 fn test_high_dim_recommends_tensor_train() {
299 let (x, y) = make_nd_data(2000, 15);
300 let profile = analyze_data(&x, &y);
301 let method = recommend_method(&profile);
302 assert_eq!(method, InterpolationMethod::TensorTrain);
303 }
304
305 #[test]
306 fn test_moderate_dim_recommends_sparse_grid() {
307 let (x, y) = make_nd_data(2000, 8);
309 let profile = analyze_data(&x, &y);
310 let method = recommend_method(&profile);
311 assert_eq!(method, InterpolationMethod::SparseGrid);
312 }
313
314 #[test]
315 fn test_small_4d_recommends_rbf() {
316 let (x, y) = make_nd_data(100, 4);
317 let profile = analyze_data(&x, &y);
318 let method = recommend_method(&profile);
319 assert_eq!(method, InterpolationMethod::RadialBasis);
320 }
321
322 #[test]
323 fn test_recommend_with_rationale_returns_string() {
324 let (x, y) = make_1d_data(20);
325 let profile = analyze_data(&x, &y);
326 let (method, reason) = recommend_with_rationale(&profile);
327 assert_eq!(method, InterpolationMethod::CubicSpline);
328 assert!(!reason.is_empty(), "rationale string should not be empty");
329 }
330
331 #[test]
332 fn test_analyze_data_smoothness_for_noisy_data() {
333 let x: Vec<Vec<f64>> = (0..20).map(|i| vec![i as f64 * 0.1]).collect();
335 let y: Vec<f64> = (0..20)
337 .map(|i| if i % 2 == 0 { 0.0 } else { 1.0 })
338 .collect();
339 let profile = analyze_data(&x, &y);
340 assert!(
341 profile.has_noise,
342 "alternating data should be flagged as noisy"
343 );
344 }
345
346 #[test]
347 fn test_periodicity_detected() {
348 use std::f64::consts::PI;
350 let n = 65_usize; let x: Vec<Vec<f64>> = (0..n)
352 .map(|i| vec![i as f64 * 2.0 * PI / (n - 1) as f64])
353 .collect();
354 let y: Vec<f64> = x.iter().map(|p| p[0].sin()).collect();
355 let profile = analyze_data(&x, &y);
357 assert!(
358 profile.is_periodic,
359 "sin data on [0,2π] should be detected as periodic; y[0]={:.4}, y[last]={:.4}",
360 y[0],
361 y[n - 1]
362 );
363 }
364
365 #[test]
366 fn test_empty_data_no_panic() {
367 let profile = analyze_data(&[], &[]);
368 assert_eq!(profile.n_points, 0);
369 assert_eq!(profile.n_dims, 0);
370 }
371}