refactor: relocate partition iterator and expose graph builder API

Move the partition iterator implementation from obikdump to obikfilter, updating imports and exposing the type publicly. Add obikidxcache as a local dependency for obikfilter. In obikindexer, expose the new build_layer_from_kmers function to enable shared graph-building logic across pipelines without code duplication.
This commit is contained in:
Eric Coissac
2026-08-26 09:39:02 +02:00
parent 16ade823d6
commit 294f132a0a
8 changed files with 35 additions and 14 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ use obikindex::KmerIndex;
use obikidxcache::index_cache::IndexCache;
use obikfilter::KmerFilter;
use crate::partition_iter::FilteredPartitionIter;
use obikfilter::FilteredPartitionIter;
/// Raw content export of a `KmerIndex` — `KmerIndex` is a foreign type
/// (`obikindex`), so this is an extension trait rather than an inherent `impl`.
-3
View File
@@ -6,6 +6,3 @@
//! reverse), same pattern as `obikindexer`/`obikquery`.
mod dump;
mod partition_iter;
pub use partition_iter::FilteredPartitionIter;
-126
View File
@@ -1,126 +0,0 @@
//! Filtered, batch-oriented iteration over an already-cached index's
//! partitions/layers — the read side of `obikfilter`'s `KmerFilter`s.
//! `IndexCache` is a foreign type (`obikidxcache`), so this is an extension
//! trait rather than an inherent `impl`.
//!
//! Only meaningful on a *complete* source index: `IndexCache` panics if a
//! layer is missing, which a finished index never has. Never use this on a
//! destination index still being built (see `obikmerge::partition_merge`,
//! which follows the same source-only-cache rule).
use obikindex::OKIResult;
use obikindex::layer::{KmerLayer, LayerContent};
use obikidxcache::index_cache::IndexCache;
use obikseq::CanonicalKmer;
use obikfilter::{KmerFilter, passes_all};
/// Kmers pulled per batch from a layer before filtering — keeps matrix reads
/// grouped by (partition, layer) for locality instead of hopping row to row
/// across the index. Same convention as `obikphylo::siblings::build`.
const BATCH_SIZE: usize = 32768;
pub trait FilteredPartitionIter {
/// Iterate all indexed kmers in partition `part`, calling `cb(kmer, row)` for each
/// kmer that passes every filter in `filters`.
///
/// `use_counts = true` → reads count columns (u32 values per genome), only
/// meaningful for `Count` layers. `use_counts = false` → reads presence
/// columns, converted to 0/1 u32 (works for both `Count` and `Presence`
/// layers — counts collapse to presence).
///
/// Returns `Ok(true)` if all kmers were visited, `Ok(false)` if the callback halted.
fn iter_partition_kmers(
&self,
part: usize,
use_counts: bool,
n_genomes: usize,
filters: &[Box<dyn KmerFilter>],
cb: impl FnMut(CanonicalKmer, Box<[u32]>) -> bool,
) -> OKIResult<bool>;
/// Like [`iter_partition_kmers`](Self::iter_partition_kmers) but the callback
/// also receives `(partition, layer)` indices, enabling debug output that
/// identifies where each kmer was stored.
fn iter_partition_kmers_located(
&self,
part: usize,
use_counts: bool,
n_genomes: usize,
filters: &[Box<dyn KmerFilter>],
cb: impl FnMut(usize, usize, CanonicalKmer, Box<[u32]>) -> bool,
) -> OKIResult<bool>;
}
impl FilteredPartitionIter for IndexCache<'_> {
fn iter_partition_kmers(
&self,
part: usize,
use_counts: bool,
n_genomes: usize,
filters: &[Box<dyn KmerFilter>],
mut cb: impl FnMut(CanonicalKmer, Box<[u32]>) -> bool,
) -> OKIResult<bool> {
for l in 0..self.n_layer(part).unwrap_or(0) {
let layer = self.get_layer(part, l).expect("layer within n_layer(part)");
if !iter_layer_kmers(layer, use_counts, n_genomes, filters, &mut |kmer, row| cb(kmer, row))? {
return Ok(false);
}
}
Ok(true)
}
fn iter_partition_kmers_located(
&self,
part: usize,
use_counts: bool,
n_genomes: usize,
filters: &[Box<dyn KmerFilter>],
mut cb: impl FnMut(usize, usize, CanonicalKmer, Box<[u32]>) -> bool,
) -> OKIResult<bool> {
for l in 0..self.n_layer(part).unwrap_or(0) {
let layer = self.get_layer(part, l).expect("layer within n_layer(part)");
if !iter_layer_kmers(layer, use_counts, n_genomes, filters, &mut |kmer, row| cb(part, l, kmer, row))? {
return Ok(false);
}
}
Ok(true)
}
}
/// Batch-and-transpose one layer's kmers into per-kmer filtered rows.
/// Returns `Ok(false)` if `cb` asked to stop early.
fn iter_layer_kmers(
layer: &KmerLayer,
use_counts: bool,
n_genomes: usize,
filters: &[Box<dyn KmerFilter>],
cb: &mut dyn FnMut(CanonicalKmer, Box<[u32]>) -> bool,
) -> OKIResult<bool> {
let read_counts = use_counts && matches!(layer.content(), LayerContent::Count);
for kmers in layer.iter_kmers_batch(BATCH_SIZE) {
// Kmers come straight from this layer's own iterator, so every one
// is a guaranteed member — a raw hash is enough, no membership
// recheck needed (see `KmerLayer::hash_batch`'s own doc).
let slots = layer.hash_batch(&kmers);
let cols: Vec<Vec<u32>> = if read_counts {
let mut cols: Vec<Vec<u32>> = vec![Vec::new(); n_genomes];
layer.fill_sub_matrix(&slots, &mut cols);
cols
} else {
let mut bool_cols: Vec<Vec<bool>> = vec![Vec::new(); n_genomes];
layer.fill_sub_matrix_carries(&slots, &mut bool_cols);
bool_cols.iter().map(|c| c.iter().map(|&b| b as u32).collect()).collect()
};
for (i, kmer) in kmers.into_iter().enumerate() {
let row: Box<[u32]> = cols.iter().map(|c| c[i]).collect();
if passes_all(filters, kmer, &row, n_genomes) && !cb(kmer, row) {
return Ok(false);
}
}
}
Ok(true)
}