seqcolyte / qc /core /src /main.rs
seqmachines's picture
Deploy Seqcolyte Studio
ff34739 verified
Raw
History Blame Contribute Delete
2.72 kB
//! `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(())
}