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}