Skip to main content

wifi_densepose_train/
error.rs

1//! Error types for the WiFi-DensePose training pipeline.
2//!
3//! This module is the single source of truth for all error types in the
4//! training crate. Every module that produces an error imports its error type
5//! from here rather than defining it inline, keeping the error hierarchy
6//! centralised and consistent.
7//!
8//! ## Hierarchy
9//!
10//! ```text
11//! TrainError (top-level)
12//! ├── ConfigError      (config validation / file loading)
13//! ├── DatasetError     (data loading, I/O, format)
14//! └── SubcarrierError  (frequency-axis resampling)
15//! ```
16
17use thiserror::Error;
18use std::path::PathBuf;
19
20// ---------------------------------------------------------------------------
21// TrainResult
22// ---------------------------------------------------------------------------
23
24/// Convenient `Result` alias used by orchestration-level functions.
25pub type TrainResult<T> = Result<T, TrainError>;
26
27// ---------------------------------------------------------------------------
28// TrainError — top-level aggregator
29// ---------------------------------------------------------------------------
30
31/// Top-level error type for the WiFi-DensePose training pipeline.
32///
33/// Orchestration-level functions (e.g. [`crate::trainer::Trainer`] methods)
34/// return `TrainResult<T>`. Lower-level functions in [`crate::config`] and
35/// [`crate::dataset`] return their own module-specific error types which are
36/// automatically coerced into `TrainError` via [`From`].
37#[derive(Debug, Error)]
38pub enum TrainError {
39    /// A configuration validation or loading error.
40    #[error("Configuration error: {0}")]
41    Config(#[from] ConfigError),
42
43    /// A dataset loading or access error.
44    #[error("Dataset error: {0}")]
45    Dataset(#[from] DatasetError),
46
47    /// JSON (de)serialization error.
48    #[error("JSON error: {0}")]
49    Json(#[from] serde_json::Error),
50
51    /// The dataset is empty and no training can be performed.
52    #[error("Dataset is empty")]
53    EmptyDataset,
54
55    /// Index out of bounds when accessing dataset items.
56    #[error("Index {index} is out of bounds for dataset of length {len}")]
57    IndexOutOfBounds {
58        /// The out-of-range index.
59        index: usize,
60        /// The total number of items in the dataset.
61        len: usize,
62    },
63
64    /// A shape mismatch was detected between two tensors.
65    #[error("Shape mismatch: expected {expected:?}, got {actual:?}")]
66    ShapeMismatch {
67        /// Expected shape.
68        expected: Vec<usize>,
69        /// Actual shape.
70        actual: Vec<usize>,
71    },
72
73    /// A training step failed.
74    #[error("Training step failed: {0}")]
75    TrainingStep(String),
76
77    /// A checkpoint could not be saved or loaded.
78    #[error("Checkpoint error: {message} (path: {path:?})")]
79    Checkpoint {
80        /// Human-readable description.
81        message: String,
82        /// Path that was being accessed.
83        path: PathBuf,
84    },
85
86    /// Feature not yet implemented.
87    #[error("Not implemented: {0}")]
88    NotImplemented(String),
89}
90
91impl TrainError {
92    /// Construct a [`TrainError::TrainingStep`].
93    pub fn training_step<S: Into<String>>(msg: S) -> Self {
94        TrainError::TrainingStep(msg.into())
95    }
96
97    /// Construct a [`TrainError::Checkpoint`].
98    pub fn checkpoint<S: Into<String>>(msg: S, path: impl Into<PathBuf>) -> Self {
99        TrainError::Checkpoint { message: msg.into(), path: path.into() }
100    }
101
102    /// Construct a [`TrainError::NotImplemented`].
103    pub fn not_implemented<S: Into<String>>(msg: S) -> Self {
104        TrainError::NotImplemented(msg.into())
105    }
106
107    /// Construct a [`TrainError::ShapeMismatch`].
108    pub fn shape_mismatch(expected: Vec<usize>, actual: Vec<usize>) -> Self {
109        TrainError::ShapeMismatch { expected, actual }
110    }
111}
112
113// ---------------------------------------------------------------------------
114// ConfigError
115// ---------------------------------------------------------------------------
116
117/// Errors produced when loading or validating a [`TrainingConfig`].
118///
119/// [`TrainingConfig`]: crate::config::TrainingConfig
120#[derive(Debug, Error)]
121pub enum ConfigError {
122    /// A field has an invalid value.
123    #[error("Invalid value for `{field}`: {reason}")]
124    InvalidValue {
125        /// Name of the field.
126        field: &'static str,
127        /// Human-readable reason.
128        reason: String,
129    },
130
131    /// A configuration file could not be read from disk.
132    #[error("Cannot read config file `{path}`: {source}")]
133    FileRead {
134        /// Path that was being read.
135        path: PathBuf,
136        /// Underlying I/O error.
137        #[source]
138        source: std::io::Error,
139    },
140
141    /// A configuration file contains malformed JSON.
142    #[error("Cannot parse config file `{path}`: {source}")]
143    ParseError {
144        /// Path that was being parsed.
145        path: PathBuf,
146        /// Underlying JSON parse error.
147        #[source]
148        source: serde_json::Error,
149    },
150
151    /// A path referenced in the config does not exist.
152    #[error("Path `{path}` in config does not exist")]
153    PathNotFound {
154        /// The missing path.
155        path: PathBuf,
156    },
157}
158
159impl ConfigError {
160    /// Construct a [`ConfigError::InvalidValue`].
161    pub fn invalid_value<S: Into<String>>(field: &'static str, reason: S) -> Self {
162        ConfigError::InvalidValue { field, reason: reason.into() }
163    }
164}
165
166// ---------------------------------------------------------------------------
167// DatasetError
168// ---------------------------------------------------------------------------
169
170/// Errors produced while loading or accessing dataset samples.
171///
172/// Production training code MUST NOT silently suppress these errors.
173/// If data is missing, training must fail explicitly so the user is aware.
174/// The [`SyntheticCsiDataset`] is the only source of non-file-system data
175/// and is restricted to proof/testing use.
176///
177/// [`SyntheticCsiDataset`]: crate::dataset::SyntheticCsiDataset
178#[derive(Debug, Error)]
179pub enum DatasetError {
180    /// A required data file or directory was not found on disk.
181    #[error("Data not found at `{path}`: {message}")]
182    DataNotFound {
183        /// Path that was expected to contain data.
184        path: PathBuf,
185        /// Additional context.
186        message: String,
187    },
188
189    /// A file was found but its format or shape is wrong.
190    #[error("Invalid data format in `{path}`: {message}")]
191    InvalidFormat {
192        /// Path of the malformed file.
193        path: PathBuf,
194        /// Description of the problem.
195        message: String,
196    },
197
198    /// A low-level I/O error while reading a data file.
199    #[error("I/O error reading `{path}`: {source}")]
200    IoError {
201        /// Path being read when the error occurred.
202        path: PathBuf,
203        /// Underlying I/O error.
204        #[source]
205        source: std::io::Error,
206    },
207
208    /// The number of subcarriers in the file doesn't match expectations.
209    #[error(
210        "Subcarrier count mismatch in `{path}`: file has {found}, expected {expected}"
211    )]
212    SubcarrierMismatch {
213        /// Path of the offending file.
214        path: PathBuf,
215        /// Subcarrier count found in the file.
216        found: usize,
217        /// Subcarrier count expected.
218        expected: usize,
219    },
220
221    /// A sample index is out of bounds.
222    #[error("Index {idx} out of bounds (dataset has {len} samples)")]
223    IndexOutOfBounds {
224        /// The requested index.
225        idx: usize,
226        /// Total length of the dataset.
227        len: usize,
228    },
229
230    /// A numpy array file could not be parsed.
231    #[error("NumPy read error in `{path}`: {message}")]
232    NpyReadError {
233        /// Path of the `.npy` file.
234        path: PathBuf,
235        /// Error description.
236        message: String,
237    },
238
239    /// Metadata for a subject is missing or malformed.
240    #[error("Metadata error for subject {subject_id}: {message}")]
241    MetadataError {
242        /// Subject whose metadata was invalid.
243        subject_id: u32,
244        /// Description of the problem.
245        message: String,
246    },
247
248    /// A data format error (e.g. wrong numpy shape) occurred.
249    ///
250    /// This is a convenience variant for short-form error messages where
251    /// the full path context is not available.
252    #[error("File format error: {0}")]
253    Format(String),
254
255    /// The data directory does not exist.
256    #[error("Directory not found: {path}")]
257    DirectoryNotFound {
258        /// The path that was not found.
259        path: String,
260    },
261
262    /// No subjects matching the requested IDs were found.
263    #[error(
264        "No subjects found in `{data_dir}` for IDs: {requested:?}"
265    )]
266    NoSubjectsFound {
267        /// Root data directory.
268        data_dir: PathBuf,
269        /// IDs that were requested.
270        requested: Vec<u32>,
271    },
272
273    /// An I/O error that carries no path context.
274    #[error("IO error: {0}")]
275    Io(#[from] std::io::Error),
276}
277
278impl DatasetError {
279    /// Construct a [`DatasetError::DataNotFound`].
280    pub fn not_found<S: Into<String>>(path: impl Into<PathBuf>, msg: S) -> Self {
281        DatasetError::DataNotFound { path: path.into(), message: msg.into() }
282    }
283
284    /// Construct a [`DatasetError::InvalidFormat`].
285    pub fn invalid_format<S: Into<String>>(path: impl Into<PathBuf>, msg: S) -> Self {
286        DatasetError::InvalidFormat { path: path.into(), message: msg.into() }
287    }
288
289    /// Construct a [`DatasetError::IoError`].
290    pub fn io_error(path: impl Into<PathBuf>, source: std::io::Error) -> Self {
291        DatasetError::IoError { path: path.into(), source }
292    }
293
294    /// Construct a [`DatasetError::SubcarrierMismatch`].
295    pub fn subcarrier_mismatch(path: impl Into<PathBuf>, found: usize, expected: usize) -> Self {
296        DatasetError::SubcarrierMismatch { path: path.into(), found, expected }
297    }
298
299    /// Construct a [`DatasetError::NpyReadError`].
300    pub fn npy_read<S: Into<String>>(path: impl Into<PathBuf>, msg: S) -> Self {
301        DatasetError::NpyReadError { path: path.into(), message: msg.into() }
302    }
303}
304
305// ---------------------------------------------------------------------------
306// SubcarrierError
307// ---------------------------------------------------------------------------
308
309/// Errors produced by the subcarrier resampling / interpolation functions.
310#[derive(Debug, Error)]
311pub enum SubcarrierError {
312    /// The source or destination count is zero.
313    #[error("Subcarrier count must be >= 1, got {count}")]
314    ZeroCount {
315        /// The offending count.
316        count: usize,
317    },
318
319    /// The array's last dimension does not match the declared source count.
320    #[error(
321        "Subcarrier shape mismatch: last dim is {actual_sc} but src_n={expected_sc} \
322         (full shape: {shape:?})"
323    )]
324    InputShapeMismatch {
325        /// Expected subcarrier count.
326        expected_sc: usize,
327        /// Actual last-dimension size.
328        actual_sc: usize,
329        /// Full shape of the input.
330        shape: Vec<usize>,
331    },
332
333    /// The requested interpolation method is not yet implemented.
334    #[error("Interpolation method `{method}` is not implemented")]
335    MethodNotImplemented {
336        /// Name of the unsupported method.
337        method: String,
338    },
339
340    /// `src_n == dst_n` — no resampling needed.
341    #[error("src_n == dst_n == {count}; call interpolate only when counts differ")]
342    NopInterpolation {
343        /// The equal count.
344        count: usize,
345    },
346
347    /// A numerical error during interpolation.
348    #[error("Numerical error: {0}")]
349    NumericalError(String),
350}
351
352impl SubcarrierError {
353    /// Construct a [`SubcarrierError::NumericalError`].
354    pub fn numerical<S: Into<String>>(msg: S) -> Self {
355        SubcarrierError::NumericalError(msg.into())
356    }
357}