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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user