dnoise 0.6.0

Denoise Bruker timsTOF .d folders with an iterative vertical ion-mobility feature filter (CLI + library).
//! Error types for the `dnoise` library.
//!
//! The library returns the typed [`DnoiseError`]; binaries can convert it into
//! `anyhow`/`eyre`/etc. via the standard [`std::error::Error`] impl. The low-level
//! type-2 decoder returns the narrower [`DecodeError`], which folds into
//! [`DnoiseError`] when used through the high-level pipeline.

use std::path::PathBuf;
use thiserror::Error;

/// Convenience alias for results produced by this crate.
pub type Result<T, E = DnoiseError> = std::result::Result<T, E>;

/// Anything that can go wrong while denoising a `.d` folder.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum DnoiseError {
    /// Invalid input, configuration, or output path.
    #[error("{0}")]
    InvalidInput(String),

    /// Installing output failed and recovery requires user attention.
    #[error("{0}")]
    Recovery(String),
    /// The input path is not a Bruker `.d` folder (missing `analysis.tdf` /
    /// `analysis.tdf_bin`).
    #[error("{0} is not a Bruker .d folder (missing analysis.tdf / analysis.tdf_bin)")]
    NotADotD(PathBuf),

    /// The output folder already exists and overwriting was not requested.
    #[error("output {0} already exists (pass force to overwrite)")]
    OutputExists(PathBuf),

    /// Opening the timsTOF frame data failed.
    #[error("opening frame data: {0}")]
    OpenFrames(String),

    /// Reading the timsTOF metadata / calibration failed (needed by the diaPASEF
    /// MS1 window gate to convert isolation m/z to TOF indices).
    #[error("reading metadata: {0}")]
    Metadata(String),

    /// Reading or decoding a single frame failed.
    #[error("reading frame {index}: {message}")]
    FrameRead {
        /// Zero-based frame index that failed.
        index: usize,
        /// Upstream reader message.
        message: String,
    },

    /// Decoding a type-2 frame record failed.
    #[error(transparent)]
    Decode(#[from] DecodeError),

    /// An underlying I/O error (copying the `.d`, writing `analysis.tdf_bin`, …).
    #[error("I/O error: {0}")]
    Io(#[from] std::io::Error),

    /// An error from the `analysis.tdf` SQLite database.
    #[error("database error: {0}")]
    Sqlite(#[from] rusqlite::Error),

    /// The run was cancelled via the [`crate::RunOptions::cancel`] token before it
    /// finished. Any partial output is incomplete and should be discarded.
    #[error("cancelled")]
    Cancelled,
}

/// Failure decoding a type-2 `tdf_bin` frame record (see [`crate::codec`]).
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum DecodeError {
    /// Decompression or encoding would exceed the supported frame size.
    #[error("frame exceeds the 256 MiB size limit")]
    SizeLimit,
    /// Scan counts, offsets, or TOF deltas are inconsistent.
    #[error("invalid frame layout or coordinates")]
    InvalidLayout,

    /// Record is shorter than the fixed 8-byte header.
    #[error("record shorter than header")]
    ShortRecord,

    /// The record's declared `total_byte_count` is inconsistent with its length.
    #[error("invalid total_byte_count {0}")]
    InvalidByteCount(usize),

    /// The decompressed payload is not a whole number of `u32` values.
    #[error("decompressed blob not u32-aligned")]
    Misaligned,

    /// The decoded `scan_count` exceeds the decompressed payload length.
    #[error("scan_count {scan_count} exceeds blob length {len}")]
    ScanCountOverflow {
        /// Decoded scan count.
        scan_count: usize,
        /// Number of `u32` values actually present.
        len: usize,
    },

    /// zstd decompression of the payload failed.
    #[error("zstd decompression failed: {0}")]
    Zstd(#[source] std::io::Error),
}

impl From<dnoise_core::Error> for DnoiseError {
    fn from(e: dnoise_core::Error) -> Self {
        match e {
            dnoise_core::Error::InvalidInput(m) => Self::InvalidInput(m),
            dnoise_core::Error::FrameRead { index, message } => Self::FrameRead { index, message },
            dnoise_core::Error::Cancelled => Self::Cancelled,
            // `dnoise_core::Error` is non-exhaustive; future variants carry
            // their message as invalid input.
            other => Self::InvalidInput(other.to_string()),
        }
    }
}