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 #[cfg_attr(
145 feature = "std",
146 error(
147 "Expression nesting too deep in {context}: depth {depth} exceeds the maximum of {max}"
148 )
149 )]
150 NestingTooDeep {
151 context: &'static str,
153 depth: usize,
155 max: usize,
157 },
158}
159
160#[cfg(not(feature = "std"))]
161impl fmt::Display for AlgorithmError {
162 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163 match self {
164 Self::Core(e) => write!(f, "Core error: {e}"),
165 Self::InvalidDimensions {
166 message,
167 actual,
168 expected,
169 } => write!(
170 f,
171 "Invalid dimensions: {message} (got {actual}, expected {expected})"
172 ),
173 Self::EmptyInput { operation } => write!(f, "Empty input: {operation}"),
174 Self::InvalidInput(message) => write!(f, "Invalid input: {message}"),
175 Self::InvalidParameter { parameter, message } => {
176 write!(f, "Invalid parameter '{parameter}': {message}")
177 }
178 Self::InvalidGeometry(message) => write!(f, "Invalid geometry: {message}"),
179 Self::IncompatibleTypes {
180 source_type,
181 target_type,
182 } => write!(f, "Incompatible types: {source_type} and {target_type}"),
183 Self::InsufficientData { operation, message } => {
184 write!(f, "Insufficient data for {operation}: {message}")
185 }
186 Self::NumericalError { operation, message } => {
187 write!(f, "Numerical error in {operation}: {message}")
188 }
189 Self::ComputationError(message) => {
190 write!(f, "Computation error: {message}")
191 }
192 Self::GeometryError { message } => write!(f, "Geometry error: {message}"),
193 Self::UnsupportedOperation { operation } => {
194 write!(f, "Unsupported operation: {operation}")
195 }
196 Self::AllocationFailed { message } => {
197 write!(f, "Memory allocation failed: {message}")
198 }
199 Self::SimdNotAvailable => write!(f, "SIMD instructions not available"),
200 Self::PathNotFound(message) => write!(f, "Path not found: {message}"),
201 Self::NestingTooDeep {
202 context,
203 depth,
204 max,
205 } => write!(
206 f,
207 "Expression nesting too deep in {context}: depth {depth} exceeds the maximum of {max}"
208 ),
209 }
210 }
211}
212
213impl AlgorithmError {
214 pub fn code(&self) -> &'static str {
219 match self {
220 Self::Core(_) => "A001",
221 Self::InvalidDimensions { .. } => "A002",
222 Self::EmptyInput { .. } => "A003",
223 Self::InvalidInput(_) => "A004",
224 Self::InvalidParameter { .. } => "A005",
225 Self::InvalidGeometry(_) => "A006",
226 Self::IncompatibleTypes { .. } => "A007",
227 Self::InsufficientData { .. } => "A008",
228 Self::NumericalError { .. } => "A009",
229 Self::ComputationError(_) => "A010",
230 Self::GeometryError { .. } => "A011",
231 Self::UnsupportedOperation { .. } => "A012",
232 Self::AllocationFailed { .. } => "A013",
233 Self::SimdNotAvailable => "A014",
234 Self::PathNotFound(_) => "A015",
235 Self::NestingTooDeep { .. } => "A016",
236 }
237 }
238
239 pub fn suggestion(&self) -> Option<&'static str> {
243 match self {
244 Self::Core(_) => Some("Check the underlying error for details"),
245 Self::InvalidDimensions { message, .. } => {
246 if message.contains("window") {
248 Some(
249 "Window size must be odd. Try adjusting to the nearest odd number (e.g., 3, 5, 7)",
250 )
251 } else if message.contains("kernel") {
252 Some("Kernel size must be odd and positive. Common values are 3, 5, 7, or 9")
253 } else {
254 Some("Check that array dimensions match the expected shape")
255 }
256 }
257 Self::EmptyInput { .. } => Some("Provide at least one data point or feature"),
258 Self::InvalidInput(_) => Some("Verify input data format and values are correct"),
259 Self::InvalidParameter { parameter, message } => {
260 if parameter.contains("window") || parameter.contains("kernel") {
262 Some("Window/kernel size must be odd and positive (e.g., 3, 5, 7)")
263 } else if parameter.contains("threshold") {
264 Some("Threshold values are typically between 0.0 and 1.0")
265 } else if parameter.contains("radius") {
266 Some("Radius must be positive and reasonable for your data resolution")
267 } else if parameter.contains("iterations") {
268 Some("Number of iterations must be positive (typically 1-1000)")
269 } else if message.contains("odd") {
270 Some("Value must be odd. Try the next odd number (current±1)")
271 } else if message.contains("positive") {
272 Some("Value must be greater than zero")
273 } else if message.contains("range") {
274 Some("Value must be within the specified range")
275 } else {
276 Some("Check parameter documentation for valid values and constraints")
277 }
278 }
279 Self::InvalidGeometry(_) => Some("Verify geometry is valid and not self-intersecting"),
280 Self::IncompatibleTypes { .. } => {
281 Some("Convert data to compatible types before processing")
282 }
283 Self::InsufficientData { .. } => Some("Provide more data points for reliable results"),
284 Self::NumericalError { .. } => {
285 Some("Check for division by zero, overflow, or invalid mathematical operations")
286 }
287 Self::ComputationError(_) => Some("Verify input data is within acceptable ranges"),
288 Self::GeometryError { .. } => Some("Check geometry validity and topology"),
289 Self::UnsupportedOperation { .. } => {
290 Some("Use a different algorithm or enable required features")
291 }
292 Self::AllocationFailed { .. } => Some("Reduce data size or increase available memory"),
293 Self::SimdNotAvailable => Some(
294 "SIMD operations are not supported on this CPU. The algorithm will use scalar fallback",
295 ),
296 Self::PathNotFound(_) => Some("Verify the path exists and is accessible"),
297 Self::NestingTooDeep { .. } => Some(
298 "Reduce the nesting of the expression, or split it into intermediate `let` bindings",
299 ),
300 }
301 }
302
303 pub fn context(&self) -> ErrorContext {
307 match self {
308 Self::Core(e) => ErrorContext::new("core_error").with_detail("error", e.to_string()),
309 Self::InvalidDimensions {
310 message,
311 actual,
312 expected,
313 } => ErrorContext::new("invalid_dimensions")
314 .with_detail("message", message.to_string())
315 .with_detail("actual", actual.to_string())
316 .with_detail("expected", expected.to_string()),
317 Self::EmptyInput { operation } => {
318 ErrorContext::new("empty_input").with_detail("operation", operation.to_string())
319 }
320 Self::InvalidInput(msg) => {
321 ErrorContext::new("invalid_input").with_detail("message", msg.clone())
322 }
323 Self::InvalidParameter { parameter, message } => ErrorContext::new("invalid_parameter")
324 .with_detail("parameter", parameter.to_string())
325 .with_detail("message", message.clone()),
326 Self::InvalidGeometry(msg) => {
327 ErrorContext::new("invalid_geometry").with_detail("message", msg.clone())
328 }
329 Self::IncompatibleTypes {
330 source_type,
331 target_type,
332 } => ErrorContext::new("incompatible_types")
333 .with_detail("source_type", source_type.to_string())
334 .with_detail("target_type", target_type.to_string()),
335 Self::InsufficientData { operation, message } => ErrorContext::new("insufficient_data")
336 .with_detail("operation", operation.to_string())
337 .with_detail("message", message.clone()),
338 Self::NumericalError { operation, message } => ErrorContext::new("numerical_error")
339 .with_detail("operation", operation.to_string())
340 .with_detail("message", message.clone()),
341 Self::ComputationError(msg) => {
342 ErrorContext::new("computation_error").with_detail("message", msg.clone())
343 }
344 Self::GeometryError { message } => {
345 ErrorContext::new("geometry_error").with_detail("message", message.clone())
346 }
347 Self::UnsupportedOperation { operation } => ErrorContext::new("unsupported_operation")
348 .with_detail("operation", operation.clone()),
349 Self::AllocationFailed { message } => {
350 ErrorContext::new("allocation_failed").with_detail("message", message.clone())
351 }
352 Self::SimdNotAvailable => ErrorContext::new("simd_not_available"),
353 Self::PathNotFound(path) => {
354 ErrorContext::new("path_not_found").with_detail("path", path.clone())
355 }
356 Self::NestingTooDeep {
357 context,
358 depth,
359 max,
360 } => ErrorContext::new("nesting_too_deep")
361 .with_detail("context", context.to_string())
362 .with_detail("depth", depth.to_string())
363 .with_detail("max", max.to_string()),
364 }
365 }
366}
367
368#[derive(Debug, Clone)]
370pub struct ErrorContext {
371 pub category: &'static str,
373 pub details: Vec<(String, String)>,
375}
376
377impl ErrorContext {
378 pub fn new(category: &'static str) -> Self {
380 Self {
381 category,
382 details: Vec::new(),
383 }
384 }
385
386 pub fn with_detail(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
388 self.details.push((key.into(), value.into()));
389 self
390 }
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396
397 #[test]
398 fn test_error_display() {
399 let err = AlgorithmError::InvalidParameter {
400 parameter: "window_size",
401 message: "must be positive".to_string(),
402 };
403 let s = format!("{err}");
404 assert!(s.contains("window_size"));
405 assert!(s.contains("must be positive"));
406 }
407
408 #[test]
409 fn test_error_from_core() {
410 let core_err = OxiGdalError::OutOfBounds {
411 message: "test".to_string(),
412 };
413 let _alg_err: AlgorithmError = core_err.into();
414 }
415
416 #[test]
417 fn test_invalid_input() {
418 let err = AlgorithmError::InvalidInput("test input".to_string());
419 let s = format!("{err}");
420 assert!(s.contains("Invalid input"));
421 assert!(s.contains("test input"));
422 }
423
424 #[test]
425 fn test_invalid_geometry() {
426 let err = AlgorithmError::InvalidGeometry("test geometry".to_string());
427 let s = format!("{err}");
428 assert!(s.contains("Invalid geometry"));
429 assert!(s.contains("test geometry"));
430 }
431
432 #[test]
433 fn test_computation_error() {
434 let err = AlgorithmError::ComputationError("test error".to_string());
435 let s = format!("{err}");
436 assert!(s.contains("Computation error"));
437 assert!(s.contains("test error"));
438 }
439
440 #[test]
441 fn test_error_codes() {
442 let err = AlgorithmError::InvalidParameter {
443 parameter: "window_size",
444 message: "must be odd".to_string(),
445 };
446 assert_eq!(err.code(), "A005");
447
448 let err = AlgorithmError::InvalidDimensions {
449 message: "mismatched",
450 actual: 4,
451 expected: 5,
452 };
453 assert_eq!(err.code(), "A002");
454
455 let err = AlgorithmError::SimdNotAvailable;
456 assert_eq!(err.code(), "A014");
457 }
458
459 #[test]
460 fn test_error_suggestions() {
461 let err = AlgorithmError::InvalidParameter {
462 parameter: "window_size",
463 message: "must be odd".to_string(),
464 };
465 assert!(err.suggestion().is_some());
466 assert!(err.suggestion().is_some_and(|s| s.contains("odd")));
467
468 let err = AlgorithmError::InvalidParameter {
469 parameter: "kernel_size",
470 message: "invalid".to_string(),
471 };
472 assert!(err.suggestion().is_some());
473 assert!(err.suggestion().is_some_and(|s| s.contains("kernel")));
474
475 let err = AlgorithmError::InvalidDimensions {
476 message: "window size",
477 actual: 4,
478 expected: 3,
479 };
480 assert!(err.suggestion().is_some());
481 assert!(
482 err.suggestion()
483 .is_some_and(|s| s.contains("Window size must be odd"))
484 );
485 }
486
487 #[test]
488 fn test_error_context() {
489 let err = AlgorithmError::InvalidParameter {
490 parameter: "window_size",
491 message: "must be odd, got 4. Try 3 or 5".to_string(),
492 };
493 let ctx = err.context();
494 assert_eq!(ctx.category, "invalid_parameter");
495 assert!(
496 ctx.details
497 .iter()
498 .any(|(k, v)| k == "parameter" && v == "window_size")
499 );
500 assert!(ctx.details.iter().any(|(k, _)| k == "message"));
501
502 let err = AlgorithmError::InvalidDimensions {
503 message: "array size mismatch",
504 actual: 100,
505 expected: 200,
506 };
507 let ctx = err.context();
508 assert_eq!(ctx.category, "invalid_dimensions");
509 assert!(ctx.details.iter().any(|(k, v)| k == "actual" && v == "100"));
510 assert!(
511 ctx.details
512 .iter()
513 .any(|(k, v)| k == "expected" && v == "200")
514 );
515 }
516
517 #[test]
518 fn test_parameter_suggestion_specificity() {
519 let err = AlgorithmError::InvalidParameter {
521 parameter: "window_size",
522 message: "test".to_string(),
523 };
524 let suggestion = err.suggestion();
525 assert!(suggestion.is_some_and(|s| s.contains("Window") || s.contains("kernel")));
526
527 let err = AlgorithmError::InvalidParameter {
529 parameter: "threshold",
530 message: "test".to_string(),
531 };
532 let suggestion = err.suggestion();
533 assert!(suggestion.is_some_and(|s| s.contains("0.0") && s.contains("1.0")));
534 }
535}