Skip to main content

shap_rs/
error.rs

1//! Error types used throughout `shap-rs`.
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6/// The result type used by `shap-rs`.
7pub type Result<T> = std::result::Result<T, ShapError>;
8
9pub(crate) fn checked_f64_shape(dimensions: &[usize], context: &str) -> Result<()> {
10    let elements = dimensions
11        .iter()
12        .try_fold(1usize, |size, dimension| size.checked_mul(*dimension));
13    let bytes = elements.and_then(|size| size.checked_mul(std::mem::size_of::<f64>()));
14    if !matches!(bytes, Some(size) if size <= isize::MAX as usize) {
15        return Err(ShapError::InvalidConfiguration(format!(
16            "{context} dimensions overflow the addressable allocation size"
17        )));
18    }
19    Ok(())
20}
21
22/// Errors that can occur while constructing or evaluating SHAP explainers.
23#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24pub enum ShapError {
25    /// Two or more arrays have incompatible dimensions.
26    DimensionMismatch { expected: String, found: String },
27
28    /// A feature index is outside the valid feature range.
29    InvalidFeatureIndex { index: usize, n_features: usize },
30
31    /// A sample index is outside the valid sample range.
32    InvalidSampleIndex { index: usize, n_samples: usize },
33
34    /// An output index is outside the valid model-output range.
35    InvalidOutputIndex { index: usize, n_outputs: usize },
36
37    /// The supplied data contains no samples.
38    EmptyData,
39
40    /// The supplied background dataset contains no samples.
41    EmptyBackground,
42
43    /// An invalid configuration was supplied to an explainer or component.
44    InvalidConfiguration(String),
45
46    /// A model prediction failed.
47    ModelError(String),
48
49    /// A masking operation failed.
50    MaskerError(String),
51
52    /// A numerical operation failed.
53    NumericalError(String),
54
55    /// A linear-system or weighted least-squares solver failed.
56    SolverError(String),
57
58    /// The requested functionality is not supported.
59    Unsupported(String),
60
61    /// An explanation failed the SHAP additivity check.
62    AdditivityError {
63        expected: f64,
64        actual: f64,
65        difference: f64,
66        tolerance: f64,
67    },
68
69    /// A required feature name or other metadata item was missing.
70    MissingMetadata(String),
71
72    /// An operation was requested with incompatible output dimensions.
73    OutputDimensionMismatch { expected: usize, found: usize },
74
75    /// An underlying error that does not have a more specific SHAP error type.
76    Other(String),
77}
78
79impl fmt::Display for ShapError {
80    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        match self {
82            Self::DimensionMismatch { expected, found } => {
83                write!(f, "dimension mismatch: expected {expected}, found {found}")
84            }
85
86            Self::InvalidFeatureIndex { index, n_features } => {
87                write!(
88                    f,
89                    "invalid feature index {index}; dataset contains {n_features} features"
90                )
91            }
92
93            Self::InvalidSampleIndex { index, n_samples } => {
94                write!(
95                    f,
96                    "invalid sample index {index}; explanation contains {n_samples} samples"
97                )
98            }
99
100            Self::InvalidOutputIndex { index, n_outputs } => {
101                write!(
102                    f,
103                    "invalid output index {index}; explanation contains {n_outputs} outputs"
104                )
105            }
106
107            Self::EmptyData => {
108                write!(f, "input data is empty")
109            }
110
111            Self::EmptyBackground => {
112                write!(f, "background dataset is empty")
113            }
114
115            Self::InvalidConfiguration(message) => {
116                write!(f, "invalid configuration: {message}")
117            }
118
119            Self::ModelError(message) => {
120                write!(f, "model error: {message}")
121            }
122
123            Self::MaskerError(message) => {
124                write!(f, "masker error: {message}")
125            }
126
127            Self::NumericalError(message) => {
128                write!(f, "numerical error: {message}")
129            }
130
131            Self::SolverError(message) => {
132                write!(f, "solver error: {message}")
133            }
134
135            Self::Unsupported(message) => {
136                write!(f, "unsupported operation: {message}")
137            }
138
139            Self::AdditivityError {
140                expected,
141                actual,
142                difference,
143                tolerance,
144            } => {
145                write!(
146                    f,
147                    "SHAP additivity check failed: expected model output \
148                     {expected:.12}, reconstructed output {actual:.12}, \
149                     difference {difference:.12}, tolerance {tolerance:.12}"
150                )
151            }
152
153            Self::MissingMetadata(message) => {
154                write!(f, "missing metadata: {message}")
155            }
156
157            Self::OutputDimensionMismatch { expected, found } => {
158                write!(
159                    f,
160                    "output dimension mismatch: expected {expected}, found {found}"
161                )
162            }
163
164            Self::Other(message) => {
165                write!(f, "{message}")
166            }
167        }
168    }
169}
170
171impl std::error::Error for ShapError {}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn displays_dimension_mismatch() {
179        let error = ShapError::DimensionMismatch {
180            expected: "10 features".to_string(),
181            found: "8 features".to_string(),
182        };
183
184        assert_eq!(
185            error.to_string(),
186            "dimension mismatch: expected 10 features, found 8 features"
187        );
188    }
189
190    #[test]
191    fn displays_invalid_feature_index() {
192        let error = ShapError::InvalidFeatureIndex {
193            index: 10,
194            n_features: 5,
195        };
196
197        assert_eq!(
198            error.to_string(),
199            "invalid feature index 10; dataset contains 5 features"
200        );
201    }
202
203    #[test]
204    fn displays_invalid_sample_and_output_indices() {
205        assert_eq!(
206            ShapError::InvalidSampleIndex {
207                index: 3,
208                n_samples: 2
209            }
210            .to_string(),
211            "invalid sample index 3; explanation contains 2 samples"
212        );
213        assert_eq!(
214            ShapError::InvalidOutputIndex {
215                index: 2,
216                n_outputs: 1
217            }
218            .to_string(),
219            "invalid output index 2; explanation contains 1 outputs"
220        );
221    }
222
223    #[test]
224    fn displays_additivity_error() {
225        let error = ShapError::AdditivityError {
226            expected: 0.8,
227            actual: 0.7,
228            difference: 0.1,
229            tolerance: 1e-6,
230        };
231
232        let message = error.to_string();
233
234        assert!(message.contains("SHAP additivity check failed"));
235        assert!(message.contains("0.800000000000"));
236        assert!(message.contains("0.700000000000"));
237    }
238
239    #[test]
240    fn result_alias_works() {
241        fn returns_result() -> Result<()> {
242            Ok(())
243        }
244
245        assert!(returns_result().is_ok());
246    }
247
248    #[test]
249    fn rejects_overflowing_or_unaddressable_shapes() {
250        assert!(checked_f64_shape(&[usize::MAX, 2], "test").is_err());
251        assert!(checked_f64_shape(&[isize::MAX as usize / 8 + 1], "test").is_err());
252        assert!(checked_f64_shape(&[2, 3, 4], "test").is_ok());
253    }
254}