feat: introduce obikpartition crate for kmer partition layer lookup
Introduces the `obikpartition` crate containing the `KmerPartition` struct to manage open layers in sequential order. The `open` constructor eagerly initializes layers under a specified directory, while the `find` method returns the index of the first layer containing a given k-mer. Dependencies are strictly scoped to `obilayeredmap` and `obikseq`, with unit tests validating layer ingestion and single-kmer lookup behavior. Batch lookups and further migration are deferred to a subsequent phase.
This commit is contained in:
Generated
+11
@@ -1655,6 +1655,17 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "obikpartition"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"obicompactvec",
|
||||
"obikseq",
|
||||
"obilayeredmap",
|
||||
"obiskio",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "obikpartitionner"
|
||||
version = "0.1.0"
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["obikseq", "obiread", "obiskbuilder", "obifastwrite", "obikmer","obikrope","obipipeline", "obikpartitionner","obiskio","obidebruinj","obilayeredmap", "obicompactvec", "obisys", "obikindex", "obitaxonomy", "obikentropy", "obikphylo"]
|
||||
members = ["obikseq", "obiread", "obiskbuilder", "obifastwrite", "obikmer","obikrope","obipipeline", "obikpartitionner","obiskio","obidebruinj","obilayeredmap", "obicompactvec", "obisys", "obikindex", "obitaxonomy", "obikentropy", "obikphylo", "obikpartition"]
|
||||
[profile.release]
|
||||
debug = 1
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "obikpartition"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
obikseq = { path = "../obikseq" }
|
||||
obilayeredmap = { path = "../obilayeredmap" }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
obikseq = { path = "../obikseq", features = ["test-utils"] }
|
||||
obiskio = { path = "../obiskio" }
|
||||
obicompactvec = { path = "../obicompactvec" }
|
||||
@@ -0,0 +1,69 @@
|
||||
//! The **Partition** tier of the `Index { Partition { Layer } }` model —
|
||||
//! see `DevDocMD/implementation/partition_layer_cache.md`'s "Definitions"
|
||||
//! section for the authoritative naming/scope discussion this crate
|
||||
//! implements. `obilayeredmap` already holds the **Layer** tier as its own
|
||||
//! crate rather than living inside `obikindex`; this crate holds the tier
|
||||
//! above it the same way.
|
||||
//!
|
||||
//! [`KmerPartition`] represents one partition's already-open layers — a
|
||||
//! read cache, opened once and held for the run, not rebuilt per lookup.
|
||||
//! It does no path computation of its own beyond the shared
|
||||
//! `obilayeredmap::layer_dir` naming primitive: which partition, which
|
||||
//! `index_dir`, how many layers, and `IndexMode` are `KmerIndex`'s
|
||||
//! (`obikindex`) job to resolve and hand in as plain arguments — this
|
||||
//! crate depends only on `obilayeredmap` and below, never on `obikindex`,
|
||||
//! so it cannot reach back for them itself.
|
||||
//!
|
||||
//! Not yet wired into any caller: `obikphylo::siblings::cache::
|
||||
//! PartitionCache` (`Vec<Vec<obilayeredmap::Layer>>`) and
|
||||
//! `obikindex::query_layer::QueryLayer` (uncached, bypasses `Layer`
|
||||
//! entirely) both still reinvent a fragment of this. Migrating them is a
|
||||
//! separate, deferred step.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use obikseq::CanonicalKmer;
|
||||
use obilayeredmap::{layer_dir, IndexMode, Layer, OLMResult};
|
||||
|
||||
/// One partition's open layers, in layer order (layer 0 first).
|
||||
pub struct KmerPartition {
|
||||
layers: Vec<Layer>,
|
||||
}
|
||||
|
||||
impl KmerPartition {
|
||||
/// Open every layer under `index_dir` (`index_dir/layer_0`,
|
||||
/// `index_dir/layer_1`, ... up to `n_layers`), eagerly — not lazily on
|
||||
/// first access, so the caller pays the mmap cost once, up front,
|
||||
/// rather than at an unpredictable point during later lookups.
|
||||
pub fn open(index_dir: &Path, mode: &IndexMode, n_layers: usize, with_counts: bool) -> OLMResult<Self> {
|
||||
let layers = (0..n_layers)
|
||||
.map(|l| Layer::open(&layer_dir(index_dir, l), mode, with_counts))
|
||||
.collect::<OLMResult<Vec<_>>>()?;
|
||||
Ok(Self { layers })
|
||||
}
|
||||
|
||||
pub fn n_layers(&self) -> usize {
|
||||
self.layers.len()
|
||||
}
|
||||
|
||||
pub fn layer(&self, i: usize) -> &Layer {
|
||||
&self.layers[i]
|
||||
}
|
||||
|
||||
pub fn layers(&self) -> &[Layer] {
|
||||
&self.layers
|
||||
}
|
||||
|
||||
/// Existence lookup of `kmer` across this partition's layers: tries
|
||||
/// each in turn, stopping at the first hit and reporting which layer it
|
||||
/// was — `find_slot`, not a data read, for a plain existence check.
|
||||
pub fn find(&self, kmer: CanonicalKmer) -> Option<usize> {
|
||||
self.layers
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(|(li, layer)| layer.find_slot(kmer).map(|_| li))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,90 @@
|
||||
use super::*;
|
||||
use obikseq::{set_k, Unitig};
|
||||
use obiskio::{UnitigFileWriter, DEFAULT_BLOCK_BITS};
|
||||
use tempfile::tempdir;
|
||||
|
||||
fn write_layer(root: &Path, l: usize, seqs: &[&[u8]], n_genomes: usize, mode: &IndexMode) {
|
||||
let dir = layer_dir(root, l);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let mut w = UnitigFileWriter::create(&dir.join("unitigs.bin")).unwrap();
|
||||
for s in seqs {
|
||||
w.write(&Unitig::from_ascii(s)).unwrap();
|
||||
}
|
||||
w.close().unwrap();
|
||||
obilayeredmap::TypedLayer::<obicompactvec::PersistentBitMatrix>::build_presence(
|
||||
&dir,
|
||||
DEFAULT_BLOCK_BITS,
|
||||
mode,
|
||||
n_genomes,
|
||||
|_kmer, _g| true,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_reads_every_layer_in_order() {
|
||||
// k=4, process-wide in test builds — see obilayeredmap's own tests for
|
||||
// why a fixed, unshared k matters here.
|
||||
set_k(4);
|
||||
let dir = tempdir().unwrap();
|
||||
let mode = IndexMode::Exact;
|
||||
write_layer(dir.path(), 0, &[b"AAATCTA"], 1, &mode);
|
||||
write_layer(dir.path(), 1, &[b"CTTCGCC"], 1, &mode);
|
||||
|
||||
let partition = KmerPartition::open(dir.path(), &mode, 2, false).unwrap();
|
||||
|
||||
assert_eq!(partition.n_layers(), 2);
|
||||
assert_eq!(partition.layers().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_reports_the_first_layer_that_carries_the_kmer() {
|
||||
set_k(4);
|
||||
let dir = tempdir().unwrap();
|
||||
let mode = IndexMode::Exact;
|
||||
write_layer(dir.path(), 0, &[b"AAATCTA"], 1, &mode);
|
||||
write_layer(dir.path(), 1, &[b"CTTCGCC"], 1, &mode);
|
||||
|
||||
let partition = KmerPartition::open(dir.path(), &mode, 2, false).unwrap();
|
||||
|
||||
let in_layer_0 = obiskio::CanonicalKmerIter::new(&layer_dir(dir.path(), 0).join("unitigs.bin"))
|
||||
.unwrap()
|
||||
.next()
|
||||
.unwrap();
|
||||
let in_layer_1 = obiskio::CanonicalKmerIter::new(&layer_dir(dir.path(), 1).join("unitigs.bin"))
|
||||
.unwrap()
|
||||
.next()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(partition.find(in_layer_0), Some(0));
|
||||
assert_eq!(partition.find(in_layer_1), Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_returns_none_for_an_absent_kmer() {
|
||||
set_k(4);
|
||||
let dir = tempdir().unwrap();
|
||||
let mode = IndexMode::Exact;
|
||||
write_layer(dir.path(), 0, &[b"AAATCTA"], 1, &mode);
|
||||
|
||||
let partition = KmerPartition::open(dir.path(), &mode, 1, false).unwrap();
|
||||
|
||||
let absent = obiskio::CanonicalKmerIter::new(&layer_dir(dir.path(), 0).join("unitigs.bin"))
|
||||
.unwrap()
|
||||
.next()
|
||||
.unwrap();
|
||||
// Not actually absent from the MPHF domain in a general sense, but a
|
||||
// kmer that was never written to this partition's only layer is what
|
||||
// "absent" means here — build a second, disjoint layer purely to
|
||||
// source a genuinely foreign kmer instead of relying on ptr_hash's
|
||||
// undefined behaviour for out-of-domain queries.
|
||||
let other_dir = tempdir().unwrap();
|
||||
write_layer(other_dir.path(), 0, &[b"GGGGTAC"], 1, &mode);
|
||||
let foreign = obiskio::CanonicalKmerIter::new(&layer_dir(other_dir.path(), 0).join("unitigs.bin"))
|
||||
.unwrap()
|
||||
.next()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(partition.find(absent), Some(0));
|
||||
assert_eq!(partition.find(foreign), None);
|
||||
}
|
||||
Reference in New Issue
Block a user