1use serde::{Deserialize, Serialize};
4use std::fmt;
5
6pub 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24pub enum ShapError {
25 DimensionMismatch { expected: String, found: String },
27
28 InvalidFeatureIndex { index: usize, n_features: usize },
30
31 InvalidSampleIndex { index: usize, n_samples: usize },
33
34 InvalidOutputIndex { index: usize, n_outputs: usize },
36
37 EmptyData,
39
40 EmptyBackground,
42
43 InvalidConfiguration(String),
45
46 ModelError(String),
48
49 MaskerError(String),
51
52 NumericalError(String),
54
55 SolverError(String),
57
58 Unsupported(String),
60
61 AdditivityError {
63 expected: f64,
64 actual: f64,
65 difference: f64,
66 tolerance: f64,
67 },
68
69 MissingMetadata(String),
71
72 OutputDimensionMismatch { expected: usize, found: usize },
74
75 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}