1#[cfg(not(feature = "std"))]
16use core::fmt;
17
18#[cfg(feature = "std")]
19use thiserror::Error;
20
21use oxigdal_core::OxiGdalError;
22
23pub type Result<T> = core::result::Result<T, AlgorithmError>;
25
26#[derive(Debug)]
28#[cfg_attr(feature = "std", derive(Error))]
29pub enum AlgorithmError {
30 #[cfg_attr(feature = "std", error("Core error: {0}"))]
32 Core(#[from] OxiGdalError),
33
34 #[cfg_attr(
36 feature = "std",
37 error("Invalid dimensions: {message} (got {actual}, expected {expected})")
38 )]
39 InvalidDimensions {
40 message: &'static str,
42 actual: usize,
44 expected: usize,
46 },
47
48 #[cfg_attr(feature = "std", error("Empty input: {operation}"))]
50 EmptyInput {
51 operation: &'static str,
53 },
54
55 #[cfg_attr(feature = "std", error("Invalid input: {0}"))]
57 InvalidInput(String),
58
59 #[cfg_attr(feature = "std", error("Invalid parameter '{parameter}': {message}"))]
61 InvalidParameter {
62 parameter: &'static str,
64 message: String,
66 },
67
68 #[cfg_attr(feature = "std", error("Invalid geometry: {0}"))]
70 InvalidGeometry(String),
71
72 #[cfg_attr(
74 feature = "std",
75 error("Incompatible data types: {source_type} and {target_type}")
76 )]
77 IncompatibleTypes {
78 source_type: &'static str,
80 target_type: &'static str,
82 },
83
84 #[cfg_attr(feature = "std", error("Insufficient data for {operation}: {message}"))]
86 InsufficientData {
87 operation: &'static str,
89 message: String,
91 },
92
93 #[cfg_attr(feature = "std", error("Numerical error in {operation}: {message}"))]
95 NumericalError {
96 operation: &'static str,
98 message: String,
100 },
101
102 #[cfg_attr(feature = "std", error("Computation error: {0}"))]
104 ComputationError(String),
105
106 #[cfg_attr(feature = "std", error("Geometry error: {message}"))]
108 GeometryError {
109 message: String,
111 },
112
113 #[cfg_attr(feature = "std", error("Unsupported operation: {operation}"))]
115 UnsupportedOperation {
116 operation: String,
118 },
119
120 #[cfg_attr(feature = "std", error("Memory allocation failed: {message}"))]
122 AllocationFailed {
123 message: String,
125 },
126
127 #[cfg_attr(
129 feature = "std",
130 error("SIMD instructions not available on this platform")
131 )]
132 SimdNotAvailable,
133
134 #[cfg_attr(feature = "std", error("Path not found: {0}"))]
136 PathNotFound(String),
137}
138
139#[cfg(not(feature = "std"))]
140impl fmt::Display for AlgorithmError {
141 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142 match self {
143 Self::Core(e) => write!(f, "Core error: {e}"),
144 Self::InvalidDimensions {
145 message,
146 actual,
147 expected,
148 } => write!(
149 f,
150 "Invalid dimensions: {message} (got {actual}, expected {expected})"
151 ),
152 Self::EmptyInput { operation } => write!(f, "Empty input: {operation}"),
153 Self::InvalidInput(message) => write!(f, "Invalid input: {message}"),
154 Self::InvalidParameter { parameter, message } => {
155 write!(f, "Invalid parameter '{parameter}': {message}")
156 }
157 Self::InvalidGeometry(message) => write!(f, "Invalid geometry: {message}"),
158 Self::IncompatibleTypes {
159 source_type,
160 target_type,
161 } => write!(f, "Incompatible types: {source_type} and {target_type}"),
162 Self::InsufficientData { operation, message } => {
163 write!(f, "Insufficient data for {operation}: {message}")
164 }
165 Self::NumericalError { operation, message } => {
166 write!(f, "Numerical error in {operation}: {message}")
167 }
168 Self::ComputationError(message) => {
169 write!(f, "Computation error: {message}")
170 }
171 Self::GeometryError { message } => write!(f, "Geometry error: {message}"),
172 Self::UnsupportedOperation { operation } => {
173 write!(f, "Unsupported operation: {operation}")
174 }
175 Self::AllocationFailed { message } => {
176 write!(f, "Memory allocation failed: {message}")
177 }
178 Self::SimdNotAvailable => write!(f, "SIMD instructions not available"),
179 Self::PathNotFound(message) => write!(f, "Path not found: {message}"),
180 }
181 }
182}
183
184impl AlgorithmError {
185 pub fn code(&self) -> &'static str {
190 match self {
191 Self::Core(_) => "A001",
192 Self::InvalidDimensions { .. } => "A002",
193 Self::EmptyInput { .. } => "A003",
194 Self::InvalidInput(_) => "A004",
195 Self::InvalidParameter { .. } => "A005",
196 Self::InvalidGeometry(_) => "A006",
197 Self::IncompatibleTypes { .. } => "A007",
198 Self::InsufficientData { .. } => "A008",
199 Self::NumericalError { .. } => "A009",
200 Self::ComputationError(_) => "A010",
201 Self::GeometryError { .. } => "A011",
202 Self::UnsupportedOperation { .. } => "A012",
203 Self::AllocationFailed { .. } => "A013",
204 Self::SimdNotAvailable => "A014",
205 Self::PathNotFound(_) => "A015",
206 }
207 }
208
209 pub fn suggestion(&self) -> Option<&'static str> {
213 match self {
214 Self::Core(_) => Some("Check the underlying error for details"),
215 Self::InvalidDimensions { message, .. } => {
216 if message.contains("window") {
218 Some(
219 "Window size must be odd. Try adjusting to the nearest odd number (e.g., 3, 5, 7)",
220 )
221 } else if message.contains("kernel") {
222 Some("Kernel size must be odd and positive. Common values are 3, 5, 7, or 9")
223 } else {
224 Some("Check that array dimensions match the expected shape")
225 }
226 }
227 Self::EmptyInput { .. } => Some("Provide at least one data point or feature"),
228 Self::InvalidInput(_) => Some("Verify input data format and values are correct"),
229 Self::InvalidParameter { parameter, message } => {
230 if parameter.contains("window") || parameter.contains("kernel") {
232 Some("Window/kernel size must be odd and positive (e.g., 3, 5, 7)")
233 } else if parameter.contains("threshold") {
234 Some("Threshold values are typically between 0.0 and 1.0")
235 } else if parameter.contains("radius") {
236 Some("Radius must be positive and reasonable for your data resolution")
237 } else if parameter.contains("iterations") {
238 Some("Number of iterations must be positive (typically 1-1000)")
239 } else if message.contains("odd") {
240 Some("Value must be odd. Try the next odd number (current±1)")
241 } else if message.contains("positive") {
242 Some("Value must be greater than zero")
243 } else if message.contains("range") {
244 Some("Value must be within the specified range")
245 } else {
246 Some("Check parameter documentation for valid values and constraints")
247 }
248 }
249 Self::InvalidGeometry(_) => Some("Verify geometry is valid and not self-intersecting"),
250 Self::IncompatibleTypes { .. } => {
251 Some("Convert data to compatible types before processing")
252 }
253 Self::InsufficientData { .. } => Some("Provide more data points for reliable results"),
254 Self::NumericalError { .. } => {
255 Some("Check for division by zero, overflow, or invalid mathematical operations")
256 }
257 Self::ComputationError(_) => Some("Verify input data is within acceptable ranges"),
258 Self::GeometryError { .. } => Some("Check geometry validity and topology"),
259 Self::UnsupportedOperation { .. } => {
260 Some("Use a different algorithm or enable required features")
261 }
262 Self::AllocationFailed { .. } => Some("Reduce data size or increase available memory"),
263 Self::SimdNotAvailable => Some(
264 "SIMD operations are not supported on this CPU. The algorithm will use scalar fallback",
265 ),
266 Self::PathNotFound(_) => Some("Verify the path exists and is accessible"),
267 }
268 }
269
270 pub fn context(&self) -> ErrorContext {
274 match self {
275 Self::Core(e) => ErrorContext::new("core_error").with_detail("error", e.to_string()),
276 Self::InvalidDimensions {
277 message,
278 actual,
279 expected,
280 } => ErrorContext::new("invalid_dimensions")
281 .with_detail("message", message.to_string())
282 .with_detail("actual", actual.to_string())
283 .with_detail("expected", expected.to_string()),
284 Self::EmptyInput { operation } => {
285 ErrorContext::new("empty_input").with_detail("operation", operation.to_string())
286 }
287 Self::InvalidInput(msg) => {
288 ErrorContext::new("invalid_input").with_detail("message", msg.clone())
289 }
290 Self::InvalidParameter { parameter, message } => ErrorContext::new("invalid_parameter")
291 .with_detail("parameter", parameter.to_string())
292 .with_detail("message", message.clone()),
293 Self::InvalidGeometry(msg) => {
294 ErrorContext::new("invalid_geometry").with_detail("message", msg.clone())
295 }
296 Self::IncompatibleTypes {
297 source_type,
298 target_type,
299 } => ErrorContext::new("incompatible_types")
300 .with_detail("source_type", source_type.to_string())
301 .with_detail("target_type", target_type.to_string()),
302 Self::InsufficientData { operation, message } => ErrorContext::new("insufficient_data")
303 .with_detail("operation", operation.to_string())
304 .with_detail("message", message.clone()),
305 Self::NumericalError { operation, message } => ErrorContext::new("numerical_error")
306 .with_detail("operation", operation.to_string())
307 .with_detail("message", message.clone()),
308 Self::ComputationError(msg) => {
309 ErrorContext::new("computation_error").with_detail("message", msg.clone())
310 }
311 Self::GeometryError { message } => {
312 ErrorContext::new("geometry_error").with_detail("message", message.clone())
313 }
314 Self::UnsupportedOperation { operation } => ErrorContext::new("unsupported_operation")
315 .with_detail("operation", operation.clone()),
316 Self::AllocationFailed { message } => {
317 ErrorContext::new("allocation_failed").with_detail("message", message.clone())
318 }
319 Self::SimdNotAvailable => ErrorContext::new("simd_not_available"),
320 Self::PathNotFound(path) => {
321 ErrorContext::new("path_not_found").with_detail("path", path.clone())
322 }
323 }
324 }
325}
326
327#[derive(Debug, Clone)]
329pub struct ErrorContext {
330 pub category: &'static str,
332 pub details: Vec<(String, String)>,
334}
335
336impl ErrorContext {
337 pub fn new(category: &'static str) -> Self {
339 Self {
340 category,
341 details: Vec::new(),
342 }
343 }
344
345 pub fn with_detail(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
347 self.details.push((key.into(), value.into()));
348 self
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355
356 #[test]
357 fn test_error_display() {
358 let err = AlgorithmError::InvalidParameter {
359 parameter: "window_size",
360 message: "must be positive".to_string(),
361 };
362 let s = format!("{err}");
363 assert!(s.contains("window_size"));
364 assert!(s.contains("must be positive"));
365 }
366
367 #[test]
368 fn test_error_from_core() {
369 let core_err = OxiGdalError::OutOfBounds {
370 message: "test".to_string(),
371 };
372 let _alg_err: AlgorithmError = core_err.into();
373 }
374
375 #[test]
376 fn test_invalid_input() {
377 let err = AlgorithmError::InvalidInput("test input".to_string());
378 let s = format!("{err}");
379 assert!(s.contains("Invalid input"));
380 assert!(s.contains("test input"));
381 }
382
383 #[test]
384 fn test_invalid_geometry() {
385 let err = AlgorithmError::InvalidGeometry("test geometry".to_string());
386 let s = format!("{err}");
387 assert!(s.contains("Invalid geometry"));
388 assert!(s.contains("test geometry"));
389 }
390
391 #[test]
392 fn test_computation_error() {
393 let err = AlgorithmError::ComputationError("test error".to_string());
394 let s = format!("{err}");
395 assert!(s.contains("Computation error"));
396 assert!(s.contains("test error"));
397 }
398
399 #[test]
400 fn test_error_codes() {
401 let err = AlgorithmError::InvalidParameter {
402 parameter: "window_size",
403 message: "must be odd".to_string(),
404 };
405 assert_eq!(err.code(), "A005");
406
407 let err = AlgorithmError::InvalidDimensions {
408 message: "mismatched",
409 actual: 4,
410 expected: 5,
411 };
412 assert_eq!(err.code(), "A002");
413
414 let err = AlgorithmError::SimdNotAvailable;
415 assert_eq!(err.code(), "A014");
416 }
417
418 #[test]
419 fn test_error_suggestions() {
420 let err = AlgorithmError::InvalidParameter {
421 parameter: "window_size",
422 message: "must be odd".to_string(),
423 };
424 assert!(err.suggestion().is_some());
425 assert!(err.suggestion().is_some_and(|s| s.contains("odd")));
426
427 let err = AlgorithmError::InvalidParameter {
428 parameter: "kernel_size",
429 message: "invalid".to_string(),
430 };
431 assert!(err.suggestion().is_some());
432 assert!(err.suggestion().is_some_and(|s| s.contains("kernel")));
433
434 let err = AlgorithmError::InvalidDimensions {
435 message: "window size",
436 actual: 4,
437 expected: 3,
438 };
439 assert!(err.suggestion().is_some());
440 assert!(
441 err.suggestion()
442 .is_some_and(|s| s.contains("Window size must be odd"))
443 );
444 }
445
446 #[test]
447 fn test_error_context() {
448 let err = AlgorithmError::InvalidParameter {
449 parameter: "window_size",
450 message: "must be odd, got 4. Try 3 or 5".to_string(),
451 };
452 let ctx = err.context();
453 assert_eq!(ctx.category, "invalid_parameter");
454 assert!(
455 ctx.details
456 .iter()
457 .any(|(k, v)| k == "parameter" && v == "window_size")
458 );
459 assert!(ctx.details.iter().any(|(k, _)| k == "message"));
460
461 let err = AlgorithmError::InvalidDimensions {
462 message: "array size mismatch",
463 actual: 100,
464 expected: 200,
465 };
466 let ctx = err.context();
467 assert_eq!(ctx.category, "invalid_dimensions");
468 assert!(ctx.details.iter().any(|(k, v)| k == "actual" && v == "100"));
469 assert!(
470 ctx.details
471 .iter()
472 .any(|(k, v)| k == "expected" && v == "200")
473 );
474 }
475
476 #[test]
477 fn test_parameter_suggestion_specificity() {
478 let err = AlgorithmError::InvalidParameter {
480 parameter: "window_size",
481 message: "test".to_string(),
482 };
483 let suggestion = err.suggestion();
484 assert!(suggestion.is_some_and(|s| s.contains("Window") || s.contains("kernel")));
485
486 let err = AlgorithmError::InvalidParameter {
488 parameter: "threshold",
489 message: "test".to_string(),
490 };
491 let suggestion = err.suggestion();
492 assert!(suggestion.is_some_and(|s| s.contains("0.0") && s.contains("1.0")));
493 }
494}