Source code for tacular.elements.dclass

from dataclasses import dataclass


[docs] @dataclass(frozen=True, slots=True) class ElementInfo: """Represents an element or specific isotope with its properties. Attributes: number: Atomic number (number of protons) mass_number: Atomic mass number (protons + neutrons), None for non-specific element symbol: Element symbol (e.g., 'C', 'H', 'O') mass: Isotopic mass in Daltons abundance: Natural abundance as fraction (0.0-1.0), None for synthetic isotopes average_mass: Average atomic mass for the element is_monoisotopic: True if most abundant isotope, False if not, None if element is non-specific """ number: int mass_number: int | None symbol: str mass: float abundance: float | None average_mass: float is_monoisotopic: bool | None def __hash__(self) -> int: return hash(str(self)) def __eq__(self, other: object) -> bool: if isinstance(other, str): return str(self) == other if isinstance(other, ElementInfo): return (self.number, self.mass_number) == (other.number, other.mass_number) return NotImplemented def _hill_order_key(self) -> tuple[int, str, int, int]: """Generate a sorting key for Hill ordering with isotope priorities""" # Hill ordering: C first, H second, then alphabetical if self.symbol == "C": hill_priority = 0 elif self.symbol == "H": hill_priority = 1 else: hill_priority = 2 # For same symbol: # 1. is_monoisotopic == None comes first if self.is_monoisotopic is None: mono_priority = 0 else: mono_priority = 1 # 2. Then sort by neutron count (lowest first) # If mass_number is None, treat neutron count as -1 (comes before 0) if self.mass_number is None: neutron = -1 else: neutron = self.mass_number - self.number return (hill_priority, self.symbol, mono_priority, neutron) def __lt__(self, other: object) -> bool: if not isinstance(other, ElementInfo): return NotImplemented return self._hill_order_key() < other._hill_order_key() def __le__(self, other: object) -> bool: if not isinstance(other, ElementInfo): return NotImplemented return self._hill_order_key() <= other._hill_order_key() def __gt__(self, other: object) -> bool: if not isinstance(other, ElementInfo): return NotImplemented return self._hill_order_key() > other._hill_order_key() def __ge__(self, other: object) -> bool: if not isinstance(other, ElementInfo): return NotImplemented return self._hill_order_key() >= other._hill_order_key() @property def neutron_count(self) -> int: """Calculate the number of neutrons in this isotope.""" if self.mass_number is None: raise ValueError("Mass number is None, cannot calculate neutron count") return self.mass_number - self.number @property def proton_count(self) -> int: """Return the number of protons (same as atomic number).""" return self.number @property def is_radioactive(self) -> bool: """Return True if this isotope is radioactive (zero natural abundance).""" return self.abundance == 0.0 def __str__(self) -> str: if self.mass_number is None: return f"{self.symbol}" return f"{self.mass_number}{self.symbol}"
[docs] def get_mass(self, monoisotopic: bool = True) -> float: """Get the mass of this element isotope. Args: monoisotopic: If True, return isotopic mass; if False, return average mass """ return self.mass if monoisotopic else self.average_mass
[docs] def to_dict(self, float_precision: int = 6) -> dict[str, object]: """Convert the ElementInfo to a dictionary. Args: float_precision: Number of decimal places for mass values """ return { "number": self.number, "symbol": self.symbol, "mass_number": self.mass_number, "mass": round(self.mass, float_precision), "abundance": self.abundance, "average_mass": round(self.average_mass, float_precision), }
def __repr__(self) -> str: return ( f"ElementInfo(number={self.number}, symbol={self.symbol}, mass_number={self.mass_number}, " f"mass={self.mass}, abundance={self.abundance}, average_mass={self.average_mass}, " f"is_monoisotopic={self.is_monoisotopic})" )
[docs] def update(self, **kwargs: object) -> "ElementInfo": """Return a new ElementInfo with updated fields. Args: **kwargs: Field names and new values to update """ # Since we use slots=True, we need to get fields manually current_values: dict[str, object] = { "number": self.number, "symbol": self.symbol, "mass_number": self.mass_number, "mass": self.mass, "abundance": self.abundance, "average_mass": self.average_mass, "is_monoisotopic": self.is_monoisotopic, } return self.__class__(**{**current_values, **kwargs}) # type: ignore
[docs] def serialize(self, count: int) -> str: """Serialize the ElementInfo to a ProForma formula element compatible string. Args: count: Number of atoms of this element """ if count == 0: raise ValueError("Count cannot be zero for serialization") if count == 1: if self.mass_number is not None: return f"[{str(self)}]" return str(self) else: if self.mass_number is not None: return f"[{str(self)}{count}]" return f"{self.symbol}{count}"