"""Base classes and protocols for mzPAF components"""
from abc import ABC, abstractmethod
from collections import Counter
from dataclasses import fields, is_dataclass
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()
def __reduce__(self):
"""Reconstruct dataclass components through their normal constructors."""
if not is_dataclass(self):
raise TypeError("Component reconstruction requires a dataclass")
return _rebuild, (type(self), {field.name: getattr(self, field.name) for field in fields(self) if field.init})
def _rebuild(cls: type, kwargs: dict[str, object]) -> object:
"""Pickle helper: call the constructor by keyword so kw-only fields round trip."""
return cls(**kwargs)
[docs]
class MassProvider(ABC):
"""Base class for objects that can provide mass"""
[docs]
@abstractmethod
def get_mass(self, *, monoisotopic: bool = True) -> float:
"""Monoisotopic (default) or average mass in Da."""
@property
def monoisotopic_mass(self) -> float:
return self.get_mass(monoisotopic=True)
@property
def average_mass(self) -> float:
return self.get_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"""
[docs]
def get_mass(self, *, monoisotopic: bool = True) -> float:
"""Calculate mass from composition"""
return sum(elem.get_mass(monoisotopic=monoisotopic) * count for elem, count in self.composition.items())
[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)"""
@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}"