feat: add phylogenetic analysis CLI command and lazy distance traits
Introduce the `obikphylo` crate to support genome-vs-genome distance matrix computation and phylogenetic tree inference via Neighbor-Joining and UPGMA algorithms. Extend the `obikmer2` CLI with a new `phylo` command that exposes configurable metrics, presence thresholds, and CSV/Newick output options. Refactor `obikindex` to expose trait-based partial aggregation for efficient distance metric finalization without full matrix materialization. Update dependency graphs and remove obsolete storage modules.
This commit is contained in:
@@ -26,7 +26,8 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::layer::utils::LAYERNAME_SUFFIX;
|
||||
|
||||
use obicompactvec::{PersistentBitMatrix, PersistentIntMatrix};
|
||||
use ndarray::{Array1, Array2};
|
||||
use obicompactvec::{BitPartials, ColumnWeights, CountPartials, PersistentBitMatrix, PersistentIntMatrix};
|
||||
use obikseq::CanonicalKmer;
|
||||
|
||||
use crate::index::error::OKIResult;
|
||||
@@ -349,3 +350,79 @@ impl KmerLayer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── obicompactvec's statistics traits — read-only accessors, same spirit
|
||||
// as `col_weights`/`nonzero_iter` above, just formalised as trait impls so
|
||||
// generic aggregation code (`obikidxcache::IndexCache`'s callers) can rely
|
||||
// on `KmerLayer: ColumnWeights`/`CountPartials`/`BitPartials` rather than
|
||||
// duck-typing matching method names. `CountPartials`/`BitPartials` each
|
||||
// only make sense for the content this layer actually holds — calling the
|
||||
// wrong one panics, same convention as `col()`/`content()` above.
|
||||
|
||||
impl ColumnWeights for KmerLayer {
|
||||
fn col_weights(&self) -> Array1<u64> {
|
||||
self.col_weights()
|
||||
}
|
||||
}
|
||||
|
||||
impl CountPartials for KmerLayer {
|
||||
fn partial_bray(&self) -> Array2<u64> {
|
||||
match self {
|
||||
KmerLayer::Count { layer, .. } => layer.partial_bray(),
|
||||
KmerLayer::Presence { .. } => panic!("CountPartials::partial_bray() called on a Presence layer"),
|
||||
KmerLayer::Empty { .. } => panic!("CountPartials::partial_bray() called on an Empty layer"),
|
||||
}
|
||||
}
|
||||
fn partial_euclidean(&self) -> Array2<f64> {
|
||||
match self {
|
||||
KmerLayer::Count { layer, .. } => layer.partial_euclidean(),
|
||||
KmerLayer::Presence { .. } => panic!("CountPartials::partial_euclidean() called on a Presence layer"),
|
||||
KmerLayer::Empty { .. } => panic!("CountPartials::partial_euclidean() called on an Empty layer"),
|
||||
}
|
||||
}
|
||||
fn partial_threshold_jaccard(&self, threshold: u32) -> (Array2<u64>, Array2<u64>) {
|
||||
match self {
|
||||
KmerLayer::Count { layer, .. } => layer.partial_threshold_jaccard(threshold),
|
||||
KmerLayer::Presence { .. } => panic!("CountPartials::partial_threshold_jaccard() called on a Presence layer"),
|
||||
KmerLayer::Empty { .. } => panic!("CountPartials::partial_threshold_jaccard() called on an Empty layer"),
|
||||
}
|
||||
}
|
||||
fn partial_relfreq_bray(&self, global: &Array1<u64>) -> Array2<f64> {
|
||||
match self {
|
||||
KmerLayer::Count { layer, .. } => layer.partial_relfreq_bray(global),
|
||||
KmerLayer::Presence { .. } => panic!("CountPartials::partial_relfreq_bray() called on a Presence layer"),
|
||||
KmerLayer::Empty { .. } => panic!("CountPartials::partial_relfreq_bray() called on an Empty layer"),
|
||||
}
|
||||
}
|
||||
fn partial_relfreq_euclidean(&self, global: &Array1<u64>) -> Array2<f64> {
|
||||
match self {
|
||||
KmerLayer::Count { layer, .. } => layer.partial_relfreq_euclidean(global),
|
||||
KmerLayer::Presence { .. } => panic!("CountPartials::partial_relfreq_euclidean() called on a Presence layer"),
|
||||
KmerLayer::Empty { .. } => panic!("CountPartials::partial_relfreq_euclidean() called on an Empty layer"),
|
||||
}
|
||||
}
|
||||
fn partial_hellinger(&self, global: &Array1<u64>) -> Array2<f64> {
|
||||
match self {
|
||||
KmerLayer::Count { layer, .. } => layer.partial_hellinger(global),
|
||||
KmerLayer::Presence { .. } => panic!("CountPartials::partial_hellinger() called on a Presence layer"),
|
||||
KmerLayer::Empty { .. } => panic!("CountPartials::partial_hellinger() called on an Empty layer"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BitPartials for KmerLayer {
|
||||
fn partial_jaccard(&self) -> (Array2<u64>, Array2<u64>) {
|
||||
match self {
|
||||
KmerLayer::Presence { layer, .. } => layer.partial_jaccard(),
|
||||
KmerLayer::Count { .. } => panic!("BitPartials::partial_jaccard() called on a Count layer"),
|
||||
KmerLayer::Empty { .. } => panic!("BitPartials::partial_jaccard() called on an Empty layer"),
|
||||
}
|
||||
}
|
||||
fn partial_hamming(&self) -> Array2<u64> {
|
||||
match self {
|
||||
KmerLayer::Presence { layer, .. } => layer.partial_hamming(),
|
||||
KmerLayer::Count { .. } => panic!("BitPartials::partial_hamming() called on a Count layer"),
|
||||
KmerLayer::Empty { .. } => panic!("BitPartials::partial_hamming() called on an Empty layer"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,11 +196,11 @@ fn count_layer_reports_count_content_and_columnar_storage() {
|
||||
set_k(4);
|
||||
let dir = tempdir().unwrap();
|
||||
write_unitigs(dir.path(), &[b"AAAACGT"]);
|
||||
TypedLayer::<PersistentIntMatrix>::build(dir.pat
|
||||
h(), DEFAUL
|
||||
T_BLOCK_BITS, &Index
|
||||
Mode::Exact, |_| 1)
|
||||
,
|
||||
TypedLayer::<PersistentIntMatrix>::build(
|
||||
dir.path(),
|
||||
DEFAULT_BLOCK_BITS,
|
||||
&IndexMode::Exact,
|
||||
|_| 1,
|
||||
).unwrap();
|
||||
let layer = TypedLayer::<PersistentIntMatrix>::open(dir.path()).unwrap();
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// use crate::layer::utils::layer_dir;
|
||||
use obicompactvec::{
|
||||
BinaryMatrix, ColBuilder, MatrixBuilder, PersistentBitMatrix, PersistentBitMatrixBuilder,
|
||||
PersistentCompactIntMatrixBuilder, PersistentIntMatrix, PersistentSparseBitMatrix,
|
||||
BinaryMatrix, BitPartials, ColBuilder, CountPartials, MatrixBuilder, PersistentBitMatrix,
|
||||
PersistentBitMatrixBuilder, PersistentCompactIntMatrixBuilder, PersistentIntMatrix,
|
||||
PersistentSparseBitMatrix,
|
||||
};
|
||||
use ndarray::{Array1, Array2};
|
||||
use obikseq::CanonicalKmer;
|
||||
use obiskio::{UnitigFileReader, UnitigFileWriter};
|
||||
use std::collections::HashMap;
|
||||
@@ -424,6 +426,29 @@ impl TypedLayer<PersistentIntMatrix> {
|
||||
) -> Box<dyn Iterator<Item = (usize, usize, u32)> + 'a> {
|
||||
self.data.nonzero_iter(slots)
|
||||
}
|
||||
|
||||
/// `CountPartials`' required primitives, delegated straight to the
|
||||
/// underlying matrix — see `obicompactvec::CountPartials` for what
|
||||
/// each one means and how its provided finalisation methods
|
||||
/// (`bray_dist_matrix`, `jaccard_dist_matrix`, ...) consume them.
|
||||
pub fn partial_bray(&self) -> Array2<u64> {
|
||||
CountPartials::partial_bray(&self.data)
|
||||
}
|
||||
pub fn partial_euclidean(&self) -> Array2<f64> {
|
||||
CountPartials::partial_euclidean(&self.data)
|
||||
}
|
||||
pub fn partial_threshold_jaccard(&self, threshold: u32) -> (Array2<u64>, Array2<u64>) {
|
||||
CountPartials::partial_threshold_jaccard(&self.data, threshold)
|
||||
}
|
||||
pub fn partial_relfreq_bray(&self, global: &Array1<u64>) -> Array2<f64> {
|
||||
CountPartials::partial_relfreq_bray(&self.data, global)
|
||||
}
|
||||
pub fn partial_relfreq_euclidean(&self, global: &Array1<u64>) -> Array2<f64> {
|
||||
CountPartials::partial_relfreq_euclidean(&self.data, global)
|
||||
}
|
||||
pub fn partial_hellinger(&self, global: &Array1<u64>) -> Array2<f64> {
|
||||
CountPartials::partial_hellinger(&self.data, global)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Mode 3 — presence/absence matrix ─────────────────────────────────────────
|
||||
@@ -487,6 +512,15 @@ impl TypedLayer<PersistentBitMatrix> {
|
||||
self.data.nonzero_iter(slots)
|
||||
}
|
||||
|
||||
/// `BitPartials`' required primitives, delegated straight to the
|
||||
/// underlying matrix.
|
||||
pub fn partial_jaccard(&self) -> (Array2<u64>, Array2<u64>) {
|
||||
BitPartials::partial_jaccard(&self.data)
|
||||
}
|
||||
pub fn partial_hamming(&self) -> Array2<u64> {
|
||||
BitPartials::partial_hamming(&self.data)
|
||||
}
|
||||
|
||||
pub fn append_genome_column(
|
||||
layer_dir: &Path,
|
||||
value_of: impl Fn(usize) -> bool,
|
||||
|
||||
Reference in New Issue
Block a user