import json
import re
from pathlib import Path
from typing import Any, Dict
from codeine.utils.dict import FrozenDict
[docs]
class TranslationTable:
"""
Translation table. Here we use the NCBI translation table IDs
(see https://www.ncbi.nlm.nih.gov/Taxonomy/Utils/wprintgc.cgi)
Uses DNA by default, but can switch to RNA by setting ``rna=True``.
"""
def __init__(self, table_id: int = 1, rna: bool = False) -> None:
"""
Parameters
----------
table_id
Which translation table to use. Default is 1, which is the standard genetic code.
rna
Whether to use RNA. Default is no (False), i.e. use DNA.
"""
self._locked = False
self.table_id = table_id
self.rna = rna
tables = self._load_tables()
try:
table = tables[str(table_id)]
except KeyError:
raise ValueError(f'Unknown NCBI translation table ID: {table_id}.')
self.name = table['name']
self.names = tuple(table.get('names', (self.name,)))
self.stop_codons = tuple(
self.normalise_sequence(codon)
for codon in table['stop_codons']
)
dna_to_aa = table['codon_to_aa']
aa_to_dna = {}
for codon, aa in dna_to_aa.items():
aa_to_dna.setdefault(aa, []).append(codon)
codons_to_aa = {
self.normalise_sequence(codon): aa
for codon, aa in dna_to_aa.items()
}
aa_to_codons = {
aa: tuple(
self.normalise_sequence(codon)
for codon in codons
)
for aa, codons in aa_to_dna.items()
}
self.codons_to_aa = FrozenDict(codons_to_aa)
self.aa_to_codons = FrozenDict(aa_to_codons)
self._locked = True
[docs]
@classmethod
def custom(
cls,
codons_to_aa: Dict[str, str],
name: str = 'Custom',
rna: bool = False,
) -> 'TranslationTable':
"""
Create a custom translation table.
This is useful for synthetic, partial, experimental, or externally
defined translation tables, or for tables that have been discovered
but not yet added to Codeine. Codons are normalised to DNA or RNA
according to the ``rna`` argument.
Parameters
----------
codons_to_aa
Mapping from codons to amino-acid symbols.
name
Name of the custom translation table. Default is ``'Custom'``.
rna
Whether the table should use RNA codons.
Returns
-------
TranslationTable
A custom translation table.
"""
obj = cls.__new__(cls)
obj._locked = False
obj.table_id = None
obj.rna = rna
obj.name = name
obj.names = (name,)
normalised = {
obj.normalise_sequence(codon): aa
for codon, aa in codons_to_aa.items()
}
if any(len(codon) != 3 for codon in normalised):
raise ValueError('All codons must have length 3.')
if any(len(aa) != 1 for aa in normalised.values()):
raise ValueError('Amino-acid symbols must be single characters.')
aa_to_codons = {}
for codon, aa in normalised.items():
aa_to_codons.setdefault(aa, []).append(codon)
obj.stop_codons = tuple(codon for codon, aa in normalised.items() if aa == '*')
obj.codons_to_aa = FrozenDict(normalised)
obj.aa_to_codons = FrozenDict({aa: tuple(codons) for aa, codons in aa_to_codons.items()})
obj._locked = True
return obj
def __setattr__(self, name: str, value: Any) -> None:
if getattr(self, '_locked', False) and name != '_locked':
raise AttributeError(f'{type(self).__name__} is immutable')
object.__setattr__(self, name, value)
def __repr__(self) -> str:
molecule = 'RNA' if self.rna else 'DNA'
lines = [
'TranslationTable',
f'Table ID: {self.table_id} ({self.name})',
f'Molecule type: {molecule}',
'',
'Table:',
]
for aa in sorted(self.aa_to_codons):
codons = ' '.join(self.aa_to_codons[aa])
lines.append(f' {aa}: {codons}')
return '\n'.join(lines)
def __getitem__(self, codon: str) -> str:
try:
codon = self.normalise_sequence(codon)
return self.codons_to_aa[codon]
except (AttributeError, TypeError, ValueError, KeyError) as e:
raise KeyError(f'Invalid codon: {codon}.') from e
[docs]
def normalise_sequence(self, seq: str) -> str:
"""
Format a sequence in the format specified by this codon table, i.e. convert to
RNA/DNA and cast to upper case.
Parameters
----------
codon
The inputted codon.
Returns
-------
The normalised codon.
"""
seq = seq.upper().replace(' ', '')
seq = seq.replace('T', 'U') if self.rna else seq.replace('U', 'T')
regex = r'^[ACGU]*$' if self.rna else r'^[ACGT]*$'
if not re.match(regex, seq):
raise ValueError('Sequence to normalise must be a nucleotide sequence.')
else:
return seq
[docs]
def translate(self, seq: str) -> str:
"""
Translate a DNA/RNA coding sequence into its amino-acid sequence.
"""
if len(seq) % 3 != 0:
raise ValueError('Sequence length must be a multiple of 3')
seq = self.normalise_sequence(seq)
return ''.join(self.codons_to_aa[seq[i:i + 3]] for i in range(0, len(seq), 3))
@staticmethod
def _load_tables() -> dict:
"""
Load tables from stored JSON entries.
"""
path = Path(__file__).parent / 'data' / 'tables.json'
with path.open() as f:
return json.load(f)