Extract phylogenetic sibling logic into new obikphylo crate

Relocate the `siblings` and `cardcomp` modules from `obikindex` to a dedicated `obikphylo` workspace member. Convert inherent methods on `KmerIndex` into extension traits, update import paths across `obikmer`, and add supporting accessor methods to `obikseq` and `obilayeredmap`. This restructuring reduces the public API surface of `obikindex` while organizing phylogenetic iteration, caching, and distance calculation logic under a dedicated crate.
This commit is contained in:
Eric Coissac
2026-08-16 14:30:34 +02:00
parent 519195d4a1
commit 5997de6707
31 changed files with 831 additions and 547 deletions
+44
View File
@@ -367,6 +367,50 @@ impl<L: KmerLength> CanonicalKmerOf<L> {
pub fn into_kmer(self) -> KmerOf<L> {
KmerOf(self.0, PhantomData)
}
/// This k-mer's own minimiser — as a standalone unit, not part of a
/// streamed sequence. Enumerates all `L::len() - MLen::len() + 1`
/// windows directly on the packed 2-bit representation (no ASCII
/// round-trip, no rolling state, no entropy tracking) and keeps the one
/// with the lowest [`hash_kmer`] — the same selection rule
/// `obiskbuilder::rolling_stat::RollingStat` computes incrementally
/// along a sequence, replicated here in `O(k)` pure bit arithmetic for
/// a single isolated k-mer (the case a streaming rolling scan is
/// unnecessary machinery for). Ties (equal hash) keep the last
/// (highest-position) window, matching `RollingStat`'s monotonic-deque
/// eviction rule (`>=` pops the previous record) — a formal detail, not
/// a practical concern: a `mix64` collision between two distinct
/// m-mers is vanishingly unlikely.
pub fn minimizer(&self) -> Minimizer {
let k = L::len();
let ml = MLen::len();
let rc = self.revcomp().raw();
let mut best_canon: RawKmer = 0;
let mut best_hash = u64::MAX;
for p in 0..=(k - ml) {
let fwd = (self.0 << (2 * p)) >> (KMER_BITS - 2 * ml);
let rev = (rc << (2 * (k - ml - p))) >> (KMER_BITS - 2 * ml);
let canon = fwd.min(rev);
let hash = hash_kmer(canon << (KMER_BITS - 2 * ml));
if hash < best_hash {
best_hash = hash;
best_canon = canon;
}
}
Minimizer::from_raw_unchecked(best_canon << (KMER_BITS - 2 * ml))
}
/// Destination partition for this k-mer, using the project-wide routing
/// rule (`minimizer().seq_hash() & mask`) — the same rule
/// `KmerPartition`/`RoutableSuperKmer` apply to k-mers read from a
/// streamed sequence, here for a standalone k-mer (e.g. a synthetic
/// variant generated outside any sequence). `n_partitions` must be a
/// power of two.
#[inline]
pub fn partition(&self, n_partitions: usize) -> usize {
let mask = (n_partitions as u64) - 1;
(self.minimizer().seq_hash() & mask) as usize
}
}
impl<L: KmerLength> Sequence for CanonicalKmerOf<L> {