use scirs2_core::ndarray::{Array1, ArrayView1, ArrayView2};
use scirs2_core::numeric::{Float, FromPrimitive};
use statrs::statistics::Statistics;
use std::fmt::{Debug, Display};
use std::ops::{AddAssign, SubAssign};
use super::types::{
BoundaryAnalysis, BoundaryTreatment, DataPointsAnalysis, EdgeCaseAnalysis, ExtrapolationRisk,
FunctionValuesAnalysis,
};
use crate::error::{InterpolateError, InterpolateResult};
pub fn analyze_interpolation_edge_cases<F>(
points: &ArrayView2<F>,
values: &ArrayView1<F>,
) -> InterpolateResult<EdgeCaseAnalysis<F>>
where
F: Float + FromPrimitive + Debug + Display + AddAssign + SubAssign,
{
if points.nrows() != values.len() {
return Err(InterpolateError::ShapeMismatch {
expected: format!("{} points", values.len()),
actual: format!("{} points", points.nrows()),
object: "edge case analysis".to_string(),
});
}
if points.nrows() == 0 {
return Err(InterpolateError::InvalidInput {
message: "Cannot analyze empty dataset".to_string(),
});
}
let data_points = analyze_data_points(points)?;
let function_values = analyze_function_values(values)?;
let boundary = analyze_boundary_conditions(points, values)?;
let overall_recommendation =
generate_overall_recommendation(&data_points, &function_values, &boundary);
let is_solvable = assess_solvability(&data_points, &function_values, &boundary);
Ok(EdgeCaseAnalysis {
data_points,
function_values,
boundary,
overall_recommendation,
is_solvable,
})
}
pub fn analyze_data_points<F>(points: &ArrayView2<F>) -> InterpolateResult<DataPointsAnalysis<F>>
where
F: Float + FromPrimitive + Debug + Display + AddAssign + SubAssign,
{
let num_points = points.nrows();
let dimension = points.ncols();
if num_points < 2 {
return Ok(DataPointsAnalysis {
num_points,
min_distance: F::zero(),
max_distance: F::zero(),
distance_ratio: F::one(),
is_collinear: false,
clustering_score: F::zero(),
has_linear_dependencies: false,
});
}
let (min_distance, max_distance, _closest_pair) = analyze_point_distances(points)?;
let distance_ratio = if min_distance > F::zero() {
max_distance / min_distance
} else {
F::infinity()
};
let is_collinear = if dimension == 2 && num_points >= 3 {
check_collinearity_2d(points)
} else {
false
};
let clustering_score = calculate_clustering_score(points);
let has_linear_dependencies = check_near_linear_dependencies(points)?;
Ok(DataPointsAnalysis {
num_points,
min_distance,
max_distance,
distance_ratio,
is_collinear,
clustering_score,
has_linear_dependencies,
})
}
pub fn analyze_function_values<F>(
values: &ArrayView1<F>,
) -> InterpolateResult<FunctionValuesAnalysis<F>>
where
F: Float + FromPrimitive + Debug + Display + AddAssign + SubAssign,
{
let n = values.len();
if n == 0 {
return Ok(FunctionValuesAnalysis::default());
}
let min_val = values.iter().fold(F::infinity(), |acc, &x| acc.min(x));
let max_val = values.iter().fold(F::neg_infinity(), |acc, &x| acc.max(x));
let value_range = max_val - min_val;
let smoothness_score = estimate_smoothness(values);
let is_smooth = smoothness_score
> F::from(0.7)
.unwrap_or_else(|| F::from(0.7).expect("Failed to convert constant to float"));
let is_monotonic = check_monotonicity(values);
let has_outliers = detect_outliers_simple(values);
let recommended_smoothing = if !is_smooth || has_outliers {
Some(suggest_smoothing_parameter(values, smoothness_score))
} else {
None
};
Ok(FunctionValuesAnalysis {
value_range,
is_smooth,
smoothness_score,
is_monotonic,
has_outliers,
recommended_smoothing,
})
}
pub fn analyze_boundary_conditions<F>(
points: &ArrayView2<F>,
values: &ArrayView1<F>,
) -> InterpolateResult<BoundaryAnalysis<F>>
where
F: Float + FromPrimitive + Debug + Display + AddAssign + SubAssign,
{
let num_points = points.nrows();
if num_points < 2 {
return Ok(BoundaryAnalysis::default());
}
let has_boundary_effects = assess_boundary_effects(points, values)?;
let recommended_boundary_treatment = recommend_boundary_treatment(points, values)?;
let extrapolation_risk = assess_extrapolation_risk(points, values)?;
let mut analysis = BoundaryAnalysis::default();
analysis.has_boundary_effects = has_boundary_effects;
analysis.recommended_boundary_treatment = recommended_boundary_treatment;
analysis.extrapolation_risk = extrapolation_risk;
Ok(analysis)
}
pub fn analyze_point_distances<F>(points: &ArrayView2<F>) -> InterpolateResult<(F, F, usize)>
where
F: Float + FromPrimitive + AddAssign,
{
let n = points.nrows();
let dim = points.ncols();
if n < 2 {
return Ok((F::zero(), F::zero(), 0));
}
let mut min_distance = F::infinity();
let mut max_distance = F::zero();
let mut closest_pair = 0;
for i in 0..n {
for j in (i + 1)..n {
let mut distance_sq = F::zero();
for k in 0..dim {
let diff = points[(i, k)] - points[(j, k)];
distance_sq += diff * diff;
}
let distance = distance_sq.sqrt();
if distance < min_distance {
min_distance = distance;
closest_pair = i;
}
if distance > max_distance {
max_distance = distance;
}
}
}
Ok((min_distance, max_distance, closest_pair))
}
fn check_near_linear_dependencies<F>(points: &ArrayView2<F>) -> InterpolateResult<bool>
where
F: Float + FromPrimitive + AddAssign,
{
let n = points.nrows();
let dim = points.ncols();
if n <= dim {
return Ok(false); }
let gram = compute_gram_matrix(points);
let min_eigenvalue = estimate_min_eigenvalue(&gram.view());
let max_eigenvalue = estimate_max_eigenvalue(&gram.view());
let condition_number = if min_eigenvalue > F::zero() {
max_eigenvalue / min_eigenvalue
} else {
F::infinity()
};
let threshold = F::from(1e12)
.unwrap_or_else(|| F::from(1e12).expect("Failed to convert constant to float"));
Ok(condition_number > threshold)
}
fn compute_gram_matrix<F>(points: &ArrayView2<F>) -> scirs2_core::ndarray::Array2<F>
where
F: Float + FromPrimitive + AddAssign,
{
let n = points.nrows();
let dim = points.ncols();
let mut gram = scirs2_core::ndarray::Array2::zeros((n, n));
for i in 0..n {
for j in 0..n {
let mut dot_product = F::zero();
for k in 0..dim {
dot_product += points[(i, k)] * points[(j, k)];
}
gram[(i, j)] = dot_product;
}
}
gram
}
fn estimate_max_eigenvalue<F>(matrix: &ArrayView2<F>) -> F
where
F: Float + FromPrimitive + AddAssign,
{
let n = matrix.nrows();
if n == 0 {
return F::zero();
}
let mut max_row_sum = F::zero();
for i in 0..n {
let mut row_sum = F::zero();
for j in 0..n {
row_sum += matrix[(i, j)].abs();
}
max_row_sum = max_row_sum.max(row_sum);
}
max_row_sum
}
fn estimate_min_eigenvalue<F>(matrix: &ArrayView2<F>) -> F
where
F: Float + FromPrimitive,
{
let n = matrix.nrows();
if n == 0 {
return F::zero();
}
let mut min_diag = F::infinity();
for i in 0..n {
min_diag = min_diag.min(matrix[(i, i)]);
}
min_diag.max(F::zero())
}
fn check_collinearity_2d<F>(points: &ArrayView2<F>) -> bool
where
F: Float + FromPrimitive,
{
let n = points.nrows();
if n < 3 || points.ncols() != 2 {
return false;
}
let tolerance = super::types::machine_epsilon::<F>()
* F::from(1000.0)
.unwrap_or_else(|| F::from(1000.0).expect("Failed to convert constant to float"));
let x1 = points[(0, 0)];
let y1 = points[(0, 1)];
let x2 = points[(1, 0)];
let y2 = points[(1, 1)];
for i in 2..n {
let x3 = points[(i, 0)];
let y3 = points[(i, 1)];
let cross_product = (x2 - x1) * (y3 - y1) - (y2 - y1) * (x3 - x1);
if cross_product.abs() > tolerance {
return false;
}
}
true
}
fn calculate_clustering_score<F>(points: &ArrayView2<F>) -> F
where
F: Float + FromPrimitive + AddAssign,
{
let n = points.nrows();
if n < 3 {
return F::zero();
}
let mut total_distance = F::zero();
let mut count = 0;
for i in 0..n {
for j in (i + 1)..n {
let mut dist_sq = F::zero();
for k in 0..points.ncols() {
let diff = points[(i, k)] - points[(j, k)];
dist_sq += diff * diff;
}
total_distance += dist_sq.sqrt();
count += 1;
}
}
let mean_distance = total_distance / F::from(count).expect("Failed to convert to float");
let mut variance = F::zero();
for i in 0..n {
for j in (i + 1)..n {
let mut dist_sq = F::zero();
for k in 0..points.ncols() {
let diff = points[(i, k)] - points[(j, k)];
dist_sq += diff * diff;
}
let distance = dist_sq.sqrt();
let diff_from_mean = distance - mean_distance;
variance += diff_from_mean * diff_from_mean;
}
}
variance = variance / F::from(count).expect("Failed to convert to float");
if mean_distance > F::zero() {
let cv = variance.sqrt() / mean_distance;
cv / (F::one() + cv) } else {
F::one() }
}
fn estimate_smoothness<F>(values: &ArrayView1<F>) -> F
where
F: Float + FromPrimitive + AddAssign,
{
let n = values.len();
if n < 3 {
return F::one(); }
let mut second_diffs = Vec::new();
for i in 1..(n - 1) {
let second_diff = values[i + 1]
- F::from(2.0).expect("Failed to convert constant to float") * values[i]
+ values[i - 1];
second_diffs.push(second_diff.abs());
}
if second_diffs.is_empty() {
return F::one();
}
let mean_second_diff: F = second_diffs.iter().fold(F::zero(), |acc, &x| acc + x)
/ F::from(second_diffs.len()).expect("Operation failed");
let max_possible_diff = values.iter().fold(F::zero(), |acc, &x| acc.max(x.abs()))
* F::from(2.0).expect("Failed to convert constant to float");
if max_possible_diff > F::zero() {
F::one() - (mean_second_diff / max_possible_diff).min(F::one())
} else {
F::one()
}
}
fn check_monotonicity<F>(values: &ArrayView1<F>) -> bool
where
F: Float + FromPrimitive,
{
let n = values.len();
if n < 2 {
return true;
}
let mut increasing = true;
let mut decreasing = true;
for i in 1..n {
if values[i] < values[i - 1] {
increasing = false;
}
if values[i] > values[i - 1] {
decreasing = false;
}
}
increasing || decreasing
}
fn detect_outliers_simple<F>(values: &ArrayView1<F>) -> bool
where
F: Float + FromPrimitive + AddAssign,
{
let n = values.len();
if n < 4 {
return false; }
let mut sorted_values: Vec<F> = values.iter().cloned().collect();
sorted_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let q1_idx = n / 4;
let q3_idx = 3 * n / 4;
let q1 = sorted_values[q1_idx];
let q3 = sorted_values[q3_idx];
let iqr = q3 - q1;
let lower_bound = q1 - F::from(1.5).expect("Failed to convert constant to float") * iqr;
let upper_bound = q3 + F::from(1.5).expect("Failed to convert constant to float") * iqr;
for &value in values.iter() {
if value < lower_bound || value > upper_bound {
return true;
}
}
false
}
fn suggest_smoothing_parameter<F>(values: &ArrayView1<F>, smoothness_score: F) -> F
where
F: Float + FromPrimitive + AddAssign,
{
let range = values.iter().fold(F::neg_infinity(), |acc, &x| acc.max(x))
- values.iter().fold(F::infinity(), |acc, &x| acc.min(x));
let base_smoothing = range
* F::from(0.01)
.unwrap_or_else(|| F::from(0.01).expect("Failed to convert constant to float"));
let smoothness_factor = F::one() - smoothness_score;
base_smoothing
* (F::one()
+ smoothness_factor
* F::from(10.0)
.unwrap_or_else(|| F::from(10.0).expect("Failed to convert constant to float")))
}
fn assess_boundary_effects<F>(
_points: &ArrayView2<F>,
values: &ArrayView1<F>,
) -> InterpolateResult<bool>
where
F: Float + FromPrimitive,
{
let n = values.len();
if n < 4 {
return Ok(false);
}
let boundary_threshold =
F::from(0.1).unwrap_or_else(|| F::from(0.1).expect("Failed to convert constant to float"));
let interior_range = values[n / 4] - values[3 * n / 4];
let boundary_deviation = (values[0] - values[1]).abs() + (values[n - 1] - values[n - 2]).abs();
Ok(boundary_deviation > boundary_threshold * interior_range.abs())
}
fn recommend_boundary_treatment<F>(
_points: &ArrayView2<F>,
values: &ArrayView1<F>,
) -> InterpolateResult<BoundaryTreatment>
where
F: Float + FromPrimitive,
{
let n = values.len();
if n < 2 {
return Ok(BoundaryTreatment::Natural);
}
if n > 4 {
let first_val = values[0];
let last_val = values[n - 1];
let tolerance = (values.iter().fold(F::neg_infinity(), |acc, &x| acc.max(x))
- values.iter().fold(F::infinity(), |acc, &x| acc.min(x)))
* F::from(0.1)
.unwrap_or_else(|| F::from(0.1).expect("Failed to convert constant to float"));
if (first_val - last_val).abs() < tolerance {
return Ok(BoundaryTreatment::Periodic);
}
}
if n > 2 {
let left_slope = values[1] - values[0];
let right_slope = values[n - 1] - values[n - 2];
if left_slope.abs() > F::zero() || right_slope.abs() > F::zero() {
return Ok(BoundaryTreatment::Clamped);
}
}
Ok(BoundaryTreatment::Natural)
}
fn assess_extrapolation_risk<F>(
points: &ArrayView2<F>,
values: &ArrayView1<F>,
) -> InterpolateResult<ExtrapolationRisk>
where
F: Float + FromPrimitive + AddAssign,
{
let smoothness = estimate_smoothness(values);
let is_monotonic = check_monotonicity(values);
let clustering_score = calculate_clustering_score(points);
let mut risk_score = F::zero();
if smoothness
< F::from(0.5).unwrap_or_else(|| F::from(0.5).expect("Failed to convert constant to float"))
{
risk_score += F::from(0.3)
.unwrap_or_else(|| F::from(0.3).expect("Failed to convert constant to float"));
}
if !is_monotonic {
risk_score += F::from(0.2)
.unwrap_or_else(|| F::from(0.2).expect("Failed to convert constant to float"));
}
if clustering_score
> F::from(0.5).unwrap_or_else(|| F::from(0.5).expect("Failed to convert constant to float"))
{
risk_score += F::from(0.3)
.unwrap_or_else(|| F::from(0.3).expect("Failed to convert constant to float"));
}
if risk_score
< F::from(0.2).unwrap_or_else(|| F::from(0.2).expect("Failed to convert constant to float"))
{
Ok(ExtrapolationRisk::Low)
} else if risk_score
< F::from(0.5).unwrap_or_else(|| F::from(0.5).expect("Failed to convert constant to float"))
{
Ok(ExtrapolationRisk::Medium)
} else if risk_score
< F::from(0.8).unwrap_or_else(|| F::from(0.8).expect("Failed to convert constant to float"))
{
Ok(ExtrapolationRisk::High)
} else {
Ok(ExtrapolationRisk::Critical)
}
}
fn generate_overall_recommendation<F>(
data_points: &DataPointsAnalysis<F>,
function_values: &FunctionValuesAnalysis<F>,
boundary: &BoundaryAnalysis<F>,
) -> String
where
F: Float + FromPrimitive + Debug + Display + AddAssign + SubAssign,
{
let mut recommendations = Vec::new();
if data_points.is_collinear {
recommendations
.push("Points are collinear - consider adding non-collinear points".to_string());
}
if data_points.clustering_score
> F::from(0.7).unwrap_or_else(|| F::from(0.7).expect("Failed to convert constant to float"))
{
recommendations
.push("Points are highly clustered - consider more uniform distribution".to_string());
}
if data_points.distance_ratio
> F::from(1000.0)
.unwrap_or_else(|| F::from(1000.0).expect("Failed to convert constant to float"))
{
recommendations
.push("Large variation in point spacing - consider regularization".to_string());
}
if !function_values.is_smooth {
recommendations
.push("Function appears non-smooth - consider smoothing or regularization".to_string());
}
if function_values.has_outliers {
recommendations.push(
"Outliers detected in function values - consider robust interpolation".to_string(),
);
}
if boundary.has_boundary_effects {
recommendations.push(format!(
"Boundary effects detected - consider {} boundary conditions",
match boundary.recommended_boundary_treatment {
BoundaryTreatment::Natural => "natural",
BoundaryTreatment::Clamped => "clamped",
BoundaryTreatment::Periodic => "periodic",
BoundaryTreatment::Custom => "custom",
}
));
}
match boundary.extrapolation_risk {
ExtrapolationRisk::Medium => {
recommendations.push("Moderate extrapolation risk - use with caution".to_string())
}
ExtrapolationRisk::High => {
recommendations.push("High extrapolation risk - avoid extrapolation".to_string())
}
ExtrapolationRisk::Critical => {
recommendations.push("Critical extrapolation risk - do not extrapolate".to_string())
}
ExtrapolationRisk::Low => {}
}
if recommendations.is_empty() {
"No significant issues detected - standard interpolation should work well".to_string()
} else {
recommendations.join("; ")
}
}
fn assess_solvability<F>(
data_points: &DataPointsAnalysis<F>,
function_values: &FunctionValuesAnalysis<F>,
boundary: &BoundaryAnalysis<F>,
) -> bool
where
F: Float + FromPrimitive + Debug + Display + AddAssign + SubAssign,
{
if data_points.num_points < 2 {
return false;
}
if data_points.max_distance <= F::zero() {
return false;
}
if data_points.clustering_score
> F::from(0.95)
.unwrap_or_else(|| F::from(0.95).expect("Failed to convert constant to float"))
{
return false;
}
if boundary.extrapolation_risk == ExtrapolationRisk::Critical {
return false;
}
if function_values.value_range <= F::zero() {
return false; }
true
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::{Array1, Array2};
#[test]
fn test_analyze_well_distributed_points() {
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])
.expect("Operation failed");
let analysis = analyze_data_points(&points.view()).expect("Operation failed");
assert_eq!(analysis.num_points, 4);
assert!(!analysis.is_collinear);
assert!(analysis.clustering_score < 0.5);
}
#[test]
fn test_collinearity_detection() {
let collinear_points = Array2::from_shape_vec((3, 2), vec![0.0, 0.0, 1.0, 1.0, 2.0, 2.0])
.expect("Operation failed");
let analysis = analyze_data_points(&collinear_points.view()).expect("Operation failed");
assert!(analysis.is_collinear);
}
#[test]
fn test_smoothness_estimation() {
let smooth_values = Array1::from_vec(vec![0.0, 1.0, 2.0, 3.0, 4.0]);
let smoothness = estimate_smoothness(&smooth_values.view());
assert!(smoothness > 0.8);
let non_smooth_values = Array1::from_vec(vec![0.0, 10.0, 0.0, 10.0, 0.0]);
let smoothness2 = estimate_smoothness(&non_smooth_values.view());
assert!(smoothness2 < 0.5);
}
#[test]
fn test_monotonicity_check() {
let increasing = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0]);
assert!(check_monotonicity(&increasing.view()));
let decreasing = Array1::from_vec(vec![4.0, 3.0, 2.0, 1.0]);
assert!(check_monotonicity(&decreasing.view()));
let non_monotonic = Array1::from_vec(vec![1.0, 3.0, 2.0, 4.0]);
assert!(!check_monotonicity(&non_monotonic.view()));
}
#[test]
fn test_outlier_detection() {
let normal_values = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
assert!(!detect_outliers_simple(&normal_values.view()));
let with_outlier = Array1::from_vec(vec![1.0, 2.0, 100.0, 4.0, 5.0]);
assert!(detect_outliers_simple(&with_outlier.view()));
}
#[test]
fn test_complete_edge_case_analysis() {
let points = Array2::from_shape_vec(
(5, 2),
vec![0.0, 0.0, 1.0, 0.0, 2.0, 0.0, 3.0, 0.0, 4.0, 0.0],
)
.expect("Operation failed");
let values = Array1::from_vec(vec![0.0, 1.0, 4.0, 9.0, 16.0]);
let analysis = analyze_interpolation_edge_cases(&points.view(), &values.view())
.expect("Operation failed");
assert!(analysis.is_solvable);
assert!(analysis.data_points.is_collinear); assert!(analysis.function_values.is_smooth); assert!(analysis.function_values.is_monotonic); }
}