sklears_linear/
errors.rs

1//! Domain-specific error types for linear models
2//!
3//! This module provides comprehensive error types specific to linear model operations,
4//! including detailed context, recovery suggestions, and error categorization.
5
6use sklears_core::error::SklearsError;
7use std::fmt;
8
9/// Comprehensive error type for linear model operations
10#[derive(Debug, Clone)]
11pub enum LinearModelError {
12    /// Data-related errors (input validation, preprocessing)
13    DataError(DataError),
14    /// Model configuration errors
15    ConfigurationError(ConfigurationError),
16    /// Numerical computation errors
17    NumericalError(NumericalError),
18    /// Optimization/convergence errors
19    OptimizationError(OptimizationError),
20    /// Model state errors (fitting, prediction)
21    StateError(StateError),
22    /// Feature-related errors
23    FeatureError(FeatureError),
24    /// Matrix operation errors
25    MatrixError(MatrixError),
26    /// Cross-validation errors
27    CrossValidationError(CrossValidationError),
28    /// Memory/resource errors
29    ResourceError(ResourceError),
30}
31
32/// Data-related errors
33#[derive(Debug, Clone)]
34pub struct DataError {
35    pub kind: DataErrorKind,
36    pub context: String,
37    pub suggestions: Vec<String>,
38    pub error_location: Option<String>,
39}
40
41#[derive(Debug, Clone)]
42pub enum DataErrorKind {
43    /// Empty dataset
44    EmptyData,
45    /// Mismatched dimensions
46    DimensionMismatch {
47        expected: Vec<usize>,
48        actual: Vec<usize>,
49    },
50    /// Invalid data values (NaN, infinity)
51    InvalidValues { count: usize, total: usize },
52    /// Missing target values
53    MissingTargets,
54    /// Insufficient data for operation
55    InsufficientData { required: usize, available: usize },
56    /// Data type incompatibility
57    IncompatibleDataType { expected: String, actual: String },
58    /// Data range issues (e.g., for Box-Cox transformation)
59    DataRangeError { min_required: f64, actual_min: f64 },
60}
61
62/// Model configuration errors
63#[derive(Debug, Clone)]
64pub struct ConfigurationError {
65    pub kind: ConfigurationErrorKind,
66    pub parameter_name: String,
67    pub provided_value: String,
68    pub valid_range: Option<String>,
69    pub suggestions: Vec<String>,
70}
71
72#[derive(Debug, Clone)]
73pub enum ConfigurationErrorKind {
74    /// Invalid parameter value
75    InvalidParameter,
76    /// Parameter out of valid range
77    OutOfRange,
78    /// Incompatible parameter combination
79    IncompatibleParameters { conflicting_params: Vec<String> },
80    /// Missing required parameter
81    MissingParameter,
82    /// Deprecated parameter usage
83    DeprecatedParameter { replacement: Option<String> },
84}
85
86/// Numerical computation errors
87#[derive(Debug, Clone)]
88pub struct NumericalError {
89    pub kind: NumericalErrorKind,
90    pub operation: String,
91    pub context: String,
92    pub matrix_info: Option<MatrixInfo>,
93    pub recovery_suggestions: Vec<String>,
94}
95
96#[derive(Debug, Clone)]
97pub enum NumericalErrorKind {
98    /// Matrix is singular or near-singular
99    SingularMatrix { condition_number: Option<f64> },
100    /// Numerical overflow
101    Overflow,
102    /// Numerical underflow
103    Underflow,
104    /// Loss of precision
105    PrecisionLoss { digits_lost: usize },
106    /// Ill-conditioned problem
107    IllConditioned { condition_number: f64 },
108    /// Failed to invert matrix
109    MatrixInversionFailed,
110    /// Eigenvalue computation failed
111    EigenvalueFailed,
112    /// Cholesky decomposition failed
113    CholeskyFailed,
114}
115
116/// Matrix-related information
117#[derive(Debug, Clone)]
118pub struct MatrixInfo {
119    pub dimensions: (usize, usize),
120    pub rank: Option<usize>,
121    pub condition_number: Option<f64>,
122    pub determinant: Option<f64>,
123    pub is_symmetric: Option<bool>,
124    pub is_positive_definite: Option<bool>,
125}
126
127/// Optimization and convergence errors
128#[derive(Debug, Clone)]
129pub struct OptimizationError {
130    pub kind: OptimizationErrorKind,
131    pub algorithm: String,
132    pub iteration: Option<usize>,
133    pub max_iterations: Option<usize>,
134    pub convergence_info: Option<ConvergenceInfo>,
135    pub suggestions: Vec<String>,
136}
137
138#[derive(Debug, Clone)]
139pub enum OptimizationErrorKind {
140    /// Failed to converge within max iterations
141    ConvergenceFailed,
142    /// Converged to local minimum (suspected)
143    LocalMinimum,
144    /// Objective function not decreasing
145    NoProgress,
146    /// Step size too small
147    StepSizeTooSmall,
148    /// Gradient computation failed
149    GradientFailed,
150    /// Hessian computation failed
151    HessianFailed,
152    /// Line search failed
153    LineSearchFailed,
154    /// Invalid optimization direction
155    InvalidDirection,
156    /// Invalid problem dimensions
157    InvalidProblemDimensions,
158    /// Model not fitted
159    ModelNotFitted,
160}
161
162/// Convergence information
163#[derive(Debug, Clone)]
164pub struct ConvergenceInfo {
165    pub final_objective: Option<f64>,
166    pub final_gradient_norm: Option<f64>,
167    pub final_step_size: Option<f64>,
168    pub objective_history: Vec<f64>,
169    pub gradient_norm_history: Vec<f64>,
170}
171
172/// Model state errors
173#[derive(Debug, Clone)]
174pub struct StateError {
175    pub kind: StateErrorKind,
176    pub current_state: String,
177    pub required_state: String,
178    pub operation: String,
179}
180
181#[derive(Debug, Clone)]
182pub enum StateErrorKind {
183    /// Model not fitted
184    NotFitted,
185    /// Model already fitted
186    AlreadyFitted,
187    /// Invalid state transition
188    InvalidStateTransition,
189    /// Operation not available in current state
190    OperationNotAvailable,
191}
192
193/// Feature-related errors
194#[derive(Debug, Clone)]
195pub struct FeatureError {
196    pub kind: FeatureErrorKind,
197    pub feature_indices: Vec<usize>,
198    pub context: String,
199    pub suggestions: Vec<String>,
200}
201
202#[derive(Debug, Clone)]
203pub enum FeatureErrorKind {
204    /// Features with zero variance
205    ZeroVariance,
206    /// Highly correlated features
207    Multicollinearity { correlation_threshold: f64 },
208    /// Features outside expected range
209    OutOfRange { min: f64, max: f64 },
210    /// Missing features
211    MissingFeatures,
212    /// Too many features relative to samples
213    CurseOfDimensionality { n_features: usize, n_samples: usize },
214    /// Feature scaling issues
215    ScalingError { method: String },
216}
217
218/// Matrix operation errors
219#[derive(Debug, Clone)]
220pub struct MatrixError {
221    pub kind: MatrixErrorKind,
222    pub operation: String,
223    pub matrix_info: MatrixInfo,
224    pub suggestions: Vec<String>,
225}
226
227#[derive(Debug, Clone)]
228pub enum MatrixErrorKind {
229    /// Dimension mismatch in operation
230    DimensionMismatch,
231    /// Matrix not square when required
232    NotSquare,
233    /// Matrix not symmetric when required
234    NotSymmetric,
235    /// Matrix not positive definite when required
236    NotPositiveDefinite,
237    /// Sparse matrix operation failed
238    SparseOperationFailed,
239    /// Memory allocation failed for matrix
240    AllocationFailed { required_bytes: usize },
241}
242
243/// Cross-validation errors
244#[derive(Debug, Clone)]
245pub struct CrossValidationError {
246    pub kind: CrossValidationErrorKind,
247    pub fold_info: Option<FoldInfo>,
248    pub context: String,
249}
250
251#[derive(Debug, Clone)]
252pub enum CrossValidationErrorKind {
253    /// Insufficient data for CV
254    InsufficientData,
255    /// Invalid fold configuration
256    InvalidFolds,
257    /// Fold contains no positive/negative samples
258    ImbalancedFold,
259    /// CV scoring failed
260    ScoringFailed { metric: String },
261    /// Early stopping criteria not met
262    EarlyStoppingFailed,
263}
264
265/// Cross-validation fold information
266#[derive(Debug, Clone)]
267pub struct FoldInfo {
268    pub current_fold: usize,
269    pub total_folds: usize,
270    pub train_size: usize,
271    pub test_size: usize,
272    pub class_distribution: Option<Vec<(String, usize)>>,
273}
274
275/// Resource-related errors
276#[derive(Debug, Clone)]
277pub struct ResourceError {
278    pub kind: ResourceErrorKind,
279    pub resource_info: ResourceInfo,
280    pub suggestions: Vec<String>,
281}
282
283#[derive(Debug, Clone)]
284pub enum ResourceErrorKind {
285    /// Insufficient memory
286    InsufficientMemory,
287    /// Operation would take too long
288    TimeoutExceeded,
289    /// File I/O error
290    FileIoError { operation: String, path: String },
291    /// Network/distributed computation error
292    NetworkError,
293}
294
295/// Resource information
296#[derive(Debug, Clone)]
297pub struct ResourceInfo {
298    pub memory_required: Option<usize>,
299    pub memory_available: Option<usize>,
300    pub time_elapsed: Option<std::time::Duration>,
301    pub time_limit: Option<std::time::Duration>,
302}
303
304impl LinearModelError {
305    /// Get error severity level
306    pub fn severity(&self) -> ErrorSeverity {
307        match self {
308            LinearModelError::DataError(e) => match e.kind {
309                DataErrorKind::EmptyData | DataErrorKind::MissingTargets => ErrorSeverity::Critical,
310                DataErrorKind::InvalidValues { .. } => ErrorSeverity::High,
311                _ => ErrorSeverity::Medium,
312            },
313            LinearModelError::NumericalError(e) => match e.kind {
314                NumericalErrorKind::SingularMatrix { .. }
315                | NumericalErrorKind::Overflow
316                | NumericalErrorKind::Underflow => ErrorSeverity::High,
317                _ => ErrorSeverity::Medium,
318            },
319            LinearModelError::OptimizationError(_) => ErrorSeverity::Medium,
320            LinearModelError::ConfigurationError(_) => ErrorSeverity::Low,
321            LinearModelError::StateError(_) => ErrorSeverity::Medium,
322            LinearModelError::FeatureError(_) => ErrorSeverity::Medium,
323            LinearModelError::MatrixError(_) => ErrorSeverity::High,
324            LinearModelError::CrossValidationError(_) => ErrorSeverity::Medium,
325            LinearModelError::ResourceError(e) => match e.kind {
326                ResourceErrorKind::InsufficientMemory => ErrorSeverity::Critical,
327                _ => ErrorSeverity::Medium,
328            },
329        }
330    }
331
332    /// Get user-friendly error message
333    pub fn user_message(&self) -> String {
334        match self {
335            LinearModelError::DataError(e) => e.user_message(),
336            LinearModelError::ConfigurationError(e) => e.user_message(),
337            LinearModelError::NumericalError(e) => e.user_message(),
338            LinearModelError::OptimizationError(e) => e.user_message(),
339            LinearModelError::StateError(e) => e.user_message(),
340            LinearModelError::FeatureError(e) => e.user_message(),
341            LinearModelError::MatrixError(e) => e.user_message(),
342            LinearModelError::CrossValidationError(e) => e.user_message(),
343            LinearModelError::ResourceError(e) => e.user_message(),
344        }
345    }
346
347    /// Get recovery suggestions
348    pub fn recovery_suggestions(&self) -> Vec<String> {
349        match self {
350            LinearModelError::DataError(e) => e.suggestions.clone(),
351            LinearModelError::ConfigurationError(e) => e.suggestions.clone(),
352            LinearModelError::NumericalError(e) => e.recovery_suggestions.clone(),
353            LinearModelError::OptimizationError(e) => e.suggestions.clone(),
354            LinearModelError::StateError(_) => vec![
355                "Check model state before calling this method".to_string(),
356                "Call fit() before predict() or transform()".to_string(),
357            ],
358            LinearModelError::FeatureError(e) => e.suggestions.clone(),
359            LinearModelError::MatrixError(e) => e.suggestions.clone(),
360            LinearModelError::CrossValidationError(_) => vec![
361                "Check data distribution across folds".to_string(),
362                "Consider stratified cross-validation".to_string(),
363            ],
364            LinearModelError::ResourceError(e) => e.suggestions.clone(),
365        }
366    }
367
368    /// Check if error is recoverable
369    pub fn is_recoverable(&self) -> bool {
370        match self {
371            LinearModelError::DataError(e) => !matches!(
372                e.kind,
373                DataErrorKind::EmptyData | DataErrorKind::MissingTargets
374            ),
375            LinearModelError::NumericalError(e) => match e.kind {
376                NumericalErrorKind::SingularMatrix { .. } => true,
377                NumericalErrorKind::Overflow | NumericalErrorKind::Underflow => false,
378                _ => true,
379            },
380            LinearModelError::ConfigurationError(_) => true,
381            LinearModelError::OptimizationError(_) => true,
382            LinearModelError::StateError(_) => true,
383            LinearModelError::FeatureError(_) => true,
384            LinearModelError::MatrixError(_) => true,
385            LinearModelError::CrossValidationError(_) => true,
386            LinearModelError::ResourceError(e) => {
387                !matches!(e.kind, ResourceErrorKind::InsufficientMemory)
388            }
389        }
390    }
391}
392
393/// Error severity levels
394#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
395pub enum ErrorSeverity {
396    Low,
397    Medium,
398    High,
399    Critical,
400}
401
402impl DataError {
403    pub fn user_message(&self) -> String {
404        match &self.kind {
405            DataErrorKind::EmptyData =>
406                "The provided dataset is empty. Please provide data with at least one sample.".to_string(),
407            DataErrorKind::DimensionMismatch { expected, actual } =>
408                format!("Data dimensions don't match. Expected {:?}, got {:?}. {}", expected, actual, self.context),
409            DataErrorKind::InvalidValues { count, total } =>
410                format!("Found {} invalid values (NaN/infinity) out of {} total values. {}", count, total, self.context),
411            DataErrorKind::MissingTargets =>
412                "Target values are missing or empty. Linear models require target values for training.".to_string(),
413            DataErrorKind::InsufficientData { required, available } =>
414                format!("Insufficient data for operation. Required: {}, Available: {}. {}", required, available, self.context),
415            DataErrorKind::IncompatibleDataType { expected, actual } =>
416                format!("Data type mismatch. Expected: {}, Actual: {}. {}", expected, actual, self.context),
417            DataErrorKind::DataRangeError { min_required, actual_min } =>
418                format!("Data values out of required range. Minimum required: {}, Actual minimum: {}. {}", min_required, actual_min, self.context),
419        }
420    }
421}
422
423impl ConfigurationError {
424    pub fn user_message(&self) -> String {
425        match &self.kind {
426            ConfigurationErrorKind::InvalidParameter => format!(
427                "Invalid value '{}' for parameter '{}'. {}",
428                self.provided_value,
429                self.parameter_name,
430                self.valid_range.as_deref().unwrap_or("")
431            ),
432            ConfigurationErrorKind::OutOfRange => format!(
433                "Parameter '{}' value '{}' is out of valid range: {}",
434                self.parameter_name,
435                self.provided_value,
436                self.valid_range.as_deref().unwrap_or("unknown")
437            ),
438            ConfigurationErrorKind::IncompatibleParameters { conflicting_params } => format!(
439                "Parameter '{}' is incompatible with: {}. Current value: '{}'",
440                self.parameter_name,
441                conflicting_params.join(", "),
442                self.provided_value
443            ),
444            ConfigurationErrorKind::MissingParameter => {
445                format!("Required parameter '{}' is missing.", self.parameter_name)
446            }
447            ConfigurationErrorKind::DeprecatedParameter { replacement } => format!(
448                "Parameter '{}' is deprecated. {}",
449                self.parameter_name,
450                replacement
451                    .as_deref()
452                    .map(|r| format!("Use '{}' instead.", r))
453                    .unwrap_or("".to_string())
454            ),
455        }
456    }
457}
458
459impl NumericalError {
460    pub fn user_message(&self) -> String {
461        match &self.kind {
462            NumericalErrorKind::SingularMatrix { condition_number } => {
463                let cond_info = condition_number
464                    .map(|c| format!(" (condition number: {:.2e})", c))
465                    .unwrap_or_default();
466                format!(
467                    "Matrix is singular or nearly singular{} during {}. {}",
468                    cond_info, self.operation, self.context
469                )
470            }
471            NumericalErrorKind::Overflow => format!(
472                "Numerical overflow occurred during {}. {}",
473                self.operation, self.context
474            ),
475            NumericalErrorKind::Underflow => format!(
476                "Numerical underflow occurred during {}. {}",
477                self.operation, self.context
478            ),
479            NumericalErrorKind::PrecisionLoss { digits_lost } => format!(
480                "Significant precision loss ({} digits) during {}. {}",
481                digits_lost, self.operation, self.context
482            ),
483            NumericalErrorKind::IllConditioned { condition_number } => format!(
484                "Ill-conditioned problem (condition number: {:.2e}) during {}. {}",
485                condition_number, self.operation, self.context
486            ),
487            NumericalErrorKind::MatrixInversionFailed => format!(
488                "Failed to invert matrix during {}. {}",
489                self.operation, self.context
490            ),
491            NumericalErrorKind::EigenvalueFailed => format!(
492                "Eigenvalue computation failed during {}. {}",
493                self.operation, self.context
494            ),
495            NumericalErrorKind::CholeskyFailed => format!(
496                "Cholesky decomposition failed during {}. Matrix may not be positive definite. {}",
497                self.operation, self.context
498            ),
499        }
500    }
501}
502
503impl OptimizationError {
504    pub fn user_message(&self) -> String {
505        match &self.kind {
506            OptimizationErrorKind::ConvergenceFailed => {
507                let iter_info = match (self.iteration, self.max_iterations) {
508                    (Some(iter), Some(max_iter)) => {
509                        format!(" after {} iterations (max: {})", iter, max_iter)
510                    }
511                    (Some(iter), None) => format!(" after {} iterations", iter),
512                    _ => String::new(),
513                };
514                format!("{} failed to converge{}.", self.algorithm, iter_info)
515            }
516            OptimizationErrorKind::LocalMinimum => format!(
517                "{} may have converged to a local minimum rather than global minimum.",
518                self.algorithm
519            ),
520            OptimizationErrorKind::NoProgress => format!(
521                "{} is not making progress. Objective function is not decreasing.",
522                self.algorithm
523            ),
524            OptimizationErrorKind::StepSizeTooSmall => format!(
525                "{} step size became too small to make progress.",
526                self.algorithm
527            ),
528            OptimizationErrorKind::GradientFailed => {
529                format!("Gradient computation failed in {}.", self.algorithm)
530            }
531            OptimizationErrorKind::HessianFailed => {
532                format!("Hessian computation failed in {}.", self.algorithm)
533            }
534            OptimizationErrorKind::LineSearchFailed => {
535                format!("Line search failed in {}.", self.algorithm)
536            }
537            OptimizationErrorKind::InvalidDirection => format!(
538                "Invalid optimization direction computed in {}.",
539                self.algorithm
540            ),
541            OptimizationErrorKind::InvalidProblemDimensions => {
542                format!("Invalid problem dimensions for {}.", self.algorithm)
543            }
544            OptimizationErrorKind::ModelNotFitted => {
545                format!("Model must be fitted before use in {}.", self.algorithm)
546            }
547        }
548    }
549}
550
551impl StateError {
552    pub fn user_message(&self) -> String {
553        format!(
554            "Cannot perform '{}' operation. Model is in '{}' state but requires '{}' state.",
555            self.operation, self.current_state, self.required_state
556        )
557    }
558}
559
560impl FeatureError {
561    pub fn user_message(&self) -> String {
562        match &self.kind {
563            FeatureErrorKind::ZeroVariance => format!(
564                "Features with zero variance detected at indices: {:?}. {}",
565                self.feature_indices, self.context
566            ),
567            FeatureErrorKind::Multicollinearity {
568                correlation_threshold,
569            } => format!(
570                "High correlation (>{}) detected between features: {:?}. {}",
571                correlation_threshold, self.feature_indices, self.context
572            ),
573            FeatureErrorKind::OutOfRange { min, max } => format!(
574                "Features out of expected range [{}, {}] at indices: {:?}. {}",
575                min, max, self.feature_indices, self.context
576            ),
577            FeatureErrorKind::MissingFeatures => format!(
578                "Missing features at indices: {:?}. {}",
579                self.feature_indices, self.context
580            ),
581            FeatureErrorKind::CurseOfDimensionality {
582                n_features,
583                n_samples,
584            } => format!(
585                "Too many features ({}) relative to samples ({}). This may lead to overfitting. {}",
586                n_features, n_samples, self.context
587            ),
588            FeatureErrorKind::ScalingError { method } => format!(
589                "Feature scaling failed using method '{}' for features: {:?}. {}",
590                method, self.feature_indices, self.context
591            ),
592        }
593    }
594}
595
596impl MatrixError {
597    pub fn user_message(&self) -> String {
598        match &self.kind {
599            MatrixErrorKind::DimensionMismatch => format!(
600                "Matrix dimension mismatch during {}. Matrix is {}x{}.",
601                self.operation, self.matrix_info.dimensions.0, self.matrix_info.dimensions.1
602            ),
603            MatrixErrorKind::NotSquare => format!(
604                "Square matrix required for {} but got {}x{} matrix.",
605                self.operation, self.matrix_info.dimensions.0, self.matrix_info.dimensions.1
606            ),
607            MatrixErrorKind::NotSymmetric => format!(
608                "Symmetric matrix required for {} but matrix is not symmetric.",
609                self.operation
610            ),
611            MatrixErrorKind::NotPositiveDefinite => format!(
612                "Positive definite matrix required for {} but matrix is not positive definite.",
613                self.operation
614            ),
615            MatrixErrorKind::SparseOperationFailed => {
616                format!("Sparse matrix operation '{}' failed.", self.operation)
617            }
618            MatrixErrorKind::AllocationFailed { required_bytes } => format!(
619                "Failed to allocate {} bytes for matrix operation '{}'.",
620                required_bytes, self.operation
621            ),
622        }
623    }
624}
625
626impl CrossValidationError {
627    pub fn user_message(&self) -> String {
628        match &self.kind {
629            CrossValidationErrorKind::InsufficientData => {
630                "Insufficient data for cross-validation. Need more samples than number of folds."
631                    .to_string()
632            }
633            CrossValidationErrorKind::InvalidFolds => {
634                "Invalid cross-validation fold configuration.".to_string()
635            }
636            CrossValidationErrorKind::ImbalancedFold => {
637                if let Some(ref fold_info) = self.fold_info {
638                    format!(
639                        "Fold {}/{} contains imbalanced classes or missing classes.",
640                        fold_info.current_fold + 1,
641                        fold_info.total_folds
642                    )
643                } else {
644                    "Cross-validation fold contains imbalanced classes.".to_string()
645                }
646            }
647            CrossValidationErrorKind::ScoringFailed { metric } => {
648                format!("Cross-validation scoring failed for metric '{}'.", metric)
649            }
650            CrossValidationErrorKind::EarlyStoppingFailed => {
651                "Early stopping criteria could not be applied during cross-validation.".to_string()
652            }
653        }
654    }
655}
656
657impl ResourceError {
658    pub fn user_message(&self) -> String {
659        match &self.kind {
660            ResourceErrorKind::InsufficientMemory => {
661                if let (Some(required), Some(available)) = (
662                    self.resource_info.memory_required,
663                    self.resource_info.memory_available,
664                ) {
665                    format!(
666                        "Insufficient memory. Required: {} bytes, Available: {} bytes.",
667                        required, available
668                    )
669                } else {
670                    "Insufficient memory for operation.".to_string()
671                }
672            }
673            ResourceErrorKind::TimeoutExceeded => {
674                if let (Some(elapsed), Some(limit)) = (
675                    self.resource_info.time_elapsed,
676                    self.resource_info.time_limit,
677                ) {
678                    format!(
679                        "Operation timed out. Elapsed: {:?}, Limit: {:?}.",
680                        elapsed, limit
681                    )
682                } else {
683                    "Operation exceeded time limit.".to_string()
684                }
685            }
686            ResourceErrorKind::FileIoError { operation, path } => format!(
687                "File I/O error during '{}' operation on path: '{}'.",
688                operation, path
689            ),
690            ResourceErrorKind::NetworkError => {
691                "Network error occurred during distributed computation.".to_string()
692            }
693        }
694    }
695}
696
697impl fmt::Display for LinearModelError {
698    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
699        write!(f, "{}", self.user_message())
700    }
701}
702
703impl std::error::Error for LinearModelError {}
704
705impl From<LinearModelError> for SklearsError {
706    fn from(err: LinearModelError) -> Self {
707        match err {
708            LinearModelError::DataError(_) => SklearsError::InvalidInput(err.to_string()),
709            LinearModelError::ConfigurationError(_) => SklearsError::InvalidInput(err.to_string()),
710            LinearModelError::NumericalError(_) => SklearsError::NumericalError(err.to_string()),
711            LinearModelError::OptimizationError(_) => {
712                SklearsError::ConvergenceError { iterations: 0 }
713            }
714            LinearModelError::StateError(_) => SklearsError::NotFitted {
715                operation: err.to_string(),
716            },
717            LinearModelError::FeatureError(_) => SklearsError::InvalidInput(err.to_string()),
718            LinearModelError::MatrixError(_) => SklearsError::NumericalError(err.to_string()),
719            LinearModelError::CrossValidationError(_) => {
720                SklearsError::InvalidInput(err.to_string())
721            }
722            LinearModelError::ResourceError(_) => SklearsError::Other(err.to_string()),
723        }
724    }
725}
726
727/// Builder for creating specific error types
728pub struct ErrorBuilder;
729
730impl ErrorBuilder {
731    /// Create a data error
732    pub fn data_error(kind: DataErrorKind, context: &str) -> LinearModelError {
733        let suggestions = match &kind {
734            DataErrorKind::EmptyData => vec![
735                "Provide a dataset with at least one sample".to_string(),
736                "Check data loading pipeline".to_string(),
737            ],
738            DataErrorKind::DimensionMismatch { .. } => vec![
739                "Check input data dimensions".to_string(),
740                "Ensure X and y have compatible shapes".to_string(),
741            ],
742            DataErrorKind::InvalidValues { .. } => vec![
743                "Remove or impute NaN/infinity values".to_string(),
744                "Check data preprocessing pipeline".to_string(),
745            ],
746            DataErrorKind::MissingTargets => {
747                vec!["Provide target values (y) for supervised learning".to_string()]
748            }
749            DataErrorKind::InsufficientData { required, .. } => vec![
750                format!("Collect at least {} samples", required),
751                "Consider reducing model complexity".to_string(),
752            ],
753            DataErrorKind::IncompatibleDataType { expected, .. } => {
754                vec![format!("Convert data to {} type", expected)]
755            }
756            DataErrorKind::DataRangeError { min_required, .. } => vec![
757                format!("Ensure all values are >= {}", min_required),
758                "Consider data transformation".to_string(),
759            ],
760        };
761
762        LinearModelError::DataError(DataError {
763            kind,
764            context: context.to_string(),
765            suggestions,
766            error_location: None,
767        })
768    }
769
770    /// Create a numerical error
771    pub fn numerical_error(
772        kind: NumericalErrorKind,
773        operation: &str,
774        context: &str,
775    ) -> LinearModelError {
776        let recovery_suggestions = match &kind {
777            NumericalErrorKind::SingularMatrix { .. } => vec![
778                "Add regularization (Ridge, Lasso)".to_string(),
779                "Remove linearly dependent features".to_string(),
780                "Use pseudo-inverse instead of inverse".to_string(),
781            ],
782            NumericalErrorKind::IllConditioned { .. } => vec![
783                "Apply feature scaling/normalization".to_string(),
784                "Add regularization".to_string(),
785                "Use iterative refinement".to_string(),
786            ],
787            _ => vec![
788                "Try different solver".to_string(),
789                "Adjust numerical precision".to_string(),
790            ],
791        };
792
793        LinearModelError::NumericalError(NumericalError {
794            kind,
795            operation: operation.to_string(),
796            context: context.to_string(),
797            matrix_info: None,
798            recovery_suggestions,
799        })
800    }
801
802    /// Create an optimization error
803    pub fn optimization_error(
804        kind: OptimizationErrorKind,
805        algorithm: &str,
806        iteration: Option<usize>,
807        max_iterations: Option<usize>,
808    ) -> LinearModelError {
809        let suggestions = match &kind {
810            OptimizationErrorKind::ConvergenceFailed => vec![
811                "Increase max_iterations".to_string(),
812                "Adjust convergence tolerance".to_string(),
813                "Try different solver".to_string(),
814                "Scale features".to_string(),
815            ],
816            OptimizationErrorKind::LocalMinimum => vec![
817                "Use different initialization".to_string(),
818                "Try global optimization method".to_string(),
819                "Add regularization".to_string(),
820            ],
821            _ => vec![
822                "Adjust learning rate".to_string(),
823                "Try different optimization algorithm".to_string(),
824            ],
825        };
826
827        LinearModelError::OptimizationError(OptimizationError {
828            kind,
829            algorithm: algorithm.to_string(),
830            iteration,
831            max_iterations,
832            convergence_info: None,
833            suggestions,
834        })
835    }
836}
837
838#[allow(non_snake_case)]
839#[cfg(test)]
840mod tests {
841    use super::*;
842
843    #[test]
844    fn test_data_error_creation() {
845        let error = ErrorBuilder::data_error(DataErrorKind::EmptyData, "Test context");
846
847        assert!(matches!(error, LinearModelError::DataError(_)));
848        assert_eq!(error.severity(), ErrorSeverity::Critical);
849        assert!(!error.recovery_suggestions().is_empty());
850    }
851
852    #[test]
853    fn test_numerical_error_creation() {
854        let error = ErrorBuilder::numerical_error(
855            NumericalErrorKind::SingularMatrix {
856                condition_number: Some(1e-15),
857            },
858            "matrix inversion",
859            "During normal equations solve",
860        );
861
862        assert!(matches!(error, LinearModelError::NumericalError(_)));
863        assert_eq!(error.severity(), ErrorSeverity::High);
864        assert!(error.is_recoverable());
865    }
866
867    #[test]
868    fn test_optimization_error_creation() {
869        let error = ErrorBuilder::optimization_error(
870            OptimizationErrorKind::ConvergenceFailed,
871            "L-BFGS",
872            Some(100),
873            Some(100),
874        );
875
876        assert!(matches!(error, LinearModelError::OptimizationError(_)));
877        assert_eq!(error.severity(), ErrorSeverity::Medium);
878        assert!(error.is_recoverable());
879    }
880
881    #[test]
882    fn test_error_conversion_to_skl_error() {
883        let linear_error = ErrorBuilder::data_error(DataErrorKind::EmptyData, "Test");
884
885        let skl_error: SklearsError = linear_error.into();
886        assert!(matches!(skl_error, SklearsError::InvalidInput(_)));
887    }
888
889    #[test]
890    fn test_user_message_formatting() {
891        let error = ErrorBuilder::data_error(
892            DataErrorKind::DimensionMismatch {
893                expected: vec![100, 10],
894                actual: vec![100, 5],
895            },
896            "Training data validation",
897        );
898
899        let message = error.user_message();
900        assert!(message.contains("dimensions don't match"));
901        assert!(message.contains("[100, 10]"));
902        assert!(message.contains("[100, 5]"));
903    }
904
905    #[test]
906    fn test_error_severity_ordering() {
907        assert!(ErrorSeverity::Critical > ErrorSeverity::High);
908        assert!(ErrorSeverity::High > ErrorSeverity::Medium);
909        assert!(ErrorSeverity::Medium > ErrorSeverity::Low);
910    }
911}