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        self.inverse_table
191            .get(&amino)
192            .ok_or(TranslationError::InvalidAmino(amino))?
193            .clone()
194            .ok_or(TranslationError::AmbiguousCodon(amino))
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use crate::prelude::*;
201    use crate::translation::{
202        CodonTable, PartialTranslationTable, TranslationError, TranslationTable,
203    };
204
205    #[test]
206    fn custom_codon_table() {
207        let mito: [(Seq<Dna>, Amino); 6] = [
208            (dna!("AAA").into(), Amino::A),
209            (dna!("ATG").into(), Amino::A),
210            (dna!("CCC").into(), Amino::C),
211            (dna!("GGG").into(), Amino::E),
212            (dna!("TTT").into(), Amino::D),
213            (dna!("TTA").into(), Amino::F),
214        ];
215
216        let table = CodonTable::from_map(mito);
217
218        let seq: Seq<Dna> = dna!("AAACCCGGGTTTTTATTAATG").into();
219        let mut amino_seq: Seq<Amino> = Seq::new();
220        for codon in seq.chunks(3) {
221            amino_seq.push(table.try_to_amino(codon).unwrap());
222        }
223        assert_eq!(amino_seq, Seq::<Amino>::try_from("ACEDFFA").unwrap());
224
225        assert_ne!(table.try_to_codon(Amino::E), Ok(dna!("CCC").into()));
226        assert_eq!(table.try_to_codon(Amino::C), Ok(dna!("CCC").into()));
227        assert_eq!(
228            table.try_to_codon(Amino::A),
229            Err(TranslationError::AmbiguousCodon(Amino::A))
230        );
231        assert_eq!(
232            table.try_to_codon(Amino::X),
233            Err(TranslationError::InvalidAmino(Amino::X))
234        );
235    }
236
237    #[test]
238    fn mitochondrial_coding_table() {
239        struct Mitochondria;
240
241        impl TranslationTable<Dna, Amino> for Mitochondria {
242            fn to_amino(&self, codon: &SeqSlice<Dna>) -> Amino {
243                if codon == dna!("AGA") || codon == dna!("AGG") {
244                    Amino::X
245                } else if codon == dna!("ATA") {
246                    Amino::M
247                } else if codon == dna!("TGA") {
248                    Amino::W
249                } else {
250                    Amino::unsafe_from_bits(Into::<u8>::into(codon))
251                }
252            }
253
254            fn to_codon(&self, _amino: Amino) -> Result<Seq<Dna>, TranslationError> {
255                unimplemented!()
256            }
257        }
258
259        let seq: Seq<Dna> =
260            dna!("AATTTGTGGGTTCGTCTGCGGCTCCGCCCTTAGTACTATGAGGACGATCAGCACCATAAGAACAAA").into();
261        let aminos: Seq<Amino> = seq
262            .windows(3)
263            .map(|codon| Mitochondria.to_amino(codon))
264            .collect::<Seq<Amino>>();
265        assert_eq!(seq.len() - 2, aminos.len());
266
267        for (x, y) in aminos.into_iter().zip(
268            &Seq::<Amino>::try_from(
269                "NIFLCVWGGVFSRVSLCARGALSPRAPPLL*SVYTLYMWE*GDTRDISQSAHTPHM*K*ENTQK",
270            )
271            .unwrap(),
272        ) {
273            assert_eq!(x, y);
274        }
275    }
276}