Source code for paftacular.comps.base

"""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}"