Extract index modules into specialized workspace subcrates

This commit partitions the obikindex crate into multiple focused subcrates (obikfilter, obikmerge, obikquery, obikrebuild, obikselect, obikstats, obikdump, and obikidxcache) to reduce coupling and clarify module boundaries. It standardizes error handling across the workspace using OKIError and OKIResult, updates index APIs to support lazy, disk-backed partition access, and migrates NUMA system utilities to a new obisys crate. All modifications are structural, focusing on dependency graph expansion, import path updates, and API surface reorganization without altering core runtime behavior.
This commit is contained in:
Eric Coissac
2026-08-22 06:25:28 +02:00
parent c9d10d55c7
commit fc4464a0ef
81 changed files with 1640 additions and 1196 deletions
+108 -2
View File
@@ -1519,6 +1519,14 @@ dependencies = [
name = "obikalgorithm" name = "obikalgorithm"
version = "0.1.0" version = "0.1.0"
[[package]]
name = "obikdump"
version = "0.1.0"
dependencies = [
"obikfilter",
"obikindex",
]
[[package]] [[package]]
name = "obikentropy" name = "obikentropy"
version = "0.1.0" version = "0.1.0"
@@ -1526,6 +1534,27 @@ dependencies = [
"obikseq", "obikseq",
] ]
[[package]]
name = "obikfilter"
version = "0.1.0"
dependencies = [
"obicompactvec",
"obikindex",
"obikseq",
"obiskio",
"obitaxonomy",
]
[[package]]
name = "obikidxcache"
version = "0.1.0"
dependencies = [
"ndarray",
"obicompactvec",
"rayon",
"tempfile",
]
[[package]] [[package]]
name = "obikindex" name = "obikindex"
version = "0.1.0" version = "0.1.0"
@@ -1533,9 +1562,7 @@ dependencies = [
"anyhow", "anyhow",
"bitvec", "bitvec",
"cacheline-ef", "cacheline-ef",
"crossbeam-channel",
"epserde", "epserde",
"hwlocality",
"indicatif", "indicatif",
"memmap2", "memmap2",
"ndarray", "ndarray",
@@ -1597,11 +1624,13 @@ dependencies = [
"obidebruinj", "obidebruinj",
"obifastwrite", "obifastwrite",
"obikalgorithm", "obikalgorithm",
"obikfilter",
"obikindex", "obikindex",
"obikindexer", "obikindexer",
"obikphylo", "obikphylo",
"obikrope", "obikrope",
"obikseq", "obikseq",
"obikstats",
"obipipeline", "obipipeline",
"obiread", "obiread",
"obiskbuilder", "obiskbuilder",
@@ -1618,6 +1647,37 @@ dependencies = [
"tracing-subscriber", "tracing-subscriber",
] ]
[[package]]
name = "obikmer2"
version = "1.2.2"
dependencies = [
"clap",
"obikalgorithm",
"obikindex",
"obikindexer",
"obikseq",
"obipipeline",
"obiread",
"obisys",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "obikmerge"
version = "0.1.0"
dependencies = [
"obicompactvec",
"obikfilter",
"obikindex",
"obikindexer",
"obikseq",
"obipipeline",
"obiskio",
"obisys",
"tracing",
]
[[package]] [[package]]
name = "obikphylo" name = "obikphylo"
version = "0.1.0" version = "0.1.0"
@@ -1626,6 +1686,7 @@ dependencies = [
"ndarray", "ndarray",
"obicompactvec", "obicompactvec",
"obikalgorithm", "obikalgorithm",
"obikidxcache",
"obikindex", "obikindex",
"obikindexer", "obikindexer",
"obikseq", "obikseq",
@@ -1641,6 +1702,29 @@ dependencies = [
"tracing-subscriber", "tracing-subscriber",
] ]
[[package]]
name = "obikquery"
version = "0.1.0"
dependencies = [
"obikindex",
]
[[package]]
name = "obikrebuild"
version = "0.1.0"
dependencies = [
"obicompactvec",
"obidebruinj",
"obikfilter",
"obikindex",
"obikindexer",
"obikmerge",
"obikseq",
"obiskio",
"obisys",
"tracing",
]
[[package]] [[package]]
name = "obikrope" name = "obikrope"
version = "0.1.0" version = "0.1.0"
@@ -1649,6 +1733,16 @@ dependencies = [
"criterion2", "criterion2",
] ]
[[package]]
name = "obikselect"
version = "0.1.0"
dependencies = [
"obicompactvec",
"obikindex",
"obisys",
"tracing",
]
[[package]] [[package]]
name = "obikseq" name = "obikseq"
version = "0.1.0" version = "0.1.0"
@@ -1660,6 +1754,15 @@ dependencies = [
"xxhash-rust", "xxhash-rust",
] ]
[[package]]
name = "obikstats"
version = "0.1.0"
dependencies = [
"obicompactvec",
"obikindex",
"rayon",
]
[[package]] [[package]]
name = "obipipeline" name = "obipipeline"
version = "0.1.0" version = "0.1.0"
@@ -1712,8 +1815,11 @@ dependencies = [
name = "obisys" name = "obisys"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"crossbeam-channel",
"hwlocality",
"indicatif", "indicatif",
"libc", "libc",
"rayon",
"sysinfo", "sysinfo",
"tracing", "tracing",
] ]
+1 -1
View File
@@ -1,5 +1,5 @@
[workspace] [workspace]
resolver = "3" resolver = "3"
members = ["obikseq", "obiread", "obiskbuilder", "obifastwrite", "obikmer","obikrope","obipipeline", "obiskio","obidebruinj", "obicompactvec", "obisys", "obikindex", "obikindexer", "obitaxonomy", "obikentropy", "obikphylo", "obikalgorithm"] members = ["obikseq", "obiread", "obiskbuilder", "obifastwrite", "obikmer","obikmer2","obikrope","obipipeline", "obiskio","obidebruinj", "obicompactvec", "obisys", "obikindex", "obikindexer", "obikquery", "obikdump", "obikfilter", "obikselect", "obikrebuild", "obikmerge", "obikstats", "obikidxcache", "obitaxonomy", "obikentropy", "obikphylo", "obikalgorithm"]
[profile.release] [profile.release]
debug = 1 debug = 1
+8
View File
@@ -0,0 +1,8 @@
[package]
name = "obikdump"
version = "0.1.0"
edition = "2024"
[dependencies]
obikindex = { path = "../obikindex" }
obikfilter = { path = "../obikfilter" }
@@ -3,9 +3,9 @@ use std::sync::atomic::{AtomicUsize, Ordering};
use rayon::prelude::*; use rayon::prelude::*;
use crate::index::error::{OKIError, OKIResult}; use obikindex::{OKIError, OKIResult};
use crate::index::kmer_index::KmerIndex; use obikindex::KmerIndex;
use crate::index::KmerFilter; use obikfilter::KmerFilter;
impl KmerIndex { impl KmerIndex {
/// Write a CSV table of all indexed kmers to `out`. /// Write a CSV table of all indexed kmers to `out`.
+8
View File
@@ -0,0 +1,8 @@
//! Raw content export of an `obikindex::KmerIndex`: staging ground for
//! code currently living in `obikindex::index`'s dump path (`dump.rs`,
//! `dump_layer.rs`), to be migrated here to lighten that crate. Kept as
//! a separate crate — not a module of `obikindex` — so the dependency
//! runs one way only (dump code depends on the data model, never the
//! reverse), same pattern as `obikindexer`/`obikquery`.
mod dump;
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "obikfilter"
version = "0.1.0"
edition = "2024"
[dependencies]
obikindex = { path = "../obikindex" }
obicompactvec = { path = "../obicompactvec" }
obikseq = { path = "../obikseq" }
obiskio = { path = "../obiskio" }
obitaxonomy = { path = "../obitaxonomy" }
@@ -1,20 +1,11 @@
use obikindex::layer::MphfLayer;
use obicompactvec::{PersistentBitMatrix, PersistentCompactIntMatrix}; use obicompactvec::{PersistentBitMatrix, PersistentCompactIntMatrix};
use obikseq::CanonicalKmer; use obikseq::CanonicalKmer;
use crate::layer::{MphfLayer, OLMError}; use obiskio::UnitigFileReader;
use obiskio::{SKError, SKResult, UnitigFileReader}; use obikindex::{OKIError, OKIResult};
use crate::index::filter::{KmerFilter, passes_all}; use crate::filter::{KmerFilter, passes_all};
use crate::index::kmer_index::KmerIndex; use obikindex::KmerIndex;
fn olm_to_sk(e: OLMError) -> SKError {
match e {
OLMError::Io(e) => SKError::Io(e),
other => SKError::InvalidData {
context: "dump",
detail: other.to_string(),
},
}
}
impl KmerIndex { impl KmerIndex {
/// Iterate all indexed kmers in partition `part`, calling `cb(kmer, row)` for each /// Iterate all indexed kmers in partition `part`, calling `cb(kmer, row)` for each
@@ -35,7 +26,7 @@ impl KmerIndex {
n_genomes: usize, n_genomes: usize,
filters: &[Box<dyn KmerFilter>], filters: &[Box<dyn KmerFilter>],
mut cb: impl FnMut(CanonicalKmer, Box<[u32]>) -> bool, mut cb: impl FnMut(CanonicalKmer, Box<[u32]>) -> bool,
) -> SKResult<bool> { ) -> OKIResult<bool> {
let index_dir = self.index_dir(part); let index_dir = self.index_dir(part);
if !index_dir.exists() { if !index_dir.exists() {
return Ok(true); return Ok(true);
@@ -43,19 +34,19 @@ impl KmerIndex {
let mut l = 0; let mut l = 0;
loop { loop {
let layer_dir = self.layer_dir(part, l); let layer_dir = self.layer_dir(part, l)?;
if !layer_dir.exists() { if !layer_dir.exists() {
break; break;
} }
l += 1; l += 1;
let mphf = MphfLayer::open(&layer_dir).map_err(olm_to_sk)?; let mphf = MphfLayer::open(&layer_dir)?;
let reader = UnitigFileReader::open_sequential(&layer_dir.join("unitigs.bin"))?; let reader = UnitigFileReader::open_sequential(&layer_dir.join("unitigs.bin"))?;
let counts_dir = layer_dir.join("counts"); let counts_dir = layer_dir.join("counts");
let presence_dir = layer_dir.join("presence"); let presence_dir = layer_dir.join("presence");
let cont = if use_counts && counts_dir.exists() { let cont = if use_counts && counts_dir.exists() {
let mat = PersistentCompactIntMatrix::open(&layer_dir).map_err(SKError::Io)?; let mat = PersistentCompactIntMatrix::open(&layer_dir).map_err(OKIError::Io)?;
let mut cont = true; let mut cont = true;
for (kmer, _, _) in reader.iter_indexed_canonical_kmers() { for (kmer, _, _) in reader.iter_indexed_canonical_kmers() {
if let Some(slot) = mphf.find(kmer) { if let Some(slot) = mphf.find(kmer) {
@@ -70,7 +61,7 @@ impl KmerIndex {
} }
cont cont
} else if !use_counts && presence_dir.exists() { } else if !use_counts && presence_dir.exists() {
let mat = PersistentBitMatrix::open(&layer_dir).map_err(SKError::Io)?; let mat = PersistentBitMatrix::open(&layer_dir).map_err(OKIError::Io)?;
let mut cont = true; let mut cont = true;
for (kmer, _, _) in reader.iter_indexed_canonical_kmers() { for (kmer, _, _) in reader.iter_indexed_canonical_kmers() {
if let Some(slot) = mphf.find(kmer) { if let Some(slot) = mphf.find(kmer) {
@@ -123,7 +114,7 @@ impl KmerIndex {
n_genomes: usize, n_genomes: usize,
filters: &[Box<dyn KmerFilter>], filters: &[Box<dyn KmerFilter>],
mut cb: impl FnMut(usize, usize, CanonicalKmer, Box<[u32]>) -> bool, mut cb: impl FnMut(usize, usize, CanonicalKmer, Box<[u32]>) -> bool,
) -> SKResult<bool> { ) -> OKIResult<bool> {
let index_dir = self.index_dir(part); let index_dir = self.index_dir(part);
if !index_dir.exists() { if !index_dir.exists() {
return Ok(true); return Ok(true);
@@ -135,14 +126,14 @@ impl KmerIndex {
if !layer_dir.exists() { if !layer_dir.exists() {
break; break;
} }
let mphf = MphfLayer::open(&layer_dir).map_err(olm_to_sk)?; let mphf = MphfLayer::open(&layer_dir)?;
let reader = UnitigFileReader::open_sequential(&layer_dir.join("unitigs.bin"))?; let reader = UnitigFileReader::open_sequential(&layer_dir.join("unitigs.bin"))?;
let counts_dir = layer_dir.join("counts"); let counts_dir = layer_dir.join("counts");
let presence_dir = layer_dir.join("presence"); let presence_dir = layer_dir.join("presence");
let cont = if use_counts && counts_dir.exists() { let cont = if use_counts && counts_dir.exists() {
let mat = PersistentCompactIntMatrix::open(&layer_dir).map_err(SKError::Io)?; let mat = PersistentCompactIntMatrix::open(&layer_dir).map_err(OKIError::Io)?;
let mut cont = true; let mut cont = true;
for (kmer, _, _) in reader.iter_indexed_canonical_kmers() { for (kmer, _, _) in reader.iter_indexed_canonical_kmers() {
if let Some(slot) = mphf.find(kmer) { if let Some(slot) = mphf.find(kmer) {
@@ -157,7 +148,7 @@ impl KmerIndex {
} }
cont cont
} else if !use_counts && presence_dir.exists() { } else if !use_counts && presence_dir.exists() {
let mat = PersistentBitMatrix::open(&layer_dir).map_err(SKError::Io)?; let mat = PersistentBitMatrix::open(&layer_dir).map_err(OKIError::Io)?;
let mut cont = true; let mut cont = true;
for (kmer, _, _) in reader.iter_indexed_canonical_kmers() { for (kmer, _, _) in reader.iter_indexed_canonical_kmers() {
if let Some(slot) = mphf.find(kmer) { if let Some(slot) = mphf.find(kmer) {
+22
View File
@@ -0,0 +1,22 @@
//! Per-kmer filtering: `KmerFilter` judges one k-mer (its canonical
//! sequence + its per-genome row) at a time, deciding whether it passes.
//! Orthogonal to genome-column selection/aggregation (`obikselect`), which
//! operates on already-retained k-mers.
//!
//! [`filter`] (the `KmerFilter` trait + its implementations) depends only
//! on `obicompactvec`/`obikseq`, not on `obikindex`. [`dump_layer`] is the
//! extension over `obikindex::KmerIndex` that actually iterates a
//! partition's k-mers through those filters (`iter_partition_kmers`,
//! `iter_partition_kmers_located`) — every k-mer, filtered or not, goes
//! through this same path (`passes_all` on an empty filter list is always
//! `true`), so this crate depends one-way on `obikindex`, not the reverse.
mod filter;
mod dump_layer;
mod predicate;
pub use filter::{
GroupQuorumFilter, KmerFilter, MaxGenomeCount, MaxGenomeFraction, MaxTotalCount,
MinComplexity, MinGenomeCount, MinGenomeFraction, MinTotalCount, passes_all,
};
pub use predicate::{GroupFilterParams, MetaPred};
@@ -1,9 +1,9 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::index::GroupQuorumFilter; use crate::filter::GroupQuorumFilter;
use obitaxonomy::{TaxPath, TaxPattern}; use obitaxonomy::{TaxPath, TaxPattern};
use crate::index::meta::{GenomeInfo, IndexMeta}; use obikindex::{GenomeInfo, IndexMeta};
// ── Operator ────────────────────────────────────────────────────────────────── // ── Operator ──────────────────────────────────────────────────────────────────
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "obikidxcache"
version = "0.1.0"
edition = "2024"
[dependencies]
obicompactvec = { path = "../obicompactvec" }
ndarray = "0.17"
rayon = "1"
[dev-dependencies]
tempfile = "3"
+11
View File
@@ -0,0 +1,11 @@
//! Caching/aggregation over already-opened `obikindex` layer/partition
//! objects — a runtime cache shape, not part of the stateless
//! `Index { Partition { Layer } }` directory-structure model itself.
//! [`LayeredStore`] wraps one entry per layer (or per partition, via
//! `LayeredStore<LayeredStore<S>>`) and propagates `ColumnWeights`/
//! `CountPartials`/`BitPartials` aggregation across them. No dependency on
//! `obikindex` — generic over any `S` implementing those traits.
mod layered_store;
pub use layered_store::LayeredStore;
-6
View File
@@ -19,13 +19,11 @@ niffler = "3.0.0"
memmap2 = "0.9.10" memmap2 = "0.9.10"
ndarray = "0.17" ndarray = "0.17"
rayon = "1" rayon = "1"
crossbeam-channel = "0.5"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
indicatif = "0.18" indicatif = "0.18"
tracing = "0.1.44" tracing = "0.1.44"
bitvec = "1" bitvec = "1"
hwlocality = { version = "1.0.0-alpha.11", features = ["vendored"], optional = true }
[dev-dependencies] [dev-dependencies]
obiread = { path = "../obiread" } obiread = { path = "../obiread" }
@@ -33,7 +31,3 @@ obikseq = { path = "../obikseq", features = ["test-utils"] }
tempfile = "3" tempfile = "3"
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] } tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
anyhow = "1" anyhow = "1"
[features]
default = ["numa"]
numa = ["hwlocality"]
+15 -4
View File
@@ -44,7 +44,11 @@ pub trait IndexBuilder: Sized {
/// For construction paths that build partitions from scratch (`select`, /// For construction paths that build partitions from scratch (`select`,
/// `rebuild`). `merge` bootstraps by copying a source index instead, so /// `rebuild`). `merge` bootstraps by copying a source index instead, so
/// it does not use this. /// it does not use this.
fn create_skeleton<P: AsRef<Path>>(output: P, config: IndexConfig, genomes: Vec<GenomeInfo>) -> OKIResult<Self>; fn create_skeleton<P: AsRef<Path>>(
output: P,
config: IndexConfig,
genomes: Vec<GenomeInfo>,
) -> OKIResult<Self>;
/// Mark `output` as fully indexed, pack its column matrices, and reopen it. /// Mark `output` as fully indexed, pack its column matrices, and reopen it.
/// ///
@@ -61,7 +65,7 @@ pub trait IndexBuilder: Sized {
impl IndexBuilder for KmerIndex { impl IndexBuilder for KmerIndex {
fn clear_output_for_create<P: AsRef<Path>>(output: P, force: bool) -> OKIResult<()> { fn clear_output_for_create<P: AsRef<Path>>(output: P, force: bool) -> OKIResult<()> {
let output = output.as_ref(); let output = output.as_ref();
if Self::exists(output) { if Self::is_an_index(output) {
if force { if force {
fs::remove_dir_all(output).map_err(OKIError::Io)?; fs::remove_dir_all(output).map_err(OKIError::Io)?;
} else { } else {
@@ -74,11 +78,18 @@ impl IndexBuilder for KmerIndex {
Ok(()) Ok(())
} }
fn create_skeleton<P: AsRef<Path>>(output: P, config: IndexConfig, genomes: Vec<GenomeInfo>) -> OKIResult<Self> { fn create_skeleton<P: AsRef<Path>>(
output: P,
config: IndexConfig,
genomes: Vec<GenomeInfo>,
) -> OKIResult<Self> {
let output = output.as_ref(); let output = output.as_ref();
fs::create_dir_all(output).map_err(OKIError::Io)?; fs::create_dir_all(output).map_err(OKIError::Io)?;
let meta = IndexMeta::create_at(output, config, genomes).map_err(OKIError::Io)?; let meta = IndexMeta::create_at(output, config, genomes).map_err(OKIError::Io)?;
Ok(KmerIndex { root_path: output.to_owned(), meta: Arc::new(meta) }) Ok(KmerIndex {
root_path: output.to_owned(),
meta: Arc::new(meta),
})
} }
fn finalize_indexed<P: AsRef<Path>>(output: P, rep: &mut Reporter) -> OKIResult<Self> { fn finalize_indexed<P: AsRef<Path>>(output: P, rep: &mut Reporter) -> OKIResult<Self> {
+6 -46
View File
@@ -1,65 +1,25 @@
use std::path::Path; use crate::index::error::{OKIError, OKIResult};
use obicompactvec::{PersistentBitVecBuilder, PersistentCompactIntVecBuilder}; use obicompactvec::{PersistentBitVecBuilder, PersistentCompactIntVecBuilder};
use crate::layer::meta::PartitionMeta;
use crate::layer::{layer_dir, IndexMode, OLMError};
use obiskio::{SKError, SKResult};
// ── olm_to_sk ────────────────────────────────────────────────────────────────
pub fn olm_to_sk(e: OLMError, context: &'static str) -> SKError {
match e {
OLMError::Io(e) => SKError::Io(e),
other => SKError::InvalidData {
context,
detail: other.to_string(),
},
}
}
// ── load_meta ────────────────────────────────────────────────────────────────
/// Load PartitionMeta, or recover it by probing layer directories.
/// Indexes built before meta.json was introduced lack the file.
pub(crate) fn load_meta(dir: &Path, context: &'static str) -> SKResult<PartitionMeta> {
match PartitionMeta::load(dir) {
Ok(m) => Ok(m),
Err(e) if matches!(e, OLMError::Io(ref io_e) if io_e.kind() == std::io::ErrorKind::NotFound) =>
{
let mut n = 0usize;
while layer_dir(dir, n).exists() {
n += 1;
}
let m = PartitionMeta {
n_layers: n,
mode: IndexMode::default(),
};
m.save(dir).map_err(|e| olm_to_sk(e, context))?;
Ok(m)
}
Err(e) => Err(olm_to_sk(e, context)),
}
}
// ── ColBuilder ──────────────────────────────────────────────────────────────── // ── ColBuilder ────────────────────────────────────────────────────────────────
pub(crate) enum ColBuilder { pub enum ColBuilder {
Bit(PersistentBitVecBuilder), Bit(PersistentBitVecBuilder),
Int(PersistentCompactIntVecBuilder), Int(PersistentCompactIntVecBuilder),
} }
impl ColBuilder { impl ColBuilder {
pub(crate) fn set_val(&mut self, slot: usize, value: u32) { pub fn set_val(&mut self, slot: usize, value: u32) {
match self { match self {
ColBuilder::Bit(b) => b.set(slot, value > 0), ColBuilder::Bit(b) => b.set(slot, value > 0),
ColBuilder::Int(b) => b.set(slot, value), ColBuilder::Int(b) => b.set(slot, value),
} }
} }
pub(crate) fn close(self) -> SKResult<()> { pub fn close(self) -> OKIResult<()> {
match self { match self {
ColBuilder::Bit(b) => b.close().map_err(SKError::Io), ColBuilder::Bit(b) => b.close().map_err(OKIError::Io),
ColBuilder::Int(b) => b.close().map_err(SKError::Io), ColBuilder::Int(b) => b.close().map_err(OKIError::Io),
} }
} }
} }
+24 -5
View File
@@ -8,6 +8,7 @@ pub enum OKIError {
Io(io::Error), Io(io::Error),
Json(serde_json::Error), Json(serde_json::Error),
Partition(SKError), Partition(SKError),
Layer(SKError),
/// Source index is not in `Indexed` state. /// Source index is not in `Indexed` state.
NotIndexed(std::path::PathBuf), NotIndexed(std::path::PathBuf),
/// Source indexes have incompatible configurations (k, m, n_bits). /// Source indexes have incompatible configurations (k, m, n_bits).
@@ -20,6 +21,9 @@ pub enum OKIError {
InvalidInput(String), InvalidInput(String),
/// Sources mix exact and approximate evidence, or use incompatible approx parameters. /// Sources mix exact and approximate evidence, or use incompatible approx parameters.
IncompatibleEvidence(String), IncompatibleEvidence(String),
/// Malformed data encountered while reading/writing a layer file, tagged
/// with the operation that hit it.
InvalidData { context: &'static str, detail: String },
} }
pub type OKIResult<T> = Result<T, OKIError>; pub type OKIResult<T> = Result<T, OKIError>;
@@ -30,12 +34,21 @@ impl fmt::Display for OKIError {
OKIError::Io(e) => write!(f, "I/O error: {e}"), OKIError::Io(e) => write!(f, "I/O error: {e}"),
OKIError::Json(e) => write!(f, "JSON error: {e}"), OKIError::Json(e) => write!(f, "JSON error: {e}"),
OKIError::Partition(e) => write!(f, "partition error: {e}"), OKIError::Partition(e) => write!(f, "partition error: {e}"),
OKIError::Layer(e) => write!(f, "layer error: {e}"),
OKIError::NotIndexed(p) => write!(f, "index not fully built: {}", p.display()), OKIError::NotIndexed(p) => write!(f, "index not fully built: {}", p.display()),
OKIError::IncompatibleConfig => write!(f, "incompatible index configurations"), OKIError::IncompatibleConfig => write!(f, "incompatible index configurations"),
OKIError::MismatchedMode => write!(f, "count mode requires all sources to have with_counts=true"), OKIError::MismatchedMode => write!(
OKIError::DuplicateGenomeLabel(l) => write!(f, "duplicate genome label across sources: {l}"), f,
"count mode requires all sources to have with_counts=true"
),
OKIError::DuplicateGenomeLabel(l) => {
write!(f, "duplicate genome label across sources: {l}")
}
OKIError::InvalidInput(m) => write!(f, "invalid input: {m}"), OKIError::InvalidInput(m) => write!(f, "invalid input: {m}"),
OKIError::IncompatibleEvidence(m) => write!(f, "incompatible evidence: {m}"), OKIError::IncompatibleEvidence(m) => write!(f, "incompatible evidence: {m}"),
OKIError::InvalidData { context, detail } => {
write!(f, "invalid data ({context}): {detail}")
}
} }
} }
} }
@@ -52,13 +65,19 @@ impl std::error::Error for OKIError {
} }
impl From<io::Error> for OKIError { impl From<io::Error> for OKIError {
fn from(e: io::Error) -> Self { OKIError::Io(e) } fn from(e: io::Error) -> Self {
OKIError::Io(e)
}
} }
impl From<serde_json::Error> for OKIError { impl From<serde_json::Error> for OKIError {
fn from(e: serde_json::Error) -> Self { OKIError::Json(e) } fn from(e: serde_json::Error) -> Self {
OKIError::Json(e)
}
} }
impl From<SKError> for OKIError { impl From<SKError> for OKIError {
fn from(e: SKError) -> Self { OKIError::Partition(e) } fn from(e: SKError) -> Self {
OKIError::Partition(e)
}
} }
+76 -43
View File
@@ -2,13 +2,13 @@ use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use crate::layer::meta::PartitionMeta; use crate::layer::KmerLayer;
use crate::partition::KmerPartition;
use obisys::progress_bar; use obisys::progress_bar;
use rayon::prelude::*; use rayon::prelude::*;
use obikseq::{set_k, set_m}; use obikseq::{set_k, set_m};
use crate::index::common::load_meta;
use crate::index::error::{OKIError, OKIResult}; use crate::index::error::{OKIError, OKIResult};
use crate::index::meta::{GenomeInfo, IndexConfig, IndexMeta}; use crate::index::meta::{GenomeInfo, IndexConfig, IndexMeta};
@@ -28,12 +28,15 @@ impl KmerIndex {
genome_info: Option<GenomeInfo>, genome_info: Option<GenomeInfo>,
) -> OKIResult<Self> { ) -> OKIResult<Self> {
let root_path = path.as_ref().to_owned(); let root_path = path.as_ref().to_owned();
fs::create_dir_all(&root_path).map_err(OKIError::Io)?; fs::create_dir(&root_path).map_err(OKIError::Io)?;
set_k(config.kmer_size); set_k(config.kmer_size);
set_m(config.minimizer_size); set_m(config.minimizer_size);
let genomes = genome_info.into_iter().collect(); let genomes = genome_info.into_iter().collect();
let meta = IndexMeta::create_at(&root_path, config, genomes).map_err(OKIError::Io)?; let meta = IndexMeta::create_at(&root_path, config, genomes).map_err(OKIError::Io)?;
Ok(Self { root_path, meta: Arc::new(meta) }) Ok(Self {
root_path,
meta: Arc::new(meta),
})
} }
pub fn open<P: AsRef<Path>>(path: P) -> OKIResult<Self> { pub fn open<P: AsRef<Path>>(path: P) -> OKIResult<Self> {
@@ -41,16 +44,30 @@ impl KmerIndex {
let meta = IndexMeta::open_at(&root_path).map_err(OKIError::Io)?; let meta = IndexMeta::open_at(&root_path).map_err(OKIError::Io)?;
set_k(meta.config.kmer_size); set_k(meta.config.kmer_size);
set_m(meta.config.minimizer_size); set_m(meta.config.minimizer_size);
Ok(Self { root_path, meta: Arc::new(meta) }) Ok(Self {
root_path,
meta: Arc::new(meta),
})
} }
/// Return `true` if `path` contains an `index.meta` file. /// Return `true` if `path` points to a valid obikmer index directory.
pub fn exists<P: AsRef<Path>>(path: P) -> bool { ///
/// A directory is considered an index only when it contains an `index.meta`
/// file. The absence of that file means the directory is not an index, even
/// if it otherwise exists.
///
/// This is a static method (associated function), not an instance method.
/// Call it as `KmerIndex::is_an_index(path)`.
///
/// `path` is generic over any type implementing [`AsRef<Path>`]. This allows
/// callers to pass `&str`, `String`, `PathBuf`, `&Path`, etc. without an
/// explicit conversion; the value is coerced via `.as_ref()` inside.
pub fn is_an_index<P: AsRef<Path>>(path: P) -> bool {
IndexMeta::exists(path.as_ref()) IndexMeta::exists(path.as_ref())
} }
/// The index's root directory. /// The index's root directory.
pub fn root_path(&self) -> &Path { pub fn dir(&self) -> &Path {
&self.root_path &self.root_path
} }
pub fn meta(&self) -> Arc<IndexMeta> { pub fn meta(&self) -> Arc<IndexMeta> {
@@ -102,42 +119,48 @@ impl KmerIndex {
/// same directory only indirectly, through this method — `obikindexer` /// same directory only indirectly, through this method — `obikindexer`
/// depends on `KmerIndex`, not the other way around — see /// depends on `KmerIndex`, not the other way around — see
/// `DevDocMD/implementation/partition_layer_cache.md`. /// `DevDocMD/implementation/partition_layer_cache.md`.
pub fn partition_dir(&self, i: usize) -> PathBuf { pub(crate) fn partition_dir(&self, i: usize) -> OKIResult<PathBuf> {
crate::partition::partition_dir(&self.root_path, i) Ok(self.partition(i)?.dir().to_path_buf())
}
pub fn partition(&self, i: usize) -> OKIResult<KmerPartition> {
let n = self.n_partitions();
if i >= n {
return Err(OKIError::InvalidData {
context: "New KmerPartition",
detail: format!("Partition {i} requested, Index has only {n} partitions"),
});
}
Ok(KmerPartition::new(self, i))
}
pub(crate) fn layer_dir(&self, i: usize, j: usize) -> OKIResult<PathBuf> {
Ok(self.layer(i, j)?.dir().to_path_buf())
}
pub fn layer(&self, i: usize, j: usize) -> OKIResult<KmerLayer> {
self.partition(i)?.layer(j)
} }
/// Path of partition `i`'s layered-index directory (`<partition>/index`). /// Path of partition `i`'s layered-index directory (`<partition>/index`).
pub fn index_dir(&self, i: usize) -> PathBuf { pub(crate) fn index_dir(&self, i: usize) -> PathBuf {
crate::partition::index_dir(&self.root_path, i) crate::partition::index_dir(&self.root_path, i)
} }
/// Path of layer `l` within partition `i`'s layered index. /// Number of layers in partition `i` — a disk scan, not a metadata read
pub fn layer_dir(&self, i: usize, l: usize) -> PathBuf { /// (see [`KmerPartition::n_layers`](crate::partition::KmerPartition::n_layers)).
crate::layer::layer_dir(&self.index_dir(i), l) pub fn n_layers(&self, i: usize) -> OKIResult<usize> {
Ok(self.partition(i)?.n_layers())
} }
/// Partition `i`'s metadata (layer count, evidence mode). Returns /// Evidence mode partition `i` was actually built with — detected from
/// `obiskio::SKResult`, not `OKIResult` — matches the error convention /// layer 0's evidence files on disk (see
/// of the partition/layer-construction code below (moved here from /// [`KmerPartition::mode`](crate::partition::KmerPartition::mode)).
/// the former `obikpartitionner` crate, which predates `OKIError`); /// Distinct from [`evidence_mode`](Self::evidence_mode): that one is the
/// `?` still converts it to `OKIResult` at any call site that needs /// index-level config, this one is per-partition ground truth (the two
/// one (`OKIError: From<SKError>`). /// agree in practice, but this is what `Layer::open` needs).
pub fn partition_meta(&self, i: usize) -> obiskio::SKResult<PartitionMeta> { pub fn partition_mode(&self, i: usize) -> OKIResult<crate::layer::IndexMode> {
load_meta(&self.index_dir(i), "partition_meta") self.partition(i)?.mode()
}
/// Number of layers in partition `i` — see [`partition_meta`](Self::partition_meta).
pub fn n_layers(&self, i: usize) -> obiskio::SKResult<usize> {
Ok(self.partition_meta(i)?.n_layers)
}
/// Evidence mode partition `i` was actually built with — see
/// [`partition_meta`](Self::partition_meta). Distinct from
/// [`evidence_mode`](Self::evidence_mode): that one is the index-level
/// config, this one is per-partition ground truth (the two agree in
/// practice, but this is what `Layer::open` needs).
pub fn partition_mode(&self, i: usize) -> obiskio::SKResult<crate::layer::IndexMode> {
Ok(self.partition_meta(i)?.mode)
} }
/// Number of layers per partition. /// Number of layers per partition.
@@ -150,8 +173,8 @@ impl KmerIndex {
} }
/// Path to the unitigs file for partition `part`, layer `layer`. /// Path to the unitigs file for partition `part`, layer `layer`.
pub fn layer_unitigs_path(&self, part: usize, layer: usize) -> PathBuf { pub fn layer_unitigs_path(&self, part: usize, layer: usize) -> OKIResult<PathBuf> {
self.layer_dir(part, layer).join("unitigs.bin") Ok(self.layer_dir(part, layer)?.join("unitigs.bin"))
} }
/// Pack all partition matrices into single-file format (presence → .pbmx, counts → .pcmx). /// Pack all partition matrices into single-file format (presence → .pbmx, counts → .pcmx).
@@ -164,13 +187,14 @@ impl KmerIndex {
/// on-disk format (see `DevDocMD/architecture/siblings.md`) — count /// on-disk format (see `DevDocMD/architecture/siblings.md`) — count
/// matrices are unaffected, sparse count matrices aren't implemented /// matrices are unaffected, sparse count matrices aren't implemented
/// (see the sparse-matrix design plan's "Explicitly deferred"). /// (see the sparse-matrix design plan's "Explicitly deferred").
pub fn pack_matrices(&self, sparse: bool) -> OKIResult<()> { /// TODO: To be moved somewhere else
pub(crate) fn pack_matrices(&self, sparse: bool) -> OKIResult<()> {
use obicompactvec::{pack_bit_matrix, pack_compact_int_matrix, pack_sparse_bit_matrix}; use obicompactvec::{pack_bit_matrix, pack_compact_int_matrix, pack_sparse_bit_matrix};
let n = self.n_partitions(); let n = self.n_partitions();
let order: Vec<usize> = (0..n).collect(); let order: Vec<usize> = (0..n).collect();
let pb = progress_bar("pack", n as u64, "partitions"); let pb = progress_bar("pack", n as u64, "partitions");
crate::index::numa::PartitionRunner::new().run( obisys::PartitionRunner::new().run(
&order, &order,
|i| -> OKIResult<()> { |i| -> OKIResult<()> {
let index_dir = self.index_dir(i); let index_dir = self.index_dir(i);
@@ -179,7 +203,7 @@ impl KmerIndex {
} }
let n_layers = self.n_layers(i)?; let n_layers = self.n_layers(i)?;
for l in 0..n_layers { for l in 0..n_layers {
let layer_dir = self.layer_dir(i, l); let layer_dir = self.layer_dir(i, l)?;
let presence_dir = layer_dir.join("presence"); let presence_dir = layer_dir.join("presence");
let counts_dir = layer_dir.join("counts"); let counts_dir = layer_dir.join("counts");
if presence_dir.exists() { if presence_dir.exists() {
@@ -207,7 +231,8 @@ impl KmerIndex {
/// ///
/// Old indexes were built before this file was required. The number of /// Old indexes were built before this file was required. The number of
/// kmers is recovered from `unitigs.bin`, which is always present. /// kmers is recovered from `unitigs.bin`, which is always present.
pub fn upgrade_layer_meta(&self) -> OKIResult<()> { /// TODO: Should not exist anymore
pub(crate) fn upgrade_layer_meta(&self) -> OKIResult<()> {
use obicompactvec::LayerMeta; use obicompactvec::LayerMeta;
use obiskio::UnitigFileReader; use obiskio::UnitigFileReader;
@@ -229,7 +254,15 @@ impl KmerIndex {
} }
}; };
for l in 0..n_layers { for l in 0..n_layers {
let layer_dir = self.layer_dir(i, l); let layer_dir = match self.layer_dir(i, l) {
Ok(d) => d,
Err(e) => {
return Some(OKIError::Io(std::io::Error::new(
std::io::ErrorKind::Other,
e.to_string(),
)));
}
};
let meta_path = layer_dir.join(LayerMeta::FILENAME); let meta_path = layer_dir.join(LayerMeta::FILENAME);
if meta_path.exists() { if meta_path.exists() {
continue; continue;
+71 -35
View File
@@ -21,8 +21,44 @@ pub struct GenomeInfo {
} }
impl GenomeInfo { impl GenomeInfo {
/// Panics if `label` fails [`validate_label`](Self::validate_label) —
/// an invalid genome label is a programmer error to surface loudly,
/// not a recoverable condition for this constructor to paper over.
/// Callers that need a graceful error (e.g. a CLI parsing user input)
/// should call [`validate_label`](Self::validate_label) themselves
/// first and report it their own way.
pub fn new(label: impl Into<String>) -> Self { pub fn new(label: impl Into<String>) -> Self {
Self { label: label.into(), meta: HashMap::new() } let label = label.into();
Self::validate_label(&label).expect("invalid genome label");
Self {
label,
meta: HashMap::new(),
}
}
/// Validate a user-supplied genome label.
///
/// Forbidden: `/` (filesystem separator), `=` (--new-label parser separator),
/// `\0` (null byte), `\n`, `\r`, `\t` (break CSV output).
/// Empty labels are also rejected.
pub fn validate_label(label: &str) -> Result<(), String> {
if label.is_empty() {
return Err("genome label must not be empty".into());
}
const FORBIDDEN: &[char] = &['/', '=', '\0', '\n', '\r', '\t'];
if let Some(c) = label.chars().find(|c| FORBIDDEN.contains(c)) {
let display = match c {
'\0' => "\\0 (null)".to_string(),
'\n' => "\\n (newline)".to_string(),
'\r' => "\\r (carriage return)".to_string(),
'\t' => "\\t (tab)".to_string(),
c => format!("'{c}'"),
};
return Err(format!(
"genome label contains forbidden character {display}"
));
}
Ok(())
} }
} }
@@ -47,7 +83,7 @@ pub struct IndexConfig {
/// On-disk shape of `index.meta` — the whole file, read/written atomically /// On-disk shape of `index.meta` — the whole file, read/written atomically
/// as one JSON blob by every `IndexMeta` operation below. /// as one JSON blob by every `IndexMeta` operation below.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
struct OnDiskMeta { struct IndexMetadata {
version: u32, version: u32,
config: IndexConfig, config: IndexConfig,
#[serde(default)] #[serde(default)]
@@ -81,21 +117,29 @@ impl IndexMeta {
/// Create a brand-new `index.meta` for `index`, from fixed config and /// Create a brand-new `index.meta` for `index`, from fixed config and
/// an initial genome list (often empty — genomes are usually added /// an initial genome list (often empty — genomes are usually added
/// later via [`push_genome`](Self::push_genome)). /// later via [`push_genome`](Self::push_genome)).
pub fn create(index: &KmerIndex, config: IndexConfig, genomes: Vec<GenomeInfo>) -> io::Result<Self> { pub fn create(
Self::create_at(index.root_path(), config, genomes) index: &KmerIndex,
config: IndexConfig,
genomes: Vec<GenomeInfo>,
) -> io::Result<Self> {
Self::create_at(index.dir(), config, genomes)
} }
/// Same as [`create`](Self::create), taking a raw path — for /// Same as [`create`](Self::create), taking a raw path — for
/// `KmerIndex::create` itself, which doesn't have a complete /// `KmerIndex::create` itself, which doesn't have a complete
/// `KmerIndex` yet to hand in (it's what's being built). /// `KmerIndex` yet to hand in (it's what's being built).
pub(crate) fn create_at(root_path: &Path, config: IndexConfig, genomes: Vec<GenomeInfo>) -> io::Result<Self> { pub(crate) fn create_at(
root_path: &Path,
config: IndexConfig,
genomes: Vec<GenomeInfo>,
) -> io::Result<Self> {
let meta = Self { let meta = Self {
root_path: root_path.to_owned(), root_path: root_path.to_owned(),
version: META_VERSION, version: META_VERSION,
config, config,
lock: RwLock::new(()), lock: RwLock::new(()),
}; };
meta.write_full(&OnDiskMeta { meta.write_full(&IndexMetadata {
version: meta.version, version: meta.version,
config: meta.config.clone(), config: meta.config.clone(),
genomes, genomes,
@@ -106,7 +150,7 @@ impl IndexMeta {
/// Reopen the `index.meta` already on disk for `index`. /// Reopen the `index.meta` already on disk for `index`.
pub fn open(index: &KmerIndex) -> io::Result<Self> { pub fn open(index: &KmerIndex) -> io::Result<Self> {
Self::open_at(index.root_path()) Self::open_at(index.dir())
} }
pub(crate) fn open_at(root_path: &Path) -> io::Result<Self> { pub(crate) fn open_at(root_path: &Path) -> io::Result<Self> {
@@ -119,7 +163,12 @@ impl IndexMeta {
}) })
} }
pub fn exists(root: &Path) -> bool { /// Returns whether the metadata file exists in `root`.
///
/// This is a static method (associated function), not an instance method.
/// It only checks for the presence of the `index.meta` file inside the
/// given directory; it does not validate its contents.
pub(crate) fn exists(root: &Path) -> bool {
root.join(META_FILENAME).exists() root.join(META_FILENAME).exists()
} }
@@ -177,10 +226,20 @@ impl IndexMeta {
/// (e.g. `self.meta = Arc::new(IndexMeta::open(self)?)`) — this method /// (e.g. `self.meta = Arc::new(IndexMeta::open(self)?)`) — this method
/// only updates the file, it has no way to reach back into whatever /// only updates the file, it has no way to reach back into whatever
/// `KmerIndex` holds it. /// `KmerIndex` holds it.
pub fn rewrite_config(&self, config: IndexConfig, genomes: Vec<GenomeInfo>) -> io::Result<()> { /// TODO: This methode is strange
pub(crate) fn rewrite_config(
&self,
config: IndexConfig,
genomes: Vec<GenomeInfo>,
) -> io::Result<()> {
let _guard = self.lock.write().unwrap(); let _guard = self.lock.write().unwrap();
let state = Self::read_full(&self.root_path)?.state; let state = Self::read_full(&self.root_path)?.state;
self.write_full(&OnDiskMeta { version: self.version, config, genomes, state }) self.write_full(&IndexMetadata {
version: self.version,
config,
genomes,
state,
})
} }
pub fn set_state(&self, state: IndexState) -> io::Result<()> { pub fn set_state(&self, state: IndexState) -> io::Result<()> {
@@ -221,12 +280,12 @@ impl IndexMeta {
// ── private ────────────────────────────────────────────────────────────── // ── private ──────────────────────────────────────────────────────────────
fn read_full(root: &Path) -> io::Result<OnDiskMeta> { fn read_full(root: &Path) -> io::Result<IndexMetadata> {
let file = fs::File::open(root.join(META_FILENAME))?; let file = fs::File::open(root.join(META_FILENAME))?;
serde_json::from_reader(file).map_err(io::Error::other) serde_json::from_reader(file).map_err(io::Error::other)
} }
fn write_full(&self, on_disk: &OnDiskMeta) -> io::Result<()> { fn write_full(&self, on_disk: &IndexMetadata) -> io::Result<()> {
let file = fs::File::create(self.root_path.join(META_FILENAME))?; let file = fs::File::create(self.root_path.join(META_FILENAME))?;
serde_json::to_writer_pretty(file, on_disk).map_err(io::Error::other) serde_json::to_writer_pretty(file, on_disk).map_err(io::Error::other)
} }
@@ -248,26 +307,3 @@ fn label_from_path(path: &Path) -> String {
s s
} }
} }
/// Validate a user-supplied genome label.
///
/// Forbidden: `/` (filesystem separator), `=` (--new-label parser separator),
/// `\0` (null byte), `\n`, `\r`, `\t` (break CSV output).
/// Empty labels are also rejected.
pub fn validate_label(label: &str) -> Result<(), String> {
if label.is_empty() {
return Err("genome label must not be empty".into());
}
const FORBIDDEN: &[char] = &['/', '=', '\0', '\n', '\r', '\t'];
if let Some(c) = label.chars().find(|c| FORBIDDEN.contains(c)) {
let display = match c {
'\0' => "\\0 (null)".to_string(),
'\n' => "\\n (newline)".to_string(),
'\r' => "\\r (carriage return)".to_string(),
'\t' => "\\t (tab)".to_string(),
c => format!("'{c}'"),
};
return Err(format!("genome label contains forbidden character {display}"));
}
Ok(())
}
+2 -28
View File
@@ -1,39 +1,13 @@
pub mod error; pub mod error;
pub mod meta; pub mod meta;
pub mod predicate;
pub mod state; pub mod state;
mod builder; mod builder;
mod common; mod common;
mod distance;
mod dump;
mod dump_layer;
pub mod filter;
mod graph_pipeline;
mod kmer_index; mod kmer_index;
mod matrix_store;
mod merge;
mod merge_layer;
mod numa;
mod query_layer;
mod rebuild;
mod rebuild_layer;
mod reindex;
mod select;
mod select_layer;
mod stats;
pub use error::{OKIError, OKIResult}; pub use error::{OKIError, OKIResult};
pub use builder::IndexBuilder; pub use builder::IndexBuilder;
pub use common::olm_to_sk; pub use common::ColBuilder;
pub use graph_pipeline::{materialize_layer, write_graph_as_unitigs};
pub use distance::{DistanceMetric, DistanceOutput};
pub use filter::{GroupQuorumFilter, KmerFilter, passes_all};
pub use kmer_index::KmerIndex; pub use kmer_index::KmerIndex;
pub use merge_layer::MergeMode; pub use meta::{GenomeInfo, IndexConfig, IndexMeta, META_FILENAME};
pub use meta::{validate_label, GenomeInfo, IndexConfig, IndexMeta, META_FILENAME};
pub use predicate::{GroupFilterParams, MetaPred};
pub use query_layer::{KmerDesc, QueryHit, QueryStats};
pub use select_layer::{AggOp, OutputCol};
pub use state::IndexState; pub use state::IndexState;
pub use stats::IndexBitsPerKmer;
pub use numa::PartitionRunner;
-21
View File
@@ -1,21 +0,0 @@
use std::sync::OnceLock;
use indicatif::MultiProgress;
static MULTI: OnceLock<MultiProgress> = OnceLock::new();
/// Initialise the shared progress display. Call once from the binary before
/// any index operation. Subsequent calls are silently ignored.
pub fn init(multi: MultiProgress) {
let _ = MULTI.set(multi);
}
/// Return the shared `MultiProgress`, creating a plain default one if the
/// binary never called [`init`].
pub fn get() -> &'static MultiProgress {
MULTI.get_or_init(MultiProgress::new)
}
pub(crate) fn multi() -> &'static MultiProgress {
get()
}
+108 -61
View File
@@ -24,77 +24,103 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use crate::layer::utils::LAYERNAME_SUFFIX;
use obicompactvec::{PersistentBitMatrix, PersistentCompactIntMatrix}; use obicompactvec::{PersistentBitMatrix, PersistentCompactIntMatrix};
use obikseq::CanonicalKmer; use obikseq::CanonicalKmer;
use crate::layer::error::OLMResult; use crate::index::error::OKIResult;
use crate::layer::meta::IndexMode;
use crate::layer::mphf_layer::{ use crate::layer::mphf_layer::{
EVIDENCE_FILE, EvidenceKind, FINGERPRINT_FILE, MPHF_FILE, UNITIGS_FILE, EVIDENCE_FILE, EvidenceKind, FINGERPRINT_FILE, MPHF_FILE, UNITIGS_FILE,
}; };
use crate::layer::typed_layer::{COUNTS_DIR, LayerContent, PRESENCE_DIR, TypedLayer}; use crate::layer::typed_layer::{COUNTS_DIR, LayerContent, PRESENCE_DIR, TypedLayer};
use crate::partition::KmerPartition;
/// Superkmer-file extension used by every pre-layer-construction artifact
/// below (`raw`/`dereplicated`) — an implementation detail of the SK file
/// format, never meant to leak as a literal string past this module.
const SK_EXT: &str = "skmer.zst";
/// One layer, at any point in its life — see the module docs. Only /// One layer, at any point in its life — see the module docs. Only
/// [`Empty`](Layer::Empty) and the two ready-to-read states /// [`Empty`](Layer::Empty) and the two ready-to-read states
/// (`Count`/`Presence`) exist so far; states in between (unitigs written, /// (`Count`/`Presence`) exist so far; states in between (unitigs written,
/// MPHF built, evidence built, matrix not yet built) are not represented /// MPHF built, evidence built, matrix not yet built) are not represented
/// yet. /// yet.
///
/// Every variant carries `dir` + `id` — a layer's identity never depends on
/// its state. `id` is its own layer number within the partition (not yet
/// consumed by anything — `KmerPartition` doesn't hand it in yet, that
/// lands with `KmerLayer::at`). `dir`/`id` live here, on `KmerLayer` itself,
/// not inside `TypedLayer`/`MphfLayer`: those only ever need `dir`
/// transiently, while opening — `KmerLayer` is what has to answer `.dir()`
/// after the fact, so it's the one that keeps it.
pub enum KmerLayer { pub enum KmerLayer {
/// Just a directory — nothing built yet. Every method below other than /// Just a directory — nothing built yet. Every method below other than
/// the path accessors panics on this variant: calling them means the /// the path accessors panics on this variant: calling them means the
/// caller assumed a layer was ready when it wasn't, an implementation /// caller assumed a layer was ready when it wasn't, an implementation
/// error to surface loudly, not paper over with a default value. /// error to surface loudly, not paper over with a default value.
Empty { Empty { dir: PathBuf, id: usize },
Count {
dir: PathBuf, dir: PathBuf,
id: usize,
layer: TypedLayer<PersistentCompactIntMatrix>,
},
Presence {
dir: PathBuf,
id: usize,
layer: TypedLayer<PersistentBitMatrix>,
}, },
Count(TypedLayer<PersistentCompactIntMatrix>),
Presence(TypedLayer<PersistentBitMatrix>),
} }
impl KmerLayer { impl KmerLayer {
pub fn new(partition: &KmerPartition, i: usize) -> Self {
let dir = partition.dir().join(format!("{LAYERNAME_SUFFIX}{i}"));
KmerLayer::Empty { dir, id: i }
}
/// An empty shell at `dir` — creates the directory (if it doesn't /// An empty shell at `dir` — creates the directory (if it doesn't
/// already exist) but nothing inside it. The starting state for /// already exist) but nothing inside it. The starting state for
/// building a new layer. /// building a new layer.
pub fn create(dir: &Path) -> std::io::Result<Self> { pub fn create(self) -> std::io::Result<Self> {
std::fs::create_dir_all(dir)?; std::fs::create_dir_all(self.dir())?;
Ok(KmerLayer::Empty { Ok(self)
dir: dir.to_owned(),
})
} }
/// Open one layer, auto-detecting count vs. presence from what is /// Open this layer, auto-detecting count vs. presence from what is
/// actually on disk (`counts/` present and wanted, else presence) — the /// actually on disk (`counts/` present, else presence) — the single
/// single source of truth every caller that opens a layer's own matrix /// source of truth every caller that opens a layer's own matrix should
/// should share, so two callers can never disagree about which format a /// share, so two callers can never disagree about which format a given
/// given layer is. Dense vs. sparse presence storage is /// layer is. Dense vs. sparse presence storage is `PersistentBitMatrix::
/// `PersistentBitMatrix::open`'s own concern (it's a 4-way /// open`'s own concern (a 4-way `Columnar`/`Packed`/`Sparse`/`Implicit`
/// `Columnar`/`Packed`/`Sparse`/`Implicit` enum internally), not decided /// enum internally), not decided here. A no-op (returns `self`
/// here. /// unchanged) if already `Count`/`Presence`.
pub fn open(layer_dir: &Path, mode: &IndexMode, with_counts: bool) -> OLMResult<Self> { pub fn open(self) -> OKIResult<Self> {
if with_counts && layer_dir.join(COUNTS_DIR).exists() { let (dir, id) = match self {
return TypedLayer::<PersistentCompactIntMatrix>::open(layer_dir, mode) KmerLayer::Empty { dir, id } => (dir, id),
.map(KmerLayer::Count); already_open => return Ok(already_open),
};
if dir.join(COUNTS_DIR).exists() {
let layer = TypedLayer::<PersistentCompactIntMatrix>::open(&dir)?;
Ok(KmerLayer::Count { dir, id, layer })
} else {
let layer = TypedLayer::<PersistentBitMatrix>::open(&dir)?;
Ok(KmerLayer::Presence { dir, id, layer })
} }
TypedLayer::<PersistentBitMatrix>::open(layer_dir, mode).map(KmerLayer::Presence)
} }
// ── Paths — only meaningful before anything is built ─────────────── // ── Paths — valid in every state, `dir` never varies with content ────
// //
// Once a layer is `Count`/`Presence`, its caller already knows the // The `mphf.bin`/`unitigs.bin`/… naming stays defined once, here,
// directory (it had to pass it to `open`) — these exist for builder // rather than re-declared as string literals at every call site that
// code holding an `Empty` layer, so the `mphf.bin`/`unitigs.bin`/ // writes into a layer directory (the same duplication `layer_dir`/
// naming stays defined once, here, rather than re-declared as // `index_dir` fixed one level up — see `DevDocMD/implementation/
// string literals at every call site that writes into a layer // partition_layer_cache.md`).
// directory (the same duplication `layer_dir`/`index_dir` fixed one
// level up — see `DevDocMD/implementation/partition_layer_cache.md`).
/// This layer's own directory. Panics on `Count`/`Presence` — by that /// This layer's own directory.
/// point the caller already has the directory it opened with; asking
/// again here would mean it lost track of its own state.
pub fn dir(&self) -> &Path { pub fn dir(&self) -> &Path {
match self { match self {
KmerLayer::Empty { dir } => dir, KmerLayer::Empty { dir, .. } => dir,
_ => panic!("Layer::dir() only available on Empty — caller already has this path"), KmerLayer::Count { dir, .. } => dir,
KmerLayer::Presence { dir, .. } => dir,
} }
} }
@@ -117,36 +143,57 @@ impl KmerLayer {
self.dir().join(PRESENCE_DIR) self.dir().join(PRESENCE_DIR)
} }
/// Path of this layer's raw, not-yet-dereplicated superkmer file —
/// written by whichever algorithm routes superkmers into this layer
/// (today: `obikindexer::algorithms::partitionner::PartitionRouter`),
/// read by whichever algorithm dereplicates it (today:
/// `obikindexer::algorithms::dereplicator::Dereplicator`). Naming this
/// once here, rather than in either algorithm submodule, is what lets
/// the two agree on the filename without depending on each other
/// directly — see `DevDocMD/implementation/partition_layer_cache.md`.
pub fn raw_superkmers_path(&self) -> PathBuf {
self.dir().join(format!("raw.{SK_EXT}"))
}
/// Path of this layer's dereplicated superkmer file — written by
/// `obikindexer::algorithms::dereplicator::Dereplicator`, read by
/// whichever algorithm counts kmer abundances from it (today:
/// `obikindexer::algorithms::partitionner::PartitionRouter::count_kmer`)
/// and, later, by `obikindex::build_index_layer` to build the real layer.
pub fn dereplicated_superkmers_path(&self) -> PathBuf {
self.dir().join(format!("dereplicated.{SK_EXT}"))
}
// ── Ready-only surface ─────────────────────────────────────────────── // ── Ready-only surface ───────────────────────────────────────────────
pub fn content(&self) -> LayerContent { pub fn content(&self) -> LayerContent {
match self { match self {
KmerLayer::Count(_) => LayerContent::Count, KmerLayer::Count { .. } => LayerContent::Count,
KmerLayer::Presence(_) => LayerContent::Presence, KmerLayer::Presence { .. } => LayerContent::Presence,
KmerLayer::Empty { .. } => panic!("Layer::content() called on an Empty layer"), KmerLayer::Empty { .. } => panic!("Layer::content() called on an Empty layer"),
} }
} }
pub fn evidence_kind(&self) -> EvidenceKind { pub fn evidence_kind(&self) -> EvidenceKind {
match self { match self {
KmerLayer::Count(l) => l.evidence_kind(), KmerLayer::Count { layer, .. } => layer.evidence_kind(),
KmerLayer::Presence(l) => l.evidence_kind(), KmerLayer::Presence { layer, .. } => layer.evidence_kind(),
KmerLayer::Empty { .. } => panic!("Layer::evidence_kind() called on an Empty layer"), KmerLayer::Empty { .. } => panic!("Layer::evidence_kind() called on an Empty layer"),
} }
} }
pub fn n(&self) -> usize { pub fn n(&self) -> usize {
match self { match self {
KmerLayer::Count(l) => l.n(), KmerLayer::Count { layer, .. } => layer.n(),
KmerLayer::Presence(l) => l.n(), KmerLayer::Presence { layer, .. } => layer.n(),
KmerLayer::Empty { .. } => panic!("Layer::n() called on an Empty layer"), KmerLayer::Empty { .. } => panic!("Layer::n() called on an Empty layer"),
} }
} }
pub fn find_slot(&self, kmer: CanonicalKmer) -> Option<usize> { pub fn find_slot(&self, kmer: CanonicalKmer) -> Option<usize> {
match self { match self {
KmerLayer::Count(l) => l.find_slot(kmer), KmerLayer::Count { layer, .. } => layer.find_slot(kmer),
KmerLayer::Presence(l) => l.find_slot(kmer), KmerLayer::Presence { layer, .. } => layer.find_slot(kmer),
KmerLayer::Empty { .. } => panic!("Layer::find_slot() called on an Empty layer"), KmerLayer::Empty { .. } => panic!("Layer::find_slot() called on an Empty layer"),
} }
} }
@@ -156,16 +203,16 @@ impl KmerLayer {
/// the evidence check `find_slot`/`find` would perform is redundant. /// the evidence check `find_slot`/`find` would perform is redundant.
pub fn hash_batch(&self, kmers: &[CanonicalKmer]) -> Vec<usize> { pub fn hash_batch(&self, kmers: &[CanonicalKmer]) -> Vec<usize> {
match self { match self {
KmerLayer::Count(l) => l.hash_batch(kmers), KmerLayer::Count { layer, .. } => layer.hash_batch(kmers),
KmerLayer::Presence(l) => l.hash_batch(kmers), KmerLayer::Presence { layer, .. } => layer.hash_batch(kmers),
KmerLayer::Empty { .. } => panic!("Layer::hash_batch() called on an Empty layer"), KmerLayer::Empty { .. } => panic!("Layer::hash_batch() called on an Empty layer"),
} }
} }
pub fn n_cols(&self) -> usize { pub fn n_cols(&self) -> usize {
match self { match self {
KmerLayer::Count(l) => l.n_cols(), KmerLayer::Count { layer, .. } => layer.n_cols(),
KmerLayer::Presence(l) => l.n_cols(), KmerLayer::Presence { layer, .. } => layer.n_cols(),
KmerLayer::Empty { .. } => panic!("Layer::n_cols() called on an Empty layer"), KmerLayer::Empty { .. } => panic!("Layer::n_cols() called on an Empty layer"),
} }
} }
@@ -178,10 +225,10 @@ impl KmerLayer {
/// presence (`!= 0`) in place. /// presence (`!= 0`) in place.
pub fn fill_sub_matrix_carries(&self, slots: &[usize], out: &mut [Vec<bool>]) { pub fn fill_sub_matrix_carries(&self, slots: &[usize], out: &mut [Vec<bool>]) {
match self { match self {
KmerLayer::Presence(l) => l.fill_sub_matrix(slots, out), KmerLayer::Presence { layer, .. } => layer.fill_sub_matrix(slots, out),
KmerLayer::Count(l) => { KmerLayer::Count { layer, .. } => {
let mut counts: Vec<Vec<u32>> = out.iter().map(|_| Vec::new()).collect(); let mut counts: Vec<Vec<u32>> = out.iter().map(|_| Vec::new()).collect();
l.fill_sub_matrix(slots, &mut counts); layer.fill_sub_matrix(slots, &mut counts);
for (o, c) in out.iter_mut().zip(counts.iter()) { for (o, c) in out.iter_mut().zip(counts.iter()) {
o.clear(); o.clear();
o.extend(c.iter().map(|&v| v != 0)); o.extend(c.iter().map(|&v| v != 0));
@@ -196,8 +243,8 @@ impl KmerLayer {
/// Raw MPHF lookup: kmer → slot, no membership check. /// Raw MPHF lookup: kmer → slot, no membership check.
pub fn hash(&self, kmer: CanonicalKmer) -> usize { pub fn hash(&self, kmer: CanonicalKmer) -> usize {
match self { match self {
KmerLayer::Count(l) => l.hash(kmer), KmerLayer::Count { layer, .. } => layer.hash(kmer),
KmerLayer::Presence(l) => l.hash(kmer), KmerLayer::Presence { layer, .. } => layer.hash(kmer),
KmerLayer::Empty { .. } => panic!("Layer::hash() called on an Empty layer"), KmerLayer::Empty { .. } => panic!("Layer::hash() called on an Empty layer"),
} }
} }
@@ -205,8 +252,8 @@ impl KmerLayer {
/// Iterate over all canonical kmers in the layer, in deterministic order. /// Iterate over all canonical kmers in the layer, in deterministic order.
pub fn iter_kmers(&self) -> crate::layer::mphf_layer::KmerIter { pub fn iter_kmers(&self) -> crate::layer::mphf_layer::KmerIter {
match self { match self {
KmerLayer::Count(l) => l.iter_kmers(), KmerLayer::Count { layer, .. } => layer.iter_kmers(),
KmerLayer::Presence(l) => l.iter_kmers(), KmerLayer::Presence { layer, .. } => layer.iter_kmers(),
KmerLayer::Empty { .. } => panic!("Layer::iter_kmers() called on an Empty layer"), KmerLayer::Empty { .. } => panic!("Layer::iter_kmers() called on an Empty layer"),
} }
} }
@@ -215,8 +262,8 @@ impl KmerLayer {
/// sequence index in `unitigs.bin`. /// sequence index in `unitigs.bin`.
pub fn enumerate_kmers(&self) -> std::iter::Enumerate<crate::layer::mphf_layer::KmerIter> { pub fn enumerate_kmers(&self) -> std::iter::Enumerate<crate::layer::mphf_layer::KmerIter> {
match self { match self {
KmerLayer::Count(l) => l.enumerate_kmers(), KmerLayer::Count { layer, .. } => layer.enumerate_kmers(),
KmerLayer::Presence(l) => l.enumerate_kmers(), KmerLayer::Presence { layer, .. } => layer.enumerate_kmers(),
KmerLayer::Empty { .. } => panic!("Layer::enumerate_kmers() called on an Empty layer"), KmerLayer::Empty { .. } => panic!("Layer::enumerate_kmers() called on an Empty layer"),
} }
} }
@@ -224,8 +271,8 @@ impl KmerLayer {
/// Iterate over the layer's canonical kmers in batches of `n`. /// Iterate over the layer's canonical kmers in batches of `n`.
pub fn iter_kmers_batch(&self, n: usize) -> crate::layer::mphf_layer::KmerBatchIter { pub fn iter_kmers_batch(&self, n: usize) -> crate::layer::mphf_layer::KmerBatchIter {
match self { match self {
KmerLayer::Count(l) => l.iter_kmers_batch(n), KmerLayer::Count { layer, .. } => layer.iter_kmers_batch(n),
KmerLayer::Presence(l) => l.iter_kmers_batch(n), KmerLayer::Presence { layer, .. } => layer.iter_kmers_batch(n),
KmerLayer::Empty { .. } => panic!("Layer::iter_kmers_batch() called on an Empty layer"), KmerLayer::Empty { .. } => panic!("Layer::iter_kmers_batch() called on an Empty layer"),
} }
} }
@@ -237,8 +284,8 @@ impl KmerLayer {
n: usize, n: usize,
) -> Box<dyn Iterator<Item = (usize, Vec<CanonicalKmer>)> + Send + 'static> { ) -> Box<dyn Iterator<Item = (usize, Vec<CanonicalKmer>)> + Send + 'static> {
match self { match self {
KmerLayer::Count(l) => Box::new(l.enumerate_kmers_batch(n)), KmerLayer::Count { layer, .. } => Box::new(layer.enumerate_kmers_batch(n)),
KmerLayer::Presence(l) => Box::new(l.enumerate_kmers_batch(n)), KmerLayer::Presence { layer, .. } => Box::new(layer.enumerate_kmers_batch(n)),
KmerLayer::Empty { .. } => { KmerLayer::Empty { .. } => {
panic!("Layer::enumerate_kmers_batch() called on an Empty layer") panic!("Layer::enumerate_kmers_batch() called on an Empty layer")
} }
-50
View File
@@ -1,50 +0,0 @@
use std::fmt;
use std::io;
#[derive(Debug)]
pub enum OLMError {
Io(io::Error),
Json(serde_json::Error),
Mphf(String),
InvalidLayer(String),
}
pub type OLMResult<T> = Result<T, OLMError>;
impl fmt::Display for OLMError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
OLMError::Io(e) => write!(f, "I/O error: {e}"),
OLMError::Json(e) => write!(f, "JSON error: {e}"),
OLMError::Mphf(s) => write!(f, "MPHF error: {s}"),
OLMError::InvalidLayer(s) => write!(f, "invalid layer: {s}"),
}
}
}
impl std::error::Error for OLMError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
OLMError::Io(e) => Some(e),
OLMError::Json(e) => Some(e),
_ => None,
}
}
}
impl From<io::Error> for OLMError {
fn from(e: io::Error) -> Self { OLMError::Io(e) }
}
impl From<serde_json::Error> for OLMError {
fn from(e: serde_json::Error) -> Self { OLMError::Json(e) }
}
impl From<obiskio::SKError> for OLMError {
fn from(e: obiskio::SKError) -> Self {
match e {
obiskio::SKError::Io(io_err) => OLMError::Io(io_err),
other => OLMError::InvalidLayer(other.to_string()),
}
}
}
+10 -10
View File
@@ -6,14 +6,14 @@ use std::path::Path;
use memmap2::Mmap; use memmap2::Mmap;
use crate::layer::error::{OLMError, OLMResult}; use crate::index::error::{OKIError, OKIResult};
pub struct Evidence { pub struct Evidence {
mmap: Mmap, mmap: Mmap,
} }
impl Evidence { impl Evidence {
pub fn open(path: &Path) -> OLMResult<Self> { pub fn open(path: &Path) -> OKIResult<Self> {
let f = File::open(path)?; let f = File::open(path)?;
let mmap = unsafe { Mmap::map(&f)? }; let mmap = unsafe { Mmap::map(&f)? };
Ok(Self { mmap }) Ok(Self { mmap })
@@ -26,16 +26,16 @@ impl Evidence {
(raw >> 7, (raw & 0x7F) as u8) (raw >> 7, (raw & 0x7F) as u8)
} }
#[inline]
pub fn encode(chunk_id: u32, rank: u8) -> u32 {
(chunk_id << 7) | (rank as u32 & 0x7F)
}
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.mmap.len() / 4 self.mmap.len() / 4
} }
} }
#[inline]
pub fn encode(chunk_id: u32, rank: u8) -> u32 {
(chunk_id << 7) | (rank as u32 & 0x7F)
}
pub struct EvidenceWriter { pub struct EvidenceWriter {
buf: Vec<u32>, buf: Vec<u32>,
} }
@@ -47,13 +47,13 @@ impl EvidenceWriter {
#[inline] #[inline]
pub fn set(&mut self, slot: usize, chunk_id: u32, rank: u8) { pub fn set(&mut self, slot: usize, chunk_id: u32, rank: u8) {
self.buf[slot] = encode(chunk_id, rank); self.buf[slot] = Evidence::encode(chunk_id, rank);
} }
pub fn write(self, path: &Path) -> OLMResult<()> { pub fn write(self, path: &Path) -> OKIResult<()> {
let mut f = BufWriter::new(File::create(path)?); let mut f = BufWriter::new(File::create(path)?);
for v in self.buf { for v in self.buf {
f.write_all(&v.to_le_bytes()).map_err(OLMError::Io)?; f.write_all(&v.to_le_bytes()).map_err(OKIError::Io)?;
} }
Ok(()) Ok(())
} }
+10 -10
View File
@@ -14,7 +14,7 @@ use std::path::Path;
use bitvec::prelude::*; use bitvec::prelude::*;
use memmap2::Mmap; use memmap2::Mmap;
use crate::layer::error::{OLMError, OLMResult}; use crate::index::error::{OKIError, OKIResult};
const MAGIC: &[u8; 4] = b"FPVF"; const MAGIC: &[u8; 4] = b"FPVF";
const HEADER: usize = 16; const HEADER: usize = 16;
@@ -30,15 +30,15 @@ pub struct FingerprintVec {
} }
impl FingerprintVec { impl FingerprintVec {
pub fn open(path: &Path) -> OLMResult<Self> { pub fn open(path: &Path) -> OKIResult<Self> {
let f = File::open(path)?; let f = File::open(path)?;
let mmap = unsafe { Mmap::map(&f)? }; let mmap = unsafe { Mmap::map(&f)? };
if mmap.len() < HEADER || &mmap[..4] != MAGIC { if mmap.len() < HEADER || &mmap[..4] != MAGIC {
return Err(OLMError::InvalidLayer("bad fingerprint magic".into())); return Err(OKIError::InvalidData { context: "layer", detail: "bad fingerprint magic".into() });
} }
let b = mmap[4]; let b = mmap[4];
if b == 0 || b > 64 { if b == 0 || b > 64 {
return Err(OLMError::InvalidLayer("invalid fingerprint width".into())); return Err(OKIError::InvalidData { context: "layer", detail: "invalid fingerprint width".into() });
} }
let n = u64::from_le_bytes(mmap[8..16].try_into().unwrap()) as usize; let n = u64::from_le_bytes(mmap[8..16].try_into().unwrap()) as usize;
let mask: u64 = if b == 64 { u64::MAX } else { (1u64 << b) - 1 }; let mask: u64 = if b == 64 { u64::MAX } else { (1u64 << b) - 1 };
@@ -89,12 +89,12 @@ impl FingerprintVecWriter {
bits[lo .. lo + self.b as usize].store_le(fingerprint); bits[lo .. lo + self.b as usize].store_le(fingerprint);
} }
pub fn write(self, path: &Path) -> OLMResult<()> { pub fn write(self, path: &Path) -> OKIResult<()> {
let mut f = BufWriter::new(File::create(path).map_err(OLMError::Io)?); let mut f = BufWriter::new(File::create(path).map_err(OKIError::Io)?);
f.write_all(MAGIC).map_err(OLMError::Io)?; f.write_all(MAGIC).map_err(OKIError::Io)?;
f.write_all(&[self.b, 0, 0, 0]).map_err(OLMError::Io)?; f.write_all(&[self.b, 0, 0, 0]).map_err(OKIError::Io)?;
f.write_all(&(self.n as u64).to_le_bytes()).map_err(OLMError::Io)?; f.write_all(&(self.n as u64).to_le_bytes()).map_err(OKIError::Io)?;
f.write_all(&self.buf).map_err(OLMError::Io)?; f.write_all(&self.buf).map_err(OKIError::Io)?;
Ok(()) Ok(())
} }
} }
-128
View File
@@ -1,128 +0,0 @@
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use obicompactvec::{PersistentBitMatrix, PersistentCompactIntMatrix, StorageKind};
use obikseq::CanonicalKmer;
use obiskio::{UnitigFileWriter, DEFAULT_BLOCK_BITS};
use crate::layer::error::{OLMError, OLMResult};
use crate::layer::typed_layer::{layer_dir, Hit, TypedLayer, LayerContent, LayerData};
use crate::layer::meta::{IndexMode, PartitionMeta};
use crate::layer::mphf_layer::EvidenceKind;
/// Layered kmer index for a single partition.
///
/// Each layer covers a disjoint kmer set. Queries probe layers in order;
/// the first match wins. Adding a dataset appends a new layer without
/// rebuilding existing ones.
pub struct LayeredMap<D: LayerData = ()> {
root: PathBuf,
meta: PartitionMeta,
layers: Vec<TypedLayer<D>>,
}
// ── Common methods ────────────────────────────────────────────────────────────
impl<D: LayerData> LayeredMap<D> {
/// Open an existing layered index at `root`.
/// The mode is read once from `PartitionMeta` and applied to all layers.
pub fn open(root: &Path) -> OLMResult<Self> {
let meta = PartitionMeta::load(root)?;
let layers = (0..meta.n_layers)
.map(|i| TypedLayer::<D>::open(&layer_dir(root, i), &meta.mode))
.collect::<OLMResult<Vec<_>>>()?;
Ok(Self { root: root.to_owned(), meta, layers })
}
/// Create a new, empty layered index at `root` with the given mode.
pub fn create(root: &Path, mode: IndexMode) -> OLMResult<Self> {
fs::create_dir_all(root)?;
let meta = PartitionMeta::new(mode);
meta.save(root)?;
Ok(Self { root: root.to_owned(), meta, layers: Vec::new() })
}
pub fn n_layers(&self) -> usize { self.layers.len() }
pub fn layer(&self, i: usize) -> &TypedLayer<D> { &self.layers[i] }
pub fn mode(&self) -> &IndexMode { &self.meta.mode }
/// TypedLayer `i`'s content (`Count`/`Presence`) — a lightweight disk probe,
/// no MPHF/matrix opened. For picking a `D` before committing to
/// [`TypedLayer::open`], or for reporting/diagnostics over an already-open
/// `LayeredMap` without re-opening every layer under a different `D`.
pub fn detect_layer_content(&self, i: usize) -> LayerContent {
LayerContent::detect(&layer_dir(&self.root, i))
}
/// TypedLayer `i`'s storage format — a lightweight disk probe, no
/// MPHF/matrix opened. See [`detect_layer_content`](Self::detect_layer_content).
pub fn detect_layer_storage(&self, i: usize) -> OLMResult<StorageKind> {
let dir = layer_dir(&self.root, i);
match LayerContent::detect(&dir) {
LayerContent::Count => PersistentCompactIntMatrix::detect_storage(&dir).map_err(OLMError::Io),
LayerContent::Presence => PersistentBitMatrix::detect_storage(&dir).map_err(OLMError::Io),
}
}
/// TypedLayer `i`'s evidence mode — a lightweight disk probe, no
/// MPHF/matrix opened. See [`detect_layer_content`](Self::detect_layer_content).
pub fn detect_layer_evidence(&self, i: usize) -> OLMResult<EvidenceKind> {
EvidenceKind::detect(&layer_dir(&self.root, i))
}
/// Query `kmer` across all layers. Returns `(layer_index, Hit)` on match.
pub fn query(&self, kmer: CanonicalKmer) -> Option<(usize, Hit<D::Item>)> {
self.layers
.iter()
.enumerate()
.find_map(|(i, layer)| layer.query(kmer).map(|hit| (i, hit)))
}
pub fn next_layer_writer(&self) -> OLMResult<UnitigFileWriter> {
let dir = layer_dir(&self.root, self.layers.len());
TypedLayer::<D>::unitig_writer(&dir)
}
fn append_layer(&mut self) -> OLMResult<()> {
let i = self.layers.len();
let dir = layer_dir(&self.root, i);
self.layers.push(TypedLayer::<D>::open(&dir, &self.meta.mode)?);
self.meta.n_layers = self.layers.len();
self.meta.save(&self.root)?;
Ok(())
}
}
// ── Mode 1 — set membership ───────────────────────────────────────────────────
impl LayeredMap<()> {
pub fn push_layer(&mut self) -> OLMResult<usize> {
let i = self.layers.len();
let dir = layer_dir(&self.root, i);
TypedLayer::<()>::build(&dir, DEFAULT_BLOCK_BITS, &self.meta.mode)?;
self.append_layer()?;
Ok(i)
}
}
// ── Mode 2 — count matrix ─────────────────────────────────────────────────────
impl LayeredMap<PersistentCompactIntMatrix> {
pub fn push_layer(&mut self, count_of: impl Fn(CanonicalKmer) -> u32) -> OLMResult<usize> {
let i = self.layers.len();
let dir = layer_dir(&self.root, i);
TypedLayer::<PersistentCompactIntMatrix>::build(&dir, DEFAULT_BLOCK_BITS, &self.meta.mode, count_of)?;
self.append_layer()?;
Ok(i)
}
pub fn push_layer_from_map(&mut self, counts: &HashMap<CanonicalKmer, u32>) -> OLMResult<usize> {
self.push_layer(|kmer| counts.get(&kmer).copied().unwrap_or(0))
}
}
#[cfg(test)]
#[path = "tests/map.rs"]
mod tests;
+30 -35
View File
@@ -1,18 +1,20 @@
use std::fs::File;
use std::path::Path; use std::path::Path;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::layer::error::OLMResult; use crate::index::error::OKIResult;
use crate::layer::fingerprint::FingerprintVec;
const META_FILE: &str = "meta.json"; use crate::layer::mphf_layer::{EvidenceKind, FINGERPRINT_FILE};
use crate::layer::utils::layer_dir;
// ── IndexMode ───────────────────────────────────────────────────────────────── // ── IndexMode ─────────────────────────────────────────────────────────────────
/// Evidence mode for an entire partitioned index — homogeneous across all layers. /// Evidence mode for an entire partitioned index — homogeneous across all layers.
/// ///
/// Determined once at build time; stored in `PartitionMeta` (`meta.json`). /// Determined once at build time. All layers within an index share the same
/// All layers within an index share the same mode. /// mode, so it is never persisted separately — [`IndexMode::detect`] reads it
/// straight back from whichever evidence file(s) layer 0 actually has on
/// disk, the same probe [`EvidenceKind::detect`] uses.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")] #[serde(tag = "type", rename_all = "snake_case")]
pub enum IndexMode { pub enum IndexMode {
@@ -20,7 +22,9 @@ pub enum IndexMode {
Exact, Exact,
/// Approximate evidence: `fingerprint.bin` only. /// Approximate evidence: `fingerprint.bin` only.
/// `b` — fingerprint bits per slot; false-positive rate ≈ 1/2^b per query. /// `b` — fingerprint bits per slot; false-positive rate ≈ 1/2^b per query.
/// `z` — Findere consecutive-kmer parameter (build-time only; not used at query time). /// `z` — Findere consecutive-kmer parameter (build-time only; not used at
/// query time, not stored on disk, always `0` once recovered via
/// [`detect`](Self::detect)).
Approx { b: u8, z: u8 }, Approx { b: u8, z: u8 },
/// Hybrid: both `fingerprint.bin` and `evidence.bin` + `unitigs.bin.idx`. /// Hybrid: both `fingerprint.bin` and `evidence.bin` + `unitigs.bin.idx`.
/// `find()` uses the fingerprint (O(1), approx); `find_strict()` uses exact evidence. /// `find()` uses the fingerprint (O(1), approx); `find_strict()` uses exact evidence.
@@ -31,33 +35,24 @@ impl Default for IndexMode {
fn default() -> Self { Self::Exact } fn default() -> Self { Self::Exact }
} }
// ── PartitionMeta ───────────────────────────────────────────────────────────── impl IndexMode {
/// Detect the evidence mode actually on disk, from partition index
/// Index-level metadata stored in `meta.json` at the root of a partition index. /// directory `root`'s layer 0 — the single source of truth every layer
#[derive(Debug, Clone, Serialize, Deserialize)] /// in the same index shares. `z` cannot be recovered (not stored in
pub struct PartitionMeta { /// `fingerprint.bin`, and irrelevant past build time), so it comes back
pub n_layers: usize, /// as `0`.
#[serde(default)] pub fn detect(root: &Path) -> OKIResult<Self> {
pub mode: IndexMode, let layer0 = layer_dir(root, 0);
} Ok(match EvidenceKind::detect(&layer0)? {
EvidenceKind::Exact => IndexMode::Exact,
impl PartitionMeta { EvidenceKind::Approx => IndexMode::Approx {
pub fn new(mode: IndexMode) -> Self { b: FingerprintVec::open(&layer0.join(FINGERPRINT_FILE))?.b(),
Self { n_layers: 0, mode } z: 0,
} },
EvidenceKind::Hybrid => IndexMode::Hybrid {
pub fn load(dir: &Path) -> OLMResult<Self> { b: FingerprintVec::open(&layer0.join(FINGERPRINT_FILE))?.b(),
let f = File::open(dir.join(META_FILE))?; z: 0,
Ok(serde_json::from_reader(f)?) },
} })
pub fn save(&self, dir: &Path) -> OLMResult<()> {
let f = File::create(dir.join(META_FILE))?;
serde_json::to_writer_pretty(f, self)?;
Ok(())
} }
} }
impl Default for PartitionMeta {
fn default() -> Self { Self::new(IndexMode::Exact) }
}
+4 -11
View File
@@ -1,20 +1,13 @@
pub mod content_layer; pub mod content_layer;
pub mod error;
pub mod evidence; pub mod evidence;
pub mod fingerprint; pub mod fingerprint;
pub mod layered_store;
pub mod map;
pub mod meta; pub mod meta;
pub(crate) mod mphf_layer; pub(crate) mod mphf_layer;
pub mod typed_layer; pub mod typed_layer;
pub(crate) mod utils;
pub use crate::index::error::{OKIError, OKIResult};
pub use content_layer::KmerLayer; pub use content_layer::KmerLayer;
pub use error::{OLMError, OLMResult}; pub use meta::IndexMode;
pub use layered_store::LayeredStore;
pub use map::LayeredMap;
pub use meta::{IndexMode, PartitionMeta};
pub use mphf_layer::{EvidenceKind, KmerBatchIter, KmerIter, MphfLayer, MphfOnly}; pub use mphf_layer::{EvidenceKind, KmerBatchIter, KmerIter, MphfLayer, MphfOnly};
pub use typed_layer::{ pub use typed_layer::{HasLayerContent, HasStorageKind, Hit, LayerContent, LayerData, TypedLayer};
HasLayerContent, HasStorageKind, Hit, LayerContent, LayerData, TypedLayer,
dereplicated_superkmers_path, layer_dir, open_data, raw_superkmers_path,
};
+38 -42
View File
@@ -10,7 +10,7 @@ use obikseq::CanonicalKmer;
use obiskio::{CanonicalKmerIter, UnitigFileReader, UnitigFileWriter, build_unitig_idx}; use obiskio::{CanonicalKmerIter, UnitigFileReader, UnitigFileWriter, build_unitig_idx};
use ptr_hash::{PtrHash, PtrHashParams, bucket_fn::CubicEps, hash::Xx64}; use ptr_hash::{PtrHash, PtrHashParams, bucket_fn::CubicEps, hash::Xx64};
use crate::layer::error::{OLMError, OLMResult}; use crate::index::error::{OKIError, OKIResult};
use crate::layer::evidence::{Evidence, EvidenceWriter}; use crate::layer::evidence::{Evidence, EvidenceWriter};
use crate::layer::fingerprint::{FingerprintVec, FingerprintVecWriter}; use crate::layer::fingerprint::{FingerprintVec, FingerprintVecWriter};
use crate::layer::meta::IndexMode; use crate::layer::meta::IndexMode;
@@ -76,7 +76,7 @@ impl LayerEvidence {
/// Identify which evidence files exist at `layer_dir`, without opening /// Identify which evidence files exist at `layer_dir`, without opening
/// any of them — a lightweight disk probe (same signal `EvidenceKind:: /// any of them — a lightweight disk probe (same signal `EvidenceKind::
/// detect` uses), not a load. /// detect` uses), not a load.
fn at(layer_dir: &Path) -> OLMResult<Self> { fn at(layer_dir: &Path) -> OKIResult<Self> {
Ok(match EvidenceKind::detect(layer_dir)? { Ok(match EvidenceKind::detect(layer_dir)? {
EvidenceKind::Exact => LayerEvidence::Exact { EvidenceKind::Exact => LayerEvidence::Exact {
evidence_path: layer_dir.join(EVIDENCE_FILE), evidence_path: layer_dir.join(EVIDENCE_FILE),
@@ -94,7 +94,7 @@ impl LayerEvidence {
} }
/// mmap the evidence file(s) this identity points to. /// mmap the evidence file(s) this identity points to.
fn open(self) -> OLMResult<OpenLayerEvidence> { fn open(self) -> OKIResult<OpenLayerEvidence> {
Ok(match self { Ok(match self {
LayerEvidence::Exact { evidence_path, unitig_path } => OpenLayerEvidence::Exact { LayerEvidence::Exact { evidence_path, unitig_path } => OpenLayerEvidence::Exact {
evidence: Evidence::open(&evidence_path)?, evidence: Evidence::open(&evidence_path)?,
@@ -127,17 +127,17 @@ impl EvidenceKind {
/// Lightweight disk probe, no MPHF/evidence opened: presence of /// Lightweight disk probe, no MPHF/evidence opened: presence of
/// `evidence.bin`/`fingerprint.bin` alone determines the mode — same /// `evidence.bin`/`fingerprint.bin` alone determines the mode — same
/// signal `MphfLayer::open` uses, just without opening either file. /// signal `MphfLayer::open` uses, just without opening either file.
pub fn detect(layer_dir: &Path) -> OLMResult<EvidenceKind> { pub fn detect(layer_dir: &Path) -> OKIResult<EvidenceKind> {
let has_evidence = layer_dir.join(EVIDENCE_FILE).exists(); let has_evidence = layer_dir.join(EVIDENCE_FILE).exists();
let has_fingerprint = layer_dir.join(FINGERPRINT_FILE).exists(); let has_fingerprint = layer_dir.join(FINGERPRINT_FILE).exists();
match (has_evidence, has_fingerprint) { match (has_evidence, has_fingerprint) {
(true, false) => Ok(EvidenceKind::Exact), (true, false) => Ok(EvidenceKind::Exact),
(false, true) => Ok(EvidenceKind::Approx), (false, true) => Ok(EvidenceKind::Approx),
(true, true) => Ok(EvidenceKind::Hybrid), (true, true) => Ok(EvidenceKind::Hybrid),
(false, false) => Err(OLMError::InvalidLayer(format!( (false, false) => Err(OKIError::InvalidData { context: "layer", detail: format!(
"no evidence.bin or fingerprint.bin in {}", "no evidence.bin or fingerprint.bin in {}",
layer_dir.display() layer_dir.display()
))), ) }),
} }
} }
} }
@@ -151,7 +151,6 @@ impl EvidenceKind {
/// - [`find_strict`](Self::find_strict) — always exact; O(1) on Exact/Hybrid layers, /// - [`find_strict`](Self::find_strict) — always exact; O(1) on Exact/Hybrid layers,
/// O(n) sequential scan on Approx layers. /// O(n) sequential scan on Approx layers.
pub struct MphfLayer { pub struct MphfLayer {
layer_dir: PathBuf,
/// Mode-independent copy — used by `iter_kmers` (all modes) and /// Mode-independent copy — used by `iter_kmers` (all modes) and
/// `find_strict`'s `Approx` fallback. `Exact`/`Hybrid` additionally /// `find_strict`'s `Approx` fallback. `Exact`/`Hybrid` additionally
/// carry their own inside `OpenLayerEvidence` (see its doc comment for /// carry their own inside `OpenLayerEvidence` (see its doc comment for
@@ -165,18 +164,15 @@ pub struct MphfLayer {
} }
impl MphfLayer { impl MphfLayer {
/// This layer's own directory.
pub(crate) fn dir(&self) -> &Path {
&self.layer_dir
}
/// Open a layer — evidence mode is auto-detected from which files exist /// Open a layer — evidence mode is auto-detected from which files exist
/// on disk (see [`LayerEvidence::at`]), never passed in: the caller /// on disk (see [`LayerEvidence::at`]), never passed in: the caller
/// can't tell this type anything about its own mode that isn't already /// can't tell this type anything about its own mode that isn't already
/// derivable from `layer_dir` itself. /// derivable from `dir` itself. `dir` is only used transiently here —
pub fn open(dir: &Path) -> OLMResult<Self> { /// not stored: `KmerLayer`, one level up, is what owns a layer's
/// directory as a persistent field, not this type.
pub fn open(dir: &Path) -> OKIResult<Self> {
let mphf: MemCase<MphfEps> = Mphf::mmap(&dir.join(MPHF_FILE), Flags::empty()) let mphf: MemCase<MphfEps> = Mphf::mmap(&dir.join(MPHF_FILE), Flags::empty())
.map_err(|e| OLMError::InvalidLayer(e.to_string()))?; .map_err(|e| OKIError::InvalidData { context: "layer", detail: e.to_string() })?;
let unitigs = Arc::new(UnitigFileReader::open(&dir.join(UNITIGS_FILE))?); let unitigs = Arc::new(UnitigFileReader::open(&dir.join(UNITIGS_FILE))?);
let ev = LayerEvidence::at(dir)?.open()?; let ev = LayerEvidence::at(dir)?.open()?;
let n = match &ev { let n = match &ev {
@@ -184,7 +180,7 @@ impl MphfLayer {
OpenLayerEvidence::Hybrid { evidence, .. } => evidence.len(), OpenLayerEvidence::Hybrid { evidence, .. } => evidence.len(),
OpenLayerEvidence::Approx { fingerprint } => fingerprint.n(), OpenLayerEvidence::Approx { fingerprint } => fingerprint.n(),
}; };
Ok(Self { layer_dir: dir.to_owned(), unitigs, mphf, ev, n }) Ok(Self { unitigs, mphf, ev, n })
} }
// ── Query API ───────────────────────────────────────────────────────────── // ── Query API ─────────────────────────────────────────────────────────────
@@ -405,9 +401,9 @@ impl Iterator for KmerBatchIter {
pub struct MphfOnly(MemCase<MphfEps>); pub struct MphfOnly(MemCase<MphfEps>);
impl MphfOnly { impl MphfOnly {
pub fn open(dir: &Path) -> OLMResult<Self> { pub fn open(dir: &Path) -> OKIResult<Self> {
let mphf: MemCase<MphfEps> = Mphf::mmap(&dir.join(MPHF_FILE), Flags::empty()) let mphf: MemCase<MphfEps> = Mphf::mmap(&dir.join(MPHF_FILE), Flags::empty())
.map_err(|e| OLMError::InvalidLayer(e.to_string()))?; .map_err(|e| OKIError::InvalidData { context: "layer", detail: e.to_string() })?;
Ok(Self(mphf)) Ok(Self(mphf))
} }
@@ -421,13 +417,13 @@ impl MphfOnly {
impl MphfLayer { impl MphfLayer {
// ── Build helpers ───────────────────────────────────────────────────────── // ── Build helpers ─────────────────────────────────────────────────────────
pub fn unitig_writer(dir: &Path) -> OLMResult<UnitigFileWriter> { pub fn unitig_writer(dir: &Path) -> OKIResult<UnitigFileWriter> {
fs::create_dir_all(dir)?; fs::create_dir_all(dir)?;
Ok(UnitigFileWriter::create(&dir.join(UNITIGS_FILE))?) Ok(UnitigFileWriter::create(&dir.join(UNITIGS_FILE))?)
} }
/// Build `evidence.bin` + `unitigs.bin.idx` from `unitigs.bin` + `mphf.bin`. /// Build `evidence.bin` + `unitigs.bin.idx` from `unitigs.bin` + `mphf.bin`.
pub fn build_exact_evidence(dir: &Path, block_bits: u8) -> OLMResult<usize> { pub fn build_exact_evidence(dir: &Path, block_bits: u8) -> OKIResult<usize> {
let unitig_path = dir.join(UNITIGS_FILE); let unitig_path = dir.join(UNITIGS_FILE);
let unitigs = UnitigFileReader::open_sequential(&unitig_path)?; let unitigs = UnitigFileReader::open_sequential(&unitig_path)?;
let n = unitigs.n_kmers(); let n = unitigs.n_kmers();
@@ -439,7 +435,7 @@ impl MphfLayer {
} }
let mphf: Mphf = Mphf::load_full(&dir.join(MPHF_FILE)) let mphf: Mphf = Mphf::load_full(&dir.join(MPHF_FILE))
.map_err(|e| OLMError::InvalidLayer(e.to_string()))?; .map_err(|e| OKIError::InvalidData { context: "layer", detail: e.to_string() })?;
let mut ev = EvidenceWriter::new(n); let mut ev = EvidenceWriter::new(n);
let mut seen = vec![0u8; (n + 7) / 8]; let mut seen = vec![0u8; (n + 7) / 8];
@@ -447,12 +443,12 @@ impl MphfLayer {
for (kmer, chunk_id, rank) in unitigs.iter_indexed_canonical_kmers() { for (kmer, chunk_id, rank) in unitigs.iter_indexed_canonical_kmers() {
let slot = mphf.index(&kmer.raw()); let slot = mphf.index(&kmer.raw());
if slot >= n { if slot >= n {
return Err(OLMError::Mphf("slot out of bounds".into())); return Err(OKIError::InvalidData { context: "mphf", detail: "slot out of bounds".into() });
} }
let byte = slot / 8; let byte = slot / 8;
let bit = 1u8 << (slot % 8); let bit = 1u8 << (slot % 8);
if seen[byte] & bit != 0 { if seen[byte] & bit != 0 {
return Err(OLMError::Mphf("duplicate slot".into())); return Err(OKIError::InvalidData { context: "mphf", detail: "duplicate slot".into() });
} }
seen[byte] |= bit; seen[byte] |= bit;
ev.set(slot, chunk_id as u32, rank as u8); ev.set(slot, chunk_id as u32, rank as u8);
@@ -464,14 +460,14 @@ impl MphfLayer {
} }
/// Build `fingerprint.bin` from `unitigs.bin` + `mphf.bin`. /// Build `fingerprint.bin` from `unitigs.bin` + `mphf.bin`.
pub fn build_approx_evidence(dir: &Path, b: u8, z: u8) -> OLMResult<usize> { pub fn build_approx_evidence(dir: &Path, b: u8, z: u8) -> OKIResult<usize> {
if b == 0 || b > 64 { if b == 0 || b > 64 {
return Err(OLMError::InvalidLayer( return Err(OKIError::InvalidData { context: "layer", detail:
"fingerprint width must be 1..=64".into(), "fingerprint width must be 1..=64".into(),
)); });
} }
if z == 0 { if z == 0 {
return Err(OLMError::InvalidLayer("z must be ≥ 1".into())); return Err(OKIError::InvalidData { context: "layer", detail: "z must be ≥ 1".into() });
} }
let unitig_path = dir.join(UNITIGS_FILE); let unitig_path = dir.join(UNITIGS_FILE);
let unitigs = UnitigFileReader::open_sequential(&unitig_path)?; let unitigs = UnitigFileReader::open_sequential(&unitig_path)?;
@@ -483,14 +479,14 @@ impl MphfLayer {
} }
let mphf: Mphf = Mphf::load_full(&dir.join(MPHF_FILE)) let mphf: Mphf = Mphf::load_full(&dir.join(MPHF_FILE))
.map_err(|e| OLMError::InvalidLayer(e.to_string()))?; .map_err(|e| OKIError::InvalidData { context: "layer", detail: e.to_string() })?;
let mut fw = FingerprintVecWriter::new(n, b); let mut fw = FingerprintVecWriter::new(n, b);
for (kmer, _, _) in unitigs.iter_indexed_canonical_kmers() { for (kmer, _, _) in unitigs.iter_indexed_canonical_kmers() {
let slot = mphf.index(&kmer.raw()); let slot = mphf.index(&kmer.raw());
if slot >= n { if slot >= n {
return Err(OLMError::Mphf("slot out of bounds".into())); return Err(OKIError::InvalidData { context: "mphf", detail: "slot out of bounds".into() });
} }
fw.set(slot, kmer.seq_hash()); fw.set(slot, kmer.seq_hash());
} }
@@ -508,24 +504,24 @@ impl MphfLayer {
dir: &Path, dir: &Path,
block_bits: u8, block_bits: u8,
mode: &IndexMode, mode: &IndexMode,
fill_slot: &mut impl FnMut(usize, CanonicalKmer) -> OLMResult<()>, fill_slot: &mut impl FnMut(usize, CanonicalKmer) -> OKIResult<()>,
) -> OLMResult<usize> { ) -> OKIResult<usize> {
use rayon::prelude::*; use rayon::prelude::*;
let unitig_path = dir.join(UNITIGS_FILE); let unitig_path = dir.join(UNITIGS_FILE);
let n = UnitigFileReader::open_sequential(&unitig_path)?.n_kmers(); let n = UnitigFileReader::open_sequential(&unitig_path)?.n_kmers();
let sk_to_olm = |e: obiskio::SKError| match e { let sk_to_olm = |e: obiskio::SKError| match e {
obiskio::SKError::Io(io) => OLMError::Io(io), obiskio::SKError::Io(io) => OKIError::Io(io),
e => OLMError::InvalidLayer(e.to_string()), e => OKIError::InvalidData { context: "layer", detail: e.to_string() },
}; };
// ── Empty layer ─────────────────────────────────────────────────────── // ── Empty layer ───────────────────────────────────────────────────────
if n == 0 { if n == 0 {
let mphf: Mphf = Mphf::try_new(&[] as &[u64], PtrHashParams::<CubicEps>::default()) let mphf: Mphf = Mphf::try_new(&[] as &[u64], PtrHashParams::<CubicEps>::default())
.ok_or_else(|| OLMError::Mphf("construction failed".into()))?; .ok_or_else(|| OKIError::InvalidData { context: "mphf", detail: "construction failed".into() })?;
mphf.store(&dir.join(MPHF_FILE)) mphf.store(&dir.join(MPHF_FILE))
.map_err(|e| OLMError::InvalidLayer(e.to_string()))?; .map_err(|e| OKIError::InvalidData { context: "layer", detail: e.to_string() })?;
match mode { match mode {
IndexMode::Exact | IndexMode::Hybrid { .. } => { IndexMode::Exact | IndexMode::Hybrid { .. } => {
fs::File::create(dir.join(EVIDENCE_FILE))?; fs::File::create(dir.join(EVIDENCE_FILE))?;
@@ -549,7 +545,7 @@ impl MphfLayer {
PtrHashParams::<CubicEps>::default(), PtrHashParams::<CubicEps>::default(),
); );
mphf.store(&dir.join(MPHF_FILE)) mphf.store(&dir.join(MPHF_FILE))
.map_err(|e| OLMError::InvalidLayer(e.to_string()))?; .map_err(|e| OKIError::InvalidData { context: "layer", detail: e.to_string() })?;
// ── Pass 2: fill evidence files + callback ──────────────────────────── // ── Pass 2: fill evidence files + callback ────────────────────────────
let unitigs2 = UnitigFileReader::open_sequential(&unitig_path)?; let unitigs2 = UnitigFileReader::open_sequential(&unitig_path)?;
@@ -561,12 +557,12 @@ impl MphfLayer {
for (kmer, chunk_id, rank) in unitigs2.iter_indexed_canonical_kmers() { for (kmer, chunk_id, rank) in unitigs2.iter_indexed_canonical_kmers() {
let slot = mphf.index(&kmer.raw()); let slot = mphf.index(&kmer.raw());
if slot >= n { if slot >= n {
return Err(OLMError::Mphf("slot out of bounds".into())); return Err(OKIError::InvalidData { context: "mphf", detail: "slot out of bounds".into() });
} }
let byte = slot / 8; let byte = slot / 8;
let bit = 1u8 << (slot % 8); let bit = 1u8 << (slot % 8);
if seen[byte] & bit != 0 { if seen[byte] & bit != 0 {
return Err(OLMError::Mphf("duplicate slot".into())); return Err(OKIError::InvalidData { context: "mphf", detail: "duplicate slot".into() });
} }
seen[byte] |= bit; seen[byte] |= bit;
ev.set(slot, chunk_id as u32, rank as u8); ev.set(slot, chunk_id as u32, rank as u8);
@@ -581,12 +577,12 @@ impl MphfLayer {
for (kmer, _, _) in unitigs2.iter_indexed_canonical_kmers() { for (kmer, _, _) in unitigs2.iter_indexed_canonical_kmers() {
let slot = mphf.index(&kmer.raw()); let slot = mphf.index(&kmer.raw());
if slot >= n { if slot >= n {
return Err(OLMError::Mphf("slot out of bounds".into())); return Err(OKIError::InvalidData { context: "mphf", detail: "slot out of bounds".into() });
} }
let byte = slot / 8; let byte = slot / 8;
let bit = 1u8 << (slot % 8); let bit = 1u8 << (slot % 8);
if seen[byte] & bit != 0 { if seen[byte] & bit != 0 {
return Err(OLMError::Mphf("duplicate slot".into())); return Err(OKIError::InvalidData { context: "mphf", detail: "duplicate slot".into() });
} }
seen[byte] |= bit; seen[byte] |= bit;
fw.set(slot, kmer.seq_hash()); fw.set(slot, kmer.seq_hash());
@@ -601,12 +597,12 @@ impl MphfLayer {
for (kmer, chunk_id, rank) in unitigs2.iter_indexed_canonical_kmers() { for (kmer, chunk_id, rank) in unitigs2.iter_indexed_canonical_kmers() {
let slot = mphf.index(&kmer.raw()); let slot = mphf.index(&kmer.raw());
if slot >= n { if slot >= n {
return Err(OLMError::Mphf("slot out of bounds".into())); return Err(OKIError::InvalidData { context: "mphf", detail: "slot out of bounds".into() });
} }
let byte = slot / 8; let byte = slot / 8;
let bit = 1u8 << (slot % 8); let bit = 1u8 << (slot % 8);
if seen[byte] & bit != 0 { if seen[byte] & bit != 0 {
return Err(OLMError::Mphf("duplicate slot".into())); return Err(OKIError::InvalidData { context: "mphf", detail: "duplicate slot".into() });
} }
seen[byte] |= bit; seen[byte] |= bit;
ev.set(slot, chunk_id as u32, rank as u8); ev.set(slot, chunk_id as u32, rank as u8);
-219
View File
@@ -1,219 +0,0 @@
use super::*;
use obicompactvec::{pack_bit_matrix, pack_sparse_bit_matrix, PersistentBitMatrix, PersistentCompactIntMatrix, StorageKind};
use obikseq::{set_k, Sequence as _, Unitig};
use obiskio::DEFAULT_BLOCK_BITS;
use crate::layer::typed_layer::LayerContent;
use crate::layer::meta::IndexMode;
use crate::layer::mphf_layer::EvidenceKind;
use tempfile::tempdir;
fn push_unitigs_and_layer(
map: &mut LayeredMap<PersistentCompactIntMatrix>,
seqs: &[&[u8]],
count: u32,
) {
let mut w = map.next_layer_writer().unwrap();
for s in seqs {
w.write(&Unitig::from_ascii(s)).unwrap();
}
w.close().unwrap();
map.push_layer(|_| count).unwrap();
}
fn canonical(ascii: &[u8]) -> CanonicalKmer {
obikseq::Kmer::from_ascii(ascii).unwrap().canonical()
}
#[test]
fn create_empty_map() {
set_k(4);
let dir = tempdir().unwrap();
let map = LayeredMap::<()>::create(dir.path(), IndexMode::Exact).unwrap();
assert_eq!(map.n_layers(), 0);
}
#[test]
fn open_reloads_layer_count() {
set_k(4);
let dir = tempdir().unwrap();
{
let mut map = LayeredMap::<PersistentCompactIntMatrix>::create(dir.path(), IndexMode::Exact).unwrap();
push_unitigs_and_layer(&mut map, &[b"AAAACGT"], 1);
}
let map = LayeredMap::<PersistentCompactIntMatrix>::open(dir.path()).unwrap();
assert_eq!(map.n_layers(), 1);
}
#[test]
fn query_finds_kmer_in_layer_zero() {
set_k(4);
let dir = tempdir().unwrap();
let mut map = LayeredMap::<PersistentCompactIntMatrix>::create(dir.path(), IndexMode::Exact).unwrap();
push_unitigs_and_layer(&mut map, &[b"AAAACGT"], 3);
let kmer = canonical(b"AAAC");
let (layer_idx, hit) = map.query(kmer).expect("kmer must be found");
assert_eq!(layer_idx, 0);
assert_eq!(hit.data[0], 3);
}
#[test]
fn query_finds_kmer_in_correct_layer() {
set_k(4);
let dir = tempdir().unwrap();
let mut map = LayeredMap::<PersistentCompactIntMatrix>::create(dir.path(), IndexMode::Exact).unwrap();
push_unitigs_and_layer(&mut map, &[b"AAAACGT"], 1);
push_unitigs_and_layer(&mut map, &[b"GGGACGT"], 2);
assert_eq!(map.n_layers(), 2);
let (li, hit) = map.query(canonical(b"AAAA")).expect("AAAA must be found");
assert_eq!(li, 0);
assert_eq!(hit.data[0], 1);
let (li, hit) = map.query(canonical(b"GGGA")).expect("GGGA must be found");
assert_eq!(li, 1);
assert_eq!(hit.data[0], 2);
}
#[test]
fn query_absent_returns_none() {
set_k(4);
let dir = tempdir().unwrap();
let mut map = LayeredMap::<PersistentCompactIntMatrix>::create(dir.path(), IndexMode::Exact).unwrap();
push_unitigs_and_layer(&mut map, &[b"AAAACGT"], 1);
let absent = canonical(b"CCCC");
assert!(map.query(absent).is_none());
}
#[test]
fn push_layer_from_map_convenience() {
set_k(4);
let dir = tempdir().unwrap();
let mut map = LayeredMap::<PersistentCompactIntMatrix>::create(dir.path(), IndexMode::Exact).unwrap();
let mut w = map.next_layer_writer().unwrap();
w.write(&Unitig::from_ascii(b"AAAACGT")).unwrap();
w.close().unwrap();
let counts: HashMap<CanonicalKmer, u32> = vec![
(canonical(b"AAAA"), 10u32),
].into_iter().collect();
map.push_layer_from_map(&counts).unwrap();
let (_, hit) = map.query(canonical(b"AAAA")).unwrap();
assert_eq!(hit.data[0], 10);
}
// ── detect_layer_content / detect_layer_storage / detect_layer_evidence ────
//
// All built directly on disk (bypassing `LayeredMap<D>::push_layer`, which
// only exists for modes 1 and 2 — mode 3/presence has no `push_layer`),
// then reopened as `LayeredMap<()>` — `()`'s `LayerData::open` never reads
// the matrix, so it's the right handle for exercising the disk-probing
// `detect_layer_*` methods regardless of what content/storage the layer
// underneath actually is.
fn write_unitigs_at(dir: &Path, seqs: &[&[u8]]) {
fs::create_dir_all(dir).unwrap();
let mut w = UnitigFileWriter::create(&dir.join(crate::layer::typed_layer::UNITIGS_FILE)).unwrap();
for s in seqs {
w.write(&Unitig::from_ascii(s)).unwrap();
}
w.close().unwrap();
}
/// Build a one-layer partition root with a presence layer at `layer_0`,
/// save `meta.json`, and reopen it as `LayeredMap<()>`.
fn presence_partition(seqs: &[&[u8]], n_genomes: usize) -> (tempfile::TempDir, LayeredMap<()>) {
let dir = tempdir().unwrap();
let mode = IndexMode::Exact;
let layer0 = layer_dir(dir.path(), 0);
write_unitigs_at(&layer0, seqs);
TypedLayer::<PersistentBitMatrix>::build_presence(&layer0, DEFAULT_BLOCK_BITS, &mode, n_genomes, |kmer, g| {
(kmer.raw().wrapping_add(g as u64)) % 2 == 0
}).unwrap();
PartitionMeta { n_layers: 1, mode }.save(dir.path()).unwrap();
let map = LayeredMap::<()>::open(dir.path()).unwrap();
(dir, map)
}
#[test]
fn detect_layer_content_count() {
set_k(4);
let dir = tempdir().unwrap();
let mut map = LayeredMap::<PersistentCompactIntMatrix>::create(dir.path(), IndexMode::Exact).unwrap();
push_unitigs_and_layer(&mut map, &[b"AAAACGT"], 3);
assert_eq!(map.detect_layer_content(0), LayerContent::Count);
}
#[test]
fn detect_layer_content_presence() {
set_k(4);
let (_dir, map) = presence_partition(&[b"AAATCTA", b"CTTCGCC", b"TGATACG"], 3);
assert_eq!(map.detect_layer_content(0), LayerContent::Presence);
}
#[test]
fn detect_layer_storage_columnar_then_packed_then_sparse() {
set_k(4);
let (dir, map) = presence_partition(&[b"AAATCTA", b"CTTCGCC", b"TGATACG"], 3);
let presence_dir = layer_dir(dir.path(), 0).join("presence");
assert_eq!(map.detect_layer_storage(0).unwrap(), StorageKind::Columnar);
pack_bit_matrix(&presence_dir).unwrap();
assert_eq!(map.detect_layer_storage(0).unwrap(), StorageKind::Packed);
pack_sparse_bit_matrix(&presence_dir).unwrap();
assert_eq!(map.detect_layer_storage(0).unwrap(), StorageKind::Sparse);
}
#[test]
fn detect_layer_storage_implicit() {
set_k(4);
let dir = tempdir().unwrap();
let mode = IndexMode::Exact;
let layer0 = layer_dir(dir.path(), 0);
write_unitigs_at(&layer0, &[b"AAAACGT"]);
// Mode-1 build: MPHF + evidence + `layer_meta.json`, no matrix at all —
// must read back as `Presence`/`Implicit`, never a third "empty" state.
TypedLayer::<()>::build(&layer0, DEFAULT_BLOCK_BITS, &mode).unwrap();
PartitionMeta { n_layers: 1, mode }.save(dir.path()).unwrap();
let map = LayeredMap::<()>::open(dir.path()).unwrap();
assert_eq!(map.detect_layer_content(0), LayerContent::Presence);
assert_eq!(map.detect_layer_storage(0).unwrap(), StorageKind::Implicit);
}
#[test]
fn detect_layer_storage_count_columnar_then_packed() {
set_k(4);
let dir = tempdir().unwrap();
let mut map = LayeredMap::<PersistentCompactIntMatrix>::create(dir.path(), IndexMode::Exact).unwrap();
push_unitigs_and_layer(&mut map, &[b"AAAACGT"], 3);
drop(map);
let map = LayeredMap::<()>::open(dir.path()).unwrap();
assert_eq!(map.detect_layer_storage(0).unwrap(), StorageKind::Columnar);
obicompactvec::pack_compact_int_matrix(&layer_dir(dir.path(), 0).join("counts")).unwrap();
assert_eq!(map.detect_layer_storage(0).unwrap(), StorageKind::Packed);
}
#[test]
fn detect_layer_evidence_exact_vs_approx() {
set_k(4);
let dir = tempdir().unwrap();
let layer0 = layer_dir(dir.path(), 0);
write_unitigs_at(&layer0, &[b"AAAACGT"]);
TypedLayer::<()>::build(&layer0, DEFAULT_BLOCK_BITS, &IndexMode::Exact).unwrap();
PartitionMeta { n_layers: 1, mode: IndexMode::Exact }.save(dir.path()).unwrap();
let map = LayeredMap::<()>::open(dir.path()).unwrap();
assert_eq!(map.detect_layer_evidence(0).unwrap(), EvidenceKind::Exact);
let approx = IndexMode::Approx { b: 8, z: 1 };
let dir2 = tempdir().unwrap();
let layer0b = layer_dir(dir2.path(), 0);
write_unitigs_at(&layer0b, &[b"AAAACGT"]);
TypedLayer::<()>::build(&layer0b, DEFAULT_BLOCK_BITS, &approx).unwrap();
PartitionMeta { n_layers: 1, mode: approx }.save(dir2.path()).unwrap();
let map2 = LayeredMap::<()>::open(dir2.path()).unwrap();
assert_eq!(map2.detect_layer_evidence(0).unwrap(), EvidenceKind::Approx);
}
+41 -84
View File
@@ -1,19 +1,18 @@
use std::collections::HashMap; use crate::layer::utils::layer_dir;
use std::fs;
use std::path::{Path, PathBuf};
use obicompactvec::{ use obicompactvec::{
BinaryMatrix, PersistentBitMatrix, PersistentBitMatrixBuilder, PersistentCompactIntMatrix, BinaryMatrix, PersistentBitMatrix, PersistentBitMatrixBuilder, PersistentCompactIntMatrix,
PersistentCompactIntMatrixBuilder, PersistentSparseBitMatrix, PersistentCompactIntMatrixBuilder, PersistentSparseBitMatrix,
}; };
use obikseq::CanonicalKmer; use obikseq::CanonicalKmer;
use obiskio::{UnitigFileReader, UnitigFileWriter}; use obiskio::{UnitigFileReader, UnitigFileWriter};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use crate::layer::error::{OLMError, OLMResult}; use crate::index::error::{OKIError, OKIResult};
use crate::layer::meta::IndexMode; use crate::layer::meta::IndexMode;
use crate::layer::mphf_layer::MphfLayer; use crate::layer::mphf_layer::MphfLayer;
pub(crate) use crate::layer::mphf_layer::UNITIGS_FILE; pub(crate) use crate::layer::mphf_layer::UNITIGS_FILE;
pub(crate) const COUNTS_DIR: &str = "counts"; pub(crate) const COUNTS_DIR: &str = "counts";
pub(crate) const PRESENCE_DIR: &str = "presence"; pub(crate) const PRESENCE_DIR: &str = "presence";
@@ -21,48 +20,10 @@ pub(crate) const PRESENCE_DIR: &str = "presence";
pub trait LayerData: Sized { pub trait LayerData: Sized {
type Item; type Item;
fn open(layer_dir: &Path) -> OLMResult<Self>; fn open(layer_dir: &Path) -> OKIResult<Self>;
fn read(&self, slot: usize) -> Self::Item; fn read(&self, slot: usize) -> Self::Item;
} }
/// TypedLayer directory path for layer `i` within a partition's index root — the
/// single source of truth for the on-disk `layer_N` naming convention
/// (mirrors what `LayeredMap::open`/`push_layer` already use internally).
///
/// `obikindex::layer` operates within a single partition's index root; it has
/// no notion of "partition" at all. Turning a partition number into that
/// root is `obikindex::KmerIndex::index_dir`'s job, one layer up — callers
/// here only ever name a layer *number*, never build the path themselves.
pub fn layer_dir(root: &Path, i: usize) -> PathBuf {
root.join(format!("layer_{i}"))
}
/// Superkmer-file extension used by every pre-layer-construction artifact
/// below (`raw`/`dereplicated`) — an implementation detail of the SK file
/// format, never meant to leak as a literal string past this module.
const SK_EXT: &str = "skmer.zst";
/// Path of a layer's raw, not-yet-dereplicated superkmer file — written by
/// whichever algorithm routes superkmers into this layer (today:
/// `obikindexer::algorithms::partitionner::PartitionRouter`), read by whichever
/// algorithm dereplicates it (today:
/// `obikindexer::algorithms::dereplicator::Dereplicator`). Naming this once
/// here, rather than in either algorithm submodule, is what lets the two
/// agree on the filename without depending on each other directly — see
/// `DevDocMD/implementation/partition_layer_cache.md`.
pub fn raw_superkmers_path(layer_dir: &Path) -> PathBuf {
layer_dir.join(format!("raw.{SK_EXT}"))
}
/// Path of a layer's dereplicated superkmer file — written by
/// `obikindexer::algorithms::dereplicator::Dereplicator`, read by whichever
/// algorithm counts kmer abundances from it (today:
/// `obikindexer::algorithms::partitionner::PartitionRouter::count_kmer`) and,
/// later, by `obikindex::build_index_layer` to build the real layer.
pub fn dereplicated_superkmers_path(layer_dir: &Path) -> PathBuf {
layer_dir.join(format!("dereplicated.{SK_EXT}"))
}
/// Opens layer `i`'s data only, skipping the MPHF — for callers that only /// Opens layer `i`'s data only, skipping the MPHF — for callers that only
/// need matrix-level operations (distance traits, column weights, group /// need matrix-level operations (distance traits, column weights, group
/// filters, sub-matrix extraction) and never look up a kmer for this layer. /// filters, sub-matrix extraction) and never look up a kmer for this layer.
@@ -74,13 +35,13 @@ pub fn dereplicated_superkmers_path(layer_dir: &Path) -> PathBuf {
/// *number* within a partition it already knows the root of, the same /// *number* within a partition it already knows the root of, the same
/// vocabulary as [`layer_dir`] and `LayeredMap` — never the `layer_N` /// vocabulary as [`layer_dir`] and `LayeredMap` — never the `layer_N`
/// naming convention itself, which stays private to this crate. /// naming convention itself, which stays private to this crate.
pub fn open_data<D: LayerData>(root: &Path, i: usize) -> OLMResult<D> { pub(crate) fn open_data<D: LayerData>(root: &Path, i: usize) -> OKIResult<D> {
D::open(&layer_dir(root, i)) D::open(&layer_dir(root, i))
} }
impl LayerData for () { impl LayerData for () {
type Item = (); type Item = ();
fn open(_layer_dir: &Path) -> OLMResult<Self> { fn open(_layer_dir: &Path) -> OKIResult<Self> {
Ok(()) Ok(())
} }
fn read(&self, _slot: usize) {} fn read(&self, _slot: usize) {}
@@ -88,8 +49,8 @@ impl LayerData for () {
impl LayerData for PersistentCompactIntMatrix { impl LayerData for PersistentCompactIntMatrix {
type Item = Box<[u32]>; type Item = Box<[u32]>;
fn open(layer_dir: &Path) -> OLMResult<Self> { fn open(layer_dir: &Path) -> OKIResult<Self> {
PersistentCompactIntMatrix::open(layer_dir).map_err(OLMError::Io) PersistentCompactIntMatrix::open(layer_dir).map_err(OKIError::Io)
} }
fn read(&self, slot: usize) -> Box<[u32]> { fn read(&self, slot: usize) -> Box<[u32]> {
self.row(slot) self.row(slot)
@@ -98,8 +59,8 @@ impl LayerData for PersistentCompactIntMatrix {
impl LayerData for PersistentBitMatrix { impl LayerData for PersistentBitMatrix {
type Item = Box<[bool]>; type Item = Box<[bool]>;
fn open(layer_dir: &Path) -> OLMResult<Self> { fn open(layer_dir: &Path) -> OKIResult<Self> {
PersistentBitMatrix::open(layer_dir).map_err(OLMError::Io) PersistentBitMatrix::open(layer_dir).map_err(OKIError::Io)
} }
fn read(&self, slot: usize) -> Box<[bool]> { fn read(&self, slot: usize) -> Box<[bool]> {
self.row(slot) self.row(slot)
@@ -108,8 +69,8 @@ impl LayerData for PersistentBitMatrix {
impl LayerData for PersistentSparseBitMatrix { impl LayerData for PersistentSparseBitMatrix {
type Item = Box<[bool]>; type Item = Box<[bool]>;
fn open(layer_dir: &Path) -> OLMResult<Self> { fn open(layer_dir: &Path) -> OKIResult<Self> {
PersistentSparseBitMatrix::open(&layer_dir.join(PRESENCE_DIR)).map_err(OLMError::Io) PersistentSparseBitMatrix::open(&layer_dir.join(PRESENCE_DIR)).map_err(OKIError::Io)
} }
fn read(&self, slot: usize) -> Box<[bool]> { fn read(&self, slot: usize) -> Box<[bool]> {
self.row(slot) self.row(slot)
@@ -201,11 +162,7 @@ pub struct Hit<T = ()> {
// ── Common read path ────────────────────────────────────────────────────────── // ── Common read path ──────────────────────────────────────────────────────────
impl<D: LayerData> TypedLayer<D> { impl<D: LayerData> TypedLayer<D> {
// `mode` no longer forwarded to `MphfLayer::open` (auto-detected from pub fn open(path: &Path) -> OKIResult<Self> {
// disk now, see mphf_layer.rs) — kept as a parameter for the moment so
// this signature (and `KmerLayer::open`'s call to it) doesn't have to
// change in the same step; dropped when content_layer.rs is done.
pub fn open(path: &Path, _mode: &IndexMode) -> OLMResult<Self> {
let mphf = MphfLayer::open(path)?; let mphf = MphfLayer::open(path)?;
let data = D::open(path)?; let data = D::open(path)?;
Ok(Self { mphf, data }) Ok(Self { mphf, data })
@@ -266,21 +223,21 @@ impl<D: LayerData> TypedLayer<D> {
self.mphf.enumerate_kmers_batch(n) self.mphf.enumerate_kmers_batch(n)
} }
pub fn unitig_writer(out_dir: &Path) -> OLMResult<UnitigFileWriter> { pub fn unitig_writer(out_dir: &Path) -> OKIResult<UnitigFileWriter> {
MphfLayer::unitig_writer(out_dir) MphfLayer::unitig_writer(out_dir)
} }
/// Build `unitigs.bin.idx` and `evidence.bin` from `unitigs.bin` and /// Build `unitigs.bin.idx` and `evidence.bin` from `unitigs.bin` and
/// `mphf.bin` already present in `layer_dir`. /// `mphf.bin` already present in `layer_dir`.
/// `block_bits` controls the `.idx` block size (2^block_bits chunks/block). /// `block_bits` controls the `.idx` block size (2^block_bits chunks/block).
pub fn build_exact_evidence(layer_dir: &Path, block_bits: u8) -> OLMResult<usize> { pub fn build_exact_evidence(layer_dir: &Path, block_bits: u8) -> OKIResult<usize> {
MphfLayer::build_exact_evidence(layer_dir, block_bits) MphfLayer::build_exact_evidence(layer_dir, block_bits)
} }
/// Build `fingerprint.bin` from `unitigs.bin` and `mphf.bin` already /// Build `fingerprint.bin` from `unitigs.bin` and `mphf.bin` already
/// present in `layer_dir`. `b` — fingerprint bits (1..=64); `z` — Findere /// present in `layer_dir`. `b` — fingerprint bits (1..=64); `z` — Findere
/// consecutive k-mer parameter (≥1). /// consecutive k-mer parameter (≥1).
pub fn build_approx_evidence(layer_dir: &Path, b: u8, z: u8) -> OLMResult<usize> { pub fn build_approx_evidence(layer_dir: &Path, b: u8, z: u8) -> OKIResult<usize> {
MphfLayer::build_approx_evidence(layer_dir, b, z) MphfLayer::build_approx_evidence(layer_dir, b, z)
} }
@@ -311,21 +268,21 @@ impl<D: LayerData + HasStorageKind> TypedLayer<D> {
// ── Mode 1 — set membership ─────────────────────────────────────────────────── // ── Mode 1 — set membership ───────────────────────────────────────────────────
impl TypedLayer<()> { impl TypedLayer<()> {
pub fn build(out_dir: &Path, block_bits: u8, mode: &IndexMode) -> OLMResult<usize> { pub fn build(out_dir: &Path, block_bits: u8, mode: &IndexMode) -> OKIResult<usize> {
MphfLayer::build(out_dir, block_bits, mode, &mut |_, _| Ok(())) MphfLayer::build(out_dir, block_bits, mode, &mut |_, _| Ok(()))
} }
/// Create a presence matrix for a set-membership layer (first merge). /// Create a presence matrix for a set-membership layer (first merge).
pub fn init_presence_matrix(layer_dir: &Path, n_kmers: usize) -> OLMResult<()> { pub fn init_presence_matrix(layer_dir: &Path, n_kmers: usize) -> OKIResult<()> {
let presence_dir = layer_dir.join(PRESENCE_DIR); let presence_dir = layer_dir.join(PRESENCE_DIR);
fs::create_dir_all(&presence_dir).map_err(OLMError::Io)?; fs::create_dir_all(&presence_dir).map_err(OKIError::Io)?;
let mut mb = let mut mb =
PersistentBitMatrixBuilder::new(n_kmers, &presence_dir).map_err(OLMError::Io)?; PersistentBitMatrixBuilder::new(n_kmers, &presence_dir).map_err(OKIError::Io)?;
mb.add_col_ones() mb.add_col_ones()
.map_err(OLMError::Io)? .map_err(OKIError::Io)?
.close() .close()
.map_err(OLMError::Io)?; .map_err(OKIError::Io)?;
mb.close().map_err(OLMError::Io) mb.close().map_err(OKIError::Io)
} }
} }
@@ -337,18 +294,18 @@ impl TypedLayer<PersistentCompactIntMatrix> {
block_bits: u8, block_bits: u8,
mode: &IndexMode, mode: &IndexMode,
count_of: impl Fn(CanonicalKmer) -> u32, count_of: impl Fn(CanonicalKmer) -> u32,
) -> OLMResult<usize> { ) -> OKIResult<usize> {
let n = UnitigFileReader::open_sequential(&out_dir.join(UNITIGS_FILE))?.n_kmers(); let n = UnitigFileReader::open_sequential(&out_dir.join(UNITIGS_FILE))?.n_kmers();
let counts_dir = out_dir.join(COUNTS_DIR); let counts_dir = out_dir.join(COUNTS_DIR);
let mut mb = let mut mb =
PersistentCompactIntMatrixBuilder::new(n, &counts_dir).map_err(OLMError::Io)?; PersistentCompactIntMatrixBuilder::new(n, &counts_dir).map_err(OKIError::Io)?;
let mut col = mb.add_col().map_err(OLMError::Io)?; let mut col = mb.add_col().map_err(OKIError::Io)?;
let n_built = MphfLayer::build(out_dir, block_bits, mode, &mut |slot, kmer| { let n_built = MphfLayer::build(out_dir, block_bits, mode, &mut |slot, kmer| {
col.set(slot, count_of(kmer)); col.set(slot, count_of(kmer));
Ok(()) Ok(())
})?; })?;
col.close().map_err(OLMError::Io)?; col.close().map_err(OKIError::Io)?;
mb.close().map_err(OLMError::Io)?; mb.close().map_err(OKIError::Io)?;
Ok(n_built) Ok(n_built)
} }
@@ -357,7 +314,7 @@ impl TypedLayer<PersistentCompactIntMatrix> {
block_bits: u8, block_bits: u8,
mode: &IndexMode, mode: &IndexMode,
counts: &HashMap<CanonicalKmer, u32>, counts: &HashMap<CanonicalKmer, u32>,
) -> OLMResult<usize> { ) -> OKIResult<usize> {
Self::build(out_dir, block_bits, mode, |kmer| { Self::build(out_dir, block_bits, mode, |kmer| {
counts.get(&kmer).copied().unwrap_or(0) counts.get(&kmer).copied().unwrap_or(0)
}) })
@@ -370,9 +327,9 @@ impl TypedLayer<PersistentCompactIntMatrix> {
pub fn append_genome_column( pub fn append_genome_column(
layer_dir: &Path, layer_dir: &Path,
value_of: impl Fn(usize) -> u32, value_of: impl Fn(usize) -> u32,
) -> OLMResult<()> { ) -> OKIResult<()> {
PersistentCompactIntMatrix::append_column(&layer_dir.join(COUNTS_DIR), value_of) PersistentCompactIntMatrix::append_column(&layer_dir.join(COUNTS_DIR), value_of)
.map_err(OLMError::Io) .map_err(OKIError::Io)
} }
/// Number of genome columns in this layer's count matrix. /// Number of genome columns in this layer's count matrix.
@@ -448,9 +405,9 @@ impl TypedLayer<PersistentBitMatrix> {
pub fn append_genome_column( pub fn append_genome_column(
layer_dir: &Path, layer_dir: &Path,
value_of: impl Fn(usize) -> bool, value_of: impl Fn(usize) -> bool,
) -> OLMResult<()> { ) -> OKIResult<()> {
PersistentBitMatrix::append_column(&layer_dir.join(PRESENCE_DIR), value_of) PersistentBitMatrix::append_column(&layer_dir.join(PRESENCE_DIR), value_of)
.map_err(OLMError::Io) .map_err(OKIError::Io)
} }
pub fn build_presence( pub fn build_presence(
@@ -459,13 +416,13 @@ impl TypedLayer<PersistentBitMatrix> {
mode: &IndexMode, mode: &IndexMode,
n_genomes: usize, n_genomes: usize,
present_in: impl Fn(CanonicalKmer, usize) -> bool, present_in: impl Fn(CanonicalKmer, usize) -> bool,
) -> OLMResult<usize> { ) -> OKIResult<usize> {
let n = UnitigFileReader::open_sequential(&out_dir.join(UNITIGS_FILE))?.n_kmers(); let n = UnitigFileReader::open_sequential(&out_dir.join(UNITIGS_FILE))?.n_kmers();
let presence_dir = out_dir.join(PRESENCE_DIR); let presence_dir = out_dir.join(PRESENCE_DIR);
let mut mb = PersistentBitMatrixBuilder::new(n, &presence_dir).map_err(OLMError::Io)?; let mut mb = PersistentBitMatrixBuilder::new(n, &presence_dir).map_err(OKIError::Io)?;
let mut cols: Vec<_> = (0..n_genomes) let mut cols: Vec<_> = (0..n_genomes)
.map(|_| mb.add_col().map_err(OLMError::Io)) .map(|_| mb.add_col().map_err(OKIError::Io))
.collect::<OLMResult<_>>()?; .collect::<OKIResult<_>>()?;
let n_built = MphfLayer::build(out_dir, block_bits, mode, &mut |slot, kmer| { let n_built = MphfLayer::build(out_dir, block_bits, mode, &mut |slot, kmer| {
for (g, col) in cols.iter_mut().enumerate() { for (g, col) in cols.iter_mut().enumerate() {
col.set(slot, present_in(kmer, g)); col.set(slot, present_in(kmer, g));
@@ -473,9 +430,9 @@ impl TypedLayer<PersistentBitMatrix> {
Ok(()) Ok(())
})?; })?;
for col in cols { for col in cols {
col.close().map_err(OLMError::Io)?; col.close().map_err(OKIError::Io)?;
} }
mb.close().map_err(OLMError::Io)?; mb.close().map_err(OKIError::Io)?;
Ok(n_built) Ok(n_built)
} }
} }
+14
View File
@@ -0,0 +1,14 @@
pub(crate) const LAYERNAME_SUFFIX: &str = "layer_";
use std::path::{Path, PathBuf};
/// TypedLayer directory path for layer `i` within a partition's index root — the
/// single source of truth for the on-disk `layer_N` naming convention
/// (mirrors what `LayeredMap::open`/`push_layer` already use internally).
///
/// `obikindex::layer` operates within a single partition's index root; it has
/// no notion of "partition" at all. Turning a partition number into that
/// root is `obikindex::KmerIndex::index_dir`'s job, one layer up — callers
/// here only ever name a layer *number*, never build the path themselves.
pub(crate) fn layer_dir(root: &Path, i: usize) -> PathBuf {
root.join(format!("{LAYERNAME_SUFFIX}{i}"))
}
+6 -6
View File
@@ -13,10 +13,10 @@ pub mod layer;
pub mod partition; pub mod partition;
pub use index::{ pub use index::{
validate_label, AggOp, DistanceMetric, DistanceOutput, GenomeInfo, GroupFilterParams, GenomeInfo,
GroupQuorumFilter, IndexBitsPerKmer, IndexBuilder, IndexConfig, IndexMeta, IndexState, IndexBuilder, IndexConfig, IndexMeta, IndexState,
KmerDesc, KmerFilter, KmerIndex, MergeMode, MetaPred, OKIError, OKIResult, OutputCol, KmerIndex, OKIError, OKIResult,
PartitionRunner, QueryHit, QueryStats, META_FILENAME, META_FILENAME,
materialize_layer, olm_to_sk, write_graph_as_unitigs, passes_all, ColBuilder,
}; };
pub use index::{filter, meta}; pub use index::meta;
+54 -33
View File
@@ -19,10 +19,13 @@
//! entirely) both still reinvent a fragment of this. Migrating them is a //! entirely) both still reinvent a fragment of this. Migrating them is a
//! separate, deferred step. //! separate, deferred step.
use crate::OKIError;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use crate::layer::{IndexMode, KmerLayer, OLMResult, layer_dir}; use crate::{
use obikseq::CanonicalKmer; KmerIndex, OKIResult,
layer::{KmerLayer, utils::layer_dir},
};
/// Partition subdirectory name, under an index's root — the single source /// Partition subdirectory name, under an index's root — the single source
/// of truth for the on-disk `partitions/part_NNNNN` naming convention. /// of truth for the on-disk `partitions/part_NNNNN` naming convention.
@@ -34,59 +37,77 @@ pub const PARTITIONS_SUBDIR: &str = "partitions";
/// Path of partition `i`'s directory under `root` — `<root>/partitions/part_NNNNN`, /// Path of partition `i`'s directory under `root` — `<root>/partitions/part_NNNNN`,
/// zero-padded to 5 digits. /// zero-padded to 5 digits.
pub fn partition_dir(root: &Path, i: usize) -> PathBuf { pub(crate) fn partition_dir(root: &Path, i: usize) -> PathBuf {
root.join(PARTITIONS_SUBDIR).join(format!("part_{i:05}")) root.join(PARTITIONS_SUBDIR).join(format!("part_{i:05}"))
} }
/// Path of partition `i`'s layered-index directory — `<partition_dir>/index`, /// Path of partition `i`'s layered-index directory — `<partition_dir>/index`,
/// the root every [`crate::layer::layer_dir`] call for this partition is /// the root every [`crate::layer::layer_dir`] call for this partition is
/// relative to. /// relative to.
pub fn index_dir(root: &Path, i: usize) -> PathBuf { pub(crate) fn index_dir(root: &Path, i: usize) -> PathBuf {
partition_dir(root, i).join("index") partition_dir(root, i).join("index")
} }
/// One partition's open layers, in layer order (layer 0 first). /// One partition — identified by its own `index_dir`, nothing eagerly
/// opened. `index_dir` is the minimal field `KmerLayer::at` needs to
/// compute a layer's path; added here only because `layer()` below can't
/// be written without it now that there's no `layers: Vec<KmerLayer>` to
/// index into.
pub struct KmerPartition { pub struct KmerPartition {
layers: Vec<KmerLayer>, partition_dir: PathBuf,
id: usize,
} }
impl KmerPartition { impl KmerPartition {
/// Open every layer under `index_dir` (`index_dir/layer_0`, pub fn new(index: &KmerIndex, i: usize) -> Self {
/// `index_dir/layer_1`, ... up to `n_layers`), eagerly — not lazily on KmerPartition {
/// first access, so the caller pays the mmap cost once, up front, partition_dir: index.dir().join(format!("part_{i:05}")),
/// rather than at an unpredictable point during later lookups. id: i,
pub fn open( }
index_dir: &Path,
mode: &IndexMode,
n_layers: usize,
with_counts: bool,
) -> OLMResult<Self> {
let layers = (0..n_layers)
.map(|l| KmerLayer::open(&layer_dir(index_dir, l), mode, with_counts))
.collect::<OLMResult<Vec<_>>>()?;
Ok(Self { layers })
} }
pub fn id(&self) -> usize {
self.id
}
/// Number of layers under `index_dir`, found by probing `layer_0`,
/// `layer_1`, ... sequentially until one is missing — a disk scan, not
/// a metadata read (`meta.json`'s own `n_layers` is redundant with this
/// and no longer consulted here). Intrinsically slow: one `exists()`
/// syscall per layer, called fresh every time — not for a hot path.
pub fn n_layers(&self) -> usize { pub fn n_layers(&self) -> usize {
self.layers.len() let mut n = 0;
while layer_dir(&self.partition_dir, n).exists() {
n += 1;
}
n
} }
pub fn layer(&self, i: usize) -> &KmerLayer { /// Evidence mode for this partition, detected from layer 0's evidence
&self.layers[i] /// files on disk (see [`crate::layer::IndexMode::detect`]) — never
/// persisted as metadata, since every layer within a partition shares
/// the same mode by construction.
pub fn mode(&self) -> OKIResult<crate::layer::IndexMode> {
crate::layer::IndexMode::detect(&self.partition_dir)
} }
pub fn layers(&self) -> &[KmerLayer] { pub fn layer(&self, i: usize) -> OKIResult<KmerLayer> {
&self.layers let n = self.n_layers();
if i >= n {
return Err(OKIError::Layer(obiskio::SKError::InvalidData {
context: "New KmerLayer",
detail: format!("Layer {i} requested, Partition has only {n} layer"),
}));
}
Ok(KmerLayer::new(self, i))
} }
/// Existence lookup of `kmer` across this partition's layers: tries pub fn layer_dir(&self, i: usize) -> OKIResult<PathBuf> {
/// each in turn, stopping at the first hit and reporting which layer it Ok(self.layer(i)?.dir().to_path_buf())
/// was — `find_slot`, not a data read, for a plain existence check. }
pub fn find(&self, kmer: CanonicalKmer) -> Option<usize> {
self.layers pub fn dir(&self) -> &Path {
.iter() &self.partition_dir
.enumerate()
.find_map(|(li, layer)| layer.find_slot(kmer).map(|_| li))
} }
} }
@@ -18,7 +18,7 @@ use crate::extensions::PrivateBuilder;
/// The actual per-partition construction (De Bruijn graph, unitigs, MPHF, /// The actual per-partition construction (De Bruijn graph, unitigs, MPHF,
/// matrix) stays a `KmerIndex` method (`build_index_layer`) — unlike /// matrix) stays a `KmerIndex` method (`build_index_layer`) — unlike
/// `Dereplicator`/`Counter`, it depends on several `obikindex`-internal /// `Dereplicator`/`Counter`, it depends on several `obikindex`-internal
/// helpers (`graph_pipeline`, `common::olm_to_sk`) already shared with /// helpers (`graph_pipeline`) already shared with
/// `merge`/`select`/`rebuild`'s own layer-construction paths, so it /// `merge`/`select`/`rebuild`'s own layer-construction paths, so it
/// belongs there, not duplicated or newly exported just for this /// belongs there, not duplicated or newly exported just for this
/// algorithm. `LayerBuilder`'s own job is the orchestration around it: /// algorithm. `LayerBuilder`'s own job is the orchestration around it:
+4 -6
View File
@@ -36,7 +36,7 @@ use obidebruinj::GraphDeBruijn;
use obikindex::layer::IndexMode; use obikindex::layer::IndexMode;
use obikindex::layer::{TypedLayer, meta::PartitionMeta}; use obikindex::layer::{TypedLayer, meta::PartitionMeta};
use obikindex::{KmerIndex, OKIError, OKIResult}; use obikindex::{KmerIndex, OKIError, OKIResult};
use obikindex::{materialize_layer, olm_to_sk, write_graph_as_unitigs}; use crate::graph_pipeline::{materialize_layer, write_graph_as_unitigs};
use obiskio::{SKError, SKFileMeta, SKFileReader}; use obiskio::{SKError, SKFileMeta, SKFileReader};
use ptr_hash::{PtrHash, bucket_fn::CubicEps, hash::Xx64}; use ptr_hash::{PtrHash, bucket_fn::CubicEps, hash::Xx64};
@@ -109,7 +109,7 @@ impl PrivateBuilder for KmerIndex {
.map(|g| g.label.as_str()) .map(|g| g.label.as_str())
.unwrap_or("unknown") .unwrap_or("unknown")
.to_owned(); .to_owned();
let spectrums_dir = self.root_path().join("spectrums"); let spectrums_dir = self.dir().join("spectrums");
fs::create_dir_all(&spectrums_dir)?; fs::create_dir_all(&spectrums_dir)?;
let path = spectrums_dir.join(format!("{label}.json")); let path = spectrums_dir.join(format!("{label}.json"));
let spectrum_map: BTreeMap<String, u64> = counts let spectrum_map: BTreeMap<String, u64> = counts
@@ -194,8 +194,7 @@ impl PrivateBuilder for KmerIndex {
(Some(mphf), Some(counts)) => counts.get(mphf.index(&kmer.raw())), (Some(mphf), Some(counts)) => counts.get(mphf.index(&kmer.raw())),
_ => 1, _ => 1,
}, },
) )?;
.map_err(|e| olm_to_sk(e, "layer build"))?;
n n
} else { } else {
materialize_layer(g, &layer0_dir, block_bits, mode)? materialize_layer(g, &layer0_dir, block_bits, mode)?
@@ -206,8 +205,7 @@ impl PrivateBuilder for KmerIndex {
n_layers: 1, n_layers: 1,
mode: mode.clone(), mode: mode.clone(),
} }
.save(index_dir) .save(index_dir)?;
.map_err(|e| olm_to_sk(e, "layer build"))?;
Ok(n_kmers) Ok(n_kmers)
} }
@@ -10,10 +10,9 @@ use obipipeline::{
use obidebruinj::GraphDeBruijn; use obidebruinj::GraphDeBruijn;
use obikseq::CanonicalKmer; use obikseq::CanonicalKmer;
use crate::layer::{IndexMode, TypedLayer}; use obikindex::layer::{IndexMode, TypedLayer};
use obiskio::{SKError, SKResult}; use obikindex::{OKIError, OKIResult};
use crate::index::common::olm_to_sk;
// ── KmerGraphData ───────────────────────────────────────────────────────────── // ── KmerGraphData ─────────────────────────────────────────────────────────────
@@ -29,16 +28,16 @@ enum KmerGraphData {
/// ///
/// `flat_fn(path, emit)`: opens path, iterates kmers, calls `emit(batch)` for each batch. /// `flat_fn(path, emit)`: opens path, iterates kmers, calls `emit(batch)` for each batch.
/// `filter(kmer) -> bool`: secondary filter applied in the Transform stage. /// `filter(kmer) -> bool`: secondary filter applied in the Transform stage.
pub(crate) fn build_graph<I, F, G>( pub fn build_graph<I, F, G>(
file_source: I, file_source: I,
flat_fn: F, flat_fn: F,
filter: G, filter: G,
n_workers: usize, n_workers: usize,
max_open: usize, max_open: usize,
) -> SKResult<GraphDeBruijn> ) -> OKIResult<GraphDeBruijn>
where where
I: Iterator<Item = PathBuf> + Send + 'static, I: Iterator<Item = PathBuf> + Send + 'static,
F: Fn(PathBuf, &mut dyn FnMut(Vec<CanonicalKmer>)) -> SKResult<()> + Send + Sync + 'static, F: Fn(PathBuf, &mut dyn FnMut(Vec<CanonicalKmer>)) -> OKIResult<()> + Send + Sync + 'static,
G: Fn(CanonicalKmer) -> bool + Send + Sync + 'static, G: Fn(CanonicalKmer) -> bool + Send + Sync + 'static,
{ {
let capacity = 2; let capacity = 2;
@@ -47,7 +46,7 @@ where
let filter = Arc::new(filter); let filter = Arc::new(filter);
let g_shared = Arc::new(Mutex::new(GraphDeBruijn::new())); let g_shared = Arc::new(Mutex::new(GraphDeBruijn::new()));
let g_sink = Arc::clone(&g_shared); let g_sink = Arc::clone(&g_shared);
let err_cap: Arc<Mutex<Option<SKError>>> = Arc::new(Mutex::new(None)); let err_cap: Arc<Mutex<Option<OKIError>>> = Arc::new(Mutex::new(None));
let err_flat = Arc::clone(&err_cap); let err_flat = Arc::clone(&err_cap);
let throttled = throttle(file_source, max_open); let throttled = throttle(file_source, max_open);
@@ -121,11 +120,11 @@ where
/// Phase 2 (write unitigs only): compute degrees, write unitigs to `layer_dir`, drop graph. /// Phase 2 (write unitigs only): compute degrees, write unitigs to `layer_dir`, drop graph.
/// ///
/// Returns n_kmers. Does NOT build the MPHF — caller does it. /// Returns n_kmers. Does NOT build the MPHF — caller does it.
pub fn write_graph_as_unitigs(g: GraphDeBruijn, layer_dir: &Path) -> SKResult<usize> { pub fn write_graph_as_unitigs(g: GraphDeBruijn, layer_dir: &Path) -> OKIResult<usize> {
let n_kmers = g.len(); let n_kmers = g.len();
g.compute_degrees_and_mark_starts(); g.compute_degrees_and_mark_starts();
std::fs::create_dir_all(layer_dir)?; std::fs::create_dir_all(layer_dir)?;
let mut uw = TypedLayer::<()>::unitig_writer(layer_dir).map_err(|e| olm_to_sk(e, "graph pipeline"))?; let mut uw = TypedLayer::<()>::unitig_writer(layer_dir)?;
g.try_for_each_unitig(|unitig| uw.write(unitig))?; g.try_for_each_unitig(|unitig| uw.write(unitig))?;
uw.close()?; uw.close()?;
drop(g); drop(g);
@@ -142,11 +141,10 @@ pub fn materialize_layer(
layer_dir: &Path, layer_dir: &Path,
block_bits: u8, block_bits: u8,
evidence: &IndexMode, evidence: &IndexMode,
) -> SKResult<usize> { ) -> OKIResult<usize> {
let n = write_graph_as_unitigs(g, layer_dir)?; let n = write_graph_as_unitigs(g, layer_dir)?;
debug!("materialize_layer: unitigs written ({n} kmers), building MPHF"); debug!("materialize_layer: unitigs written ({n} kmers), building MPHF");
TypedLayer::<()>::build(layer_dir, block_bits, evidence) TypedLayer::<()>::build(layer_dir, block_bits, evidence)?;
.map_err(|e| olm_to_sk(e, "graph pipeline"))?;
debug!("materialize_layer: MPHF build done"); debug!("materialize_layer: MPHF build done");
Ok(n) Ok(n)
} }
+3
View File
@@ -14,3 +14,6 @@
pub mod algorithms; pub mod algorithms;
pub(crate) mod extensions; pub(crate) mod extensions;
pub mod graph_pipeline;
pub use graph_pipeline::{build_graph, materialize_layer, write_graph_as_unitigs};
+3 -1
View File
@@ -19,6 +19,8 @@ obisys = { path = "../obisys" }
obiskio = { path = "../obiskio" } obiskio = { path = "../obiskio" }
obikindex = { path = "../obikindex", default-features = false } obikindex = { path = "../obikindex", default-features = false }
obikindexer = { path = "../obikindexer" } obikindexer = { path = "../obikindexer" }
obikfilter = { path = "../obikfilter" }
obikstats = { path = "../obikstats" }
obikalgorithm = { path = "../obikalgorithm" } obikalgorithm = { path = "../obikalgorithm" }
obikphylo = { path = "../obikphylo" } obikphylo = { path = "../obikphylo" }
obitaxonomy = { path = "../obitaxonomy" } obitaxonomy = { path = "../obitaxonomy" }
@@ -37,5 +39,5 @@ pprof = { version = "0.15", features = ["prost-codec"], optional = true }
[features] [features]
default = ["numa"] default = ["numa"]
numa = ["obikindex/numa"] numa = ["obisys/numa"]
profiling = ["dep:pprof"] profiling = ["dep:pprof"]
+29 -17
View File
@@ -2,13 +2,13 @@ use std::path::PathBuf;
use std::time::Instant; use std::time::Instant;
use clap::Args; use clap::Args;
use obikalgorithm::Algorithm;
use obikindex::layer::IndexMode;
use obikindex::{GenomeInfo, IndexBuilder, IndexConfig, IndexState, KmerIndex};
use obikindexer::algorithms::counter::Counter; use obikindexer::algorithms::counter::Counter;
use obikindexer::algorithms::dereplicator::Dereplicator; use obikindexer::algorithms::dereplicator::Dereplicator;
use obikindexer::algorithms::layer_builder::LayerBuilder; use obikindexer::algorithms::layer_builder::LayerBuilder;
use obikindexer::algorithms::partitionner::PartitionRouter; use obikindexer::algorithms::partitionner::PartitionRouter;
use obikindex::{validate_label, GenomeInfo, IndexBuilder, IndexConfig, IndexState, KmerIndex};
use obikindex::layer::IndexMode;
use obikalgorithm::Algorithm;
fn current_state(idx: &KmerIndex) -> IndexState { fn current_state(idx: &KmerIndex) -> IndexState {
idx.state().unwrap_or_else(|e| { idx.state().unwrap_or_else(|e| {
@@ -18,10 +18,12 @@ fn current_state(idx: &KmerIndex) -> IndexState {
} }
fn parse_key_value(s: &str) -> Result<(String, String), String> { fn parse_key_value(s: &str) -> Result<(String, String), String> {
let pos = s.find('=').ok_or_else(|| format!("invalid key=value: no '=' in '{s}'"))?; let pos = s
.find('=')
.ok_or_else(|| format!("invalid key=value: no '=' in '{s}'"))?;
Ok((s[..pos].to_string(), s[pos + 1..].to_string())) Ok((s[..pos].to_string(), s[pos + 1..].to_string()))
} }
use obisys::{progress_bar, spinner, Progress, Reporter, Stage}; use obisys::{Progress, Reporter, Stage, progress_bar, spinner};
use tracing::info; use tracing::info;
use crate::cli::{CommonArgs, block_size_to_bits, partitions_to_bits}; use crate::cli::{CommonArgs, block_size_to_bits, partitions_to_bits};
@@ -108,9 +110,7 @@ pub(crate) fn resolve_approx_params(
const DEFAULT_B: u8 = 8; const DEFAULT_B: u8 = 8;
const DEFAULT_Z: u8 = 1; const DEFAULT_Z: u8 = 1;
let bits_needed = |fp: f64| -> u8 { let bits_needed = |fp: f64| -> u8 { (-fp.log2()).ceil() as u8 };
(-fp.log2()).ceil() as u8
};
match (z_opt, b_opt, fp_opt) { match (z_opt, b_opt, fp_opt) {
// All three given: use b and z, recompute fp conservatively. // All three given: use b and z, recompute fp conservatively.
@@ -198,7 +198,7 @@ pub fn run(args: IndexArgs) {
}; };
// ── Open or create the index ───────────────────────────────────────────── // ── Open or create the index ─────────────────────────────────────────────
if KmerIndex::exists(&output) { if KmerIndex::is_an_index(&output) {
if !args.force { if !args.force {
eprintln!( eprintln!(
"error: an index already exists at {} (use --force to overwrite it)", "error: an index already exists at {} (use --force to overwrite it)",
@@ -211,11 +211,20 @@ pub fn run(args: IndexArgs) {
eprintln!("error removing existing index: {e}"); eprintln!("error removing existing index: {e}");
std::process::exit(1); std::process::exit(1);
}); });
} else if output.exists() {
eprintln!(
"error: {} exists but is not an obikmer index, it cannot be deleted",
output.display()
);
std::process::exit(1);
} }
let n_bits = partitions_to_bits(args.common.partitions); let n_bits = partitions_to_bits(args.common.partitions);
let effective = 1usize << n_bits; let effective = 1usize << n_bits;
if effective != args.common.partitions { if effective != args.common.partitions {
info!("partitions: {} → {} (next power of 2)", args.common.partitions, effective); info!(
"partitions: {} → {} (next power of 2)",
args.common.partitions, effective
);
} }
let block_bits = block_size_to_bits(args.block_size); let block_bits = block_size_to_bits(args.block_size);
let config = IndexConfig { let config = IndexConfig {
@@ -227,7 +236,7 @@ pub fn run(args: IndexArgs) {
block_bits, block_bits,
}; };
let genome_info = args.label.as_ref().map(|label| { let genome_info = args.label.as_ref().map(|label| {
validate_label(label).unwrap_or_else(|e| { GenomeInfo::validate_label(label).unwrap_or_else(|e| {
eprintln!("error: --label: {e}"); eprintln!("error: --label: {e}");
std::process::exit(1); std::process::exit(1);
}); });
@@ -242,7 +251,6 @@ pub fn run(args: IndexArgs) {
std::process::exit(1); std::process::exit(1);
}); });
// ── Stage 1: scatter ───────────────────────────────────────────────────── // ── Stage 1: scatter ─────────────────────────────────────────────────────
if current_state(&idx) < IndexState::Scattered { if current_state(&idx) < IndexState::Scattered {
let n_workers = args.common.threads.max(1); let n_workers = args.common.threads.max(1);
@@ -272,16 +280,20 @@ pub fn run(args: IndexArgs) {
last_bases = p.position; last_bases = p.position;
let bp = p.position as f64; let bp = p.position as f64;
let (count_str, rate_str) = if bp >= 1e9 { let (count_str, rate_str) = if bp >= 1e9 {
(format!("{:.2} Gbp", bp / 1e9), format!("{:.0} Mbp/s", ema_rate / 1e6)) (
format!("{:.2} Gbp", bp / 1e9),
format!("{:.0} Mbp/s", ema_rate / 1e6),
)
} else { } else {
(format!("{:.0} Mbp", bp / 1e6), format!("{:.0} Mbp/s", ema_rate / 1e6)) (
format!("{:.0} Mbp", bp / 1e6),
format!("{:.0} Mbp/s", ema_rate / 1e6),
)
}; };
pb.set_message(format!("{count_str} {rate_str}")); pb.set_message(format!("{count_str} {rate_str}"));
}); });
router router.run().unwrap_or_else(|e| {
.run()
.unwrap_or_else(|e| {
eprintln!("error: {e}"); eprintln!("error: {e}");
std::process::exit(1); std::process::exit(1);
}); });
+2 -2
View File
@@ -1,6 +1,6 @@
use clap::Args; use clap::Args;
use obikindex::{GroupFilterParams, IndexMeta, MetaPred}; use obikindex::IndexMeta;
use obikindex::KmerFilter; use obikfilter::{GroupFilterParams, KmerFilter, MetaPred};
/// CLI args for ingroup/outgroup filtering — embeddable in any command via `#[command(flatten)]`. /// CLI args for ingroup/outgroup filtering — embeddable in any command via `#[command(flatten)]`.
#[derive(Args)] #[derive(Args)]
+3 -2
View File
@@ -1,6 +1,7 @@
use std::path::PathBuf; use std::path::PathBuf;
use obikindex::{validate_label, IndexBitsPerKmer, KmerIndex}; use obikindex::{GenomeInfo, KmerIndex};
use obikstats::IndexBitsPerKmer;
use tracing::info; use tracing::info;
pub(super) fn run_stats(index_path: &PathBuf) { pub(super) fn run_stats(index_path: &PathBuf) {
@@ -76,7 +77,7 @@ pub(super) fn run_rename(index_path: &PathBuf, spec: &str) {
std::process::exit(1); std::process::exit(1);
}); });
validate_label(&new_label).unwrap_or_else(|e| { GenomeInfo::validate_label(&new_label).unwrap_or_else(|e| {
eprintln!("error: --new-label: {e}"); eprintln!("error: --new-label: {e}");
std::process::exit(1); std::process::exit(1);
}); });
+24
View File
@@ -0,0 +1,24 @@
[package]
name = "obikmer2"
version = "1.2.2"
edition = "2024"
[[bin]]
name = "obikmer2"
path = "src/main.rs"
[dependencies]
obikseq = { path = "../obikseq" }
obiread = { path = "../obiread" }
obipipeline = { path = "../obipipeline" }
obisys = { path = "../obisys" }
obikindex = { path = "../obikindex", default-features = false }
obikindexer = { path = "../obikindexer" }
obikalgorithm = { path = "../obikalgorithm" }
clap = { version = "4", features = ["derive"] }
tracing = "0.1.44"
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
[features]
default = ["numa"]
numa = ["obisys/numa"]
+114
View File
@@ -0,0 +1,114 @@
use std::path::PathBuf;
use clap::Args;
use obiread::NucPage;
use obikseq::RoutableSuperKmer;
use obipipeline::Throttled;
// ── Shared arguments ──────────────────────────────────────────────────────────
#[derive(Args)]
pub struct CommonArgs {
/// Input files or directories (FASTA/FASTQ, optionally gzip-compressed).
/// If omitted, reads from stdin.
#[arg(num_args = 0..)]
pub inputs: Vec<String>,
/// k-mer size
#[arg(short, long, default_value_t = 31)]
pub kmer_size: usize,
/// Minimizer size
#[arg(short, long, default_value_t = 11)]
pub minimizer_size: usize,
/// Entropy threshold (k-mers with score ≤ theta are rejected)
#[arg(long, default_value_t = 0.7)]
pub theta: f64,
/// Maximum sub-word size for entropy computation
#[arg(long, default_value_t = 6)]
pub level_max: usize,
/// Number of partitions (rounded up to the next power of 2)
#[arg(short, long, default_value_t = 256)]
pub partitions: usize,
/// Number of worker threads
#[arg(
short = 'T',
long,
default_value_t = obisys::effective_parallelism()
)]
pub threads: usize,
/// Maximum number of input files open simultaneously.
/// Defaults to threads/4 (minimum 1). Keep below the number of workers
/// to ensure CPU workers are always available for the transform stage.
#[arg(long)]
pub max_open_files: Option<usize>,
}
/// Smallest `b` such that `2^b >= n` (i.e. `n.next_power_of_two().ilog2()`).
/// Minimum 1 (degenerate n=0 or n=1 → 1 partition).
pub fn partitions_to_bits(n: usize) -> usize {
n.max(1).next_power_of_two().trailing_zeros() as usize
}
/// Convert a block size (number of unitigs per block) to its `block_bits` exponent.
/// `block_size=1` → `block_bits=0` (one entry per unitig, O(1) random access).
pub fn block_size_to_bits(n: usize) -> u8 {
n.max(1).next_power_of_two().trailing_zeros() as u8
}
impl CommonArgs {
/// Validate k and m constraints. Exits on error.
pub fn validate(&self) {
let k = self.kmer_size;
let m = self.minimizer_size;
if k < 11 || k > 31 {
eprintln!("error: --kmer-size must be in [11, 31] (got {k})");
std::process::exit(1);
}
if k % 2 == 0 {
eprintln!("error: --kmer-size must be odd (got {k}); even k allows palindromic k-mers");
std::process::exit(1);
}
if m < 3 || m >= k {
eprintln!("error: --minimizer-size must be in [3, k−1] = [3, {}] (got {m})", k - 1);
std::process::exit(1);
}
if m % 2 == 0 {
eprintln!("error: --minimizer-size must be odd (got {m})");
std::process::exit(1);
}
}
pub fn effective_max_open(&self) -> usize {
self.max_open_files
.unwrap_or_else(|| (self.threads / 4).max(1))
.max(1)
}
pub fn seqfile_paths(&self) -> obiread::PathIter {
let paths: Vec<PathBuf> = if self.inputs.is_empty() {
vec![PathBuf::from("-")]
} else {
self.inputs.iter().map(PathBuf::from).collect()
};
obiread::PathIter::new(paths)
}
}
// ── Pipeline data carrier ─────────────────────────────────────────────────────
pub enum PipelineData {
Path(Throttled<PathBuf>),
NucPage(NucPage),
Batch(Vec<RoutableSuperKmer>),
}
unsafe impl Send for PipelineData {}
unsafe impl Sync for PipelineData {}
+363
View File
@@ -0,0 +1,363 @@
use std::path::PathBuf;
use std::time::Instant;
use clap::Args;
use obikalgorithm::Algorithm;
use obikindex::layer::IndexMode;
use obikindex::{GenomeInfo, IndexBuilder, IndexConfig, IndexState, KmerIndex};
use obikindexer::algorithms::counter::Counter;
use obikindexer::algorithms::dereplicator::Dereplicator;
use obikindexer::algorithms::layer_builder::LayerBuilder;
use obikindexer::algorithms::partitionner::PartitionRouter;
fn current_state(idx: &KmerIndex) -> IndexState {
idx.state().unwrap_or_else(|e| {
eprintln!("error reading index metadata: {e}");
std::process::exit(1);
})
}
fn parse_key_value(s: &str) -> Result<(String, String), String> {
let pos = s
.find('=')
.ok_or_else(|| format!("invalid key=value: no '=' in '{s}'"))?;
Ok((s[..pos].to_string(), s[pos + 1..].to_string()))
}
use obisys::{Progress, Reporter, Stage, progress_bar, spinner};
use tracing::info;
use crate::cli::{CommonArgs, block_size_to_bits, partitions_to_bits};
#[derive(Args)]
pub struct IndexArgs {
/// Output index directory
#[arg(short, long)]
pub output: PathBuf,
/// Overwrite output directory if it already exists
#[arg(long, default_value_t = false)]
pub force: bool,
/// Genome label (default: input filename without path/extension)
#[arg(long)]
pub label: Option<String>,
/// Genome categorical metadata as key=value pairs (repeatable)
#[arg(long = "meta", value_parser = parse_key_value)]
pub meta: Vec<(String, String)>,
/// Minimum kmer abundance (inclusive)
#[arg(long, default_value_t = 1)]
pub min_abundance: u32,
/// Maximum kmer abundance (inclusive)
#[arg(long)]
pub max_abundance: Option<u32>,
/// Store kmer counts in the index (default: set membership only)
#[arg(long, default_value_t = false)]
pub with_counts: bool,
/// Keep intermediate build files (dereplicated superkmers, mphf1, counts1)
#[arg(long, default_value_t = false)]
pub keep_intermediate: bool,
/// Use approximate (fingerprint-based) evidence instead of exact evidence.
/// False-positive rate per z-window: 1/2^(b·z).
#[arg(long, default_value_t = false)]
pub approx: bool,
/// Findere z parameter: number of consecutive k-mers that must all match.
/// Effective indexed k-mer size is kmer_size - z + 1.
#[arg(short = 'z', long, default_value = None)]
pub findere_z: Option<u8>,
/// Fingerprint bits per slot (b). FP per z-window = 1/2^(b·z).
#[arg(long, default_value = None)]
pub evidence_bits: Option<u8>,
/// Target false-positive rate per z-window (e.g. 0.01).
/// Used to derive missing b or z.
#[arg(long, default_value = None)]
pub fp: Option<f64>,
/// Block size for exact evidence `.idx` (number of unitigs per block).
/// Must be a power of two; rounded up if not. Default 1 = O(1) random access.
#[arg(long, default_value_t = 1)]
pub block_size: usize,
#[command(flatten)]
pub common: CommonArgs,
}
/// Resolve the (z, b, fp) triplet from the user-supplied subset.
///
/// Model: FP = 1/2^(b·z) ⟹ b·z = ⌈-log₂(fp)⌉
///
/// Rules when one value is missing (conservative = ceiling):
/// given z, b → fp = 1/2^(b·z)
/// given z, fp → b = ⌈-log₂(fp) / z⌉
/// given b, fp → z = ⌈-log₂(fp) / b⌉
/// given z only → b = 8 (default), fp derived
/// given b only → z = 1 (default), fp derived
/// given fp only → b = 8 (default), z derived
/// none given → z = 1, b = 8, fp = 1/256
pub(crate) fn resolve_approx_params(
z_opt: Option<u8>,
b_opt: Option<u8>,
fp_opt: Option<f64>,
) -> (u8, u8, f64) {
const DEFAULT_B: u8 = 8;
const DEFAULT_Z: u8 = 1;
let bits_needed = |fp: f64| -> u8 { (-fp.log2()).ceil() as u8 };
match (z_opt, b_opt, fp_opt) {
// All three given: use b and z, recompute fp conservatively.
(Some(z), Some(b), Some(_fp)) => {
let fp = 1.0_f64 / (1u64 << (b as u32 * z as u32)) as f64;
(z, b, fp)
}
// Two given, derive third.
(Some(z), Some(b), None) => {
let fp = 1.0_f64 / (1u64 << (b as u32 * z as u32)) as f64;
(z, b, fp)
}
(Some(z), None, Some(fp)) => {
let bz = (-fp.log2()).ceil() as u32;
let b = ((bz + z as u32 - 1) / z as u32).max(1) as u8;
let actual_fp = 1.0_f64 / (1u64 << (b as u32 * z as u32)) as f64;
(z, b, actual_fp)
}
(None, Some(b), Some(fp)) => {
let bz = (-fp.log2()).ceil() as u32;
let z = ((bz + b as u32 - 1) / b as u32).max(1) as u8;
let actual_fp = 1.0_f64 / (1u64 << (b as u32 * z as u32)) as f64;
(z, b, actual_fp)
}
// One given, apply defaults for the other.
(Some(z), None, None) => {
let b = DEFAULT_B;
let fp = 1.0_f64 / (1u64 << (b as u32 * z as u32)) as f64;
(z, b, fp)
}
(None, Some(b), None) => {
let z = DEFAULT_Z;
let fp = 1.0_f64 / (1u64 << (b as u32 * z as u32)) as f64;
(z, b, fp)
}
(None, None, Some(fp)) => {
let b = DEFAULT_B;
let z = ((bits_needed(fp) as u32 + b as u32 - 1) / b as u32).max(1) as u8;
let actual_fp = 1.0_f64 / (1u64 << (b as u32 * z as u32)) as f64;
(z, b, actual_fp)
}
// None given: defaults.
(None, None, None) => {
let b = DEFAULT_B;
let z = DEFAULT_Z;
let fp = 1.0_f64 / (1u64 << (b as u32 * z as u32)) as f64;
(z, b, fp)
}
}
}
pub fn run(args: IndexArgs) {
args.common.validate();
let output = args.output.clone();
let mut rep = Reporter::new();
// Locked for the whole build (including a possible --force removal +
// recreation below): a second `index` run resuming/overwriting the same
// output directory concurrently would otherwise corrupt it. Unlinking
// the lock file via --force's remove_dir_all is safe — the held file
// descriptor keeps the lock regardless of the directory entry.
let _lock = obisys::DirLock::acquire(&output).unwrap_or_else(|e| {
eprintln!("error locking output directory {}: {e}", output.display());
std::process::exit(1);
});
// ── Resolve evidence kind ────────────────────────────────────────────────
let (evidence, effective_kmer_size) = if args.approx {
let (z, b, fp) = resolve_approx_params(args.findere_z, args.evidence_bits, args.fp);
let k = args.common.kmer_size;
if z as usize >= k {
eprintln!(
"error: Findere z={z} must be < kmer-size={k} \
(effective kmer size k−z+1 = {} ≤ 0)",
k as isize - z as isize + 1
);
std::process::exit(1);
}
let s = k - z as usize + 1;
info!("approximate evidence: b={b}, z={z}, fp={fp:.2e}, indexed kmer size={s}");
(IndexMode::Approx { b, z }, s)
} else {
(IndexMode::Exact, args.common.kmer_size)
};
// ── Open or create the index ─────────────────────────────────────────────
if KmerIndex::is_an_index(&output) {
if !args.force {
eprintln!(
"error: an index already exists at {} (use --force to overwrite it)",
output.display()
);
std::process::exit(1);
}
info!("--force: removing existing index at {}", output.display());
std::fs::remove_dir_all(&output).unwrap_or_else(|e| {
eprintln!("error removing existing index: {e}");
std::process::exit(1);
});
} else if output.exists() {
eprintln!(
"error: {} exists but is not an obikmer index, it cannot be deleted",
output.display()
);
std::process::exit(1);
}
let n_bits = partitions_to_bits(args.common.partitions);
let effective = 1usize << n_bits;
if effective != args.common.partitions {
info!(
"partitions: {} → {} (next power of 2)",
args.common.partitions, effective
);
}
let block_bits = block_size_to_bits(args.block_size);
let config = IndexConfig {
kmer_size: effective_kmer_size,
minimizer_size: args.common.minimizer_size,
n_bits,
with_counts: args.with_counts,
evidence: evidence.clone(),
block_bits,
};
let genome_info = args.label.as_ref().map(|label| {
GenomeInfo::validate_label(label).unwrap_or_else(|e| {
eprintln!("error: --label: {e}");
std::process::exit(1);
});
let mut info = GenomeInfo::new(label.clone());
for (k, v) in &args.meta {
info.meta.insert(k.clone(), v.clone());
}
info
});
let idx = KmerIndex::create(&output, config, genome_info).unwrap_or_else(|e| {
eprintln!("error creating index: {e}");
std::process::exit(1);
});
// ── Stage 1: scatter ─────────────────────────────────────────────────────
if current_state(&idx) < IndexState::Scattered {
let n_workers = args.common.threads.max(1);
let max_open = args.common.effective_max_open();
let t = Stage::start("scatter");
let pb = spinner("scatter");
let mut ema_rate: f64 = 0.0;
let mut last_t = Instant::now();
let mut last_bases: u64 = 0;
const ALPHA: f64 = 0.15;
let mut router = PartitionRouter::new(&idx)
.level_max(args.common.level_max)
.theta(args.common.theta)
.workers(n_workers)
.max_open(max_open)
.files(args.common.seqfile_paths())
.on_progress(|p: Progress| {
let now = Instant::now();
let dt = now.duration_since(last_t).as_secs_f64();
if dt > 0.0 {
let instant = (p.position - last_bases) as f64 / dt;
ema_rate = ALPHA * instant + (1.0 - ALPHA) * ema_rate;
}
last_t = now;
last_bases = p.position;
let bp = p.position as f64;
let (count_str, rate_str) = if bp >= 1e9 {
(
format!("{:.2} Gbp", bp / 1e9),
format!("{:.0} Mbp/s", ema_rate / 1e6),
)
} else {
(
format!("{:.0} Mbp", bp / 1e6),
format!("{:.0} Mbp/s", ema_rate / 1e6),
)
};
pb.set_message(format!("{count_str} {rate_str}"));
});
router.run().unwrap_or_else(|e| {
eprintln!("error: {e}");
std::process::exit(1);
});
pb.finish_and_clear();
rep.push(t.stop());
drop(router); // ends the borrow of `idx` early — `PartitionRouter`'s `Drop` impl would otherwise extend it to the end of scope (`run()` already called `close()`, which marks scatter done, internally)
} else {
info!("scatter already done, skipping");
}
// ── Stage 2: dereplicate + count ─────────────────────────────────────────
if current_state(&idx) < IndexState::Counted {
let t = Stage::start("dereplicate");
let pb = progress_bar("dereplication", idx.n_partitions() as u64, "partitions");
Dereplicator::new(&idx)
.on_progress(|_: Progress| pb.inc(1))
.run()
.unwrap_or_else(|e| {
eprintln!("error: {e}");
std::process::exit(1);
});
pb.finish_and_clear();
rep.push(t.stop());
let t = Stage::start("count_kmer");
let pb = progress_bar("counting", idx.n_partitions() as u64, "partitions");
// `Counter::run` writes `spectrums/{label}.json` and marks count
// done (`count.done`) internally once every partition succeeds.
Counter::new(&idx)
.keep_partial(args.keep_intermediate)
.on_progress(|_: Progress| pb.inc(1))
.run()
.unwrap_or_else(|e| {
eprintln!("error: {e}");
std::process::exit(1);
});
pb.finish_and_clear();
rep.push(t.stop());
} else {
info!("dereplicate+count already done, skipping");
}
// ── Stage 3: build layered index ─────────────────────────────────────────
if current_state(&idx) < IndexState::Indexed {
let t = Stage::start("index");
let pb = progress_bar("index", idx.n_partitions() as u64, "partitions");
let total_kmers = LayerBuilder::new(&idx)
.min_abundance(args.min_abundance)
.max_abundance(args.max_abundance)
.keep_intermediate(args.keep_intermediate)
.on_progress(|_: Progress| pb.inc(1))
.run()
.unwrap_or_else(|e| {
eprintln!("error: {e}");
std::process::exit(1);
});
pb.finish_and_clear();
info!("done — {total_kmers} total kmers indexed");
rep.push(t.stop());
// `LayerBuilder::run` marks the index done (`index.done`) internally
// once every partition succeeds.
} else {
info!("index already built, skipping");
}
rep.print();
}
+1
View File
@@ -0,0 +1 @@
pub mod index;
+32
View File
@@ -0,0 +1,32 @@
mod cli;
mod cmd;
use clap::{Parser, Subcommand};
use tracing_subscriber::{EnvFilter, fmt};
#[derive(Parser)]
#[command(name = "obikmer2", about = "DNA k-mer tools", version)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Build the complete genome index (scatter → dereplicate → count → layered MPHF)
Index(cmd::index::IndexArgs),
}
fn main() {
fmt()
.with_env_filter(
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")),
)
.with_writer(std::io::stderr)
.init();
let cli = Cli::parse();
match cli.command {
Commands::Index(args) => cmd::index::run(args),
}
}
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "obikmerge"
version = "0.1.0"
edition = "2024"
[dependencies]
obikindex = { path = "../obikindex" }
obikindexer = { path = "../obikindexer" }
obikfilter = { path = "../obikfilter" }
obicompactvec = { path = "../obicompactvec" }
obiskio = { path = "../obiskio" }
obikseq = { path = "../obikseq" }
obipipeline = { path = "../obipipeline" }
obisys = { path = "../obisys" }
tracing = "0.1.44"
+17
View File
@@ -0,0 +1,17 @@
//! Merging multiple `obikindex::KmerIndex` sources into one: bootstrapping
//! from the first source, then merging each remaining source partition by
//! partition (de Bruijn graph union + column fill). A maintenance/
//! transformation operation on already-built indexes, not part of the
//! `Index { Partition { Layer } }` data model itself — kept out of
//! `obikindex` so that crate doesn't grow this algorithm's own
//! dependencies (`obipipeline`, `obidebruinj` via `obikindexer`).
//!
//! [`merge_layer`] holds the per-partition merge primitive
//! (`merge_partition`, `SrcLayerData`) — the latter is also reused by
//! `obikrebuild::rebuild_layer`, hence its `pub` visibility here.
mod merge;
mod merge_layer;
pub use merge::*;
pub use merge_layer::{MergeMode, SrcLayerData};
@@ -3,19 +3,20 @@ use std::fs;
use std::io; use std::io;
use std::path::Path; use std::path::Path;
use crate::index::builder::IndexBuilder; use obikindex::IndexBuilder;
use obisys::{Reporter, Stage, progress_bar, spinner}; use obisys::{Reporter, Stage, progress_bar, spinner};
use tracing::{debug, info}; use tracing::{debug, info};
use crate::layer::IndexMode; use obikindex::layer::IndexMode;
use crate::index::error::{OKIError, OKIResult}; use obikindex::{OKIError, OKIResult};
use crate::index::kmer_index::KmerIndex; use obikindex::KmerIndex;
use crate::index::meta::{GenomeInfo, IndexMeta}; use obikindex::{GenomeInfo, IndexMeta};
use crate::index::state::IndexState; use obikindex::IndexState;
use obisys::PartitionRunner;
pub use crate::index::merge_layer::MergeMode; pub use crate::merge_layer::MergeMode;
// ── per-partition diagnostic record ────────────────────────────────────────── // ── per-partition diagnostic record ──────────────────────────────────────────
@@ -229,7 +230,7 @@ impl KmerIndex {
let srcs = &srcs; let srcs = &srcs;
let evidence = &evidence; let evidence = &evidence;
let runner = crate::index::numa::PartitionRunner::new(); let runner = PartitionRunner::new();
let mut part_stats: Vec<PartStat> = Vec::with_capacity(n_partitions); let mut part_stats: Vec<PartStat> = Vec::with_capacity(n_partitions);
runner runner
@@ -20,16 +20,18 @@ use tracing::debug;
use obicompactvec::{PersistentBitMatrixBuilder, PersistentCompactIntMatrixBuilder}; use obicompactvec::{PersistentBitMatrixBuilder, PersistentCompactIntMatrixBuilder};
use obikseq::CanonicalKmer; use obikseq::CanonicalKmer;
use crate::layer::{IndexMode, TypedLayer, LayeredMap, MphfOnly, layer_dir}; use obikindex::layer::{IndexMode, TypedLayer, LayeredMap, MphfOnly};
use obiskio::{SKError, SKResult, UnitigFileReader}; use obikindex::layer::utils::layer_dir;
use obiskio::UnitigFileReader;
use obikindex::{OKIError, OKIResult};
use crate::index::common::{ColBuilder, load_meta, olm_to_sk}; use obikindex::{ColBuilder, load_meta};
use crate::index::graph_pipeline::{build_graph, materialize_layer}; use obikindexer::{build_graph, materialize_layer};
use crate::index::kmer_index::KmerIndex; use obikindex::KmerIndex;
mod src_layer; mod src_layer;
pub(crate) use src_layer::SrcLayerData; pub use src_layer::SrcLayerData;
// ── MergeMode ───────────────────────────────────────────────────────────────── // ── MergeMode ─────────────────────────────────────────────────────────────────
@@ -216,15 +218,14 @@ impl KmerIndex {
n_dst_genomes: usize, n_dst_genomes: usize,
block_bits: u8, block_bits: u8,
evidence: &IndexMode, evidence: &IndexMode,
) -> SKResult<usize> { ) -> OKIResult<usize> {
let dst_index_dir = self.index_dir(i); let dst_index_dir = self.index_dir(i);
if !dst_index_dir.exists() { if !dst_index_dir.exists() {
return Ok(0); return Ok(0);
} }
load_meta(&dst_index_dir, "merge")?; // ensure meta.json exists before LayeredMap::open load_meta(&dst_index_dir)?; // ensure meta.json exists before LayeredMap::open
let dst_map = let dst_map = Arc::new(LayeredMap::<()>::open(&dst_index_dir)?);
Arc::new(LayeredMap::<()>::open(&dst_index_dir).map_err(|e| olm_to_sk(e, "merge"))?);
let n_dst_layers = dst_map.n_layers(); let n_dst_layers = dst_map.n_layers();
let n_src_total: usize = sources.iter().map(|(_, n)| *n).sum(); let n_src_total: usize = sources.iter().map(|(_, n)| *n).sum();
@@ -235,8 +236,7 @@ impl KmerIndex {
TypedLayer::<()>::init_presence_matrix( TypedLayer::<()>::init_presence_matrix(
&layer_dir(&dst_index_dir, l), &layer_dir(&dst_index_dir, l),
dst_map.layer(l).n(), dst_map.layer(l).n(),
) )?;
.map_err(|e| olm_to_sk(e, "merge"))?;
} }
} }
@@ -254,7 +254,7 @@ impl KmerIndex {
if !src_index_dir.exists() { if !src_index_dir.exists() {
continue; continue;
} }
let src_meta = load_meta(&src_index_dir, "merge")?; let src_meta = load_meta(&src_index_dir)?;
for l in 0..src_meta.n_layers { for l in 0..src_meta.n_layers {
let p = layer_dir(&src_index_dir, l).join("unitigs.bin"); let p = layer_dir(&src_index_dir, l).join("unitigs.bin");
if p.exists() { if p.exists() {
@@ -277,7 +277,7 @@ impl KmerIndex {
let g = build_graph( let g = build_graph(
unitig_paths.into_iter(), unitig_paths.into_iter(),
move |path: PathBuf, emit: &mut dyn FnMut(Vec<CanonicalKmer>)| -> SKResult<()> { move |path: PathBuf, emit: &mut dyn FnMut(Vec<CanonicalKmer>)| -> OKIResult<()> {
let reader = UnitigFileReader::open_sequential(&path)?; let reader = UnitigFileReader::open_sequential(&path)?;
let mut batch: Vec<CanonicalKmer> = Vec::with_capacity(BATCH); let mut batch: Vec<CanonicalKmer> = Vec::with_capacity(BATCH);
for (kmer, _, _) in reader.iter_indexed_canonical_kmers() { for (kmer, _, _) in reader.iter_indexed_canonical_kmers() {
@@ -318,9 +318,7 @@ impl KmerIndex {
let t_open = std::time::Instant::now(); let t_open = std::time::Instant::now();
let new_mphf: Option<Arc<MphfOnly>> = if any_new { let new_mphf: Option<Arc<MphfOnly>> = if any_new {
Some(Arc::new( Some(Arc::new(MphfOnly::open(&new_layer_dir)?))
MphfOnly::open(&new_layer_dir).map_err(|e| olm_to_sk(e, "merge"))?,
))
} else { } else {
None None
}; };
@@ -340,13 +338,13 @@ impl KmerIndex {
MergeMode::Count => new_layer_dir.join("counts"), MergeMode::Count => new_layer_dir.join("counts"),
}; };
fs::create_dir_all(&data_dir)?; fs::create_dir_all(&data_dir)?;
let mut mb = MatrixBuilder::new(mode, n_new, &data_dir).map_err(SKError::Io)?; let mut mb = MatrixBuilder::new(mode, n_new, &data_dir).map_err(OKIError::Io)?;
for _ in 0..n_dst_genomes { for _ in 0..n_dst_genomes {
mb.add_absent_col().map_err(SKError::Io)?; mb.add_absent_col().map_err(OKIError::Io)?;
} }
let cols = (0..n_src_total) let cols = (0..n_src_total)
.map(|_| mb.add_col().map_err(SKError::Io)) .map(|_| mb.add_col().map_err(OKIError::Io))
.collect::<SKResult<Vec<_>>>()?; .collect::<OKIResult<Vec<_>>>()?;
(cols, Some(mb)) (cols, Some(mb))
} else { } else {
(vec![], None) (vec![], None)
@@ -365,10 +363,10 @@ impl KmerIndex {
MergeMode::Presence => layer_dir.join("presence"), MergeMode::Presence => layer_dir.join("presence"),
MergeMode::Count => layer_dir.join("counts"), MergeMode::Count => layer_dir.join("counts"),
}; };
let mut mb = MatrixBuilder::resume(mode, &data_dir).map_err(SKError::Io)?; let mut mb = MatrixBuilder::resume(mode, &data_dir).map_err(OKIError::Io)?;
let cols = (0..n_src_total) let cols = (0..n_src_total)
.map(|_| mb.add_col().map_err(SKError::Io)) .map(|_| mb.add_col().map_err(OKIError::Io))
.collect::<SKResult<Vec<_>>>()?; .collect::<OKIResult<Vec<_>>>()?;
exist_mbs.push(mb); exist_mbs.push(mb);
exist_builders.push(cols); exist_builders.push(cols);
} }
@@ -391,7 +389,7 @@ impl KmerIndex {
col_offset += src_n; col_offset += src_n;
continue; continue;
} }
let src_meta = load_meta(&src_index_dir, "merge")?; let src_meta = load_meta(&src_index_dir)?;
for l in 0..src_meta.n_layers { for l in 0..src_meta.n_layers {
let src_layer_dir = layer_dir(&src_index_dir, l); let src_layer_dir = layer_dir(&src_index_dir, l);
if src_layer_dir.join("unitigs.bin").exists() { if src_layer_dir.join("unitigs.bin").exists() {
@@ -552,7 +550,7 @@ impl KmerIndex {
.into_inner() .into_inner()
.unwrap_or_else(|e| e.into_inner()) .unwrap_or_else(|e| e.into_inner())
{ {
return Err(SKError::InvalidData { return Err(OKIError::InvalidData {
context: "merge pass2", context: "merge pass2",
detail: msg, detail: msg,
}); });
@@ -568,7 +566,7 @@ impl KmerIndex {
.unwrap_or_else(|e| e.into_inner()) .unwrap_or_else(|e| e.into_inner())
.close()?; .close()?;
} }
mb.close().map_err(SKError::Io)?; mb.close().map_err(OKIError::Io)?;
} }
for b in new_locked { for b in new_locked {
@@ -579,13 +577,12 @@ impl KmerIndex {
.close()?; .close()?;
} }
if let Some(mb) = new_mb { if let Some(mb) = new_mb {
mb.close().map_err(SKError::Io)?; mb.close().map_err(OKIError::Io)?;
let mut part_meta = self.partition_meta(i)?; let mut part_meta = self.partition_meta(i)?;
part_meta.n_layers = new_layer_idx + 1; part_meta.n_layers = new_layer_idx + 1;
part_meta part_meta
.save(&dst_index_dir) .save(&dst_index_dir)?;
.map_err(|e| olm_to_sk(e, "merge"))?;
} }
debug!( debug!(
@@ -2,44 +2,43 @@ use std::path::Path;
use obicompactvec::{MatrixGroupOps, PersistentBitMatrix, PersistentCompactIntMatrix}; use obicompactvec::{MatrixGroupOps, PersistentBitMatrix, PersistentCompactIntMatrix};
use obikseq::CanonicalKmer; use obikseq::CanonicalKmer;
use crate::layer::MphfOnly; use obikindex::layer::MphfOnly;
use obiskio::{SKError, SKResult}; use obikindex::{OKIError, OKIResult};
use crate::index::common::olm_to_sk;
use super::MergeMode; use super::MergeMode;
// ── SrcLayerData — opened source matrix for pass-2 lookup ───────────────────── // ── SrcLayerData — opened source matrix for pass-2 lookup ─────────────────────
pub(crate) enum SrcLayerData { pub enum SrcLayerData {
Presence(MphfOnly, PersistentBitMatrix), Presence(MphfOnly, PersistentBitMatrix),
Count(MphfOnly, PersistentCompactIntMatrix), Count(MphfOnly, PersistentCompactIntMatrix),
} }
impl SrcLayerData { impl SrcLayerData {
pub(crate) fn open(layer_dir: &Path, merge_mode: MergeMode) -> SKResult<Self> { pub fn open(layer_dir: &Path, merge_mode: MergeMode) -> OKIResult<Self> {
let counts_dir = layer_dir.join("counts"); let counts_dir = layer_dir.join("counts");
match merge_mode { match merge_mode {
MergeMode::Presence => { MergeMode::Presence => {
if counts_dir.exists() && !layer_dir.join("presence").exists() { if counts_dir.exists() && !layer_dir.join("presence").exists() {
let mphf = MphfOnly::open(layer_dir).map_err(|e| olm_to_sk(e, "merge"))?; let mphf = MphfOnly::open(layer_dir)?;
let mat = PersistentCompactIntMatrix::open(layer_dir).map_err(SKError::Io)?; let mat = PersistentCompactIntMatrix::open(layer_dir).map_err(OKIError::Io)?;
Ok(SrcLayerData::Count(mphf, mat)) Ok(SrcLayerData::Count(mphf, mat))
} else { } else {
// presence dir exists, or neither exists → Implicit handled by open() // presence dir exists, or neither exists → Implicit handled by open()
let mphf = MphfOnly::open(layer_dir).map_err(|e| olm_to_sk(e, "merge"))?; let mphf = MphfOnly::open(layer_dir)?;
let mat = PersistentBitMatrix::open(layer_dir).map_err(SKError::Io)?; let mat = PersistentBitMatrix::open(layer_dir).map_err(OKIError::Io)?;
Ok(SrcLayerData::Presence(mphf, mat)) Ok(SrcLayerData::Presence(mphf, mat))
} }
} }
MergeMode::Count => { MergeMode::Count => {
let mphf = MphfOnly::open(layer_dir).map_err(|e| olm_to_sk(e, "merge"))?; let mphf = MphfOnly::open(layer_dir)?;
if counts_dir.exists() { if counts_dir.exists() {
let mat = PersistentCompactIntMatrix::open(layer_dir).map_err(SKError::Io)?; let mat = PersistentCompactIntMatrix::open(layer_dir).map_err(OKIError::Io)?;
Ok(SrcLayerData::Count(mphf, mat)) Ok(SrcLayerData::Count(mphf, mat))
} else { } else {
// No counts → treat as implicit presence (all 1s) // No counts → treat as implicit presence (all 1s)
let mat = PersistentBitMatrix::open(layer_dir).map_err(SKError::Io)?; let mat = PersistentBitMatrix::open(layer_dir).map_err(OKIError::Io)?;
Ok(SrcLayerData::Presence(mphf, mat)) Ok(SrcLayerData::Presence(mphf, mat))
} }
} }
@@ -58,7 +57,7 @@ impl SrcLayerData {
buf buf
} }
pub(crate) fn n_slots(&self) -> usize { pub fn n_slots(&self) -> usize {
match self { match self {
SrcLayerData::Presence(_, mat) => mat.n(), SrcLayerData::Presence(_, mat) => mat.n(),
SrcLayerData::Count(_, mat) => mat.n(), SrcLayerData::Count(_, mat) => mat.n(),
@@ -67,7 +66,7 @@ impl SrcLayerData {
/// MPHF lookup: returns the slot index for `kmer` (kmer must be in the domain). /// MPHF lookup: returns the slot index for `kmer` (kmer must be in the domain).
#[inline] #[inline]
pub(crate) fn slot(&self, kmer: CanonicalKmer) -> usize { pub fn slot(&self, kmer: CanonicalKmer) -> usize {
match self { match self {
SrcLayerData::Presence(mphf, _) => mphf.index(kmer), SrcLayerData::Presence(mphf, _) => mphf.index(kmer),
SrcLayerData::Count(mphf, _) => mphf.index(kmer), SrcLayerData::Count(mphf, _) => mphf.index(kmer),
@@ -76,7 +75,7 @@ impl SrcLayerData {
/// Row lookup by slot index, bypassing the MPHF. /// Row lookup by slot index, bypassing the MPHF.
#[inline] #[inline]
pub(crate) fn fill_row_by_slot(&self, slot: usize, n_genomes: usize) -> Vec<u32> { pub fn fill_row_by_slot(&self, slot: usize, n_genomes: usize) -> Vec<u32> {
let mut buf = vec![0u32; n_genomes]; let mut buf = vec![0u32; n_genomes];
match self { match self {
SrcLayerData::Presence(_, mat) => mat.fill_row(slot, &mut buf), SrcLayerData::Presence(_, mat) => mat.fill_row(slot, &mut buf),
@@ -86,7 +85,7 @@ impl SrcLayerData {
} }
/// Call `f` with a reference to the underlying matrix as `&dyn MatrixGroupOps`. /// Call `f` with a reference to the underlying matrix as `&dyn MatrixGroupOps`.
pub(crate) fn with_matrix<R>(&self, f: impl FnOnce(&dyn MatrixGroupOps) -> R) -> R { pub fn with_matrix<R>(&self, f: impl FnOnce(&dyn MatrixGroupOps) -> R) -> R {
match self { match self {
SrcLayerData::Presence(_, mat) => f(mat), SrcLayerData::Presence(_, mat) => f(mat),
SrcLayerData::Count(_, mat) => f(mat), SrcLayerData::Count(_, mat) => f(mat),
+1
View File
@@ -9,6 +9,7 @@ obikseq = { path = "../obikseq" }
obiskio = { path = "../obiskio" } obiskio = { path = "../obiskio" }
obisys = { path = "../obisys" } obisys = { path = "../obisys" }
obicompactvec = { path = "../obicompactvec" } obicompactvec = { path = "../obicompactvec" }
obikidxcache = { path = "../obikidxcache" }
obiskbuilder = { path = "../obiskbuilder" } obiskbuilder = { path = "../obiskbuilder" }
obipipeline = { path = "../obipipeline" } obipipeline = { path = "../obipipeline" }
memmap2 = "0.9" memmap2 = "0.9"
@@ -1,10 +1,10 @@
use ndarray::Array2; use ndarray::Array2;
use obicompactvec::traits::{BitPartials, CountPartials}; use obicompactvec::traits::{BitPartials, CountPartials};
use crate::layer::LayeredStore; use obikidxcache::LayeredStore;
use rayon::prelude::*; use rayon::prelude::*;
use crate::index::error::{OKIError, OKIResult}; use obikindex::{OKIError, OKIResult};
use crate::index::kmer_index::KmerIndex; use obikindex::KmerIndex;
// ── Public API ──────────────────────────────────────────────────────────────── // ── Public API ────────────────────────────────────────────────────────────────
+3
View File
@@ -9,6 +9,9 @@
//! incrementally. //! incrementally.
mod cardcomp; mod cardcomp;
mod distance;
mod matrix_store;
pub mod siblings; pub mod siblings;
pub use cardcomp::{cardinality_transition_probs, composition_transition_probs, pairwise_cost_matrix}; pub use cardcomp::{cardinality_transition_probs, composition_transition_probs, pairwise_cost_matrix};
pub use distance::{DistanceMetric, DistanceOutput};
@@ -1,46 +1,47 @@
use obikindex::OKIResult;
use obikindex::layer::open_data;
use obikidxcache::LayeredStore;
use obicompactvec::{PersistentBitMatrix, PersistentCompactIntMatrix}; use obicompactvec::{PersistentBitMatrix, PersistentCompactIntMatrix};
use crate::layer::{LayeredStore, open_data};
use obiskio::SKResult;
use crate::index::common::{load_meta, olm_to_sk}; use obikindex::load_meta;
use crate::index::kmer_index::KmerIndex; use obikindex::KmerIndex;
impl KmerIndex { impl KmerIndex {
/// Open all count matrices for partition `part`, one per layer. /// Open all count matrices for partition `part`, one per layer.
/// Layers without a `counts/` directory are skipped. /// Layers without a `counts/` directory are skipped.
pub fn count_store(&self, part: usize) -> SKResult<LayeredStore<PersistentCompactIntMatrix>> { pub fn count_store(&self, part: usize) -> OKIResult<LayeredStore<PersistentCompactIntMatrix>> {
let index_dir = self.index_dir(part); let index_dir = self.index_dir(part);
if !index_dir.exists() { if !index_dir.exists() {
return Ok(LayeredStore::new(vec![])); return Ok(LayeredStore::new(vec![]));
} }
let n_layers = load_meta(&index_dir, "distance")?.n_layers; let n_layers = self.n_layers();
let matrices = (0..n_layers) let matrices = (0..n_layers)
.filter_map(|l| { .filter_map(|l| {
self.layer_dir(part, l) self.layer_dir(part, l)
.join("counts") .join("counts")
.exists() .exists()
.then(|| open_data(&index_dir, l).map_err(|e| olm_to_sk(e, "distance"))) .then(|| open_data(&index_dir, l))
}) })
.collect::<SKResult<Vec<_>>>()?; .collect::<OKIResult<Vec<_>>>()?;
Ok(LayeredStore::new(matrices)) Ok(LayeredStore::new(matrices))
} }
/// Open all presence matrices for partition `part`, one per layer. /// Open all presence matrices for partition `part`, one per layer.
/// Layers without a `presence/` directory are skipped. /// Layers without a `presence/` directory are skipped.
pub fn presence_store(&self, part: usize) -> SKResult<LayeredStore<PersistentBitMatrix>> { pub fn presence_store(&self, part: usize) -> OKIResult<LayeredStore<PersistentBitMatrix>> {
let index_dir = self.index_dir(part); let index_dir = self.index_dir(part);
if !index_dir.exists() { if !index_dir.exists() {
return Ok(LayeredStore::new(vec![])); return Ok(LayeredStore::new(vec![]));
} }
let n_layers = load_meta(&index_dir, "distance")?.n_layers; let n_layers = load_meta(&index_dir)?.n_layers;
let matrices = (0..n_layers) let matrices = (0..n_layers)
.filter_map(|l| { .filter_map(|l| {
self.layer_dir(part, l) self.layer_dir(part, l)
.join("presence") .join("presence")
.exists() .exists()
.then(|| open_data(&index_dir, l).map_err(|e| olm_to_sk(e, "distance"))) .then(|| open_data(&index_dir, l))
}) })
.collect::<SKResult<Vec<_>>>()?; .collect::<OKIResult<Vec<_>>>()?;
Ok(LayeredStore::new(matrices)) Ok(LayeredStore::new(matrices))
} }
} }
+2 -2
View File
@@ -14,7 +14,7 @@ use obikindex::OKIResult;
use super::cache::PartitionCache; use super::cache::PartitionCache;
use super::helpers::{central_base, is_minorant}; use super::helpers::{central_base, is_minorant};
use super::{ANNEX_FILE_NAME, FamilyMask, SiblingAnnexBuilder, olm_to_ok}; use super::{ANNEX_FILE_NAME, FamilyMask, SiblingAnnexBuilder};
// ── obipipeline data types ───────────────────────────────────────────────── // ── obipipeline data types ─────────────────────────────────────────────────
@@ -125,7 +125,7 @@ fn build_layer_sibling_annex(
l: usize, l: usize,
cache: &Arc<PartitionCache>, cache: &Arc<PartitionCache>,
) -> OKIResult<u64> { ) -> OKIResult<u64> {
let mphf = MphfLayer::open(layer_dir).map_err(olm_to_ok)?; let mphf = MphfLayer::open(layer_dir)?;
let k = index.kmer_size(); let k = index.kmer_size();
let n = mphf.n(); let n = mphf.n();
+3 -3
View File
@@ -65,7 +65,7 @@ use obikindex::layer::KmerLayer;
use super::cache::PartitionCache; use super::cache::PartitionCache;
use super::helpers::central_base; use super::helpers::central_base;
use super::iter::{SiblingEntry, SiblingLayerExt}; use super::iter::{SiblingEntry, SiblingLayerExt};
use super::{ANNEX_FILE_NAME, FamilyMask, SiblingAnnex, olm_to_ok}; use super::{ANNEX_FILE_NAME, FamilyMask, SiblingAnnex};
/// Families per batch — see the module docs for the memory-vs-per-partition- /// Families per batch — see the module docs for the memory-vs-per-partition-
/// density trade-off this picks a point on. At ~90 genomes and a few /// density trade-off this picks a point on. At ~90 genomes and a few
@@ -194,10 +194,10 @@ pub(super) fn scan_layer_families(
let index_dir = layer_dir let index_dir = layer_dir
.parent() .parent()
.expect("layer_dir has a parent index dir"); .expect("layer_dir has a parent index dir");
let meta = PartitionMeta::load(index_dir).map_err(olm_to_ok)?; let meta = PartitionMeta::load(index_dir)?;
let annex = Arc::new(SiblingAnnex::open(&layer_dir.join(ANNEX_FILE_NAME))?); let annex = Arc::new(SiblingAnnex::open(&layer_dir.join(ANNEX_FILE_NAME))?);
let mat = KmerLayer::open(layer_dir, &meta.mode, with_counts).map_err(olm_to_ok)?; let mat = KmerLayer::open(layer_dir, &meta.mode, with_counts)?;
let n_cols = mat.n_cols().min(n_genomes); let n_cols = mat.n_cols().min(n_genomes);
let ctx = Arc::new(LayerCtx { let ctx = Arc::new(LayerCtx {
-11
View File
@@ -71,15 +71,4 @@ pub(crate) use siblingannex::{FamilyMask, SiblingAnnex, SiblingAnnexBuilder};
pub use stats::{SiblingAnnexStats, SiblingStatsExt}; pub use stats::{SiblingAnnexStats, SiblingStatsExt};
pub use subsample::EntropyBias; pub use subsample::EntropyBias;
use obikindex::layer::OLMError;
use obikindex::OKIError;
pub(super) const ANNEX_FILE_NAME: &str = "siblings.psib"; pub(super) const ANNEX_FILE_NAME: &str = "siblings.psib";
pub(super) fn olm_to_ok(e: OLMError) -> OKIError {
match e {
OLMError::Io(e) => OKIError::Io(e),
other => OKIError::InvalidInput(format!("layered-map error: {other}")),
}
}
+7
View File
@@ -0,0 +1,7 @@
[package]
name = "obikquery"
version = "0.1.0"
edition = "2024"
[dependencies]
obikindex = { path = "../obikindex" }
+10
View File
@@ -0,0 +1,10 @@
//! Query-side operations on an `obikindex::KmerIndex`: staging ground for
//! code currently living in `obikindex::index`'s query path, to be migrated
//! here to lighten that crate — see `DevDocMD/` for the rationale. Kept as
//! a separate crate — not a module of `obikindex` — so the dependency runs
//! one way only (query code depends on the data model, never the reverse),
//! same pattern as `obikindexer` for the build side.
mod query_layer;
pub use query_layer::{KmerDesc, QueryHit, QueryStats};
@@ -3,20 +3,10 @@ use std::path::Path;
use obicompactvec::{PersistentBitMatrix, PersistentCompactIntMatrix}; use obicompactvec::{PersistentBitMatrix, PersistentCompactIntMatrix};
use obikseq::CanonicalKmer; use obikseq::CanonicalKmer;
use crate::layer::{MphfLayer, OLMError}; use obikindex::layer::MphfLayer;
use obiskio::{SKError, SKResult}; use obikindex::{OKIError, OKIResult};
use crate::index::kmer_index::KmerIndex; use obikindex::KmerIndex;
fn olm_to_sk(e: OLMError) -> SKError {
match e {
OLMError::Io(io_err) => SKError::Io(io_err),
other => SKError::InvalidData {
context: "query",
detail: other.to_string(),
},
}
}
// ── per-layer query handle ──────────────────────────────────────────────────── // ── per-layer query handle ────────────────────────────────────────────────────
@@ -26,21 +16,21 @@ enum QueryLayer {
} }
impl QueryLayer { impl QueryLayer {
fn open(layer_dir: &Path, with_counts: bool) -> SKResult<Self> { fn open(layer_dir: &Path, with_counts: bool) -> OKIResult<Self> {
let mphf = MphfLayer::open(layer_dir).map_err(olm_to_sk)?; let mphf = MphfLayer::open(layer_dir)?;
let counts_dir = layer_dir.join("counts"); let counts_dir = layer_dir.join("counts");
let presence_dir = layer_dir.join("presence"); let presence_dir = layer_dir.join("presence");
if with_counts && counts_dir.exists() { if with_counts && counts_dir.exists() {
let mat = PersistentCompactIntMatrix::open(layer_dir).map_err(SKError::Io)?; let mat = PersistentCompactIntMatrix::open(layer_dir).map_err(OKIError::Io)?;
Ok(QueryLayer::Count(mphf, mat)) Ok(QueryLayer::Count(mphf, mat))
} else if presence_dir.exists() || !counts_dir.exists() { } else if presence_dir.exists() || !counts_dir.exists() {
// presence mode, or no matrix at all → Implicit handled inside open() // presence mode, or no matrix at all → Implicit handled inside open()
let mat = PersistentBitMatrix::open(layer_dir).map_err(SKError::Io)?; let mat = PersistentBitMatrix::open(layer_dir).map_err(OKIError::Io)?;
Ok(QueryLayer::Presence(mphf, mat)) Ok(QueryLayer::Presence(mphf, mat))
} else { } else {
// counts exist but not presence — count layer, no presence requested // counts exist but not presence — count layer, no presence requested
let mat = PersistentCompactIntMatrix::open(layer_dir).map_err(SKError::Io)?; let mat = PersistentCompactIntMatrix::open(layer_dir).map_err(OKIError::Io)?;
Ok(QueryLayer::Count(mphf, mat)) Ok(QueryLayer::Count(mphf, mat))
} }
} }
@@ -168,7 +158,7 @@ impl KmerIndex {
n_genomes: usize, n_genomes: usize,
with_counts: bool, with_counts: bool,
mut on_event: F, mut on_event: F,
) -> SKResult<QueryStats> ) -> OKIResult<QueryStats>
where where
F: FnMut(QueryHit), F: FnMut(QueryHit),
{ {
@@ -186,7 +176,7 @@ impl KmerIndex {
let meta = self.partition_meta(part_idx)?; let meta = self.partition_meta(part_idx)?;
let layers: Vec<QueryLayer> = (0..meta.n_layers) let layers: Vec<QueryLayer> = (0..meta.n_layers)
.map(|i| QueryLayer::open(&self.layer_dir(part_idx, i), with_counts)) .map(|i| QueryLayer::open(&self.layer_dir(part_idx, i), with_counts))
.collect::<SKResult<_>>()?; .collect::<OKIResult<_>>()?;
// ── Stage 1: MPHF-only pass, bucket hits by (layer_idx, slot) ──────── // ── Stage 1: MPHF-only pass, bucket hits by (layer_idx, slot) ────────
let mut by_layer: Vec<HashMap<usize, &Vec<KmerDesc>>> = let mut by_layer: Vec<HashMap<usize, &Vec<KmerDesc>>> =
@@ -1,5 +1,5 @@
use super::*; use super::*;
use crate::index::meta::IndexConfig; use obikindex::IndexConfig;
// ── QueryStats::AddAssign ─────────────────────────────────────────────────── // ── QueryStats::AddAssign ───────────────────────────────────────────────────
@@ -50,7 +50,7 @@ fn query_partition_with_missing_index_dir_returns_default_stats() {
minimizer_size: 9, minimizer_size: 9,
n_bits: 2, n_bits: 2,
with_counts: false, with_counts: false,
evidence: crate::layer::IndexMode::Exact, evidence: obikindex::layer::IndexMode::Exact,
block_bits: 0, block_bits: 0,
}; };
let index = KmerIndex::create(tmp.path().join("idx"), config, None).expect("create index"); let index = KmerIndex::create(tmp.path().join("idx"), config, None).expect("create index");
@@ -78,7 +78,7 @@ fn query_partition_with_empty_kmers_is_a_noop() {
minimizer_size: 9, minimizer_size: 9,
n_bits: 2, n_bits: 2,
with_counts: false, with_counts: false,
evidence: crate::layer::IndexMode::Exact, evidence: obikindex::layer::IndexMode::Exact,
block_bits: 0, block_bits: 0,
}; };
let index = KmerIndex::create(tmp.path().join("idx"), config, None).expect("create index"); let index = KmerIndex::create(tmp.path().join("idx"), config, None).expect("create index");
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "obikrebuild"
version = "0.1.0"
edition = "2024"
[dependencies]
obikindex = { path = "../obikindex" }
obikindexer = { path = "../obikindexer" }
obikfilter = { path = "../obikfilter" }
obikmerge = { path = "../obikmerge" }
obicompactvec = { path = "../obicompactvec" }
obiskio = { path = "../obiskio" }
obikseq = { path = "../obikseq" }
obidebruinj = { path = "../obidebruinj" }
obisys = { path = "../obisys" }
tracing = "0.1.44"
+12
View File
@@ -0,0 +1,12 @@
//! Rebuilding an `obikindex::KmerIndex`: compacting a source index into a
//! new single-layer index while applying k-mer filters (`rebuild`), and
//! reindexing an existing index's evidence representation in place
//! (`reindex`, `Exact`/`Approx`/`Hybrid`). Both are maintenance/
//! transformation operations on an already-built index, not part of the
//! `Index { Partition { Layer } }` data model itself — kept out of
//! `obikindex` so that crate doesn't grow every algorithm's own
//! dependencies (`obikfilter`, `obikmerge`, `obikindexer`).
mod rebuild;
mod rebuild_layer;
mod reindex;
@@ -1,13 +1,15 @@
use std::path::Path; use std::path::Path;
use crate::index::builder::IndexBuilder; use obikindex::IndexBuilder;
use crate::index::{KmerFilter, MergeMode}; use obikfilter::KmerFilter;
use obikmerge::MergeMode;
use obisys::{Reporter, Stage, progress_bar}; use obisys::{Reporter, Stage, progress_bar};
use tracing::info; use tracing::info;
use crate::index::error::{OKIError, OKIResult}; use obikindex::{OKIError, OKIResult};
use crate::index::kmer_index::KmerIndex; use obikindex::KmerIndex;
use crate::index::state::IndexState; use obikindex::IndexState;
use obisys::PartitionRunner;
impl KmerIndex { impl KmerIndex {
/// Rebuild `src` into a new compact single-layer index at `output`. /// Rebuild `src` into a new compact single-layer index at `output`.
@@ -61,7 +63,7 @@ impl KmerIndex {
let pb = progress_bar("rebuild", n_partitions as u64, "partitions"); let pb = progress_bar("rebuild", n_partitions as u64, "partitions");
let order: Vec<usize> = (0..n_partitions).collect(); let order: Vec<usize> = (0..n_partitions).collect();
let runner = crate::index::numa::PartitionRunner::new(); let runner = PartitionRunner::new();
runner.run( runner.run(
&order, &order,
|i| dst_partition.rebuild_partition(src, i, filters, mode, n_genomes, block_bits), |i| dst_partition.rebuild_partition(src, i, filters, mode, n_genomes, block_bits),
@@ -6,15 +6,17 @@ use obicompactvec::{
}; };
use obidebruinj::GraphDeBruijn; use obidebruinj::GraphDeBruijn;
use obikseq::CanonicalKmer; use obikseq::CanonicalKmer;
use crate::layer::meta::PartitionMeta; use obikindex::layer::meta::PartitionMeta;
use crate::layer::{IndexMode, MphfLayer, layer_dir}; use obikindex::layer::{IndexMode, MphfLayer};
use obiskio::{SKError, SKResult, UnitigFileReader}; use obikindex::layer::utils::layer_dir;
use obiskio::UnitigFileReader;
use obikindex::{OKIError, OKIResult};
use crate::index::common::{load_meta, olm_to_sk}; use obikindex::load_meta;
use crate::index::filter::KmerFilter; use obikfilter::KmerFilter;
use crate::index::graph_pipeline::materialize_layer; use obikindexer::materialize_layer;
use crate::index::merge_layer::{MergeMode, SrcLayerData}; use obikmerge::{MergeMode, SrcLayerData};
use crate::index::kmer_index::KmerIndex; use obikindex::KmerIndex;
// ── Builders — pair matrix builder + column builders for one mode ───────────── // ── Builders — pair matrix builder + column builders for one mode ─────────────
@@ -27,22 +29,22 @@ enum Builders {
} }
impl Builders { impl Builders {
fn new(mode: MergeMode, n: usize, dir: &Path, n_genomes: usize) -> SKResult<Self> { fn new(mode: MergeMode, n: usize, dir: &Path, n_genomes: usize) -> OKIResult<Self> {
match mode { match mode {
MergeMode::Presence => { MergeMode::Presence => {
let mut mat = PersistentBitMatrixBuilder::new(n, dir).map_err(SKError::Io)?; let mut mat = PersistentBitMatrixBuilder::new(n, dir).map_err(OKIError::Io)?;
let mut cols = Vec::with_capacity(n_genomes); let mut cols = Vec::with_capacity(n_genomes);
for _ in 0..n_genomes { for _ in 0..n_genomes {
cols.push(mat.add_col().map_err(SKError::Io)?); cols.push(mat.add_col().map_err(OKIError::Io)?);
} }
Ok(Builders::Presence(mat, cols)) Ok(Builders::Presence(mat, cols))
} }
MergeMode::Count => { MergeMode::Count => {
let mut mat = let mut mat =
PersistentCompactIntMatrixBuilder::new(n, dir).map_err(SKError::Io)?; PersistentCompactIntMatrixBuilder::new(n, dir).map_err(OKIError::Io)?;
let mut cols = Vec::with_capacity(n_genomes); let mut cols = Vec::with_capacity(n_genomes);
for _ in 0..n_genomes { for _ in 0..n_genomes {
cols.push(mat.add_col().map_err(SKError::Io)?); cols.push(mat.add_col().map_err(OKIError::Io)?);
} }
Ok(Builders::Count(mat, cols)) Ok(Builders::Count(mat, cols))
} }
@@ -56,19 +58,19 @@ impl Builders {
} }
} }
fn close(self) -> SKResult<()> { fn close(self) -> OKIResult<()> {
match self { match self {
Builders::Presence(mat, cols) => { Builders::Presence(mat, cols) => {
for b in cols { for b in cols {
b.close().map_err(SKError::Io)?; b.close().map_err(OKIError::Io)?;
} }
mat.close().map_err(SKError::Io) mat.close().map_err(OKIError::Io)
} }
Builders::Count(mat, cols) => { Builders::Count(mat, cols) => {
for b in cols { for b in cols {
b.close().map_err(SKError::Io)?; b.close().map_err(OKIError::Io)?;
} }
mat.close().map_err(SKError::Io) mat.close().map_err(OKIError::Io)
} }
} }
} }
@@ -86,7 +88,7 @@ fn try_compute_combined_mask(
filters: &[Box<dyn KmerFilter>], filters: &[Box<dyn KmerFilter>],
src_data: &SrcLayerData, src_data: &SrcLayerData,
n_genomes: usize, n_genomes: usize,
) -> SKResult<Option<obicompactvec::TempBitVec>> { ) -> OKIResult<Option<obicompactvec::TempBitVec>> {
if filters.is_empty() { if filters.is_empty() {
return Ok(None); return Ok(None);
} }
@@ -101,7 +103,7 @@ fn try_compute_combined_mask(
let n = src_data.n_slots(); let n = src_data.n_slots();
let mask = src_data let mask = src_data
.with_matrix(|mat| eval_filter_mask(&combined, mat, n)) .with_matrix(|mat| eval_filter_mask(&combined, mat, n))
.map_err(SKError::Io)?; .map_err(OKIError::Io)?;
Ok(Some(mask)) Ok(Some(mask))
} }
@@ -118,8 +120,8 @@ fn iter_src_kmers_masked(
n_genomes: usize, n_genomes: usize,
filters: &[Box<dyn KmerFilter>], filters: &[Box<dyn KmerFilter>],
mut cb: impl FnMut(CanonicalKmer), mut cb: impl FnMut(CanonicalKmer),
) -> SKResult<()> { ) -> OKIResult<()> {
let src_meta = load_meta(src_index_dir, "rebuild")?; let src_meta = load_meta(src_index_dir)?;
for l in 0..src_meta.n_layers { for l in 0..src_meta.n_layers {
let src_layer_dir = layer_dir(src_index_dir, l); let src_layer_dir = layer_dir(src_index_dir, l);
let unitigs_path = src_layer_dir.join("unitigs.bin"); let unitigs_path = src_layer_dir.join("unitigs.bin");
@@ -159,8 +161,8 @@ fn iter_src_layers(
n_genomes: usize, n_genomes: usize,
filters: &[Box<dyn KmerFilter>], filters: &[Box<dyn KmerFilter>],
mut cb: impl FnMut(CanonicalKmer, Box<[u32]>), mut cb: impl FnMut(CanonicalKmer, Box<[u32]>),
) -> SKResult<()> { ) -> OKIResult<()> {
let src_meta = load_meta(src_index_dir, "rebuild")?; let src_meta = load_meta(src_index_dir)?;
for l in 0..src_meta.n_layers { for l in 0..src_meta.n_layers {
let src_layer_dir = layer_dir(src_index_dir, l); let src_layer_dir = layer_dir(src_index_dir, l);
let unitigs_path = src_layer_dir.join("unitigs.bin"); let unitigs_path = src_layer_dir.join("unitigs.bin");
@@ -209,13 +211,13 @@ impl KmerIndex {
mode: MergeMode, mode: MergeMode,
n_genomes: usize, n_genomes: usize,
block_bits: u8, block_bits: u8,
) -> SKResult<()> { ) -> OKIResult<()> {
let src_index_dir = src.index_dir(i); let src_index_dir = src.index_dir(i);
if !src_index_dir.exists() { if !src_index_dir.exists() {
return Ok(()); return Ok(());
} }
let src_meta = load_meta(&src_index_dir, "rebuild")?; let src_meta = load_meta(&src_index_dir)?;
if src_meta.n_layers == 0 { if src_meta.n_layers == 0 {
return Ok(()); return Ok(());
} }
@@ -235,8 +237,7 @@ impl KmerIndex {
let dst_layer_dir = self.layer_dir(i, 0); let dst_layer_dir = self.layer_dir(i, 0);
let n_new = materialize_layer(g, &dst_layer_dir, block_bits, &IndexMode::Exact)?; let n_new = materialize_layer(g, &dst_layer_dir, block_bits, &IndexMode::Exact)?;
let dst_mphf = MphfLayer::open(&dst_layer_dir) let dst_mphf = MphfLayer::open(&dst_layer_dir)?;
.map_err(|e| olm_to_sk(e, "rebuild"))?;
// ── Prepare matrix builders (one column per genome) ─────────────────── // ── Prepare matrix builders (one column per genome) ───────────────────
let data_dir = match mode { let data_dir = match mode {
@@ -262,8 +263,7 @@ impl KmerIndex {
n_layers: 1, n_layers: 1,
mode: IndexMode::Exact, mode: IndexMode::Exact,
} }
.save(&dst_index_dir) .save(&dst_index_dir)?;
.map_err(|e| olm_to_sk(e, "rebuild"))?;
Ok(()) Ok(())
} }
@@ -1,24 +1,21 @@
use crate::index::builder::IndexBuilder; use obikindex::IndexBuilder;
use crate::index::meta::IndexMeta; use obikindex::IndexMeta;
use crate::layer::{IndexMode, TypedLayer}; use obikindex::layer::{IndexMode, TypedLayer};
use obisys::{Reporter, Stage, progress_bar}; use obisys::{Reporter, Stage, progress_bar};
use std::fs; use std::fs;
use std::path::Path; use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
use tracing::info; use tracing::info;
use crate::index::error::{OKIError, OKIResult}; use obikindex::{OKIError, OKIResult};
use crate::index::kmer_index::KmerIndex; use obikindex::KmerIndex;
use crate::index::state::IndexState; use obikindex::IndexState;
use obisys::PartitionRunner;
const EVIDENCE_FILE: &str = "evidence.bin"; const EVIDENCE_FILE: &str = "evidence.bin";
const FINGERPRINT_FILE: &str = "fingerprint.bin"; const FINGERPRINT_FILE: &str = "fingerprint.bin";
const UNITIG_IDX_FILE: &str = "unitigs.bin.idx"; const UNITIG_IDX_FILE: &str = "unitigs.bin.idx";
fn olm_to_oki(e: crate::layer::OLMError) -> OKIError {
OKIError::InvalidInput(e.to_string())
}
impl KmerIndex { impl KmerIndex {
/// Convert every layer's evidence bundle to `target` in-place. /// Convert every layer's evidence bundle to `target` in-place.
/// ///
@@ -47,7 +44,7 @@ impl KmerIndex {
let pb = progress_bar("reindex", n as u64, "partitions"); let pb = progress_bar("reindex", n as u64, "partitions");
let order: Vec<usize> = (0..n).collect(); let order: Vec<usize> = (0..n).collect();
let runner = crate::index::numa::PartitionRunner::new(); let runner = PartitionRunner::new();
runner.run( runner.run(
&order, &order,
|i| { |i| {
@@ -96,10 +93,10 @@ fn reindex_partition(
fn reindex_layer(layer_dir: &Path, target: &IndexMode, block_bits: u8) -> OKIResult<()> { fn reindex_layer(layer_dir: &Path, target: &IndexMode, block_bits: u8) -> OKIResult<()> {
match target { match target {
IndexMode::Exact => { IndexMode::Exact => {
TypedLayer::<()>::build_exact_evidence(layer_dir, block_bits).map_err(olm_to_oki)?; TypedLayer::<()>::build_exact_evidence(layer_dir, block_bits)?;
} }
IndexMode::Approx { b, z } | IndexMode::Hybrid { b, z } => { IndexMode::Approx { b, z } | IndexMode::Hybrid { b, z } => {
TypedLayer::<()>::build_approx_evidence(layer_dir, *b, *z).map_err(olm_to_oki)?; TypedLayer::<()>::build_approx_evidence(layer_dir, *b, *z)?;
} }
} }
remove_stale_evidence(layer_dir, target) remove_stale_evidence(layer_dir, target)
+10
View File
@@ -0,0 +1,10 @@
[package]
name = "obikselect"
version = "0.1.0"
edition = "2024"
[dependencies]
obikindex = { path = "../obikindex" }
obicompactvec = { path = "../obicompactvec" }
obisys = { path = "../obisys" }
tracing = "0.1.44"
+11
View File
@@ -0,0 +1,11 @@
//! Genome-column selection/aggregation on an `obikindex::KmerIndex`:
//! projecting/aggregating genome columns (`AggOp`) into new output columns
//! (`OutputCol`), either into a new index (`select`) or in place
//! (`select_in_place`). Orthogonal to per-kmer filtering (`obikfilter`),
//! which operates on rows, not columns. Kept as a separate crate — not a
//! module of `obikindex` — so the dependency runs one way only.
mod select;
mod select_layer;
pub use select_layer::{AggOp, OutputCol};
@@ -1,15 +1,15 @@
use std::path::Path; use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
use crate::index::builder::IndexBuilder; use obikindex::IndexBuilder;
use crate::index::OutputCol; use crate::OutputCol;
use obisys::{Reporter, Stage, progress_bar}; use obisys::{Reporter, Stage, progress_bar, PartitionRunner};
use tracing::info; use tracing::info;
use crate::index::error::{OKIError, OKIResult}; use obikindex::{OKIError, OKIResult};
use crate::index::kmer_index::KmerIndex; use obikindex::KmerIndex;
use crate::index::meta::{GenomeInfo, IndexMeta}; use obikindex::{GenomeInfo, IndexMeta};
use crate::index::state::IndexState; use obikindex::IndexState;
impl KmerIndex { impl KmerIndex {
/// Create a new index at `output` by projecting/aggregating the genome columns /// Create a new index at `output` by projecting/aggregating the genome columns
@@ -58,7 +58,7 @@ impl KmerIndex {
let pb = progress_bar("select", n_partitions as u64, "partitions"); let pb = progress_bar("select", n_partitions as u64, "partitions");
let order: Vec<usize> = (0..n_partitions).collect(); let order: Vec<usize> = (0..n_partitions).collect();
let runner = crate::index::numa::PartitionRunner::new(); let runner = PartitionRunner::new();
runner runner
.run( .run(
&order, &order,
@@ -113,7 +113,7 @@ impl KmerIndex {
let pb = progress_bar("select", n_partitions as u64, "partitions"); let pb = progress_bar("select", n_partitions as u64, "partitions");
let order: Vec<usize> = (0..n_partitions).collect(); let order: Vec<usize> = (0..n_partitions).collect();
let runner = crate::index::numa::PartitionRunner::new(); let runner = PartitionRunner::new();
runner runner
.run( .run(
&order, &order,
@@ -6,10 +6,9 @@ use obicompactvec::{
ColGroup, MatrixGroupOps, PersistentBitMatrix, PersistentBitMatrixBuilder, ColGroup, MatrixGroupOps, PersistentBitMatrix, PersistentBitMatrixBuilder,
PersistentCompactIntMatrix, PersistentCompactIntMatrixBuilder, PersistentCompactIntMatrix, PersistentCompactIntMatrixBuilder,
}; };
use crate::layer::OLMError; use obikindex::{OKIError, OKIResult};
use obiskio::{SKError, SKResult};
use crate::index::kmer_index::KmerIndex; use obikindex::KmerIndex;
// ── AggOp ───────────────────────────────────────────────────────────────────── // ── AggOp ─────────────────────────────────────────────────────────────────────
@@ -39,16 +38,6 @@ pub struct OutputCol {
// ── Helpers ─────────────────────────────────────────────────────────────────── // ── Helpers ───────────────────────────────────────────────────────────────────
fn olm_to_sk(e: OLMError) -> SKError {
match e {
OLMError::Io(e) => SKError::Io(e),
other => SKError::InvalidData {
context: "select",
detail: other.to_string(),
},
}
}
/// Copy all plain files (not subdirectories) from `src_dir` to `dst_dir`. /// Copy all plain files (not subdirectories) from `src_dir` to `dst_dir`.
fn copy_layer_files(src_dir: &Path, dst_dir: &Path) -> io::Result<()> { fn copy_layer_files(src_dir: &Path, dst_dir: &Path) -> io::Result<()> {
for entry in fs::read_dir(src_dir)? { for entry in fs::read_dir(src_dir)? {
@@ -71,97 +60,97 @@ fn fill_builders(
output_presence: bool, output_presence: bool,
mut dst_bit: Option<&mut PersistentBitMatrixBuilder>, mut dst_bit: Option<&mut PersistentBitMatrixBuilder>,
mut dst_int: Option<&mut PersistentCompactIntMatrixBuilder>, mut dst_int: Option<&mut PersistentCompactIntMatrixBuilder>,
) -> SKResult<()> { ) -> OKIResult<()> {
if src_is_count { if src_is_count {
let mat = PersistentCompactIntMatrix::open(src_layer_dir).map_err(SKError::Io)?; let mat = PersistentCompactIntMatrix::open(src_layer_dir).map_err(OKIError::Io)?;
for spec in specs { for spec in specs {
let g = ColGroup::new(&spec.label, spec.indices.clone()); let g = ColGroup::new(&spec.label, spec.indices.clone());
if output_presence { if output_presence {
let b = dst_bit.as_deref_mut().unwrap(); let b = dst_bit.as_deref_mut().unwrap();
match spec.op { match spec.op {
AggOp::Any => { AggOp::Any => {
b.add_col_from(&mat.partial_group_any(&g, threshold).map_err(SKError::Io)?) b.add_col_from(&mat.partial_group_any(&g, threshold).map_err(OKIError::Io)?)
} }
AggOp::All => { AggOp::All => {
b.add_col_from(&mat.partial_group_all(&g, threshold).map_err(SKError::Io)?) b.add_col_from(&mat.partial_group_all(&g, threshold).map_err(OKIError::Io)?)
} }
AggOp::None => { AggOp::None => {
b.add_col_from(&mat.partial_group_none(&g, threshold).map_err(SKError::Io)?) b.add_col_from(&mat.partial_group_none(&g, threshold).map_err(OKIError::Io)?)
} }
AggOp::Sum => { AggOp::Sum => {
b.add_col_from_int(&mat.partial_group_sum(&g).map_err(SKError::Io)?) b.add_col_from_int(&mat.partial_group_sum(&g).map_err(OKIError::Io)?)
} }
AggOp::Min => { AggOp::Min => {
b.add_col_from_int(&mat.partial_group_min(&g).map_err(SKError::Io)?) b.add_col_from_int(&mat.partial_group_min(&g).map_err(OKIError::Io)?)
} }
AggOp::Max => { AggOp::Max => {
b.add_col_from_int(&mat.partial_group_max(&g).map_err(SKError::Io)?) b.add_col_from_int(&mat.partial_group_max(&g).map_err(OKIError::Io)?)
} }
} }
.map_err(SKError::Io)?; .map_err(OKIError::Io)?;
} else { } else {
let b = dst_int.as_deref_mut().unwrap(); let b = dst_int.as_deref_mut().unwrap();
match spec.op { match spec.op {
AggOp::Sum => b.add_col_from(&mat.partial_group_sum(&g).map_err(SKError::Io)?), AggOp::Sum => b.add_col_from(&mat.partial_group_sum(&g).map_err(OKIError::Io)?),
AggOp::Min => b.add_col_from(&mat.partial_group_min(&g).map_err(SKError::Io)?), AggOp::Min => b.add_col_from(&mat.partial_group_min(&g).map_err(OKIError::Io)?),
AggOp::Max => b.add_col_from(&mat.partial_group_max(&g).map_err(SKError::Io)?), AggOp::Max => b.add_col_from(&mat.partial_group_max(&g).map_err(OKIError::Io)?),
AggOp::Any => b.add_col_from_bit( AggOp::Any => b.add_col_from_bit(
&mat.partial_group_any(&g, threshold).map_err(SKError::Io)?, &mat.partial_group_any(&g, threshold).map_err(OKIError::Io)?,
), ),
AggOp::All => b.add_col_from_bit( AggOp::All => b.add_col_from_bit(
&mat.partial_group_all(&g, threshold).map_err(SKError::Io)?, &mat.partial_group_all(&g, threshold).map_err(OKIError::Io)?,
), ),
AggOp::None => b.add_col_from_bit( AggOp::None => b.add_col_from_bit(
&mat.partial_group_none(&g, threshold).map_err(SKError::Io)?, &mat.partial_group_none(&g, threshold).map_err(OKIError::Io)?,
), ),
} }
.map_err(SKError::Io)?; .map_err(OKIError::Io)?;
} }
} }
} else { } else {
let mat = PersistentBitMatrix::open(src_layer_dir).map_err(SKError::Io)?; let mat = PersistentBitMatrix::open(src_layer_dir).map_err(OKIError::Io)?;
for spec in specs { for spec in specs {
let g = ColGroup::new(&spec.label, spec.indices.clone()); let g = ColGroup::new(&spec.label, spec.indices.clone());
if output_presence { if output_presence {
let b = dst_bit.as_deref_mut().unwrap(); let b = dst_bit.as_deref_mut().unwrap();
match spec.op { match spec.op {
AggOp::Any => { AggOp::Any => {
b.add_col_from(&mat.partial_group_any(&g, 1).map_err(SKError::Io)?) b.add_col_from(&mat.partial_group_any(&g, 1).map_err(OKIError::Io)?)
} }
AggOp::All => { AggOp::All => {
b.add_col_from(&mat.partial_group_all(&g, 1).map_err(SKError::Io)?) b.add_col_from(&mat.partial_group_all(&g, 1).map_err(OKIError::Io)?)
} }
AggOp::None => { AggOp::None => {
b.add_col_from(&mat.partial_group_none(&g, 1).map_err(SKError::Io)?) b.add_col_from(&mat.partial_group_none(&g, 1).map_err(OKIError::Io)?)
} }
AggOp::Sum => { AggOp::Sum => {
b.add_col_from_int(&mat.partial_group_sum(&g).map_err(SKError::Io)?) b.add_col_from_int(&mat.partial_group_sum(&g).map_err(OKIError::Io)?)
} }
AggOp::Min => { AggOp::Min => {
b.add_col_from_int(&mat.partial_group_min(&g).map_err(SKError::Io)?) b.add_col_from_int(&mat.partial_group_min(&g).map_err(OKIError::Io)?)
} }
AggOp::Max => { AggOp::Max => {
b.add_col_from_int(&mat.partial_group_max(&g).map_err(SKError::Io)?) b.add_col_from_int(&mat.partial_group_max(&g).map_err(OKIError::Io)?)
} }
} }
.map_err(SKError::Io)?; .map_err(OKIError::Io)?;
} else { } else {
let b = dst_int.as_deref_mut().unwrap(); let b = dst_int.as_deref_mut().unwrap();
match spec.op { match spec.op {
AggOp::Sum => b.add_col_from(&mat.partial_group_sum(&g).map_err(SKError::Io)?), AggOp::Sum => b.add_col_from(&mat.partial_group_sum(&g).map_err(OKIError::Io)?),
AggOp::Min => b.add_col_from(&mat.partial_group_min(&g).map_err(SKError::Io)?), AggOp::Min => b.add_col_from(&mat.partial_group_min(&g).map_err(OKIError::Io)?),
AggOp::Max => b.add_col_from(&mat.partial_group_max(&g).map_err(SKError::Io)?), AggOp::Max => b.add_col_from(&mat.partial_group_max(&g).map_err(OKIError::Io)?),
AggOp::Any => { AggOp::Any => {
b.add_col_from_bit(&mat.partial_group_any(&g, 1).map_err(SKError::Io)?) b.add_col_from_bit(&mat.partial_group_any(&g, 1).map_err(OKIError::Io)?)
} }
AggOp::All => { AggOp::All => {
b.add_col_from_bit(&mat.partial_group_all(&g, 1).map_err(SKError::Io)?) b.add_col_from_bit(&mat.partial_group_all(&g, 1).map_err(OKIError::Io)?)
} }
AggOp::None => { AggOp::None => {
b.add_col_from_bit(&mat.partial_group_none(&g, 1).map_err(SKError::Io)?) b.add_col_from_bit(&mat.partial_group_none(&g, 1).map_err(OKIError::Io)?)
} }
} }
.map_err(SKError::Io)?; .map_err(OKIError::Io)?;
} }
} }
} }
@@ -185,7 +174,7 @@ impl KmerIndex {
threshold: u32, threshold: u32,
output_presence: bool, output_presence: bool,
in_place: bool, in_place: bool,
) -> SKResult<()> { ) -> OKIResult<()> {
let src_index_dir = src.index_dir(i); let src_index_dir = src.index_dir(i);
if !src_index_dir.exists() { if !src_index_dir.exists() {
return Ok(()); return Ok(());
@@ -222,11 +211,11 @@ impl KmerIndex {
// Determine number of slots and detect implicit layers. // Determine number of slots and detect implicit layers.
let n = if counts_dir.exists() { let n = if counts_dir.exists() {
PersistentCompactIntMatrix::open(&src_layer_dir) PersistentCompactIntMatrix::open(&src_layer_dir)
.map_err(SKError::Io)? .map_err(OKIError::Io)?
.n() .n()
} else if presence_dir.exists() { } else if presence_dir.exists() {
PersistentBitMatrix::open(&src_layer_dir) PersistentBitMatrix::open(&src_layer_dir)
.map_err(SKError::Io)? .map_err(OKIError::Io)?
.n() .n()
} else { } else {
// Implicit single-genome layer: no data matrix needed in output either. // Implicit single-genome layer: no data matrix needed in output either.
@@ -255,7 +244,7 @@ impl KmerIndex {
let (mut dst_bit, mut dst_int) = if output_presence { let (mut dst_bit, mut dst_int) = if output_presence {
( (
Some(PersistentBitMatrixBuilder::new(n, &dst_data_dir).map_err(SKError::Io)?), Some(PersistentBitMatrixBuilder::new(n, &dst_data_dir).map_err(OKIError::Io)?),
None, None,
) )
} else { } else {
@@ -263,7 +252,7 @@ impl KmerIndex {
None, None,
Some( Some(
PersistentCompactIntMatrixBuilder::new(n, &dst_data_dir) PersistentCompactIntMatrixBuilder::new(n, &dst_data_dir)
.map_err(SKError::Io)?, .map_err(OKIError::Io)?,
), ),
) )
}; };
@@ -279,9 +268,9 @@ impl KmerIndex {
)?; )?;
if output_presence { if output_presence {
dst_bit.unwrap().close().map_err(SKError::Io)?; dst_bit.unwrap().close().map_err(OKIError::Io)?;
} else { } else {
dst_int.unwrap().close().map_err(SKError::Io)?; dst_int.unwrap().close().map_err(OKIError::Io)?;
} }
// In-place: swap old data dir for new. // In-place: swap old data dir for new.
@@ -300,8 +289,7 @@ impl KmerIndex {
if !in_place { if !in_place {
src.partition_meta(i)? src.partition_meta(i)?
.save(&dst_index_dir) .save(&dst_index_dir)?;
.map_err(olm_to_sk)?;
} }
Ok(()) Ok(())
+9
View File
@@ -0,0 +1,9 @@
[package]
name = "obikstats"
version = "0.1.0"
edition = "2024"
[dependencies]
obikindex = { path = "../obikindex" }
obicompactvec = { path = "../obicompactvec" }
rayon = "1"
+10
View File
@@ -0,0 +1,10 @@
//! Read-only aggregation/statistics over an already-built
//! `obikindex::KmerIndex`: bits-per-kmer breakdown by index component
//! (`bits_per_kmer`), per-genome k-mer counts (`genome_kmer_counts`).
//! Not part of the `Index { Partition { Layer } }` data model itself —
//! kept out of `obikindex` like every other read/write extension
//! (`obikquery`, `obikdump`, `obikselect`, `obikrebuild`, `obikmerge`).
mod stats;
pub use stats::IndexBitsPerKmer;
@@ -5,8 +5,8 @@ use obicompactvec::{LayerMeta, PersistentBitMatrix, PersistentCompactIntMatrix};
use obicompactvec::traits::ColumnWeights; use obicompactvec::traits::ColumnWeights;
use rayon::prelude::*; use rayon::prelude::*;
use crate::index::error::{OKIError, OKIResult}; use obikindex::{OKIError, OKIResult};
use crate::index::kmer_index::KmerIndex; use obikindex::KmerIndex;
/// Bits per kmer broken down by index component. /// Bits per kmer broken down by index component.
pub struct IndexBitsPerKmer { pub struct IndexBitsPerKmer {
@@ -95,7 +95,8 @@ impl KmerIndex {
let n_layers = self.n_layers(i).unwrap_or(0); let n_layers = self.n_layers(i).unwrap_or(0);
(0..n_layers).fold((0usize, 0u64, 0u64, 0u64), |acc, l| { (0..n_layers).fold((0usize, 0u64, 0u64, 0u64), |acc, l| {
let lb = layer_bytes(&self.layer_dir(i, l)); let Ok(layer_dir) = self.layer_dir(i, l) else { return acc };
let lb = layer_bytes(&layer_dir);
(acc.0 + lb.n_kmers, acc.1 + lb.mphf, acc.2 + lb.evidence, acc.3 + lb.matrix) (acc.0 + lb.n_kmers, acc.1 + lb.mphf, acc.2 + lb.evidence, acc.3 + lb.matrix)
}) })
}) })
@@ -144,7 +145,7 @@ impl KmerIndex {
let n_layers = self.n_layers(i).unwrap_or(0); let n_layers = self.n_layers(i).unwrap_or(0);
for l in 0..n_layers { for l in 0..n_layers {
let this_layer_dir = self.layer_dir(i, l); let Ok(this_layer_dir) = self.layer_dir(i, l) else { continue };
if !this_layer_dir.exists() { continue; } if !this_layer_dir.exists() { continue; }
n_kmers += LayerMeta::load(&this_layer_dir).map(|m| m.n).unwrap_or(0); n_kmers += LayerMeta::load(&this_layer_dir).map(|m| m.n).unwrap_or(0);
@@ -153,12 +154,12 @@ impl KmerIndex {
if this_layer_dir.join("counts").exists() if this_layer_dir.join("counts").exists()
&& !this_layer_dir.join("presence").exists() && !this_layer_dir.join("presence").exists()
{ {
match crate::layer::open_data::<PersistentCompactIntMatrix>(&index_dir, l) { match obikindex::layer::open_data::<PersistentCompactIntMatrix>(&index_dir, l) {
Ok(m) => Box::new(m), Ok(m) => Box::new(m),
Err(_) => continue, Err(_) => continue,
} }
} else { } else {
match crate::layer::open_data::<PersistentBitMatrix>(&index_dir, l) { match obikindex::layer::open_data::<PersistentBitMatrix>(&index_dir, l) {
Ok(m) => Box::new(m), Ok(m) => Box::new(m),
Err(_) => continue, Err(_) => continue,
} }
+7
View File
@@ -8,3 +8,10 @@ libc = "0.2"
sysinfo = "0.39" sysinfo = "0.39"
indicatif = "0.18" indicatif = "0.18"
tracing = "0.1" tracing = "0.1"
crossbeam-channel = "0.5"
rayon = "1"
hwlocality = { version = "1.0.0-alpha.11", features = ["vendored"], optional = true }
[features]
default = ["numa"]
numa = ["hwlocality"]
+2
View File
@@ -3,12 +3,14 @@
mod budget; mod budget;
mod lock; mod lock;
mod numa;
mod progress; mod progress;
mod resources; mod resources;
mod stage; mod stage;
pub use budget::MemoryBudget; pub use budget::MemoryBudget;
pub use lock::DirLock; pub use lock::DirLock;
pub use numa::PartitionRunner;
pub use progress::{Progress, TracedBar, progress_bar, spinner}; pub use progress::{Progress, TracedBar, progress_bar, spinner};
pub use resources::{CpuSample, IoSample, available_memory_bytes, effective_parallelism, peak_rss_bytes}; pub use resources::{CpuSample, IoSample, available_memory_bytes, effective_parallelism, peak_rss_bytes};
pub use stage::{Reporter, Stage, StageStats}; pub use stage::{Reporter, Stage, StageStats};
@@ -2,7 +2,7 @@ use std::sync::Arc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use crossbeam_channel::unbounded; use crossbeam_channel::unbounded;
use obisys::{CpuSample, IoSample}; use crate::{CpuSample, IoSample};
use tracing::debug; use tracing::debug;
use super::topology::{build, pin_current_thread}; use super::topology::{build, pin_current_thread};
@@ -66,7 +66,7 @@ pub fn build() -> NumaSetup {
} }
// UMA fallback: single synthetic node, all cores, no pool, no pinning. // UMA fallback: single synthetic node, all cores, no pool, no pinning.
let n_cores = obisys::effective_parallelism(); let n_cores = crate::effective_parallelism();
debug!("UMA: single synthetic node, {} core(s)", n_cores); debug!("UMA: single synthetic node, {} core(s)", n_cores);
NumaSetup { NumaSetup {
pools: vec![None], pools: vec![None],
@@ -76,7 +76,7 @@ pub fn build() -> NumaSetup {
#[cfg(not(feature = "numa"))] #[cfg(not(feature = "numa"))]
pub fn build() -> NumaSetup { pub fn build() -> NumaSetup {
let n_cores = obisys::effective_parallelism(); let n_cores = crate::effective_parallelism();
debug!("UMA: single synthetic node, {} core(s)", n_cores); debug!("UMA: single synthetic node, {} core(s)", n_cores);
NumaSetup { NumaSetup {
pools: vec![None], pools: vec![None],