"""Base classes and protocols for mzPAF components"""
from abc import ABC, abstractmethod
from collections import Counter
from tacular import ElementInfo
[docs]
class Serializable(ABC):
"""Base class for serializable objects"""
[docs]
@abstractmethod
def serialize(self) -> str:
pass
def __str__(self) -> str:
return self.serialize()
[docs]
class MassProvider(ABC):
"""Base class for objects that can provide mass"""
[docs]
@abstractmethod
def mass(self, monoisotopic: bool = True) -> float:
"""Calculate mass"""
pass
@property
def monoisotopic_mass(self) -> float:
return self.mass(monoisotopic=True)
@property
def average_mass(self) -> float:
return self.mass(monoisotopic=False)
[docs]
class CompositionProvider(ABC):
"""Base class for objects that can provide composition"""
@property
@abstractmethod
def composition(self) -> Counter["ElementInfo"]:
"""Get elemental composition"""
pass
@property
def dict_composition(self) -> dict[str, int]:
"""Get composition as a dictionary with element symbols as keys"""
return {str(elem): count for elem, count in self.composition.items()}
[docs]
def mass(self, monoisotopic: bool = True) -> float:
"""Calculate mass from composition"""
m = 0.0
for elem, count in self.composition.items():
m += elem.get_mass(monoisotopic) * count
return m
[docs]
class ScalableComposition(CompositionProvider):
"""Mixin for compositions that scale by count and sign"""
count: int
@property
@abstractmethod
def _single_composition(self) -> Counter["ElementInfo"]:
"""Get composition for single instance (before scaling)"""
pass
@property
def composition(self) -> Counter["ElementInfo"]:
"""Get scaled composition"""
return Counter({elem: count * self.count for elem, count in self._single_composition.items()})
@property
def _sign_prefix(self) -> str:
"""Get sign prefix for serialization"""
sign_str = "+" if self.count > 0 else "-"
count_str = "" if abs(self.count) == 1 else str(abs(self.count))
return f"{sign_str}{count_str}"