tensorlogic_sklears_kernels/kernel_pca/error.rs
1//! Error type for the Kernel PCA research-preview module.
2//!
3//! This is intentionally separate from the crate-wide
4//! [`crate::error::KernelError`]: the Kernel PCA API exposes a narrow set
5//! of failure modes (fit-before-transform, bad dimensions, degenerate
6//! eigenproblems, ...) that are easier to pattern-match on via a
7//! dedicated enum.
8//!
9//! The error implements [`std::error::Error`] via `thiserror`, so it
10//! converts cleanly into `anyhow::Error` or any other boxed-error type a
11//! caller might already be using.
12
13use thiserror::Error;
14
15/// Result alias used throughout the [`crate::kernel_pca`] module.
16pub type KernelPcaResult<T> = std::result::Result<T, KernelPcaError>;
17
18/// Failure modes for [`crate::kernel_pca::KernelPCA`] and friends.
19///
20/// Variants are deliberately coarse — one variant per *kind* of failure
21/// — and each carries a `String` message that names the offending
22/// argument. The five variants listed below match the
23/// v0.2.0 research-preview contract.
24#[derive(Debug, Error)]
25pub enum KernelPcaError {
26 /// Operation was requested against a model that has not been fitted
27 /// yet (e.g. `transform` before `fit`).
28 #[error("Kernel PCA model has not been fitted yet: {0}")]
29 NotFitted(String),
30
31 /// Input violates an invariant of the Kernel PCA API — empty
32 /// training set, zero-dimensional feature vector, `n_components = 0`
33 /// requested, etc.
34 #[error("Invalid input to Kernel PCA: {0}")]
35 InvalidInput(String),
36
37 /// The underlying symmetric eigensolver refused the problem or
38 /// failed numerically. The inner `String` is the message reported
39 /// by the solver.
40 #[error("Eigendecomposition of the centered Gram matrix failed: {0}")]
41 EigendecompositionFailed(String),
42
43 /// Vector or matrix supplied to `transform` has a different feature
44 /// dimension than the data used at `fit` time.
45 #[error(
46 "Dimension mismatch: expected feature dimension {expected}, got {got} (context: {context})"
47 )]
48 DimensionMismatch {
49 /// Feature dimension that was recorded at `fit` time.
50 expected: usize,
51 /// Feature dimension that was supplied at `transform` time.
52 got: usize,
53 /// Human-readable description of where the mismatch occurred.
54 context: String,
55 },
56
57 /// The eigendecomposition produced fewer positive eigenvalues than
58 /// the number of components requested by the caller — i.e. the
59 /// kernel matrix is effectively low-rank and the requested KPCA
60 /// embedding dimension is unreachable.
61 #[error(
62 "Requested {requested} components but only {available} positive eigenvalues are available"
63 )]
64 InsufficientComponents {
65 /// `n_components` passed to the config.
66 requested: usize,
67 /// Number of eigenvalues above the positivity threshold.
68 available: usize,
69 },
70}
71
72impl KernelPcaError {
73 /// Convenience constructor used throughout the module to wrap a
74 /// [`crate::error::KernelError`] produced by a kernel evaluation
75 /// into a [`KernelPcaError::InvalidInput`] with the kernel's error
76 /// message preserved. Kernel failures during KPCA are almost always
77 /// caused by bad input (wrong feature dimension, NaN, ...), not by
78 /// internal KPCA bugs, so this mapping is usually the right one.
79 pub(crate) fn from_kernel(err: crate::error::KernelError) -> Self {
80 KernelPcaError::InvalidInput(format!("kernel evaluation failed: {}", err))
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn not_fitted_display_contains_context() {
90 let err = KernelPcaError::NotFitted("transform".to_string());
91 let msg = err.to_string();
92 assert!(msg.contains("not been fitted"));
93 assert!(msg.contains("transform"));
94 }
95
96 #[test]
97 fn dimension_mismatch_display_mentions_both_sides() {
98 let err = KernelPcaError::DimensionMismatch {
99 expected: 3,
100 got: 5,
101 context: "transform input".to_string(),
102 };
103 let msg = err.to_string();
104 assert!(msg.contains("3"));
105 assert!(msg.contains("5"));
106 assert!(msg.contains("transform input"));
107 }
108
109 #[test]
110 fn insufficient_components_display_is_informative() {
111 let err = KernelPcaError::InsufficientComponents {
112 requested: 10,
113 available: 3,
114 };
115 let msg = err.to_string();
116 assert!(msg.contains("10"));
117 assert!(msg.contains("3"));
118 }
119
120 #[test]
121 fn from_kernel_wraps_message() {
122 let kernel_err = crate::error::KernelError::ComputationError("boom".to_string());
123 let pca_err = KernelPcaError::from_kernel(kernel_err);
124 let msg = pca_err.to_string();
125 assert!(msg.contains("boom"));
126 }
127}