use crate::error::{DnaError, Result};
use ruvector_core::{
types::{DbOptions, DistanceMetric, HnswConfig},
VectorDB,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Nucleotide {
A,
C,
G,
T,
N,
}
impl Nucleotide {
pub fn complement(&self) -> Self {
match self {
Nucleotide::A => Nucleotide::T,
Nucleotide::T => Nucleotide::A,
Nucleotide::C => Nucleotide::G,
Nucleotide::G => Nucleotide::C,
Nucleotide::N => Nucleotide::N,
}
}
pub fn to_u8(&self) -> u8 {
match self {
Nucleotide::A => 0,
Nucleotide::C => 1,
Nucleotide::G => 2,
Nucleotide::T => 3,
Nucleotide::N => 4,
}
}
pub fn from_u8(val: u8) -> Result<Self> {
match val {
0 => Ok(Nucleotide::A),
1 => Ok(Nucleotide::C),
2 => Ok(Nucleotide::G),
3 => Ok(Nucleotide::T),
4 => Ok(Nucleotide::N),
_ => Err(DnaError::InvalidSequence(format!(
"Invalid nucleotide encoding: {}",
val
))),
}
}
}
impl fmt::Display for Nucleotide {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
match self {
Nucleotide::A => 'A',
Nucleotide::C => 'C',
Nucleotide::G => 'G',
Nucleotide::T => 'T',
Nucleotide::N => 'N',
}
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DnaSequence {
bases: Vec<Nucleotide>,
}
impl DnaSequence {
pub fn new(bases: Vec<Nucleotide>) -> Self {
Self { bases }
}
pub fn from_str(s: &str) -> Result<Self> {
let bases: Result<Vec<_>> = s
.chars()
.map(|c| match c.to_ascii_uppercase() {
'A' => Ok(Nucleotide::A),
'C' => Ok(Nucleotide::C),
'G' => Ok(Nucleotide::G),
'T' => Ok(Nucleotide::T),
'N' => Ok(Nucleotide::N),
_ => Err(DnaError::InvalidSequence(format!(
"Invalid character: {}",
c
))),
})
.collect();
let bases = bases?;
if bases.is_empty() {
return Err(DnaError::EmptySequence);
}
Ok(Self { bases })
}
pub fn complement(&self) -> Self {
Self {
bases: self.bases.iter().map(|b| b.complement()).collect(),
}
}
pub fn reverse_complement(&self) -> Self {
Self {
bases: self.bases.iter().rev().map(|b| b.complement()).collect(),
}
}
pub fn to_kmer_vector(&self, k: usize, dims: usize) -> Result<Vec<f32>> {
if k == 0 || k > 15 {
return Err(DnaError::InvalidKmerSize(k));
}
if self.bases.len() < k {
return Err(DnaError::InvalidSequence(
"Sequence shorter than k-mer size".to_string(),
));
}
let mut vector = vec![0.0f32; dims];
let base: u64 = 5;
let pow_k = base.pow(k as u32 - 1);
let mut hash = self.bases[..k].iter().fold(0u64, |acc, &b| {
acc.wrapping_mul(5).wrapping_add(b.to_u8() as u64)
});
vector[(hash as usize) % dims] += 1.0;
for i in 1..=(self.bases.len() - k) {
let old = self.bases[i - 1].to_u8() as u64;
let new = self.bases[i + k - 1].to_u8() as u64;
hash = hash
.wrapping_sub(old.wrapping_mul(pow_k))
.wrapping_mul(5)
.wrapping_add(new);
vector[(hash as usize) % dims] += 1.0;
}
let magnitude: f32 = vector.iter().map(|x| x * x).sum::<f32>().sqrt();
if magnitude > 0.0 {
let inv = 1.0 / magnitude;
for v in &mut vector {
*v *= inv;
}
}
Ok(vector)
}
pub fn len(&self) -> usize {
self.bases.len()
}
pub fn is_empty(&self) -> bool {
self.bases.is_empty()
}
pub fn get(&self, index: usize) -> Option<Nucleotide> {
self.bases.get(index).copied()
}
pub fn bases(&self) -> &[Nucleotide] {
&self.bases
}
pub fn encode_one_hot(&self) -> Vec<f32> {
let mut result = vec![0.0f32; self.bases.len() * 4];
for (i, base) in self.bases.iter().enumerate() {
let offset = i * 4;
match base {
Nucleotide::A => result[offset] = 1.0,
Nucleotide::C => result[offset + 1] = 1.0,
Nucleotide::G => result[offset + 2] = 1.0,
Nucleotide::T => result[offset + 3] = 1.0,
Nucleotide::N => {} }
}
result
}
pub fn translate(&self) -> Result<ProteinSequence> {
if self.bases.len() < 3 {
return Err(DnaError::InvalidSequence(
"Sequence too short for translation".to_string(),
));
}
let mut residues = Vec::new();
for chunk in self.bases.chunks(3) {
if chunk.len() < 3 {
break;
}
let codon = (chunk[0], chunk[1], chunk[2]);
let aa = match codon {
(Nucleotide::A, Nucleotide::T, Nucleotide::G) => ProteinResidue::M, (Nucleotide::T, Nucleotide::G, Nucleotide::G) => ProteinResidue::W, (Nucleotide::T, Nucleotide::T, Nucleotide::T)
| (Nucleotide::T, Nucleotide::T, Nucleotide::C) => ProteinResidue::F, (Nucleotide::T, Nucleotide::T, Nucleotide::A)
| (Nucleotide::T, Nucleotide::T, Nucleotide::G)
| (Nucleotide::C, Nucleotide::T, _) => ProteinResidue::L, (Nucleotide::A, Nucleotide::T, Nucleotide::T)
| (Nucleotide::A, Nucleotide::T, Nucleotide::C)
| (Nucleotide::A, Nucleotide::T, Nucleotide::A) => ProteinResidue::I, (Nucleotide::G, Nucleotide::T, _) => ProteinResidue::V, (Nucleotide::T, Nucleotide::C, _)
| (Nucleotide::A, Nucleotide::G, Nucleotide::T)
| (Nucleotide::A, Nucleotide::G, Nucleotide::C) => ProteinResidue::S, (Nucleotide::C, Nucleotide::C, _) => ProteinResidue::P, (Nucleotide::A, Nucleotide::C, _) => ProteinResidue::T, (Nucleotide::G, Nucleotide::C, _) => ProteinResidue::A, (Nucleotide::T, Nucleotide::A, Nucleotide::T)
| (Nucleotide::T, Nucleotide::A, Nucleotide::C) => ProteinResidue::Y, (Nucleotide::C, Nucleotide::A, Nucleotide::T)
| (Nucleotide::C, Nucleotide::A, Nucleotide::C) => ProteinResidue::H, (Nucleotide::C, Nucleotide::A, Nucleotide::A)
| (Nucleotide::C, Nucleotide::A, Nucleotide::G) => ProteinResidue::Q, (Nucleotide::A, Nucleotide::A, Nucleotide::T)
| (Nucleotide::A, Nucleotide::A, Nucleotide::C) => ProteinResidue::N, (Nucleotide::A, Nucleotide::A, Nucleotide::A)
| (Nucleotide::A, Nucleotide::A, Nucleotide::G) => ProteinResidue::K, (Nucleotide::G, Nucleotide::A, Nucleotide::T)
| (Nucleotide::G, Nucleotide::A, Nucleotide::C) => ProteinResidue::D, (Nucleotide::G, Nucleotide::A, Nucleotide::A)
| (Nucleotide::G, Nucleotide::A, Nucleotide::G) => ProteinResidue::E, (Nucleotide::T, Nucleotide::G, Nucleotide::T)
| (Nucleotide::T, Nucleotide::G, Nucleotide::C) => ProteinResidue::C, (Nucleotide::C, Nucleotide::G, _)
| (Nucleotide::A, Nucleotide::G, Nucleotide::A)
| (Nucleotide::A, Nucleotide::G, Nucleotide::G) => ProteinResidue::R, (Nucleotide::G, Nucleotide::G, _) => ProteinResidue::G, (Nucleotide::T, Nucleotide::A, Nucleotide::A)
| (Nucleotide::T, Nucleotide::A, Nucleotide::G)
| (Nucleotide::T, Nucleotide::G, Nucleotide::A) => break,
_ => ProteinResidue::X, };
residues.push(aa);
}
Ok(ProteinSequence::new(residues))
}
pub fn align_with_attention(&self, reference: &DnaSequence) -> Result<AlignmentResult> {
if self.is_empty() || reference.is_empty() {
return Err(DnaError::AlignmentError(
"Cannot align empty sequences".to_string(),
));
}
let query_len = self.len();
let ref_len = reference.len();
let mut best_score = i32::MIN;
let mut best_offset = 0;
for offset in 0..ref_len.saturating_sub(query_len / 2) {
let mut score: i32 = 0;
let overlap = query_len.min(ref_len - offset);
for i in 0..overlap {
if self.bases[i] == reference.bases[offset + i] {
score += 2; } else {
score -= 1; }
}
if score > best_score {
best_score = score;
best_offset = offset;
}
}
let overlap = query_len.min(ref_len.saturating_sub(best_offset));
let mut cigar = Vec::new();
let mut match_run = 0;
for i in 0..overlap {
if self.bases[i] == reference.bases[best_offset + i] {
match_run += 1;
} else {
if match_run > 0 {
cigar.push(CigarOp::M(match_run));
match_run = 0;
}
cigar.push(CigarOp::M(1)); }
}
if match_run > 0 {
cigar.push(CigarOp::M(match_run));
}
Ok(AlignmentResult {
score: best_score,
cigar,
mapped_position: GenomicPosition {
chromosome: 1,
position: best_offset as u64,
reference_allele: reference
.bases
.get(best_offset)
.copied()
.unwrap_or(Nucleotide::N),
alternate_allele: None,
},
mapping_quality: QualityScore::new(
((best_score.max(0) as f64 / overlap.max(1) as f64) * 60.0).min(60.0) as u8,
)
.unwrap_or(QualityScore(0)),
})
}
}
impl fmt::Display for DnaSequence {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for base in &self.bases {
write!(f, "{}", base)?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GenomicPosition {
pub chromosome: u8,
pub position: u64,
pub reference_allele: Nucleotide,
pub alternate_allele: Option<Nucleotide>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct QualityScore(u8);
impl QualityScore {
pub fn new(score: u8) -> Result<Self> {
if score > 93 {
return Err(DnaError::InvalidQuality(score));
}
Ok(Self(score))
}
pub fn value(&self) -> u8 {
self.0
}
pub fn to_error_probability(&self) -> f64 {
10_f64.powf(-(self.0 as f64) / 10.0)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Variant {
Snp {
position: GenomicPosition,
quality: QualityScore,
},
Insertion {
position: GenomicPosition,
inserted_bases: DnaSequence,
quality: QualityScore,
},
Deletion {
position: GenomicPosition,
deleted_length: usize,
quality: QualityScore,
},
StructuralVariant {
chromosome: u8,
start: u64,
end: u64,
variant_type: String,
quality: QualityScore,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CigarOp {
M(usize),
I(usize),
D(usize),
S(usize),
H(usize),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AlignmentResult {
pub score: i32,
pub cigar: Vec<CigarOp>,
pub mapped_position: GenomicPosition,
pub mapping_quality: QualityScore,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ProteinResidue {
A,
C,
D,
E,
F,
G,
H,
I,
K,
L,
M,
N,
P,
Q,
R,
S,
T,
V,
W,
Y,
X,
}
impl ProteinResidue {
pub fn to_char(&self) -> char {
match self {
ProteinResidue::A => 'A',
ProteinResidue::C => 'C',
ProteinResidue::D => 'D',
ProteinResidue::E => 'E',
ProteinResidue::F => 'F',
ProteinResidue::G => 'G',
ProteinResidue::H => 'H',
ProteinResidue::I => 'I',
ProteinResidue::K => 'K',
ProteinResidue::L => 'L',
ProteinResidue::M => 'M',
ProteinResidue::N => 'N',
ProteinResidue::P => 'P',
ProteinResidue::Q => 'Q',
ProteinResidue::R => 'R',
ProteinResidue::S => 'S',
ProteinResidue::T => 'T',
ProteinResidue::V => 'V',
ProteinResidue::W => 'W',
ProteinResidue::Y => 'Y',
ProteinResidue::X => 'X',
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProteinSequence {
residues: Vec<ProteinResidue>,
}
impl ProteinSequence {
pub fn new(residues: Vec<ProteinResidue>) -> Self {
Self { residues }
}
pub fn residues(&self) -> &[ProteinResidue] {
&self.residues
}
pub fn len(&self) -> usize {
self.residues.len()
}
pub fn is_empty(&self) -> bool {
self.residues.is_empty()
}
pub fn build_contact_graph(&self, distance_threshold: f32) -> Result<ContactGraph> {
if self.residues.is_empty() {
return Err(DnaError::InvalidSequence(
"Cannot build contact graph for empty protein".to_string(),
));
}
let n = self.residues.len();
let threshold = distance_threshold as usize;
let mut edges = Vec::new();
for i in 0..n {
for j in (i + 4)..n {
let seq_dist = j - i;
if seq_dist <= threshold {
let contact_prob = 1.0 / (1.0 + (seq_dist as f32 - 4.0) / threshold as f32);
edges.push((i, j, contact_prob));
}
}
}
Ok(ContactGraph {
num_residues: n,
distance_threshold,
edges,
})
}
pub fn predict_contacts(&self, graph: &ContactGraph) -> Result<Vec<(usize, usize, f32)>> {
let mut predictions: Vec<(usize, usize, f32)> = graph
.edges
.iter()
.map(|&(i, j, base_score)| {
let boost = if i < self.residues.len() && j < self.residues.len() {
let ri = &self.residues[i];
let rj = &self.residues[j];
let hydrophobic = |r: &ProteinResidue| {
matches!(
r,
ProteinResidue::A
| ProteinResidue::V
| ProteinResidue::L
| ProteinResidue::I
| ProteinResidue::F
| ProteinResidue::W
| ProteinResidue::M
)
};
if hydrophobic(ri) && hydrophobic(rj) {
1.5
} else {
1.0
}
} else {
1.0
};
(i, j, (base_score * boost).min(1.0))
})
.collect();
predictions.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal));
Ok(predictions)
}
}
#[derive(Debug, Clone)]
pub struct ContactGraph {
pub num_residues: usize,
pub distance_threshold: f32,
pub edges: Vec<(usize, usize, f32)>,
}
pub struct KmerIndex {
db: VectorDB,
k: usize,
dims: usize,
}
impl KmerIndex {
pub fn new(k: usize, dims: usize, storage_path: &str) -> Result<Self> {
let options = DbOptions {
dimensions: dims,
distance_metric: DistanceMetric::Cosine,
storage_path: storage_path.to_string(),
hnsw_config: Some(HnswConfig {
m: 16,
ef_construction: 200,
ef_search: 100,
max_elements: 1_000_000,
}),
quantization: None,
};
let db = VectorDB::new(options)?;
Ok(Self { db, k, dims })
}
pub fn db(&self) -> &VectorDB {
&self.db
}
pub fn k(&self) -> usize {
self.k
}
pub fn dims(&self) -> usize {
self.dims
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnalysisConfig {
pub kmer_size: usize,
pub vector_dims: usize,
pub min_quality: u8,
pub match_score: i32,
pub mismatch_penalty: i32,
pub gap_open_penalty: i32,
pub gap_extend_penalty: i32,
pub parameters: HashMap<String, serde_json::Value>,
}
impl Default for AnalysisConfig {
fn default() -> Self {
Self {
kmer_size: 11,
vector_dims: 512,
min_quality: 20,
match_score: 2,
mismatch_penalty: -1,
gap_open_penalty: -3,
gap_extend_penalty: -1,
parameters: HashMap::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_nucleotide_complement() {
assert_eq!(Nucleotide::A.complement(), Nucleotide::T);
assert_eq!(Nucleotide::G.complement(), Nucleotide::C);
}
#[test]
fn test_dna_sequence() {
let seq = DnaSequence::from_str("ACGT").unwrap();
assert_eq!(seq.len(), 4);
assert_eq!(seq.to_string(), "ACGT");
}
#[test]
fn test_reverse_complement() {
let seq = DnaSequence::from_str("ACGT").unwrap();
let rc = seq.reverse_complement();
assert_eq!(rc.to_string(), "ACGT");
}
}