Skip to main content

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}