Skip to main content

oxigdal_ml/
error.rs

1//! Error types for OxiGDAL ML operations
2//!
3//! This module provides ML-specific error types that integrate with
4//! the core OxiGDAL error hierarchy.
5
6use thiserror::Error;
7
8/// The main result type for ML operations
9pub type Result<T> = core::result::Result<T, MlError>;
10
11/// ML-specific errors
12#[derive(Debug, Error)]
13pub enum MlError {
14    /// Model loading error
15    #[error("Model error: {0}")]
16    Model(#[from] ModelError),
17
18    /// Inference error
19    #[error("Inference error: {0}")]
20    Inference(#[from] InferenceError),
21
22    /// Preprocessing error
23    #[error("Preprocessing error: {0}")]
24    Preprocessing(#[from] PreprocessingError),
25
26    /// Postprocessing error
27    #[error("Postprocessing error: {0}")]
28    Postprocessing(#[from] PostprocessingError),
29
30    /// OxiGDAL core error
31    #[error("OxiGDAL error: {0}")]
32    OxiGdal(#[from] oxigdal_core::OxiGdalError),
33
34    /// ONNX Runtime error
35    #[error("ONNX Runtime error: {0}")]
36    Ort(String),
37
38    /// I/O error
39    #[error("I/O error: {0}")]
40    Io(#[from] std::io::Error),
41
42    /// Serialization error
43    #[error("Serialization error: {0}")]
44    Serialization(#[from] serde_json::Error),
45
46    /// Invalid configuration
47    #[error("Invalid configuration: {0}")]
48    InvalidConfig(String),
49
50    /// Feature not available
51    #[error("Feature not available: {feature}. Enable with feature flag: {flag}")]
52    FeatureNotAvailable {
53        /// The feature name
54        feature: String,
55        /// The required feature flag
56        flag: String,
57    },
58}
59
60/// Model-related errors
61#[derive(Debug, Error)]
62pub enum ModelError {
63    /// Model file not found
64    #[error("Model file not found: {path}")]
65    NotFound {
66        /// The model file path
67        path: String,
68    },
69
70    /// Model loading failed
71    #[error("Failed to load model: {reason}")]
72    LoadFailed {
73        /// The reason for failure
74        reason: String,
75    },
76
77    /// Invalid model format
78    #[error("Invalid model format: {message}")]
79    InvalidFormat {
80        /// Error message
81        message: String,
82    },
83
84    /// Model initialization failed
85    #[error("Model initialization failed: {reason}")]
86    InitializationFailed {
87        /// The reason for failure
88        reason: String,
89    },
90
91    /// Incompatible model version
92    #[error("Incompatible model version: expected {expected}, got {actual}")]
93    IncompatibleVersion {
94        /// Expected version
95        expected: String,
96        /// Actual version
97        actual: String,
98    },
99
100    /// Missing required input
101    #[error("Missing required model input: {input_name}")]
102    MissingInput {
103        /// Input name
104        input_name: String,
105    },
106
107    /// Missing required output
108    #[error("Missing required model output: {output_name}")]
109    MissingOutput {
110        /// Output name
111        output_name: String,
112    },
113}
114
115/// Inference-related errors
116#[derive(Debug, Error)]
117pub enum InferenceError {
118    /// Invalid input shape
119    #[error("Invalid input shape: expected {expected:?}, got {actual:?}")]
120    InvalidInputShape {
121        /// Expected shape
122        expected: Vec<usize>,
123        /// Actual shape
124        actual: Vec<usize>,
125    },
126
127    /// Invalid input type
128    #[error("Invalid input type: expected {expected}, got {actual}")]
129    InvalidInputType {
130        /// Expected type
131        expected: String,
132        /// Actual type
133        actual: String,
134    },
135
136    /// Band (channel) count mismatch between the input buffer and the model.
137    ///
138    /// A [`RasterBuffer`](oxigdal_core::buffer::RasterBuffer) is single-band, so
139    /// a single buffer can only ever supply one channel. This error is raised
140    /// when a model declares more (or fewer) input channels than the supplied
141    /// buffer provides, instead of silently building a wrong-shaped tensor.
142    #[error(
143        "Band count mismatch: model expects {expected} channel(s) but the input buffer supplies {actual}"
144    )]
145    InvalidBandCount {
146        /// Channel count the model expects
147        expected: usize,
148        /// Channel count the buffer supplies
149        actual: usize,
150    },
151
152    /// Batch size mismatch
153    #[error("Batch size mismatch: expected {expected}, got {actual}")]
154    BatchSizeMismatch {
155        /// Expected batch size
156        expected: usize,
157        /// Actual batch size
158        actual: usize,
159    },
160
161    /// Inference failed
162    #[error("Inference failed: {reason}")]
163    Failed {
164        /// The reason for failure
165        reason: String,
166    },
167
168    /// Output parsing failed
169    #[error("Failed to parse output: {reason}")]
170    OutputParsingFailed {
171        /// The reason for failure
172        reason: String,
173    },
174
175    /// GPU not available
176    #[error("GPU acceleration requested but not available: {message}")]
177    GpuNotAvailable {
178        /// Error message
179        message: String,
180    },
181}
182
183/// Preprocessing-related errors
184#[derive(Debug, Error)]
185pub enum PreprocessingError {
186    /// Invalid normalization parameters
187    #[error("Invalid normalization parameters: {message}")]
188    InvalidNormalization {
189        /// Error message
190        message: String,
191    },
192
193    /// Tiling failed
194    #[error("Tiling failed: {reason}")]
195    TilingFailed {
196        /// The reason for failure
197        reason: String,
198    },
199
200    /// Padding failed
201    #[error("Padding failed: {reason}")]
202    PaddingFailed {
203        /// The reason for failure
204        reason: String,
205    },
206
207    /// Invalid tile size
208    #[error("Invalid tile size: width={width}, height={height}")]
209    InvalidTileSize {
210        /// Tile width
211        width: usize,
212        /// Tile height
213        height: usize,
214    },
215
216    /// Channel mismatch
217    #[error("Channel mismatch: expected {expected}, got {actual}")]
218    ChannelMismatch {
219        /// Expected channels
220        expected: usize,
221        /// Actual channels
222        actual: usize,
223    },
224
225    /// Augmentation failed
226    #[error("Data augmentation failed: {reason}")]
227    AugmentationFailed {
228        /// The reason for failure
229        reason: String,
230    },
231}
232
233/// Postprocessing-related errors
234#[derive(Debug, Error)]
235pub enum PostprocessingError {
236    /// Tile merging failed
237    #[error("Tile merging failed: {reason}")]
238    MergingFailed {
239        /// The reason for failure
240        reason: String,
241    },
242
243    /// Threshold out of range
244    #[error("Threshold out of range: must be between 0.0 and 1.0, got {value}")]
245    InvalidThreshold {
246        /// The invalid threshold value
247        value: f32,
248    },
249
250    /// Polygon conversion failed
251    #[error("Polygon conversion failed: {reason}")]
252    PolygonConversionFailed {
253        /// The reason for failure
254        reason: String,
255    },
256
257    /// NMS failed
258    #[error("Non-maximum suppression failed: {reason}")]
259    NmsFailed {
260        /// The reason for failure
261        reason: String,
262    },
263
264    /// Export failed
265    #[error("Export failed: {reason}")]
266    ExportFailed {
267        /// The reason for failure
268        reason: String,
269    },
270
271    /// Invalid class ID
272    #[error("Invalid class ID: {class_id}")]
273    InvalidClassId {
274        /// The invalid class ID
275        class_id: usize,
276    },
277}
278
279impl From<oxionnx::OnnxError> for MlError {
280    fn from(err: oxionnx::OnnxError) -> Self {
281        MlError::Ort(err.to_string())
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288
289    #[test]
290    fn test_error_display() {
291        let err = ModelError::NotFound {
292            path: "/path/to/model.onnx".to_string(),
293        };
294        assert!(err.to_string().contains("Model file not found"));
295        assert!(err.to_string().contains("/path/to/model.onnx"));
296    }
297
298    #[test]
299    fn test_error_conversion() {
300        let model_err = ModelError::LoadFailed {
301            reason: "test".to_string(),
302        };
303        let ml_err: MlError = model_err.into();
304        assert!(matches!(ml_err, MlError::Model(_)));
305    }
306
307    #[test]
308    fn test_invalid_threshold() {
309        let err = PostprocessingError::InvalidThreshold { value: 1.5 };
310        assert!(err.to_string().contains("1.5"));
311    }
312}