Push zunrplorkwkt #70
Generated
+108
-2
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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`.
|
||||||
@@ -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;
|
||||||
@@ -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) {
|
||||||
@@ -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 ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -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"
|
||||||
@@ -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;
|
||||||
@@ -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"]
|
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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(())
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn len(&self) -> usize {
|
|
||||||
self.mmap.len() / 4
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn encode(chunk_id: u32, rank: u8) -> u32 {
|
pub fn encode(chunk_id: u32, rank: u8) -> u32 {
|
||||||
(chunk_id << 7) | (rank as u32 & 0x7F)
|
(chunk_id << 7) | (rank as u32 & 0x7F)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.mmap.len() / 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
|
|
||||||
@@ -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) }
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}"))
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -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};
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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"]
|
||||||
@@ -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 {}
|
||||||
|
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod index;
|
||||||
@@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
@@ -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!(
|
||||||
+15
-16
@@ -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),
|
||||||
@@ -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 ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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}")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[package]
|
||||||
|
name = "obikquery"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
obikindex = { path = "../obikindex" }
|
||||||
@@ -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>>> =
|
||||||
+3
-3
@@ -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");
|
||||||
@@ -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"
|
||||||
@@ -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)
|
||||||
@@ -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"
|
||||||
@@ -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(())
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "obikstats"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
obikindex = { path = "../obikindex" }
|
||||||
|
obicompactvec = { path = "../obicompactvec" }
|
||||||
|
rayon = "1"
|
||||||
@@ -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,
|
||||||
}
|
}
|
||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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],
|
||||||
Reference in New Issue
Block a user