mzLib callable from Rust. A separate repo and crate, the Rust sibling of pyMzLib, built on the same language-neutral bridge pyMzLib already uses. Written 2026-07-23 from everything the pyMzLib build taught us — so this is not a green-field design, it's a port of a proven one.
Pick-up for a fresh chat: read this, then read the three pyMzLib reference docs it points to (
code/pyMzLib/docs/contributing/conventions.md,design/expansion-plans/flashlfq-tranche.md,design/bakeoff-flashlfq/DESIGN.mdunderE:\CodeReview\pyMzLib). The single most important idea is in §1.
pyMzLib's architecture was deliberately designed, from day one, so a Rust binding would be cheap. The
.NET bridge is a self-contained executable that speaks a versioned JSON envelope over
stdin/stdout and assumes nothing about its caller. It already does all the genuinely hard parts:
- the mzLib interop and the composition of mzLib's own methods (digestion, fragmentation, PRIDE,
MakeIdentifications+FlashLfqEngine); - the availability-vs-correctness error classification (timeouts/5xx →
ServiceUnavailable, the rest by exception type); - keeping engine chatter off stdout so the envelope is clean;
- carrying its own .NET runtime so no .NET install is needed.
mzLibRust does none of that again. It is a thin, idiomatic Rust crate that spawns the bridge, writes stdin, reads one JSON line, and deserializes it. The port is: transport module + typed structs
- an error enum + tests + docs. That's it.
The wire contract to target is exactly what pyMzLib consumes — see _bridge.py and the three verb
handlers (Program.cs, Peptidoform.cs, Quantification.cs) in the pyMzLib repo. Every field name is
the snake_case of an mzLib/FlashLFQ name; do not rename on the Rust side either.
| pyMzLib | mzLibRust | notes |
|---|---|---|
_bridge.py (only transport-aware module) |
a bridge module |
std::process::Command, write stdin, read stdout, serde_json the envelope. The ONLY place that knows the bridge exists. |
typed dataclasses (PrideFile, Digest, FlashLfqResults, …) |
#[derive(Deserialize)] structs (serde) |
Mirror the wire, mzLib-named. rename_all = "snake_case" is unnecessary — the wire is already snake_case. |
exception hierarchy (PyMzLibError → UsageError/BridgeError/ServiceUnavailableError/…) |
one MzLibError enum (thiserror) |
Map envelope.error.type: "usage"→Usage, "ServiceUnavailable"→ServiceUnavailable, else Bridge{ error_type, message }. Plus Timeout, BridgeNotFound, Io. |
pymzlib.pride.list_files(...) |
mzlib::pride::list_files(...) -> Result<Vec<PrideFile>, MzLibError> |
Functions return Result, not exceptions — the natural Rust shape. |
PYMZLIB_BRIDGE env override |
MZLIB_BRIDGE env override |
Same escape hatch for dev/offline. |
The hardest-won FlashLFQ lesson today: a peptide intensity is 0.0 when missing, a protein
intensity is None (NaN, "could not be resolved"). In Rust this is not a footnote, it's the types:
struct Peptide { intensities: HashMap<String, f64>, /* 0.0 = missing */ }
struct ProteinGroup { intensities: HashMap<String, Option<f64>>, /* None = unresolvable */ }Option<f64> makes the distinction unignorable at the call site — a strictly better outcome than the
Python docs having to warn about it. Bake this in from the first line; it's a selling point.
Python solved the ~130 MB payload with a wheel that carries the binary. Rust has no wheel, and
crates.io rejects large binaries (~10 MB soft limit). This is the only part with no proven answer,
so it's the thing to /grill-me on before writing code. Options:
- A. Download-at-build (
build.rs) — the build script fetches the platform bridge binary from a GitHub Release, checksum-verified, cached inOUT_DIR/a user cache. Crate stays tiny and publishable; keeps the zero-.NET-install promise. Cost: builds need network once (mitigate with the env override + a vendored/offline mode). This is whatort,onnxruntime-style crates do. Recommended default. - B. Locate-at-runtime +
MZLIB_BRIDGE— ship no binary; the user installs the bridge separately. Simplest crate, most user friction (contradicts the zero-friction goal). Good as the dev/offline fallback, not as the primary story. - C. Embed via
include_bytes!/ a-syscrate — not viable at 130 MB.
Recommendation: A with B as the escape hatch. And note the two payload-shrink levers we already found: mzLib issue #1103 (TorchSharp/libtorch is ~238 MB of the payload and is dragged in transitively — making it optional shrinks both bindings) and the mzML-only native-reader prune (~20 MB of Thermo/Bruker readers). Both matter more for a download-at-build model.
Sub-decision under A: the bridge lives in smith-chem-wisc/pyMzLib today, and its CI already builds
it for all four platforms. Options, cleanest last:
- mzLibRust's CI builds the bridge itself from the pyMzLib bridge source (clone +
publish-bridge.ps1) — self-contained but duplicates build logic. - pyMzLib CI also uploads the raw self-contained bridge binaries as release assets (small
addition to
wheels.yml); mzLibRust'sbuild.rsdownloads those. Low-friction, shared source of truth. - Extract the bridge into its own repo
smith-chem-wisc/mzlib-bridgethat both pyMzLib and mzLibRust consume. The clean long-term end state (D6's logical conclusion), but a refactor of pyMzLib — defer until both bindings are real.
Recommend (2) for the first cut, (3) as the eventual home.
pyMzLib discovered its bugs and doc gaps by shipping and running biologist bake-offs. mzLibRust starts knowing all of them, so it can implement all three tranches correctly from the start:
- FlashLFQ: expose
peaksfrom day one as the match-between-runs surface — the peptide roll-up drops most MBR transfers (140 peaks vs 52 at peptide level; a whole run's transfers invisible). Option<f64>for protein intensity,f64for peptide (§2).- Document MBR's requirements up front: a complete/balanced design (same fractions across every
condition & replicate), and
mbr_q_value_thresholdas the FDR control that makes transfers trustworthy (a pyteomics bake-off arm proved you get ~80% false transfers without it). - Follow mzLib/FlashLFQ names; disclose the traps (census, isoform cap, fixed charges).
- Full
detection_typevocabulary (MSMS,MBR,MSMSIdentifiedButNotQuantified,MSMSAmbiguousPeakfinding,NotDetected).
Carry as-is: one JSON envelope per call; error classification lives in the bridge (free for Rust); large/variadic input on stdin not argv; follow mzLib naming; disclose the traps; language-neutral wire; the bridge is a translation layer, not a second implementation; offline fixtures by default + live canaries that skip (not fail) on outage.
Changes for Rust idiom:
- "Zero third-party runtime deps" (a Python-specific goal) → minimal, well-established deps:
serde,serde_json,thiserror; nothing heavy or exotic. State a low MSRV. - Errors are
Result<T, MzLibError>, not exceptions. - Prefer
Option<T>where the wire can be null;f64where it is a real 0.
- M0 — bridge round-trips.
mzlib::bridge_version()spawns the bridge, parses the envelope, checks the protocol version. Proves the whole transport + distribution story end to end (incl. a "no .NET installed" container test, like pyMzLib's). - M1 — PRIDE tranche. The narrow vignette (
list_files,download,total_size_bytes,PrideFile) — same "prove the packaging story" role pyMzLib's PRIDE M0 played. - M2 — peptidoform tranche.
mzlib::peptidoform::fragments(accession)→ typedDigest. - M3 — FlashLFQ tranche.
mzlib::flashlfq::quantify(...)→ typedFlashLfqResultswithpeaks,Option<f64>proteins,mbr_rescued_peptide_count. Correct from the start (§4). - Distribution/CI:
build.rsdownload-at-build; cargo build+test matrix (win/linux/macos);docs.rs; publish to crates.io. A biologist/rustacean bake-off to validate + find doc gaps, same methodology asdesign/bakeoff-flashlfq/DESIGN.md.
Rust proteomics is nascent but not empty. Before building, check what exists so mzLibRust brings what's missing rather than duplicating:
mzdata— mzML/mzMLb/raw reading in Rust (the read layer a hand-rolled quant would use).rustyms— peptide fragmentation + ProForma in Rust. Overlaps the peptidoform tranche — check carefully; the value is mzLib's UniProt annotation census + digestion, not re-fragmenting.sage— a Rust search engine (identifications), not quant. There is no mzLib-equivalent breadth (FlashLFQ, PRIDE client, deconvolution, the tested chemistry). So the value prop — and the bake-off contrast — is even sharper than pyteomics-vs-pymzlib was.
- Each binding is its own sibling folder + its own public repo —
E:\CodeReview\mzLibRust, laterE:\CodeReview\mzLibR— peers of pyMzLib, never nested under it. Different toolchains (PyPI / crates.io / CRAN), CI, and release cadences; nesting would tangle git + CI and confuse/project(cwd = identity, so one folder must be one project). - A multi-language paper ("mzLib for Python, Rust, and R — a language-neutral bridge") is its
own
/projectfolder + repo (e.g.mzlib-bindings-paper), referencing the binding repos as sources — the same way the lab already keeps code and papers in separate repos. Prefer one such paper over three per-language notes; pyMzLib's tentative Technical Note folds into it. - The real shared substrate is the bridge → eventually extract
smith-chem-wisc/mzlib-bridge(see §3), consumed by all bindings. Not a shared folder; a shared repo. - R is the same pattern and about as cheap: call the bridge via
processx+jsonlite.
- Distribution (§3): download-at-build vs env-locate — and where the bridge binary comes from.
- Crate name on crates.io (
mzlib? check availability; the repo ismzLibRust). Repo:smith-chem-wisc/mzLibRustpublic, workspace private like pyMzLib. - Sync vs async API — the bridge is a subprocess; sync is fine and simplest; an optional async (tokio) layer can come later.
- MSRV and the dependency floor.
- Whether to version the crate with the bridge or independently (pyMzLib versions the wheel independently of the bridge; mirror that).
/project initthis folder (E:\CodeReview\mzLibRust); create the public product reposmith-chem-wisc/mzLibRust+ the private workspace, mirroring pyMzLib's two-repo split./grill-meon the distribution decision (§3) — it shapes everything.- Prior-art check (§7):
mzdata,rustyms,sage, and the mzLib seam. - M0: bridge round-trip + the no-.NET proof, proving distribution before any tranche.
- Then M1 PRIDE, reusing the exact wire pyMzLib already consumes.
The user writes no Rust either (as with Python) — Claude owns all Rust authorship, idiom, packaging,
and CI. Explain tradeoffs in terms the user knows (C#/mzLib), the way PYTHON_PRIMER.md did for Python.