seqcolyte / qc /core /src /spec.rs
seqmachines's picture
Deploy Seqcolyte Studio
ff34739 verified
Raw
History Blame Contribute Delete
3.21 kB
//! Loose spec accessors over `serde_json::Value` — no schema re-validation (the Python side
//! already loaded/validated the spec). Mirrors the handful of `seqcolyte.spec.model.Spec`
//! accessors the checks/eval need: R1 `cycles`, the cell-barcode segment slice, the TSO oligo,
//! and `platform_params.dark_base`.
use anyhow::{anyhow, Result};
use serde_json::Value;
const TSO_OLIGO: &str = "oligo_template_switching_oligo_tso";
pub struct SpecInfo {
pub r1_cycles: i64,
pub cb_start: usize,
pub cb_len: usize,
pub tso: Option<String>,
pub dark_base: Option<u8>,
}
pub fn parse_spec(v: &Value) -> Result<SpecInfo> {
let reads = v
.get("read_structure")
.and_then(|r| r.get("reads"))
.and_then(|r| r.as_array())
.ok_or_else(|| anyhow!("spec missing read_structure.reads"))?;
let r1 = reads
.iter()
.find(|rd| rd.get("read").and_then(|x| x.as_str()) == Some("R1"))
.ok_or_else(|| anyhow!("spec has no R1 read"))?;
// `spec.read("R1").get("cycles", 28)`
let r1_cycles = r1.get("cycles").and_then(|c| c.as_i64()).unwrap_or(28);
// `spec.segment_slice("R1", "cell_barcode")` via segment_offsets.
let (cb_start, cb_len) = cell_barcode_slice(r1)?;
// `spec.oligo_sequence("oligo_template_switching_oligo_tso")` (absent -> None; checks skip).
let tso = v
.get("oligos")
.and_then(|o| o.as_array())
.and_then(|arr| {
arr.iter()
.find(|ol| ol.get("oligo_id").and_then(|x| x.as_str()) == Some(TSO_OLIGO))
})
.and_then(|ol| ol.get("sequence"))
.and_then(|s| s.as_str())
.map(|s| s.to_string());
// `spec.platform_params.get("dark_base")` — falsey (missing / null / empty) -> None.
let dark_base = v
.get("platform_params")
.and_then(|p| p.get("dark_base"))
.and_then(|d| d.as_str())
.filter(|s| !s.is_empty())
.and_then(|s| s.bytes().next());
Ok(SpecInfo {
r1_cycles,
cb_start,
cb_len,
tso,
dark_base,
})
}
/// Port of `segment_offsets("R1")["cell_barcode"]`: sort R1 segments by `order`, accumulate
/// fixed `length`, stop at the first segment without a fixed length, return cell_barcode's
/// `(start, len)`.
fn cell_barcode_slice(r1: &Value) -> Result<(usize, usize)> {
let segs = r1
.get("segments")
.and_then(|s| s.as_array())
.ok_or_else(|| anyhow!("R1 has no segments"))?;
let mut sorted: Vec<&Value> = segs.iter().collect();
sorted.sort_by_key(|s| s.get("order").and_then(|o| o.as_i64()).unwrap_or(0));
let mut pos: usize = 0;
let mut cb: Option<(usize, usize)> = None;
for s in sorted {
let length = match s.get("length").and_then(|l| l.as_i64()) {
Some(l) => l as usize,
None => break, // variable-length segment: stop accumulating definite offsets
};
let name = s.get("name").and_then(|n| n.as_str()).unwrap_or("");
if name == "cell_barcode" {
cb = Some((pos, length));
}
pos += length;
}
cb.ok_or_else(|| anyhow!("R1 has no fixed-length cell_barcode segment"))
}