Add repeatable --exclude-genome flag to obikmer distance command
Integrate in-memory row/column zeroing and alignment filtering across SNP, Sankoff, TNT, and IQ-TREE output paths. Add strict validation for missing labels, implement `write_iqtree` with empirical stationary frequencies, and introduce `--raw-snp-counts` diagnostic CSV output. Update documentation to reflect experimental validation of backbone resolution limits and theoretical considerations for CTMC rate matrices.
This commit is contained in:
@@ -85,6 +85,24 @@ pub struct DistanceArgs {
|
||||
#[arg(long)]
|
||||
pub sibling_annex: bool,
|
||||
|
||||
/// Exclude a genome (by its exact label) from every computation below
|
||||
/// that reads the sibling annex — `--raw-snp-distance`/`--raw-snp-counts`,
|
||||
/// `--snp`, and `--sankoff` (and everything `--sankoff` implies:
|
||||
/// `p_hat`, `sub_cost`, `c_ctx`, the exported matrix/alignment,
|
||||
/// `--tnt`/`--phyg`/`--iqtree`). Repeatable. Does *not* affect the plain
|
||||
/// `--metric` distance matrix/NJ/UPGMA path (a different, unrelated
|
||||
/// computation). Applied by zeroing the excluded genome's row/column
|
||||
/// after `raw_snp_distance` runs (a pair with zero counts is already
|
||||
/// skipped by `calibrate_p_hat`/`base_pair_tally`, so this needs no
|
||||
/// change to the underlying traversal) and by dropping its row from
|
||||
/// `snp_pseudo_alignment`'s output — the annex is still built/scanned
|
||||
/// for the excluded genome too, just not used afterward. For a genome
|
||||
/// with almost no informative sites shared with anything else (see
|
||||
/// `docmd/theory/evolutionary_distances.md`, the IQ-TREE/Mash rogue-taxon
|
||||
/// discussion), its presence can otherwise silently bias `p_hat`/`R`.
|
||||
#[arg(long = "exclude-genome", value_name = "LABEL")]
|
||||
pub exclude_genome: Vec<String>,
|
||||
|
||||
/// Tally the sibling-count distribution (CSV) of an already-built annex
|
||||
/// (run with `--sibling-annex` first, in this invocation or an earlier
|
||||
/// one). A separate, occasional diagnostic pass — not run every time the
|
||||
@@ -211,6 +229,52 @@ pub fn run(args: DistanceArgs) {
|
||||
let labels: Vec<String> = idx.meta().genomes.iter().map(|g| g.label.clone()).collect();
|
||||
let n = labels.len();
|
||||
|
||||
// ── Genome exclusion (`--exclude-genome`) ───────────────────────────────
|
||||
// Applied by zeroing a `RawSnpDistanceOutput`'s excluded rows/columns
|
||||
// (`zero_excluded_pairs`) — `calibrate_p_hat`/`base_pair_tally` already
|
||||
// skip any pair with zero total counts, so this needs no change to
|
||||
// `obikindex`'s traversal — and by dropping the excluded genome's row
|
||||
// from a `SnpAlignment` plus the matching label (`drop_excluded`),
|
||||
// since an all-`∅` row for an "excluded" genome would otherwise still
|
||||
// reach TNT/PhyG/IQ-TREE as a real (empty) taxon.
|
||||
let exclude_mask: Vec<bool> = {
|
||||
let mut mask = vec![false; n];
|
||||
for label in &args.exclude_genome {
|
||||
match labels.iter().position(|l| l == label) {
|
||||
Some(i) => mask[i] = true,
|
||||
None => {
|
||||
eprintln!("error: --exclude-genome {label:?} does not match any genome in this index");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
mask
|
||||
};
|
||||
let zero_excluded_pairs = |result: &mut RawSnpDistanceOutput| {
|
||||
for i in 0..n {
|
||||
if !exclude_mask[i] {
|
||||
continue;
|
||||
}
|
||||
for j in 0..n {
|
||||
result.snp[[i, j]] = 0;
|
||||
result.snp[[j, i]] = 0;
|
||||
result.shared[[i, j]] = 0;
|
||||
result.shared[[j, i]] = 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
let drop_excluded = |alignment: SnpAlignment| -> (SnpAlignment, Vec<String>) {
|
||||
let sequences = alignment.sequences.into_iter().enumerate()
|
||||
.filter(|(i, _)| !exclude_mask[*i])
|
||||
.map(|(_, seq)| seq)
|
||||
.collect();
|
||||
let kept_labels = labels.iter().enumerate()
|
||||
.filter(|(i, _)| !exclude_mask[*i])
|
||||
.map(|(_, l)| l.clone())
|
||||
.collect();
|
||||
(SnpAlignment { sequences }, kept_labels)
|
||||
};
|
||||
|
||||
// ── Sibling-count/minorant annex (independent of the distance metric) ──
|
||||
// Construction (`--sibling-annex`) and stats (`--sibling-stats`) are
|
||||
// deliberately decoupled: the annex is meant to be (re)built routinely,
|
||||
@@ -237,17 +301,19 @@ pub fn run(args: DistanceArgs) {
|
||||
write_sibling_stats_csv(&stats, &labels, &args.output);
|
||||
}
|
||||
if args.raw_snp_distance {
|
||||
let result = idx.raw_snp_distance().unwrap_or_else(|e| {
|
||||
let mut result = idx.raw_snp_distance().unwrap_or_else(|e| {
|
||||
eprintln!("error computing raw SNP distance: {e}");
|
||||
std::process::exit(1);
|
||||
});
|
||||
zero_excluded_pairs(&mut result);
|
||||
write_raw_snp_distance_csv(&result, &labels, &args.output);
|
||||
}
|
||||
if args.raw_snp_counts {
|
||||
let result = idx.raw_snp_distance().unwrap_or_else(|e| {
|
||||
let mut result = idx.raw_snp_distance().unwrap_or_else(|e| {
|
||||
eprintln!("error computing raw SNP distance: {e}");
|
||||
std::process::exit(1);
|
||||
});
|
||||
zero_excluded_pairs(&mut result);
|
||||
write_raw_snp_counts_csv(&result, &labels, &args.output);
|
||||
}
|
||||
if args.snp {
|
||||
@@ -255,13 +321,15 @@ pub fn run(args: DistanceArgs) {
|
||||
eprintln!("error computing SNP pseudo-alignment: {e}");
|
||||
std::process::exit(1);
|
||||
});
|
||||
write_snp_fasta(&alignment, &labels, &args.output);
|
||||
let (alignment, kept_labels) = drop_excluded(alignment);
|
||||
write_snp_fasta(&alignment, &kept_labels, &args.output);
|
||||
}
|
||||
if args.sankoff || args.tnt || args.phyg || args.iqtree {
|
||||
let raw = idx.raw_snp_distance().unwrap_or_else(|e| {
|
||||
let mut raw = idx.raw_snp_distance().unwrap_or_else(|e| {
|
||||
eprintln!("error computing raw SNP distance: {e}");
|
||||
std::process::exit(1);
|
||||
});
|
||||
zero_excluded_pairs(&mut raw);
|
||||
let estimate = calibrate_p_hat(&raw, args.sankoff_ratio_ceiling);
|
||||
let m = (idx.kmer_size() - 1) / 2;
|
||||
|
||||
@@ -282,16 +350,17 @@ pub fn run(args: DistanceArgs) {
|
||||
eprintln!("error computing SNP pseudo-alignment: {e}");
|
||||
std::process::exit(1);
|
||||
});
|
||||
write_sankoff_alignment_fasta(&alignment, &labels, &args.output);
|
||||
let (alignment, kept_labels) = drop_excluded(alignment);
|
||||
write_sankoff_alignment_fasta(&alignment, &kept_labels, &args.output);
|
||||
|
||||
if args.tnt {
|
||||
write_sankoff_tnt(&matrix, &alignment, &labels, &args.output, args.sankoff_cost_scale);
|
||||
write_sankoff_tnt(&matrix, &alignment, &kept_labels, &args.output, args.sankoff_cost_scale);
|
||||
}
|
||||
if args.phyg {
|
||||
write_sankoff_phyg(&matrix, &args.output, args.sankoff_cost_scale);
|
||||
}
|
||||
if args.iqtree {
|
||||
write_iqtree(&matrix, &alignment, &labels, &args.output);
|
||||
write_iqtree(&matrix, &alignment, &kept_labels, &args.output);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user