Skip to main content

bixverse_sc_data/
errors.rs

1//! Errors raised by `bixverse-sc-data`.
2
3use std::io;
4use thiserror::Error;
5
6/// Error variants raised by `bixverse-sc-data`.
7///
8/// Grouped by subsystem.
9#[derive(Debug, Error)]
10pub enum ScDataErrors {
11    // -- lower crates --
12    /// Propagates an error from `bixverse-sparse`.
13    #[error(transparent)]
14    Sparse(#[from] bixverse_sparse::errors::SparseErrors),
15
16    /// Propagates an error from `bixverse-core`.
17    #[cfg(feature = "analysis")]
18    #[error(transparent)]
19    Core(#[from] bixverse_core::errors::CoreErrors),
20
21    // -- arguments --
22    /// A caller-supplied argument is invalid.
23    #[error("Invalid argument: {0}")]
24    InvalidArgument(String),
25
26    // -- Binary file I/O --
27    /// Wraps any `std::io::Error` encountered while reading or writing the
28    /// bixverse binary sparse format.
29    ///
30    /// Covers file creation, seeks, buffered writes, and raw reads against
31    /// the memory-mapped region. Use `#[from]` conversion so `?` works on
32    /// any `io::Result`.
33    #[error("I/O error on binary file: {0}")]
34    BinaryIo(#[from] io::Error),
35
36    /// Error if counts do not seem to be raw counts during i/o
37    #[error("The counts you are trying to load in do not seem to be raw counts")]
38    NotRawCounts,
39
40    /// Bincode failed to encode the `FileHeader` or `SparseDataHeader`.
41    ///
42    /// Practically unreachable for the fixed-layout headers used here, but
43    /// kept as a non-panicking path so the writer never aborts a long ingest.
44    #[error("Failed to encode file header")]
45    HeaderEncodeFailed,
46
47    /// Bincode failed to decode the `FileHeader` or `SparseDataHeader`.
48    ///
49    /// Indicates the file is truncated, corrupt, or was not produced by
50    /// bixverse. The 64-byte fixed file header is the first thing read, so
51    /// most "wrong file type" errors surface here.
52    #[error("Failed to decode file header - file may be corrupt or truncated")]
53    HeaderDecodeFailed,
54
55    /// File version does not match the current `SC_FILE_VERSION`.
56    ///
57    /// Returned on read when an older or newer binary is opened. The fix is
58    /// to regenerate the file with the current bixverse version.
59    #[error("File version mismatch: expected {expected}, got {found}")]
60    FileVersionMismatch {
61        /// Version the current build expects.
62        expected: u32,
63        /// Version actually read from the file header.
64        found: u32,
65    },
66
67    /// A chunk buffer was shorter than the minimum header length.
68    ///
69    /// Raised before any field is parsed from a decompressed chunk, so the
70    /// reader can fail early instead of indexing out of bounds.
71    #[error("Chunk buffer too small: expected at least {expected} bytes, got {found}")]
72    ChunkBufferTooSmall {
73        /// Minimum bytes required for the chunk header.
74        expected: usize,
75        /// Actual bytes available in the buffer.
76        found: usize,
77    },
78
79    /// LZ4 decompression of a chunk failed.
80    ///
81    /// The offset is reported to make corrupt regions locatable. Usually
82    /// indicates file truncation or a mismatched compressed-size prefix.
83    #[error("Failed to decompress chunk at offset {0}")]
84    ChunkDecompressionFailed(u64),
85
86    /// A caller-supplied original index was not present in the file's index
87    /// map.
88    ///
89    /// Indicates either a bug in the calling code (asking for an index that
90    /// was never written) or a file that was generated with a different set
91    /// of cells/genes than the caller assumes.
92    #[error("Chunk index {0} not found in file index map")]
93    ChunkIndexNotFound(usize),
94
95    /// A cell-based reader method was called on a gene-based file, or vice
96    /// versa.
97    ///
98    /// Replaces the previous runtime asserts. The two string fields describe
99    /// the file's actual layout and the layout the caller requested.
100    #[error("Reader mode mismatch: file is {actual}, requested {requested}")]
101    ReaderModeMismatch {
102        /// What the file is: "cell-based" or "gene-based".
103        actual: &'static str,
104        /// What the caller requested: "cell-based" or "gene-based".
105        requested: &'static str,
106    },
107
108    /// A chunk's declared payload lengths exceed the bytes actually present.
109    ///
110    /// The three length fields in a chunk header are untrusted input. This is
111    /// raised after they have been parsed but before any payload slice is
112    /// taken, so a corrupt length cannot index out of bounds.
113    #[error("Chunk payload truncated: header declares {expected} bytes, buffer holds {found}")]
114    ChunkPayloadTruncated {
115        /// Total bytes the header's length fields imply.
116        expected: usize,
117        /// Actual bytes available in the decompressed chunk.
118        found: usize,
119    },
120
121    /// The raw-count element-size discriminant was not recognised.
122    ///
123    /// Valid values are `RAW_ELEM_U16` (2), `RAW_ELEM_U32` (4), and 0 for
124    /// legacy files where the byte was zero padding. Anything else means the
125    /// chunk is corrupt; reinterpreting it would misparse the whole payload.
126    #[error("Invalid raw count element size discriminant: {0}")]
127    RawElemSizeInvalid(u8),
128
129    /// A v4 chunk is malformed: a bad codec byte, a bit width above 32, a
130    /// body that ends before its own header says it should, or layers of
131    /// unequal length handed to the encoder.
132    #[error("Corrupt v4 chunk: {0}")]
133    V4ChunkCorrupt(String),
134
135    /// A v4 chunk drops its norm layer and recomputes it per entry, but the
136    /// file carries no per-cell library-size table to recompute it from.
137    #[error("v4 gene chunk needs the per-cell library-size table, which the file does not carry")]
138    V4MissingCellLibraryTable,
139
140    /// The file is not a bixverse count archive.
141    #[error("Not a bixverse count archive (magic string mismatch)")]
142    ArchiveMagicMismatch,
143
144    /// The archive was written by an incompatible archive format version.
145    #[error("Archive version mismatch: expected {expected}, got {found}")]
146    ArchiveVersionMismatch {
147        /// Version the current build expects.
148        expected: u32,
149        /// Version actually read from the archive.
150        found: u32,
151    },
152
153    /// The archive is truncated or corrupt, or a chunk cannot be archived.
154    #[error("Corrupt count archive: {0}")]
155    ArchiveCorrupt(String),
156
157    /// A raw count read from disk does not fit the requested numeric type.
158    ///
159    /// Raised by `from_gene_chunks` / `from_cell_chunks` instead of silently
160    /// saturating. Pick a wider `T` (`u32`, `f32`, `f64`) for the affected
161    /// dataset.
162    #[error("Raw count {value} does not fit target type {target_type}")]
163    RawCountOverflow {
164        /// The count that could not be represented.
165        value: u32,
166        /// Name of the type it was being converted into.
167        target_type: &'static str,
168    },
169
170    /// The `target_size` recorded in a file header disagrees with the one the
171    /// caller requested.
172    ///
173    /// Undoing a file's own library-size normalisation requires the exact
174    /// factor the writer used, so a mismatch always yields wrong numbers.
175    /// Files written before `target_size` entered the header report `0.0` and
176    /// are exempt from the check.
177    #[error(
178        "Target size mismatch: file was normalised against {header}, but {requested} was requested"
179    )]
180    TargetSizeMismatch {
181        /// Value stored in the file header.
182        header: f32,
183        /// Value the caller supplied.
184        requested: f32,
185    },
186
187    /// Error for h5 ingestion if feature type is not found
188    #[error(
189        "The requested feature type ({requested}) was not found. On file found features are {found}"
190    )]
191    FeatureTypeNotFound {
192        /// Requested feature type
193        requested: String,
194        /// Found feature types
195        found: String,
196    },
197
198    /// Error if the h5 string type cannot be read
199    #[error("h5: Unexpected str type identified: {0}")]
200    H5UnexpectedStringType(String),
201
202    // -- HDF5 / h5ad --
203    /// Wraps any error from the `hdf5` crate.
204    ///
205    /// Covers file open failures, missing datasets (`X/data`, `X/indices`,
206    /// `X/indptr`, `obs/*`), dtype mismatches, and slice reads. The wrapped
207    /// error carries the original HDF5 message.
208    #[error("HDF5 error: {0}")]
209    #[cfg(feature = "h5")]
210    Hdf5(#[from] hdf5::Error),
211
212    /// Provide custom error message for unsupported file formats.
213    #[error("Unknown or unsupported h5ad format: {0}")]
214    UnsupportH5ADFormat(String),
215
216    /// A dataset is compressed with a filter this build cannot decode.
217    ///
218    /// Raised before the first read, because HDF5 itself reports the missing
219    /// filter as a missing plugin directory: the path baked in at build time
220    /// is a cargo `OUT_DIR` that no longer exists. Either re-save the file
221    /// with `compression="gzip"`, or build with the `hdf5-filters` feature,
222    /// which adds LZF and Blosc.
223    #[error(
224        "HDF5 dataset '{dataset}' uses filter {filter_id} ({filter_name}), which this build cannot decode"
225    )]
226    UnsupportedH5Filter {
227        /// Path of the dataset inside the file.
228        dataset: String,
229        /// The HDF5 filter identifier.
230        filter_id: i32,
231        /// Human readable name for that identifier.
232        filter_name: String,
233    },
234
235    /// A named `obs` column was requested but does not exist in the h5ad.
236    ///
237    /// Primarily raised by `write_h5_normalised_counts` when the caller
238    /// passes an `obs_lib_size_col` that isn't in the file.
239    #[error("obs column '{0}' not found in h5ad file")]
240    ObsColumnMissing(String),
241
242    /// The library-size vector read from `obs` does not match the number of
243    /// cells in `X`.
244    ///
245    /// Indicates a malformed h5ad — typical cause is a user-edited obs table
246    /// whose row count drifted from the matrix.
247    #[error("Library size column length ({found}) does not match cell count ({expected})")]
248    LibSizeLengthMismatch {
249        /// Expected length (number of cells in `X`).
250        expected: usize,
251        /// Actual length of the obs column.
252        found: usize,
253    },
254
255    // -- MTX --
256    /// The header of an MTX file is malformed.
257    ///
258    /// The static string describes which part of the header failed (shape
259    /// line missing, wrong field count, unparseable counts). Comment lines
260    /// starting with `%` are skipped before this fires.
261    #[error("Invalid MTX header: {0}")]
262    MtxHeaderInvalid(&'static str),
263
264    /// A field of the MTX shape line could not be parsed as a number.
265    ///
266    /// `field` names the offending column ("cell count", "gene count",
267    /// "entry count") so that malformed inputs can be diagnosed without
268    /// quoting user data. Which of the first two fields is which depends on
269    /// the `cells_as_rows` orientation.
270    #[error("Failed to parse MTX value as {field}")]
271    MtxParseError {
272        /// Which field failed: "cell count", "gene count", or "entry count".
273        field: &'static str,
274    },
275
276    // -- Format parsing --
277    /// The `cs_type` string did not match a known sparse format.
278    ///
279    /// Accepted values are "csc" and "csr" (case-insensitive at the parser
280    /// level). The unrecognised input is echoed back in the message.
281    #[error("Unknown compressed sparse format: '{0}' (expected 'csc' or 'csr')")]
282    UnknownSparseFormat(String),
283}