Spaces:
Sleeping
Sleeping
| //! `qc-core` — the Seqcolyte QC compute core (parity-preserving port of the reference Python) | |
| //! (`qc/profile.py` + `qc/checks.py` + `qc/eval.py`). Reads a FASTQ pair once and prints | |
| //! `{ "profile": {…}, "findings": […], "eval": {…}|null }` to stdout. | |
| mod checks; | |
| mod cli; | |
| mod eval; | |
| mod fmt; | |
| mod model; | |
| mod seqops; | |
| mod spec; | |
| mod stream; | |
| mod whitelist; | |
| use anyhow::{bail, Context, Result}; | |
| use clap::Parser; | |
| use memchr::memmem::Finder; | |
| use crate::cli::Args; | |
| use crate::model::{LenStats, Output, Profile}; | |
| const ADAPTER_STEM: &[u8] = b"AGATCGGAAGAGC"; | |
| fn main() -> Result<()> { | |
| let args = Args::parse(); | |
| let spec_text = std::fs::read_to_string(&args.spec) | |
| .with_context(|| format!("reading spec {}", args.spec))?; | |
| let spec_json: serde_json::Value = | |
| serde_json::from_str(&spec_text).with_context(|| format!("parsing spec {}", args.spec))?; | |
| let spec = spec::parse_spec(&spec_json)?; | |
| // Whitelist (optional) — load once, encoded to u64. | |
| let wl = match &args.whitelist { | |
| Some(p) => Some( | |
| whitelist::load(p, spec.cb_len).with_context(|| format!("loading whitelist {}", p))?, | |
| ), | |
| None => None, | |
| }; | |
| // The Python eval calls spec.oligo_sequence(TSO), which raises if the TSO is absent. | |
| // Mirror that: labels without a TSO oligo is an error (rather than silently scoring | |
| // TSO-less predictions). | |
| if args.labels.is_some() && spec.tso.is_none() { | |
| bail!("labels given but spec has no TSO oligo (oligo_template_switching_oligo_tso)"); | |
| } | |
| let ctx = stream::Ctx { | |
| tso: spec.tso.as_deref().map(str::as_bytes), | |
| dark: spec.dark_base, | |
| stem: Finder::new(ADAPTER_STEM), | |
| wl: wl.as_ref(), | |
| cb_start: spec.cb_start, | |
| cb_len: spec.cb_len, | |
| collect_preds: args.labels.is_some(), | |
| }; | |
| let res = stream::stream(&args.r1, &args.r2, &ctx, args.max_reads) | |
| .with_context(|| "streaming FASTQ pair")?; | |
| let profile = Profile { | |
| n_pairs: res.acc.n, | |
| r1_len: LenStats { | |
| min: res.h1.min(), | |
| max: res.h1.max(), | |
| modal: res.h1.modal(), | |
| }, | |
| r2_len: LenStats { | |
| min: res.h2.min(), | |
| max: res.h2.max(), | |
| modal: res.h2.modal(), | |
| }, | |
| }; | |
| let findings = checks::build_findings(&spec, &res.acc, &res.h1, wl.is_some()); | |
| let eval = match &args.labels { | |
| Some(p) => Some(eval::evaluate(&res.acc.preds, p).with_context(|| format!("eval vs {}", p))?), | |
| None => None, | |
| }; | |
| let output = Output { | |
| profile, | |
| findings, | |
| eval, | |
| }; | |
| serde_json::to_writer(std::io::stdout(), &output)?; | |
| println!(); | |
| Ok(()) | |
| } | |