1use scirs2_core::ndarray::{ArrayView1, ArrayView2};
7use scirs2_core::numeric::{Float, FromPrimitive};
8use std::fmt::{Debug, Display};
9use std::ops::{AddAssign, SubAssign};
10
11use super::types::{BoundaryAnalysis, DataPointsAnalysis, FunctionValuesAnalysis};
12use crate::error::{InterpolateError, InterpolateResult};
13
14pub fn suggest_data_based_regularization<F>(min_distance: F, distance_ratio: F) -> F
16where
17 F: Float + FromPrimitive + Debug + Display + AddAssign + SubAssign,
18{
19 let base_reg = super::types::machine_epsilon::<F>()
20 * F::from(1000.0)
21 .unwrap_or_else(|| F::from(1000.0).expect("Failed to convert constant to float"));
22
23 let spacing_factor = if min_distance > F::zero() {
25 F::one() / min_distance.sqrt()
26 } else {
27 F::from(1e6).unwrap_or_else(|| F::from(1e6).expect("Failed to convert constant to float"))
28 };
29
30 let ratio_factor = if distance_ratio > F::one() {
32 distance_ratio.ln().max(F::one())
33 } else {
34 F::one()
35 };
36
37 base_reg * spacing_factor * ratio_factor
38}
39
40pub fn analyze_interpolation_data<F>(
42 points: &ArrayView2<F>,
43 values: &ArrayView1<F>,
44) -> InterpolateResult<InterpolationDataReport<F>>
45where
46 F: Float + FromPrimitive + Debug + Display + AddAssign + SubAssign,
47{
48 if points.nrows() != values.len() {
49 return Err(InterpolateError::ShapeMismatch {
50 expected: format!("{} points", values.len()),
51 actual: format!("{} points", points.nrows()),
52 object: "data analysis".to_string(),
53 });
54 }
55
56 let data_points = super::edge_cases::analyze_data_points(points)?;
57 let function_values = super::edge_cases::analyze_function_values(values)?;
58 let boundary = super::edge_cases::analyze_boundary_conditions(points, values)?;
59
60 let scaling_analysis = analyze_data_scaling(points, values)?;
62 let noise_analysis = analyze_noise_characteristics(values)?;
63 let interpolation_method_recommendation =
64 recommend_interpolation_method(&data_points, &function_values)?;
65
66 Ok(InterpolationDataReport {
67 data_points,
68 function_values,
69 boundary,
70 scaling_analysis,
71 noise_analysis,
72 interpolation_method_recommendation,
73 })
74}
75
76#[derive(Debug, Clone)]
78pub struct InterpolationDataReport<F>
79where
80 F: Float + FromPrimitive + Debug + Display + AddAssign + SubAssign,
81{
82 pub data_points: DataPointsAnalysis<F>,
83 pub function_values: FunctionValuesAnalysis<F>,
84 pub boundary: BoundaryAnalysis<F>,
85 pub scaling_analysis: DataScalingAnalysis<F>,
86 pub noise_analysis: NoiseAnalysis<F>,
87 pub interpolation_method_recommendation: InterpolationMethodRecommendation,
88}
89
90#[derive(Debug, Clone)]
92pub struct DataScalingAnalysis<F>
93where
94 F: Float + FromPrimitive + Debug + Display + AddAssign + SubAssign,
95{
96 pub coordinate_scale: F,
98 pub value_scale: F,
100 pub scaling_recommended: bool,
102 pub condition_improvement_factor: F,
104}
105
106#[derive(Debug, Clone)]
108pub struct NoiseAnalysis<F>
109where
110 F: Float + FromPrimitive + Debug + Display + AddAssign + SubAssign,
111{
112 pub estimated_noise_level: F,
114 pub signal_noise_ratio: F,
116 pub is_noisy: bool,
118 pub denoising_strategy: DenoisingStrategy,
120}
121
122#[derive(Debug, Clone, Copy, PartialEq)]
124pub enum DenoisingStrategy {
125 None,
127 SmoothingSplines,
129 RobustInterpolation,
131 WaveletDenoising,
133 TotalVariationRegularization,
135}
136
137#[derive(Debug, Clone)]
139pub struct InterpolationMethodRecommendation {
140 pub primary_method: InterpolationMethod,
142 pub alternative_methods: Vec<InterpolationMethod>,
144 pub recommendation_reason: String,
146}
147
148#[derive(Debug, Clone, Copy, PartialEq)]
150pub enum InterpolationMethod {
151 Linear,
153 CubicSpline,
155 BSpline,
157 NURBS,
159 RadialBasisFunction,
161 Kriging,
163 ThinPlateSpline,
165 PiecewisePolynomial,
167}
168
169fn analyze_data_scaling<F>(
171 points: &ArrayView2<F>,
172 values: &ArrayView1<F>,
173) -> InterpolateResult<DataScalingAnalysis<F>>
174where
175 F: Float + FromPrimitive + Debug + Display + AddAssign + SubAssign,
176{
177 let num_points = points.nrows();
178 let dimension = points.ncols();
179
180 if num_points == 0 {
181 return Ok(DataScalingAnalysis {
182 coordinate_scale: F::one(),
183 value_scale: F::one(),
184 scaling_recommended: false,
185 condition_improvement_factor: F::one(),
186 });
187 }
188
189 let mut coord_ranges = Vec::new();
191 for j in 0..dimension {
192 let mut min_coord = F::infinity();
193 let mut max_coord = F::neg_infinity();
194 for i in 0..num_points {
195 let coord = points[(i, j)];
196 min_coord = min_coord.min(coord);
197 max_coord = max_coord.max(coord);
198 }
199 coord_ranges.push(max_coord - min_coord);
200 }
201
202 let max_coord_range = coord_ranges.iter().fold(F::zero(), |acc, &x| acc.max(x));
203 let coordinate_scale = if max_coord_range > F::zero() {
204 F::one() / max_coord_range
205 } else {
206 F::one()
207 };
208
209 let min_value = values.iter().fold(F::infinity(), |acc, &x| acc.min(x));
211 let max_value = values.iter().fold(F::neg_infinity(), |acc, &x| acc.max(x));
212 let value_range = max_value - min_value;
213 let value_scale = if value_range > F::zero() {
214 F::one() / value_range
215 } else {
216 F::one()
217 };
218
219 let coord_scale_factor = max_coord_range;
221 let value_scale_factor = value_range;
222 let scaling_recommended = coord_scale_factor
223 >= F::from(1000.0)
224 .unwrap_or_else(|| F::from(1000.0).expect("Failed to convert constant to float"))
225 || coord_scale_factor
226 <= F::from(0.001)
227 .unwrap_or_else(|| F::from(0.001).expect("Failed to convert constant to float"))
228 || value_scale_factor
229 >= F::from(1000.0)
230 .unwrap_or_else(|| F::from(1000.0).expect("Failed to convert constant to float"))
231 || value_scale_factor
232 <= F::from(0.001)
233 .unwrap_or_else(|| F::from(0.001).expect("Failed to convert constant to float"));
234
235 let condition_improvement_factor = if scaling_recommended {
237 let coord_improvement = coord_scale_factor.min(F::one() / coord_scale_factor);
238 let value_improvement = value_scale_factor.min(F::one() / value_scale_factor);
239 coord_improvement * value_improvement
240 } else {
241 F::one()
242 };
243
244 Ok(DataScalingAnalysis {
245 coordinate_scale,
246 value_scale,
247 scaling_recommended,
248 condition_improvement_factor,
249 })
250}
251
252pub fn analyze_noise_characteristics<F>(
254 values: &ArrayView1<F>,
255) -> InterpolateResult<NoiseAnalysis<F>>
256where
257 F: Float + FromPrimitive + Debug + Display + AddAssign + SubAssign,
258{
259 let n = values.len();
260 if n < 3 {
261 return Ok(NoiseAnalysis {
262 estimated_noise_level: F::zero(),
263 signal_noise_ratio: F::infinity(),
264 is_noisy: false,
265 denoising_strategy: DenoisingStrategy::None,
266 });
267 }
268
269 let mut differences = Vec::new();
271 for i in 1..n {
272 differences.push(values[i] - values[i - 1]);
273 }
274
275 let mut second_differences = Vec::new();
277 for i in 1..(n - 1) {
278 let second_diff = values[i + 1]
279 - F::from(2.0).expect("Failed to convert constant to float") * values[i]
280 + values[i - 1];
281 second_differences.push(second_diff.abs());
282 }
283
284 let mut sorted_second_diffs = second_differences.clone();
286 sorted_second_diffs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
287
288 let estimated_noise_level = if !sorted_second_diffs.is_empty() {
289 let median_idx = sorted_second_diffs.len() / 2;
290 sorted_second_diffs[median_idx]
291 / F::from(1.4826)
292 .unwrap_or_else(|| F::from(1.4826).expect("Failed to convert constant to float"))
293 } else {
295 F::zero()
296 };
297
298 let signal_range = values.iter().fold(F::neg_infinity(), |acc, &x| acc.max(x))
300 - values.iter().fold(F::infinity(), |acc, &x| acc.min(x));
301
302 let signal_noise_ratio = if estimated_noise_level > F::zero() {
303 signal_range / estimated_noise_level
304 } else {
305 F::infinity()
306 };
307
308 let noise_threshold = F::from(10.0)
310 .unwrap_or_else(|| F::from(10.0).expect("Failed to convert constant to float"));
311 let is_noisy = signal_noise_ratio < noise_threshold;
312
313 let denoising_strategy = if !is_noisy {
315 DenoisingStrategy::None
316 } else if signal_noise_ratio
317 > F::from(5.0).unwrap_or_else(|| F::from(5.0).expect("Failed to convert constant to float"))
318 {
319 DenoisingStrategy::SmoothingSplines
320 } else if signal_noise_ratio
321 > F::from(2.0).unwrap_or_else(|| F::from(2.0).expect("Failed to convert constant to float"))
322 {
323 DenoisingStrategy::RobustInterpolation
324 } else {
325 DenoisingStrategy::WaveletDenoising
326 };
327
328 Ok(NoiseAnalysis {
329 estimated_noise_level,
330 signal_noise_ratio,
331 is_noisy,
332 denoising_strategy,
333 })
334}
335
336fn recommend_interpolation_method<F>(
338 data_points: &DataPointsAnalysis<F>,
339 function_values: &FunctionValuesAnalysis<F>,
340) -> InterpolateResult<InterpolationMethodRecommendation>
341where
342 F: Float + FromPrimitive + Debug + Display + AddAssign + SubAssign,
343{
344 let mut primary_method = InterpolationMethod::CubicSpline;
345 let mut alternative_methods = Vec::new();
346 let mut reasons = Vec::new();
347
348 if data_points.num_points < 10 {
350 primary_method = InterpolationMethod::Linear;
351 alternative_methods.push(InterpolationMethod::CubicSpline);
352 reasons.push("Few data points favor simpler methods".to_string());
353 } else if data_points.num_points > 1000 {
354 primary_method = InterpolationMethod::BSpline;
355 alternative_methods.push(InterpolationMethod::ThinPlateSpline);
356 reasons.push("Large datasets benefit from B-spline efficiency".to_string());
357 }
358
359 if !function_values.is_smooth {
361 if function_values.has_outliers {
362 if data_points.num_points >= 10 {
363 primary_method = InterpolationMethod::RadialBasisFunction;
364 alternative_methods.push(InterpolationMethod::Kriging);
365 reasons.push("Non-smooth functions with outliers need robust methods".to_string());
366 }
367 } else if data_points.num_points >= 10 {
368 primary_method = InterpolationMethod::PiecewisePolynomial;
369 alternative_methods.push(InterpolationMethod::BSpline);
370 reasons.push("Non-smooth functions benefit from piecewise approaches".to_string());
371 }
372 } else if function_values.is_monotonic && data_points.num_points >= 10 {
373 primary_method = InterpolationMethod::CubicSpline;
374 alternative_methods.push(InterpolationMethod::BSpline);
375 reasons.push("Smooth monotonic functions are ideal for spline interpolation".to_string());
376 }
377
378 if data_points.is_collinear {
380 primary_method = InterpolationMethod::Linear;
381 alternative_methods.push(InterpolationMethod::CubicSpline);
382 reasons.push("Collinear points suggest 1D interpolation".to_string());
383 } else if data_points.clustering_score
384 > F::from(0.7).unwrap_or_else(|| F::from(0.7).expect("Failed to convert constant to float"))
385 {
386 primary_method = InterpolationMethod::Kriging;
387 alternative_methods.push(InterpolationMethod::RadialBasisFunction);
388 reasons.push("Clustered data benefits from spatial interpolation methods".to_string());
389 }
390
391 if alternative_methods.is_empty() {
393 match primary_method {
394 InterpolationMethod::Linear => {
395 alternative_methods.push(InterpolationMethod::CubicSpline);
396 alternative_methods.push(InterpolationMethod::BSpline);
397 }
398 InterpolationMethod::CubicSpline => {
399 alternative_methods.push(InterpolationMethod::BSpline);
400 alternative_methods.push(InterpolationMethod::ThinPlateSpline);
401 }
402 InterpolationMethod::BSpline => {
403 alternative_methods.push(InterpolationMethod::CubicSpline);
404 alternative_methods.push(InterpolationMethod::NURBS);
405 }
406 _ => {
407 alternative_methods.push(InterpolationMethod::CubicSpline);
408 alternative_methods.push(InterpolationMethod::BSpline);
409 }
410 }
411 }
412
413 let recommendation_reason = if reasons.is_empty() {
414 "Based on standard interpolation guidelines".to_string()
415 } else {
416 reasons.join("; ")
417 };
418
419 Ok(InterpolationMethodRecommendation {
420 primary_method,
421 alternative_methods,
422 recommendation_reason,
423 })
424}
425
426pub fn analyze_sampling_density<F>(
428 points: &ArrayView2<F>,
429 target_accuracy: F,
430) -> InterpolateResult<SamplingDensityAnalysis<F>>
431where
432 F: Float + FromPrimitive + Debug + Display + AddAssign + SubAssign,
433{
434 let num_points = points.nrows();
435 let dimension = points.ncols();
436
437 if num_points < 2 {
438 return Ok(SamplingDensityAnalysis {
439 current_density: F::zero(),
440 recommended_density: F::one(),
441 density_adequate: false,
442 suggested_additional_points: 10,
443 sampling_strategy: SamplingStrategy::Uniform,
444 });
445 }
446
447 let (min_distance, max_distance, _) = super::edge_cases::analyze_point_distances(points)?;
449 let current_density = F::one() / min_distance.max(super::types::machine_epsilon::<F>());
450
451 let accuracy_factor = F::one() / target_accuracy.max(super::types::machine_epsilon::<F>());
453 let recommended_density = current_density * accuracy_factor.sqrt();
454
455 let density_adequate = current_density
457 >= recommended_density
458 * F::from(0.5)
459 .unwrap_or_else(|| F::from(0.5).expect("Failed to convert constant to float"));
460
461 let suggested_additional_points = if density_adequate {
463 0
464 } else {
465 let density_ratio = recommended_density / current_density;
466 (num_points as f64 * (density_ratio.to_f64().unwrap_or(2.0) - 1.0)).ceil() as usize
467 };
468
469 let distance_ratio = if min_distance > F::zero() {
471 max_distance / min_distance
472 } else {
473 F::infinity()
474 };
475
476 let sampling_strategy = if distance_ratio
477 > F::from(100.0)
478 .unwrap_or_else(|| F::from(100.0).expect("Failed to convert constant to float"))
479 {
480 SamplingStrategy::Adaptive
481 } else if dimension > 2 {
482 SamplingStrategy::QuasiRandom
483 } else {
484 SamplingStrategy::Uniform
485 };
486
487 Ok(SamplingDensityAnalysis {
488 current_density,
489 recommended_density,
490 density_adequate,
491 suggested_additional_points,
492 sampling_strategy,
493 })
494}
495
496#[derive(Debug, Clone)]
498pub struct SamplingDensityAnalysis<F>
499where
500 F: Float + FromPrimitive + Debug + Display + AddAssign + SubAssign,
501{
502 pub current_density: F,
503 pub recommended_density: F,
504 pub density_adequate: bool,
505 pub suggested_additional_points: usize,
506 pub sampling_strategy: SamplingStrategy,
507}
508
509#[derive(Debug, Clone, Copy, PartialEq)]
511pub enum SamplingStrategy {
512 Uniform,
514 Adaptive,
516 QuasiRandom,
518 LatinHypercube,
520 ImportanceSampling,
522}
523
524#[cfg(test)]
525mod tests {
526 use super::*;
527 use scirs2_core::ndarray::{Array1, Array2};
528
529 #[test]
530 fn test_data_scaling_analysis() {
531 let points = Array2::from_shape_vec(
532 (4, 2),
533 vec![0.0, 0.0, 1000.0, 0.0, 0.0, 1000.0, 1000.0, 1000.0],
534 )
535 .expect("Operation failed");
536 let values = Array1::from_vec(vec![0.0, 0.001, 0.001, 0.002]);
537
538 let scaling =
539 analyze_data_scaling(&points.view(), &values.view()).expect("Operation failed");
540 assert!(scaling.scaling_recommended);
541 assert!(scaling.coordinate_scale < 1.0);
542 assert!(scaling.value_scale > 1.0);
543 }
544
545 #[test]
546 fn test_noise_analysis() {
547 let smooth_values = Array1::from_vec(vec![0.0, 1.0, 2.0, 3.0, 4.0]);
549 let smooth_analysis =
550 analyze_noise_characteristics(&smooth_values.view()).expect("Operation failed");
551 assert!(!smooth_analysis.is_noisy);
552 assert_eq!(smooth_analysis.denoising_strategy, DenoisingStrategy::None);
553
554 let noisy_values = Array1::from_vec(vec![0.0, 1.1, 1.9, 3.05, 3.95]);
556 let noisy_analysis =
557 analyze_noise_characteristics(&noisy_values.view()).expect("Operation failed");
558 assert!(noisy_analysis.estimated_noise_level > 0.0);
559 }
560
561 #[test]
562 fn test_interpolation_method_recommendation() {
563 let small_data = DataPointsAnalysis {
565 num_points: 5,
566 min_distance: 1.0,
567 max_distance: 4.0,
568 distance_ratio: 4.0,
569 is_collinear: false,
570 clustering_score: 0.3,
571 has_linear_dependencies: false,
572 };
573
574 let smooth_function = FunctionValuesAnalysis {
575 value_range: 10.0,
576 is_smooth: true,
577 smoothness_score: 0.9,
578 is_monotonic: true,
579 has_outliers: false,
580 recommended_smoothing: None,
581 };
582
583 let recommendation = recommend_interpolation_method(&small_data, &smooth_function)
584 .expect("Operation failed");
585 assert_eq!(recommendation.primary_method, InterpolationMethod::Linear);
586 }
587
588 #[test]
589 fn test_sampling_density_analysis() {
590 let points = Array2::from_shape_vec((4, 2), vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0])
591 .expect("Operation failed");
592
593 let analysis = analyze_sampling_density(&points.view(), 0.01).expect("Operation failed");
594 assert!(analysis.current_density > 0.0);
595 assert!(analysis.recommended_density > 0.0);
596 }
597
598 #[test]
599 fn test_complete_data_analysis() {
600 let points = Array2::from_shape_vec(
601 (5, 2),
602 vec![0.0, 0.0, 1.0, 0.0, 2.0, 0.0, 3.0, 0.0, 4.0, 0.0],
603 )
604 .expect("Operation failed");
605 let values = Array1::from_vec(vec![0.0, 1.0, 4.0, 9.0, 16.0]);
606
607 let report =
608 analyze_interpolation_data(&points.view(), &values.view()).expect("Operation failed");
609
610 assert!(report.data_points.is_collinear);
611 assert!(report.function_values.is_smooth);
612 assert!(report.function_values.is_monotonic);
613 assert!(!report.noise_analysis.is_noisy);
614 }
615}