mod em;
mod kmer;
mod myaln;
use anyhow::{Context, Result};
pub fn mem_reset_peak() {}
pub fn mem_peak_delta() -> usize {
0
}
use clap::{Args, Parser, Subcommand};
use flate2::read::MultiGzDecoder;
use kmer::KmerSet;
use needletail::parse_fastx_reader;
use std::fs::File;
use std::io::{BufRead, BufReader, BufWriter, Read, Write};
use std::process::{Command, Stdio};
use std::sync::mpsc::sync_channel;
use std::sync::Arc;
use std::thread;
const EMB_MTREF_GZ: &[u8] = include_bytes!("embedded/mtref.txt.gz");
const EMB_RCRS_GZ: &[u8] = include_bytes!("embedded/rcrs_wrap.fa.gz");
fn gunzip(bytes: &'static [u8]) -> Result<Vec<u8>> {
let mut out = Vec::new();
MultiGzDecoder::new(bytes)
.read_to_end(&mut out)
.context("decompress embedded reference")?;
Ok(out)
}
fn embedded_refdata() -> Result<em::RefData> {
let raw = gunzip(EMB_MTREF_GZ)?;
em::RefData::load_reader(BufReader::new(&raw[..]))
}
fn embedded_kmer_ref() -> Result<Vec<u8>> {
let raw = gunzip(EMB_RCRS_GZ)?;
let mut seq = Vec::new();
for line in raw.split(|&b| b == b'\n') {
if line.first() == Some(&b'>') {
if !seq.is_empty() {
break;
}
} else {
seq.extend(line.iter().filter(|b| !b.is_ascii_whitespace()));
}
}
Ok(seq)
}
#[derive(Parser)]
#[command(name = "mtnoc", version, about = "mtNoC — de-identified mtDNA contributor lower bound")]
struct Cli {
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Subcommand)]
enum Cmd {
Fast(EstimateArgs),
Accurate(EstimateArgs),
#[command(hide = true)]
Validate {
#[arg(long)]
reference: String,
#[arg(long, default_value = "NC_012920.1")]
contig: String,
#[arg(long)]
c1: String,
#[arg(long)]
c2: String,
#[arg(long, default_value_t = 0.90)]
min_identity: f64,
#[arg(long)]
truth_names: String,
#[arg(long)]
truth_pos: String,
},
#[command(hide = true)]
Rngtest,
#[command(hide = true)]
Emtest {
#[arg(long)]
profiles: String,
#[arg(long, default_value_t = 0.02)]
min_prop: f64,
#[arg(long, default_value_t = 30)]
n_boot: usize,
#[arg(long, default_value_t = 0)]
seed: u64,
},
}
#[derive(Args)]
struct EstimateArgs {
#[arg(long, value_delimiter = ',', conflicts_with = "in_stem")]
in1: Vec<String>,
#[arg(long, value_delimiter = ',', conflicts_with = "in_stem")]
in2: Vec<String>,
#[arg(long = "in", value_delimiter = ',')]
in_stem: Vec<String>,
#[arg(long, value_delimiter = ',')]
out1: Vec<String>,
#[arg(long, value_delimiter = ',')]
out2: Vec<String>,
#[arg(long = "out", value_delimiter = ',')]
out_stem: Vec<String>,
#[arg(long)]
r#ref: String,
#[arg(long, default_value = "NC_012920.1")]
contig: String,
#[arg(long, value_delimiter = ',')]
bam: Vec<String>,
#[arg(long, default_value = "strobealign")]
strobealign: String,
#[arg(long, default_value = "samtools")]
samtools: String,
#[arg(long, default_value_t = 0.90)]
min_identity: f64,
#[arg(long, default_value_t = 0.02)]
min_prop: f64,
#[arg(long, default_value_t = 30)]
n_boot: usize,
#[arg(long, default_value_t = 0)]
seed: u64,
#[arg(long, default_value_t = 0)]
thread: usize,
#[arg(long)]
header: bool,
}
fn base_code(b: u8) -> i8 {
match b {
b'A' | b'a' => 0,
b'C' | b'c' => 1,
b'G' | b'g' => 2,
b'T' | b't' => 3,
_ => -1,
}
}
fn to_read_profile(rd: &em::RefData, obs: &[(i64, u8)]) -> em::ReadProfile {
let mut prof = em::ReadProfile::new();
for &(pos, base) in obs {
if let Some(&j) = rd.info_idx.get(&pos) {
let c = base_code(base);
if c >= 0 {
prof.push((j, c));
}
}
}
prof
}
const OUT_HEADER: &str =
"sample\tinformative_reads\tcontributors_ci95\tcomposition";
fn format_row(sample: &str, rd: &em::RefData, est: &em::RangeEstimate) -> String {
let comp: Vec<String> = est
.components
.iter()
.map(|c| format!("{}:{:.4}:{}r", rd.haplo[c.rep as usize], c.proportion, c.reads))
.collect();
let ci = format!("{};{};{}", est.ci_lo, est.lower_bound_sub, est.ci_hi);
format!("{sample}\t{}\t{ci}\t{}", est.informative_reads, comp.join(";"))
}
#[allow(clippy::too_many_arguments)]
fn estimate_fast(
rd: &em::RefData,
kset: &Arc<KmerSet>,
index: &myaln::RefIndex,
r1: &str,
r2: &str,
out1: Option<&str>,
out2: Option<&str>,
min_identity: f64,
min_prop: f64,
n_boot: usize,
seed: u64,
) -> Result<em::RangeEstimate> {
let reads = prefilter(kset, r1, r2, out1, out2, 1)?;
eprintln!("[fast] candidate mate-reads: {}", reads.len());
use rayon::prelude::*;
let per_mate: Vec<Option<(String, Vec<(i64, u8)>)>> = reads
.par_iter()
.map(|rd_read| {
myaln::align_with_alleles(index, &rd_read.seq, &rd_read.qual).and_then(|(a, alleles)| {
if a.ident >= min_identity {
Some((rd_read.name.clone(), alleles))
} else {
None
}
})
})
.collect();
use rustc_hash::FxHashMap;
let mut frags: FxHashMap<String, em::ReadProfile> = FxHashMap::default();
let mut n_mapped = 0usize;
for (name, obs) in per_mate.into_iter().flatten() {
n_mapped += 1;
let prof = to_read_profile(rd, &obs);
frags.entry(name).or_default().extend(prof);
}
eprintln!("[fast] mate alignments accepted: {n_mapped}; fragments: {}", frags.len());
let reads_v: Vec<em::ReadProfile> = frags.into_values().collect();
Ok(em::estimate_range(rd, &reads_v, min_prop, n_boot, 0.8, 8.0, 4000, seed, 200))
}
fn run_emtest(
profiles: &str,
min_prop: f64,
n_boot: usize,
seed: u64,
) -> Result<()> {
let rd = embedded_refdata()?;
let f = BufReader::new(File::open(profiles)?);
let mut reads_v: Vec<em::ReadProfile> = Vec::new();
for line in f.lines() {
let line = line?;
let mut it = line.splitn(2, '\t');
let _qname = it.next().unwrap_or("");
let rest = it.next().unwrap_or("");
if rest.is_empty() {
continue;
}
let mut obs: Vec<(i64, u8)> = Vec::new();
for tok in rest.split(',') {
let mut pa = tok.split(':');
if let (Some(p), Some(a)) = (pa.next(), pa.next()) {
if let Ok(pos) = p.parse::<i64>() {
let base = a.as_bytes().first().copied().unwrap_or(b'N');
obs.push((pos, base));
}
}
}
reads_v.push(to_read_profile(&rd, &obs));
}
eprintln!("[emtest] fragments loaded: {}", reads_v.len());
let est = em::estimate_range(&rd, &reads_v, min_prop, n_boot, 0.8, 8.0, 4000, seed, 200);
println!("{OUT_HEADER}");
println!("{}", format_row("emtest", &rd, &est));
Ok(())
}
fn parse_pileup_bases(bases: &[u8], ref_base: u8) -> Vec<u8> {
let ref_base = ref_base.to_ascii_uppercase();
let mut out: Vec<u8> = Vec::new();
let n = bases.len();
let mut i = 0usize;
while i < n {
let ch = bases[i];
match ch {
b'^' => i += 2, b'$' => i += 1, b'+' | b'-' => {
let mut j = i + 1;
let mut num = 0usize;
let mut has = false;
while j < n && bases[j].is_ascii_digit() {
num = num * 10 + (bases[j] - b'0') as usize;
j += 1;
has = true;
}
i = j + if has { num } else { 0 };
}
b'.' | b',' => {
out.push(ref_base);
i += 1;
}
b'A' | b'C' | b'G' | b'T' | b'a' | b'c' | b'g' | b't' => {
out.push(ch.to_ascii_uppercase());
i += 1;
}
b'N' | b'n' => {
out.push(b'N');
i += 1;
}
b'*' | b'#' => {
out.push(b'*');
i += 1;
}
_ => i += 1, }
}
out
}
fn filter_bam_accurate(samtools: &str, in_bam: &str, out_bam: &str, contig: &str) -> Result<()> {
let pipeline = format!(
"{s} view -h -e '[NM]/qlen <= 0.027 && mapq >= 20' \"{i}\" {c} | \
awk 'BEGIN{{OFS=\"\\t\"}} /^@/{{print;next}} {{gsub(/,/,\"_\",$1); print}}' | \
{s} view -b -o \"{o}\" -",
s = samtools,
i = in_bam,
c = contig,
o = out_bam
);
let status = Command::new("bash")
.arg("-c")
.arg(&pipeline)
.status()
.context("spawn samtools filter pipeline")?;
anyhow::ensure!(status.success(), "samtools filter pipeline failed");
let status = Command::new(samtools)
.args(["index", out_bam])
.status()
.context("spawn samtools index")?;
anyhow::ensure!(status.success(), "samtools index failed");
Ok(())
}
fn pileup_profiles(
rd: &em::RefData,
samtools: &str,
reference: &str,
contig: &str,
bam: &str,
) -> Result<Vec<em::ReadProfile>> {
let mut child = Command::new(samtools)
.args([
"mpileup",
"-f",
reference,
"-r",
contig,
"-q",
"20",
"-Q",
"20",
"--output-QNAME",
bam,
])
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.context("spawn samtools mpileup")?;
let stdout = child.stdout.take().context("mpileup stdout")?;
let reader = BufReader::new(stdout);
use rustc_hash::FxHashMap;
let mut order: Vec<em::ReadProfile> = Vec::new();
let mut idx: FxHashMap<String, usize> = FxHashMap::default();
for line in reader.lines() {
let line = line?;
let cols: Vec<&str> = line.split('\t').collect();
if cols.len() < 7 {
continue;
}
let pos: i64 = match cols[1].parse() {
Ok(p) => p,
Err(_) => continue,
};
let j = match rd.info_idx.get(&pos) {
Some(&j) => j,
None => continue,
};
let ref_base = cols[2].as_bytes().first().copied().unwrap_or(b'N');
let alleles = parse_pileup_bases(cols[4].as_bytes(), ref_base);
let names: Vec<&str> = if cols[6].is_empty() {
Vec::new()
} else {
cols[6].split(',').collect()
};
if names.len() != alleles.len() {
continue;
}
for (name, &allele) in names.iter().zip(alleles.iter()) {
let code = base_code(allele); let entry = *idx.entry((*name).to_string()).or_insert_with(|| {
order.push(em::ReadProfile::new());
order.len() - 1
});
order[entry].push((j, code));
}
}
let status = child.wait().context("wait samtools mpileup")?;
anyhow::ensure!(status.success(), "samtools mpileup failed");
Ok(order)
}
fn align_strobealign(
strobealign: &str,
samtools: &str,
reference: &str,
r1: &str,
r2: &str,
out_bam: &str,
threads: usize,
) -> Result<()> {
let t = threads.max(1);
let pipeline = format!(
"{sb} -t {t} \"{r}\" \"{r1}\" \"{r2}\" | {st} sort -@ {t} -o \"{o}\" -",
sb = strobealign, st = samtools, r = reference, r1 = r1, r2 = r2, o = out_bam, t = t,
);
let status = Command::new("bash")
.arg("-c")
.arg(&pipeline)
.status()
.context("spawn strobealign | samtools sort pipeline")?;
anyhow::ensure!(status.success(), "strobealign | samtools sort failed");
let status = Command::new(samtools)
.args(["index", out_bam])
.status()
.context("spawn samtools index")?;
anyhow::ensure!(status.success(), "samtools index failed");
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn estimate_accurate(
rd: &em::RefData,
reference: &str,
contig: &str,
r1: &str,
r2: &str,
bam: Option<&str>,
strobealign: &str,
samtools: &str,
sample: &str,
min_prop: f64,
n_boot: usize,
seed: u64,
threads: usize,
) -> Result<em::RangeEstimate> {
let safe = sample.replace('/', "_");
let pid = std::process::id();
let raw = std::env::temp_dir().join(format!("mtnoc_raw_{safe}_{pid}.bam"));
let raw_s = raw.to_string_lossy().to_string();
let flt = std::env::temp_dir().join(format!("mtnoc_flt_{safe}_{pid}.bam"));
let flt_s = flt.to_string_lossy().to_string();
let result = (|| -> Result<em::RangeEstimate> {
let in_bam = match bam {
Some(b) => b.to_string(),
None => {
align_strobealign(strobealign, samtools, reference, r1, r2, &raw_s, threads)?;
raw_s.clone()
}
};
filter_bam_accurate(samtools, &in_bam, &flt_s, contig)?;
let profiles = pileup_profiles(rd, samtools, reference, contig, &flt_s)?;
eprintln!("[accurate] fragments: {}", profiles.len());
Ok(em::estimate_range(rd, &profiles, min_prop, n_boot, 0.8, 8.0, 4000, seed, 200))
})();
for p in [&raw_s, &format!("{raw_s}.bai"), &flt_s, &format!("{flt_s}.bai")] {
let _ = std::fs::remove_file(p);
}
result
}
struct Chunk {
buf: Vec<u8>, ends: Vec<u32>, matched: Vec<bool>,
}
impl Chunk {
fn with_capacity(n: usize) -> Self {
Chunk { buf: Vec::with_capacity(n * 384), ends: Vec::with_capacity(n), matched: Vec::with_capacity(n) }
}
#[inline]
fn len(&self) -> usize {
self.ends.len()
}
#[inline]
fn is_empty(&self) -> bool {
self.ends.is_empty()
}
}
const CHUNK: usize = 8192;
fn spawn_producer(
path: String,
kset: Arc<KmerSet>,
min_kmers: usize,
) -> (thread::JoinHandle<Result<()>>, std::sync::mpsc::Receiver<Chunk>) {
let (tx, rx) = sync_channel::<Chunk>(8);
let h = thread::spawn(move || -> Result<()> {
let f = File::open(&path).with_context(|| format!("open {path}"))?;
let dec = MultiGzDecoder::new(BufReader::with_capacity(1 << 20, f));
let mut reader = parse_fastx_reader(dec).with_context(|| format!("parse {path}"))?;
let mut chunk = Chunk::with_capacity(CHUNK);
while let Some(rec) = reader.next() {
let rec = rec.with_context(|| format!("record in {path}"))?;
let m = kset.matches(&rec.raw_seq(), min_kmers);
chunk.buf.extend_from_slice(rec.all());
chunk.ends.push(chunk.buf.len() as u32);
chunk.matched.push(m);
if chunk.len() >= CHUNK {
if tx.send(std::mem::replace(&mut chunk, Chunk::with_capacity(CHUNK))).is_err() {
break;
}
}
}
if !chunk.is_empty() {
let _ = tx.send(chunk);
}
Ok(())
});
(h, rx)
}
fn parse_fastq_record(raw: &[u8], mate: u8) -> Option<myaln::Read> {
let mut it = raw.split(|&b| b == b'\n');
let header = it.next()?;
let seq = it.next()?;
let _plus = it.next()?;
let qual = it.next()?;
if header.first() != Some(&b'@') {
return None;
}
let nb = &header[1..];
let end = nb.iter().position(|b| b.is_ascii_whitespace()).unwrap_or(nb.len());
let name: String = nb[..end].iter().map(|&b| if b == b',' { '_' } else { b as char }).collect();
Some(myaln::Read { name, mate, seq: seq.to_vec(), qual: qual.to_vec() })
}
fn prefilter(
kset: &Arc<KmerSet>,
r1: &str,
r2: &str,
out1: Option<&str>,
out2: Option<&str>,
min_kmers: usize,
) -> Result<Vec<myaln::Read>> {
let (h1, rx1) = spawn_producer(r1.to_string(), kset.clone(), min_kmers);
let (h2, rx2) = spawn_producer(r2.to_string(), kset.clone(), min_kmers);
let mut w1 = out1.map(File::create).transpose()?.map(|f| BufWriter::with_capacity(1 << 20, f));
let mut w2 = out2.map(File::create).transpose()?.map(|f| BufWriter::with_capacity(1 << 20, f));
let mut reads1: Vec<myaln::Read> = Vec::new();
let mut reads2: Vec<myaln::Read> = Vec::new();
let mut total: u64 = 0;
let mut kept: u64 = 0;
loop {
let c1 = rx1.recv().ok();
let c2 = rx2.recv().ok();
match (c1, c2) {
(Some(c1), Some(c2)) => {
if c1.len() != c2.len() {
anyhow::bail!("R1/R2 chunk length mismatch");
}
let (mut s1, mut s2) = (0u32, 0u32);
for i in 0..c1.len() {
let (e1, e2) = (c1.ends[i], c2.ends[i]);
total += 1;
if c1.matched[i] || c2.matched[i] {
kept += 1;
let rec1 = &c1.buf[s1 as usize..e1 as usize];
let rec2 = &c2.buf[s2 as usize..e2 as usize];
if let Some(w) = w1.as_mut() {
w.write_all(rec1)?;
w.write_all(b"\n")?;
}
if let Some(w) = w2.as_mut() {
w.write_all(rec2)?;
w.write_all(b"\n")?;
}
if let Some(r) = parse_fastq_record(rec1, 1) {
reads1.push(r);
}
if let Some(r) = parse_fastq_record(rec2, 2) {
reads2.push(r);
}
}
s1 = e1;
s2 = e2;
}
}
(None, None) => break,
_ => anyhow::bail!("R1/R2 record count mismatch"),
}
}
if let Some(w) = w1.as_mut() {
w.flush()?;
}
if let Some(w) = w2.as_mut() {
w.flush()?;
}
h1.join().unwrap()?;
h2.join().unwrap()?;
eprintln!(
"[fast] prefilter pairs total={total} kept={kept} ({:.4}%)",
100.0 * kept as f64 / total.max(1) as f64
);
reads1.append(&mut reads2);
Ok(reads1)
}
struct Job {
name: String,
r1: String,
r2: String,
out1: Option<String>,
out2: Option<String>,
bam: Option<String>,
}
fn derive_name(r1: &str) -> String {
let base = std::path::Path::new(r1)
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| r1.to_string());
for tag in ["_R1", ".R1", "_1.", ".1."] {
if let Some(idx) = base.find(tag) {
return base[..idx].to_string();
}
}
base.split('.').next().unwrap_or(&base).to_string()
}
fn resolve_stem(stem: &str) -> Result<(String, String)> {
for (t1, t2) in [("_R1", "_R2"), (".R1", ".R2"), ("_1", "_2"), (".1", ".2")] {
for ext in [".fastq.gz", ".fq.gz", ".fastq", ".fq"] {
let r1 = format!("{stem}{t1}{ext}");
let r2 = format!("{stem}{t2}{ext}");
if std::path::Path::new(&r1).exists() && std::path::Path::new(&r2).exists() {
return Ok((r1, r2));
}
}
}
anyhow::bail!("could not resolve R1/R2 for stem '{stem}' (tried _R1/.R1/_1/.1 × .fastq[.gz]/.fq[.gz])")
}
fn resolve_jobs(a: &EstimateArgs) -> Result<Vec<Job>> {
let pairs: Vec<(String, String)> = if !a.in_stem.is_empty() {
a.in_stem.iter().map(|s| resolve_stem(s)).collect::<Result<_>>()?
} else if !a.in1.is_empty() || !a.in2.is_empty() {
anyhow::ensure!(
a.in1.len() == a.in2.len(),
"--in1 and --in2 must list the same number of files ({} vs {})",
a.in1.len(),
a.in2.len()
);
a.in1.iter().cloned().zip(a.in2.iter().cloned()).collect()
} else if !a.bam.is_empty() {
Vec::new()
} else {
anyhow::bail!("no input: give --in1/--in2, --in <stem>, or (accurate) --bam");
};
let outs: Vec<(Option<String>, Option<String>)> = if !a.out_stem.is_empty() {
a.out_stem
.iter()
.map(|s| (Some(format!("{s}_R1.fq")), Some(format!("{s}_R2.fq"))))
.collect()
} else if !a.out1.is_empty() || !a.out2.is_empty() {
anyhow::ensure!(
a.out1.len() == a.out2.len(),
"--out1 and --out2 must list the same number of files"
);
a.out1.iter().cloned().map(Some).zip(a.out2.iter().cloned().map(Some)).collect()
} else {
Vec::new()
};
if pairs.is_empty() && !a.bam.is_empty() {
return Ok(a
.bam
.iter()
.map(|b| Job {
name: std::path::Path::new(b)
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| b.clone()),
r1: String::new(),
r2: String::new(),
out1: None,
out2: None,
bam: Some(b.clone()),
})
.collect());
}
if !outs.is_empty() {
anyhow::ensure!(
outs.len() == pairs.len(),
"number of outputs ({}) must match number of input samples ({})",
outs.len(),
pairs.len()
);
}
if !a.bam.is_empty() {
anyhow::ensure!(
a.bam.len() == pairs.len(),
"number of --bam ({}) must match number of input samples ({})",
a.bam.len(),
pairs.len()
);
}
Ok(pairs
.into_iter()
.enumerate()
.map(|(i, (r1, r2))| {
let name = derive_name(&r1);
let (out1, out2) = outs.get(i).cloned().unwrap_or((None, None));
Job { name, r1, r2, out1, out2, bam: a.bam.get(i).cloned() }
})
.collect())
}
fn run_estimate_cmd(a: &EstimateArgs, accurate: bool) -> Result<()> {
if a.thread > 0 {
rayon::ThreadPoolBuilder::new().num_threads(a.thread).build_global().ok();
}
let jobs = resolve_jobs(a)?;
anyhow::ensure!(!jobs.is_empty(), "no samples to process");
let rd = embedded_refdata()?;
let index = myaln::RefIndex::build_multi(&a.r#ref, &a.contig)?;
let kset = if accurate {
Arc::new(KmerSet::from_seq(&[]))
} else {
Arc::new(KmerSet::from_seq(&embedded_kmer_ref()?))
};
if !accurate {
eprintln!("[fast] prefilter reference k-mers: {}", kset.len());
}
if a.header {
println!("{OUT_HEADER}");
}
let out = std::io::stdout();
for job in &jobs {
eprintln!("[{}] sample {}", if accurate { "accurate" } else { "fast" }, job.name);
let est = if accurate {
estimate_accurate(
&rd, &a.r#ref, &a.contig, &job.r1, &job.r2, job.bam.as_deref(),
&a.strobealign, &a.samtools, &job.name, a.min_prop, a.n_boot, a.seed, a.thread,
)?
} else {
estimate_fast(
&rd, &kset, &index, &job.r1, &job.r2, job.out1.as_deref(), job.out2.as_deref(),
a.min_identity, a.min_prop, a.n_boot, a.seed,
)?
};
use std::io::Write as _;
writeln!(out.lock(), "{}", format_row(&job.name, &rd, &est))?;
}
Ok(())
}
fn main() -> Result<()> {
let cli = Cli::parse();
match cli.cmd {
Cmd::Fast(a) => run_estimate_cmd(&a, false)?,
Cmd::Accurate(a) => run_estimate_cmd(&a, true)?,
Cmd::Validate { reference, contig, c1, c2, min_identity, truth_names, truth_pos } => {
use rustc_hash::FxHashMap;
use std::collections::HashSet;
let truth: HashSet<String> = BufReader::new(File::open(&truth_names)?)
.lines()
.filter_map(|l| l.ok())
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty())
.collect();
let mut tpos: FxHashMap<(String, u8), i64> = FxHashMap::default();
for l in BufReader::new(File::open(&truth_pos)?).lines() {
let l = l?;
let mut it = l.split('\t');
if let (Some(q), Some(m), Some(p)) = (it.next(), it.next(), it.next()) {
if let (Ok(m), Ok(p)) = (m.parse::<u8>(), p.parse::<i64>()) {
tpos.entry((q.to_string(), m)).and_modify(|e| { if p < *e { *e = p } }).or_insert(p);
}
}
}
myaln::run_validate(&reference, &contig, &c1, &c2, min_identity, &truth, &tpos)?
}
Cmd::Rngtest => em::rng_selftest(),
Cmd::Emtest { profiles, min_prop, n_boot, seed } => {
run_emtest(&profiles, min_prop, n_boot, seed)?
}
}
Ok(())
}