use crate::error::Result;
use crate::types::{DnaSequence, KmerIndex, Nucleotide, ProteinResidue, ProteinSequence};
use ruvector_core::types::{SearchQuery, VectorEntry};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::Instant;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PipelineConfig {
pub k: usize,
pub window_size: usize,
pub min_depth: usize,
pub min_quality: u8,
}
impl Default for PipelineConfig {
fn default() -> Self {
Self {
k: 21,
window_size: 512,
min_depth: 10,
min_quality: 20,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KmerAnalysisResult {
pub total_kmers: usize,
pub unique_kmers: usize,
pub gc_content: f64,
pub top_similar_sequences: Vec<SimilarSequence>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SimilarSequence {
pub id: String,
pub similarity: f32,
pub position: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VariantCall {
pub position: u64,
pub reference: Nucleotide,
pub alternate: Nucleotide,
pub quality: u8,
pub depth: usize,
pub allele_frequency: f64,
}
#[derive(Debug, Clone)]
pub struct PileupColumn {
pub position: u64,
pub reference: Nucleotide,
pub bases: Vec<Nucleotide>,
pub qualities: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProteinAnalysisResult {
pub sequence: String,
pub length: usize,
pub predicted_contacts: Vec<(usize, usize, f32)>,
pub secondary_structure: Vec<char>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FullAnalysisResult {
pub kmer_stats: KmerAnalysisResult,
pub variants: Vec<VariantCall>,
pub proteins: Vec<ProteinAnalysisResult>,
pub execution_time_ms: u128,
}
pub struct GenomicPipeline {
config: PipelineConfig,
}
impl GenomicPipeline {
pub fn new(config: PipelineConfig) -> Self {
Self { config }
}
pub fn run_kmer_analysis(&self, sequences: &[(&str, &[u8])]) -> Result<KmerAnalysisResult> {
let mut total_kmers = 0;
let mut kmer_set = std::collections::HashSet::new();
let mut gc_count = 0;
let mut total_bases = 0;
let index = KmerIndex::new(self.config.k, 384, ":memory:")?;
for (id, seq) in sequences {
if seq.len() < self.config.k {
continue;
}
total_bases += seq.len();
for window in seq.windows(self.config.k) {
total_kmers += 1;
kmer_set.insert(window.to_vec());
for &base in window {
if base == b'G' || base == b'C' {
gc_count += 1;
}
}
}
let dna_seq = DnaSequence::from_str(&String::from_utf8_lossy(seq))?;
if let Ok(vector) = dna_seq.to_kmer_vector(self.config.k, 384) {
let entry = VectorEntry {
id: Some(id.to_string()),
vector,
metadata: None,
};
let _ = index.db().insert(entry);
}
}
let gc_content = if total_bases > 0 {
(gc_count as f64) / (total_bases as f64)
} else {
0.0
};
let mut top_similar = Vec::new();
if !sequences.is_empty() {
if let Some((query_id, query_seq)) = sequences.first() {
let dna_seq = DnaSequence::from_str(&String::from_utf8_lossy(query_seq))?;
if let Ok(query_vector) = dna_seq.to_kmer_vector(self.config.k, 384) {
let search_query = SearchQuery {
vector: query_vector,
k: 5,
filter: None,
ef_search: None,
};
if let Ok(results) = index.db().search(search_query) {
for result in results {
if result.id != *query_id {
top_similar.push(SimilarSequence {
id: result.id.clone(),
similarity: result.score,
position: 0,
});
}
}
}
}
}
}
Ok(KmerAnalysisResult {
total_kmers,
unique_kmers: kmer_set.len(),
gc_content,
top_similar_sequences: top_similar,
})
}
pub fn run_variant_calling(
&self,
pileups: &[PileupColumn],
_reference: &[u8],
) -> Result<Vec<VariantCall>> {
let mut variants = Vec::new();
for pileup in pileups {
if pileup.bases.len() < self.config.min_depth {
continue;
}
let mut allele_counts: HashMap<Nucleotide, usize> = HashMap::new();
for &base in &pileup.bases {
*allele_counts.entry(base).or_insert(0) += 1;
}
let _ref_count = allele_counts.get(&pileup.reference).copied().unwrap_or(0);
for (&allele, &count) in &allele_counts {
if allele == pileup.reference || allele == Nucleotide::N {
continue;
}
let allele_freq = count as f64 / pileup.bases.len() as f64;
if allele_freq > 0.2 && count >= 3 {
let quality = pileup
.qualities
.iter()
.take(count)
.map(|&q| q as u16)
.sum::<u16>()
.min(255) as u8;
if quality >= self.config.min_quality {
variants.push(VariantCall {
position: pileup.position,
reference: pileup.reference,
alternate: allele,
quality,
depth: pileup.bases.len(),
allele_frequency: allele_freq,
});
}
}
}
}
Ok(variants)
}
pub fn run_protein_analysis(&self, dna: &[u8]) -> Result<ProteinAnalysisResult> {
let protein = self.translate_dna(dna)?;
let contacts = self.predict_protein_contacts(&protein)?;
let secondary_structure = self.predict_secondary_structure(&protein);
Ok(ProteinAnalysisResult {
sequence: protein.residues().iter().map(|r| r.to_char()).collect(),
length: protein.len(),
predicted_contacts: contacts,
secondary_structure,
})
}
pub fn run_full_pipeline(
&self,
sequence: &[u8],
reference: &[u8],
) -> Result<FullAnalysisResult> {
let start = Instant::now();
let kmer_stats =
self.run_kmer_analysis(&[("query", sequence), ("reference", reference)])?;
let pileups = self.generate_pileups(sequence, reference)?;
let variants = self.run_variant_calling(&pileups, reference)?;
let proteins = self.find_orfs_and_translate(sequence)?;
let execution_time_ms = start.elapsed().as_millis();
Ok(FullAnalysisResult {
kmer_stats,
variants,
proteins,
execution_time_ms,
})
}
fn translate_dna(&self, dna: &[u8]) -> Result<ProteinSequence> {
let mut residues = Vec::new();
for codon in dna.chunks(3) {
if codon.len() < 3 {
break;
}
let aa = self.codon_to_amino_acid(codon);
if aa == ProteinResidue::X {
break; }
residues.push(aa);
}
Ok(ProteinSequence::new(residues))
}
fn codon_to_amino_acid(&self, codon: &[u8]) -> ProteinResidue {
match codon {
b"ATG" => ProteinResidue::M,
b"TGG" => ProteinResidue::W,
b"TTT" | b"TTC" => ProteinResidue::F,
b"TTA" | b"TTG" | b"CTT" | b"CTC" | b"CTA" | b"CTG" => ProteinResidue::L,
b"ATT" | b"ATC" | b"ATA" => ProteinResidue::I,
b"GTT" | b"GTC" | b"GTA" | b"GTG" => ProteinResidue::V,
b"TCT" | b"TCC" | b"TCA" | b"TCG" | b"AGT" | b"AGC" => ProteinResidue::S,
b"CCT" | b"CCC" | b"CCA" | b"CCG" => ProteinResidue::P,
b"ACT" | b"ACC" | b"ACA" | b"ACG" => ProteinResidue::T,
b"GCT" | b"GCC" | b"GCA" | b"GCG" => ProteinResidue::A,
b"TAT" | b"TAC" => ProteinResidue::Y,
b"CAT" | b"CAC" => ProteinResidue::H,
b"CAA" | b"CAG" => ProteinResidue::Q,
b"AAT" | b"AAC" => ProteinResidue::N,
b"AAA" | b"AAG" => ProteinResidue::K,
b"GAT" | b"GAC" => ProteinResidue::D,
b"GAA" | b"GAG" => ProteinResidue::E,
b"TGT" | b"TGC" => ProteinResidue::C,
b"CGT" | b"CGC" | b"CGA" | b"CGG" | b"AGA" | b"AGG" => ProteinResidue::R,
b"GGT" | b"GGC" | b"GGA" | b"GGG" => ProteinResidue::G,
_ => ProteinResidue::X, }
}
fn predict_protein_contacts(
&self,
protein: &ProteinSequence,
) -> Result<Vec<(usize, usize, f32)>> {
let residues = protein.residues();
let n = residues.len();
if n < 5 {
return Ok(Vec::new());
}
let features: Vec<f32> = residues
.iter()
.map(|r| r.to_char() as u8 as f32 / 255.0)
.collect();
let mut contacts = Vec::new();
for i in 0..n {
for j in (i + 5)..n {
let score = (features[i] + features[j]) / 2.0;
if score > 0.5 {
contacts.push((i, j, score));
}
}
}
contacts.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap());
contacts.truncate(10);
Ok(contacts)
}
fn predict_secondary_structure(&self, protein: &ProteinSequence) -> Vec<char> {
protein
.residues()
.iter()
.map(|r| match r {
ProteinResidue::A | ProteinResidue::E | ProteinResidue::L | ProteinResidue::M => {
'H'
}
ProteinResidue::V | ProteinResidue::I | ProteinResidue::Y | ProteinResidue::F => {
'E'
}
_ => 'C',
})
.collect()
}
fn generate_pileups(&self, sequence: &[u8], reference: &[u8]) -> Result<Vec<PileupColumn>> {
let mut pileups = Vec::new();
let min_len = sequence.len().min(reference.len());
for i in 0..min_len {
let ref_base = match reference[i] {
b'A' => Nucleotide::A,
b'C' => Nucleotide::C,
b'G' => Nucleotide::G,
b'T' => Nucleotide::T,
_ => Nucleotide::N,
};
let seq_base = match sequence[i] {
b'A' => Nucleotide::A,
b'C' => Nucleotide::C,
b'G' => Nucleotide::G,
b'T' => Nucleotide::T,
_ => Nucleotide::N,
};
let depth = 15 + (i % 10);
let bases = vec![seq_base; depth];
let qualities = vec![30; depth];
pileups.push(PileupColumn {
position: i as u64,
reference: ref_base,
bases,
qualities,
});
}
Ok(pileups)
}
fn find_orfs_and_translate(&self, sequence: &[u8]) -> Result<Vec<ProteinAnalysisResult>> {
let mut proteins = Vec::new();
for i in 0..sequence.len().saturating_sub(30) {
if sequence[i..].starts_with(b"ATG") {
let orf = &sequence[i..];
if let Ok(protein_result) = self.run_protein_analysis(orf) {
if protein_result.length >= 10 {
proteins.push(protein_result);
if proteins.len() >= 3 {
break;
}
}
}
}
}
Ok(proteins)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pipeline_creation() {
let config = PipelineConfig::default();
let pipeline = GenomicPipeline::new(config);
assert_eq!(pipeline.config.k, 21);
}
#[test]
fn test_kmer_analysis() {
let config = PipelineConfig::default();
let pipeline = GenomicPipeline::new(config);
let sequences = vec![("seq1", b"ACGTACGTACGTACGTACGTACGT".as_ref())];
let result = pipeline.run_kmer_analysis(&sequences);
assert!(result.is_ok());
}
}