|
@@ -6,6 +6,7 @@
|
|
|
|
|
|
|
|
use std::collections::HashMap;
|
|
use std::collections::HashMap;
|
|
|
use std::io::{BufRead, Write};
|
|
use std::io::{BufRead, Write};
|
|
|
|
|
+use std::sync::Arc;
|
|
|
use std::{
|
|
use std::{
|
|
|
fs::File,
|
|
fs::File,
|
|
|
io::BufReader,
|
|
io::BufReader,
|
|
@@ -13,21 +14,76 @@ use std::{
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
use anyhow::Context;
|
|
use anyhow::Context;
|
|
|
-use noodles_fasta::io::{BufReader as NoodlesBufReader, IndexedReader};
|
|
|
|
|
|
|
+use noodles_fasta::{self as fasta, fai, io::indexed_reader::Builder, io::IndexedReader};
|
|
|
|
|
+use pandora_lib_smb::{SmbReader, SmbSession};
|
|
|
|
|
|
|
|
use crate::{positions::GenomePosition, variant::vcf_variant::ReferenceAlternative};
|
|
use crate::{positions::GenomePosition, variant::vcf_variant::ReferenceAlternative};
|
|
|
|
|
|
|
|
-/// Open an indexed FASTA file (requires a `.fai` index alongside the path).
|
|
|
|
|
-///
|
|
|
|
|
-/// # Errors
|
|
|
|
|
-///
|
|
|
|
|
-/// Returns an error if the FASTA or its index cannot be opened.
|
|
|
|
|
-pub fn open_indexed_fasta(
|
|
|
|
|
- reference: &Path,
|
|
|
|
|
-) -> anyhow::Result<IndexedReader<NoodlesBufReader<File>>> {
|
|
|
|
|
- noodles_fasta::io::indexed_reader::Builder::default()
|
|
|
|
|
- .build_from_path(reference)
|
|
|
|
|
- .map_err(|e| anyhow::anyhow!("Failed to open indexed FASTA {}: {e}", reference.display()))
|
|
|
|
|
|
|
+
|
|
|
|
|
+pub struct FaiReader<R> {
|
|
|
|
|
+ inner: IndexedReader<std::io::BufReader<R>>,
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+impl<R> FaiReader<R>
|
|
|
|
|
+where
|
|
|
|
|
+ R: std::io::Read + std::io::Seek,
|
|
|
|
|
+{
|
|
|
|
|
+ /// Build from an already-open data reader plus pre-parsed `.fai` bytes.
|
|
|
|
|
+ fn from_reader(data: R, fai_bytes: &[u8]) -> anyhow::Result<Self> {
|
|
|
|
|
+ let index = fai::io::Reader::new(fai_bytes)
|
|
|
|
|
+ .read_index()
|
|
|
|
|
+ .context("parse .fai index")?;
|
|
|
|
|
+ let inner = Builder::default()
|
|
|
|
|
+ .set_index(index)
|
|
|
|
|
+ .build_from_reader(std::io::BufReader::new(data))
|
|
|
|
|
+ .map_err(|e| anyhow::anyhow!("Failed to build indexed FASTA: {e}"))?;
|
|
|
|
|
+ Ok(Self { inner })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// e.g. delegate region queries — same shape as your existing usage.
|
|
|
|
|
+ pub fn query(&mut self, region: &noodles_core::Region) -> std::io::Result<fasta::Record> {
|
|
|
|
|
+ self.inner.query(region)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+impl FaiReader<File> {
|
|
|
|
|
+ /// Open a local indexed FASTA from disk (replaces `open_indexed_fasta`).
|
|
|
|
|
+ pub fn open(reference: &Path) -> anyhow::Result<Self> {
|
|
|
|
|
+ let fai_path = reference.with_extension(
|
|
|
|
|
+ format!("{}.fai", reference.extension().and_then(|e| e.to_str()).unwrap_or("")),
|
|
|
|
|
+ );
|
|
|
|
|
+ // or just reference.to_string_lossy() + ".fai", matching your existing convention
|
|
|
|
|
+ let fai_bytes = std::fs::read(&fai_path)
|
|
|
|
|
+ .with_context(|| format!("read {}", fai_path.display()))?;
|
|
|
|
|
+ let data = File::open(reference)
|
|
|
|
|
+ .with_context(|| format!("open {}", reference.display()))?;
|
|
|
|
|
+ Self::from_reader(data, &fai_bytes)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+impl FaiReader<SmbReader> {
|
|
|
|
|
+ /// Open an indexed FASTA hosted over SMB.
|
|
|
|
|
+ pub async fn open_smb(session: &Arc<SmbSession>, fasta_rel_path: &str) -> anyhow::Result<Self> {
|
|
|
|
|
+ let fai_rel_path = format!("{fasta_rel_path}.fai");
|
|
|
|
|
+ let fai_bytes = session
|
|
|
|
|
+ .download_file_bytes(&fai_rel_path)
|
|
|
|
|
+ .await
|
|
|
|
|
+ .context("download .fai over SMB")?;
|
|
|
|
|
+ let data = session
|
|
|
|
|
+ .open_smb_reader(fasta_rel_path)
|
|
|
|
|
+ .await
|
|
|
|
|
+ .context("open SMB reader for FASTA data")?;
|
|
|
|
|
+ Self::from_reader(data, &fai_bytes)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+impl<R> std::ops::Deref for FaiReader<R> {
|
|
|
|
|
+ type Target = IndexedReader<std::io::BufReader<R>>;
|
|
|
|
|
+ fn deref(&self) -> &Self::Target { &self.inner }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+impl<R> std::ops::DerefMut for FaiReader<R> {
|
|
|
|
|
+ fn deref_mut(&mut self) -> &mut Self::Target { &mut self.inner }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// Fetch a fixed-width window of reference sequence centred on `pos0`.
|
|
/// Fetch a fixed-width window of reference sequence centred on `pos0`.
|
|
@@ -40,8 +96,8 @@ pub fn open_indexed_fasta(
|
|
|
///
|
|
///
|
|
|
/// Returns an error if the region is out of bounds or the sequence cannot be
|
|
/// Returns an error if the region is out of bounds or the sequence cannot be
|
|
|
/// decoded as UTF-8.
|
|
/// decoded as UTF-8.
|
|
|
-pub fn sequence_at(
|
|
|
|
|
- fasta_reader: &mut IndexedReader<noodles_fasta::io::BufReader<File>>,
|
|
|
|
|
|
|
+pub fn sequence_at<R: std::io::Read + std::io::Seek>(
|
|
|
|
|
+ fasta_reader: &mut FaiReader<R>,
|
|
|
contig: &str,
|
|
contig: &str,
|
|
|
pos0: usize,
|
|
pos0: usize,
|
|
|
len: usize,
|
|
len: usize,
|
|
@@ -75,8 +131,8 @@ pub fn sequence_at(
|
|
|
///
|
|
///
|
|
|
/// Returns an error if the region is out of bounds or the sequence cannot be
|
|
/// Returns an error if the region is out of bounds or the sequence cannot be
|
|
|
/// decoded as UTF-8.
|
|
/// decoded as UTF-8.
|
|
|
-pub fn sequence_range(
|
|
|
|
|
- fasta_reader: &mut IndexedReader<noodles_fasta::io::BufReader<File>>,
|
|
|
|
|
|
|
+pub fn sequence_range<R: std::io::Read + std::io::Seek>(
|
|
|
|
|
+ fasta_reader: &mut FaiReader<R>,
|
|
|
contig: &str,
|
|
contig: &str,
|
|
|
start0: usize, // 0-based inclusive
|
|
start0: usize, // 0-based inclusive
|
|
|
end0_inclusive: usize, // 0-based inclusive
|
|
end0_inclusive: usize, // 0-based inclusive
|
|
@@ -106,8 +162,8 @@ pub fn sequence_range(
|
|
|
///
|
|
///
|
|
|
/// This mirrors VCF's anchored allele representation, where indels include the
|
|
/// This mirrors VCF's anchored allele representation, where indels include the
|
|
|
/// base immediately before the changed sequence.
|
|
/// base immediately before the changed sequence.
|
|
|
-pub fn reference_alternative_from_svlen(
|
|
|
|
|
- fasta_reader: &mut IndexedReader<noodles_fasta::io::BufReader<File>>,
|
|
|
|
|
|
|
+pub fn reference_alternative_from_svlen<R: std::io::Read + std::io::Seek>(
|
|
|
|
|
+ fasta_reader: &mut FaiReader<R>,
|
|
|
position: &GenomePosition,
|
|
position: &GenomePosition,
|
|
|
svlen: i32,
|
|
svlen: i32,
|
|
|
) -> anyhow::Result<ReferenceAlternative> {
|
|
) -> anyhow::Result<ReferenceAlternative> {
|
|
@@ -124,8 +180,8 @@ pub fn reference_alternative_from_svlen(
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
sequence_range(fasta_reader, &position.contig(), start0, end0_inclusive)?
|
|
sequence_range(fasta_reader, &position.contig(), start0, end0_inclusive)?
|
|
|
- .parse()
|
|
|
|
|
- .context("failed to parse FASTA sequence as ReferenceAlternative")
|
|
|
|
|
|
|
+ .parse()
|
|
|
|
|
+ .context("failed to parse FASTA sequence as ReferenceAlternative")
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// Fetch the VCF reference allele for an insertion from FASTA.
|
|
/// Fetch the VCF reference allele for an insertion from FASTA.
|
|
@@ -133,8 +189,8 @@ pub fn reference_alternative_from_svlen(
|
|
|
/// Insertions are represented with an anchor base in VCF, so the reference
|
|
/// Insertions are represented with an anchor base in VCF, so the reference
|
|
|
/// allele is just the base at `position`. `svlen` is validated for consistency
|
|
/// allele is just the base at `position`. `svlen` is validated for consistency
|
|
|
/// but cannot identify the inserted sequence by itself.
|
|
/// but cannot identify the inserted sequence by itself.
|
|
|
-pub fn insertion_reference_from_svlen(
|
|
|
|
|
- fasta_reader: &mut IndexedReader<noodles_fasta::io::BufReader<File>>,
|
|
|
|
|
|
|
+pub fn insertion_reference_from_svlen<R: std::io::Read + std::io::Seek>(
|
|
|
|
|
+ fasta_reader: &mut FaiReader<R>,
|
|
|
position: &GenomePosition,
|
|
position: &GenomePosition,
|
|
|
svlen: i32,
|
|
svlen: i32,
|
|
|
) -> anyhow::Result<ReferenceAlternative> {
|
|
) -> anyhow::Result<ReferenceAlternative> {
|
|
@@ -150,8 +206,8 @@ pub fn insertion_reference_from_svlen(
|
|
|
///
|
|
///
|
|
|
/// This is the correct helper for ordinary insertions where the inserted
|
|
/// This is the correct helper for ordinary insertions where the inserted
|
|
|
/// sequence comes from the caller (`SVINSSEQ`, sequence-resolved ALT, etc.).
|
|
/// sequence comes from the caller (`SVINSSEQ`, sequence-resolved ALT, etc.).
|
|
|
-pub fn insertion_alternative_from_sequence(
|
|
|
|
|
- fasta_reader: &mut IndexedReader<noodles_fasta::io::BufReader<File>>,
|
|
|
|
|
|
|
+pub fn insertion_alternative_from_sequence<R: std::io::Read + std::io::Seek>(
|
|
|
|
|
+ fasta_reader: &mut FaiReader<R>,
|
|
|
position: &GenomePosition,
|
|
position: &GenomePosition,
|
|
|
inserted_sequence: &str,
|
|
inserted_sequence: &str,
|
|
|
) -> anyhow::Result<ReferenceAlternative> {
|
|
) -> anyhow::Result<ReferenceAlternative> {
|
|
@@ -172,8 +228,8 @@ pub fn insertion_alternative_from_sequence(
|
|
|
/// This is only valid for tandem-duplication-like insertions where the inserted
|
|
/// This is only valid for tandem-duplication-like insertions where the inserted
|
|
|
/// bases are copied from the reference immediately after the anchor. For novel
|
|
/// bases are copied from the reference immediately after the anchor. For novel
|
|
|
/// inserted sequence, use [`insertion_alternative_from_sequence`] instead.
|
|
/// inserted sequence, use [`insertion_alternative_from_sequence`] instead.
|
|
|
-pub fn reference_derived_insertion_alternative_from_svlen(
|
|
|
|
|
- fasta_reader: &mut IndexedReader<noodles_fasta::io::BufReader<File>>,
|
|
|
|
|
|
|
+pub fn reference_derived_insertion_alternative_from_svlen<R: std::io::Read + std::io::Seek>(
|
|
|
|
|
+ fasta_reader: &mut FaiReader<R>,
|
|
|
position: &GenomePosition,
|
|
position: &GenomePosition,
|
|
|
svlen: i32,
|
|
svlen: i32,
|
|
|
) -> anyhow::Result<ReferenceAlternative> {
|
|
) -> anyhow::Result<ReferenceAlternative> {
|
|
@@ -185,14 +241,9 @@ pub fn reference_derived_insertion_alternative_from_svlen(
|
|
|
.checked_add(len)
|
|
.checked_add(len)
|
|
|
.context("SVLEN FASTA range end coordinate overflow")?;
|
|
.context("SVLEN FASTA range end coordinate overflow")?;
|
|
|
|
|
|
|
|
- sequence_range(
|
|
|
|
|
- fasta_reader,
|
|
|
|
|
- &position.contig(),
|
|
|
|
|
- start0,
|
|
|
|
|
- end0_inclusive,
|
|
|
|
|
- )?
|
|
|
|
|
- .parse()
|
|
|
|
|
- .context("failed to parse FASTA sequence as ReferenceAlternative")
|
|
|
|
|
|
|
+ sequence_range(fasta_reader, &position.contig(), start0, end0_inclusive)?
|
|
|
|
|
+ .parse()
|
|
|
|
|
+ .context("failed to parse FASTA sequence as ReferenceAlternative")
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// A single-contig FASTA file produced by [`split_fasta`].
|
|
/// A single-contig FASTA file produced by [`split_fasta`].
|