rename KmerPartition to KmerPartitions and update Mat enum

Rename the KmerPartition type to KmerPartitions across obikindex, obikpartitionner, and obikphylo/siblings to reflect an updated data model. Update the Mat enum in siblings/cache.rs to add a SparsePresence variant and simplify opening logic by delegating sparse versus dense detection to PersistentBitMatrix. Apply consistent code formatting, import reordering, and multi-line refactoring throughout the affected modules.
This commit is contained in:
Eric Coissac
2026-08-20 15:57:33 +02:00
parent 76cbd3a886
commit f9ef6b8391
26 changed files with 1290 additions and 733 deletions
@@ -5,6 +5,70 @@ themselves not started; ownership split (below) still open. Preparatory
encapsulation work (partition/layer path and metadata accessors on encapsulation work (partition/layer path and metadata accessors on
`KmerPartition`) landed the same day — see "Preparatory work done" below. `KmerPartition`) landed the same day — see "Preparatory work done" below.
## Type-to-concept mapping: Index / Partition / Layer
The conceptual nesting `Index { Partition { Layer { MPHF, Evidence, Matrix
} } } }` does **not** have one Rust type per level — worth stating
explicitly, since two of the names below are misleading.
- **Index** = `obikindex::KmerIndex` — `{ root_path, meta: IndexMeta,
partition: KmerPartition }`. The field is named `partition` (singular),
but it holds the *whole* multi-partition structure below — the name
suggests "one partition", the value is all of them.
- **Partition, the collection (not one partition)** =
`obikpartitionner::KmerPartition`. Despite the singular name, this owns
*every* partition of the index: `root_path`, `n_partitions`, and
per-partition accessors that all take an explicit index `i`
(`partition_dir(i)`, `index_dir(i)`, `layer_dir(i, l)`,
`partition_meta(i)`, `n_layers(i)`, `index_mode(i)` — see the
`part_dir`/`layer_dir` and `PartitionMeta`-encapsulation work above).
It never holds one partition's layers open in memory — no `Vec<Layer<D>>`
here, only path arithmetic and metadata reads. A more honest name would
be `KmerPartitionSet` or `KmerPartitioner`; renaming is out of scope for
now, just worth knowing it's not "a partition."
- **Partition, one of them** = **no dedicated type exists today**. The
structural equivalent of "one partition, its layers held open" is
`obilayeredmap::LayeredMap<D>` — `{ root, meta: PartitionMeta, layers:
Vec<Layer<D>> }` — but `LayeredMap` itself doesn't know it's playing that
role: it operates purely on whatever `root` path it's given and has no
notion of `KmerPartition`, `n_partitions`, or a partition index `i` (see
"Layering: who owns what" below — established independently while
fixing `open_data`/`layer_dir`). Every caller that wants "one partition,
opened" builds the path itself
(`kmer_partition.index_dir(i)`/`.layer_dir(i, l)`) and either passes it
to `LayeredMap::open` or — as `obikphylo::siblings::cache` does —
bypasses `LayeredMap` entirely and opens each `Layer<D>` directly.
- **Layer** = `obilayeredmap::Layer<D>` — `{ mphf: MphfLayer, data: D }`.
- **MPHF** = `MphfLayer.mphf: MemCase<MphfEps>` — kmer → slot.
- **Evidence** = `MphfLayer.ev: LayerEvidence` (`Exact`/`Approx`/
`Hybrid` — `evidence.bin`/`fingerprint.bin`; see `EvidenceKind`).
- **Matrix** = `Layer<D>.data: D` — `PersistentBitMatrix` /
`PersistentCompactIntMatrix` / `PersistentSparseBitMatrix` / `()`.
Today's actual nesting, in Rust terms:
```
KmerIndex
└─ partition: KmerPartition (all N partitions — paths + metadata only)
└─ (opened on demand, per i) LayeredMap<D> ← "one partition", unnamed as such
└─ layers: Vec<Layer<D>>
└─ Layer<D> { mphf: MphfLayer, data: D }
├─ mphf.mphf → MPHF
├─ mphf.ev → Evidence
└─ data → Matrix
```
`obikphylo::siblings::cache::PartitionCache` — the thing (2) is meant to
generalise — skips the middle of this nesting entirely: it doesn't build a
`Vec<LayeredMap<D>>`, it builds `mats: Vec<Vec<Mat>>` directly — outer
index = partition `i` (via `KmerPartition`), inner index = layer `l` (via
`Layer<D>`/`Mat`) — reconstructing "one partition, its layers open" as a
bare nested `Vec` because no owning type for that concept exists to reuse.
That gap is exactly what (2) needs to fill, and (1) is exactly what would
let a per-partition type hold layers of mixed `D` (today `LayeredMap<D>`
can't, being monomorphic — see "The gap in `obilayeredmap`'s existing
cache" below).
## The problem ## The problem
Reading a layer's data (MPHF + matrix) is not free: `MphfLayer::open` mmaps Reading a layer's data (MPHF + matrix) is not free: `MphfLayer::open` mmaps
@@ -217,13 +281,18 @@ storage decisions, now a *third* copy of the same logic to keep in sync)
about the code. Lesson: for a `pub enum` whose variant list matters, about the code. Lesson: for a `pub enum` whose variant list matters,
read the definition unfiltered, don't grep for the variant names you read the definition unfiltered, don't grep for the variant names you
expect to find. expect to find.
- One real consequence of that same correction: `obikphylo::siblings:: - One real consequence of that same correction, **fixed (2026-08-20)**:
cache::Mat::SparsePresence(Layer<PersistentSparseBitMatrix>)` looks `obikphylo::siblings::cache::Mat::SparsePresence(Layer<
redundant now — `Mat::Presence(Layer<PersistentBitMatrix>)` alone PersistentSparseBitMatrix>)` was redundant — `Mat::Presence(Layer<
would already handle sparse layers transparently, since PersistentBitMatrix>)` alone already handles sparse layers
`PersistentBitMatrix` absorbs `Sparse` internally. Likely `Mat` predates transparently, since `PersistentBitMatrix` absorbs `Sparse` internally.
`PersistentBitMatrix` growing native sparse support. Signalled, not Removed the variant, the `presence/is_multi.prsb` probe in `Mat::open`
removed — no mandate to touch `obikphylo` for this. (now just opens `Layer::<PersistentBitMatrix>` unconditionally for the
non-count case — sparse-vs-dense is `PersistentBitMatrix::open`'s own
concern), and every now-single-armed match in `find_slot`/`index_batch`/
`iter_minorants_batch`/`n_cols`/`fill_sub_matrix_carries`. Full
workspace test suite green after, including the 27 `obikphylo::siblings`
tests that exercise `pack_matrices(true)`/sparse through `Mat`.
## Remaining instance of the PartitionMeta-encapsulation problem ## Remaining instance of the PartitionMeta-encapsulation problem
+92 -30
View File
@@ -2,7 +2,7 @@ use std::collections::BTreeMap;
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use obikpartitionner::{KmerPartition, KmerSpectrum, PARTITIONS_SUBDIR}; use obikpartitionner::{KmerPartitions, KmerSpectrum, PARTITIONS_SUBDIR};
use obisys::{Reporter, Stage, progress_bar}; use obisys::{Reporter, Stage, progress_bar};
use rayon::prelude::*; use rayon::prelude::*;
use tracing::info; use tracing::info;
@@ -16,7 +16,7 @@ use crate::state::{IndexState, SENTINEL_COUNTED, SENTINEL_INDEXED, SENTINEL_SCAT
pub struct KmerIndex { pub struct KmerIndex {
pub(crate) root_path: PathBuf, pub(crate) root_path: PathBuf,
pub(crate) meta: IndexMeta, pub(crate) meta: IndexMeta,
pub(crate) partition: KmerPartition, pub(crate) partition: KmerPartitions,
} }
impl KmerIndex { impl KmerIndex {
@@ -31,7 +31,7 @@ impl KmerIndex {
force: bool, force: bool,
) -> OKIResult<Self> { ) -> OKIResult<Self> {
let root_path = path.as_ref().to_owned(); let root_path = path.as_ref().to_owned();
let partition = KmerPartition::create( let partition = KmerPartitions::create(
&root_path, &root_path,
config.n_bits, config.n_bits,
config.kmer_size, config.kmer_size,
@@ -45,7 +45,11 @@ impl KmerIndex {
meta.genomes.push(info); meta.genomes.push(info);
} }
meta.write(&root_path)?; meta.write(&root_path)?;
Ok(Self { root_path, meta, partition }) Ok(Self {
root_path,
meta,
partition,
})
} }
pub fn open<P: AsRef<Path>>(path: P) -> OKIResult<Self> { pub fn open<P: AsRef<Path>>(path: P) -> OKIResult<Self> {
@@ -53,13 +57,17 @@ impl KmerIndex {
let meta = IndexMeta::read(&root_path).map_err(OKIError::Io)?; let meta = IndexMeta::read(&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);
let partition = KmerPartition::open_with_config( let partition = KmerPartitions::open_with_config(
&root_path, &root_path,
meta.config.kmer_size, meta.config.kmer_size,
meta.config.minimizer_size, meta.config.minimizer_size,
meta.config.n_bits, meta.config.n_bits,
)?; )?;
Ok(Self { root_path, meta, partition }) Ok(Self {
root_path,
meta,
partition,
})
} }
/// Return `true` if `path` contains an `index.meta` file. /// Return `true` if `path` contains an `index.meta` file.
@@ -96,12 +104,12 @@ impl KmerIndex {
pub(crate) fn create_skeleton<P: AsRef<Path>>( pub(crate) fn create_skeleton<P: AsRef<Path>>(
output: P, output: P,
meta: &IndexMeta, meta: &IndexMeta,
) -> OKIResult<KmerPartition> { ) -> OKIResult<KmerPartitions> {
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)?;
meta.write(output).map_err(OKIError::Io)?; meta.write(output).map_err(OKIError::Io)?;
fs::create_dir_all(output.join(PARTITIONS_SUBDIR)).map_err(OKIError::Io)?; fs::create_dir_all(output.join(PARTITIONS_SUBDIR)).map_err(OKIError::Io)?;
Ok(KmerPartition::open_with_config( Ok(KmerPartitions::open_with_config(
output, output,
meta.config.kmer_size, meta.config.kmer_size,
meta.config.minimizer_size, meta.config.minimizer_size,
@@ -134,12 +142,24 @@ impl KmerIndex {
/// The index's root directory — needed by out-of-crate extension code /// The index's root directory — needed by out-of-crate extension code
/// (e.g. `obikphylo`) that opens its own `KmerPartition` handle onto /// (e.g. `obikphylo`) that opens its own `KmerPartition` handle onto
/// the same on-disk index. /// the same on-disk index.
pub fn root_path(&self) -> &Path { &self.root_path } pub fn root_path(&self) -> &Path {
pub fn meta(&self) -> &IndexMeta { &self.meta } &self.root_path
pub fn meta_mut(&mut self) -> &mut IndexMeta { &mut self.meta } }
pub fn kmer_size(&self) -> usize { self.meta.config.kmer_size } pub fn meta(&self) -> &IndexMeta {
pub fn minimizer_size(&self) -> usize { self.meta.config.minimizer_size } &self.meta
pub fn n_partitions(&self) -> usize { self.partition.n_partitions() } }
pub fn meta_mut(&mut self) -> &mut IndexMeta {
&mut self.meta
}
pub fn kmer_size(&self) -> usize {
self.meta.config.kmer_size
}
pub fn minimizer_size(&self) -> usize {
self.meta.config.minimizer_size
}
pub fn n_partitions(&self) -> usize {
self.partition.n_partitions()
}
/// Number of layers per partition. /// Number of layers per partition.
/// ///
@@ -152,7 +172,7 @@ impl KmerIndex {
/// Expose the inner partition so the caller can run scatter into it. /// Expose the inner partition so the caller can run scatter into it.
/// Call `mark_scattered` once scatter is complete. /// Call `mark_scattered` once scatter is complete.
pub fn partition_mut(&mut self) -> &mut KmerPartition { pub fn partition_mut(&mut self) -> &mut KmerPartitions {
&mut self.partition &mut self.partition
} }
@@ -174,7 +194,11 @@ impl KmerIndex {
/// ///
/// Writes `spectrums/{label}.json` and touches `count.done` upon completion. /// Writes `spectrums/{label}.json` and touches `count.done` upon completion.
/// Per-partition spectrum files are removed unless `keep_intermediate` is true. /// Per-partition spectrum files are removed unless `keep_intermediate` is true.
pub fn dereplicate_and_count(&self, keep_intermediate: bool, rep: &mut Reporter) -> OKIResult<()> { pub fn dereplicate_and_count(
&self,
keep_intermediate: bool,
rep: &mut Reporter,
) -> OKIResult<()> {
let t = Stage::start("dereplicate"); let t = Stage::start("dereplicate");
self.partition.dereplicate()?; self.partition.dereplicate()?;
rep.push(t.stop()); rep.push(t.stop());
@@ -189,11 +213,17 @@ impl KmerIndex {
} }
fn write_spectrum(&self, sp: &KmerSpectrum) -> OKIResult<()> { fn write_spectrum(&self, sp: &KmerSpectrum) -> OKIResult<()> {
let label = self.meta.genomes.first().map(|g| g.label.as_str()).unwrap_or("unknown"); let label = self
.meta
.genomes
.first()
.map(|g| g.label.as_str())
.unwrap_or("unknown");
let spectrums_dir = self.root_path.join("spectrums"); let spectrums_dir = self.root_path.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> = sp.counts let spectrum_map: BTreeMap<String, u64> = sp
.counts
.iter() .iter()
.map(|(&c, &f)| (format!("{c:010}"), f)) .map(|(&c, &f)| (format!("{c:010}"), f))
.collect(); .collect();
@@ -226,9 +256,19 @@ impl KmerIndex {
let order: Vec<usize> = (0..n).collect(); let order: Vec<usize> = (0..n).collect();
let runner = crate::numa::PartitionRunner::new(); let runner = crate::numa::PartitionRunner::new();
runner.run( runner
.run(
&order, &order,
|i| self.partition.build_index_layer(i, min_ab, max_ab, with_counts, &evidence, block_bits), |i| {
self.partition.build_index_layer(
i,
min_ab,
max_ab,
with_counts,
&evidence,
block_bits,
)
},
|i, n_kmers, _| { |i, n_kmers, _| {
if n_kmers > 0 { if n_kmers > 0 {
total_kmers += n_kmers; total_kmers += n_kmers;
@@ -236,7 +276,8 @@ impl KmerIndex {
pb.set_message(format!("{i}: {n_kmers} kmers")); pb.set_message(format!("{i}: {n_kmers} kmers"));
} }
}, },
).map_err(OKIError::Partition)?; )
.map_err(OKIError::Partition)?;
pb.finish_and_clear(); pb.finish_and_clear();
info!("done — {} total kmers indexed", total_kmers); info!("done — {} total kmers indexed", total_kmers);
@@ -253,7 +294,7 @@ impl KmerIndex {
} }
/// Borrow the inner partition for direct superkmer-level queries. /// Borrow the inner partition for direct superkmer-level queries.
pub fn partition(&self) -> &KmerPartition { pub fn partition(&self) -> &KmerPartitions {
&self.partition &self.partition
} }
@@ -282,7 +323,9 @@ impl KmerIndex {
&order, &order,
|i| -> OKIResult<()> { |i| -> OKIResult<()> {
let index_dir = self.partition.index_dir(i); let index_dir = self.partition.index_dir(i);
if !index_dir.exists() { return Ok(()); } if !index_dir.exists() {
return Ok(());
}
let n_layers = self.partition.n_layers(i)?; let n_layers = self.partition.n_layers(i)?;
for l in 0..n_layers { for l in 0..n_layers {
let layer_dir = self.partition.layer_dir(i, l); let layer_dir = self.partition.layer_dir(i, l);
@@ -295,11 +338,15 @@ impl KmerIndex {
pack_bit_matrix(&presence_dir).map_err(OKIError::Io)?; pack_bit_matrix(&presence_dir).map_err(OKIError::Io)?;
} }
} }
if counts_dir.exists() { pack_compact_int_matrix(&counts_dir).map_err(OKIError::Io)?; } if counts_dir.exists() {
pack_compact_int_matrix(&counts_dir).map_err(OKIError::Io)?;
}
} }
Ok(()) Ok(())
}, },
|_, _, _| { pb.inc(1); }, |_, _, _| {
pb.inc(1);
},
)?; )?;
pb.finish_and_clear(); pb.finish_and_clear();
Ok(()) Ok(())
@@ -318,15 +365,24 @@ impl KmerIndex {
.into_par_iter() .into_par_iter()
.filter_map(|i| { .filter_map(|i| {
let index_dir = self.partition.index_dir(i); let index_dir = self.partition.index_dir(i);
if !index_dir.exists() { return None; } if !index_dir.exists() {
return None;
}
let n_layers = match self.partition.n_layers(i) { let n_layers = match self.partition.n_layers(i) {
Ok(n) => n, Ok(n) => n,
Err(e) => return Some(OKIError::Io(std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))), Err(e) => {
return Some(OKIError::Io(std::io::Error::new(
std::io::ErrorKind::Other,
e.to_string(),
)));
}
}; };
for l in 0..n_layers { for l in 0..n_layers {
let layer_dir = self.partition.layer_dir(i, l); let layer_dir = self.partition.layer_dir(i, l);
let meta_path = layer_dir.join(LayerMeta::FILENAME); let meta_path = layer_dir.join(LayerMeta::FILENAME);
if meta_path.exists() { continue; } if meta_path.exists() {
continue;
}
let unitigs_path = layer_dir.join("unitigs.bin"); let unitigs_path = layer_dir.join("unitigs.bin");
let n_kmers = match UnitigFileReader::open_sequential(&unitigs_path) { let n_kmers = match UnitigFileReader::open_sequential(&unitigs_path) {
Ok(r) => r.n_kmers(), Ok(r) => r.n_kmers(),
@@ -340,7 +396,9 @@ impl KmerIndex {
}) })
.collect(); .collect();
if let Some(e) = errors.into_iter().next() { return Err(e); } if let Some(e) = errors.into_iter().next() {
return Err(e);
}
Ok(()) Ok(())
} }
} }
@@ -355,7 +413,11 @@ fn label_from_path(path: &Path) -> String {
while let Some(pos) = s.rfind('.') { while let Some(pos) = s.rfind('.') {
s.truncate(pos); s.truncate(pos);
} }
if s.is_empty() { "unknown".to_string() } else { s } if s.is_empty() {
"unknown".to_string()
} else {
s
}
} }
fn touch(path: &Path) -> Result<(), std::io::Error> { fn touch(path: &Path) -> Result<(), std::io::Error> {
+20 -5
View File
@@ -193,7 +193,7 @@ impl KmerIndex {
let block_bits = dst.meta.config.block_bits; let block_bits = dst.meta.config.block_bits;
// Pre-build source list once (avoid rebuilding per partition) // Pre-build source list once (avoid rebuilding per partition)
let srcs: Vec<(&obikpartitionner::KmerPartition, usize)> = remaining_sources let srcs: Vec<(&obikpartitionner::KmerPartitions, usize)> = remaining_sources
.iter() .iter()
.map(|s| (&s.partition, s.meta.genomes.len())) .map(|s| (&s.partition, s.meta.genomes.len()))
.collect(); .collect();
@@ -221,9 +221,19 @@ impl KmerIndex {
let runner = crate::numa::PartitionRunner::new(); let runner = crate::numa::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.run( runner
.run(
&order, &order,
|i| dst_partition.merge_partition(i, srcs, mode, n_dst_genomes, block_bits, evidence), |i| {
dst_partition.merge_partition(
i,
srcs,
mode,
n_dst_genomes,
block_bits,
evidence,
)
},
|i, g_len, dur| { |i, g_len, dur| {
pb.inc(1); pb.inc(1);
debug!( debug!(
@@ -231,9 +241,14 @@ impl KmerIndex {
dur.as_secs_f64(), dur.as_secs_f64(),
g_len, g_len,
); );
part_stats.push(PartStat { id: i, unitig_bytes: partition_sizes[i], g_len }); part_stats.push(PartStat {
id: i,
unitig_bytes: partition_sizes[i],
g_len,
});
}, },
).map_err(OKIError::Partition)?; )
.map_err(OKIError::Partition)?;
pb.finish_and_clear(); pb.finish_and_clear();
+19 -8
View File
@@ -1,8 +1,8 @@
use std::fs; use obikpartitionner::KmerPartitions;
use std::path::Path;
use obikpartitionner::KmerPartition;
use obilayeredmap::{IndexMode, layer::Layer}; use obilayeredmap::{IndexMode, layer::Layer};
use obisys::{Reporter, Stage, progress_bar}; use obisys::{Reporter, Stage, progress_bar};
use std::fs;
use std::path::Path;
use tracing::info; use tracing::info;
use crate::error::{OKIError, OKIResult}; use crate::error::{OKIError, OKIResult};
@@ -48,9 +48,13 @@ impl KmerIndex {
let runner = crate::numa::PartitionRunner::new(); let runner = crate::numa::PartitionRunner::new();
runner.run( runner.run(
&order, &order,
|i| reindex_partition(&self.partition, i, &target, block_bits) |i| {
.map_err(|e| OKIError::InvalidInput(format!("partition {i}: {e}"))), reindex_partition(&self.partition, i, &target, block_bits)
|_, _, _| { pb.inc(1); }, .map_err(|e| OKIError::InvalidInput(format!("partition {i}: {e}")))
},
|_, _, _| {
pb.inc(1);
},
)?; )?;
pb.finish_and_clear(); pb.finish_and_clear();
@@ -66,11 +70,18 @@ impl KmerIndex {
} }
/// Process all layers of one partition's index directory. /// Process all layers of one partition's index directory.
fn reindex_partition(partition: &KmerPartition, i: usize, target: &IndexMode, block_bits: u8) -> OKIResult<()> { fn reindex_partition(
partition: &KmerPartitions
, i: usize
, target: &IndexMode
, block_bits: u,
8) -> OKIResult<()> {
if !partition.index_dir(i).exists() { if !partition.index_dir(i).exists() {
return Ok(()); return Ok(());
} }
let n_layers = partition.n_layers(i).map_err(|e| OKIError::InvalidInput(e.to_string()))?; let n_layers = partition
.n_layers(i)
.map_err(|e| OKIError::InvalidInput(e.to_string()))?;
for layer_idx in 0..n_layers { for layer_idx in 0..n_layers {
reindex_layer(&partition.layer_dir(i, layer_idx), target, block_bits)?; reindex_layer(&partition.layer_dir(i, layer_idx), target, block_bits)?;
} }
+48 -14
View File
@@ -1,6 +1,6 @@
use std::path::Path; use std::path::Path;
use obikpartitionner::{KmerPartition, OutputCol}; use obikpartitionner::{KmerPartitions, OutputCol};
use obisys::{Reporter, Stage, progress_bar}; use obisys::{Reporter, Stage, progress_bar};
use tracing::info; use tracing::info;
@@ -35,7 +35,8 @@ impl KmerIndex {
let mut meta = IndexMeta::new(src.meta.config.clone()); let mut meta = IndexMeta::new(src.meta.config.clone());
meta.config.with_counts = !output_presence; meta.config.with_counts = !output_presence;
meta.genomes = specs.iter() meta.genomes = specs
.iter()
.map(|s| GenomeInfo::new(s.label.clone())) .map(|s| GenomeInfo::new(s.label.clone()))
.collect(); .collect();
@@ -46,7 +47,9 @@ impl KmerIndex {
info!( info!(
"select: {} partition(s), {} source genome(s) → {} output column(s)", "select: {} partition(s), {} source genome(s) → {} output column(s)",
n_partitions, n_src_genomes, specs.len(), n_partitions,
n_src_genomes,
specs.len(),
); );
let t = Stage::start("select"); let t = Stage::start("select");
@@ -55,11 +58,25 @@ impl KmerIndex {
let order: Vec<usize> = (0..n_partitions).collect(); let order: Vec<usize> = (0..n_partitions).collect();
let runner = crate::numa::PartitionRunner::new(); let runner = crate::numa::PartitionRunner::new();
runner.run( runner
.run(
&order, &order,
|i| dst_partition.select_partition(src_partition, i, specs, n_src_genomes, threshold, output_presence, false), |i| {
|_, _, _| { pb.inc(1); }, dst_partition.select_partition(
).map_err(OKIError::Partition)?; src_partition,
i,
specs,
n_src_genomes,
threshold,
output_presence,
false,
)
},
|_, _, _| {
pb.inc(1);
},
)
.map_err(OKIError::Partition)?;
pb.finish_and_clear(); pb.finish_and_clear();
rep.push(t.stop()); rep.push(t.stop());
@@ -84,7 +101,7 @@ impl KmerIndex {
let n_src_genomes = self.meta.genomes.len(); let n_src_genomes = self.meta.genomes.len();
let n_partitions = self.partition.n_partitions(); let n_partitions = self.partition.n_partitions();
let src_partition = KmerPartition::open_with_config( let src_partition = KmerPartitions::open_with_config(
&self.root_path, &self.root_path,
self.meta.config.kmer_size, self.meta.config.kmer_size,
self.meta.config.minimizer_size, self.meta.config.minimizer_size,
@@ -93,7 +110,9 @@ impl KmerIndex {
info!( info!(
"select (in-place): {} partition(s), {} source genome(s) → {} output column(s)", "select (in-place): {} partition(s), {} source genome(s) → {} output column(s)",
n_partitions, n_src_genomes, specs.len(), n_partitions,
n_src_genomes,
specs.len(),
); );
let t = Stage::start("select"); let t = Stage::start("select");
@@ -102,17 +121,32 @@ impl KmerIndex {
let partition = &self.partition; let partition = &self.partition;
let order: Vec<usize> = (0..n_partitions).collect(); let order: Vec<usize> = (0..n_partitions).collect();
let runner = crate::numa::PartitionRunner::new(); let runner = crate::numa::PartitionRunner::new();
runner.run( runner
.run(
&order, &order,
|i| partition.select_partition(&src_partition, i, specs, n_src_genomes, threshold, output_presence, true), |i| {
|_, _, _| { pb.inc(1); }, partition.select_partition(
).map_err(OKIError::Partition)?; &src_partition,
i,
specs,
n_src_genomes,
threshold,
output_presence,
true,
)
},
|_, _, _| {
pb.inc(1);
},
)
.map_err(OKIError::Partition)?;
pb.finish_and_clear(); pb.finish_and_clear();
rep.push(t.stop()); rep.push(t.stop());
self.meta.config.with_counts = !output_presence; self.meta.config.with_counts = !output_presence;
self.meta.genomes = specs.iter() self.meta.genomes = specs
.iter()
.map(|s| GenomeInfo::new(s.label.clone())) .map(|s| GenomeInfo::new(s.label.clone()))
.collect(); .collect();
self.meta.write(&self.root_path)?; self.meta.write(&self.root_path)?;
+18 -9
View File
@@ -1,12 +1,12 @@
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use std::time::Instant; use std::time::Instant;
use obisys::spinner; use obikpartitionner::KmerPartitions;
use obiread::NucPage;
use obikpartitionner::KmerPartition;
use obipipeline::{ThrottleGuard, Throttled, throttle}; use obipipeline::{ThrottleGuard, Throttled, throttle};
use obiread::NucPage;
use obisys::spinner;
use obisys::{Reporter, Stage}; use obisys::{Reporter, Stage};
use tracing::info; use tracing::info;
@@ -38,7 +38,7 @@ impl Drop for GuardedIter {
/// Run scatter: normalise → build superkmers → route to partition → close. /// Run scatter: normalise → build superkmers → route to partition → close.
/// Reports the "scatter" stage to `rep`. /// Reports the "scatter" stage to `rep`.
pub fn scatter( pub fn scatter(
kp: &mut KmerPartition, kp: &mut KmerPartitions,
path_source: impl Iterator<Item = PathBuf> + Send + 'static, path_source: impl Iterator<Item = PathBuf> + Send + 'static,
k: usize, k: usize,
level_max: usize, level_max: usize,
@@ -95,7 +95,8 @@ pub fn scatter(
const ALPHA: f64 = 0.15; const ALPHA: f64 = 0.15;
for batch in pipe.apply(throttled, n_workers, 1) { for batch in pipe.apply(throttled, n_workers, 1) {
total_bases += batch.iter() total_bases += batch
.iter()
.map(|sk| (sk.seql() as u64).saturating_sub(kmer_overlap)) .map(|sk| (sk.seql() as u64).saturating_sub(kmer_overlap))
.sum::<u64>(); .sum::<u64>();
let now = Instant::now(); let now = Instant::now();
@@ -107,14 +108,22 @@ pub fn scatter(
last_bases = total_bases; last_bases = total_bases;
let bp = total_bases as f64; let bp = total_bases 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),
)
}; };
let n_files = file_count.load(Ordering::Relaxed); let n_files = file_count.load(Ordering::Relaxed);
let r = flat_active.load(Ordering::Relaxed); let r = flat_active.load(Ordering::Relaxed);
let c = transform_active.load(Ordering::Relaxed); let c = transform_active.load(Ordering::Relaxed);
pb.set_message(format!("{count_str} {rate_str} {n_files} files [R:{r} C:{c}]")); pb.set_message(format!(
"{count_str} {rate_str} {n_files} files [R:{r} C:{c}]"
));
} }
kp.write_batch(batch).unwrap_or_else(|e| { kp.write_batch(batch).unwrap_or_else(|e| {
eprintln!("error: {e}"); eprintln!("error: {e}");
+9 -5
View File
@@ -1,11 +1,11 @@
use obicompactvec::{PersistentBitMatrix, PersistentCompactIntMatrix}; use obicompactvec::{PersistentBitMatrix, PersistentCompactIntMatrix};
use obilayeredmap::{open_data, LayeredStore}; use obilayeredmap::{LayeredStore, open_data};
use obiskio::SKResult; use obiskio::SKResult;
use crate::common::{load_meta, olm_to_sk}; use crate::common::{load_meta, olm_to_sk};
use crate::partition::KmerPartition; use crate::partition::KmerPartitions;
impl KmerPartition { impl KmerPartitions {
/// 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) -> SKResult<LayeredStore<PersistentCompactIntMatrix>> {
@@ -16,7 +16,9 @@ impl KmerPartition {
let n_layers = load_meta(&index_dir, "distance")?.n_layers; let n_layers = load_meta(&index_dir, "distance")?.n_layers;
let matrices = (0..n_layers) let matrices = (0..n_layers)
.filter_map(|l| { .filter_map(|l| {
self.layer_dir(part, l).join("counts").exists() self.layer_dir(part, l)
.join("counts")
.exists()
.then(|| open_data(&index_dir, l).map_err(|e| olm_to_sk(e, "distance"))) .then(|| open_data(&index_dir, l).map_err(|e| olm_to_sk(e, "distance")))
}) })
.collect::<SKResult<Vec<_>>>()?; .collect::<SKResult<Vec<_>>>()?;
@@ -33,7 +35,9 @@ impl KmerPartition {
let n_layers = load_meta(&index_dir, "distance")?.n_layers; let n_layers = load_meta(&index_dir, "distance")?.n_layers;
let matrices = (0..n_layers) let matrices = (0..n_layers)
.filter_map(|l| { .filter_map(|l| {
self.layer_dir(part, l).join("presence").exists() self.layer_dir(part, l)
.join("presence")
.exists()
.then(|| open_data(&index_dir, l).map_err(|e| olm_to_sk(e, "distance"))) .then(|| open_data(&index_dir, l).map_err(|e| olm_to_sk(e, "distance")))
}) })
.collect::<SKResult<Vec<_>>>()?; .collect::<SKResult<Vec<_>>>()?;
+43 -16
View File
@@ -1,19 +1,22 @@
use obicompactvec::{PersistentBitMatrix, PersistentCompactIntMatrix}; use obicompactvec::{PersistentBitMatrix, PersistentCompactIntMatrix};
use obikseq::CanonicalKmer; use obikseq::CanonicalKmer;
use obiskio::{SKError, SKResult, UnitigFileReader};
use obilayeredmap::{IndexMode, MphfLayer, OLMError}; use obilayeredmap::{IndexMode, MphfLayer, OLMError};
use obiskio::{SKError, SKResult, UnitigFileReader};
use crate::filter::{KmerFilter, passes_all}; use crate::filter::{KmerFilter, passes_all};
use crate::partition::KmerPartition; use crate::partition::KmerPartitions;
fn olm_to_sk(e: OLMError) -> SKError { fn olm_to_sk(e: OLMError) -> SKError {
match e { match e {
OLMError::Io(e) => SKError::Io(e), OLMError::Io(e) => SKError::Io(e),
other => SKError::InvalidData { context: "dump", detail: other.to_string() }, other => SKError::InvalidData {
context: "dump",
detail: other.to_string(),
},
} }
} }
impl KmerPartition { impl KmerPartitions {
/// 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
/// kmer that passes every filter in `filters`. /// kmer that passes every filter in `filters`.
/// ///
@@ -43,7 +46,9 @@ impl KmerPartition {
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() { break; } if !layer_dir.exists() {
break;
}
l += 1; l += 1;
let mphf = MphfLayer::open(&layer_dir, &index_mode).map_err(olm_to_sk)?; let mphf = MphfLayer::open(&layer_dir, &index_mode).map_err(olm_to_sk)?;
let reader = UnitigFileReader::open_sequential(&layer_dir.join("unitigs.bin"))?; let reader = UnitigFileReader::open_sequential(&layer_dir.join("unitigs.bin"))?;
@@ -59,7 +64,9 @@ impl KmerPartition {
let row = mat.row(slot); let row = mat.row(slot);
if passes_all(filters, kmer, &row, n_genomes) { if passes_all(filters, kmer, &row, n_genomes) {
cont = cb(kmer, row); cont = cb(kmer, row);
if !cont { break; } if !cont {
break;
}
} }
} }
} }
@@ -72,7 +79,9 @@ impl KmerPartition {
let row: Box<[u32]> = mat.row(slot).iter().map(|&b| b as u32).collect(); let row: Box<[u32]> = mat.row(slot).iter().map(|&b| b as u32).collect();
if passes_all(filters, kmer, &row, n_genomes) { if passes_all(filters, kmer, &row, n_genomes) {
cont = cb(kmer, row); cont = cb(kmer, row);
if !cont { break; } if !cont {
break;
}
} }
} }
} }
@@ -86,15 +95,21 @@ impl KmerPartition {
let all_present: Box<[u32]> = vec![1u32; n_genomes].into(); let all_present: Box<[u32]> = vec![1u32; n_genomes].into();
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 mphf.find(kmer).is_some() && passes_all(filters, kmer, &all_present, n_genomes) { if mphf.find(kmer).is_some()
&& passes_all(filters, kmer, &all_present, n_genomes)
{
cont = cb(kmer, all_present.clone()); cont = cb(kmer, all_present.clone());
if !cont { break; } if !cont {
break;
}
} }
} }
cont cont
}; };
if !cont { return Ok(false); } if !cont {
return Ok(false);
}
} }
Ok(true) Ok(true)
@@ -121,7 +136,9 @@ impl KmerPartition {
let mut layer = 0; let mut layer = 0;
loop { loop {
let layer_dir = self.layer_dir(part, layer); let layer_dir = self.layer_dir(part, layer);
if !layer_dir.exists() { break; } if !layer_dir.exists() {
break;
}
let mphf = MphfLayer::open(&layer_dir, &index_mode).map_err(olm_to_sk)?; let mphf = MphfLayer::open(&layer_dir, &index_mode).map_err(olm_to_sk)?;
let reader = UnitigFileReader::open_sequential(&layer_dir.join("unitigs.bin"))?; let reader = UnitigFileReader::open_sequential(&layer_dir.join("unitigs.bin"))?;
@@ -136,7 +153,9 @@ impl KmerPartition {
let row = mat.row(slot); let row = mat.row(slot);
if passes_all(filters, kmer, &row, n_genomes) { if passes_all(filters, kmer, &row, n_genomes) {
cont = cb(part, layer, kmer, row); cont = cb(part, layer, kmer, row);
if !cont { break; } if !cont {
break;
}
} }
} }
} }
@@ -149,7 +168,9 @@ impl KmerPartition {
let row: Box<[u32]> = mat.row(slot).iter().map(|&b| b as u32).collect(); let row: Box<[u32]> = mat.row(slot).iter().map(|&b| b as u32).collect();
if passes_all(filters, kmer, &row, n_genomes) { if passes_all(filters, kmer, &row, n_genomes) {
cont = cb(part, layer, kmer, row); cont = cb(part, layer, kmer, row);
if !cont { break; } if !cont {
break;
}
} }
} }
} }
@@ -161,15 +182,21 @@ impl KmerPartition {
let all_present: Box<[u32]> = vec![1u32; n_genomes].into(); let all_present: Box<[u32]> = vec![1u32; n_genomes].into();
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 mphf.find(kmer).is_some() && passes_all(filters, kmer, &all_present, n_genomes) { if mphf.find(kmer).is_some()
&& passes_all(filters, kmer, &all_present, n_genomes)
{
cont = cb(part, layer, kmer, all_present.clone()); cont = cb(part, layer, kmer, all_present.clone());
if !cont { break; } if !cont {
break;
}
} }
} }
cont cont
}; };
if !cont { return Ok(false); } if !cont {
return Ok(false);
}
layer += 1; layer += 1;
} }
+8 -10
View File
@@ -12,7 +12,7 @@ use ptr_hash::{PtrHash, bucket_fn::CubicEps, hash::Xx64};
use crate::common::olm_to_sk; use crate::common::olm_to_sk;
use crate::graph_pipeline::{materialize_layer, write_graph_as_unitigs}; use crate::graph_pipeline::{materialize_layer, write_graph_as_unitigs};
use crate::partition::KmerPartition; use crate::partition::KmerPartitions;
type Mphf = PtrHash<u64, CubicEps, CachelineEfVec<Vec<CachelineEf>>, Xx64, Vec<u8>>; type Mphf = PtrHash<u64, CubicEps, CachelineEfVec<Vec<CachelineEf>>, Xx64, Vec<u8>>;
@@ -24,7 +24,7 @@ fn remove_if_exists(path: &std::path::Path) {
} }
} }
impl KmerPartition { impl KmerPartitions {
/// Build the layered MPHF index for partition `i`. /// Build the layered MPHF index for partition `i`.
/// ///
/// Returns the number of canonical k-mers indexed, or 0 if the partition /// Returns the number of canonical k-mers indexed, or 0 if the partition
@@ -93,17 +93,15 @@ impl KmerPartition {
} }
} }
let n_kmers = if with_counts { let n_kmers =
if with_counts {
let n = write_graph_as_unitigs(g, &layer_dir)?; let n = write_graph_as_unitigs(g, &layer_dir)?;
Layer::<PersistentCompactIntMatrix>::build( Layer::<PersistentCompactIntMatrix>::build(&layer_dir, block_bits, mode, |kmer| {
&layer_dir, match (&mphf1_opt, &counts1_opt) {
block_bits,
mode,
|kmer| match (&mphf1_opt, &counts1_opt) {
(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"))?; .map_err(|e| olm_to_sk(e, "layer build"))?;
n n
} else { } else {
+2 -2
View File
@@ -1,7 +1,7 @@
pub mod filter;
mod common; mod common;
mod distance; mod distance;
mod dump_layer; mod dump_layer;
pub mod filter;
mod graph_pipeline; mod graph_pipeline;
mod index_layer; mod index_layer;
mod kmer_sort; mod kmer_sort;
@@ -13,6 +13,6 @@ mod select_layer;
pub use filter::{GroupQuorumFilter, KmerFilter, passes_all}; pub use filter::{GroupQuorumFilter, KmerFilter, passes_all};
pub use merge_layer::MergeMode; pub use merge_layer::MergeMode;
pub use partition::{KmerPartition, KmerSpectrum, PARTITIONS_SUBDIR}; pub use partition::{KmerPartitions, KmerSpectrum, PARTITIONS_SUBDIR};
pub use query_layer::{KmerDesc, QueryHit, QueryStats}; pub use query_layer::{KmerDesc, QueryHit, QueryStats};
pub use select_layer::{AggOp, OutputCol}; pub use select_layer::{AggOp, OutputCol};
+98 -37
View File
@@ -12,21 +12,20 @@ use std::io;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use tracing::debug;
use obipipeline::{ use obipipeline::{
Pipeline, PipelineError, PipelineSender, SharedFlatFn, Stage, WorkerPool, Pipeline, PipelineError, PipelineSender, SharedFlatFn, Stage, ThrottleGuard, WorkerPool,
ThrottleGuard, throttle, make_sink, make_source, make_transform, throttle,
make_sink, make_source, make_transform,
}; };
use tracing::debug;
use obicompactvec::{PersistentBitMatrixBuilder, PersistentCompactIntMatrixBuilder}; use obicompactvec::{PersistentBitMatrixBuilder, PersistentCompactIntMatrixBuilder};
use obikseq::CanonicalKmer; use obikseq::CanonicalKmer;
use obilayeredmap::{layer_dir, IndexMode, Layer, LayeredMap, MphfOnly}; use obilayeredmap::{IndexMode, Layer, LayeredMap, MphfOnly, layer_dir};
use obiskio::{SKError, SKResult, UnitigFileReader}; use obiskio::{SKError, SKResult, UnitigFileReader};
use crate::common::{ColBuilder, load_meta, olm_to_sk}; use crate::common::{ColBuilder, load_meta, olm_to_sk};
use crate::graph_pipeline::{build_graph, materialize_layer}; use crate::graph_pipeline::{build_graph, materialize_layer};
use crate::partition::KmerPartition; use crate::partition::KmerPartitions;
mod src_layer; mod src_layer;
@@ -117,7 +116,11 @@ mod matrix_builder_tests {
// One source column, filled like pass 2 would. // One source column, filled like pass 2 would.
let mut col = mb.add_col().unwrap(); let mut col = mb.add_col().unwrap();
match &mut col { match &mut col {
ColBuilder::Bit(b) => { b.set(0, true); b.set(1, false); b.set(2, true); } ColBuilder::Bit(b) => {
b.set(0, true);
b.set(1, false);
b.set(2, true);
}
ColBuilder::Int(_) => unreachable!(), ColBuilder::Int(_) => unreachable!(),
} }
col.close().unwrap(); col.close().unwrap();
@@ -140,7 +143,10 @@ mod matrix_builder_tests {
let mut col = mb.add_col().unwrap(); let mut col = mb.add_col().unwrap();
match &mut col { match &mut col {
ColBuilder::Int(b) => { b.set(0, 7); b.set(1, 42); } ColBuilder::Int(b) => {
b.set(0, 7);
b.set(1, 42);
}
ColBuilder::Bit(_) => unreachable!(), ColBuilder::Bit(_) => unreachable!(),
} }
col.close().unwrap(); col.close().unwrap();
@@ -171,7 +177,11 @@ mod matrix_builder_tests {
for vals in [[true, false, true], [false, true, false]] { for vals in [[true, false, true], [false, true, false]] {
let mut col = mb.add_col().unwrap(); let mut col = mb.add_col().unwrap();
match &mut col { match &mut col {
ColBuilder::Bit(b) => for (slot, v) in vals.into_iter().enumerate() { b.set(slot, v); }, ColBuilder::Bit(b) => {
for (slot, v) in vals.into_iter().enumerate() {
b.set(slot, v);
}
}
ColBuilder::Int(_) => unreachable!(), ColBuilder::Int(_) => unreachable!(),
} }
col.close().unwrap(); col.close().unwrap();
@@ -188,7 +198,7 @@ mod matrix_builder_tests {
// ── KmerPartition::merge_partition ──────────────────────────────────────────── // ── KmerPartition::merge_partition ────────────────────────────────────────────
impl KmerPartition { impl KmerPartitions {
/// Merge `sources` into destination partition `i`. /// Merge `sources` into destination partition `i`.
/// ///
/// Each entry in `sources` is `(partition, n_genomes)` where `n_genomes` is /// Each entry in `sources` is `(partition, n_genomes)` where `n_genomes` is
@@ -201,7 +211,7 @@ impl KmerPartition {
pub fn merge_partition( pub fn merge_partition(
&self, &self,
i: usize, i: usize,
sources: &[(&KmerPartition, usize)], sources: &[(&KmerPartitions, usize)],
mode: MergeMode, mode: MergeMode,
n_dst_genomes: usize, n_dst_genomes: usize,
block_bits: u8, block_bits: u8,
@@ -213,7 +223,8 @@ impl KmerPartition {
} }
load_meta(&dst_index_dir, "merge")?; // ensure meta.json exists before LayeredMap::open load_meta(&dst_index_dir, "merge")?; // ensure meta.json exists before LayeredMap::open
let dst_map = Arc::new(LayeredMap::<()>::open(&dst_index_dir).map_err(|e| olm_to_sk(e, "merge"))?); let dst_map =
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();
@@ -221,7 +232,10 @@ impl KmerPartition {
// (all slots true — every kmer in those layers belongs to genome_0). // (all slots true — every kmer in those layers belongs to genome_0).
if n_dst_genomes == 1 && mode == MergeMode::Presence { if n_dst_genomes == 1 && mode == MergeMode::Presence {
for l in 0..n_dst_layers { for l in 0..n_dst_layers {
Layer::<()>::init_presence_matrix(&layer_dir(&dst_index_dir, l), dst_map.layer(l).n()) Layer::<()>::init_presence_matrix(
&layer_dir(&dst_index_dir, l),
dst_map.layer(l).n(),
)
.map_err(|e| olm_to_sk(e, "merge"))?; .map_err(|e| olm_to_sk(e, "merge"))?;
} }
} }
@@ -283,7 +297,10 @@ impl KmerPartition {
)?; )?;
let any_new = g.len() > 0; let any_new = g.len() > 0;
debug!("partition {i}: de Bruijn graph done — {} new kmers", g.len()); debug!(
"partition {i}: de Bruijn graph done — {} new kmers",
g.len()
);
// Build new layer from de Bruijn graph if there are new kmers. // Build new layer from de Bruijn graph if there are new kmers.
let new_layer_idx = n_dst_layers; let new_layer_idx = n_dst_layers;
@@ -301,11 +318,16 @@ impl KmerPartition {
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(MphfOnly::open(&new_layer_dir).map_err(|e| olm_to_sk(e, "merge"))?)) Some(Arc::new(
MphfOnly::open(&new_layer_dir).map_err(|e| olm_to_sk(e, "merge"))?,
))
} else { } else {
None None
}; };
debug!("partition {i}: MPHF open in {:.3}s", t_open.elapsed().as_secs_f64()); debug!(
"partition {i}: MPHF open in {:.3}s",
t_open.elapsed().as_secs_f64()
);
// ── Prepare matrix directories for the new layer ────────────────────── // ── Prepare matrix directories for the new layer ──────────────────────
// Absent columns (dst genomes) get an all-zero/false column. Source-genome // Absent columns (dst genomes) get an all-zero/false column. Source-genome
@@ -351,7 +373,10 @@ impl KmerPartition {
exist_builders.push(cols); exist_builders.push(cols);
} }
debug!("partition {i}: builders ready in {:.3}s", t_builders.elapsed().as_secs_f64()); debug!(
"partition {i}: builders ready in {:.3}s",
t_builders.elapsed().as_secs_f64()
);
// ── Pass 2: fill builders (pipeline) ───────────────────────────────── // ── Pass 2: fill builders (pipeline) ─────────────────────────────────
let t_pass2 = std::time::Instant::now(); let t_pass2 = std::time::Instant::now();
@@ -391,7 +416,8 @@ impl KmerPartition {
.into_iter() .into_iter()
.map(|b| Arc::new(Mutex::new(b))) .map(|b| Arc::new(Mutex::new(b)))
.collect(); .collect();
let exist_sink: Vec<Vec<Arc<Mutex<ColBuilder>>>> = exist_locked.iter() let exist_sink: Vec<Vec<Arc<Mutex<ColBuilder>>>> = exist_locked
.iter()
.map(|layer| layer.iter().map(Arc::clone).collect()) .map(|layer| layer.iter().map(Arc::clone).collect())
.collect(); .collect();
let new_sink: Vec<Arc<Mutex<ColBuilder>>> = new_locked.iter().map(Arc::clone).collect(); let new_sink: Vec<Arc<Mutex<ColBuilder>>> = new_locked.iter().map(Arc::clone).collect();
@@ -404,17 +430,22 @@ impl KmerPartition {
let throttled_pass2 = throttle(pass2_items.into_iter(), max_open); let throttled_pass2 = throttle(pass2_items.into_iter(), max_open);
let pipeline2 = Pipeline::new( let pipeline2 = Pipeline::new(
make_source!(Pass2Data, throttled_pass2.map(|t| { make_source!(
Pass2Data,
throttled_pass2.map(|t| {
let (col_offset, src_n, src_layer_dir) = t.item; let (col_offset, src_n, src_layer_dir) = t.item;
(col_offset, src_n, src_layer_dir, t.guard) (col_offset, src_n, src_layer_dir, t.guard)
}), SrcLayer), }),
SrcLayer
),
vec![ vec![
Stage::Flat(Arc::new( Stage::Flat(Arc::new(
move |data: Pass2Data, move |data: Pass2Data,
push: &PipelineSender<Result<Pass2Data, PipelineError>>, push: &PipelineSender<Result<Pass2Data, PipelineError>>,
delta: &PipelineSender<isize>| delta: &PipelineSender<isize>| {
if let Pass2Data::SrcLayer((col_offset, src_n, src_layer_dir, _guard)) =
data
{ {
if let Pass2Data::SrcLayer((col_offset, src_n, src_layer_dir, _guard)) = data {
// _guard dropped at end of block, releasing the slot. // _guard dropped at end of block, releasing the slot.
let reader = match UnitigFileReader::open_sequential( let reader = match UnitigFileReader::open_sequential(
&src_layer_dir.join("unitigs.bin"), &src_layer_dir.join("unitigs.bin"),
@@ -440,27 +471,39 @@ impl KmerPartition {
for (kmer, _, _) in reader.iter_indexed_canonical_kmers() { for (kmer, _, _) in reader.iter_indexed_canonical_kmers() {
batch.push(kmer); batch.push(kmer);
if batch.len() == BATCH { if batch.len() == BATCH {
let b = std::mem::replace(&mut batch, Vec::with_capacity(BATCH)); let b =
std::mem::replace(&mut batch, Vec::with_capacity(BATCH));
push.send(Ok(Pass2Data::RawBatch(( push.send(Ok(Pass2Data::RawBatch((
col_offset, src_n, Arc::clone(&src_data), b, col_offset,
)))).ok(); src_n,
Arc::clone(&src_data),
b,
))))
.ok();
count += 1; count += 1;
} }
} }
if !batch.is_empty() { if !batch.is_empty() {
push.send(Ok(Pass2Data::RawBatch(( push.send(Ok(Pass2Data::RawBatch((
col_offset, src_n, src_data, batch, col_offset, src_n, src_data, batch,
)))).ok(); ))))
.ok();
count += 1; count += 1;
} }
delta.send(count - 1).ok(); delta.send(count - 1).ok();
} }
} },
) as SharedFlatFn<Pass2Data>), ) as SharedFlatFn<Pass2Data>),
make_transform!(Pass2Data, { make_transform!(
move |(col_offset, src_n, src_data, kmers): (usize, usize, Arc<SrcLayerData>, Vec<CanonicalKmer>)| Pass2Data,
-> Vec<(Option<usize>, usize, usize, u32)>
{ {
move |(col_offset, src_n, src_data, kmers): (
usize,
usize,
Arc<SrcLayerData>,
Vec<CanonicalKmer>,
)|
-> Vec<(Option<usize>, usize, usize, u32)> {
let mut ops: Vec<(Option<usize>, usize, usize, u32)> = Vec::new(); let mut ops: Vec<(Option<usize>, usize, usize, u32)> = Vec::new();
for kmer in kmers { for kmer in kmers {
let values = src_data.lookup(kmer, src_n); let values = src_data.lookup(kmer, src_n);
@@ -477,9 +520,14 @@ impl KmerPartition {
} }
ops ops
} }
}, RawBatch, WriteBatch), },
RawBatch,
WriteBatch
),
], ],
make_sink!(Pass2Data, { make_sink!(
Pass2Data,
{
move |ops: Vec<(Option<usize>, usize, usize, u32)>| { move |ops: Vec<(Option<usize>, usize, usize, u32)>| {
for (layer_opt, col, slot, val) in ops { for (layer_opt, col, slot, val) in ops {
match layer_opt { match layer_opt {
@@ -488,18 +536,26 @@ impl KmerPartition {
} }
} }
} }
}, WriteBatch), },
WriteBatch
),
); );
WorkerPool::new(pipeline2, n_workers, capacity).run(); WorkerPool::new(pipeline2, n_workers, capacity).run();
debug!("partition {i}: pass2 pipeline done in {:.3}s", t_pass2.elapsed().as_secs_f64()); debug!(
"partition {i}: pass2 pipeline done in {:.3}s",
t_pass2.elapsed().as_secs_f64()
);
if let Some(msg) = Arc::try_unwrap(pass2_err) if let Some(msg) = Arc::try_unwrap(pass2_err)
.unwrap_or_else(|_| panic!("pass2: pass2_err not uniquely owned")) .unwrap_or_else(|_| panic!("pass2: pass2_err not uniquely owned"))
.into_inner() .into_inner()
.unwrap_or_else(|e| e.into_inner()) .unwrap_or_else(|e| e.into_inner())
{ {
return Err(SKError::InvalidData { context: "merge pass2", detail: msg }); return Err(SKError::InvalidData {
context: "merge pass2",
detail: msg,
});
} }
let t_close = std::time::Instant::now(); let t_close = std::time::Instant::now();
@@ -527,10 +583,15 @@ impl KmerPartition {
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.save(&dst_index_dir).map_err(|e| olm_to_sk(e, "merge"))?; part_meta
.save(&dst_index_dir)
.map_err(|e| olm_to_sk(e, "merge"))?;
} }
debug!("partition {i}: builders closed in {:.3}s", t_close.elapsed().as_secs_f64()); debug!(
"partition {i}: builders closed in {:.3}s",
t_close.elapsed().as_secs_f64()
);
Ok(n_new) Ok(n_new)
} }
@@ -7,9 +7,9 @@ use std::time::Instant;
use obisys::progress_bar; use obisys::progress_bar;
use obikseq::RoutableSuperKmer; use obikseq::RoutableSuperKmer;
use obiskio::SKResult;
use obilayeredmap::IndexMode; use obilayeredmap::IndexMode;
use obilayeredmap::meta::PartitionMeta; use obilayeredmap::meta::PartitionMeta;
use obiskio::SKResult;
use rayon::prelude::*; use rayon::prelude::*;
use remove_dir_all::remove_dir_all; use remove_dir_all::remove_dir_all;
use sysinfo::System; use sysinfo::System;
@@ -38,7 +38,7 @@ pub struct KmerSpectrum {
pub counts: BTreeMap<u32, u64>, pub counts: BTreeMap<u32, u64>,
} }
pub struct KmerPartition { pub struct KmerPartitions {
root_path: PathBuf, root_path: PathBuf,
n_partitions: usize, n_partitions: usize,
partitions_mask: u64, partitions_mask: u64,
@@ -49,7 +49,7 @@ pub struct KmerPartition {
closed: bool, closed: bool,
} }
impl KmerPartition { impl KmerPartitions {
pub fn create<P: AsRef<Path>>( pub fn create<P: AsRef<Path>>(
path: P, path: P,
n_bits: usize, n_bits: usize,
@@ -179,7 +179,9 @@ impl KmerPartition {
/// Path of partition `i` directory. /// Path of partition `i` directory.
pub fn partition_dir(&self, i: usize) -> PathBuf { pub fn partition_dir(&self, i: usize) -> PathBuf {
self.root_path.join(PARTITIONS_SUBDIR).join(format!("part_{i:05}")) self.root_path
.join(PARTITIONS_SUBDIR)
.join(format!("part_{i:05}"))
} }
/// Path of partition `i`'s layered-index directory (`<partition>/index`). /// Path of partition `i`'s layered-index directory (`<partition>/index`).
@@ -380,7 +382,7 @@ impl KmerPartition {
} }
} }
impl Drop for KmerPartition { impl Drop for KmerPartitions {
fn drop(&mut self) { fn drop(&mut self) {
let _ = self.close(); let _ = self.close();
} }
+1 -1
View File
@@ -13,7 +13,7 @@ mod kmer_partition;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
pub use kmer_partition::{KmerPartition, KmerSpectrum}; pub use kmer_partition::{KmerPartitions, KmerSpectrum};
const SK_EXT: &str = "skmer.zst"; const SK_EXT: &str = "skmer.zst";
pub const PARTITIONS_SUBDIR: &str = "partitions"; pub const PARTITIONS_SUBDIR: &str = "partitions";
+11 -10
View File
@@ -6,7 +6,7 @@ use obikseq::SuperKmer;
use obiskbuilder::build_superkmers; use obiskbuilder::build_superkmers;
use super::count::count_partition; use super::count::count_partition;
use super::{KmerPartition, PARTITIONS_SUBDIR}; use super::{KmerPartitions, PARTITIONS_SUBDIR};
const K: usize = 11; const K: usize = 11;
const M: usize = 5; const M: usize = 5;
@@ -46,7 +46,7 @@ fn pipeline_counts(seqs: &[&[u8]]) -> (u64, u64) {
let superkmers: Vec<_> = build_superkmers(rope, K, 1, 0.0); let superkmers: Vec<_> = build_superkmers(rope, K, 1, 0.0);
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
let mut kp = KmerPartition::create(dir.path(), 0, K, M, true).unwrap(); let mut kp = KmerPartitions::create(dir.path(), 0, K, M, true).unwrap();
kp.write_batch(superkmers).unwrap(); kp.write_batch(superkmers).unwrap();
kp.close().unwrap(); kp.close().unwrap();
kp.dereplicate().unwrap(); kp.dereplicate().unwrap();
@@ -58,9 +58,9 @@ fn pipeline_counts(seqs: &[&[u8]]) -> (u64, u64) {
} }
count_partition(&part_dir, &dedup_path, 1 << 20).unwrap(); count_partition(&part_dir, &dedup_path, 1 << 20).unwrap();
let spec: serde_json::Value = serde_json::from_reader( let spec: serde_json::Value =
fs::File::open(part_dir.join("kmer_spectrum_raw.json")).unwrap(), serde_json::from_reader(fs::File::open(part_dir.join("kmer_spectrum_raw.json")).unwrap())
).unwrap(); .unwrap();
let f0 = spec["f0"].as_u64().unwrap_or(0); let f0 = spec["f0"].as_u64().unwrap_or(0);
let f1 = spec["f1"].as_u64().unwrap_or(0); let f1 = spec["f1"].as_u64().unwrap_or(0);
(f0, f1) (f0, f1)
@@ -77,10 +77,7 @@ fn single_sequence_f0_f1_match() {
#[test] #[test]
fn two_sequences_f0_f1_match() { fn two_sequences_f0_f1_match() {
let seqs: &[&[u8]] = &[ let seqs: &[&[u8]] = &[b"ACGTACGTACGTACGTACGT", b"TGCATGCATGCATGCATGCA"];
b"ACGTACGTACGTACGTACGT",
b"TGCATGCATGCATGCATGCA",
];
let (ef0, ef1) = direct_counts(seqs); let (ef0, ef1) = direct_counts(seqs);
let (gf0, gf1) = pipeline_counts(seqs); let (gf0, gf1) = pipeline_counts(seqs);
assert_eq!(gf0, ef0, "f0 wrong: expected {ef0}, got {gf0}"); assert_eq!(gf0, ef0, "f0 wrong: expected {ef0}, got {gf0}");
@@ -103,7 +100,11 @@ fn many_sequences_f0_f1_match() {
// multiple minimizer boundaries per sequence. // multiple minimizer boundaries per sequence.
let bases = b"ACGT"; let bases = b"ACGT";
let seqs: Vec<Vec<u8>> = (0..20u32) let seqs: Vec<Vec<u8>> = (0..20u32)
.map(|i| (0..40).map(|j| bases[((i * 7 + j * 3) % 4) as usize]).collect()) .map(|i| {
(0..40)
.map(|j| bases[((i * 7 + j * 3) % 4) as usize])
.collect()
})
.collect(); .collect();
let seq_refs: Vec<&[u8]> = seqs.iter().map(|v| v.as_slice()).collect(); let seq_refs: Vec<&[u8]> = seqs.iter().map(|v| v.as_slice()).collect();
let (ef0, ef1) = direct_counts(&seq_refs); let (ef0, ef1) = direct_counts(&seq_refs);
+11 -5
View File
@@ -3,15 +3,18 @@ use std::path::Path;
use obicompactvec::{PersistentBitMatrix, PersistentCompactIntMatrix}; use obicompactvec::{PersistentBitMatrix, PersistentCompactIntMatrix};
use obikseq::CanonicalKmer; use obikseq::CanonicalKmer;
use obiskio::{SKError, SKResult};
use obilayeredmap::{IndexMode, MphfLayer, OLMError}; use obilayeredmap::{IndexMode, MphfLayer, OLMError};
use obiskio::{SKError, SKResult};
use crate::partition::KmerPartition; use crate::partition::KmerPartitions;
fn olm_to_sk(e: OLMError) -> SKError { fn olm_to_sk(e: OLMError) -> SKError {
match e { match e {
OLMError::Io(io_err) => SKError::Io(io_err), OLMError::Io(io_err) => SKError::Io(io_err),
other => SKError::InvalidData { context: "query", detail: other.to_string() }, other => SKError::InvalidData {
context: "query",
detail: other.to_string(),
},
} }
} }
@@ -70,7 +73,10 @@ impl QueryLayer {
/// slot)` `col_value` point lookup, which this layer's `Sparse` /// slot)` `col_value` point lookup, which this layer's `Sparse`
/// presence matrices paid for badly: each such lookup rebuilt the /// presence matrices paid for badly: each such lookup rebuilt the
/// entire row just to return one cell. /// entire row just to return one cell.
fn nonzero_iter<'a>(&'a self, slots: &'a [usize]) -> Box<dyn Iterator<Item = (usize, usize, u32)> + 'a> { fn nonzero_iter<'a>(
&'a self,
slots: &'a [usize],
) -> Box<dyn Iterator<Item = (usize, usize, u32)> + 'a> {
match self { match self {
QueryLayer::Presence(_, mat) => mat.nonzero_iter(slots), QueryLayer::Presence(_, mat) => mat.nonzero_iter(slots),
QueryLayer::Count(_, mat) => Box::new(mat.nonzero_iter(slots)), QueryLayer::Count(_, mat) => Box::new(mat.nonzero_iter(slots)),
@@ -137,7 +143,7 @@ pub enum QueryHit<'a> {
// ── KmerPartition::query_partition_with ────────────────────────────────────── // ── KmerPartition::query_partition_with ──────────────────────────────────────
impl KmerPartition { impl KmerPartitions {
/// Query a single partition for a pre-deduplicated map of canonical /// Query a single partition for a pre-deduplicated map of canonical
/// k-mers → their occurrences (`seq_idx`, `pos`) in the query batch. /// k-mers → their occurrences (`seq_idx`, `pos`) in the query batch.
/// ///
+36 -17
View File
@@ -1,27 +1,29 @@
use std::path::Path; use std::path::Path;
use obicompactvec::{ use obicompactvec::{
FilterMask, eval_filter_mask, FilterMask, PersistentBitMatrixBuilder, PersistentBitVecBuilder,
PersistentBitMatrixBuilder, PersistentBitVecBuilder, PersistentCompactIntMatrixBuilder, PersistentCompactIntVecBuilder, eval_filter_mask,
PersistentCompactIntMatrixBuilder, PersistentCompactIntVecBuilder,
}; };
use obidebruinj::GraphDeBruijn; use obidebruinj::GraphDeBruijn;
use obikseq::CanonicalKmer; use obikseq::CanonicalKmer;
use obilayeredmap::meta::PartitionMeta; use obilayeredmap::meta::PartitionMeta;
use obilayeredmap::{layer_dir, IndexMode, MphfLayer}; use obilayeredmap::{IndexMode, MphfLayer, layer_dir};
use obiskio::{SKError, SKResult, UnitigFileReader}; use obiskio::{SKError, SKResult, UnitigFileReader};
use crate::common::{load_meta, olm_to_sk}; use crate::common::{load_meta, olm_to_sk};
use crate::filter::KmerFilter; use crate::filter::KmerFilter;
use crate::graph_pipeline::materialize_layer; use crate::graph_pipeline::materialize_layer;
use crate::merge_layer::{MergeMode, SrcLayerData}; use crate::merge_layer::{MergeMode, SrcLayerData};
use crate::partition::KmerPartition; use crate::partition::KmerPartitions;
// ── Builders — pair matrix builder + column builders for one mode ───────────── // ── Builders — pair matrix builder + column builders for one mode ─────────────
enum Builders { enum Builders {
Presence(PersistentBitMatrixBuilder, Vec<PersistentBitVecBuilder>), Presence(PersistentBitMatrixBuilder, Vec<PersistentBitVecBuilder>),
Count(PersistentCompactIntMatrixBuilder, Vec<PersistentCompactIntVecBuilder>), Count(
PersistentCompactIntMatrixBuilder,
Vec<PersistentCompactIntVecBuilder>,
),
} }
impl Builders { impl Builders {
@@ -30,13 +32,18 @@ impl Builders {
MergeMode::Presence => { MergeMode::Presence => {
let mut mat = PersistentBitMatrixBuilder::new(n, dir).map_err(SKError::Io)?; let mut mat = PersistentBitMatrixBuilder::new(n, dir).map_err(SKError::Io)?;
let mut cols = Vec::with_capacity(n_genomes); let mut cols = Vec::with_capacity(n_genomes);
for _ in 0..n_genomes { cols.push(mat.add_col().map_err(SKError::Io)?); } for _ in 0..n_genomes {
cols.push(mat.add_col().map_err(SKError::Io)?);
}
Ok(Builders::Presence(mat, cols)) Ok(Builders::Presence(mat, cols))
} }
MergeMode::Count => { MergeMode::Count => {
let mut mat = PersistentCompactIntMatrixBuilder::new(n, dir).map_err(SKError::Io)?; let mut mat =
PersistentCompactIntMatrixBuilder::new(n, dir).map_err(SKError::Io)?;
let mut cols = Vec::with_capacity(n_genomes); let mut cols = Vec::with_capacity(n_genomes);
for _ in 0..n_genomes { cols.push(mat.add_col().map_err(SKError::Io)?); } for _ in 0..n_genomes {
cols.push(mat.add_col().map_err(SKError::Io)?);
}
Ok(Builders::Count(mat, cols)) Ok(Builders::Count(mat, cols))
} }
} }
@@ -52,11 +59,15 @@ impl Builders {
fn close(self) -> SKResult<()> { fn close(self) -> SKResult<()> {
match self { match self {
Builders::Presence(mat, cols) => { Builders::Presence(mat, cols) => {
for b in cols { b.close().map_err(SKError::Io)?; } for b in cols {
b.close().map_err(SKError::Io)?;
}
mat.close().map_err(SKError::Io) mat.close().map_err(SKError::Io)
} }
Builders::Count(mat, cols) => { Builders::Count(mat, cols) => {
for b in cols { b.close().map_err(SKError::Io)?; } for b in cols {
b.close().map_err(SKError::Io)?;
}
mat.close().map_err(SKError::Io) mat.close().map_err(SKError::Io)
} }
} }
@@ -112,7 +123,9 @@ fn iter_src_kmers_masked(
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");
if !unitigs_path.exists() { continue; } if !unitigs_path.exists() {
continue;
}
let src_data = SrcLayerData::open(&src_layer_dir, mode)?; let src_data = SrcLayerData::open(&src_layer_dir, mode)?;
let mask = try_compute_combined_mask(filters, &src_data, n_genomes)?; let mask = try_compute_combined_mask(filters, &src_data, n_genomes)?;
@@ -127,7 +140,9 @@ fn iter_src_kmers_masked(
filters.iter().all(|f| f.passes(kmer, &row, n_genomes)) filters.iter().all(|f| f.passes(kmer, &row, n_genomes))
} }
}; };
if passes { cb(kmer); } if passes {
cb(kmer);
}
} }
} }
Ok(()) Ok(())
@@ -149,7 +164,9 @@ fn iter_src_layers(
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");
if !unitigs_path.exists() { continue; } if !unitigs_path.exists() {
continue;
}
let src_data = SrcLayerData::open(&src_layer_dir, mode)?; let src_data = SrcLayerData::open(&src_layer_dir, mode)?;
let mask = try_compute_combined_mask(filters, &src_data, n_genomes)?; let mask = try_compute_combined_mask(filters, &src_data, n_genomes)?;
@@ -158,7 +175,9 @@ fn iter_src_layers(
for (kmer, _, _) in reader.iter_indexed_canonical_kmers() { for (kmer, _, _) in reader.iter_indexed_canonical_kmers() {
let slot = src_data.slot(kmer); let slot = src_data.slot(kmer);
if let Some(ref m) = mask { if let Some(ref m) = mask {
if !m.get(slot) { continue; } if !m.get(slot) {
continue;
}
let row = src_data.fill_row_by_slot(slot, n_genomes); let row = src_data.fill_row_by_slot(slot, n_genomes);
cb(kmer, row.into_boxed_slice()); cb(kmer, row.into_boxed_slice());
} else { } else {
@@ -174,7 +193,7 @@ fn iter_src_layers(
// ── KmerPartition::rebuild_partition ───────────────────────────────────────── // ── KmerPartition::rebuild_partition ─────────────────────────────────────────
impl KmerPartition { impl KmerPartitions {
/// Rebuild partition `i` from `src` into `self` (an empty destination partition). /// Rebuild partition `i` from `src` into `self` (an empty destination partition).
/// ///
/// Only k-mers whose per-genome row passes all `filters` are written. /// Only k-mers whose per-genome row passes all `filters` are written.
@@ -184,7 +203,7 @@ impl KmerPartition {
/// `n_genomes` is the number of genome columns in the source (and destination). /// `n_genomes` is the number of genome columns in the source (and destination).
pub fn rebuild_partition( pub fn rebuild_partition(
&self, &self,
src: &KmerPartition, src: &KmerPartitions,
i: usize, i: usize,
filters: &[Box<dyn KmerFilter>], filters: &[Box<dyn KmerFilter>],
mode: MergeMode, mode: MergeMode,
+111 -43
View File
@@ -3,14 +3,13 @@ use std::io;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use obicompactvec::{ use obicompactvec::{
ColGroup, MatrixGroupOps, ColGroup, MatrixGroupOps, PersistentBitMatrix, PersistentBitMatrixBuilder,
PersistentBitMatrix, PersistentBitMatrixBuilder,
PersistentCompactIntMatrix, PersistentCompactIntMatrixBuilder, PersistentCompactIntMatrix, PersistentCompactIntMatrixBuilder,
}; };
use obilayeredmap::OLMError; use obilayeredmap::OLMError;
use obiskio::{SKError, SKResult}; use obiskio::{SKError, SKResult};
use crate::partition::KmerPartition; use crate::partition::KmerPartitions;
// ── AggOp ───────────────────────────────────────────────────────────────────── // ── AggOp ─────────────────────────────────────────────────────────────────────
@@ -43,7 +42,10 @@ pub struct OutputCol {
fn olm_to_sk(e: OLMError) -> SKError { fn olm_to_sk(e: OLMError) -> SKError {
match e { match e {
OLMError::Io(e) => SKError::Io(e), OLMError::Io(e) => SKError::Io(e),
other => SKError::InvalidData { context: "select", detail: other.to_string() }, other => SKError::InvalidData {
context: "select",
detail: other.to_string(),
},
} }
} }
@@ -77,23 +79,43 @@ fn fill_builders(
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 => b.add_col_from (&mat.partial_group_any (&g, threshold).map_err(SKError::Io)?), AggOp::Any => {
AggOp::All => b.add_col_from (&mat.partial_group_all (&g, threshold).map_err(SKError::Io)?), b.add_col_from(&mat.partial_group_any(&g, threshold).map_err(SKError::Io)?)
AggOp::None => b.add_col_from (&mat.partial_group_none(&g, threshold).map_err(SKError::Io)?), }
AggOp::Sum => b.add_col_from_int(&mat.partial_group_sum (&g).map_err(SKError::Io)?), AggOp::All => {
AggOp::Min => b.add_col_from_int(&mat.partial_group_min (&g).map_err(SKError::Io)?), b.add_col_from(&mat.partial_group_all(&g, threshold).map_err(SKError::Io)?)
AggOp::Max => b.add_col_from_int(&mat.partial_group_max (&g).map_err(SKError::Io)?), }
}.map_err(SKError::Io)?; AggOp::None => {
b.add_col_from(&mat.partial_group_none(&g, threshold).map_err(SKError::Io)?)
}
AggOp::Sum => {
b.add_col_from_int(&mat.partial_group_sum(&g).map_err(SKError::Io)?)
}
AggOp::Min => {
b.add_col_from_int(&mat.partial_group_min(&g).map_err(SKError::Io)?)
}
AggOp::Max => {
b.add_col_from_int(&mat.partial_group_max(&g).map_err(SKError::Io)?)
}
}
.map_err(SKError::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(SKError::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(SKError::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(SKError::Io)?),
AggOp::Any => b.add_col_from_bit(&mat.partial_group_any (&g, threshold).map_err(SKError::Io)?), AggOp::Any => 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_any(&g, threshold).map_err(SKError::Io)?,
AggOp::None => b.add_col_from_bit(&mat.partial_group_none(&g, threshold).map_err(SKError::Io)?), ),
}.map_err(SKError::Io)?; AggOp::All => b.add_col_from_bit(
&mat.partial_group_all(&g, threshold).map_err(SKError::Io)?,
),
AggOp::None => b.add_col_from_bit(
&mat.partial_group_none(&g, threshold).map_err(SKError::Io)?,
),
}
.map_err(SKError::Io)?;
} }
} }
} else { } else {
@@ -103,23 +125,43 @@ fn fill_builders(
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 => b.add_col_from (&mat.partial_group_any (&g, 1).map_err(SKError::Io)?), AggOp::Any => {
AggOp::All => b.add_col_from (&mat.partial_group_all (&g, 1).map_err(SKError::Io)?), b.add_col_from(&mat.partial_group_any(&g, 1).map_err(SKError::Io)?)
AggOp::None => b.add_col_from (&mat.partial_group_none(&g, 1).map_err(SKError::Io)?), }
AggOp::Sum => b.add_col_from_int(&mat.partial_group_sum (&g).map_err(SKError::Io)?), AggOp::All => {
AggOp::Min => b.add_col_from_int(&mat.partial_group_min (&g).map_err(SKError::Io)?), b.add_col_from(&mat.partial_group_all(&g, 1).map_err(SKError::Io)?)
AggOp::Max => b.add_col_from_int(&mat.partial_group_max (&g).map_err(SKError::Io)?), }
}.map_err(SKError::Io)?; AggOp::None => {
b.add_col_from(&mat.partial_group_none(&g, 1).map_err(SKError::Io)?)
}
AggOp::Sum => {
b.add_col_from_int(&mat.partial_group_sum(&g).map_err(SKError::Io)?)
}
AggOp::Min => {
b.add_col_from_int(&mat.partial_group_min(&g).map_err(SKError::Io)?)
}
AggOp::Max => {
b.add_col_from_int(&mat.partial_group_max(&g).map_err(SKError::Io)?)
}
}
.map_err(SKError::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(SKError::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(SKError::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(SKError::Io)?),
AggOp::Any => b.add_col_from_bit(&mat.partial_group_any (&g, 1).map_err(SKError::Io)?), AggOp::Any => {
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_any(&g, 1).map_err(SKError::Io)?)
AggOp::None => b.add_col_from_bit(&mat.partial_group_none(&g, 1).map_err(SKError::Io)?), }
}.map_err(SKError::Io)?; AggOp::All => {
b.add_col_from_bit(&mat.partial_group_all(&g, 1).map_err(SKError::Io)?)
}
AggOp::None => {
b.add_col_from_bit(&mat.partial_group_none(&g, 1).map_err(SKError::Io)?)
}
}
.map_err(SKError::Io)?;
} }
} }
} }
@@ -128,7 +170,7 @@ fn fill_builders(
// ── KmerPartition::select_partition ────────────────────────────────────────── // ── KmerPartition::select_partition ──────────────────────────────────────────
impl KmerPartition { impl KmerPartitions {
/// Rewrite the data matrices of partition `i` in `src` into `self`. /// Rewrite the data matrices of partition `i` in `src` into `self`.
/// ///
/// `specs` defines the output columns (projection/aggregation). /// `specs` defines the output columns (projection/aggregation).
@@ -136,7 +178,7 @@ impl KmerPartition {
/// `in_place` — `self` and `src` share the same root; write to temp dirs then swap. /// `in_place` — `self` and `src` share the same root; write to temp dirs then swap.
pub fn select_partition( pub fn select_partition(
&self, &self,
src: &KmerPartition, src: &KmerPartitions,
i: usize, i: usize,
specs: &[OutputCol], specs: &[OutputCol],
_n_src_genomes: usize, _n_src_genomes: usize,
@@ -159,11 +201,17 @@ impl KmerPartition {
fs::create_dir_all(&dst_index_dir)?; fs::create_dir_all(&dst_index_dir)?;
} }
let data_subdir = if output_presence { "presence" } else { "counts" }; let data_subdir = if output_presence {
"presence"
} else {
"counts"
};
for l in 0..n_src_layers { for l in 0..n_src_layers {
let src_layer_dir = src.layer_dir(i, l); let src_layer_dir = src.layer_dir(i, l);
if !src_layer_dir.exists() { continue; } if !src_layer_dir.exists() {
continue;
}
let dst_layer_dir = self.layer_dir(i, l); let dst_layer_dir = self.layer_dir(i, l);
@@ -173,9 +221,13 @@ impl KmerPartition {
// 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).map_err(SKError::Io)?.n() PersistentCompactIntMatrix::open(&src_layer_dir)
.map_err(SKError::Io)?
.n()
} else if presence_dir.exists() { } else if presence_dir.exists() {
PersistentBitMatrix::open(&src_layer_dir).map_err(SKError::Io)?.n() PersistentBitMatrix::open(&src_layer_dir)
.map_err(SKError::Io)?
.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.
if !in_place { if !in_place {
@@ -202,14 +254,28 @@ impl KmerPartition {
fs::create_dir_all(&dst_data_dir)?; fs::create_dir_all(&dst_data_dir)?;
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)?), None) (
Some(PersistentBitMatrixBuilder::new(n, &dst_data_dir).map_err(SKError::Io)?),
None,
)
} else { } else {
(None, Some(PersistentCompactIntMatrixBuilder::new(n, &dst_data_dir).map_err(SKError::Io)?)) (
None,
Some(
PersistentCompactIntMatrixBuilder::new(n, &dst_data_dir)
.map_err(SKError::Io)?,
),
)
}; };
fill_builders( fill_builders(
specs, &src_layer_dir, src_is_count, threshold, output_presence, specs,
dst_bit.as_mut(), dst_int.as_mut(), &src_layer_dir,
src_is_count,
threshold,
output_presence,
dst_bit.as_mut(),
dst_int.as_mut(),
)?; )?;
if output_presence { if output_presence {
@@ -233,7 +299,9 @@ impl KmerPartition {
} }
if !in_place { if !in_place {
src.partition_meta(i)?.save(&dst_index_dir).map_err(olm_to_sk)?; src.partition_meta(i)?
.save(&dst_index_dir)
.map_err(olm_to_sk)?;
} }
Ok(()) Ok(())
@@ -44,8 +44,8 @@ fn query_stats_default_is_zero() {
#[test] #[test]
fn query_partition_with_missing_index_dir_returns_default_stats() { fn query_partition_with_missing_index_dir_returns_default_stats() {
let tmp = tempfile::tempdir().expect("tempdir"); let tmp = tempfile::tempdir().expect("tempdir");
let partition = KmerPartition::create(tmp.path().join("idx"), 2, 21, 9, false) let partition =
.expect("create partition"); KmerPartitions::create(tmp.path().join("idx"), 2, 21, 9, false.expect("create partition");
let mut kmers: HashMap<CanonicalKmer, Vec<KmerDesc>> = HashMap::new(); let mut kmers: HashMap<CanonicalKmer, Vec<KmerDesc>> = HashMap::new();
// Any well-formed canonical k-mer works here — the call must return // Any well-formed canonical k-mer works here — the call must return
@@ -65,8 +65,8 @@ fn query_partition_with_missing_index_dir_returns_default_stats() {
#[test] #[test]
fn query_partition_with_empty_kmers_is_a_noop() { fn query_partition_with_empty_kmers_is_a_noop() {
let tmp = tempfile::tempdir().expect("tempdir"); let tmp = tempfile::tempdir().expect("tempdir");
let partition = KmerPartition::create(tmp.path().join("idx"), 2, 21, 9, false) let partition =
.expect("create partition"); KmerPartitions::create(tmp.path().join("idx"), 2, 21, 9, false.expect("create partition");
let kmers: HashMap<CanonicalKmer, Vec<KmerDesc>> = HashMap::new(); let kmers: HashMap<CanonicalKmer, Vec<KmerDesc>> = HashMap::new();
let stats = partition let stats = partition
+27 -9
View File
@@ -1,13 +1,13 @@
use std::sync::Arc; use std::sync::Arc;
use obikpartitionner::KmerPartition; use obikpartitionner::KmerPartitions;
use obisys::progress_bar; use obisys::progress_bar;
use obikindex::{OKIError, OKIResult};
use obikindex::KmerIndex; use obikindex::KmerIndex;
use obikindex::{OKIError, OKIResult};
use super::cache::PartitionCache; use super::cache::PartitionCache;
use super::family_scan::{scan_layer_families, Selection}; use super::family_scan::{Selection, scan_layer_families};
use super::subsample::EntropyBias; use super::subsample::EntropyBias;
/// IUPAC ambiguity code for a per-genome family presence mask (bit `b` set /// IUPAC ambiguity code for a per-genome family presence mask (bit `b` set
@@ -70,18 +70,26 @@ pub trait SnpAlignmentExt {
/// that sample (or, with `subsample = None`, filter the whole index) /// that sample (or, with `subsample = None`, filter the whole index)
/// by a Gaussian kernel on each family's entropy — see /// by a Gaussian kernel on each family's entropy — see
/// `DevDocMD/architecture/siblings.md`, "Entropy-biased selection". /// `DevDocMD/architecture/siblings.md`, "Entropy-biased selection".
fn snp_pseudo_alignment(&self, subsample: Option<usize>, entropy_bias: Option<EntropyBias>) -> OKIResult<SnpAlignment>; fn snp_pseudo_alignment(
&self,
subsample: Option<usize>,
entropy_bias: Option<EntropyBias>,
) -> OKIResult<SnpAlignment>;
} }
impl SnpAlignmentExt for KmerIndex { impl SnpAlignmentExt for KmerIndex {
fn snp_pseudo_alignment(&self, subsample: Option<usize>, entropy_bias: Option<EntropyBias>) -> OKIResult<SnpAlignment> { fn snp_pseudo_alignment(
&self,
subsample: Option<usize>,
entropy_bias: Option<EntropyBias>,
) -> OKIResult<SnpAlignment> {
let n_parts = self.n_partitions(); let n_parts = self.n_partitions();
let n_genomes = self.meta().genomes.len(); let n_genomes = self.meta().genomes.len();
let with_counts = self.meta().config.with_counts; let with_counts = self.meta().config.with_counts;
let k = self.kmer_size(); let k = self.kmer_size();
let n_bits = n_parts.trailing_zeros() as usize; let n_bits = n_parts.trailing_zeros() as usize;
let partition = KmerPartition::open_with_config( let partition = KmerPartitions::open_with_config(
self.root_path(), self.root_path(),
self.kmer_size(), self.kmer_size(),
self.minimizer_size(), self.minimizer_size(),
@@ -90,7 +98,8 @@ impl SnpAlignmentExt for KmerIndex {
.map_err(OKIError::Partition)?; .map_err(OKIError::Partition)?;
let cache = Arc::new(PartitionCache::build(&partition, n_parts, with_counts)?); let cache = Arc::new(PartitionCache::build(&partition, n_parts, with_counts)?);
let layer_dirs = super::family_scan::sibling_layer_dirs(self)?; let layer_dirs = super::family_scan::sibling_layer_dirs(self)?;
let selections = super::subsample::compute_selections(self, &layer_dirs, subsample, entropy_bias)?; let selections =
super::subsample::compute_selections(self, &layer_dirs, subsample, entropy_bias)?;
let pb = progress_bar("snp_pseudo_alignment", layer_dirs.len() as u64, "layers"); let pb = progress_bar("snp_pseudo_alignment", layer_dirs.len() as u64, "layers");
// One layer at a time, not `par_iter()` over layers — same // One layer at a time, not `par_iter()` over layers — same
@@ -109,14 +118,23 @@ impl SnpAlignmentExt for KmerIndex {
None => Selection::All, None => Selection::All,
Some(set) => Selection::Some(set), Some(set) => Selection::Some(set),
}; };
scan_layer_families(layer_dir, n_parts, n_genomes, with_counts, k, &cache, &selection, |_family_idx, mask, genome_mask| { scan_layer_families(
layer_dir,
n_parts,
n_genomes,
with_counts,
k,
&cache,
&selection,
|_family_idx, mask, genome_mask| {
if mask.family_size() < 2 { if mask.family_size() < 2 {
return; // monomorphic family — no signal, skip return; // monomorphic family — no signal, skip
} }
for (g, &m) in genome_mask.iter().enumerate() { for (g, &m) in genome_mask.iter().enumerate() {
sequences[g].push(iupac_code(m)); sequences[g].push(iupac_code(m));
} }
})?; },
)?;
pb.inc(1); pb.inc(1);
} }
pb.finish_and_clear(); pb.finish_and_clear();
+47 -17
View File
@@ -1,22 +1,22 @@
use std::path::Path; use std::path::Path;
use std::sync::atomic::Ordering;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::Ordering;
use rayon::prelude::*; use rayon::prelude::*;
use obikpartitionner::KmerPartition; use obikpartitionner::KmerPartitions;
use obipipeline::ThrottleGuard;
use obikseq::CanonicalKmer; use obikseq::CanonicalKmer;
use obilayeredmap::MphfLayer; use obilayeredmap::MphfLayer;
use obilayeredmap::meta::IndexMode; use obilayeredmap::meta::IndexMode;
use obipipeline::ThrottleGuard;
use obisys::progress_bar; use obisys::progress_bar;
use obikindex::{OKIError, OKIResult};
use obikindex::KmerIndex; use obikindex::KmerIndex;
use obikindex::{OKIError, 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::{olm_to_ok, FamilyMask, SiblingAnnexBuilder, ANNEX_FILE_NAME}; use super::{ANNEX_FILE_NAME, FamilyMask, SiblingAnnexBuilder, olm_to_ok};
// ── obipipeline data types ───────────────────────────────────────────────── // ── obipipeline data types ─────────────────────────────────────────────────
@@ -78,7 +78,7 @@ impl SiblingAnnexBuildExt for KmerIndex {
let n_parts = self.n_partitions(); let n_parts = self.n_partitions();
let n_bits = n_parts.trailing_zeros() as usize; let n_bits = n_parts.trailing_zeros() as usize;
let partition = KmerPartition::open_with_config( let partition = KmerPartitions::open_with_config(
self.root_path(), self.root_path(),
self.kmer_size(), self.kmer_size(),
self.minimizer_size(), self.minimizer_size(),
@@ -87,7 +87,11 @@ impl SiblingAnnexBuildExt for KmerIndex {
.map_err(OKIError::Partition)?; .map_err(OKIError::Partition)?;
tracing::info!("opening {n_parts} partition(s) for the sibling-annex sweep"); tracing::info!("opening {n_parts} partition(s) for the sibling-annex sweep");
let cache = Arc::new(PartitionCache::build(&partition, n_parts, self.meta().config.with_counts)?); let cache = Arc::new(PartitionCache::build(
&partition,
n_parts,
self.meta().config.with_counts,
)?);
let pb = progress_bar("sibling_annex", n_parts as u64, "partitions"); let pb = progress_bar("sibling_annex", n_parts as u64, "partitions");
let mut total_slots: u64 = 0; let mut total_slots: u64 = 0;
@@ -102,12 +106,19 @@ impl SiblingAnnexBuildExt for KmerIndex {
let mut part_slots: u64 = 0; let mut part_slots: u64 = 0;
for l in 0..meta.n_layers { for l in 0..meta.n_layers {
part_slots += build_layer_sibling_annex( part_slots += build_layer_sibling_annex(
self, &self.partition().layer_dir(part, l), &meta.mode, n_parts, l, &cache, self,
&self.partition().layer_dir(part, l),
&meta.mode,
n_parts,
l,
&cache,
)?; )?;
} }
total_slots += part_slots; total_slots += part_slots;
pb.inc(1); pb.inc(1);
pb.set_message(format!("partition {part}: {part_slots} kmers ({total_slots} total)")); pb.set_message(format!(
"partition {part}: {part_slots} kmers ({total_slots} total)"
));
} }
pb.finish_and_clear(); pb.finish_and_clear();
tracing::info!("sibling annex built — {total_slots} kmers across {n_parts} partitions"); tracing::info!("sibling annex built — {total_slots} kmers across {n_parts} partitions");
@@ -216,14 +227,22 @@ fn build_layer_sibling_annex(
// this same pass, since this is exactly the iteration order the // this same pass, since this is exactly the iteration order the
// annex is keyed on — no separate seeding pass needed. // annex is keyed on — no separate seeding pass needed.
let seed_builder = Arc::clone(&builder); let seed_builder = Arc::clone(&builder);
let batches = mphf.enumerate_kmers_batch(BATCH_SIZE).map(move |(start, kmers)| { let batches = mphf
.enumerate_kmers_batch(BATCH_SIZE)
.map(move |(start, kmers)| {
kmers kmers
.into_iter() .into_iter()
.enumerate() .enumerate()
.map(|(i, kmer)| { .map(|(i, kmer)| {
let base = central_base(kmer, k); let base = central_base(kmer, k);
let bits = if fast_mode { FamilyMask::layer_bits(base, l) } else { FamilyMask::presence_bits(base) }; let bits = if fast_mode {
seed_builder.atomic_slot(start + i).fetch_or(bits, Ordering::Relaxed); FamilyMask::layer_bits(base, l)
} else {
FamilyMask::presence_bits(base)
};
seed_builder
.atomic_slot(start + i)
.fetch_or(bits, Ordering::Relaxed);
(start + i, kmer) (start + i, kmer)
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
@@ -235,7 +254,9 @@ fn build_layer_sibling_annex(
let check_empty = |label: &str| { let check_empty = |label: &str| {
let empty = (0..n).filter(|&slot| builder.get(slot).is_none()).count(); let empty = (0..n).filter(|&slot| builder.get(slot).is_none()).count();
if empty > 0 { if empty > 0 {
tracing::warn!("{label}: {empty} mask slots still empty after resolution (layer {layer_dir:?})"); tracing::warn!(
"{label}: {empty} mask slots still empty after resolution (layer {layer_dir:?})"
);
} }
}; };
let throttled = obipipeline::throttle(batches, n_workers).map(|t| SourceBatch { let throttled = obipipeline::throttle(batches, n_workers).map(|t| SourceBatch {
@@ -274,7 +295,8 @@ fn build_layer_sibling_annex(
// instead of faulting pages in and out as lookups jump between // instead of faulting pages in and out as lookups jump between
// partitions in whatever order the pipeline happens to produce // partitions in whatever order the pipeline happens to produce
// them. Each batch's throttle permit drops here, once accumulated. // them. Each batch's throttle permit drops here, once accumulated.
let mut outgoing: Vec<Vec<(CanonicalKmer, usize, u8)>> = (0..n_parts).map(|_| Vec::new()).collect(); let mut outgoing: Vec<Vec<(CanonicalKmer, usize, u8)>> =
(0..n_parts).map(|_| Vec::new()).collect();
for vb in pipe.apply(throttled, n_workers, capacity) { for vb in pipe.apply(throttled, n_workers, capacity) {
for (dest_partition, variant, source_order, base) in vb.items { for (dest_partition, variant, source_order, base) in vb.items {
outgoing[dest_partition].push((variant, source_order, base)); outgoing[dest_partition].push((variant, source_order, base));
@@ -313,8 +335,14 @@ fn build_layer_sibling_annex(
work.par_iter().for_each(|&(dest, chunk)| { work.par_iter().for_each(|&(dest, chunk)| {
for &(variant, source_order, base) in chunk { for &(variant, source_order, base) in chunk {
if let Some(layer) = cache.find(dest, variant) { if let Some(layer) = cache.find(dest, variant) {
let bits = if fast_mode { FamilyMask::layer_bits(base, layer) } else { FamilyMask::presence_bits(base) }; let bits = if fast_mode {
builder.atomic_slot(source_order).fetch_or(bits, Ordering::Relaxed); FamilyMask::layer_bits(base, layer)
} else {
FamilyMask::presence_bits(base)
};
builder
.atomic_slot(source_order)
.fetch_or(bits, Ordering::Relaxed);
} }
} }
}); });
@@ -339,7 +367,9 @@ fn build_layer_sibling_annex(
// with the now-fully-drained `batches` iterator — this is the only // with the now-fully-drained `batches` iterator — this is the only
// remaining strong reference, so reclaiming plain ownership for the // remaining strong reference, so reclaiming plain ownership for the
// sequential `set` calls below is guaranteed to succeed. // sequential `set` calls below is guaranteed to succeed.
let mut builder = Arc::try_unwrap(builder).unwrap_or_else(|_| panic!("sibling-annex builder still shared after every concurrent phase completed")); let mut builder = Arc::try_unwrap(builder).unwrap_or_else(|_| {
panic!("sibling-annex builder still shared after every concurrent phase completed")
});
for (order, kmer) in mphf.enumerate_kmers() { for (order, kmer) in mphf.enumerate_kmers() {
let final_mask = builder.get(order).expect("seeded by construction"); let final_mask = builder.get(order).expect("seeded by construction");
let minorant = is_minorant(kmer, final_mask, k); let minorant = is_minorant(kmer, final_mask, k);
+40 -39
View File
@@ -2,17 +2,17 @@ use rayon::prelude::*;
use std::path::Path; use std::path::Path;
use obicompactvec::{PersistentBitMatrix, PersistentCompactIntMatrix, PersistentSparseBitMatrix}; use obicompactvec::{PersistentBitMatrix, PersistentCompactIntMatrix};
use obikpartitionner::KmerPartition; use obikpartitionner::KmerPartitions;
use obikseq::CanonicalKmer; use obikseq::CanonicalKmer;
use obilayeredmap::{Layer, OLMResult};
use obilayeredmap::meta::IndexMode; use obilayeredmap::meta::IndexMode;
use obilayeredmap::{Layer, OLMResult};
use obisys::progress_bar; use obisys::progress_bar;
use obikindex::OKIResult; use obikindex::OKIResult;
use super::iter::SiblingLayerExt;
use super::SiblingAnnex; use super::SiblingAnnex;
use super::iter::SiblingLayerExt;
/// Every partition's already-open layers, built **once** for the whole /// Every partition's already-open layers, built **once** for the whole
/// `build_sibling_annex` run and shared (read-only) across every lookup, in /// `build_sibling_annex` run and shared (read-only) across every lookup, in
@@ -41,47 +41,40 @@ use super::SiblingAnnex;
/// going back through the low-level pieces `Layer` already assembles. /// going back through the low-level pieces `Layer` already assembles.
pub(super) enum Mat { pub(super) enum Mat {
Count(Layer<PersistentCompactIntMatrix>), Count(Layer<PersistentCompactIntMatrix>),
/// Dense *or* sparse (`pack --sparse`d) presence — `PersistentBitMatrix`
/// itself is a 4-way enum (`Columnar`/`Packed`/`Sparse`/`Implicit`) that
/// already auto-detects sparse storage in its own `open()` (checks
/// `presence/sparse_meta.json`) and dispatches every method
/// (`row`/`fill_row`/`nonzero_iter`/...) across all four internally —
/// see `DevDocMD/implementation/partition_layer_cache.md`. A separate
/// `SparsePresence(Layer<PersistentSparseBitMatrix>)` arm used to exist
/// here, opened via its own `presence/is_multi.prsb` check; it
/// predated `PersistentBitMatrix` growing native sparse support and
/// was pure duplication by the time it was removed — every method
/// below dispatched it identically to this arm.
Presence(Layer<PersistentBitMatrix>), Presence(Layer<PersistentBitMatrix>),
/// Same role as `Presence`, over a layer packed by `pack --sparse`
/// (`PersistentSparseBitMatrix`) instead of the dense form — see
/// `DevDocMD/architecture/siblings.md`'s sparse-matrix section. Every
/// `Mat` method below dispatches to the same `Layer<D>` generic code
/// (`D: LayerData`) as `Presence`, so this arm is purely a storage
/// choice, not a behavioural difference.
SparsePresence(Layer<PersistentSparseBitMatrix>),
} }
impl Mat { impl Mat {
/// Open one layer's matrix, auto-detecting count vs. dense-presence vs. /// Open one layer's matrix, auto-detecting count vs. presence from what
/// sparse-presence from what is actually on disk — the single source of /// is actually on disk — the single source of truth for *every* caller
/// truth for *every* caller that opens a layer's own matrix (this /// that opens a layer's own matrix (this module's cross-partition
/// module's cross-partition [`PartitionCache::build`] and /// [`PartitionCache::build`] and `family_scan::scan_layer_families`'s
/// `family_scan::scan_layer_families`'s own-layer lookup alike), so the /// own-layer lookup alike), so the two can never disagree. Sparse vs.
/// two can never disagree about which format a layer's `presence/` was /// dense presence storage is `PersistentBitMatrix::open`'s own concern
/// packed to. Before this existed, `scan_layer_families` open-coded its /// (see the [`Presence`](Mat::Presence) variant's docs), not decided
/// own (non-sparse-aware) copy of this decision: on a `pack --sparse`d /// here.
/// layer, `presence/matrix.pbmx` no longer exists (removed by
/// `pack_sparse_bit_matrix`), so a caller that only checks for it and
/// otherwise unconditionally opens `PersistentBitMatrix` doesn't error —
/// `PersistentBitMatrix::open`'s own auto-detection falls all the way
/// through to the `Implicit` (mono-genome, all-present) case, silently
/// corrupting every read.
pub(super) fn open(layer_dir: &Path, mode: &IndexMode, with_counts: bool) -> OLMResult<Self> { pub(super) fn open(layer_dir: &Path, mode: &IndexMode, with_counts: bool) -> OLMResult<Self> {
if with_counts && layer_dir.join("counts").exists() { if with_counts && layer_dir.join("counts").exists() {
return Layer::<PersistentCompactIntMatrix>::open(layer_dir, mode).map(Mat::Count); return Layer::<PersistentCompactIntMatrix>::open(layer_dir, mode).map(Mat::Count);
} }
if layer_dir.join("presence").join("is_multi.prsb").exists() {
Layer::<PersistentSparseBitMatrix>::open(layer_dir, mode).map(Mat::SparsePresence)
} else {
Layer::<PersistentBitMatrix>::open(layer_dir, mode).map(Mat::Presence) Layer::<PersistentBitMatrix>::open(layer_dir, mode).map(Mat::Presence)
} }
}
fn find_slot(&self, kmer: CanonicalKmer) -> Option<usize> { fn find_slot(&self, kmer: CanonicalKmer) -> Option<usize> {
match self { match self {
Mat::Count(l) => l.find_slot(kmer), Mat::Count(l) => l.find_slot(kmer),
Mat::Presence(l) => l.find_slot(kmer), Mat::Presence(l) => l.find_slot(kmer),
Mat::SparsePresence(l) => l.find_slot(kmer),
} }
} }
@@ -95,7 +88,6 @@ impl Mat {
match self { match self {
Mat::Count(l) => l.index_batch(kmers), Mat::Count(l) => l.index_batch(kmers),
Mat::Presence(l) => l.index_batch(kmers), Mat::Presence(l) => l.index_batch(kmers),
Mat::SparsePresence(l) => l.index_batch(kmers),
} }
} }
@@ -110,7 +102,6 @@ impl Mat {
match self { match self {
Mat::Count(l) => l.iter_minorants_batch(annex, batch_size), Mat::Count(l) => l.iter_minorants_batch(annex, batch_size),
Mat::Presence(l) => l.iter_minorants_batch(annex, batch_size), Mat::Presence(l) => l.iter_minorants_batch(annex, batch_size),
Mat::SparsePresence(l) => l.iter_minorants_batch(annex, batch_size),
} }
} }
@@ -118,7 +109,6 @@ impl Mat {
match self { match self {
Mat::Count(l) => l.n_cols(), Mat::Count(l) => l.n_cols(),
Mat::Presence(l) => l.n_cols(), Mat::Presence(l) => l.n_cols(),
Mat::SparsePresence(l) => l.n_cols(),
} }
} }
@@ -138,7 +128,6 @@ impl Mat {
pub(super) fn fill_sub_matrix_carries(&self, slots: &[usize], out: &mut [Vec<bool>]) { pub(super) fn fill_sub_matrix_carries(&self, slots: &[usize], out: &mut [Vec<bool>]) {
match self { match self {
Mat::Presence(l) => l.fill_sub_matrix(slots, out), Mat::Presence(l) => l.fill_sub_matrix(slots, out),
Mat::SparsePresence(l) => l.fill_sub_matrix(slots, out),
Mat::Count(l) => { Mat::Count(l) => {
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); l.fill_sub_matrix(slots, &mut counts);
@@ -169,7 +158,11 @@ pub(super) struct PartitionCache {
} }
impl PartitionCache { impl PartitionCache {
pub(super) fn build(partition: &KmerPartition, n_parts: usize, with_counts: bool) -> OKIResult<Self> { pub(super) fn build(
partition: &KmerPartitions,
n_parts: usize,
with_counts: bool,
) -> OKIResult<Self> {
let pb = progress_bar("open_partitions", n_parts as u64, "partitions"); let pb = progress_bar("open_partitions", n_parts as u64, "partitions");
let built: Vec<(Vec<Mat>, usize)> = (0..n_parts) let built: Vec<(Vec<Mat>, usize)> = (0..n_parts)
.into_par_iter() .into_par_iter()
@@ -182,7 +175,10 @@ impl PartitionCache {
let meta = partition.partition_meta(part)?; let meta = partition.partition_meta(part)?;
let mut mats = Vec::with_capacity(meta.n_layers); let mut mats = Vec::with_capacity(meta.n_layers);
for l in 0..meta.n_layers { for l in 0..meta.n_layers {
let Ok(mat) = Mat::open(&partition.layer_dir(part, l), &meta.mode, with_counts) else { continue }; let Ok(mat) = Mat::open(&partition.layer_dir(part, l), &meta.mode, with_counts)
else {
continue;
};
mats.push(mat); mats.push(mat);
} }
pb.inc(1); pb.inc(1);
@@ -257,7 +253,9 @@ impl PartitionCache {
n_genomes: usize, n_genomes: usize,
mut on_hit: impl FnMut(usize, u8, usize), mut on_hit: impl FnMut(usize, u8, usize),
) { ) {
let Some(mats) = self.mats.get(dest_partition) else { return }; let Some(mats) = self.mats.get(dest_partition) else {
return;
};
// First hit wins, same semantics as the old per-query loop (a // First hit wins, same semantics as the old per-query loop (a
// variant present in an earlier layer shadows later ones). // variant present in an earlier layer shadows later ones).
@@ -297,7 +295,9 @@ impl PartitionCache {
n_genomes: usize, n_genomes: usize,
mut on_hit: impl FnMut(usize, u8, usize), mut on_hit: impl FnMut(usize, u8, usize),
) { ) {
let Some(mats) = self.mats.get(dest_partition) else { return }; let Some(mats) = self.mats.get(dest_partition) else {
return;
};
let mut by_layer: Vec<Vec<(CanonicalKmer, usize, u8)>> = vec![Vec::new(); mats.len()]; let mut by_layer: Vec<Vec<(CanonicalKmer, usize, u8)>> = vec![Vec::new(); mats.len()];
for &(variant, family_idx, base, layer) in queries { for &(variant, family_idx, base, layer) in queries {
@@ -311,7 +311,8 @@ impl PartitionCache {
continue; continue;
} }
let mat = &mats[li]; let mat = &mats[li];
let variants: Vec<CanonicalKmer> = entries.iter().map(|&(variant, _, _)| variant).collect(); let variants: Vec<CanonicalKmer> =
entries.iter().map(|&(variant, _, _)| variant).collect();
let slots = mat.index_batch(&variants); let slots = mat.index_batch(&variants);
let hits: Vec<(usize, usize, u8)> = slots let hits: Vec<(usize, usize, u8)> = slots
.into_iter() .into_iter()
+25 -8
View File
@@ -2,15 +2,15 @@ use std::sync::Arc;
use ndarray::Array2; use ndarray::Array2;
use obikpartitionner::KmerPartition; use obikpartitionner::KmerPartitions;
use obisys::progress_bar; use obisys::progress_bar;
use obikindex::{OKIError, OKIResult};
use obikindex::KmerIndex; use obikindex::KmerIndex;
use obikindex::{OKIError, OKIResult};
use super::cache::PartitionCache; use super::cache::PartitionCache;
use super::distance::RawSnpDistanceOutput; use super::distance::RawSnpDistanceOutput;
use super::family_scan::{scan_layer_families, Selection}; use super::family_scan::{Selection, scan_layer_families};
/// See [`KmerIndex::cardinality_tally`]. /// See [`KmerIndex::cardinality_tally`].
pub struct CardinalityTally { pub struct CardinalityTally {
@@ -52,11 +52,19 @@ pub trait CardinalityExt {
/// gets the matching restriction via `scan_family_pairs`'s new /// gets the matching restriction via `scan_family_pairs`'s new
/// `variable` flag, rather than a `family_size()` check of its own (it /// `variable` flag, rather than a `family_size()` check of its own (it
/// doesn't have direct access to the family's mask). /// doesn't have direct access to the family's mask).
fn cardinality_tally(&self, raw: &RawSnpDistanceOutput, ratio_ceiling: f64) -> OKIResult<CardinalityTally>; fn cardinality_tally(
&self,
raw: &RawSnpDistanceOutput,
ratio_ceiling: f64,
) -> OKIResult<CardinalityTally>;
} }
impl CardinalityExt for KmerIndex { impl CardinalityExt for KmerIndex {
fn cardinality_tally(&self, raw: &RawSnpDistanceOutput, ratio_ceiling: f64) -> OKIResult<CardinalityTally> { fn cardinality_tally(
&self,
raw: &RawSnpDistanceOutput,
ratio_ceiling: f64,
) -> OKIResult<CardinalityTally> {
let n_parts = self.n_partitions(); let n_parts = self.n_partitions();
let n_genomes = self.meta().genomes.len(); let n_genomes = self.meta().genomes.len();
let with_counts = self.meta().config.with_counts; let with_counts = self.meta().config.with_counts;
@@ -72,7 +80,7 @@ impl CardinalityExt for KmerIndex {
total > 0 && (snp as f64 / total as f64) <= ratio_ceiling total > 0 && (snp as f64 / total as f64) <= ratio_ceiling
}); });
let partition = KmerPartition::open_with_config( let partition = KmerPartitions::open_with_config(
self.root_path(), self.root_path(),
self.kmer_size(), self.kmer_size(),
self.minimizer_size(), self.minimizer_size(),
@@ -89,7 +97,15 @@ impl CardinalityExt for KmerIndex {
// family comes back — no per-layer buffer. // family comes back — no per-layer buffer.
let mut total = [[0u64; 5]; 5]; let mut total = [[0u64; 5]; 5];
for layer_dir in &layer_dirs { for layer_dir in &layer_dirs {
scan_layer_families(layer_dir, n_parts, n_genomes, with_counts, k, &cache, &Selection::All, |_family_idx, mask, genome_mask| { scan_layer_families(
layer_dir,
n_parts,
n_genomes,
with_counts,
k,
&cache,
&Selection::All,
|_family_idx, mask, genome_mask| {
if mask.family_size() < 2 { if mask.family_size() < 2 {
// Fully invariant family (never varies anywhere in // Fully invariant family (never varies anywhere in
// the index) — genome-wide background, not // the index) — genome-wide background, not
@@ -115,7 +131,8 @@ impl CardinalityExt for KmerIndex {
} }
} }
} }
})?; },
)?;
pb.inc(1); pb.inc(1);
} }
pb.finish_and_clear(); pb.finish_and_clear();
+32 -10
View File
@@ -2,14 +2,14 @@ use std::sync::Arc;
use ndarray::Array2; use ndarray::Array2;
use obikpartitionner::KmerPartition; use obikpartitionner::KmerPartitions;
use obisys::progress_bar; use obisys::progress_bar;
use obikindex::{OKIError, OKIResult};
use obikindex::KmerIndex; use obikindex::KmerIndex;
use obikindex::{OKIError, OKIResult};
use super::cache::PartitionCache; use super::cache::PartitionCache;
use super::family_scan::{scan_layer_families, Selection}; use super::family_scan::{Selection, scan_layer_families};
/// Raw p-distance restricted to loci that are single-copy in **both** /// Raw p-distance restricted to loci that are single-copy in **both**
/// genomes of a pair — the "stringent / paralogy-aware" locus eligibility /// genomes of a pair — the "stringent / paralogy-aware" locus eligibility
@@ -72,7 +72,7 @@ where
let k = index.kmer_size(); let k = index.kmer_size();
let n_bits = n_parts.trailing_zeros() as usize; let n_bits = n_parts.trailing_zeros() as usize;
let partition = KmerPartition::open_with_config( let partition = KmerPartitions::open_with_config(
index.root_path(), index.root_path(),
index.kmer_size(), index.kmer_size(),
index.minimizer_size(), index.minimizer_size(),
@@ -90,7 +90,15 @@ where
for layer_dir in &layer_dirs { for layer_dir in &layer_dirs {
let mut acc = zero(); let mut acc = zero();
scan_layer_families(layer_dir, n_parts, n_genomes, with_counts, k, &cache, &Selection::All, |_family_idx, mask, genome_mask| { scan_layer_families(
layer_dir,
n_parts,
n_genomes,
with_counts,
k,
&cache,
&Selection::All,
|_family_idx, mask, genome_mask| {
let variable = mask.family_size() >= 2; let variable = mask.family_size() >= 2;
// Per genome: which single form (if exactly one) it // Per genome: which single form (if exactly one) it
@@ -109,7 +117,8 @@ where
on_pair(&mut acc, i, j, bi, bj, variable); on_pair(&mut acc, i, j, bi, bj, variable);
} }
} }
})?; },
)?;
total = combine(total, acc); total = combine(total, acc);
pb.inc(1); pb.inc(1);
@@ -117,7 +126,7 @@ where
pb.finish_and_clear(); pb.finish_and_clear();
Ok(total) Ok(total)
} }
/// Adds [`raw_snp_distance`](Self::raw_snp_distance) and /// Adds [`raw_snp_distance`](Self::raw_snp_distance) and
/// [`base_pair_tally`](Self::base_pair_tally) to `KmerIndex`. /// [`base_pair_tally`](Self::base_pair_tally) to `KmerIndex`.
@@ -141,7 +150,11 @@ pub trait DistanceExt {
/// keeps aggregate SNP/shared counts per genome pair, not which bases /// keeps aggregate SNP/shared counts per genome pair, not which bases
/// were actually involved at each locus, and the ratio-ceiling filter /// were actually involved at each locus, and the ratio-ceiling filter
/// can only be evaluated once the aggregate counts are known. /// can only be evaluated once the aggregate counts are known.
fn base_pair_tally(&self, raw: &RawSnpDistanceOutput, ratio_ceiling: f64) -> OKIResult<BasePairTally>; fn base_pair_tally(
&self,
raw: &RawSnpDistanceOutput,
ratio_ceiling: f64,
) -> OKIResult<BasePairTally>;
} }
impl DistanceExt for KmerIndex { impl DistanceExt for KmerIndex {
@@ -150,7 +163,12 @@ impl DistanceExt for KmerIndex {
let (snp, shared) = scan_family_pairs( let (snp, shared) = scan_family_pairs(
self, self,
"raw_snp_distance", "raw_snp_distance",
|| (Array2::<u64>::zeros((n_genomes, n_genomes)), Array2::<u64>::zeros((n_genomes, n_genomes))), || {
(
Array2::<u64>::zeros((n_genomes, n_genomes)),
Array2::<u64>::zeros((n_genomes, n_genomes)),
)
},
|(snp, shared), i, j, bi, bj, _variable| { |(snp, shared), i, j, bi, bj, _variable| {
if bi == bj { if bi == bj {
shared[[i, j]] += 1; shared[[i, j]] += 1;
@@ -169,7 +187,11 @@ impl DistanceExt for KmerIndex {
Ok(RawSnpDistanceOutput { snp, shared }) Ok(RawSnpDistanceOutput { snp, shared })
} }
fn base_pair_tally(&self, raw: &RawSnpDistanceOutput, ratio_ceiling: f64) -> OKIResult<BasePairTally> { fn base_pair_tally(
&self,
raw: &RawSnpDistanceOutput,
ratio_ceiling: f64,
) -> OKIResult<BasePairTally> {
let n_genomes = self.meta().genomes.len(); let n_genomes = self.meta().genomes.len();
let included = Array2::from_shape_fn((n_genomes, n_genomes), |(i, j)| { let included = Array2::from_shape_fn((n_genomes, n_genomes), |(i, j)| {
if i == j { if i == j {
+68 -19
View File
@@ -13,14 +13,14 @@ use std::io::{BufWriter, Write};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use obikpartitionner::KmerPartition; use obikpartitionner::KmerPartitions;
use obisys::progress_bar; use obisys::progress_bar;
use obikindex::{OKIError, OKIResult};
use obikindex::KmerIndex; use obikindex::KmerIndex;
use obikindex::{OKIError, OKIResult};
use super::cache::PartitionCache; use super::cache::PartitionCache;
use super::entropy_annex::{EntropyAnnexBuilder, ENTROPY_ANNEX_FILE_NAME}; use super::entropy_annex::{ENTROPY_ANNEX_FILE_NAME, EntropyAnnexBuilder};
use super::family_scan::{Selection, scan_layer_families}; use super::family_scan::{Selection, scan_layer_families};
use super::subsample::EntropyBias; use super::subsample::EntropyBias;
@@ -120,18 +120,28 @@ pub trait ShannonEntropyExt {
/// `DevDocMD/architecture/siblings.md`, "Entropy-biased selection") — /// `DevDocMD/architecture/siblings.md`, "Entropy-biased selection") —
/// useful here specifically to see the biased sample's own entropy /// useful here specifically to see the biased sample's own entropy
/// distribution, not just to speed up a distance computation. /// distribution, not just to speed up a distance computation.
fn shannon_entropy_csv(&self, path: &Path, subsample: Option<usize>, entropy_bias: Option<EntropyBias>) -> OKIResult<()>; fn shannon_entropy_csv(
&self,
path: &Path,
subsample: Option<usize>,
entropy_bias: Option<EntropyBias>,
) -> OKIResult<()>;
} }
impl ShannonEntropyExt for KmerIndex { impl ShannonEntropyExt for KmerIndex {
fn shannon_entropy_csv(&self, path: &Path, subsample: Option<usize>, entropy_bias: Option<EntropyBias>) -> OKIResult<()> { fn shannon_entropy_csv(
&self,
path: &Path,
subsample: Option<usize>,
entropy_bias: Option<EntropyBias>,
) -> OKIResult<()> {
let n_parts = self.n_partitions(); let n_parts = self.n_partitions();
let n_genomes = self.meta().genomes.len(); let n_genomes = self.meta().genomes.len();
let with_counts = self.meta().config.with_counts; let with_counts = self.meta().config.with_counts;
let k = self.kmer_size(); let k = self.kmer_size();
let n_bits = n_parts.trailing_zeros() as usize; let n_bits = n_parts.trailing_zeros() as usize;
let partition = KmerPartition::open_with_config( let partition = KmerPartitions::open_with_config(
self.root_path(), self.root_path(),
self.kmer_size(), self.kmer_size(),
self.minimizer_size(), self.minimizer_size(),
@@ -140,31 +150,53 @@ impl ShannonEntropyExt for KmerIndex {
.map_err(OKIError::Partition)?; .map_err(OKIError::Partition)?;
let cache = Arc::new(PartitionCache::build(&partition, n_parts, with_counts)?); let cache = Arc::new(PartitionCache::build(&partition, n_parts, with_counts)?);
let layer_dirs = super::family_scan::sibling_layer_dirs(self)?; let layer_dirs = super::family_scan::sibling_layer_dirs(self)?;
let selections = super::subsample::compute_selections(self, &layer_dirs, subsample, entropy_bias)?; let selections =
super::subsample::compute_selections(self, &layer_dirs, subsample, entropy_bias)?;
let mut f = BufWriter::new(std::fs::File::create(path).map_err(OKIError::Io)?); let mut f = BufWriter::new(std::fs::File::create(path).map_err(OKIError::Io)?);
writeln!(f, "layer,family_idx,entropy15,entropy4,family_size,n_genomes_present").map_err(OKIError::Io)?; writeln!(
f,
"layer,family_idx,entropy15,entropy4,family_size,n_genomes_present"
)
.map_err(OKIError::Io)?;
let pb = progress_bar("shannon_entropy", layer_dirs.len() as u64, "layers"); let pb = progress_bar("shannon_entropy", layer_dirs.len() as u64, "layers");
for (layer_ord, (layer_dir, layer_selection)) in layer_dirs.iter().zip(selections.iter()).enumerate() { for (layer_ord, (layer_dir, layer_selection)) in
layer_dirs.iter().zip(selections.iter()).enumerate()
{
let selection = match layer_selection { let selection = match layer_selection {
None => Selection::All, None => Selection::All,
Some(set) => Selection::Some(set), Some(set) => Selection::Some(set),
}; };
let mut write_err = None; let mut write_err = None;
scan_layer_families(layer_dir, n_parts, n_genomes, with_counts, k, &cache, &selection, |family_idx, mask, genome_mask| { scan_layer_families(
layer_dir,
n_parts,
n_genomes,
with_counts,
k,
&cache,
&selection,
|family_idx, mask, genome_mask| {
if write_err.is_some() { if write_err.is_some() {
return; return;
} }
if mask.family_size() < 2 { if mask.family_size() < 2 {
return; // monomorphic family — entropy trivially 0, no signal, skip return; // monomorphic family — entropy trivially 0, no signal, skip
} }
let Some((h15, n_present)) = family_entropy(genome_mask) else { return }; let Some((h15, n_present)) = family_entropy(genome_mask) else {
return;
};
let h4 = family_entropy_4(genome_mask).map_or(0.0, |(h, _)| h); let h4 = family_entropy_4(genome_mask).map_or(0.0, |(h, _)| h);
if let Err(e) = writeln!(f, "{layer_ord},{family_idx},{h15:.6},{h4:.6},{},{n_present}", mask.family_size()) { if let Err(e) = writeln!(
f,
"{layer_ord},{family_idx},{h15:.6},{h4:.6},{},{n_present}",
mask.family_size()
) {
write_err = Some(e); write_err = Some(e);
} }
})?; },
)?;
if let Some(e) = write_err { if let Some(e) = write_err {
return Err(OKIError::Io(e)); return Err(OKIError::Io(e));
} }
@@ -194,7 +226,9 @@ impl ShannonEntropyExt for KmerIndex {
/// `scan_layer_families`'s expensive per-genome resolution for the ~98% /// `scan_layer_families`'s expensive per-genome resolution for the ~98%
/// of minorants that are monomorphic only to discard the result. /// of minorants that are monomorphic only to discard the result.
pub(super) fn ensure_entropy_annexes(index: &KmerIndex, layer_dirs: &[PathBuf]) -> OKIResult<()> { pub(super) fn ensure_entropy_annexes(index: &KmerIndex, layer_dirs: &[PathBuf]) -> OKIResult<()> {
let missing: Vec<usize> = layer_dirs.iter().enumerate() let missing: Vec<usize> = layer_dirs
.iter()
.enumerate()
.filter(|(_, dir)| !dir.join(ENTROPY_ANNEX_FILE_NAME).exists()) .filter(|(_, dir)| !dir.join(ENTROPY_ANNEX_FILE_NAME).exists())
.map(|(i, _)| i) .map(|(i, _)| i)
.collect(); .collect();
@@ -208,7 +242,7 @@ pub(super) fn ensure_entropy_annexes(index: &KmerIndex, layer_dirs: &[PathBuf])
let k = index.kmer_size(); let k = index.kmer_size();
let n_bits = n_parts.trailing_zeros() as usize; let n_bits = n_parts.trailing_zeros() as usize;
let partition = KmerPartition::open_with_config( let partition = KmerPartitions::open_with_config(
index.root_path(), index.root_path(),
index.kmer_size(), index.kmer_size(),
index.minimizer_size(), index.minimizer_size(),
@@ -221,15 +255,30 @@ pub(super) fn ensure_entropy_annexes(index: &KmerIndex, layer_dirs: &[PathBuf])
let pb = progress_bar("entropy_annex_build", missing.len() as u64, "layers"); let pb = progress_bar("entropy_annex_build", missing.len() as u64, "layers");
for &li in &missing { for &li in &missing {
let layer_dir = &layer_dirs[li]; let layer_dir = &layer_dirs[li];
let mut builder = EntropyAnnexBuilder::new(minorant_counts[li] as usize, &layer_dir.join(ENTROPY_ANNEX_FILE_NAME)) let mut builder = EntropyAnnexBuilder::new(
minorant_counts[li] as usize,
&layer_dir.join(ENTROPY_ANNEX_FILE_NAME),
)
.map_err(OKIError::Io)?; .map_err(OKIError::Io)?;
let eligible = super::subsample::non_monomorphic_selection_layer(layer_dir)?; let eligible = super::subsample::non_monomorphic_selection_layer(layer_dir)?;
scan_layer_families(layer_dir, n_parts, n_genomes, with_counts, k, &cache, &Selection::Some(&eligible), |family_idx, mask, genome_mask| { scan_layer_families(
debug_assert!(mask.family_size() >= 2, "Selection::Some(eligible) must only visit non-monomorphic minorants"); layer_dir,
n_parts,
n_genomes,
with_counts,
k,
&cache,
&Selection::Some(&eligible),
|family_idx, mask, genome_mask| {
debug_assert!(
mask.family_size() >= 2,
"Selection::Some(eligible) must only visit non-monomorphic minorants"
);
if let Some((h15, _)) = family_entropy(genome_mask) { if let Some((h15, _)) = family_entropy(genome_mask) {
builder.set(family_idx, h15 as f32); builder.set(family_idx, h15 as f32);
} }
})?; },
)?;
builder.close().map_err(OKIError::Io)?; builder.close().map_err(OKIError::Io)?;
pb.inc(1); pb.inc(1);
} }
+35 -12
View File
@@ -29,18 +29,18 @@ use std::sync::Arc;
use ndarray::Array2; use ndarray::Array2;
use obikpartitionner::KmerPartition; use obikpartitionner::KmerPartitions;
use obisys::progress_bar; use obisys::progress_bar;
use obikindex::{OKIError, OKIResult};
use obikindex::KmerIndex; use obikindex::KmerIndex;
use obikindex::{OKIError, OKIResult};
use super::alignment::{iupac_code, SnpAlignment}; use super::alignment::{SnpAlignment, iupac_code};
use super::cache::PartitionCache; use super::cache::PartitionCache;
use super::cardinality::CardinalityTally; use super::cardinality::CardinalityTally;
use super::distance::{BasePairTally, RawSnpDistanceOutput}; use super::distance::{BasePairTally, RawSnpDistanceOutput};
use super::family_scan::{scan_layer_families, sibling_layer_dirs, Selection}; use super::family_scan::{Selection, scan_layer_families, sibling_layer_dirs};
use super::subsample::{compute_selections, EntropyBias}; use super::subsample::{EntropyBias, compute_selections};
/// Every output the `--sankoff`/`--tnt`/`--phyg`/`--iqtree` pipeline needs, /// Every output the `--sankoff`/`--tnt`/`--phyg`/`--iqtree` pipeline needs,
/// computed together from one shared, possibly-subsampled/entropy-biased /// computed together from one shared, possibly-subsampled/entropy-biased
@@ -85,7 +85,7 @@ impl SankoffBundleExt for KmerIndex {
let k = self.kmer_size(); let k = self.kmer_size();
let n_bits = n_parts.trailing_zeros() as usize; let n_bits = n_parts.trailing_zeros() as usize;
let partition = KmerPartition::open_with_config( let partition = KmerPartitions::open_with_config(
self.root_path(), self.root_path(),
self.kmer_size(), self.kmer_size(),
self.minimizer_size(), self.minimizer_size(),
@@ -108,7 +108,15 @@ impl SankoffBundleExt for KmerIndex {
None => Selection::All, None => Selection::All,
Some(set) => Selection::Some(set), Some(set) => Selection::Some(set),
}; };
scan_layer_families(layer_dir, n_parts, n_genomes, with_counts, k, &cache, &selection, |_family_idx, _mask, genome_mask| { scan_layer_families(
layer_dir,
n_parts,
n_genomes,
with_counts,
k,
&cache,
&selection,
|_family_idx, _mask, genome_mask| {
let single_form = |g: usize| -> Option<u8> { let single_form = |g: usize| -> Option<u8> {
let m = genome_mask[g]; let m = genome_mask[g];
(m.count_ones() == 1).then(|| m.trailing_zeros() as u8) (m.count_ones() == 1).then(|| m.trailing_zeros() as u8)
@@ -126,7 +134,8 @@ impl SankoffBundleExt for KmerIndex {
} }
} }
} }
})?; },
)?;
pb.inc(1); pb.inc(1);
} }
pb.finish_and_clear(); pb.finish_and_clear();
@@ -168,7 +177,15 @@ impl SankoffBundleExt for KmerIndex {
None => Selection::All, None => Selection::All,
Some(set) => Selection::Some(set), Some(set) => Selection::Some(set),
}; };
scan_layer_families(layer_dir, n_parts, n_genomes, with_counts, k, &cache, &selection, |_family_idx, mask, genome_mask| { scan_layer_families(
layer_dir,
n_parts,
n_genomes,
with_counts,
k,
&cache,
&selection,
|_family_idx, mask, genome_mask| {
let variable = mask.family_size() >= 2; let variable = mask.family_size() >= 2;
// base-pair tally — same pairwise single-form resolution as pass A. // base-pair tally — same pairwise single-form resolution as pass A.
@@ -222,15 +239,21 @@ impl SankoffBundleExt for KmerIndex {
sequences[g].push(iupac_code(m)); sequences[g].push(iupac_code(m));
} }
} }
})?; },
)?;
pb.inc(1); pb.inc(1);
} }
pb.finish_and_clear(); pb.finish_and_clear();
Ok(SankoffBundle { Ok(SankoffBundle {
raw, raw,
base_pair_tally: BasePairTally { counts: bp_counts, same: bp_same }, base_pair_tally: BasePairTally {
cardinality_tally: CardinalityTally { counts: card_counts }, counts: bp_counts,
same: bp_same,
},
cardinality_tally: CardinalityTally {
counts: card_counts,
},
alignment: SnpAlignment { sequences }, alignment: SnpAlignment { sequences },
}) })
} }
+18 -7
View File
@@ -2,16 +2,16 @@ use std::sync::Arc;
use rayon::prelude::*; use rayon::prelude::*;
use obikpartitionner::KmerPartition; use obikpartitionner::KmerPartitions;
use obisys::progress_bar; use obisys::progress_bar;
use obikindex::{OKIError, OKIResult};
use obikindex::KmerIndex; use obikindex::KmerIndex;
use obikindex::{OKIError, OKIResult};
use super::ANNEX_FILE_NAME; use super::ANNEX_FILE_NAME;
use super::SiblingAnnex; use super::SiblingAnnex;
use super::cache::PartitionCache; use super::cache::PartitionCache;
use super::family_scan::{scan_layer_families, Selection}; use super::family_scan::{Selection, scan_layer_families};
/// Distribution of family sizes (1-4), read back from an already-built /// Distribution of family sizes (1-4), read back from an already-built
/// annex (see [`super::build::SiblingAnnexBuildExt::build_sibling_annex`]) /// annex (see [`super::build::SiblingAnnexBuildExt::build_sibling_annex`])
@@ -81,7 +81,9 @@ impl SiblingStatsExt for KmerIndex {
|mut counts, layer_dir| -> OKIResult<[u64; 4]> { |mut counts, layer_dir| -> OKIResult<[u64; 4]> {
let annex = SiblingAnnex::open(&layer_dir.join(ANNEX_FILE_NAME))?; let annex = SiblingAnnex::open(&layer_dir.join(ANNEX_FILE_NAME))?;
for slot in 0..annex.len() { for slot in 0..annex.len() {
let Some(mask) = annex.get(slot) else { continue }; let Some(mask) = annex.get(slot) else {
continue;
};
if !mask.is_minorant() { if !mask.is_minorant() {
continue; // family tallied once, at its minorant continue; // family tallied once, at its minorant
} }
@@ -112,7 +114,7 @@ impl SiblingStatsExt for KmerIndex {
// Same whole-run cache as `build_sibling_annex` — see its docs for // Same whole-run cache as `build_sibling_annex` — see its docs for
// why re-opening per lookup (or per call to a batching helper) is // why re-opening per lookup (or per call to a batching helper) is
// not good enough on a real index. // not good enough on a real index.
let partition = KmerPartition::open_with_config( let partition = KmerPartitions::open_with_config(
self.root_path(), self.root_path(),
self.kmer_size(), self.kmer_size(),
self.minimizer_size(), self.minimizer_size(),
@@ -131,7 +133,15 @@ impl SiblingStatsExt for KmerIndex {
..Default::default() ..Default::default()
}; };
for layer_dir in &layer_dirs { for layer_dir in &layer_dirs {
scan_layer_families(layer_dir, n_parts, n_genomes, with_counts, k, &cache, &Selection::All, |_family_idx, mask, genome_mask| { scan_layer_families(
layer_dir,
n_parts,
n_genomes,
with_counts,
k,
&cache,
&Selection::All,
|_family_idx, mask, genome_mask| {
// "Genome g represents this family" means g carries // "Genome g represents this family" means g carries
// *any* of its members, not just the minorant's own — // *any* of its members, not just the minorant's own —
// `genome_mask[g] != 0` is exactly that. // `genome_mask[g] != 0` is exactly that.
@@ -142,7 +152,8 @@ impl SiblingStatsExt for KmerIndex {
stats.per_genome[g][s] += 1; stats.per_genome[g][s] += 1;
} }
} }
})?; },
)?;
pb.inc(1); pb.inc(1);
} }
pb.finish_and_clear(); pb.finish_and_clear();