From 37431601d381e69f6788624fdd93a3a5fec3868e Mon Sep 17 00:00:00 2001 From: Douwe Schulte Date: Fri, 25 Aug 2023 14:56:26 +0200 Subject: [PATCH] Removed unused parts of the GUI and small fixes --- alignment/alignment.rs | 398 -------------------------- alignment/alphabet.rs | 283 ------------------- alignment/aminoacid.rs | 170 ------------ alignment/bin.rs | 101 ------- alignment/lib.rs | 42 --- alignment/template.rs | 123 --------- src-tauri/Cargo.lock | 4 +- src-tauri/Cargo.toml | 10 +- src-tauri/alignment/output.html | 109 -------- src-tauri/alignment/preview.jpg | Bin 82998 -> 0 bytes src-tauri/alignment/readme.md | 28 -- src-tauri/alignment/src/alignment.rs | 399 --------------------------- src-tauri/alignment/src/alphabet.rs | 283 ------------------- src-tauri/alignment/src/aminoacid.rs | 170 ------------ src-tauri/alignment/src/bin.rs | 102 ------- src-tauri/alignment/src/lib.rs | 42 --- src-tauri/alignment/src/template.rs | 128 --------- src-tauri/build.rs | 178 +++++------- src-tauri/src/main.rs | 106 +------ src-tauri/tauri.conf.json | 8 +- src/main.js | 20 -- 21 files changed, 70 insertions(+), 2634 deletions(-) delete mode 100644 alignment/alignment.rs delete mode 100644 alignment/alphabet.rs delete mode 100644 alignment/aminoacid.rs delete mode 100644 alignment/bin.rs delete mode 100644 alignment/lib.rs delete mode 100644 alignment/template.rs delete mode 100644 src-tauri/alignment/output.html delete mode 100644 src-tauri/alignment/preview.jpg delete mode 100644 src-tauri/alignment/readme.md delete mode 100644 src-tauri/alignment/src/alignment.rs delete mode 100644 src-tauri/alignment/src/alphabet.rs delete mode 100644 src-tauri/alignment/src/aminoacid.rs delete mode 100644 src-tauri/alignment/src/bin.rs delete mode 100644 src-tauri/alignment/src/lib.rs delete mode 100644 src-tauri/alignment/src/template.rs diff --git a/alignment/alignment.rs b/alignment/alignment.rs deleted file mode 100644 index 0b91007..0000000 --- a/alignment/alignment.rs +++ /dev/null @@ -1,398 +0,0 @@ -use crate::alphabet::{Alphabet, Scoring}; -use crate::aminoacid::*; -use itertools::Itertools; -use std::fmt::Write; - -/// An alignment of two reads. -#[derive(Debug, Clone)] -pub struct Alignment { - /// The score of this alignment - pub score: isize, - /// The path or steps taken for the alignment - pub path: Vec, - /// The position in the first sequence where the alignment starts - pub start_a: usize, - /// The position in the second sequence where the alignment starts - pub start_b: usize, - /// The first sequence - pub seq_a: Vec, - /// The second sequence - pub seq_b: Vec, -} - -impl Alignment { - fn short(&self) -> String { - self.path.iter().map(Piece::short).join("") - } - - fn aligned(&self) -> String { - let blocks: Vec = " ▁▂▃▄▅▆▇█".chars().collect(); - let blocks_neg: Vec = " ▔▔▔▀▀▀▀█".chars().collect(); - let mut str_a = String::new(); - let mut str_b = String::new(); - let mut str_blocks = String::new(); - let mut str_blocks_neg = String::new(); - let mut loc_a = self.start_a; - let mut loc_b = self.start_b; - - for piece in &self.path { - let l = std::cmp::max(piece.step_b, piece.step_a); - if piece.step_a == 0 { - let _ = write!(str_a, "{:- 0 { - " ".to_string() - } else { - #[allow(clippy::cast_sign_loss)] // Checked above - blocks_neg[-piece.local_score as usize].to_string() - }, - l as usize - ) - ); - - loc_a += piece.step_a as usize; - loc_b += piece.step_b as usize; - } - - format!("{}\n{}\n{}\n{}", str_a, str_b, str_blocks, str_blocks_neg) - } - - /// Generate a summary of this alignment for printing to the command line - pub fn summary(&self) -> String { - format!( - "score: {}\npath: {}\nstart: ({}, {})\naligned:\n{}", - self.score, - self.short(), - self.start_a, - self.start_b, - self.aligned() - ) - } - - /// The total number of residues matched on the first sequence - pub fn len_a(&self) -> usize { - self.path.iter().map(|p| p.step_a as usize).sum() - } - - /// The total number of residues matched on the second sequence - pub fn len_b(&self) -> usize { - self.path.iter().map(|p| p.step_b as usize).sum() - } -} - -/// A piece in an alignment, determining what step was taken in the alignment and how this impacted the score -#[derive(Clone, Default, Debug)] -pub struct Piece { - /// The total score of the path up till now - pub score: isize, - /// The local contribution to the score of this piece - pub local_score: i8, - /// The number of steps on the first sequence - pub step_a: u8, - /// The number of steps on the second sequence - pub step_b: u8, -} - -impl Piece { - /// Create a new alignment piece - pub const fn new(score: isize, local_score: i8, step_a: u8, step_b: u8) -> Self { - Self { - score, - local_score, - step_a, - step_b, - } - } -} - -impl Piece { - /// Display this piece very compactly - pub fn short(&self) -> String { - match (self.step_a, self.step_b) { - (0, 1) => "I".to_string(), - (1, 0) => "D".to_string(), - (1, 1) => "M".to_string(), - (a, b) => format!("S[{},{}]", b, a), - } - } -} - -/// The type of alignment to perform -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum Type { - /// Global alignment, which tries to find the best alignment to link both sequences fully to each other, like the Needleman Wunsch algorithm - Global, - /// Local alignment, which tries to find the best patch of both sequences to align to each other, this could lead to trailing ends on both sides of both sequences, like the Smith Waterman - Local, - /// Hybrid alignment, the second sequence will be fully aligned to the first sequence, this could lead to trailing ends on the first sequence but not on the second. - GlobalForB, -} - -impl Type { - const fn global(self) -> bool { - !matches!(self, Self::Local) - } -} - -/// # Panics -/// It panics when the length of `seq_a` or `seq_b` is bigger then [`isize::MAX`]. -#[allow(clippy::too_many_lines)] -pub fn align(seq_a: &[AminoAcid], seq_b: &[AminoAcid], alphabet: &Alphabet, ty: Type) -> Alignment { - assert!(isize::try_from(seq_a.len()).is_ok()); - assert!(isize::try_from(seq_b.len()).is_ok()); - let mut matrix = vec![vec![Piece::default(); seq_b.len() + 1]; seq_a.len() + 1]; - let mut high = (0, 0, 0); - - if ty.global() { - #[allow(clippy::cast_possible_wrap)] - // b is always less than seq_b - for index_b in 0..=seq_b.len() { - matrix[0][index_b] = Piece::new( - (index_b as isize) * Scoring::GapExtendPenalty as isize, - Scoring::GapExtendPenalty as i8, - 0, - if index_b == 0 { 0 } else { 1 }, - ); - } - } - if ty == Type::Global { - #[allow(clippy::cast_possible_wrap)] - // a is always less than seq_a - for (index_a, row) in matrix.iter_mut().enumerate() { - row[0] = Piece::new( - (index_a as isize) * Scoring::GapExtendPenalty as isize, - Scoring::GapExtendPenalty as i8, - if index_a == 0 { 0 } else { 1 }, - 0, - ); - } - } - - let mut values = Vec::with_capacity(Alphabet::STEPS * Alphabet::STEPS + 2); - for index_a in 1..=seq_a.len() { - for index_b in 1..=seq_b.len() { - values.clear(); - for len_a in 0..=Alphabet::STEPS { - for len_b in 0..=Alphabet::STEPS { - if len_a == 0 && len_b != 1 - || len_a != 1 && len_b == 0 - || len_a > index_a - || len_b > index_b - { - continue; // Do not allow double gaps (just makes no sense) - } - #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] - // len_a and b are always less then Alphabet::STEPS - let score = if len_a == 0 || len_b == 0 { - Scoring::GapExtendPenalty as i8 // Defined to always be one gap - } else { - alphabet[( - &seq_a[index_a - len_a..index_a], - &seq_b[index_b - len_b..index_b], - )] - }; - if score == 0 { - continue; - } - values.push(Piece::new( - matrix[index_a - len_a][index_b - len_b].score + score as isize, - score, - len_a as u8, - len_b as u8, - )); - } - } - let value = values - .iter() - .max_by(|x, y| x.score.cmp(&y.score)) - .cloned() - .unwrap_or_default(); - if value.score >= high.0 { - high = (value.score, index_a, index_b); - } - matrix[index_a][index_b] = value; - } - } - - // loop back - if ty == Type::Global { - high = ( - matrix[seq_a.len()][seq_b.len()].score, - seq_a.len(), - seq_b.len(), - ); - } else if ty == Type::GlobalForB { - let value = (0..=seq_a.len()) - .map(|v| (v, matrix[v][seq_b.len()].score)) - .max_by(|a, b| a.1.cmp(&b.1)) - .unwrap_or_default(); - high = (value.1, value.0, seq_b.len()); - } - let mut path = Vec::new(); - let high_score = high.0; - //dbg!(&highest_score); - //dbg!(&matrix); - while !(high.1 == 0 && high.2 == 0) { - let value = matrix[high.1][high.2].clone(); - if value.step_a == 0 && value.step_b == 0 { - break; - } - high = ( - 0, - high.1 - value.step_a as usize, - high.2 - value.step_b as usize, - ); - path.push(value); - } - //dbg!(&path); - Alignment { - score: high_score, - path: path.into_iter().rev().collect(), - start_a: high.1, - start_b: high.2, - seq_a: seq_a.to_owned(), - seq_b: seq_b.to_owned(), - } -} - -#[cfg(test)] -mod tests { - use crate::alignment::{align, Type}; - use crate::alphabet::Alphabet; - use crate::aminoacid::AminoAcid::*; - - #[test] - fn equal() { - let alphabet = Alphabet::default(); - let a = vec![A, C, C, G, W]; - let b = vec![A, C, C, G, W]; - let result = align(&a, &b, &alphabet, Type::Local); - dbg!(&result); - assert_eq!(40, result.score); - assert_eq!("MMMMM", &result.short()); - } - - #[test] - fn insertion() { - let alphabet = Alphabet::default(); - let a = vec![A, C, G, W]; - let b = vec![A, C, F, G, W]; - let result = align(&a, &b, &alphabet, Type::Local); - dbg!(&result); - assert_eq!(27, result.score); - assert_eq!("MMIMM", &result.short()); - } - - #[test] - fn deletion() { - let alphabet = Alphabet::default(); - let a = vec![A, C, F, G, W]; - let b = vec![A, C, G, W]; - let result = align(&a, &b, &alphabet, Type::Local); - dbg!(&result); - assert_eq!(27, result.score); - assert_eq!("MMDMM", &result.short()); - } - - #[test] - fn iso_mass() { - let alphabet = Alphabet::default(); - let a = vec![A, F, G, G, W]; - let b = vec![A, F, N, W]; - let result = align(&a, &b, &alphabet, Type::Local); - dbg!(&result); - dbg!(result.short()); - assert_eq!(29, result.score); - assert_eq!("MMS[1,2]M", &result.short()); - } - - #[test] - fn switched() { - let alphabet = Alphabet::default(); - let a = vec![A, F, G, G, W]; - let b = vec![A, G, F, G, W]; - let result = align(&a, &b, &alphabet, Type::Local); - dbg!(&result); - dbg!(result.short()); - assert_eq!(28, result.score); - assert_eq!("MS[2,2]MM", &result.short()); - } - - #[test] - fn local() { - let alphabet = Alphabet::default(); - let a = vec![A, F, G, G, E, W]; - let b = vec![F, G, G, D]; - let result = align(&a, &b, &alphabet, Type::Local); - dbg!(&result); - dbg!(result.short()); - assert_eq!(24, result.score); - assert_eq!("MMM", &result.short()); - } - - #[test] - fn global() { - let alphabet = Alphabet::default(); - let a = vec![A, F, G, G, E, W]; - let b = vec![F, G, G, D]; - let result = align(&a, &b, &alphabet, Type::Global); - dbg!(&result); - println!("{}", result.summary()); - assert_eq!(13, result.score); - assert_eq!("DMMMDM", &result.short()); - assert_eq!(0, result.start_a, "A global alignment should start at 0"); - } - - #[test] - fn global_for_b() { - let alphabet = Alphabet::default(); - let a = vec![A, F, G, G, E, W]; - let b = vec![F, G, G, D]; - let result = align(&a, &b, &alphabet, Type::GlobalForB); - dbg!(&result); - dbg!(result.short()); - assert_eq!(23, result.score); - assert_eq!("MMMM", &result.short()); - assert_eq!(0, result.start_b, "A global alignment should start at 0"); - } -} diff --git a/alignment/alphabet.rs b/alignment/alphabet.rs deleted file mode 100644 index 6b58ea0..0000000 --- a/alignment/alphabet.rs +++ /dev/null @@ -1,283 +0,0 @@ -use crate::aminoacid::AminoAcid; -use crate::aminoacid::AminoAcid::*; -use itertools::Itertools; - -/// An alphabet to determine the score of two amino acid sets -pub struct Alphabet { - array: Vec>, -} - -impl std::ops::Index<(&[AminoAcid], &[AminoAcid])> for Alphabet { - type Output = i8; - fn index(&self, index: (&[AminoAcid], &[AminoAcid])) -> &Self::Output { - &self.array[get_index(index.0)][get_index(index.1)] - } -} - -fn get_index_ref(set: &[&AminoAcid]) -> usize { - set.iter() - .fold(0, |acc, item| acc * AminoAcid::MAX + **item as usize) -} - -fn get_index(set: &[AminoAcid]) -> usize { - set.iter() - .fold(0, |acc, item| acc * AminoAcid::MAX + *item as usize) -} - -impl Alphabet { - /// The number of steps to trace back, if updated a lot of other code has to be updated as well - pub const STEPS: usize = 3; -} - -#[repr(i8)] -#[derive(Clone, Default, Debug)] -pub enum Scoring { - /// The score for identity, should be the highest score of the bunch - Identity = 8, - /// The score for a mismatch - #[default] - Mismatch = -1, - /// The score for an iso mass set, eg Q<>AG - IsoMass = 5, - /// The score for a modification - Modification = 3, - /// The score for a switched set, defined as this value times the size of the set (eg AG scores 4 with GA) - Switched = 2, - /// The score for scoring a gap, should be less than `MISMATCH` - GapStartPenalty = -5, - GapExtendPenalty = -3, -} - -#[allow(clippy::too_many_lines)] -impl Default for Alphabet { - fn default() -> Self { - macro_rules! sets { - ($($($($id:ident),+);+)|+) => { - vec![ - $(vec![ - $(vec![$($id),+],)+ - ],)+ - ] - }; - } - - #[allow(clippy::cast_possible_truncation)] - // STEPS is always within bounds for u32 - let mut alphabet = Self { - array: vec![ - vec![0; (AminoAcid::MAX + 1).pow(Self::STEPS as u32)]; - (AminoAcid::MAX + 1).pow(Self::STEPS as u32) - ], - }; - - for x in 0..=AminoAcid::MAX { - for y in 0..=AminoAcid::MAX { - alphabet.array[x][y] = if x == y { - Scoring::Identity as i8 - } else { - Scoring::Mismatch as i8 - }; - } - } - let iso_mass = sets!( - I; L| - N; G,G| - Q; A,G| - A,V; G,L; G,I| - A,N; Q,G; A,G,G| - L,S; I,S; T,V| - A,M; C,V| - N,V; A,A,A; G,G,V| - N,T; Q,S; A,G,S; G,G,T| - L,N; I,N; Q,V; A,G,V; G,G,L; G,G,I| - D,L; D,I; E,V| - Q,T; A,A,S; A,G,T| - A,Y; F,S| - L,Q; I,Q; A,A,V; A,G,L; A,G,I| - N,Q; A,N,G; Q,G,G| - K,N; G,G,K| - E,N; D,Q; A,D,G; E,G,G| - D,K; A,A,T; G,S,V| - M,N; A,A,C; G,G,M| - A,S; G,T| - A,A,L; A,A,I; G,V,V| - Q,Q; A,A,N; A,Q,G| - E,Q; A,A,D; A,E,G| - E,K; A,S,V; G,L,S; G,I,S; G,T,V| - M,Q; A,G,M; C,G,V| - A,A,Q; N,G,V - ); - - for set in iso_mass { - for set in set.iter().permutations(2) { - let a = set[0]; - let b = set[1]; - for seq_a in a.iter().permutations(a.len()) { - for seq_b in b.iter().permutations(b.len()) { - alphabet.array[get_index_ref(&seq_a)][get_index_ref(&seq_b)] = - Scoring::IsoMass as i8; - } - } - } - } - - let modifications = sets!( - //N;D| // Amidation only at N term - Q;E| // Deamidation - D;N| // Deamidation - C;T| // Disulfide bond - T;D| // Methylation - S;T| // Methylation - D;E| // Methylation - R;A,V;G,L| // Methylation - Q;A,A // Methylation - ); - - for set in modifications { - let a = &set[0]; - for seq_b in set.iter().skip(1) { - alphabet.array[get_index(a)][get_index(seq_b.as_slice())] = - Scoring::Modification as i8; - } - } - - let amino_acids = (1..=AminoAcid::MAX) - .map(|a| AminoAcid::try_from(a).unwrap()) - .collect_vec(); - for size in 2..=Self::STEPS { - for set in amino_acids - .iter() - .combinations_with_replacement(size) - .flat_map(|v| v.into_iter().permutations(size)) - { - if set.iter().all(|v| *v == set[0]) { - continue; // Do not add [A, A] or [A, A, A] etc as SWITCHED - } - #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] - // set.len() is at max equal to Self::STEPS - for switched in set.clone().into_iter().permutations(size) { - alphabet.array[get_index_ref(&set)][get_index_ref(&switched)] = - Scoring::Switched as i8 * set.len() as i8; - } - } - } - - alphabet - } -} - -#[cfg(test)] -mod tests { - use super::{Alphabet, Scoring}; - use crate::aminoacid::AminoAcid::*; - - #[test] - fn identity() { - let alphabet = Alphabet::default(); - assert_eq!( - Scoring::Identity as i8, - alphabet[([A].as_slice(), [A].as_slice())] - ); - assert_eq!(0, alphabet[([A, A].as_slice(), [A, A].as_slice())]); - assert_eq!(0, alphabet[([A, A, A].as_slice(), [A, A, A].as_slice())]); - } - - #[test] - fn similarity() { - let alphabet = Alphabet::default(); - assert_eq!( - Scoring::IsoMass as i8, - alphabet[([I].as_slice(), [L].as_slice())] - ); - assert_eq!( - Scoring::IsoMass as i8, - alphabet[([N].as_slice(), [G, G].as_slice())] - ); - assert_eq!( - Scoring::IsoMass as i8, - alphabet[([G, G].as_slice(), [N].as_slice())] - ); - assert_eq!( - Scoring::IsoMass as i8, - alphabet[([A, S].as_slice(), [G, T].as_slice())] - ); - assert_eq!( - Scoring::IsoMass as i8, - alphabet[([S, A].as_slice(), [G, T].as_slice())] - ); - assert_eq!( - Scoring::IsoMass as i8, - alphabet[([S, A].as_slice(), [T, G].as_slice())] - ); - assert_eq!( - Scoring::IsoMass as i8, - alphabet[([A, S].as_slice(), [T, G].as_slice())] - ); - assert_eq!( - Scoring::IsoMass as i8, - alphabet[([L, Q].as_slice(), [A, V, A].as_slice())] - ); - } - - #[test] - fn inequality() { - let alphabet = Alphabet::default(); - assert_eq!( - Scoring::Mismatch as i8, - alphabet[([I].as_slice(), [Q].as_slice())] - ); - assert_eq!(0, alphabet[([Q].as_slice(), [G, G].as_slice())]); - assert_eq!(0, alphabet[([A, E].as_slice(), [G, T].as_slice())]); - assert_eq!(0, alphabet[([E, Q].as_slice(), [A, V, A].as_slice())]); - } - - #[test] - fn switched() { - let alphabet = Alphabet::default(); - assert_eq!( - Scoring::Switched as i8 * 2, - alphabet[([E, Q].as_slice(), [Q, E].as_slice())] - ); - assert_eq!( - Scoring::Switched as i8 * 3, - alphabet[([D, A, C].as_slice(), [A, C, D].as_slice())] - ); - assert_eq!( - Scoring::Switched as i8 * 3, - alphabet[([C, D, A].as_slice(), [A, C, D].as_slice())] - ); - assert_eq!( - Scoring::Switched as i8 * 3, - alphabet[([A, C, D].as_slice(), [D, A, C].as_slice())] - ); - assert_eq!( - Scoring::Switched as i8 * 3, - alphabet[([C, D, A].as_slice(), [D, A, C].as_slice())] - ); - assert_eq!( - Scoring::Switched as i8 * 3, - alphabet[([A, C, D].as_slice(), [C, D, A].as_slice())] - ); - assert_eq!( - Scoring::Switched as i8 * 3, - alphabet[([D, A, C].as_slice(), [C, D, A].as_slice())] - ); - assert_eq!( - Scoring::Switched as i8 * 3, - alphabet[([V, A, A].as_slice(), [A, V, A].as_slice())] - ); - } - - #[test] - fn modification() { - let alphabet = Alphabet::default(); - assert_eq!( - Scoring::Modification as i8, - alphabet[([D].as_slice(), [N].as_slice())] - ); - assert_eq!( - Scoring::Mismatch as i8, - alphabet[([N].as_slice(), [D].as_slice())] - ); - } -} diff --git a/alignment/aminoacid.rs b/alignment/aminoacid.rs deleted file mode 100644 index 71c5ffd..0000000 --- a/alignment/aminoacid.rs +++ /dev/null @@ -1,170 +0,0 @@ -use itertools::Itertools; -use std::fmt::Display; - -/// All aminoacids -#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Eq, Ord)] -pub enum AminoAcid { - /// Alanine - A = 1, - /// Arginine - R, - /// Asparagine - N, - /// Aspartic acid - D, - /// Cysteine - C, - /// Glutamine - Q, - /// Glutamic acid - E, - /// Glycine - G, - /// Histidine - H, - /// Isoleucine - I, - /// Leucine - L, - /// Lysine - K, - /// Methionine - M, - /// Phenylalanine - F, - /// Proline - P, - /// Serine - S, - /// Threonine - T, - /// Tryptophan - W, - /// Tyrosine - Y, - /// Valine - V, - /// Weird - B, - /// Also weird - Z, - /// Single gap - X, - /// Longer gap - Gap, -} - -impl AminoAcid { - /// The total number of normal amino acids (disregards Gap) - pub const MAX: usize = 23; -} - -impl Display for AminoAcid { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(match self { - Self::A => "A", - Self::R => "R", - Self::N => "N", - Self::D => "D", - Self::C => "C", - Self::Q => "Q", - Self::E => "E", - Self::G => "G", - Self::H => "H", - Self::I => "I", - Self::L => "L", - Self::K => "K", - Self::M => "M", - Self::F => "F", - Self::P => "P", - Self::S => "S", - Self::T => "T", - Self::W => "W", - Self::Y => "Y", - Self::V => "V", - Self::B => "B", - Self::Z => "Z", - Self::X => "X", - Self::Gap => "*", - }) - } -} - -impl TryFrom for AminoAcid { - type Error = (); - fn try_from(num: usize) -> Result { - match num { - 1 => Ok(Self::A), - 2 => Ok(Self::R), - 3 => Ok(Self::N), - 4 => Ok(Self::D), - 5 => Ok(Self::C), - 6 => Ok(Self::Q), - 7 => Ok(Self::E), - 8 => Ok(Self::G), - 9 => Ok(Self::H), - 10 => Ok(Self::I), - 11 => Ok(Self::L), - 12 => Ok(Self::K), - 13 => Ok(Self::M), - 14 => Ok(Self::F), - 15 => Ok(Self::P), - 16 => Ok(Self::S), - 17 => Ok(Self::T), - 18 => Ok(Self::W), - 19 => Ok(Self::Y), - 20 => Ok(Self::V), - 21 => Ok(Self::B), - 22 => Ok(Self::Z), - 23 => Ok(Self::X), - 24 => Ok(Self::Gap), - _ => Err(()), - } - } -} - -impl TryFrom for AminoAcid { - type Error = (); - fn try_from(value: char) -> Result { - match value { - 'A' => Ok(Self::A), - 'R' => Ok(Self::R), - 'N' => Ok(Self::N), - 'D' => Ok(Self::D), - 'C' => Ok(Self::C), - 'Q' => Ok(Self::Q), - 'E' => Ok(Self::E), - 'G' => Ok(Self::G), - 'H' => Ok(Self::H), - 'I' => Ok(Self::I), - 'L' => Ok(Self::L), - 'K' => Ok(Self::K), - 'M' => Ok(Self::M), - 'F' => Ok(Self::F), - 'P' => Ok(Self::P), - 'S' => Ok(Self::S), - 'T' => Ok(Self::T), - 'W' => Ok(Self::W), - 'Y' => Ok(Self::Y), - 'V' => Ok(Self::V), - 'B' => Ok(Self::B), - 'Z' => Ok(Self::Z), - 'X' => Ok(Self::X), - '*' => Ok(Self::Gap), - _ => Err(()), - } - } -} - -/// Create an aminoacid sequence from a string, just ignores any non aminoacids characters -pub fn sequence_from_string(value: &str) -> Vec { - value - .chars() - .filter_map(|v| AminoAcid::try_from(v).ok()) - .collect() -} - -/// Generate a string from a sequence of aminoacids -pub fn sequence_to_string(value: &[AminoAcid]) -> String { - value.iter().map(std::string::ToString::to_string).join("") -} diff --git a/alignment/bin.rs b/alignment/bin.rs deleted file mode 100644 index 078b800..0000000 --- a/alignment/bin.rs +++ /dev/null @@ -1,101 +0,0 @@ -#![allow(dead_code)] -#![warn(clippy::pedantic, clippy::nursery, clippy::all)] -#![allow(clippy::enum_glob_use, clippy::wildcard_imports)] -use mass_alignment::template::Template; -use mass_alignment::*; - -fn main() { - let alphabet = Alphabet::default(); - //let template = aminoacid::sequence_from_string("XXXXXXXXXXXXXXXXXXXXYFDYWGQGTLVTVSS"); - let template = mass_alignment::sequence_from_string("EVQLVESGGGLVQPGGSLRLSCAASGFTVSSNYMSWVRQAPGKGLEWVSVIYSGGSTYYADSVKGRFTISRDNSKNTLYLQMNSLRAEDTAVYYCARXXXXXXXXXXXXXXXXXXXX"); - let reads: Vec> = [ - //"SRWGGDGFYAMDYWGQGTLVTV", - //"DWNGFYAMDYWGQGTLVTVSS", - //"RWGGDGFYAMDYWGQGTLVTV", - //"HVPHGDGFYAMDYWGQGTLVT", - //"WRGGDGFYAMDYWGQGTLVT", - //"SRWGGDGFYAMDYWGQGTLV", - //"RWGGDGFYAMDYWGQGTLVT", - //"WRNDGFYAMDYWGQGTLVT", - //"RWGGDGFYAMDYWGQGTLV", - //"MARNDGFYAMDYWGQGTLV", - //"RWNDGFYAMDYWGQGTLV", - //"SRWGGNGFYWDYWGQGT", - //"RWNDGFYWDYWGQGT", - //"DYWGQGTLVVTSS", - //"DYWGQGTLVTVSS", - //"DYWGQGTLVTV", - //"DYWGQGTLVT", - //"WGQGTLVT", - "DLQLVESGGGLVGAKSPPGTLSAAASGFNL", - "DLQLVESGGGLVGAKSPPGTLSAAASGFNL", - "EVQLVESGGGLVQPGGSLSGAKYHSGFNL", - "EVVQLVESGGGLVQPGGSLGVLSCAASGF", - "DLQLVESGGGLVQPGGSLGVLSCAASGF", - "DLQLVESGGGLVQPGTPLYWNAASGFNL", - "DLQLVESGGGLVQPGGSLRLSCAASGF", - "QVQLVESGGGLVQPGGSLRLSCAASGF", - "EVQLVESGGGLPVQGGSLRLSCAADGF", - "EVQLVESGGGLVQPGGSLRLSCAASGF", - "EVQLVSGEGGLVQPGGSLRLSCAASGF", - "QVELVESGGGLVQPGGSLRLSCAASGF", - "TLSADTSKNTAYLQMNSLRAEDTAVY", - "RFTLSADTSKNTAYLQMNSLRAEDTA", - "QLVESGGGLVQPGGSLTHVAGAGHSGF", - "SADTSKNTAYLQMNSLRAEDTAVYY", - "LMLTDGYTRYADSVKGRFTLSADTS", - "QLVESGGGLVQPGGSLRLSCAASGF", - "QLVESGGGLVQPGGSLRLSCQTGF", - "LVESGGGLVQPNSLRLSCAASGF", - ] - .into_iter() - .map(mass_alignment::sequence_from_string) - .collect(); - - let template = Template::new( - template, - reads.iter().map(std::vec::Vec::as_slice).collect(), - &alphabet, - ); - let content = format!( - " - - - - - - - -
-
{} -
-
- -", - template.generate_html() - ); - std::fs::write("test.html", content).unwrap(); -} diff --git a/alignment/lib.rs b/alignment/lib.rs deleted file mode 100644 index 720fa4f..0000000 --- a/alignment/lib.rs +++ /dev/null @@ -1,42 +0,0 @@ -//! An algorithm based on Needleman Wunsch/Smith Waterman but extended to allow for mass based alignment. -//! The mass based part gives the option to match two sets of aminoacids with different sizes. -//! For example the set {Q} matches {AG} because these have the same mass and so are commonly misclassified -//! is de novo sequencing for peptides. Besides iso mass definitions it also handles swaps with finesse, -//! meaning that {AG} matches {GA} with a well defined score to allow for these mistakes to be fixed. The -//! last important addition is the handling of post translational modifications meaning that {Q} matches {E} -//! but not the other way around to allow for deamidation of the sample in reference to the template. -//! -//! ```rust -//! use mass_alignment::*; -//! use mass_alignment::AminoAcid::*; -//! -//! let alphabet = Alphabet::default(); -//! let template = &[A,G,Q,S,T,Q]; -//! let query = &[Q,E,S,W]; -//! let result = align(template, query, &alphabet, Type::GlobalForB); -//! println!("{}", result.summary()); -//! assert_eq!(15, result.score) -//! ``` - -#![allow(dead_code)] -#![warn(clippy::pedantic, clippy::nursery, clippy::all, missing_docs)] -#![allow( - clippy::enum_glob_use, - clippy::wildcard_imports, - clippy::must_use_candidate -)] -/// The module containing all alignment handling -mod alignment; -/// The module containing all alphabet handling -mod alphabet; -/// The module containing the definition for aminoacids -mod aminoacid; -/// The module containing the definition for templates -pub mod template; - -pub use crate::alignment::align; -pub use crate::alignment::*; -pub use crate::alphabet::Alphabet; -pub use crate::aminoacid::sequence_from_string; -pub use crate::aminoacid::sequence_to_string; -pub use crate::aminoacid::AminoAcid; diff --git a/alignment/template.rs b/alignment/template.rs deleted file mode 100644 index 80e188b..0000000 --- a/alignment/template.rs +++ /dev/null @@ -1,123 +0,0 @@ -use crate::alignment::Alignment; -use crate::alphabet::Scoring; -use crate::aminoacid::{self, AminoAcid}; -use crate::{align, Alphabet}; -use itertools::Itertools; -use std::fmt::Write; - -/// A template that is matched with many reads -pub struct Template { - /// The sequence of this template - pub sequence: Vec, - /// The reads matched to this template - pub reads: Vec, -} - -impl Template { - /// Create a new template by matching the given reads to the given template sequence - pub fn new(sequence: Vec, reads: Vec<&[AminoAcid]>, alphabet: &Alphabet) -> Self { - Self { - reads: reads - .into_iter() - .map(|v| align(&sequence, v, alphabet, crate::alignment::Type::GlobalForB)) - .collect(), - sequence, - } - } - - /// Generate HTML for a reads alignment, all styling is missing and it is only a small part of a document - pub fn generate_html(&self) -> String { - let mut insertions = vec![0; self.sequence.len()]; - for read in &self.reads { - let mut loc_a = read.start_a; - let mut insertion = 0; - - for piece in &read.path { - if piece.step_a == 0 && piece.step_b == 1 { - insertion += 1; - } else if insertion != 0 { - insertions[loc_a] = std::cmp::max(insertions[loc_a], insertion); - insertion = 0; - } else { - insertion = 0; - } - loc_a += piece.step_a as usize; - } - } - - let mut output = format!("
1....
", insertions.iter().sum::() + self.sequence.len()); - for (ins, seq) in insertions.iter().zip(&self.sequence) { - let _ = write!(output, "{:-<1$}{2}", "", ins, seq); - } - let _ = write!(output, "
"); - - for read in &self.reads { - let _ = write!( - output, - "
", - insertions[0..read.start_a].iter().sum::() + read.start_a + 1, - insertions[0..read.start_a + read.len_a()] - .iter() - .sum::() - + read.start_a - + read.len_a() - + 3 - ); - let mut loc_a = read.start_a; - let mut loc_b = read.start_b; - let mut insertion = 0; - for piece in &read.path { - if piece.step_a == 0 && piece.step_b == 1 { - insertion += 1; - } else { - let _ = write!(output, "{:-<1$}", "", insertions[loc_a] - insertion); - insertion = 0; - } - let _ = write!( - output, - "{}", - match (piece.step_a, piece.step_b) { - (0 | 1, 1) => read.seq_b[loc_b].to_string(), - (1, 0) => "-".to_string(), - #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] - // a is defined to be in range 0..=Alphabet::STEPS - (a, b) => { - let inner = if a == b { - // As a equals b it is a swap or iso length iso mass sets, add in the missing insertions (if any) - // Because a equals b the length of the sequence patch and insertion patch is always equal. - // This means that the resulting insertions makes the text nicely aligned. - read.seq_b[loc_b..loc_b + b as usize] - .iter() - .zip(&insertions[loc_a..loc_a + a as usize]) - .map(|(sb, sa)| format!("{:->1$}", sb.to_string(), sa + 1)) - .join("") - } else { - aminoacid::sequence_to_string( - &read.seq_b[loc_b..loc_b + b as usize], - ) - }; - format!( - "{}", - if a == b && piece.local_score == Scoring::Switched as i8 * a as i8 - { - " swap" - } else { - "" - }, - inner.len(), - insertions[loc_a..loc_a + a as usize].iter().sum::() - + a as usize, - inner - ) - } - } - ); - loc_a += piece.step_a as usize; - loc_b += piece.step_b as usize; - } - let _ = write!(output, "
"); - } - let _ = write!(output, "
"); - output - } -} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 2862c3b..bce10bd 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2379,9 +2379,9 @@ checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" [[package]] name = "rustyms" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb1fee3148a098d6ef3973dcafbca097e952d0a0c5c2302ed01c053f8857086" +checksum = "5149c3f32626cca00d98cf4392506f310923730b9b9b6badb34d4ee9fb7f2b6d" dependencies = [ "itertools", "regex", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 650c3dd..db13040 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -8,14 +8,6 @@ repository = "https://github.com/snijderlab/annotator" edition = "2021" rust-version = "1.57" -[lib] -name = "mass_alignment" -path = "alignment/src/lib.rs" - -[[bin]] -name = "mass_alignment_bin" -path = "alignment/src/bin.rs" - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [build-dependencies] @@ -27,7 +19,7 @@ serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } tauri = { version = "1.2", features = ["dialog-open"] } pdbtbx = "0.10" -rustyms = "0.4.0" +rustyms = "0.4.1" proc_interface = { path = "../src-proc-interface" } [features] diff --git a/src-tauri/alignment/output.html b/src-tauri/alignment/output.html deleted file mode 100644 index 0db2d7d..0000000 --- a/src-tauri/alignment/output.html +++ /dev/null @@ -1,109 +0,0 @@ - - - - - - - - - -
-
-
-
1....
-
- EVQLVESGGGLVQ--PGGSL-RLSCAASGFTVSSNYMSWVRQAPGKGLEWVSVI-YSGGSTYYADSVKGRFTISRDNSKNTLYLQMNSLRAEDTAVYYCARXXXXXXXXXXXXXXXXXXXX -
-
DLQLVESGGGLVGAKSPPGT---LSAAASGFNL
-
DLQLVESGGGLVGAKSPPGT---LSAAASGFNL
-
EVQLVESGGGLVQ--PGGSL-SGAKYHSGFNL
-
EVQLVESGGGLVQ--PGGSLGVLSCAASGF
-
DLQLVESGGGLVQ--PGGSLGVLSCAASGF
-
DLQLVESGGGLVQ--PGTPL--YWNAASGFNL
-
DLQLVESGGGLVQ--PGGSL-RLSCAASGF
-
QVQLVESGGGLVQ--PGGSL-RLSCAASGF
-
EVQLVESGGGLVQ--PGGSL-RLSCAADGF
-
EVQLVESGGGLVQ--PGGSL-RLSCAASGF
-
EVQLVSGEGGLVQ--PGGSL-RLSCAASGF
-
QVELVESGGGLVQ--PGGSL-RLSCAASGF
-
TLSADTSKNTAYLQMNSLRAEDTAVY
-
RFTLSADTSKNTAYLQMNSLRAEDTA
-
QLVESGGGLVQ--PGGSLTHVAGGHSGF
-
SADTSKNTAYLQMNSLRAEDTAVYY
-
LMLTDGYTRYADSVKGRFTLSADTS
-
QLVESGGGLVQ--PGGSL-RLSCAASGF
-
QLVESGGGLVQ--PGGSL-RLSCQTGF
-
LVESGGGLVQ--PNSL-RLSCAASGF
-
-
-
- - - - \ No newline at end of file diff --git a/src-tauri/alignment/preview.jpg b/src-tauri/alignment/preview.jpg deleted file mode 100644 index c79259e8bfaa426972da06d78201c0fe5da9887e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 82998 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 diff --git a/src-tauri/alignment/readme.md b/src-tauri/alignment/readme.md deleted file mode 100644 index 9785f1a..0000000 --- a/src-tauri/alignment/readme.md +++ /dev/null @@ -1,28 +0,0 @@ -# Mass Alignment -This is an algorithm based on Smith Waterman/Needleman Wunsch for sequence alignment at the aminoacid level, but extended for the use of mass spectrometry. It has some notable features for de novo sequenced -peptides: -* Detects sets of aminoacids of the same mass, even if they differ in length. {Q} matches {AG} -* Detects swaps of aminoacids (up to length 3) {AVG} matches {GAV} -* Handles modifications. {Q} matches {E} but not the other way around (deamidation) - -It scales similarly to SW/NW, with N*M. This implementation is quite fast ~118ns * N * M. Below is a preview of an alignment based on this algorithm. The alignment generating code can also be found in this project. - -![preview of a peptide alignment](preview.jpg) - -_grey: found isomass, underline: found swap_ - -## Usage - -There is a library `mass_alignment` under `src\lib.rs`. The alignment code can be found under `src\alignment.rs`. The alphabet (lookup matrix) generation under `src\alphabet.rs`. And the HTML generation under `src\template.html`+`src\bin.rs`. - -There is a binary to generate the above preview in `src\bin.rs`. You can use this with `cargo run` this generates `test.html` in the root folder. - -## Building - -[Use cargo](https://www.rust-lang.org/tools/install) - -Commands: -* `cargo run` runs `bin.rs` -* `cargo doc --open` builds the documentation -* `cargo test` runs the unit tests -* `cargo bench` runs the benchmarks (will be saved in `target/benchmark_result.csv`) \ No newline at end of file diff --git a/src-tauri/alignment/src/alignment.rs b/src-tauri/alignment/src/alignment.rs deleted file mode 100644 index 78f315f..0000000 --- a/src-tauri/alignment/src/alignment.rs +++ /dev/null @@ -1,399 +0,0 @@ -use crate::alphabet::{Alphabet, Scoring}; -use crate::aminoacid::*; -use itertools::Itertools; -use std::fmt::Write; - -/// An alignment of two reads. -#[derive(Debug, Clone)] -pub struct Alignment { - /// The score of this alignment - pub score: isize, - /// The path or steps taken for the alignment - pub path: Vec, - /// The position in the first sequence where the alignment starts - pub start_a: usize, - /// The position in the second sequence where the alignment starts - pub start_b: usize, - /// The first sequence - pub seq_a: Vec, - /// The second sequence - pub seq_b: Vec, -} - -impl Alignment { - fn short(&self) -> String { - self.path.iter().map(Piece::short).join("") - } - - fn aligned(&self) -> String { - let blocks: Vec = " ▁▂▃▄▅▆▇█".chars().collect(); - let blocks_neg: Vec = " ▔▔▔▀▀▀▀█".chars().collect(); - let mut str_a = String::new(); - let mut str_b = String::new(); - let mut str_blocks = String::new(); - let mut str_blocks_neg = String::new(); - let mut loc_a = self.start_a; - let mut loc_b = self.start_b; - - for piece in &self.path { - let l = std::cmp::max(piece.step_b, piece.step_a); - if piece.step_a == 0 { - let _ = write!(str_a, "{:- 0 { - " ".to_string() - } else { - #[allow(clippy::cast_sign_loss)] // Checked above - blocks_neg[-piece.local_score as usize].to_string() - }, - l as usize - ) - ); - - loc_a += piece.step_a as usize; - loc_b += piece.step_b as usize; - } - - format!("{}\n{}\n{}\n{}", str_a, str_b, str_blocks, str_blocks_neg) - } - - /// Generate a summary of this alignment for printing to the command line - pub fn summary(&self) -> String { - format!( - "score: {}\npath: {}\nstart: ({}, {})\naligned:\n{}", - self.score, - self.short(), - self.start_a, - self.start_b, - self.aligned() - ) - } - - /// The total number of residues matched on the first sequence - pub fn len_a(&self) -> usize { - self.path.iter().map(|p| p.step_a as usize).sum() - } - - /// The total number of residues matched on the second sequence - pub fn len_b(&self) -> usize { - self.path.iter().map(|p| p.step_b as usize).sum() - } -} - -/// A piece in an alignment, determining what step was taken in the alignment and how this impacted the score -#[derive(Clone, Default, Debug)] -pub struct Piece { - /// The total score of the path up till now - pub score: isize, - /// The local contribution to the score of this piece - pub local_score: i8, - /// The number of steps on the first sequence - pub step_a: u8, - /// The number of steps on the second sequence - pub step_b: u8, -} - -impl Piece { - /// Create a new alignment piece - pub const fn new(score: isize, local_score: i8, step_a: u8, step_b: u8) -> Self { - Self { - score, - local_score, - step_a, - step_b, - } - } -} - -impl Piece { - /// Display this piece very compactly - pub fn short(&self) -> String { - match (self.step_a, self.step_b) { - (0, 1) => "I".to_string(), - (1, 0) => "D".to_string(), - (1, 1) => "M".to_string(), - (a, b) => format!("S[{},{}]", b, a), - } - } -} - -/// The type of alignment to perform -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum Type { - /// Global alignment, which tries to find the best alignment to link both sequences fully to each other, like the Needleman Wunsch algorithm - Global, - /// Local alignment, which tries to find the best patch of both sequences to align to each other, this could lead to trailing ends on both sides of both sequences, like the Smith Waterman - Local, - /// Hybrid alignment, the second sequence will be fully aligned to the first sequence, this could lead to trailing ends on the first sequence but not on the second. - GlobalForB, -} - -impl Type { - const fn global(self) -> bool { - !matches!(self, Self::Local) - } -} - -/// # Panics -/// It panics when the length of `seq_a` or `seq_b` is bigger then [`isize::MAX`]. -#[allow(clippy::too_many_lines)] -pub fn align(seq_a: &[AminoAcid], seq_b: &[AminoAcid], alphabet: &Alphabet, ty: Type) -> Alignment { - assert!(isize::try_from(seq_a.len()).is_ok()); - assert!(isize::try_from(seq_b.len()).is_ok()); - let mut matrix = vec![vec![Piece::default(); seq_b.len() + 1]; seq_a.len() + 1]; - let mut high = (0, 0, 0); - - if ty.global() { - #[allow(clippy::cast_possible_wrap)] - // b is always less than seq_b - for index_b in 0..=seq_b.len() { - matrix[0][index_b] = Piece::new( - (index_b as isize) * Scoring::GapExtendPenalty as isize, - Scoring::GapExtendPenalty as i8, - 0, - u8::from(index_b != 0), - ); - } - } - if ty == Type::Global { - #[allow(clippy::cast_possible_wrap)] - // a is always less than seq_a - for (index_a, row) in matrix.iter_mut().enumerate() { - row[0] = Piece::new( - (index_a as isize) * Scoring::GapExtendPenalty as isize, - Scoring::GapExtendPenalty as i8, - u8::from(index_a != 0), - 0, - ); - } - } - - let mut values = Vec::with_capacity(Alphabet::STEPS * Alphabet::STEPS + 2); - for index_a in 1..=seq_a.len() { - for index_b in 1..=seq_b.len() { - values.clear(); - for len_a in 0..=Alphabet::STEPS { - for len_b in 0..=Alphabet::STEPS { - if len_a == 0 && len_b != 1 - || len_a != 1 && len_b == 0 - || len_a > index_a - || len_b > index_b - { - continue; // Do not allow double gaps (just makes no sense) - } - #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] - // len_a and b are always less then Alphabet::STEPS - let score = if len_a == 0 || len_b == 0 { - Scoring::GapExtendPenalty as i8 // Defined to always be one gap - } else { - alphabet[( - &seq_a[index_a - len_a..index_a], - &seq_b[index_b - len_b..index_b], - )] - }; - if score == 0 { - continue; - } - #[allow(clippy::cast_possible_truncation)] - values.push(Piece::new( - matrix[index_a - len_a][index_b - len_b].score + score as isize, - score, - len_a as u8, - len_b as u8, - )); - } - } - let value = values - .iter() - .max_by(|x, y| x.score.cmp(&y.score)) - .cloned() - .unwrap_or_default(); - if value.score >= high.0 { - high = (value.score, index_a, index_b); - } - matrix[index_a][index_b] = value; - } - } - - // loop back - if ty == Type::Global { - high = ( - matrix[seq_a.len()][seq_b.len()].score, - seq_a.len(), - seq_b.len(), - ); - } else if ty == Type::GlobalForB { - let value = (0..=seq_a.len()) - .map(|v| (v, matrix[v][seq_b.len()].score)) - .max_by(|a, b| a.1.cmp(&b.1)) - .unwrap_or_default(); - high = (value.1, value.0, seq_b.len()); - } - let mut path = Vec::new(); - let high_score = high.0; - //dbg!(&highest_score); - //dbg!(&matrix); - while !(high.1 == 0 && high.2 == 0) { - let value = matrix[high.1][high.2].clone(); - if value.step_a == 0 && value.step_b == 0 { - break; - } - high = ( - 0, - high.1 - value.step_a as usize, - high.2 - value.step_b as usize, - ); - path.push(value); - } - //dbg!(&path); - Alignment { - score: high_score, - path: path.into_iter().rev().collect(), - start_a: high.1, - start_b: high.2, - seq_a: seq_a.to_owned(), - seq_b: seq_b.to_owned(), - } -} - -#[cfg(test)] -mod tests { - use crate::alignment::{align, Type}; - use crate::alphabet::Alphabet; - use crate::aminoacid::AminoAcid::*; - - #[test] - fn equal() { - let alphabet = Alphabet::default(); - let a = vec![A, C, C, G, W]; - let b = vec![A, C, C, G, W]; - let result = align(&a, &b, &alphabet, Type::Local); - dbg!(&result); - assert_eq!(40, result.score); - assert_eq!("MMMMM", &result.short()); - } - - #[test] - fn insertion() { - let alphabet = Alphabet::default(); - let a = vec![A, C, G, W]; - let b = vec![A, C, F, G, W]; - let result = align(&a, &b, &alphabet, Type::Local); - dbg!(&result); - assert_eq!(27, result.score); - assert_eq!("MMIMM", &result.short()); - } - - #[test] - fn deletion() { - let alphabet = Alphabet::default(); - let a = vec![A, C, F, G, W]; - let b = vec![A, C, G, W]; - let result = align(&a, &b, &alphabet, Type::Local); - dbg!(&result); - assert_eq!(27, result.score); - assert_eq!("MMDMM", &result.short()); - } - - #[test] - fn iso_mass() { - let alphabet = Alphabet::default(); - let a = vec![A, F, G, G, W]; - let b = vec![A, F, N, W]; - let result = align(&a, &b, &alphabet, Type::Local); - dbg!(&result); - dbg!(result.short()); - assert_eq!(29, result.score); - assert_eq!("MMS[1,2]M", &result.short()); - } - - #[test] - fn switched() { - let alphabet = Alphabet::default(); - let a = vec![A, F, G, G, W]; - let b = vec![A, G, F, G, W]; - let result = align(&a, &b, &alphabet, Type::Local); - dbg!(&result); - dbg!(result.short()); - assert_eq!(28, result.score); - assert_eq!("MS[2,2]MM", &result.short()); - } - - #[test] - fn local() { - let alphabet = Alphabet::default(); - let a = vec![A, F, G, G, E, W]; - let b = vec![F, G, G, D]; - let result = align(&a, &b, &alphabet, Type::Local); - dbg!(&result); - dbg!(result.short()); - assert_eq!(24, result.score); - assert_eq!("MMM", &result.short()); - } - - #[test] - fn global() { - let alphabet = Alphabet::default(); - let a = vec![A, F, G, G, E, W]; - let b = vec![F, G, G, D]; - let result = align(&a, &b, &alphabet, Type::Global); - dbg!(&result); - println!("{}", result.summary()); - assert_eq!(13, result.score); - assert_eq!("DMMMDM", &result.short()); - assert_eq!(0, result.start_a, "A global alignment should start at 0"); - } - - #[test] - fn global_for_b() { - let alphabet = Alphabet::default(); - let a = vec![A, F, G, G, E, W]; - let b = vec![F, G, G, D]; - let result = align(&a, &b, &alphabet, Type::GlobalForB); - dbg!(&result); - dbg!(result.short()); - assert_eq!(23, result.score); - assert_eq!("MMMM", &result.short()); - assert_eq!(0, result.start_b, "A global alignment should start at 0"); - } -} diff --git a/src-tauri/alignment/src/alphabet.rs b/src-tauri/alignment/src/alphabet.rs deleted file mode 100644 index 6b58ea0..0000000 --- a/src-tauri/alignment/src/alphabet.rs +++ /dev/null @@ -1,283 +0,0 @@ -use crate::aminoacid::AminoAcid; -use crate::aminoacid::AminoAcid::*; -use itertools::Itertools; - -/// An alphabet to determine the score of two amino acid sets -pub struct Alphabet { - array: Vec>, -} - -impl std::ops::Index<(&[AminoAcid], &[AminoAcid])> for Alphabet { - type Output = i8; - fn index(&self, index: (&[AminoAcid], &[AminoAcid])) -> &Self::Output { - &self.array[get_index(index.0)][get_index(index.1)] - } -} - -fn get_index_ref(set: &[&AminoAcid]) -> usize { - set.iter() - .fold(0, |acc, item| acc * AminoAcid::MAX + **item as usize) -} - -fn get_index(set: &[AminoAcid]) -> usize { - set.iter() - .fold(0, |acc, item| acc * AminoAcid::MAX + *item as usize) -} - -impl Alphabet { - /// The number of steps to trace back, if updated a lot of other code has to be updated as well - pub const STEPS: usize = 3; -} - -#[repr(i8)] -#[derive(Clone, Default, Debug)] -pub enum Scoring { - /// The score for identity, should be the highest score of the bunch - Identity = 8, - /// The score for a mismatch - #[default] - Mismatch = -1, - /// The score for an iso mass set, eg Q<>AG - IsoMass = 5, - /// The score for a modification - Modification = 3, - /// The score for a switched set, defined as this value times the size of the set (eg AG scores 4 with GA) - Switched = 2, - /// The score for scoring a gap, should be less than `MISMATCH` - GapStartPenalty = -5, - GapExtendPenalty = -3, -} - -#[allow(clippy::too_many_lines)] -impl Default for Alphabet { - fn default() -> Self { - macro_rules! sets { - ($($($($id:ident),+);+)|+) => { - vec![ - $(vec![ - $(vec![$($id),+],)+ - ],)+ - ] - }; - } - - #[allow(clippy::cast_possible_truncation)] - // STEPS is always within bounds for u32 - let mut alphabet = Self { - array: vec![ - vec![0; (AminoAcid::MAX + 1).pow(Self::STEPS as u32)]; - (AminoAcid::MAX + 1).pow(Self::STEPS as u32) - ], - }; - - for x in 0..=AminoAcid::MAX { - for y in 0..=AminoAcid::MAX { - alphabet.array[x][y] = if x == y { - Scoring::Identity as i8 - } else { - Scoring::Mismatch as i8 - }; - } - } - let iso_mass = sets!( - I; L| - N; G,G| - Q; A,G| - A,V; G,L; G,I| - A,N; Q,G; A,G,G| - L,S; I,S; T,V| - A,M; C,V| - N,V; A,A,A; G,G,V| - N,T; Q,S; A,G,S; G,G,T| - L,N; I,N; Q,V; A,G,V; G,G,L; G,G,I| - D,L; D,I; E,V| - Q,T; A,A,S; A,G,T| - A,Y; F,S| - L,Q; I,Q; A,A,V; A,G,L; A,G,I| - N,Q; A,N,G; Q,G,G| - K,N; G,G,K| - E,N; D,Q; A,D,G; E,G,G| - D,K; A,A,T; G,S,V| - M,N; A,A,C; G,G,M| - A,S; G,T| - A,A,L; A,A,I; G,V,V| - Q,Q; A,A,N; A,Q,G| - E,Q; A,A,D; A,E,G| - E,K; A,S,V; G,L,S; G,I,S; G,T,V| - M,Q; A,G,M; C,G,V| - A,A,Q; N,G,V - ); - - for set in iso_mass { - for set in set.iter().permutations(2) { - let a = set[0]; - let b = set[1]; - for seq_a in a.iter().permutations(a.len()) { - for seq_b in b.iter().permutations(b.len()) { - alphabet.array[get_index_ref(&seq_a)][get_index_ref(&seq_b)] = - Scoring::IsoMass as i8; - } - } - } - } - - let modifications = sets!( - //N;D| // Amidation only at N term - Q;E| // Deamidation - D;N| // Deamidation - C;T| // Disulfide bond - T;D| // Methylation - S;T| // Methylation - D;E| // Methylation - R;A,V;G,L| // Methylation - Q;A,A // Methylation - ); - - for set in modifications { - let a = &set[0]; - for seq_b in set.iter().skip(1) { - alphabet.array[get_index(a)][get_index(seq_b.as_slice())] = - Scoring::Modification as i8; - } - } - - let amino_acids = (1..=AminoAcid::MAX) - .map(|a| AminoAcid::try_from(a).unwrap()) - .collect_vec(); - for size in 2..=Self::STEPS { - for set in amino_acids - .iter() - .combinations_with_replacement(size) - .flat_map(|v| v.into_iter().permutations(size)) - { - if set.iter().all(|v| *v == set[0]) { - continue; // Do not add [A, A] or [A, A, A] etc as SWITCHED - } - #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] - // set.len() is at max equal to Self::STEPS - for switched in set.clone().into_iter().permutations(size) { - alphabet.array[get_index_ref(&set)][get_index_ref(&switched)] = - Scoring::Switched as i8 * set.len() as i8; - } - } - } - - alphabet - } -} - -#[cfg(test)] -mod tests { - use super::{Alphabet, Scoring}; - use crate::aminoacid::AminoAcid::*; - - #[test] - fn identity() { - let alphabet = Alphabet::default(); - assert_eq!( - Scoring::Identity as i8, - alphabet[([A].as_slice(), [A].as_slice())] - ); - assert_eq!(0, alphabet[([A, A].as_slice(), [A, A].as_slice())]); - assert_eq!(0, alphabet[([A, A, A].as_slice(), [A, A, A].as_slice())]); - } - - #[test] - fn similarity() { - let alphabet = Alphabet::default(); - assert_eq!( - Scoring::IsoMass as i8, - alphabet[([I].as_slice(), [L].as_slice())] - ); - assert_eq!( - Scoring::IsoMass as i8, - alphabet[([N].as_slice(), [G, G].as_slice())] - ); - assert_eq!( - Scoring::IsoMass as i8, - alphabet[([G, G].as_slice(), [N].as_slice())] - ); - assert_eq!( - Scoring::IsoMass as i8, - alphabet[([A, S].as_slice(), [G, T].as_slice())] - ); - assert_eq!( - Scoring::IsoMass as i8, - alphabet[([S, A].as_slice(), [G, T].as_slice())] - ); - assert_eq!( - Scoring::IsoMass as i8, - alphabet[([S, A].as_slice(), [T, G].as_slice())] - ); - assert_eq!( - Scoring::IsoMass as i8, - alphabet[([A, S].as_slice(), [T, G].as_slice())] - ); - assert_eq!( - Scoring::IsoMass as i8, - alphabet[([L, Q].as_slice(), [A, V, A].as_slice())] - ); - } - - #[test] - fn inequality() { - let alphabet = Alphabet::default(); - assert_eq!( - Scoring::Mismatch as i8, - alphabet[([I].as_slice(), [Q].as_slice())] - ); - assert_eq!(0, alphabet[([Q].as_slice(), [G, G].as_slice())]); - assert_eq!(0, alphabet[([A, E].as_slice(), [G, T].as_slice())]); - assert_eq!(0, alphabet[([E, Q].as_slice(), [A, V, A].as_slice())]); - } - - #[test] - fn switched() { - let alphabet = Alphabet::default(); - assert_eq!( - Scoring::Switched as i8 * 2, - alphabet[([E, Q].as_slice(), [Q, E].as_slice())] - ); - assert_eq!( - Scoring::Switched as i8 * 3, - alphabet[([D, A, C].as_slice(), [A, C, D].as_slice())] - ); - assert_eq!( - Scoring::Switched as i8 * 3, - alphabet[([C, D, A].as_slice(), [A, C, D].as_slice())] - ); - assert_eq!( - Scoring::Switched as i8 * 3, - alphabet[([A, C, D].as_slice(), [D, A, C].as_slice())] - ); - assert_eq!( - Scoring::Switched as i8 * 3, - alphabet[([C, D, A].as_slice(), [D, A, C].as_slice())] - ); - assert_eq!( - Scoring::Switched as i8 * 3, - alphabet[([A, C, D].as_slice(), [C, D, A].as_slice())] - ); - assert_eq!( - Scoring::Switched as i8 * 3, - alphabet[([D, A, C].as_slice(), [C, D, A].as_slice())] - ); - assert_eq!( - Scoring::Switched as i8 * 3, - alphabet[([V, A, A].as_slice(), [A, V, A].as_slice())] - ); - } - - #[test] - fn modification() { - let alphabet = Alphabet::default(); - assert_eq!( - Scoring::Modification as i8, - alphabet[([D].as_slice(), [N].as_slice())] - ); - assert_eq!( - Scoring::Mismatch as i8, - alphabet[([N].as_slice(), [D].as_slice())] - ); - } -} diff --git a/src-tauri/alignment/src/aminoacid.rs b/src-tauri/alignment/src/aminoacid.rs deleted file mode 100644 index 71c5ffd..0000000 --- a/src-tauri/alignment/src/aminoacid.rs +++ /dev/null @@ -1,170 +0,0 @@ -use itertools::Itertools; -use std::fmt::Display; - -/// All aminoacids -#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Eq, Ord)] -pub enum AminoAcid { - /// Alanine - A = 1, - /// Arginine - R, - /// Asparagine - N, - /// Aspartic acid - D, - /// Cysteine - C, - /// Glutamine - Q, - /// Glutamic acid - E, - /// Glycine - G, - /// Histidine - H, - /// Isoleucine - I, - /// Leucine - L, - /// Lysine - K, - /// Methionine - M, - /// Phenylalanine - F, - /// Proline - P, - /// Serine - S, - /// Threonine - T, - /// Tryptophan - W, - /// Tyrosine - Y, - /// Valine - V, - /// Weird - B, - /// Also weird - Z, - /// Single gap - X, - /// Longer gap - Gap, -} - -impl AminoAcid { - /// The total number of normal amino acids (disregards Gap) - pub const MAX: usize = 23; -} - -impl Display for AminoAcid { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(match self { - Self::A => "A", - Self::R => "R", - Self::N => "N", - Self::D => "D", - Self::C => "C", - Self::Q => "Q", - Self::E => "E", - Self::G => "G", - Self::H => "H", - Self::I => "I", - Self::L => "L", - Self::K => "K", - Self::M => "M", - Self::F => "F", - Self::P => "P", - Self::S => "S", - Self::T => "T", - Self::W => "W", - Self::Y => "Y", - Self::V => "V", - Self::B => "B", - Self::Z => "Z", - Self::X => "X", - Self::Gap => "*", - }) - } -} - -impl TryFrom for AminoAcid { - type Error = (); - fn try_from(num: usize) -> Result { - match num { - 1 => Ok(Self::A), - 2 => Ok(Self::R), - 3 => Ok(Self::N), - 4 => Ok(Self::D), - 5 => Ok(Self::C), - 6 => Ok(Self::Q), - 7 => Ok(Self::E), - 8 => Ok(Self::G), - 9 => Ok(Self::H), - 10 => Ok(Self::I), - 11 => Ok(Self::L), - 12 => Ok(Self::K), - 13 => Ok(Self::M), - 14 => Ok(Self::F), - 15 => Ok(Self::P), - 16 => Ok(Self::S), - 17 => Ok(Self::T), - 18 => Ok(Self::W), - 19 => Ok(Self::Y), - 20 => Ok(Self::V), - 21 => Ok(Self::B), - 22 => Ok(Self::Z), - 23 => Ok(Self::X), - 24 => Ok(Self::Gap), - _ => Err(()), - } - } -} - -impl TryFrom for AminoAcid { - type Error = (); - fn try_from(value: char) -> Result { - match value { - 'A' => Ok(Self::A), - 'R' => Ok(Self::R), - 'N' => Ok(Self::N), - 'D' => Ok(Self::D), - 'C' => Ok(Self::C), - 'Q' => Ok(Self::Q), - 'E' => Ok(Self::E), - 'G' => Ok(Self::G), - 'H' => Ok(Self::H), - 'I' => Ok(Self::I), - 'L' => Ok(Self::L), - 'K' => Ok(Self::K), - 'M' => Ok(Self::M), - 'F' => Ok(Self::F), - 'P' => Ok(Self::P), - 'S' => Ok(Self::S), - 'T' => Ok(Self::T), - 'W' => Ok(Self::W), - 'Y' => Ok(Self::Y), - 'V' => Ok(Self::V), - 'B' => Ok(Self::B), - 'Z' => Ok(Self::Z), - 'X' => Ok(Self::X), - '*' => Ok(Self::Gap), - _ => Err(()), - } - } -} - -/// Create an aminoacid sequence from a string, just ignores any non aminoacids characters -pub fn sequence_from_string(value: &str) -> Vec { - value - .chars() - .filter_map(|v| AminoAcid::try_from(v).ok()) - .collect() -} - -/// Generate a string from a sequence of aminoacids -pub fn sequence_to_string(value: &[AminoAcid]) -> String { - value.iter().map(std::string::ToString::to_string).join("") -} diff --git a/src-tauri/alignment/src/bin.rs b/src-tauri/alignment/src/bin.rs deleted file mode 100644 index a75f168..0000000 --- a/src-tauri/alignment/src/bin.rs +++ /dev/null @@ -1,102 +0,0 @@ -#![allow(dead_code)] -#![warn(clippy::pedantic, clippy::nursery, clippy::all)] -#![allow(clippy::enum_glob_use, clippy::wildcard_imports)] -use mass_alignment::template::Template; -use mass_alignment::*; - -fn main() { - let alphabet = Alphabet::default(); - //let template = aminoacid::sequence_from_string("XXXXXXXXXXXXXXXXXXXXYFDYWGQGTLVTVSS"); - let template = mass_alignment::sequence_from_string("EVQLVESGGGLVQPGGSLRLSCAASGFTVSSNYMSWVRQAPGKGLEWVSVIYSGGSTYYADSVKGRFTISRDNSKNTLYLQMNSLRAEDTAVYYCARXXXXXXXXXXXXXXXXXXXX"); - let reads: Vec> = [ - //"SRWGGDGFYAMDYWGQGTLVTV", - //"DWNGFYAMDYWGQGTLVTVSS", - //"RWGGDGFYAMDYWGQGTLVTV", - //"HVPHGDGFYAMDYWGQGTLVT", - //"WRGGDGFYAMDYWGQGTLVT", - //"SRWGGDGFYAMDYWGQGTLV", - //"RWGGDGFYAMDYWGQGTLVT", - //"WRNDGFYAMDYWGQGTLVT", - //"RWGGDGFYAMDYWGQGTLV", - //"MARNDGFYAMDYWGQGTLV", - //"RWNDGFYAMDYWGQGTLV", - //"SRWGGNGFYWDYWGQGT", - //"RWNDGFYWDYWGQGT", - //"DYWGQGTLVVTSS", - //"DYWGQGTLVTVSS", - //"DYWGQGTLVTV", - //"DYWGQGTLVT", - //"WGQGTLVT", - "DLQLVESGGGLVGAKSPPGTLSAAASGFNL", - "DLQLVESGGGLVGAKSPPGTLSAAASGFNL", - "EVQLVESGGGLVQPGGSLSGAKYHSGFNL", - "EVVQLVESGGGLVQPGGSLGVLSCAASGF", - "DLQLVESGGGLVQPGGSLGVLSCAASGF", - "DLQLVESGGGLVQPGTPLYWNAASGFNL", - "DLQLVESGGGLVQPGGSLRLSCAASGF", - "QVQLVESGGGLVQPGGSLRLSCAASGF", - "EVQLVESGGGLPVQGGSLRLSCAADGF", - "EVQLVESGGGLVQPGGSLRLSCAASGF", - "EVQLVSGEGGLVQPGGSLRLSCAASGF", - "QVELVESGGGLVQPGGSLRLSCAASGF", - "TLSADTSKNTAYLQMNSLRAEDTAVY", - "RFTLSADTSKNTAYLQMNSLRAEDTA", - "QLVESGGGLVQPGGSLTHVAGAGHSGF", - "SADTSKNTAYLQMNSLRAEDTAVYY", - "LMLTDGYTRYADSVKGRFTLSADTS", - "QLVESGGGLVQPGGSLRLSCAASGF", - "QLVESGGGLVQPGGSLRLSCQTGF", - "LVESGGGLVQPNSLRLSCAASGF", - ] - .into_iter() - .map(mass_alignment::sequence_from_string) - .collect(); - - let template = Template::new( - template, - reads.iter().map(std::vec::Vec::as_slice).collect(), - &alphabet, - Type::GlobalForB, - ); - let content = format!( - " - - - - - - - -
-
{} -
-
- -", - template.generate_html() - ); - std::fs::write("test.html", content).unwrap(); -} diff --git a/src-tauri/alignment/src/lib.rs b/src-tauri/alignment/src/lib.rs deleted file mode 100644 index 720fa4f..0000000 --- a/src-tauri/alignment/src/lib.rs +++ /dev/null @@ -1,42 +0,0 @@ -//! An algorithm based on Needleman Wunsch/Smith Waterman but extended to allow for mass based alignment. -//! The mass based part gives the option to match two sets of aminoacids with different sizes. -//! For example the set {Q} matches {AG} because these have the same mass and so are commonly misclassified -//! is de novo sequencing for peptides. Besides iso mass definitions it also handles swaps with finesse, -//! meaning that {AG} matches {GA} with a well defined score to allow for these mistakes to be fixed. The -//! last important addition is the handling of post translational modifications meaning that {Q} matches {E} -//! but not the other way around to allow for deamidation of the sample in reference to the template. -//! -//! ```rust -//! use mass_alignment::*; -//! use mass_alignment::AminoAcid::*; -//! -//! let alphabet = Alphabet::default(); -//! let template = &[A,G,Q,S,T,Q]; -//! let query = &[Q,E,S,W]; -//! let result = align(template, query, &alphabet, Type::GlobalForB); -//! println!("{}", result.summary()); -//! assert_eq!(15, result.score) -//! ``` - -#![allow(dead_code)] -#![warn(clippy::pedantic, clippy::nursery, clippy::all, missing_docs)] -#![allow( - clippy::enum_glob_use, - clippy::wildcard_imports, - clippy::must_use_candidate -)] -/// The module containing all alignment handling -mod alignment; -/// The module containing all alphabet handling -mod alphabet; -/// The module containing the definition for aminoacids -mod aminoacid; -/// The module containing the definition for templates -pub mod template; - -pub use crate::alignment::align; -pub use crate::alignment::*; -pub use crate::alphabet::Alphabet; -pub use crate::aminoacid::sequence_from_string; -pub use crate::aminoacid::sequence_to_string; -pub use crate::aminoacid::AminoAcid; diff --git a/src-tauri/alignment/src/template.rs b/src-tauri/alignment/src/template.rs deleted file mode 100644 index d94f6e3..0000000 --- a/src-tauri/alignment/src/template.rs +++ /dev/null @@ -1,128 +0,0 @@ -use crate::alignment::{self, Alignment}; -use crate::alphabet::Scoring; -use crate::aminoacid::{self, AminoAcid}; -use crate::{align, Alphabet}; -use itertools::Itertools; -use std::fmt::Write; - -/// A template that is matched with many reads -pub struct Template { - /// The sequence of this template - pub sequence: Vec, - /// The reads matched to this template - pub reads: Vec, -} - -impl Template { - /// Create a new template by matching the given reads to the given template sequence - pub fn new( - sequence: Vec, - reads: Vec<&[AminoAcid]>, - alphabet: &Alphabet, - alignment_type: alignment::Type, - ) -> Self { - Self { - reads: reads - .into_iter() - .map(|v| align(&sequence, v, alphabet, alignment_type)) - .collect(), - sequence, - } - } - - /// Generate HTML for a reads alignment, all styling is missing and it is only a small part of a document - pub fn generate_html(&self) -> String { - let mut insertions = vec![0; self.sequence.len()]; - for read in &self.reads { - let mut loc_a = read.start_a; - let mut insertion = 0; - - for piece in &read.path { - if piece.step_a == 0 && piece.step_b == 1 { - insertion += 1; - } else if insertion != 0 { - insertions[loc_a] = std::cmp::max(insertions[loc_a], insertion); - insertion = 0; - } else { - insertion = 0; - } - loc_a += piece.step_a as usize; - } - } - - let mut output = format!("
1....
", insertions.iter().sum::() + self.sequence.len()); - for (ins, seq) in insertions.iter().zip(&self.sequence) { - let _ = write!(output, "{:-<1$}{2}", "", ins, seq); - } - let _ = write!(output, "
"); - - for read in &self.reads { - let _ = write!( - output, - "
", - insertions[0..read.start_a].iter().sum::() + read.start_a + 1, - insertions[0..read.start_a + read.len_a()] - .iter() - .sum::() - + read.start_a - + read.len_a() - + 3 - ); - let mut loc_a = read.start_a; - let mut loc_b = read.start_b; - let mut insertion = 0; - for piece in &read.path { - if piece.step_a == 0 && piece.step_b == 1 { - insertion += 1; - } else { - let _ = write!(output, "{:-<1$}", "", insertions[loc_a] - insertion); - insertion = 0; - } - let _ = write!( - output, - "{}", - match (piece.step_a, piece.step_b) { - (0 | 1, 1) => read.seq_b[loc_b].to_string(), - (1, 0) => "-".to_string(), - #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] - // a is defined to be in range 0..=Alphabet::STEPS - (a, b) => { - let inner = if a == b { - // As a equals b it is a swap or iso length iso mass sets, add in the missing insertions (if any) - // Because a equals b the length of the sequence patch and insertion patch is always equal. - // This means that the resulting insertions makes the text nicely aligned. - read.seq_b[loc_b..loc_b + b as usize] - .iter() - .zip(&insertions[loc_a..loc_a + a as usize]) - .map(|(sb, sa)| format!("{:->1$}", sb.to_string(), sa + 1)) - .join("") - } else { - aminoacid::sequence_to_string( - &read.seq_b[loc_b..loc_b + b as usize], - ) - }; - format!( - "{}", - if a == b && piece.local_score == Scoring::Switched as i8 * a as i8 - { - " swap" - } else { - "" - }, - inner.len(), - insertions[loc_a..loc_a + a as usize].iter().sum::() - + a as usize, - inner - ) - } - } - ); - loc_a += piece.step_a as usize; - loc_b += piece.step_b as usize; - } - let _ = write!(output, "
"); - } - let _ = write!(output, "
"); - output - } -} diff --git a/src-tauri/build.rs b/src-tauri/build.rs index 76b12c6..2eca015 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -15,7 +15,7 @@ r#" - Stitch+Ox + Annotator @@ -23,112 +23,59 @@ r#" - -
- Spectra -
-

Load spectra

- - - -
-
-

Annotate

- - - - - - - - - - - - -
- Custom model -

Ion

-

Location

-

Loss

"#).unwrap(); +
+

Load spectra

+ + + +
+
+

Annotate

+ + + + + + + + + + + + +
+ Custom model +

Ion

+

Location

+

Loss

"#).unwrap(); for ion in ["a", "b", "c", "d", "v", "w", "x", "y", "z"] { write!( writer, r#" -
- - - -
- "#, +
+ + + +
+ "#, ion ) .unwrap(); @@ -136,19 +83,18 @@ LVESGGGLVQPNSLRLSCAASGF write!( writer, r#" - -
- - - -
-

-    
-
- Logs -
-

-    
+ +
+ + + +
+

+  
+
+ Logs +
+

   
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index fc8d936..31cc6f0 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -3,99 +3,15 @@ windows_subsystem = "windows" )] -use itertools::Itertools; -use mass_alignment::{template::Template, *}; -use pdbtbx::*; use rustyms::{e, Charge, Location, Model, NeutralLoss}; use rustyms::{mz, MassOverCharge}; use state::State; -use std::collections::HashMap; use std::sync::Mutex; mod html_builder; mod render; mod state; -// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command -#[tauri::command] -fn align_sequences(template: &str, reads: &str, alignment_type: &str) -> String { - let alphabet = Alphabet::default(); - let template = sequence_from_string(template); - let reads: Vec> = reads.split('\n').map(sequence_from_string).collect(); - let alignment_type = match alignment_type { - "1" => Type::Local, - "2" => Type::GlobalForB, - "3" => Type::Global, - _ => panic!("Incorrect alignment type"), - }; - - let result = Template::new( - template, - reads.iter().map(|a| a.as_slice()).collect(), - &alphabet, - alignment_type, - ); - result.generate_html() -} - -#[tauri::command] -fn load_cif(path: &str, min_length: usize, warn: bool) -> Result<(String, String), String> { - let result = open(path, StrictnessLevel::Loose); - if let Ok(file) = result { - let warnings = file.1.into_iter().map(|err| format!("{}", err)).join("\n"); - let pdb = file.0; - let mut found_unknown = HashMap::new(); - let output = pdb - .chains() - .map(|c| { - c.conformers() - .filter_map(|a| { - match AMINO_ACIDS - .iter() - .position(|err| *err == a.name()) - .and_then(|v| AMINO_ACIDS_CHAR.get(v)) - { - Some(s) => Some(s), - None => { - if warn && !IGNORE_LIST.contains(&a.name()) { - found_unknown.insert( - a.name(), - 1 + found_unknown.get(a.name()).unwrap_or(&0), - ); - }; - None - } - } - }) - .collect::() - }) - .filter(|a| a.len() >= min_length) - .join("\n"); - let warnings = warnings + "\n" + &found_unknown.into_iter().map(|name| { - format!( - "{}", - PDBError::new( - ErrorLevel::GeneralWarning, - "Unrecognised residue", - format!( - "This name was not recognised as an Amino Acid or common solvent. It was found {} time{}.", - name.1, - if name.1 != 1 { "s" } else { "" } - ), - Context::show(name.0), - ) - ) - }).join("\n"); - Ok((output, warnings)) - } else { - Err(result - .unwrap_err() - .into_iter() - .map(|a| format!("{}", a)) - .collect()) - } -} - type ModifiableState<'a> = tauri::State<'a, std::sync::Mutex>; // Learn more about Tauri commands at https://tauri.app/v1/guides/features/command @@ -173,32 +89,12 @@ fn annotate_spectrum( )) } -/// All amino acids. Includes Amber-specific naming conventions for (de-)protonated versions, CYS involved in -/// disulfide bonding and the like. -const AMINO_ACIDS: &[&str] = &[ - "ALA", "ARG", "ASH", "ASN", "ASP", "ASX", "CYS", "CYX", "GLH", "GLN", "GLU", "GLY", "HID", - "HIE", "HIM", "HIP", "HIS", "ILE", "LEU", "LYN", "LYS", "MET", "PHE", "PRO", "SER", "THR", - "TRP", "TYR", "VAL", "SEC", "PYL", -]; - -const AMINO_ACIDS_CHAR: &[char] = &[ - 'A', 'R', 'N', 'N', 'D', 'B', 'C', 'C', 'Q', 'Q', 'E', 'G', 'H', 'H', 'H', 'H', 'H', 'I', 'L', - 'K', 'K', 'M', 'F', 'P', 'S', 'T', 'W', 'Y', 'V', 'U', 'O', -]; - -const IGNORE_LIST: &[&str] = &["HOH", "WAT", "ADP", "DMS"]; // Common solvents I recognised - fn main() { tauri::Builder::default() .manage(Mutex::new(State { spectra: Vec::new(), })) - .invoke_handler(tauri::generate_handler![ - align_sequences, - load_cif, - load_mgf, - annotate_spectrum - ]) + .invoke_handler(tauri::generate_handler![load_mgf, annotate_spectrum]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 792234f..47fd1f0 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -7,8 +7,8 @@ "withGlobalTauri": true }, "package": { - "productName": "stitch-oxide", - "version": "0.0.0" + "productName": "annotator", + "version": "0.1.0" }, "tauri": { "allowlist": { @@ -32,7 +32,7 @@ "icons/icon.icns", "icons/icon.ico" ], - "identifier": "com.stitch.stitch-oxide", + "identifier": "com.snijderlab.annotator", "longDescription": "", "macOS": { "entitlements": null, @@ -62,7 +62,7 @@ "height": 600, "resizable": true, "alwaysOnTop": false, - "title": "Stitch[+Oxide]", + "title": "Annotator", "width": 800 } ] diff --git a/src/main.js b/src/main.js index 1db9926..829999d 100644 --- a/src/main.js +++ b/src/main.js @@ -1,25 +1,5 @@ const { invoke } = window.__TAURI__.tauri; -let sequenceInputA; -let sequenceInputB; -let sequenceType; -let alignmentScore; - -async function align() { - alignmentScore.innerHTML = await invoke("align_sequences", { template: sequenceInputA.value, reads: sequenceInputB.value, alignmentType: sequenceType.value }); -} - -async function load_cif() { - try { - var result = await invoke("load_cif", { path: document.querySelector("#load-path").value, minLength: Number(document.querySelector("#load-min-length").value), warn: true }); - sequenceInputB.value = result[0]; - document.querySelector("#error-log").innerText = result[1]; - } catch (error) { - console.log(error); - document.querySelector("#error-log").innerText = error; - } -} - async function load_mgf() { try { let result = await invoke("load_mgf", { path: document.querySelector("#load-mgf-path").dataset.filepath });