from typing import Any, Dict, Optional, Tuple, Union
from codeine.translation.tables import TranslationTable
from codeine.utils.dict import FrozenDict
WeightDict = Dict[str, Dict[str, Union[float, int]]]
[docs]
class CodonWeights:
"""
A class to store codon weights, for example codon usage information for a
specific organism.
Input weights are grouped by amino acid:
.. code-block:: python
{
'A': {'GCT': 1.0, 'GCC': 1.0, ...},
'R': {'CGT': 1.0, 'CGC': 1.0, ...},
...
}
Stored weights are flat:
.. code-block:: python
{
'GCT': 0.25,
'GCC': 0.25,
...
}
Weights are normalised per amino acid. Weights can be inputted as either
RNA or DNA, they will be normalised later.
"""
def __init__(
self,
weights: WeightDict,
) -> None:
"""
Parameters
----------
weights
Codon weights grouped by amino acid.
Example:
.. code-block:: python
{
'A': {'GCT': 1.0, 'GCC': 1.0, 'GCA': 1.0, 'GCG': 1.0},
'R': {'CGT': 1.0, 'CGC': 1.0, 'CGA': 1.0,
'CGG': 1.0, 'AGA': 1.0, 'AGG': 1.0},
...
}
"""
self._locked = False
weights_flat: Dict[str, float] = {}
aa_to_codons: Dict[str, Tuple[str, ...]] = {}
observed_codons = set()
for aa, codon_weights in weights.items():
aa = aa.upper()
if aa in aa_to_codons:
raise ValueError(f'Duplicate weights for amino acid {aa}')
normalised_codon_weights: Dict[str, Union[float, int]] = {}
for codon, weight in codon_weights.items():
codon = codon.upper()
if codon in normalised_codon_weights:
raise ValueError(f'Duplicate weight for codon {codon} for amino acid {aa}')
if codon in observed_codons:
raise ValueError(f'Duplicate weight for codon {codon}')
if weight < 0:
raise ValueError(f'Weight for codon {codon} cannot be negative')
normalised_codon_weights[codon] = weight
observed_codons.add(codon)
total = sum(normalised_codon_weights.values())
if total <= 0:
raise ValueError(f'Weights for amino acid {aa} must sum to > 0')
codons = tuple(normalised_codon_weights.keys())
for codon, weight in normalised_codon_weights.items():
weights_flat[codon] = float(weight) / total
aa_to_codons[aa] = codons
self.aa_to_codons = FrozenDict(aa_to_codons)
self.weights = FrozenDict(weights_flat)
self._locked = True
def __setattr__(self, name: str, value: Any) -> None:
if getattr(self, '_locked', False):
raise AttributeError(f'{type(self).__name__} is immutable')
super().__setattr__(name, value)
def __repr__(self) -> str:
lines = [
'CodonWeights',
'',
'Weights:',
]
for aa in sorted(self.aa_to_codons):
weights = ' '.join(
f'{codon}={self.weights[codon]:.3f}'
for codon in self.aa_to_codons[aa]
)
lines.append(f' {aa}: {weights}')
return '\n'.join(lines)
def __getitem__(self, codon: str) -> float:
return self.weights[codon]
[docs]
def for_table(self, table: TranslationTable) -> 'CodonWeights':
"""
Resolve this CodonWeights class against a translation table, i.e. check that the
codons match and normalise to RNA/DNA, and return the resolved version.
Parameters
----------
table
The translation table to resolve against.
Returns
-------
A resolved ``CodonWeights`` object.
"""
expected_aas = set(table.aa_to_codons.keys())
observed_aas = set(self.aa_to_codons.keys())
missing_aas = expected_aas - observed_aas
if missing_aas:
raise ValueError(f'Missing weights for amino acid(s): {sorted(missing_aas)}')
extra_aas = observed_aas - expected_aas
if extra_aas:
raise ValueError(f'Unknown amino acid(s): {sorted(extra_aas)}')
resolved_weights: WeightDict = {}
resolved_codons = set()
changed = False
for aa, codons in self.aa_to_codons.items():
codon_weights: Dict[str, float] = {}
for codon in codons:
resolved_codon = table.normalise_sequence(codon)
if resolved_codon != codon:
changed = True
if resolved_codon in resolved_codons:
raise ValueError(f'Duplicate weight for codon {resolved_codon}')
resolved_codons.add(resolved_codon)
codon_weights[resolved_codon] = self.weights[codon]
expected_codons = set(table.aa_to_codons[aa])
observed_codons = set(codon_weights.keys())
missing_codons = expected_codons - observed_codons
if missing_codons:
raise ValueError(f'Missing weights for codon(s) for amino acid {aa}: {sorted(missing_codons)}')
extra_codons = observed_codons - expected_codons
if extra_codons:
raise ValueError(f'Unknown codon(s) for amino acid {aa}: {sorted(extra_codons)}')
resolved_weights[aa] = codon_weights
if not changed:
return self
return type(self)(resolved_weights)
[docs]
def restrict(
self,
table: TranslationTable,
) -> Tuple[TranslationTable, 'CodonWeights']:
"""
Return compatible translation table and codon weights containing only codons
with positive weight.
The weights are first resolved against the supplied translation table. Codons with zero
weight are then removed from both the table and the returned weights.
Parameters
----------
table
The translation table to restrict.
Returns
-------
TranslationTable and CodonWeights
A matched translation table and codon weights containing only codons with positive weight.
"""
weights = self.for_table(table)
omitted_codons = [codon for codon in table.codons_to_aa if weights[codon] == 0]
if not omitted_codons:
return table, weights
restricted_codons_to_aa = {
codon: aa
for codon, aa in table.codons_to_aa.items()
if weights[codon] > 0
}
restricted_weights: WeightDict = {
aa: {
codon: weights[codon]
for codon in codons
if weights[codon] > 0
}
for aa, codons in table.aa_to_codons.items()
}
if len(omitted_codons) <= 6:
omitted = ', '.join(sorted(omitted_codons))
name = f'{table.name} (omitting {omitted})'
else:
name = f'{table.name} (omitting {len(omitted_codons)} zero-weight codons)'
restricted_table = TranslationTable.custom(
codons_to_aa=restricted_codons_to_aa,
name=name,
rna=table.rna,
)
return restricted_table, type(self)(restricted_weights)
[docs]
def threshold(self, min_weight: float) -> 'CodonWeights':
"""
Set codon weights below a minimum value to zero.
The threshold is applied to the normalised (fractional) weight of each codon within
its amino acid. Remaining non-zero weights are then renormalised.
Parameters
----------
min_weight
Minimum codon weight to retain, between 0 and 1.
Returns
-------
A new ``CodonWeights`` object with weights below the threshold set to
zero. If no weights are changed, return this object unchanged.
"""
if min_weight < 0 or min_weight > 1:
raise ValueError('min_weight must be between 0 and 1.')
thresholded_weights: WeightDict = {}
changed = False
for aa, codons in self.aa_to_codons.items():
codon_weights = {}
for codon in codons:
weight = self.weights[codon]
if weight < min_weight:
weight = 0.0
changed = True
codon_weights[codon] = weight
thresholded_weights[aa] = codon_weights
if not changed:
return self
return type(self)(thresholded_weights)
[docs]
def by_aa(self, aa: str) -> Dict[str, float]:
"""
Return the codon weights corresponding to a particular AA.
Parameters
----------
aa
The amino acid of interest.
Returns
-------
A set of codon weights keyed by codon.
"""
codons = self.aa_to_codons[aa.upper()]
weights = {codon: self.weights[codon] for codon in codons}
return weights
[docs]
@classmethod
def ecoli(cls) -> 'CodonWeights':
"""
Construct a ``CodonWeights`` object with codon probabilities corresponding to E. coli.
Weights are based on codon usage counts from GenScript:
https://www.genscript.com/tools/codon-frequency-table
Returns
-------
A ``CodonWeights`` object corresponding to E. Coli
"""
from codeine.translation.data.weights import ECOLI_WEIGHTS
return cls(ECOLI_WEIGHTS)
[docs]
@classmethod
def yeast(cls) -> 'CodonWeights':
"""
Construct a ``CodonWeights`` object with codon probabilities corresponding to 'yeast'.
Weights are based on codon usage counts from GenScript:
https://www.genscript.com/tools/codon-frequency-table
Returns
-------
A ``CodonWeights`` object corresponding to S. cerevisiea
"""
from codeine.translation.data.weights import YEAST_WEIGHTS
return cls(YEAST_WEIGHTS)
[docs]
@classmethod
def human(cls) -> 'CodonWeights':
"""
Construct a ``CodonWeights`` object with codon probabilities corresponding to Human.
Weights are based on codon usage counts from GenScript:
https://www.genscript.com/tools/codon-frequency-table
Returns
-------
A ``CodonWeights`` object corresponding to Human.
"""
from codeine.translation.data.weights import HUMAN_WEIGHTS
return cls(HUMAN_WEIGHTS)
[docs]
@classmethod
def mouse(cls) -> 'CodonWeights':
"""
Construct a ``CodonWeights`` object with codon probabilities corresponding to Mouse.
Weights are based on codon usage counts from GenScript:
https://www.genscript.com/tools/codon-frequency-table
Returns
-------
A ``CodonWeights`` object corresponding to Mouse.
"""
from codeine.translation.data.weights import MOUSE_WEIGHTS
return cls(MOUSE_WEIGHTS)
[docs]
@classmethod
def arabidopsis(cls) -> 'CodonWeights':
"""
Construct a ``CodonWeights`` object with codon probabilities corresponding to A. thaliana.
Weights are based on codon usage counts from GenScript:
https://www.genscript.com/tools/codon-frequency-table
Returns
-------
A ``CodonWeights`` object corresponding to Arabidopsis thaliana.
"""
from codeine.translation.data.weights import ARABIDOPSIS_WEIGHTS
return cls(ARABIDOPSIS_WEIGHTS)
[docs]
@classmethod
def drosophila(cls) -> 'CodonWeights':
"""
Construct a ``CodonWeights`` object with codon probabilities corresponding to D. melanogaster.
Weights are based on codon usage counts from GenScript:
https://www.genscript.com/tools/codon-frequency-table
Returns
-------
A ``CodonWeights`` object corresponding to Drosophila melanogaster.
"""
from codeine.translation.data.weights import DROSOPHILA_WEIGHTS
return cls(DROSOPHILA_WEIGHTS)