Skip to main content

bio_seq/
translation.rs

1//! # Amino acid translation tables
2//!
3//! This module provides traits for implementing amino acid translation tables.
4//!
5//! Enable the translation feature in `Cargo.toml`:
6//!
7//! ```toml
8//! [dependencies]
9//! bio-seq = { version="0.15", features=["translation"] }
10//! ```
11//!
12//! ## Examples
13//!
14//! The standard genetic code is provided as a `translation::STANDARD` constant:
15//!
16//! ```rust
17//! use bio_seq::prelude::*;
18//! use bio_seq::translation::STANDARD;
19//! use bio_seq::translation::TranslationTable;
20//!
21//! let seq = dna!("AATTTGTGGGTTCGTCTGCGGCTCCGCCCTTAGTACTATGAGGACGATCAGCACCATAAGAACAAA");
22//!
23//! let aminos: Seq<Amino> = seq
24//!     .windows(3)
25//!     .map(|codon| STANDARD.to_amino(&codon))
26//!     .collect::<Seq<Amino>>();
27//!
28//! assert_eq!(
29//!     aminos,
30//!     Seq::<Amino>::try_from("NIFLCVWGGVFSRVSLCARGALSPRAPPLL*SVYTLYM*ERGDTRDISQSAHTPHI*KRENTQK").unwrap()
31//! );
32//!
33//! ```
34//!
35//! Custom translation tables can be implemented from associative datastructures:
36//!
37//! ```
38//! use bio_seq::prelude::*;
39//! use bio_seq::translation::{TranslationTable, TranslationError};
40//!
41//! struct Mitochondria;
42//! impl TranslationTable<Dna, Amino> for Mitochondria {
43//!     fn to_amino(&self, codon: &SeqSlice<Dna>) -> Amino {
44//!         if codon == dna!("AGA") {
45//!             Amino::X
46//!         } else if codon == dna!("AGG") {
47//!             Amino::X
48//!         } else if codon == dna!("ATA") {
49//!             Amino::M
50//!        } else if codon == dna!("TGA") {
51//!             Amino::W
52//!         } else {
53//!             Amino::unsafe_from_bits(Into::<u8>::into(codon))
54//!         }
55//!     }
56//!     fn to_codon(&self, _amino: Amino) -> Result<Seq<Dna>, TranslationError> {
57//!         unimplemented!()
58//!     }
59//! }
60//!
61//! let seq: Seq<Dna> =
62//!     dna!("AATTTGTGGGTTCGTCTGCGGCTCCGCCCTTAGTACTATGAGGACGATCAGCACCATAAGAACAAA").into();
63//! let aminos: Seq<Amino> = seq
64//!     .windows(3)
65//!     .map(|codon| Mitochondria.to_amino(&codon))
66//!     .collect::<Seq<Amino>>();
67//! assert_eq!(seq.len() - 2, aminos.len());
68//!
69//! for (x, y) in aminos.into_iter().zip(
70//!    Seq::<Amino>::try_from(
71//!         "NIFLCVWGGVFSRVSLCARGALSPRAPPLL*SVYTLYMWE*GDTRDISQSAHTPHM*K*ENTQK",
72//!     )
73//!     .unwrap()
74//!     .into_iter()) {
75//!
76//!     assert_eq!(x, y)
77//! }
78//! ```
79//!
80//! ## Errors
81//!
82//! Translation tables may not be complete or they may be ambiguous
83//!
84use core::cmp::Eq;
85use core::fmt;
86use std::collections::HashMap;
87
88use crate::codec::Codec;
89use crate::prelude::{Amino, Dna, Seq, SeqSlice};
90
91mod standard;
92
93pub use crate::translation::standard::STANDARD;
94
95/// Error conditions for codon/amino acid translation
96#[derive(Debug, PartialEq, Eq, Clone)]
97pub enum TranslationError<A: Codec = Dna, B: Codec = Amino> {
98    /// Amino acid can be translated from multiple codons
99    AmbiguousCodon(B),
100    /// Codon sequence maps to multiple amino acids
101    AmbiguousTranslation(Seq<A>),
102    /// Codon sequence does not map to an amino acid
103    InvalidCodon(Seq<A>),
104    /// Amino acid symbol is not valid (i.e. `X`)
105    InvalidAmino(B),
106}
107
108impl<A: Codec, B: Codec> fmt::Display for TranslationError<A, B> {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110        match self {
111            TranslationError::AmbiguousCodon(amino) => {
112                let amino = amino.to_char();
113                write!(f, "Multiple codon sequences: {amino}")
114            }
115            TranslationError::AmbiguousTranslation(codon) => {
116                write!(f, "Ambiguous translations for codon: {codon}")
117            }
118            TranslationError::InvalidCodon(codon) => write!(f, "Invalid codon sequence: {codon}"),
119            TranslationError::InvalidAmino(amino) => {
120                let amino = amino.to_char();
121                write!(f, "Invalid amino acid character: {amino}")
122            }
123        }
124    }
125}
126
127// #![feature(error_in_core)
128impl<A: Codec, B: Codec> std::error::Error for TranslationError<A, B> {}
129
130/// A codon translation table where all codons map to amino acids
131pub trait TranslationTable<A: Codec, B: Codec> {
132    fn to_amino(&self, codon: &SeqSlice<A>) -> B;
133
134    /// # Errors
135    ///
136    /// Will return `Err` when an amino acid has multiple codons (most cases)
137    fn to_codon(&self, amino: B) -> Result<Seq<A>, TranslationError<A, B>>;
138}
139
140/// A partial translation table where not all triples of characters map to amino acids
141pub trait PartialTranslationTable<A: Codec, B: Codec> {
142    /// # Errors
143    ///
144    /// Will return an `Err` if a codon does not map to an amino acid. This would be
145    /// the case for a translation table from codons with ambiguous nucleotide codes such as `ANC`, `SWS`, `NNN`, etc.
146    fn try_to_amino(&self, codon: &SeqSlice<A>) -> Result<B, TranslationError<A, B>>;
147    /// # Errors
148    ///
149    /// Will return an `Err` if the amino acid can be translated from different codons
150    fn try_to_codon(&self, amino: B) -> Result<Seq<A>, TranslationError<A, B>>;
151}
152
153/// A customisable translation table
154pub struct CodonTable<A: Codec, B: Codec> {
155    // I'm open to using a better bidirectional mapping datastructure
156    table: HashMap<Seq<A>, B>,
157    inverse_table: HashMap<B, Option<Seq<A>>>,
158}
159
160impl<A: Codec, B: Codec> CodonTable<A, B> {
161    pub fn from_map<T>(table: T) -> Self
162    where
163        T: Into<HashMap<Seq<A>, B>>,
164    {
165        let table: HashMap<Seq<A>, B> = table.into();
166        let mut inverse_table = HashMap::new();
167        for (codon, amino) in &table {
168            if inverse_table.contains_key(amino) {
169                inverse_table.insert(*amino, None);
170            } else {
171                inverse_table.insert(*amino, Some(codon.clone()));
172            }
173        }
174        CodonTable {
175            table,
176            inverse_table,
177        }
178    }
179}
180
181impl<A: Codec, B: Codec> PartialTranslationTable<A, B> for CodonTable<A, B> {
182    fn try_to_amino(&self, codon: &SeqSlice<A>) -> Result<B, TranslationError<A, B>> {
183        self.table
184            .get(codon)
185            .ok_or_else(|| TranslationError::InvalidCodon(codon.into()))
186            .copied()
187    }
188
189    fn try_to_codon(&self, amino: B) -> Result<Seq<A>, TranslationError<A, B>> {
190        if let Some(codon) = self.inverse_table.get(&amino) {
191            match codon {
192                Some(codon) => Ok(codon.clone()),
193                None => Err(TranslationError::AmbiguousCodon(amino)),
194            }
195        } else {
196            Err(TranslationError::InvalidAmino(amino))
197        }
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use crate::prelude::*;
204    use crate::translation::{
205        CodonTable, PartialTranslationTable, TranslationError, TranslationTable,
206    };
207
208    #[test]
209    fn custom_codon_table() {
210        let mito: [(Seq<Dna>, Amino); 6] = [
211            (dna!("AAA").into(), Amino::A),
212            (dna!("ATG").into(), Amino::A),
213            (dna!("CCC").into(), Amino::C),
214            (dna!("GGG").into(), Amino::E),
215            (dna!("TTT").into(), Amino::D),
216            (dna!("TTA").into(), Amino::F),
217        ];
218
219        let table = CodonTable::from_map(mito);
220
221        let seq: Seq<Dna> = dna!("AAACCCGGGTTTTTATTAATG").into();
222        let mut amino_seq: Seq<Amino> = Seq::new();
223        for codon in seq.chunks(3) {
224            amino_seq.push(table.try_to_amino(codon).unwrap());
225        }
226        assert_eq!(amino_seq, Seq::<Amino>::try_from("ACEDFFA").unwrap());
227
228        assert_ne!(table.try_to_codon(Amino::E), Ok(dna!("CCC").into()));
229        assert_eq!(table.try_to_codon(Amino::C), Ok(dna!("CCC").into()));
230        assert_eq!(
231            table.try_to_codon(Amino::A),
232            Err(TranslationError::AmbiguousCodon(Amino::A))
233        );
234        assert_eq!(
235            table.try_to_codon(Amino::X),
236            Err(TranslationError::InvalidAmino(Amino::X))
237        );
238    }
239
240    #[test]
241    fn mitochondrial_coding_table() {
242        struct Mitochondria;
243
244        impl TranslationTable<Dna, Amino> for Mitochondria {
245            fn to_amino(&self, codon: &SeqSlice<Dna>) -> Amino {
246                if codon == dna!("AGA") || codon == dna!("AGG") {
247                    Amino::X
248                } else if codon == dna!("ATA") {
249                    Amino::M
250                } else if codon == dna!("TGA") {
251                    Amino::W
252                } else {
253                    Amino::unsafe_from_bits(Into::<u8>::into(codon))
254                }
255            }
256
257            fn to_codon(&self, _amino: Amino) -> Result<Seq<Dna>, TranslationError> {
258                unimplemented!()
259            }
260        }
261
262        let seq: Seq<Dna> =
263            dna!("AATTTGTGGGTTCGTCTGCGGCTCCGCCCTTAGTACTATGAGGACGATCAGCACCATAAGAACAAA").into();
264        let aminos: Seq<Amino> = seq
265            .windows(3)
266            .map(|codon| Mitochondria.to_amino(codon))
267            .collect::<Seq<Amino>>();
268        assert_eq!(seq.len() - 2, aminos.len());
269
270        for (x, y) in aminos.into_iter().zip(
271            &Seq::<Amino>::try_from(
272                "NIFLCVWGGVFSRVSLCARGALSPRAPPLL*SVYTLYMWE*GDTRDISQSAHTPHM*K*ENTQK",
273            )
274            .unwrap(),
275        ) {
276            assert_eq!(x, y);
277        }
278    }
279}