1use sklears_core::error::SklearsError;
7use std::fmt;
8
9#[derive(Debug, Clone)]
11pub enum LinearModelError {
12 DataError(DataError),
14 ConfigurationError(ConfigurationError),
16 NumericalError(NumericalError),
18 OptimizationError(OptimizationError),
20 StateError(StateError),
22 FeatureError(FeatureError),
24 MatrixError(MatrixError),
26 CrossValidationError(CrossValidationError),
28 ResourceError(ResourceError),
30}
31
32#[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 EmptyData,
45 DimensionMismatch {
47 expected: Vec<usize>,
48 actual: Vec<usize>,
49 },
50 InvalidValues { count: usize, total: usize },
52 MissingTargets,
54 InsufficientData { required: usize, available: usize },
56 IncompatibleDataType { expected: String, actual: String },
58 DataRangeError { min_required: f64, actual_min: f64 },
60}
61
62#[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 InvalidParameter,
76 OutOfRange,
78 IncompatibleParameters { conflicting_params: Vec<String> },
80 MissingParameter,
82 DeprecatedParameter { replacement: Option<String> },
84}
85
86#[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 SingularMatrix { condition_number: Option<f64> },
100 Overflow,
102 Underflow,
104 PrecisionLoss { digits_lost: usize },
106 IllConditioned { condition_number: f64 },
108 MatrixInversionFailed,
110 EigenvalueFailed,
112 CholeskyFailed,
114}
115
116#[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#[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 ConvergenceFailed,
142 LocalMinimum,
144 NoProgress,
146 StepSizeTooSmall,
148 GradientFailed,
150 HessianFailed,
152 LineSearchFailed,
154 InvalidDirection,
156 InvalidProblemDimensions,
158 ModelNotFitted,
160}
161
162#[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#[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 NotFitted,
185 AlreadyFitted,
187 InvalidStateTransition,
189 OperationNotAvailable,
191}
192
193#[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 ZeroVariance,
206 Multicollinearity { correlation_threshold: f64 },
208 OutOfRange { min: f64, max: f64 },
210 MissingFeatures,
212 CurseOfDimensionality { n_features: usize, n_samples: usize },
214 ScalingError { method: String },
216}
217
218#[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 DimensionMismatch,
231 NotSquare,
233 NotSymmetric,
235 NotPositiveDefinite,
237 SparseOperationFailed,
239 AllocationFailed { required_bytes: usize },
241}
242
243#[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 InsufficientData,
255 InvalidFolds,
257 ImbalancedFold,
259 ScoringFailed { metric: String },
261 EarlyStoppingFailed,
263}
264
265#[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#[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 InsufficientMemory,
287 TimeoutExceeded,
289 FileIoError { operation: String, path: String },
291 NetworkError,
293}
294
295#[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 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 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 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 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#[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
727pub struct ErrorBuilder;
729
730impl ErrorBuilder {
731 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 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 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}