Skip to main content

scirs2_interpolate/numerical_stability_modules/
data_analysis.rs

1//! Data analysis utilities for interpolation problem assessment
2//!
3//! This module provides specialized analysis functions for understanding
4//! the characteristics of interpolation data and suggesting optimal approaches.
5
6use 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
14/// Suggest data-based regularization parameter
15pub 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    // Adjust based on point spacing
24    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    // Adjust based on distance ratio (how uniform the spacing is)
31    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
40/// Comprehensive analysis of interpolation data characteristics
41pub 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    // Additional specialized analyses
61    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/// Complete interpolation data analysis report
77#[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/// Data scaling analysis
91#[derive(Debug, Clone)]
92pub struct DataScalingAnalysis<F>
93where
94    F: Float + FromPrimitive + Debug + Display + AddAssign + SubAssign,
95{
96    /// Scale factor for input coordinates
97    pub coordinate_scale: F,
98    /// Scale factor for function values
99    pub value_scale: F,
100    /// Whether scaling is recommended
101    pub scaling_recommended: bool,
102    /// Condition number improvement estimate with scaling
103    pub condition_improvement_factor: F,
104}
105
106/// Noise characteristics analysis
107#[derive(Debug, Clone)]
108pub struct NoiseAnalysis<F>
109where
110    F: Float + FromPrimitive + Debug + Display + AddAssign + SubAssign,
111{
112    /// Estimated noise level
113    pub estimated_noise_level: F,
114    /// Signal-to-noise ratio estimate
115    pub signal_noise_ratio: F,
116    /// Whether the function appears noisy
117    pub is_noisy: bool,
118    /// Recommended denoising strategy
119    pub denoising_strategy: DenoisingStrategy,
120}
121
122/// Denoising strategy recommendations
123#[derive(Debug, Clone, Copy, PartialEq)]
124pub enum DenoisingStrategy {
125    /// No denoising needed
126    None,
127    /// Apply smoothing splines
128    SmoothingSplines,
129    /// Use robust interpolation
130    RobustInterpolation,
131    /// Apply wavelet denoising
132    WaveletDenoising,
133    /// Use total variation regularization
134    TotalVariationRegularization,
135}
136
137/// Interpolation method recommendations
138#[derive(Debug, Clone)]
139pub struct InterpolationMethodRecommendation {
140    /// Primary recommended method
141    pub primary_method: InterpolationMethod,
142    /// Alternative methods to consider
143    pub alternative_methods: Vec<InterpolationMethod>,
144    /// Explanation for the recommendation
145    pub recommendation_reason: String,
146}
147
148/// Available interpolation methods
149#[derive(Debug, Clone, Copy, PartialEq)]
150pub enum InterpolationMethod {
151    /// Linear interpolation
152    Linear,
153    /// Cubic spline interpolation
154    CubicSpline,
155    /// B-spline interpolation
156    BSpline,
157    /// NURBS interpolation
158    NURBS,
159    /// Radial basis function interpolation
160    RadialBasisFunction,
161    /// Kriging interpolation
162    Kriging,
163    /// Thin plate spline
164    ThinPlateSpline,
165    /// Piecewise polynomial
166    PiecewisePolynomial,
167}
168
169/// Analyze data scaling characteristics
170fn 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    // Analyze coordinate scaling
190    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    // Analyze value scaling
210    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    // Determine if scaling is recommended
220    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    // Estimate condition number improvement
236    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
252/// Analyze noise characteristics in function values
253pub 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    // Estimate noise using finite differences
270    let mut differences = Vec::new();
271    for i in 1..n {
272        differences.push(values[i] - values[i - 1]);
273    }
274
275    // Second differences to separate noise from signal
276    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    // Estimate noise level using median absolute deviation of second differences
285    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    // MAD to std conversion
294    } else {
295        F::zero()
296    };
297
298    // Estimate signal level
299    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    // Determine if data is noisy
309    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    // Recommend denoising strategy
314    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
336/// Recommend interpolation method based on data characteristics
337fn 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    // Consider data point characteristics
349    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    // Consider function characteristics (but don't override small dataset preference)
360    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    // Consider geometric characteristics
379    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    // Ensure we have alternatives
392    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
426/// Analyze sampling density and suggest optimal point distribution
427pub 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    // Calculate current sampling density
448    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    // Estimate required density based on target accuracy
452    let accuracy_factor = F::one() / target_accuracy.max(super::types::machine_epsilon::<F>());
453    let recommended_density = current_density * accuracy_factor.sqrt();
454
455    // Check if current density is adequate
456    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    // Suggest additional points if needed
462    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    // Recommend sampling strategy
470    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/// Sampling density analysis results
497#[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/// Sampling strategy recommendations
510#[derive(Debug, Clone, Copy, PartialEq)]
511pub enum SamplingStrategy {
512    /// Uniform grid sampling
513    Uniform,
514    /// Adaptive sampling based on function behavior
515    Adaptive,
516    /// Quasi-random sampling (Halton, Sobol, etc.)
517    QuasiRandom,
518    /// Latin hypercube sampling
519    LatinHypercube,
520    /// Importance sampling
521    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        // Create smooth data
548        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        // Create noisy data
555        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        // Test small dataset
564        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}