Skip to main content

scirs2_stats/
error_context.rs

1//! Enhanced error context and recovery suggestions
2//!
3//! This module provides enhanced error context with detailed recovery suggestions
4//! for common statistical computation errors.
5
6use crate::error::{StatsError, StatsResult};
7use std::fmt::Display;
8
9/// Error context with detailed information and recovery suggestions
10#[derive(Debug)]
11pub struct EnhancedError {
12    /// The original error
13    pub error: StatsError,
14    /// Additional context about where the error occurred
15    pub context: String,
16    /// Specific recovery suggestions
17    pub suggestions: Vec<String>,
18    /// Related documentation or examples
19    pub see_also: Vec<String>,
20}
21
22impl EnhancedError {
23    /// Create a new enhanced error
24    pub fn new(error: StatsError, context: impl Into<String>) -> Self {
25        Self {
26            error,
27            context: context.into(),
28            suggestions: Vec::new(),
29            see_also: Vec::new(),
30        }
31    }
32
33    /// Add a recovery suggestion
34    pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
35        self.suggestions.push(suggestion.into());
36        self
37    }
38
39    /// Add multiple recovery suggestions
40    pub fn with_suggestions(
41        mut self,
42        suggestions: impl IntoIterator<Item = impl Into<String>>,
43    ) -> Self {
44        self.suggestions
45            .extend(suggestions.into_iter().map(|s| s.into()));
46        self
47    }
48
49    /// Add a reference to related documentation or examples
50    pub fn see_also(mut self, reference: impl Into<String>) -> Self {
51        self.see_also.push(reference.into());
52        self
53    }
54
55    /// Convert to StatsError with formatted message
56    pub fn into_error(self) -> StatsError {
57        let mut message = format!("{}\nContext: {}", self.error, self.context);
58
59        if !self.suggestions.is_empty() {
60            message.push_str("\n\nSuggestions:");
61            for (i, suggestion) in self.suggestions.iter().enumerate() {
62                message.push_str(&format!("\n  {}. {}", i + 1, suggestion));
63            }
64        }
65
66        if !self.see_also.is_empty() {
67            message.push_str("\n\nSee also:");
68            for reference in &self.see_also {
69                message.push_str(&format!("\n  - {}", reference));
70            }
71        }
72
73        StatsError::computation(message)
74    }
75}
76
77/// Enhanced validation with detailed error context
78pub mod enhanced_validation {
79    use super::*;
80    use scirs2_core::numeric::Float;
81
82    /// Validate distribution parameters with enhanced error messages
83    pub fn validate_distribution_params<F: Float + Display>(
84        params: &[(F, &str, ParamType)],
85        distribution_name: &str,
86    ) -> StatsResult<()> {
87        for &(value, name, param_type) in params {
88            match param_type {
89                ParamType::Positive => {
90                    if value <= F::zero() {
91                        return Err(EnhancedError::new(
92                            StatsError::domain(format!("{} must be positive, got {}", name, value)),
93                            format!("Invalid {} parameter for {} distribution", name, distribution_name),
94                        )
95                        .with_suggestions(vec![
96                            format!("Ensure {} > 0", name),
97                            "Check your data preprocessing steps".to_string(),
98                            "Consider using a different distribution if negative values are expected".to_string(),
99                        ])
100                        .see_also(format!("distributions::{}", distribution_name.to_lowercase()))
101                        .into_error());
102                    }
103                }
104                ParamType::NonNegative => {
105                    if value < F::zero() {
106                        return Err(EnhancedError::new(
107                            StatsError::domain(format!(
108                                "{} must be non-negative, got {}",
109                                name, value
110                            )),
111                            format!(
112                                "Invalid {} parameter for {} distribution",
113                                name, distribution_name
114                            ),
115                        )
116                        .with_suggestions(vec![
117                            format!("Ensure {} >= 0", name),
118                            "Check for data entry errors".to_string(),
119                        ])
120                        .into_error());
121                    }
122                }
123                ParamType::Probability => {
124                    if value < F::zero() || value > F::one() {
125                        return Err(EnhancedError::new(
126                            StatsError::domain(format!(
127                                "{} must be in [0, 1], got {}",
128                                name, value
129                            )),
130                            format!(
131                                "Invalid probability parameter '{}' for {} distribution",
132                                name, distribution_name
133                            ),
134                        )
135                        .with_suggestions(vec![
136                            "Ensure probability is between 0 and 1 (inclusive)",
137                            "Check if you're using a proportion instead of a percentage",
138                            "Verify your probability calculations",
139                        ])
140                        .into_error());
141                    }
142                }
143                ParamType::Integer => {
144                    if value.floor() != value {
145                        return Err(EnhancedError::new(
146                            StatsError::domain(format!(
147                                "{} must be an integer, got {}",
148                                name, value
149                            )),
150                            format!(
151                                "Invalid {} parameter for {} distribution",
152                                name, distribution_name
153                            ),
154                        )
155                        .with_suggestions(vec![
156                            "Round to the nearest integer if appropriate",
157                            "Check if you're using the correct distribution",
158                        ])
159                        .into_error());
160                    }
161                }
162                ParamType::PositiveInteger => {
163                    if value.floor() != value || value <= F::zero() {
164                        return Err(EnhancedError::new(
165                            StatsError::domain(format!(
166                                "{} must be a positive integer, got {}",
167                                name, value
168                            )),
169                            format!(
170                                "Invalid {} parameter for {} distribution",
171                                name, distribution_name
172                            ),
173                        )
174                        .with_suggestions(vec![
175                            "Ensure the value is a positive whole number",
176                            "Check your counting or indexing logic",
177                        ])
178                        .into_error());
179                    }
180                }
181            }
182        }
183        Ok(())
184    }
185
186    /// Parameter type for validation
187    #[derive(Debug, Copy, Clone)]
188    pub enum ParamType {
189        Positive,
190        NonNegative,
191        Probability,
192        Integer,
193        PositiveInteger,
194    }
195}
196
197/// Enhanced error handling for numerical computations
198pub mod numerical {
199    use super::*;
200
201    /// Handle numerical overflow with context
202    pub fn handle_overflow(operation: &str, values: &[impl Display]) -> StatsError {
203        let value_str = values
204            .iter()
205            .map(|v| v.to_string())
206            .collect::<Vec<_>>()
207            .join(", ");
208
209        EnhancedError::new(
210            StatsError::computation("Numerical overflow"),
211            format!(
212                "Overflow occurred during {} with values: [{}]",
213                operation, value_str
214            ),
215        )
216        .with_suggestions(vec![
217            "Scale your input data to smaller magnitudes",
218            "Use logarithmic transformations if appropriate",
219            "Consider using higher precision data types (f64 instead of f32)",
220            "Check for extreme outliers in your data",
221        ])
222        .see_also("numerical_stability")
223        .into_error()
224    }
225
226    /// Handle convergence failure with context
227    pub fn handle_convergence_failure(
228        algorithm: &str,
229        iterations: usize,
230        tolerance: f64,
231    ) -> StatsError {
232        EnhancedError::new(
233            StatsError::computation("Algorithm failed to converge"),
234            format!(
235                "{} failed to converge after {} iterations (tolerance: {})",
236                algorithm, iterations, tolerance
237            ),
238        )
239        .with_suggestions(vec![
240            "Increase the maximum number of iterations",
241            "Relax the convergence tolerance",
242            "Check if your data is well-conditioned",
243            "Try different initial values",
244            "Consider using a different algorithm",
245        ])
246        .into_error()
247    }
248
249    /// Handle singular matrix errors
250    pub fn handle_singular_matrix(context: &str) -> StatsError {
251        EnhancedError::new(
252            StatsError::computation("Matrix is singular or near-singular"),
253            format!("Singular matrix encountered in {}", context),
254        )
255        .with_suggestions(vec![
256            "Check for linear dependencies in your data",
257            "Remove collinear features",
258            "Add regularization to your model",
259            "Ensure you have more observations than features",
260            "Check for duplicate rows or columns",
261        ])
262        .see_also("linear_algebra")
263        .into_error()
264    }
265}
266
267/// Enhanced error handling for data validation
268pub mod data_validation {
269    use super::*;
270    use scirs2_core::numeric::Float;
271
272    /// Validate input data with enhanced error messages
273    pub fn validatedata_quality<T>(data: &[T], context: &str, allow_empty: bool) -> StatsResult<()>
274    where
275        T: Float + Display,
276    {
277        if data.is_empty() && !allow_empty {
278            return Err(EnhancedError::new(
279                StatsError::invalid_argument("Empty data array"),
280                format!("Empty input data for {}", context),
281            )
282            .with_suggestions(vec![
283                "Ensure your data loading process completed successfully",
284                "Check if filters removed all data points",
285                "Verify the data source is not _empty",
286            ])
287            .into_error());
288        }
289
290        // Check for NaN or infinite values
291        let nan_count = data.iter().filter(|&&x| x.is_nan()).count();
292        let inf_count = data.iter().filter(|&&x| x.is_infinite()).count();
293
294        if nan_count > 0 {
295            return Err(EnhancedError::new(
296                StatsError::invalid_argument(format!("Found {} NaN values", nan_count)),
297                format!("Invalid data values in {}", context),
298            )
299            .with_suggestions(vec![
300                "Use dropna() or similar to remove NaN values",
301                "Check for division by zero in calculations",
302                "Verify data import didn't introduce NaN values",
303                "Consider imputation methods if appropriate",
304            ])
305            .see_also("data_preprocessing")
306            .into_error());
307        }
308
309        if inf_count > 0 {
310            return Err(EnhancedError::new(
311                StatsError::invalid_argument(format!("Found {} infinite values", inf_count)),
312                format!("Invalid data values in {}", context),
313            )
314            .with_suggestions(vec![
315                "Check for numerical overflow in calculations",
316                "Apply bounds checking before operations",
317                "Consider log transformations for large values",
318                "Remove or cap extreme outliers",
319            ])
320            .into_error());
321        }
322
323        Ok(())
324    }
325
326    /// Validate array shapes match requirements
327    pub fn validateshape_compatibility(
328        actualshape: &[usize],
329        expectedshape: &[Option<usize>],
330        array_name: &str,
331    ) -> StatsResult<()> {
332        if actualshape.len() != expectedshape.len() {
333            return Err(EnhancedError::new(
334                StatsError::dimension_mismatch(format!(
335                    "Expected {}-dimensional array, got {}-dimensional",
336                    expectedshape.len(),
337                    actualshape.len()
338                )),
339                format!("Shape mismatch for {}", array_name),
340            )
341            .with_suggestions(vec![
342                "Reshape your array using reshape() or similar",
343                "Check if you're passing the correct variable",
344                "Verify data preprocessing steps maintained correct dimensions",
345            ])
346            .into_error());
347        }
348
349        for (i, (&actual, &expected)) in actualshape.iter().zip(expectedshape.iter()).enumerate() {
350            if let Some(expected_dim) = expected {
351                if actual != expected_dim {
352                    return Err(EnhancedError::new(
353                        StatsError::dimension_mismatch(format!(
354                            "Dimension {} mismatch: expected {}, got {}",
355                            i, expected_dim, actual
356                        )),
357                        format!("Invalid shape for {}", array_name),
358                    )
359                    .with_suggestions(vec![
360                        format!("Ensure dimension {} has size {}", i, expected_dim),
361                        "Check array slicing or indexing operations".to_string(),
362                    ])
363                    .into_error());
364                }
365            }
366        }
367
368        Ok(())
369    }
370}