|
|
@@ -1,7 +1,75 @@
|
|
|
-use log::{info, warn};
|
|
|
-use std::fs;
|
|
|
-use std::io::{self, BufReader, BufWriter, Read, Write};
|
|
|
-use std::path::{Path, PathBuf};
|
|
|
+//! Genome uniqueness index based on suffix arrays.
|
|
|
+//!
|
|
|
+//! This module computes, stores, and queries minimal unique sequence lengths
|
|
|
+//! across a genome.
|
|
|
+//!
|
|
|
+//! For each genomic position `pos`, the index stores the shortest sequence
|
|
|
+//! starting at `pos` that occurs exactly once in the genome:
|
|
|
+//!
|
|
|
+//! ```text
|
|
|
+//! genome[pos .. pos + k)
|
|
|
+//! ```
|
|
|
+//!
|
|
|
+//! where `k` is the minimal unique length at `pos`.
|
|
|
+//!
|
|
|
+//! Internally, uniqueness is computed from a suffix array and LCP
|
|
|
+//! (Longest Common Prefix) array, allowing construction in `O(n log n)`
|
|
|
+//! time and constant-time uniqueness queries.
|
|
|
+//!
|
|
|
+//! # Query types
|
|
|
+//!
|
|
|
+//! The index supports two complementary views of uniqueness:
|
|
|
+//!
|
|
|
+//! ## Unique intervals containing a position
|
|
|
+//!
|
|
|
+//! Given a genomic position, the index can return a unique interval that
|
|
|
+//! contains that position.
|
|
|
+//!
|
|
|
+//! ```text
|
|
|
+//! start <= pos < end
|
|
|
+//! ```
|
|
|
+//!
|
|
|
+//! Several intervals may satisfy this condition; [`AnchorBias`] controls
|
|
|
+//! how a candidate interval is selected.
|
|
|
+//!
|
|
|
+//! ## Independent left/right uniqueness
|
|
|
+//!
|
|
|
+//! The index can also report how much sequence is required on either side
|
|
|
+//! of a position to obtain uniqueness independently.
|
|
|
+//!
|
|
|
+//! For a position `pos`:
|
|
|
+//!
|
|
|
+//! ```text
|
|
|
+//! before: genome[pos - k .. pos)
|
|
|
+//! after : genome[pos .. pos + k)
|
|
|
+//! ```
|
|
|
+//!
|
|
|
+//! [`UniqueContext`] returns the smallest unique sequence ending at `pos`
|
|
|
+//! (`before`) and the smallest unique sequence starting at `pos` (`after`).
|
|
|
+//!
|
|
|
+//! To support efficient left-side queries, the index stores uniqueness
|
|
|
+//! information for both the forward genome and its reverse complement-free
|
|
|
+//! reversal.
|
|
|
+//!
|
|
|
+//! # Caching
|
|
|
+//!
|
|
|
+//! Construction can be expensive for large genomes. The module therefore
|
|
|
+//! supports disk-backed caching of precomputed uniqueness vectors.
|
|
|
+//!
|
|
|
+//! Cached data are keyed by genome metadata and loaded transparently when
|
|
|
+//! available.
|
|
|
+//!
|
|
|
+//! # Main types
|
|
|
+//!
|
|
|
+//! - [`UniquenessIndex`] — uniqueness index for a single sequence.
|
|
|
+//! - [`GenomeIndex`] — multi-contig genome-wide index.
|
|
|
+//! - [`UniqueInterval`] — unique interval containing a queried position.
|
|
|
+//! - [`UniqueContext`] — independent left/right uniqueness lengths.
|
|
|
+//! - [`GenomicPos`] — genomic coordinate used by [`GenomeIndex`].
|
|
|
+
|
|
|
+use log::warn;
|
|
|
+use std::io::{self};
|
|
|
+use std::path::Path;
|
|
|
|
|
|
mod cache;
|
|
|
mod genome_index;
|
|
|
@@ -35,6 +103,14 @@ fn build_min_unique_len(genome: &[u8]) -> Vec<usize> {
|
|
|
mul
|
|
|
}
|
|
|
|
|
|
+pub struct UniqueContext {
|
|
|
+ /// Bases required before `pos`.
|
|
|
+ pub before: Option<usize>,
|
|
|
+
|
|
|
+ /// Bases required from `pos` onward.
|
|
|
+ pub after: Option<usize>,
|
|
|
+}
|
|
|
+
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
pub struct UniqueInterval {
|
|
|
pub start: usize,
|
|
|
@@ -56,6 +132,7 @@ pub enum AnchorBias {
|
|
|
|
|
|
pub struct UniquenessIndex {
|
|
|
min_unique_len: Vec<usize>,
|
|
|
+ rev_min_unique_len: Vec<usize>,
|
|
|
genome_len: usize,
|
|
|
}
|
|
|
|
|
|
@@ -64,33 +141,83 @@ impl UniquenessIndex {
|
|
|
/// Cache lives in `cache_dir` (created if absent).
|
|
|
/// If the cache file exists and is valid, loading is O(n) read — no recomputation.
|
|
|
pub fn build_with_cache(genome: &[u8], key: &CacheKey, cache_dir: &Path) -> io::Result<Self> {
|
|
|
- let cache_path = cache_dir.join(key.file_name());
|
|
|
- if cache_path.exists() {
|
|
|
- match load_from_cache(&cache_path, genome.len()) {
|
|
|
- Ok(mul) => return Ok(Self::from_raw(mul)),
|
|
|
- Err(e) => warn!("[cache] invalid ({e}), rebuilding..."),
|
|
|
+ std::fs::create_dir_all(cache_dir)?;
|
|
|
+
|
|
|
+ let fwd_path = cache_dir.join(key.file_name());
|
|
|
+ let rev_path = cache_dir.join(format!("rev.{}", key.file_name()));
|
|
|
+
|
|
|
+ let min_unique_len = match load_from_cache(&fwd_path, genome.len()) {
|
|
|
+ Ok(mul) => mul,
|
|
|
+ Err(e) => {
|
|
|
+ if fwd_path.exists() {
|
|
|
+ warn!("[cache] invalid forward ({e}), rebuilding...");
|
|
|
+ }
|
|
|
+
|
|
|
+ let mul = build_min_unique_len(genome);
|
|
|
+ save_to_cache(&fwd_path, &mul)?;
|
|
|
+ mul
|
|
|
}
|
|
|
- }
|
|
|
- let mul = build_min_unique_len(genome);
|
|
|
- save_to_cache(&cache_path, &mul)?;
|
|
|
- Ok(Self::from_raw(mul))
|
|
|
+ };
|
|
|
+
|
|
|
+ let rev_min_unique_len = match load_from_cache(&rev_path, genome.len()) {
|
|
|
+ Ok(mul) => mul,
|
|
|
+ Err(e) => {
|
|
|
+ if rev_path.exists() {
|
|
|
+ warn!("[cache] invalid reverse ({e}), rebuilding...");
|
|
|
+ }
|
|
|
+
|
|
|
+ let mut rev = genome.to_vec();
|
|
|
+ rev.reverse();
|
|
|
+
|
|
|
+ let mul = build_min_unique_len(&rev);
|
|
|
+ save_to_cache(&rev_path, &mul)?;
|
|
|
+ mul
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ Ok(Self::from_raw(min_unique_len, rev_min_unique_len))
|
|
|
}
|
|
|
|
|
|
/// Construct directly from a precomputed `min_unique_len` vector.
|
|
|
/// Used when loading from disk cache — skips SA/LCP computation entirely.
|
|
|
- pub fn from_raw(min_unique_len: Vec<usize>) -> Self {
|
|
|
+ pub fn from_raw(min_unique_len: Vec<usize>, rev_min_unique_len: Vec<usize>) -> Self {
|
|
|
+ assert_eq!(min_unique_len.len(), rev_min_unique_len.len());
|
|
|
+
|
|
|
let genome_len = min_unique_len.len();
|
|
|
+
|
|
|
Self {
|
|
|
min_unique_len,
|
|
|
+ rev_min_unique_len,
|
|
|
genome_len,
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/// Build without cache (e.g. for tests or one-shot use).
|
|
|
pub fn build(genome: &[u8]) -> Self {
|
|
|
- Self::from_raw(build_min_unique_len(genome))
|
|
|
+ let min_unique_len = build_min_unique_len(genome);
|
|
|
+
|
|
|
+ let mut rev = genome.to_vec();
|
|
|
+ rev.reverse();
|
|
|
+ let rev_min_unique_len = build_min_unique_len(&rev);
|
|
|
+
|
|
|
+ Self::from_raw(min_unique_len, rev_min_unique_len)
|
|
|
}
|
|
|
|
|
|
+ /// Returns the number of nucleotides before and after `pos` needed to form
|
|
|
+ /// a unique sequence containing `pos`.
|
|
|
+ ///
|
|
|
+ /// The returned flank defines the half-open interval:
|
|
|
+ ///
|
|
|
+ /// `[pos - front, pos + back)`
|
|
|
+ ///
|
|
|
+ /// where `front` is the number of nucleotides before `pos`, and `back` is the
|
|
|
+ /// number of nucleotides from `pos` onward, including the nucleotide at `pos`.
|
|
|
+ ///
|
|
|
+ /// Returns `None` if no unique sequence containing `pos` can be found.
|
|
|
+ ///
|
|
|
+ /// # Panics
|
|
|
+ ///
|
|
|
+ /// Panics if `pos >= self.genome_len()`.
|
|
|
pub fn minimal_unique_interval_containing(
|
|
|
&self,
|
|
|
pos: usize,
|
|
|
@@ -161,6 +288,44 @@ impl UniquenessIndex {
|
|
|
pub fn genome_len(&self) -> usize {
|
|
|
self.genome_len
|
|
|
}
|
|
|
+
|
|
|
+ /// Returns how much sequence is needed before and after `pos`
|
|
|
+ /// to obtain uniqueness independently on each side.
|
|
|
+ ///
|
|
|
+ /// `before = Some(k)` means:
|
|
|
+ ///
|
|
|
+ /// `genome[pos - k .. pos)` is the shortest unique sequence ending at `pos`.
|
|
|
+ ///
|
|
|
+ /// `after = Some(k)` means:
|
|
|
+ ///
|
|
|
+ /// `genome[pos .. pos + k)` is the shortest unique sequence starting at `pos`.
|
|
|
+ ///
|
|
|
+ /// Returns `None` for a side if no unique sequence exists on that side.
|
|
|
+ ///
|
|
|
+ /// # Panics
|
|
|
+ ///
|
|
|
+ /// Panics if `pos >= self.genome_len()`.
|
|
|
+ pub fn unique_context_at(&self, pos: usize) -> UniqueContext {
|
|
|
+ assert!(pos < self.genome_len);
|
|
|
+
|
|
|
+ let after = match self.min_unique_len[pos] {
|
|
|
+ usize::MAX => None,
|
|
|
+ k => Some(k),
|
|
|
+ };
|
|
|
+
|
|
|
+ let before = if pos == 0 {
|
|
|
+ None
|
|
|
+ } else {
|
|
|
+ let rev_pos = self.genome_len - pos;
|
|
|
+
|
|
|
+ match self.rev_min_unique_len[rev_pos] {
|
|
|
+ usize::MAX => None,
|
|
|
+ k => Some(k),
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ UniqueContext { before, after }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// tests/uniqueness.rs
|
|
|
@@ -558,7 +723,7 @@ mod tests {
|
|
|
let index = GenomeIndex::build(
|
|
|
"/home/t_steimle/ref/hs1/chm13v2.0.fa",
|
|
|
"/home/t_steimle/ref/hs1/hs1_uniqueness",
|
|
|
- None, // None = tout indexer
|
|
|
+ Some(&["chr1"]), // None = tout indexer
|
|
|
)?;
|
|
|
|
|
|
let pos = GenomicPos {
|