feat: add annotate CLI command for applying genome metadata via CSV

Introduces the `annotate` subcommand to apply genome metadata from an external CSV file to a pre-built k-mer index. The command supports configurable field separators, ID columns, and null markers, while providing a `--dump` option to export current index metadata as sorted CSV. Supporting changes include minor internal refactoring in `obikindex` to use object-level directory accessors and updates to the `csv` dependency.
This commit is contained in:
Eric Coissac
2026-08-22 17:23:39 +02:00
parent fb31a35c76
commit fba9c65b1a
7 changed files with 219 additions and 26 deletions
+13 -13
View File
@@ -25,25 +25,25 @@ fn main() -> anyhow::Result<()> {
let mut first_mismatch = None;
for part in 0..n_parts {
let index_dir_sparse = sparse.index_dir(part);
let index_dir_dense = dense.index_dir(part);
if !index_dir_sparse.exists() || !index_dir_dense.exists() {
let Ok(part_sparse) = sparse.partition(part) else {
continue;
}
};
let Ok(part_dense) = dense.partition(part) else {
continue;
};
for layer in 0..n_layers {
let layer_dir_sparse = sparse.layer_dir(part, layer);
let layer_dir_dense = dense.layer_dir(part, layer);
if !layer_dir_sparse.exists() || !layer_dir_dense.exists() {
let Ok(layer_sparse) = part_sparse.layer(layer) else {
continue;
}
};
let Ok(layer_dense) = part_dense.layer(layer) else {
continue;
};
let presence_sparse = layer_dir_sparse.join("presence");
let presence_dense = layer_dir_dense.join("presence");
let presence_sparse = layer_sparse.presence_dir();
let layer_dir_dense = layer_dense.dir().to_path_buf();
if !presence_sparse.exists() || !presence_dense.exists() {
if !presence_sparse.exists() || !layer_dense.presence_dir().exists() {
continue;
}
+16 -13
View File
@@ -150,7 +150,7 @@ impl HasStorageKind for PersistentBitMatrix {
// ── Structures ────────────────────────────────────────────────────────────────
pub struct TypedLayer<D: LayerData = ()> {
mphf: MphfLayer,
kmerstore: MphfLayer,
data: D,
}
@@ -165,11 +165,14 @@ impl<D: LayerData> TypedLayer<D> {
pub fn open(path: &Path) -> OKIResult<Self> {
let mphf = MphfLayer::open(path)?;
let data = D::open(path)?;
Ok(Self { mphf, data })
Ok(Self {
kmerstore: mphf,
data,
})
}
pub fn query(&self, kmer: CanonicalKmer) -> Option<Hit<D::Item>> {
self.mphf.find(kmer).map(|slot| Hit {
self.kmerstore.find(kmer).map(|slot| Hit {
slot,
data: self.data.read(slot),
})
@@ -181,37 +184,37 @@ impl<D: LayerData> TypedLayer<D> {
/// sweep), so a plain `query` reading — and discarding — a full row
/// per lookup would be wasted work.
pub fn find_slot(&self, kmer: CanonicalKmer) -> Option<usize> {
self.mphf.find(kmer)
self.kmerstore.find(kmer)
}
pub fn n(&self) -> usize {
self.mphf.n()
self.kmerstore.n()
}
/// Raw MPHF lookup: kmer → slot, no membership check.
pub fn hash(&self, kmer: CanonicalKmer) -> usize {
self.mphf.hash(kmer)
self.kmerstore.hash(kmer)
}
/// Batch raw MPHF lookup: kmers → slots, no membership check.
pub fn hash_batch(&self, kmers: &[CanonicalKmer]) -> Vec<usize> {
self.mphf.hash_batch(kmers)
self.kmerstore.hash_batch(kmers)
}
/// Iterate over all canonical kmers in the layer, in deterministic order.
pub fn iter_kmers(&self) -> crate::layer::mphf_layer::KmerIter {
self.mphf.iter_kmers()
self.kmerstore.iter_kmers()
}
/// Iterate over all canonical kmers, each paired with its zero-based
/// sequence index in `unitigs.bin`.
pub fn enumerate_kmers(&self) -> std::iter::Enumerate<crate::layer::mphf_layer::KmerIter> {
self.mphf.enumerate_kmers()
self.kmerstore.enumerate_kmers()
}
/// Iterate over the layer's canonical kmers in batches of `n`.
pub fn iter_kmers_batch(&self, n: usize) -> crate::layer::mphf_layer::KmerBatchIter {
self.mphf.iter_kmers_batch(n)
self.kmerstore.iter_kmers_batch(n)
}
/// Iterate over batches, each paired with the zero-based index of the
@@ -220,13 +223,13 @@ impl<D: LayerData> TypedLayer<D> {
&self,
n: usize,
) -> impl Iterator<Item = (usize, Vec<CanonicalKmer>)> + Send + 'static {
self.mphf.enumerate_kmers_batch(n)
self.kmerstore.enumerate_kmers_batch(n)
}
/// Iterate over the layer's unitigs (whole sequences, not decomposed
/// into k-mers).
pub fn iter_unitigs(&self) -> crate::layer::mphf_layer::UnitigIter {
self.mphf.iter_unitigs()
self.kmerstore.iter_unitigs()
}
pub fn unitig_writer(out_dir: &Path) -> OKIResult<UnitigFileWriter> {
@@ -251,7 +254,7 @@ impl<D: LayerData> TypedLayer<D> {
/// already in memory (`MphfLayer`'s own `LayerEvidence`), no disk
/// access. Available regardless of `D`, unlike `content`/`storage_kind`.
pub fn evidence_kind(&self) -> crate::layer::mphf_layer::EvidenceKind {
self.mphf.evidence_kind()
self.kmerstore.evidence_kind()
}
}