# mass-decomposition

A pure-Rust crate for decomposing monoisotopic masses into elemental formulas.
It is intentionally small and dependency-light: no Polars, no pyo3, and no I/O.
Callers provide masses and constraints; the crate returns ranked formula
candidates.

## What it does

Given a target mass (for example a precursor ion or an MS/MS fragment), the
crate enumerates all formulas that fit within a user-supplied element set and
mass tolerance. Results can be filtered and ranked by:

- exact monoisotopic mass error,
- double-bond equivalent (DBE) bounds,
- optional isotope-pattern agreement for the precursor.

The crate also provides lower-level building blocks such as a
`SpectrumDecomposer` for batch fragment processing and helpers for converting
between formula representations.

## Quick start

```rust
use mass_decomposition::{
    DecompositionConfig, MassTolerance, PrecursorInfo, decompose_precursor,
};

let precursor = PrecursorInfo {
    monoisotopic_mass: 194.0803766, // caffeine
    charge: 1,
    adduct: Some("[M+H]+".to_string()),
    isotopic_pattern: None,
};

let mut config = DecompositionConfig::default_lcms_positive();
config.mass_tolerance = MassTolerance::Ppm(5.0);

let candidates = decompose_precursor(&precursor, &config).unwrap();
for (formula, error_ppm) in &candidates {
    println!("{:?}  error = {:.3} ppm", formula.composition, error_ppm);
}
```

## The 200 Da rule

Mass accuracy is usually expressed in parts per million (ppm). At very low
masses, however, a fixed ppm value can become impractically small in absolute
(Dalton) terms. For example, 1 ppm at 100 Da is 0.0001 Da — a tolerance that is
tighter than most instruments can deliver and tighter than the mass defect
spacing of common small-molecule formulas.

To avoid this, the crate applies the **200 Da rule**: whenever a tolerance is
converted between Daltons and ppm, the reference mass is clamped to at least
200 Da:

```text
ppm = da / max(target_mass, 200 Da) * 1_000_000
da  = ppm * max(target_mass, 200 Da) / 1_000_000
```

Consequences:

- A `MassTolerance::Dalton(0.01)` tolerance behaves as ~50 ppm for a 200 Da ion
  and as ~5 ppm for a 2000 Da ion.
- For any target below 200 Da the tolerance is still referenced to 200 Da, so
  the search window does not collapse to a fraction of a millidalton.
- This matches common practice in small-molecule mass spectrometry and keeps the
  decomposer fast and numerically stable.

## Tolerance units

`DecompositionConfig` (and other config structs) accept a `MassTolerance`:

```rust
MassTolerance::Dalton(0.01)  // absolute tolerance in Da
MassTolerance::Ppm(5.0)      // relative tolerance in ppm
```

The enum enforces that exactly one unit is supplied. Use Daltons when the
instrument calibration is best described by an absolute window; use ppm for
high-resolution instruments whose error scales with mass.

## Features

- `serde`: enables `Deserialize` on the kwargs/config structs and on
  `MassTolerance`. Useful when the crate is driven from a Python/Rust boundary
  or a configuration file.

## Cargo

```toml
[dependencies]
mass-decomposition = { path = "path/to/mass-decomposition" }
```

Run tests with:

```bash
cargo test
```
