from collections.abc import Iterator
from functools import cache, cached_property
from ..elements import ElementInfo
from .data import AMINO_ACID_INFOS, AminoAcid
from .dclass import AminoAcidInfo
[docs]
class AALookup:
def __init__(self, data: dict[AminoAcid, AminoAcidInfo]):
# Convert all keys to their string representation for one-letter codes
self.one_letter_to_info = {str(aa): info for aa, info in data.items()}
self.three_letter_to_info = {info.three_letter_code.lower(): info for info in data.values()}
self.name_to_info = {info.name.lower(): info for info in data.values()}
def _query_one_letter(self, code: str) -> AminoAcidInfo | None:
return self.one_letter_to_info.get(code.upper())
def _query_three_letter(self, code: str) -> AminoAcidInfo | None:
return self.three_letter_to_info.get(code.lower())
def _query_name(self, name: str) -> AminoAcidInfo | None:
return self.name_to_info.get(name.lower())
[docs]
@cache
def one_letter(self, code: str) -> AminoAcidInfo:
val = self._query_one_letter(code)
if val is not None:
return val
raise KeyError(f"Amino acid with one-letter code '{code}' not found.")
[docs]
@cache
def three_letter(self, code: str) -> AminoAcidInfo:
val = self._query_three_letter(code)
if val is not None:
return val
raise KeyError(f"Amino acid with three-letter code '{code}' not found.")
[docs]
@cache
def name(self, name: str) -> AminoAcidInfo:
val = self._query_name(name)
if val is not None:
return val
raise KeyError(f"Amino acid with name '{name}' not found.")
@cache
def __getitem__(self, key: str) -> AminoAcidInfo:
info = self._query_one_letter(key)
if info is not None:
return info
info = self._query_three_letter(key)
if info is not None:
return info
info = self._query_name(key)
if info is not None:
return info
raise KeyError(f"Amino acid '{key}' not found by one-letter code, three-letter code, or name.")
def __contains__(self, key: str) -> bool:
try:
_ = self[key]
return True
except KeyError:
return False
[docs]
@cached_property
def ordered_amino_acids(self) -> tuple[AminoAcidInfo, ...]:
"""Get amino acids in order of one-letter codes A-Z"""
return tuple(self.one_letter_to_info[aa] for aa in sorted(self.one_letter_to_info.keys()))
[docs]
@cached_property
def ambiguous_amino_acids(self) -> tuple[AminoAcidInfo, ...]:
"""Get ambiguous amino acids (B, J, X, Z)"""
return tuple(aa for aa in self.ordered_amino_acids if aa.is_ambiguous)
[docs]
@cached_property
def mass_amino_acids(self) -> tuple[AminoAcidInfo, ...]:
"""Get amino acids that have defined masses"""
return tuple(
aa for aa in self.ordered_amino_acids if aa.monoisotopic_mass is not None and aa.average_mass is not None
)
[docs]
@cached_property
def unambiguous_amino_acids(self) -> tuple[AminoAcidInfo, ...]:
"""Get unambiguous amino acids (all except B, J, X, Z)"""
return tuple(aa for aa in self.ordered_amino_acids if not aa.is_ambiguous)
[docs]
@cached_property
def mass_unambiguous_amino_acids(self) -> tuple[AminoAcidInfo, ...]:
"""Get unambiguous amino acids that have defined masses"""
return tuple(
aa
for aa in self.unambiguous_amino_acids
if aa.monoisotopic_mass is not None and aa.average_mass is not None
)
[docs]
def is_ambiguous(self, key: str) -> bool:
"""Check if the amino acid identified by key is ambiguous"""
aa_info = self[key]
return aa_info.is_ambiguous
[docs]
def is_mass_ambiguous(self, key: str) -> bool:
"""Check if the amino acid identified by key has mass ambiguity"""
aa_info = self[key]
return aa_info.is_mass_ambiguous
[docs]
def is_unambiguous(self, key: str) -> bool:
"""Check if the amino acid identified by key is unambiguous"""
aa_info = self[key]
return not aa_info.is_ambiguous
[docs]
def is_mass_unambiguous(self, key: str) -> bool:
"""Check if the amino acid identified by key has no mass ambiguity"""
aa_info = self[key]
return not aa_info.is_mass_ambiguous
[docs]
def mass(self, key: str, monoisotopic: bool = True) -> float:
"""Get the mass of the amino acid identified by key"""
aa_info = self[key]
if monoisotopic:
if aa_info.monoisotopic_mass is None:
raise ValueError(f"Amino acid '{key}' does not have a defined monoisotopic mass.")
return aa_info.monoisotopic_mass
else:
if aa_info.average_mass is None:
raise ValueError(f"Amino acid '{key}' does not have a defined average mass.")
return aa_info.average_mass
[docs]
def composition(self, key: str) -> dict[ElementInfo, int]:
"""Get the elemental composition of the amino acid identified by key"""
aa_info = self[key]
if aa_info.composition is None:
raise ValueError(f"Amino acid '{key}' does not have a defined elemental composition.")
return aa_info.composition
def __iter__(self) -> Iterator[AminoAcidInfo]:
"""Iterator over all amino acids in order of one-letter codes A-Z"""
yield from self.ordered_amino_acids
[docs]
def get(self, key: str, default: AminoAcidInfo | None = None) -> AminoAcidInfo | None:
"""Get amino acid info or None if not found"""
try:
return self[key]
except KeyError:
return default
AA_LOOKUP = AALookup(AMINO_ACID_INFOS)
# prime the cache for all amino acids
for aa in AMINO_ACID_INFOS.keys():
AA_LOOKUP[aa]
# load cahced properties
_ = AA_LOOKUP.ordered_amino_acids
_ = AA_LOOKUP.ambiguous_amino_acids
_ = AA_LOOKUP.mass_amino_acids
_ = AA_LOOKUP.unambiguous_amino_acids
_ = AA_LOOKUP.mass_unambiguous_amino_acids
ORDERED_AMINO_ACIDS = [aa.id for aa in AA_LOOKUP.ordered_amino_acids]