Push zunrplorkwkt #70
@@ -218,29 +218,28 @@ impl IndexMeta {
|
||||
self.write_full(&on_disk)
|
||||
}
|
||||
|
||||
// /// Overwrite `config` and the whole `genomes` list at once, preserving
|
||||
// /// `state`. `config` otherwise never changes once an index exists —
|
||||
// /// this is the deliberate, rare exception for construction paths that
|
||||
// /// legitimately rewrite it in place (`select_in_place`, `reindex`).
|
||||
// /// The caller must refresh its own cached `Arc<IndexMeta>` afterward
|
||||
// /// (e.g. `self.meta = Arc::new(IndexMeta::open(self)?)`) — this method
|
||||
// /// only updates the file, it has no way to reach back into whatever
|
||||
// /// `KmerIndex` holds it.
|
||||
// /// TODO: This methode is strange
|
||||
// pub(crate) fn rewrite_config(
|
||||
// &self,
|
||||
// config: IndexConfig,
|
||||
// genomes: Vec<GenomeInfo>,
|
||||
// ) -> io::Result<()> {
|
||||
// let _guard = self.lock.write().unwrap();
|
||||
// let state = Self::read_full(&self.root_path)?.state;
|
||||
// self.write_full(&IndexMetadata {
|
||||
// version: self.version,
|
||||
// config,
|
||||
// genomes,
|
||||
// state,
|
||||
// })
|
||||
// }
|
||||
/// Update the evidence representation in place — `evidence`, and (for
|
||||
/// `Exact`) its block size. The only `IndexConfig` fields a reindex is
|
||||
/// ever allowed to touch: `kmer_size`/`minimizer_size`/`n_bits`/
|
||||
/// `with_counts` are baked into the already-built MPHF/unitigs/matrices
|
||||
/// and can never change post-build, unlike the evidence bundle, which
|
||||
/// is derived from `unitigs.bin` + `mphf.bin` alone and can be
|
||||
/// rebuilt in a different form at any time (see
|
||||
/// `obikindexer::IndexBuilder`).
|
||||
///
|
||||
/// The caller must refresh its own cached `Arc<IndexMeta>` afterward
|
||||
/// (e.g. `self.meta = Arc::new(IndexMeta::open(self)?)`) — this method
|
||||
/// only updates the file, it has no way to reach back into whatever
|
||||
/// `KmerIndex` holds it.
|
||||
pub fn set_evidence(&self, evidence: IndexMode, block_bits: u8) -> io::Result<()> {
|
||||
let _guard = self.lock.write().unwrap();
|
||||
let mut on_disk = Self::read_full(&self.root_path)?;
|
||||
on_disk.config.evidence = evidence;
|
||||
if matches!(on_disk.config.evidence, IndexMode::Exact) {
|
||||
on_disk.config.block_bits = block_bits;
|
||||
}
|
||||
self.write_full(&on_disk)
|
||||
}
|
||||
|
||||
pub fn set_state(&self, state: IndexState) -> io::Result<()> {
|
||||
let _guard = self.lock.write().unwrap();
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
//! `IndexBuilder` — public, idempotent evidence-construction primitives
|
||||
//! for an already-built `KmerLayer` (`unitigs.bin` + `mphf.bin` already on
|
||||
//! disk; only the evidence bundle — `evidence.bin`/`unitigs.bin.idx` and/or
|
||||
//! `fingerprint.bin` — is touched). Unlike `PrivateBuilder` (this module's
|
||||
//! private sibling), these are meant for callers outside this crate that
|
||||
//! need to add or convert a layer's evidence after the fact — today,
|
||||
//! `obikrebuild::reindex`.
|
||||
//!
|
||||
//! `KmerLayer` itself (`obikindex`) stays a pure data-model type: it
|
||||
//! exposes paths and reads content, it never builds anything. Construction
|
||||
//! lives here instead, in the crate whose whole purpose is building index
|
||||
//! content — same reasoning as `PrivateBuilder`, just public.
|
||||
|
||||
use obikindex::OKIResult;
|
||||
use obikindex::layer::fingerprint::FingerprintVec;
|
||||
use obikindex::layer::{KmerLayer, MphfLayer};
|
||||
|
||||
pub trait IndexBuilder {
|
||||
/// Build this layer's exact evidence bundle (`evidence.bin` +
|
||||
/// `unitigs.bin.idx`) if not already present.
|
||||
///
|
||||
/// No-op (`Ok(0)`) if exact evidence already exists — both `Exact` and
|
||||
/// `Hybrid` layers already carry it.
|
||||
fn build_exact_evidence(&self, block_bits: u8) -> OKIResult<usize>;
|
||||
|
||||
/// Build this layer's approximate (fingerprint) evidence with `b` bits
|
||||
/// per slot and Findere parameter `z`, if not already present with a
|
||||
/// matching `b`.
|
||||
///
|
||||
/// No-op (`Ok(0)`) if `Approx`/`Hybrid` evidence with the same `b`
|
||||
/// already exists — rebuilds it (same file, same `b`, whatever `z`)
|
||||
/// otherwise. `z` is never persisted (see `IndexMode::Approx`'s own
|
||||
/// doc): it only governs how many consecutive stored k-mers a query
|
||||
/// must match, so changing it alone is not something this method has
|
||||
/// any file-level evidence to detect — the caller decides.
|
||||
fn build_approx_evidence(&self, b: u8, z: u8) -> OKIResult<usize>;
|
||||
|
||||
/// Build whichever of the exact/approximate bundles this layer is
|
||||
/// still missing, so it ends up carrying both (`Hybrid`).
|
||||
///
|
||||
/// Just the composition of `build_exact_evidence` and
|
||||
/// `build_approx_evidence` — each is already independently idempotent,
|
||||
/// so there is no extra "what's already there" decision to make here.
|
||||
fn build_hybrid_evidence(&self, b: u8, z: u8, block_bits: u8) -> OKIResult<usize>;
|
||||
}
|
||||
|
||||
impl IndexBuilder for KmerLayer {
|
||||
fn build_exact_evidence(&self, block_bits: u8) -> OKIResult<usize> {
|
||||
if self.evidence_path().exists() {
|
||||
return Ok(0);
|
||||
}
|
||||
MphfLayer::build_exact_evidence(self.dir(), block_bits)
|
||||
}
|
||||
|
||||
fn build_approx_evidence(&self, b: u8, z: u8) -> OKIResult<usize> {
|
||||
let fingerprint_path = self.fingerprint_path();
|
||||
if fingerprint_path.exists() && FingerprintVec::open(&fingerprint_path)?.b() == b {
|
||||
return Ok(0);
|
||||
}
|
||||
MphfLayer::build_approx_evidence(self.dir(), b, z)
|
||||
}
|
||||
|
||||
fn build_hybrid_evidence(&self, b: u8, z: u8, block_bits: u8) -> OKIResult<usize> {
|
||||
let n_exact = self.build_exact_evidence(block_bits)?;
|
||||
let n_approx = self.build_approx_evidence(b, z)?;
|
||||
Ok(n_exact.max(n_approx))
|
||||
}
|
||||
}
|
||||
@@ -1,245 +1,20 @@
|
||||
//! Construction-only `KmerIndex` operations — semantically meaningless
|
||||
//! once an index is built and being used for reading (query, dump, phylo,
|
||||
//! or as a `merge`/`select`/`rebuild` *source*): sentinel marking, the
|
||||
//! per-partition layer-build primitive, and cleanup of build-time scratch
|
||||
//! files. Private to this crate — no consumer outside the
|
||||
//! indexing-pipeline algorithms in `obikindexer::algorithms` should ever
|
||||
//! need them. See `DevDocMD/implementation/partition_layer_cache.md`,
|
||||
//! "(8)".
|
||||
//! Extension traits over `obikindex`'s foreign types (`KmerIndex`,
|
||||
//! `KmerLayer`) — Rust's orphan rule requires such traits to be defined in
|
||||
//! the crate that implements them for a foreign type, not in `obikindex`
|
||||
//! itself; `obikindex` has no way to make a trait "visible to only some
|
||||
//! crates" from its own side.
|
||||
//!
|
||||
//! Defined here, not in `obikindex`, so it can be genuinely `pub(crate)`
|
||||
//! while extending a foreign type (`KmerIndex`) — Rust's orphan rule
|
||||
//! requires the trait itself to be local to the crate that implements it
|
||||
//! for a foreign type; `obikindex` has no way to make a trait "private
|
||||
//! except to `obikindexer`" from its own side.
|
||||
//!
|
||||
//! Scoped to the six methods exclusive to this crate's own pipeline
|
||||
//! (`partitionner`/`dereplicator`/`counter`/`layer_builder`).
|
||||
//! `clear_output_for_create`/`create_skeleton`/`finalize_indexed`/`state`
|
||||
//! stay inherent on `KmerIndex`, deliberately not moved here: they're also
|
||||
//! called from `obikindex`'s own `merge`/`select`/`rebuild`/`reindex`
|
||||
//! modules, which — living inside `obikindex` itself — could never reach a
|
||||
//! trait defined in `obikindexer` (the dependency only runs
|
||||
//! `obikindexer → obikindex`, never the other way). Moving those four
|
||||
//! would require extracting `merge`/`select`/`rebuild`/`reindex` into
|
||||
//! algorithms first — separate, larger future work, not this one.
|
||||
//! Two traits, two visibilities, one per submodule:
|
||||
//! - [`private_builder::PrivateBuilder`] (`pub(crate)`) — construction-only
|
||||
//! `KmerIndex` operations exclusive to this crate's own indexing
|
||||
//! pipeline (`algorithms::{partitionner,dereplicator,counter,layer_builder}`).
|
||||
//! - [`index_builder::IndexBuilder`] (`pub`, re-exported at the crate
|
||||
//! root) — idempotent per-layer evidence-construction primitives, for
|
||||
//! any external caller that needs to add/convert a layer's evidence
|
||||
//! bundle after the fact (`obikrebuild::reindex`).
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
mod index_builder;
|
||||
mod private_builder;
|
||||
|
||||
use cacheline_ef::{CachelineEf, CachelineEfVec};
|
||||
use epserde::prelude::*;
|
||||
use obicompactvec::{PersistentCompactIntMatrix, PersistentCompactIntVec};
|
||||
use obidebruinj::GraphDeBruijn;
|
||||
use obikindex::layer::IndexMode;
|
||||
use obikindex::layer::{KmerLayer, TypedLayer};
|
||||
use obikindex::{KmerIndex, OKIError, OKIResult};
|
||||
use crate::graph_pipeline::{materialize_layer, write_graph_as_unitigs};
|
||||
use obiskio::{SKError, SKFileMeta, SKFileReader};
|
||||
use ptr_hash::{PtrHash, bucket_fn::CubicEps, hash::Xx64};
|
||||
|
||||
type Mphf = PtrHash<u64, CubicEps, CachelineEfVec<Vec<CachelineEf>>, Xx64, Vec<u8>>;
|
||||
|
||||
pub(crate) trait PrivateBuilder {
|
||||
/// Mark scatter as complete (`IndexState::Scattered`).
|
||||
///
|
||||
/// If no genome label was set at creation time, one is derived from
|
||||
/// the index root directory name (stripped of all extensions). `&self`
|
||||
/// — not `&mut self` anymore: nothing is cached in memory to mutate,
|
||||
/// see `DevDocMD/implementation/partition_layer_cache.md`, "(11)".
|
||||
fn mark_scattered(&self) -> OKIResult<()>;
|
||||
|
||||
/// Mark dereplicate+count as complete (`IndexState::Counted`).
|
||||
fn mark_counted(&self) -> OKIResult<()>;
|
||||
|
||||
/// Partition `i`'s layer 0 — the one and only layer every pipeline
|
||||
/// algorithm in this crate (`partitionner`/`dereplicator`/`counter`/
|
||||
/// `layer_builder`) reads from or writes to; later layers only exist
|
||||
/// after a `merge`, which lives in `obikindex`, not here.
|
||||
fn layer0(&self, i: usize) -> OKIResult<KmerLayer>;
|
||||
|
||||
/// Mark layer construction as complete (`IndexState::Indexed`).
|
||||
fn mark_indexed(&self) -> OKIResult<()>;
|
||||
|
||||
/// Write `spectrums/{label}.json` from an already-computed kmer
|
||||
/// spectrum (`f0`/`f1`/abundance histogram). Takes plain values rather
|
||||
/// than `algorithms::counter::KmerSpectrum` — `KmerIndex` is the data
|
||||
/// model, `Counter` the algorithm that depends on it, not the other
|
||||
/// way around, and this is the only field of that type it actually
|
||||
/// uses.
|
||||
fn write_spectrum(&self, f0: u64, f1: u64, counts: &BTreeMap<u32, u64>) -> OKIResult<()>;
|
||||
|
||||
/// Build the layered MPHF index for partition `i`.
|
||||
///
|
||||
/// Returns the number of canonical k-mers indexed, or 0 if the
|
||||
/// partition has no data or its layer was already built
|
||||
/// (resume-safe).
|
||||
///
|
||||
/// Abundance filtering is applied when `min_ab > 1` or
|
||||
/// `max_ab.is_some()`, using `mphf1.bin` + `counts1.bin` if they
|
||||
/// exist. Count payload is stored iff `with_counts` is true.
|
||||
fn build_index_layer(
|
||||
&self,
|
||||
i: usize,
|
||||
min_ab: u32,
|
||||
max_ab: Option<u32>,
|
||||
with_counts: bool,
|
||||
mode: &IndexMode,
|
||||
block_bits: u8,
|
||||
) -> Result<usize, SKError>;
|
||||
|
||||
/// Remove intermediate build artifacts for partition `i`: dereplicated
|
||||
/// superkmers (+ sidecar), `mphf1.bin`, `counts1.bin`.
|
||||
fn remove_build_artifacts(&self, i: usize);
|
||||
}
|
||||
|
||||
impl PrivateBuilder for KmerIndex {
|
||||
fn mark_scattered(&self) -> OKIResult<()> {
|
||||
self.meta().mark_scattered().map_err(OKIError::Io)
|
||||
}
|
||||
|
||||
fn mark_counted(&self) -> OKIResult<()> {
|
||||
self.meta().mark_counted().map_err(OKIError::Io)
|
||||
}
|
||||
|
||||
fn layer0(&self, i: usize) -> OKIResult<KmerLayer> {
|
||||
self.partition(i)?.layer(0)
|
||||
}
|
||||
|
||||
fn mark_indexed(&self) -> OKIResult<()> {
|
||||
self.meta().mark_indexed().map_err(OKIError::Io)
|
||||
}
|
||||
|
||||
fn write_spectrum(&self, f0: u64, f1: u64, counts: &BTreeMap<u32, u64>) -> OKIResult<()> {
|
||||
let genomes = self.meta().genomes().map_err(OKIError::Io)?;
|
||||
let label = genomes
|
||||
.first()
|
||||
.map(|g| g.label.as_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_owned();
|
||||
let spectrums_dir = self.dir().join("spectrums");
|
||||
fs::create_dir_all(&spectrums_dir)?;
|
||||
let path = spectrums_dir.join(format!("{label}.json"));
|
||||
let spectrum_map: BTreeMap<String, u64> = counts
|
||||
.iter()
|
||||
.map(|(&c, &f)| (format!("{c:010}"), f))
|
||||
.collect();
|
||||
let f = fs::File::create(&path)?;
|
||||
serde_json::to_writer_pretty(
|
||||
f,
|
||||
&serde_json::json!({ "f0": f0, "f1": f1, "spectrum": spectrum_map }),
|
||||
)
|
||||
.map_err(OKIError::Json)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_index_layer(
|
||||
&self,
|
||||
i: usize,
|
||||
min_ab: u32,
|
||||
max_ab: Option<u32>,
|
||||
with_counts: bool,
|
||||
mode: &IndexMode,
|
||||
block_bits: u8,
|
||||
) -> Result<usize, SKError> {
|
||||
let layer0 = self.layer0(i).map_err(|e| io::Error::other(e.to_string()))?;
|
||||
let layer0_dir = layer0.dir();
|
||||
let dedup_path = layer0.dereplicated_superkmers_path();
|
||||
if !dedup_path.exists() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
if layer0_dir.join("mphf.bin").exists() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let filter_active = min_ab > 1 || max_ab.is_some();
|
||||
let need_counts = filter_active || with_counts;
|
||||
|
||||
let mphf1_opt: Option<Mphf> = if need_counts {
|
||||
let p = layer0_dir.join("mphf1.bin");
|
||||
p.exists().then(|| Mphf::load_full(&p).ok()).flatten()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let counts1_opt: Option<PersistentCompactIntVec> = if need_counts {
|
||||
let p = layer0_dir.join("counts1.bin");
|
||||
p.exists()
|
||||
.then(|| PersistentCompactIntVec::open(&p).ok())
|
||||
.flatten()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut g = GraphDeBruijn::new();
|
||||
let mut reader = SKFileReader::open(&dedup_path)?;
|
||||
for sk in reader.iter() {
|
||||
for kmer in sk.iter_canonical_kmers() {
|
||||
let accept = if filter_active {
|
||||
match (&mphf1_opt, &counts1_opt) {
|
||||
(Some(mphf), Some(counts)) => {
|
||||
let ab = counts.get(mphf.index(&kmer.raw()));
|
||||
ab >= min_ab && max_ab.map_or(true, |max| ab <= max)
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
} else {
|
||||
true
|
||||
};
|
||||
if accept {
|
||||
g.push(kmer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let n_kmers = if with_counts {
|
||||
let n = write_graph_as_unitigs(g, layer0_dir)
|
||||
.map_err(|e| io::Error::other(e.to_string()))?;
|
||||
TypedLayer::<PersistentCompactIntMatrix>::build(
|
||||
layer0_dir,
|
||||
block_bits,
|
||||
mode,
|
||||
|kmer| match (&mphf1_opt, &counts1_opt) {
|
||||
(Some(mphf), Some(counts)) => counts.get(mphf.index(&kmer.raw())),
|
||||
_ => 1,
|
||||
},
|
||||
)
|
||||
.map_err(|e| io::Error::other(e.to_string()))?;
|
||||
n
|
||||
} else {
|
||||
materialize_layer(g, layer0_dir, block_bits, mode)
|
||||
.map_err(|e| io::Error::other(e.to_string()))?
|
||||
};
|
||||
|
||||
Ok(n_kmers)
|
||||
}
|
||||
|
||||
fn remove_build_artifacts(&self, i: usize) {
|
||||
let layer0 = match self.layer0(i) {
|
||||
Ok(layer) => layer,
|
||||
Err(e) => {
|
||||
eprintln!("warning: could not locate layer 0 of partition {i}: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let layer0_dir = layer0.dir();
|
||||
let dedup = layer0.dereplicated_superkmers_path();
|
||||
remove_if_exists(&SKFileMeta::sidecar_path(&dedup));
|
||||
remove_if_exists(&dedup);
|
||||
remove_if_exists(&layer0_dir.join("mphf1.bin"));
|
||||
remove_if_exists(&layer0_dir.join("counts1.bin"));
|
||||
}
|
||||
}
|
||||
|
||||
// ── private helpers ──────────────────────────────────────────────────────
|
||||
|
||||
fn remove_if_exists(path: &Path) {
|
||||
if let Err(e) = fs::remove_file(path) {
|
||||
if e.kind() != io::ErrorKind::NotFound {
|
||||
eprintln!("warning: could not remove {}: {e}", path.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
pub use index_builder::IndexBuilder;
|
||||
pub(crate) use private_builder::PrivateBuilder;
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
//! `PrivateBuilder` — construction-only `KmerIndex` operations:
|
||||
//! sentinel marking, the per-partition layer-build primitive, and cleanup
|
||||
//! of build-time scratch files. Semantically meaningless once an index is
|
||||
//! built and being used for reading (query, dump, phylo, or as a
|
||||
//! `merge`/`select`/`rebuild` *source*).
|
||||
//!
|
||||
//! Scoped to the six methods exclusive to this crate's own pipeline
|
||||
//! (`partitionner`/`dereplicator`/`counter`/`layer_builder`) — no
|
||||
//! consumer outside those algorithms should ever need them, hence
|
||||
//! `pub(crate)` (see the module-level orphan-rule note in `extensions`'s
|
||||
//! `mod.rs`). See `DevDocMD/implementation/partition_layer_cache.md`, "(8)".
|
||||
//!
|
||||
//! `clear_output_for_create`/`create_skeleton`/`finalize_indexed`/`state`
|
||||
//! stay inherent on `KmerIndex`, deliberately not moved here: they're also
|
||||
//! called from `obikindex`'s own `merge`/`select`/`rebuild`/`reindex`
|
||||
//! modules, which — living inside `obikindex` itself — could never reach a
|
||||
//! trait defined in `obikindexer` (the dependency only runs
|
||||
//! `obikindexer → obikindex`, never the other way). Moving those four
|
||||
//! would require extracting `merge`/`select`/`rebuild`/`reindex` into
|
||||
//! algorithms first — separate, larger future work, not this one.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
|
||||
use cacheline_ef::{CachelineEf, CachelineEfVec};
|
||||
use epserde::prelude::*;
|
||||
use obicompactvec::{PersistentCompactIntMatrix, PersistentCompactIntVec};
|
||||
use obidebruinj::GraphDeBruijn;
|
||||
use obikindex::layer::IndexMode;
|
||||
use obikindex::layer::{KmerLayer, TypedLayer};
|
||||
use obikindex::{KmerIndex, OKIError, OKIResult};
|
||||
use crate::graph_pipeline::{materialize_layer, write_graph_as_unitigs};
|
||||
use obiskio::{SKError, SKFileMeta, SKFileReader};
|
||||
use ptr_hash::{PtrHash, bucket_fn::CubicEps, hash::Xx64};
|
||||
|
||||
type Mphf = PtrHash<u64, CubicEps, CachelineEfVec<Vec<CachelineEf>>, Xx64, Vec<u8>>;
|
||||
|
||||
pub(crate) trait PrivateBuilder {
|
||||
/// Mark scatter as complete (`IndexState::Scattered`).
|
||||
///
|
||||
/// If no genome label was set at creation time, one is derived from
|
||||
/// the index root directory name (stripped of all extensions). `&self`
|
||||
/// — not `&mut self` anymore: nothing is cached in memory to mutate,
|
||||
/// see `DevDocMD/implementation/partition_layer_cache.md`, "(11)".
|
||||
fn mark_scattered(&self) -> OKIResult<()>;
|
||||
|
||||
/// Mark dereplicate+count as complete (`IndexState::Counted`).
|
||||
fn mark_counted(&self) -> OKIResult<()>;
|
||||
|
||||
/// Partition `i`'s layer 0 — the one and only layer every pipeline
|
||||
/// algorithm in this crate (`partitionner`/`dereplicator`/`counter`/
|
||||
/// `layer_builder`) reads from or writes to; later layers only exist
|
||||
/// after a `merge`, which lives in `obikindex`, not here.
|
||||
fn layer0(&self, i: usize) -> OKIResult<KmerLayer>;
|
||||
|
||||
/// Mark layer construction as complete (`IndexState::Indexed`).
|
||||
fn mark_indexed(&self) -> OKIResult<()>;
|
||||
|
||||
/// Write `spectrums/{label}.json` from an already-computed kmer
|
||||
/// spectrum (`f0`/`f1`/abundance histogram). Takes plain values rather
|
||||
/// than `algorithms::counter::KmerSpectrum` — `KmerIndex` is the data
|
||||
/// model, `Counter` the algorithm that depends on it, not the other
|
||||
/// way around, and this is the only field of that type it actually
|
||||
/// uses.
|
||||
fn write_spectrum(&self, f0: u64, f1: u64, counts: &BTreeMap<u32, u64>) -> OKIResult<()>;
|
||||
|
||||
/// Build the layered MPHF index for partition `i`.
|
||||
///
|
||||
/// Returns the number of canonical k-mers indexed, or 0 if the
|
||||
/// partition has no data or its layer was already built
|
||||
/// (resume-safe).
|
||||
///
|
||||
/// Abundance filtering is applied when `min_ab > 1` or
|
||||
/// `max_ab.is_some()`, using `mphf1.bin` + `counts1.bin` if they
|
||||
/// exist. Count payload is stored iff `with_counts` is true.
|
||||
fn build_index_layer(
|
||||
&self,
|
||||
i: usize,
|
||||
min_ab: u32,
|
||||
max_ab: Option<u32>,
|
||||
with_counts: bool,
|
||||
mode: &IndexMode,
|
||||
block_bits: u8,
|
||||
) -> Result<usize, SKError>;
|
||||
|
||||
/// Remove intermediate build artifacts for partition `i`: dereplicated
|
||||
/// superkmers (+ sidecar), `mphf1.bin`, `counts1.bin`.
|
||||
fn remove_build_artifacts(&self, i: usize);
|
||||
}
|
||||
|
||||
impl PrivateBuilder for KmerIndex {
|
||||
fn mark_scattered(&self) -> OKIResult<()> {
|
||||
self.meta().mark_scattered().map_err(OKIError::Io)
|
||||
}
|
||||
|
||||
fn mark_counted(&self) -> OKIResult<()> {
|
||||
self.meta().mark_counted().map_err(OKIError::Io)
|
||||
}
|
||||
|
||||
fn layer0(&self, i: usize) -> OKIResult<KmerLayer> {
|
||||
self.partition(i)?.layer(0)
|
||||
}
|
||||
|
||||
fn mark_indexed(&self) -> OKIResult<()> {
|
||||
self.meta().mark_indexed().map_err(OKIError::Io)
|
||||
}
|
||||
|
||||
fn write_spectrum(&self, f0: u64, f1: u64, counts: &BTreeMap<u32, u64>) -> OKIResult<()> {
|
||||
let genomes = self.meta().genomes().map_err(OKIError::Io)?;
|
||||
let label = genomes
|
||||
.first()
|
||||
.map(|g| g.label.as_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_owned();
|
||||
let spectrums_dir = self.dir().join("spectrums");
|
||||
fs::create_dir_all(&spectrums_dir)?;
|
||||
let path = spectrums_dir.join(format!("{label}.json"));
|
||||
let spectrum_map: BTreeMap<String, u64> = counts
|
||||
.iter()
|
||||
.map(|(&c, &f)| (format!("{c:010}"), f))
|
||||
.collect();
|
||||
let f = fs::File::create(&path)?;
|
||||
serde_json::to_writer_pretty(
|
||||
f,
|
||||
&serde_json::json!({ "f0": f0, "f1": f1, "spectrum": spectrum_map }),
|
||||
)
|
||||
.map_err(OKIError::Json)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_index_layer(
|
||||
&self,
|
||||
i: usize,
|
||||
min_ab: u32,
|
||||
max_ab: Option<u32>,
|
||||
with_counts: bool,
|
||||
mode: &IndexMode,
|
||||
block_bits: u8,
|
||||
) -> Result<usize, SKError> {
|
||||
let layer0 = self.layer0(i).map_err(|e| io::Error::other(e.to_string()))?;
|
||||
let layer0_dir = layer0.dir();
|
||||
let dedup_path = layer0.dereplicated_superkmers_path();
|
||||
if !dedup_path.exists() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
if layer0_dir.join("mphf.bin").exists() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let filter_active = min_ab > 1 || max_ab.is_some();
|
||||
let need_counts = filter_active || with_counts;
|
||||
|
||||
let mphf1_opt: Option<Mphf> = if need_counts {
|
||||
let p = layer0_dir.join("mphf1.bin");
|
||||
p.exists().then(|| Mphf::load_full(&p).ok()).flatten()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let counts1_opt: Option<PersistentCompactIntVec> = if need_counts {
|
||||
let p = layer0_dir.join("counts1.bin");
|
||||
p.exists()
|
||||
.then(|| PersistentCompactIntVec::open(&p).ok())
|
||||
.flatten()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut g = GraphDeBruijn::new();
|
||||
let mut reader = SKFileReader::open(&dedup_path)?;
|
||||
for sk in reader.iter() {
|
||||
for kmer in sk.iter_canonical_kmers() {
|
||||
let accept = if filter_active {
|
||||
match (&mphf1_opt, &counts1_opt) {
|
||||
(Some(mphf), Some(counts)) => {
|
||||
let ab = counts.get(mphf.index(&kmer.raw()));
|
||||
ab >= min_ab && max_ab.map_or(true, |max| ab <= max)
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
} else {
|
||||
true
|
||||
};
|
||||
if accept {
|
||||
g.push(kmer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let n_kmers = if with_counts {
|
||||
let n = write_graph_as_unitigs(g, layer0_dir)
|
||||
.map_err(|e| io::Error::other(e.to_string()))?;
|
||||
TypedLayer::<PersistentCompactIntMatrix>::build(
|
||||
layer0_dir,
|
||||
block_bits,
|
||||
mode,
|
||||
|kmer| match (&mphf1_opt, &counts1_opt) {
|
||||
(Some(mphf), Some(counts)) => counts.get(mphf.index(&kmer.raw())),
|
||||
_ => 1,
|
||||
},
|
||||
)
|
||||
.map_err(|e| io::Error::other(e.to_string()))?;
|
||||
n
|
||||
} else {
|
||||
materialize_layer(g, layer0_dir, block_bits, mode)
|
||||
.map_err(|e| io::Error::other(e.to_string()))?
|
||||
};
|
||||
|
||||
Ok(n_kmers)
|
||||
}
|
||||
|
||||
fn remove_build_artifacts(&self, i: usize) {
|
||||
let layer0 = match self.layer0(i) {
|
||||
Ok(layer) => layer,
|
||||
Err(e) => {
|
||||
eprintln!("warning: could not locate layer 0 of partition {i}: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let layer0_dir = layer0.dir();
|
||||
let dedup = layer0.dereplicated_superkmers_path();
|
||||
remove_if_exists(&SKFileMeta::sidecar_path(&dedup));
|
||||
remove_if_exists(&dedup);
|
||||
remove_if_exists(&layer0_dir.join("mphf1.bin"));
|
||||
remove_if_exists(&layer0_dir.join("counts1.bin"));
|
||||
}
|
||||
}
|
||||
|
||||
// ── private helpers ──────────────────────────────────────────────────────
|
||||
|
||||
fn remove_if_exists(path: &Path) {
|
||||
if let Err(e) = fs::remove_file(path) {
|
||||
if e.kind() != io::ErrorKind::NotFound {
|
||||
eprintln!("warning: could not remove {}: {e}", path.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,11 +9,14 @@
|
||||
//! (routing raw super-kmers into partitions), `dereplicator` (deduplicating
|
||||
//! a partition's raw super-kmers), `counter` (counting unique canonical
|
||||
//! kmers), `layer_builder` (turning dereplicated super-kmers + counts into
|
||||
//! the real layer 0). [`extensions`] (private — see its own module docs)
|
||||
//! holds construction-only `KmerIndex` operations these algorithms share.
|
||||
//! the real layer 0). `extensions` (module itself private — see its own
|
||||
//! docs) holds `KmerIndex`/`KmerLayer` extension traits: some exclusive to
|
||||
//! this crate's own pipeline, one ([`IndexBuilder`]) public and re-exported
|
||||
//! here for external callers.
|
||||
|
||||
pub mod algorithms;
|
||||
pub(crate) mod extensions;
|
||||
mod extensions;
|
||||
pub mod graph_pipeline;
|
||||
|
||||
pub use extensions::IndexBuilder;
|
||||
pub use graph_pipeline::{build_graph, build_layer_from_kmers, materialize_layer, write_graph_as_unitigs};
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::Args;
|
||||
use obikindex::KmerIndex;
|
||||
use obikindex::layer::IndexMode;
|
||||
use obikrebuild::IndexReindex;
|
||||
use obisys::Reporter;
|
||||
use tracing::info;
|
||||
|
||||
use crate::cli::block_size_to_bits;
|
||||
use super::index::resolve_approx_params;
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct ConvertArgs {
|
||||
/// Index directory to convert (modified in-place)
|
||||
pub index: PathBuf,
|
||||
|
||||
#[command(flatten)]
|
||||
mode: ModeArgs,
|
||||
|
||||
/// Fingerprint bits per slot (b) — required with --hybrid-evidence
|
||||
/// when the source index is currently exact (there is no existing
|
||||
/// fingerprint to inherit `b` from); rejected otherwise (the source
|
||||
/// already fixes `b`, either from its own --approx-evidence or from
|
||||
/// an already-approximate/hybrid index being converted to hybrid).
|
||||
#[arg(long, value_name = "BITS")]
|
||||
pub evidence_bits: Option<u8>,
|
||||
|
||||
/// Findere z parameter: number of consecutive *stored* k-mers (length
|
||||
/// = the index's own k-mer size, fixed forever at `index` build time)
|
||||
/// that must all match to confirm a hit.
|
||||
///
|
||||
/// WARNING: this does NOT shorten the indexed k-mer length the way
|
||||
/// `-z` does at `index` build time — that length can never change
|
||||
/// after the fact. Instead it *extends* the effective match window:
|
||||
/// requiring z consecutive stored k-mers of length s to all match is
|
||||
/// equivalent to requiring (s + z − 1) consecutive identical bases in
|
||||
/// the query. E.g. on a k=31 index, z=2 requires 32 consecutive
|
||||
/// matching bases, not 30 — going from z=1 to z=2 makes matching
|
||||
/// *stricter*, it does not "index shorter k-mers".
|
||||
#[arg(short = 'z', long)]
|
||||
pub findere_z: Option<u8>,
|
||||
|
||||
/// Target false-positive rate per z-window (e.g. 0.01) — derives `b`
|
||||
/// or `z` when one of them isn't given directly (see `index --fp` for
|
||||
/// the same resolution rules).
|
||||
#[arg(long)]
|
||||
pub fp: Option<f64>,
|
||||
|
||||
/// Block size for exact evidence `.idx` (number of unitigs per block).
|
||||
/// Ignored when converting to pure approximate evidence.
|
||||
#[arg(long, default_value_t = 1)]
|
||||
pub block_size: usize,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
#[group(required = true, multiple = false)]
|
||||
struct ModeArgs {
|
||||
/// Convert to exact evidence (zero false positives).
|
||||
#[arg(long)]
|
||||
exact_evidence: bool,
|
||||
|
||||
/// Convert to approximate (fingerprint-only) evidence. Value = fingerprint bits per slot (b).
|
||||
#[arg(long, value_name = "BITS")]
|
||||
approx_evidence: Option<u8>,
|
||||
|
||||
/// Convert to hybrid evidence (both exact and approximate bundles kept).
|
||||
#[arg(long)]
|
||||
hybrid_evidence: bool,
|
||||
}
|
||||
|
||||
pub fn run(args: ConvertArgs) {
|
||||
// Modifies the index in place; acquired before opening so a concurrent
|
||||
// writer can't slip in between the open and the convert below.
|
||||
let _lock = obisys::DirLock::acquire(&args.index).unwrap_or_else(|e| {
|
||||
eprintln!("error locking index directory {}: {e}", args.index.display());
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
let idx = KmerIndex::open(&args.index).unwrap_or_else(|e| {
|
||||
eprintln!("error opening index: {e}");
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
let current = idx.meta().config().evidence.clone();
|
||||
|
||||
let target = if args.mode.exact_evidence {
|
||||
warn_if_evidence_params_given(&args, "--exact-evidence");
|
||||
info!("target: exact evidence");
|
||||
IndexMode::Exact
|
||||
} else if let Some(bits) = args.mode.approx_evidence {
|
||||
let (z, b, fp) = resolve_approx_params(args.findere_z, Some(bits), args.fp);
|
||||
info!("target: approximate evidence — b={b}, z={z}, fp={fp:.2e}");
|
||||
IndexMode::Approx { b, z }
|
||||
} else {
|
||||
debug_assert!(args.mode.hybrid_evidence);
|
||||
match current {
|
||||
IndexMode::Exact => {
|
||||
let bits = args.evidence_bits.unwrap_or_else(|| {
|
||||
eprintln!(
|
||||
"error: --hybrid-evidence requires --evidence-bits when starting from an exact index"
|
||||
);
|
||||
std::process::exit(1);
|
||||
});
|
||||
let (z, b, fp) = resolve_approx_params(args.findere_z, Some(bits), args.fp);
|
||||
info!("target: hybrid evidence — b={b}, z={z}, fp={fp:.2e}");
|
||||
IndexMode::Hybrid { b, z }
|
||||
}
|
||||
IndexMode::Approx { b, z } | IndexMode::Hybrid { b, z } => {
|
||||
if args.evidence_bits.is_some() || args.findere_z.is_some() || args.fp.is_some() {
|
||||
eprintln!(
|
||||
"error: --evidence-bits/-z/--fp are not allowed with --hybrid-evidence \
|
||||
when the source index is already approximate/hybrid — its existing \
|
||||
b={b}, z={z} are reused as-is"
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
info!("target: hybrid evidence — reusing existing b={b}, z={z}");
|
||||
IndexMode::Hybrid { b, z }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let block_bits = block_size_to_bits(args.block_size);
|
||||
let mut rep = Reporter::new();
|
||||
idx.reindex(target, block_bits, &mut rep).unwrap_or_else(|e| {
|
||||
eprintln!("convert error: {e}");
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
rep.print();
|
||||
}
|
||||
|
||||
fn warn_if_evidence_params_given(args: &ConvertArgs, mode: &str) {
|
||||
if args.evidence_bits.is_some() || args.findere_z.is_some() || args.fp.is_some() {
|
||||
eprintln!("warning: --evidence-bits/-z/--fp ignored with {mode}");
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod annotate;
|
||||
pub mod convert;
|
||||
pub mod dump;
|
||||
pub mod estimate;
|
||||
pub mod filter;
|
||||
|
||||
@@ -37,6 +37,8 @@ enum Commands {
|
||||
Annotate(cmd::annotate::AnnotateArgs),
|
||||
/// Maintenance/inspection operations on already-built indexes
|
||||
Utils(cmd::utils::UtilsArgs),
|
||||
/// Convert an index's evidence representation (exact/approximate/hybrid), in place
|
||||
Convert(cmd::convert::ConvertArgs),
|
||||
}
|
||||
|
||||
fn main() {
|
||||
@@ -61,5 +63,6 @@ fn main() {
|
||||
Commands::Estimate(args) => cmd::estimate::run(args),
|
||||
Commands::Annotate(args) => cmd::annotate::run(args),
|
||||
Commands::Utils(args) => cmd::utils::run(args),
|
||||
Commands::Convert(args) => cmd::convert::run(args),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,9 @@
|
||||
//!
|
||||
//! [`compact_layer`] collapses a partition's accumulated layers (the
|
||||
//! stopgap `obikmerge` uses to add genomes without a full rebuild) back
|
||||
//! into one, in place. `reindex` (reindexing an existing index's evidence
|
||||
//! representation in place — `Exact`/`Approx`/`Hybrid`) is temporarily
|
||||
//! disabled: it predates this session's refactor and still reaches
|
||||
//! `obikindex` internals (`root_path`/`meta` fields, `index_dir`/
|
||||
//! `layer_dir` methods) that are no longer accessible from outside that
|
||||
//! crate. Fixing it is separate, future work.
|
||||
//! into one, in place. [`reindex`] converts an existing index's evidence
|
||||
//! representation in place (`Exact`/`Approx`/`Hybrid`), delegating the
|
||||
//! actual per-layer construction to `obikindexer::IndexBuilder`.
|
||||
//!
|
||||
//! Removing k-mers (filtering) and repacking matrix files are *not* here:
|
||||
//! the former lives in `obikfilter::Filter` (a fresh index at a new
|
||||
@@ -21,6 +18,7 @@
|
||||
//! cannot depend back on this crate.
|
||||
|
||||
mod compact_layer;
|
||||
// mod reindex; // disabled — see module doc above.
|
||||
mod reindex;
|
||||
|
||||
pub use compact_layer::IndexCompact;
|
||||
pub use reindex::IndexReindex;
|
||||
|
||||
@@ -1,43 +1,52 @@
|
||||
use obikindex::IndexBuilder;
|
||||
use obikindex::IndexMeta;
|
||||
use obikindex::layer::{IndexMode, TypedLayer};
|
||||
use obisys::{Reporter, Stage, progress_bar};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
//! Convert an existing index's evidence representation in place —
|
||||
//! `Exact`/`Approx`/`Hybrid`. `unitigs.bin`/`mphf.bin` are never touched;
|
||||
//! only the evidence bundle (`evidence.bin`/`unitigs.bin.idx` and/or
|
||||
//! `fingerprint.bin`) is — the actual per-layer construction is
|
||||
//! `obikindexer::IndexBuilder`'s job, this module only orchestrates it
|
||||
//! across every partition/layer and updates `index.meta` on success.
|
||||
//!
|
||||
//! `KmerIndex` is foreign to this crate, so this is an extension trait,
|
||||
//! same reasoning as `IndexCompact`/`IndexDump`/`IndexUnitigs`.
|
||||
|
||||
use obikindex::layer::{IndexMode, KmerLayer};
|
||||
use obikindex::{IndexState, KmerIndex, OKIError, OKIResult};
|
||||
use obikindexer::IndexBuilder;
|
||||
use obisys::{PartitionRunner, Reporter, Stage, progress_bar};
|
||||
use tracing::info;
|
||||
|
||||
use obikindex::{OKIError, OKIResult};
|
||||
use obikindex::KmerIndex;
|
||||
use obikindex::IndexState;
|
||||
use obisys::PartitionRunner;
|
||||
|
||||
const EVIDENCE_FILE: &str = "evidence.bin";
|
||||
const FINGERPRINT_FILE: &str = "fingerprint.bin";
|
||||
/// `unitigs.bin.idx`'s filename isn't exposed by `obikindex`/`obiskio` as a
|
||||
/// path accessor (it's a private implementation detail of exact-evidence
|
||||
/// construction) — mirrored here as a literal, same as before this module
|
||||
/// was disabled.
|
||||
const UNITIG_IDX_FILE: &str = "unitigs.bin.idx";
|
||||
|
||||
impl KmerIndex {
|
||||
/// Convert every layer's evidence bundle to `target` in-place.
|
||||
pub trait IndexReindex {
|
||||
/// Convert every layer's evidence bundle to `target`, in place.
|
||||
///
|
||||
/// - `Exact` → builds `evidence.bin` + `unitigs.bin.idx`, removes `fingerprint.bin`
|
||||
/// - `Approx` → builds `fingerprint.bin`, removes `evidence.bin` + `unitigs.bin.idx`
|
||||
/// - `Exact` → builds `evidence.bin` + `unitigs.bin.idx` where missing, removes `fingerprint.bin`.
|
||||
/// - `Approx` → builds `fingerprint.bin` where missing or built with a different `b`, removes `evidence.bin` + `unitigs.bin.idx`.
|
||||
/// - `Hybrid` → builds whichever bundle a layer is still missing, keeps both.
|
||||
///
|
||||
/// The MPHF (`mphf.bin`) and unitigs (`unitigs.bin`) are never touched.
|
||||
/// `index.meta` is updated with the new evidence kind on success.
|
||||
pub fn reindex(
|
||||
&mut self,
|
||||
target: IndexMode,
|
||||
block_bits: u8,
|
||||
rep: &mut Reporter,
|
||||
) -> OKIResult<()> {
|
||||
if self.state()? != IndexState::Indexed {
|
||||
return Err(OKIError::NotIndexed(self.root_path.clone()));
|
||||
/// Each layer's own build step is idempotent (see `IndexBuilder`), so
|
||||
/// re-running this with the same `target` on an already-converted
|
||||
/// index is a cheap no-op per layer, not a full rebuild.
|
||||
fn reindex(&self, target: IndexMode, block_bits: u8, rep: &mut Reporter) -> OKIResult<()>;
|
||||
}
|
||||
|
||||
impl IndexReindex for KmerIndex {
|
||||
fn reindex(&self, target: IndexMode, block_bits: u8, rep: &mut Reporter) -> OKIResult<()> {
|
||||
if self.meta().state().map_err(OKIError::Io)? != IndexState::Indexed {
|
||||
return Err(OKIError::InvalidInput(format!(
|
||||
"{}: index is not fully built",
|
||||
self.dir().display()
|
||||
)));
|
||||
}
|
||||
|
||||
let n = self.n_partitions();
|
||||
info!(
|
||||
"reindex {} partition(s): {:?} → {:?}",
|
||||
n, self.meta.config.evidence, target,
|
||||
"reindex {} partition(s): {:?} → {target:?}",
|
||||
n,
|
||||
self.meta().config().evidence,
|
||||
);
|
||||
|
||||
let t = Stage::start("reindex");
|
||||
@@ -58,68 +67,56 @@ impl KmerIndex {
|
||||
|
||||
pb.finish_and_clear();
|
||||
|
||||
let mut config = self.meta.config.clone();
|
||||
config.evidence = target;
|
||||
if matches!(config.evidence, IndexMode::Exact) {
|
||||
config.block_bits = block_bits;
|
||||
}
|
||||
let genomes = self.meta.genomes().map_err(OKIError::Io)?;
|
||||
self.meta.rewrite_config(config, genomes).map_err(OKIError::Io)?;
|
||||
self.meta = Arc::new(IndexMeta::open(self).map_err(OKIError::Io)?);
|
||||
self.meta().set_evidence(target, block_bits).map_err(OKIError::Io)?;
|
||||
rep.push(t.stop());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Process all layers of one partition's index directory.
|
||||
fn reindex_partition(
|
||||
index: &KmerIndex,
|
||||
i: usize,
|
||||
target: &IndexMode,
|
||||
block_bits: u8,
|
||||
) -> OKIResult<()> {
|
||||
if !index.index_dir(i).exists() {
|
||||
return Ok(());
|
||||
}
|
||||
let n_layers = index
|
||||
.n_layers(i)
|
||||
.map_err(|e| OKIError::InvalidInput(e.to_string()))?;
|
||||
for layer_idx in 0..n_layers {
|
||||
reindex_layer(&index.layer_dir(i, layer_idx), target, block_bits)?;
|
||||
/// Process every layer of partition `i`.
|
||||
fn reindex_partition(index: &KmerIndex, i: usize, target: &IndexMode, block_bits: u8) -> OKIResult<()> {
|
||||
let partition = index.partition(i)?;
|
||||
let n_layers = partition.n_layers();
|
||||
for l in 0..n_layers {
|
||||
let layer = partition.layer(l)?.open()?;
|
||||
reindex_layer(&layer, target, block_bits)?;
|
||||
remove_stale_evidence(&layer, target);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reindex_layer(layer_dir: &Path, target: &IndexMode, block_bits: u8) -> OKIResult<()> {
|
||||
fn reindex_layer(layer: &KmerLayer, target: &IndexMode, block_bits: u8) -> OKIResult<()> {
|
||||
match target {
|
||||
IndexMode::Exact => {
|
||||
TypedLayer::<()>::build_exact_evidence(layer_dir, block_bits)?;
|
||||
layer.build_exact_evidence(block_bits)?;
|
||||
}
|
||||
IndexMode::Approx { b, z } | IndexMode::Hybrid { b, z } => {
|
||||
TypedLayer::<()>::build_approx_evidence(layer_dir, *b, *z)?;
|
||||
IndexMode::Approx { b, z } => {
|
||||
layer.build_approx_evidence(*b, *z)?;
|
||||
}
|
||||
IndexMode::Hybrid { b, z } => {
|
||||
layer.build_hybrid_evidence(*b, *z, block_bits)?;
|
||||
}
|
||||
}
|
||||
remove_stale_evidence(layer_dir, target)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_stale_evidence(layer_dir: &Path, target: &IndexMode) -> OKIResult<()> {
|
||||
fn remove_stale_evidence(layer: &KmerLayer, target: &IndexMode) {
|
||||
match target {
|
||||
IndexMode::Exact => {
|
||||
remove_if_exists(&layer_dir.join(FINGERPRINT_FILE));
|
||||
remove_if_exists(&layer.fingerprint_path());
|
||||
}
|
||||
IndexMode::Approx { .. } => {
|
||||
remove_if_exists(&layer_dir.join(EVIDENCE_FILE));
|
||||
remove_if_exists(&layer_dir.join(UNITIG_IDX_FILE));
|
||||
remove_if_exists(&layer.evidence_path());
|
||||
remove_if_exists(&layer.dir().join(UNITIG_IDX_FILE));
|
||||
}
|
||||
IndexMode::Hybrid { .. } => {
|
||||
// both bundles kept — nothing to remove
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_if_exists(path: &Path) {
|
||||
if let Err(e) = fs::remove_file(path) {
|
||||
fn remove_if_exists(path: &std::path::Path) {
|
||||
if let Err(e) = std::fs::remove_file(path) {
|
||||
if e.kind() != std::io::ErrorKind::NotFound {
|
||||
eprintln!("warning: could not remove {}: {e}", path.display());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user