from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Callable, Dict, Hashable, Iterator, List, Optional, Tuple
def _clean_counts(counts: Dict[str, int] | None = None) -> Dict[str, int]:
"""
Normalize a species-to-coefficient mapping.
:param counts: Raw mapping from species id to stoichiometric coefficient.
:type counts: Dict[str, int] | None
:returns: Cleaned mapping with strictly positive integer coefficients.
:rtype: Dict[str, int]
"""
out: Dict[str, int] = {}
if not counts:
return out
for species_id, coeff in counts.items():
if not isinstance(species_id, str):
raise TypeError(f"Species ids must be str, got {type(species_id).__name__}")
if not isinstance(coeff, int):
raise TypeError(
f"Stoichiometric coefficient for {species_id!r} must be int"
)
if coeff < 0:
raise ValueError(
f"Stoichiometric coefficient for {species_id!r} must be >= 0"
)
if coeff > 0:
out[species_id] = coeff
return dict(sorted(out.items(), key=lambda kv: kv[0]))
[docs]
@dataclass
class RXNSide:
"""
Stoichiometric multiset for one reaction side.
:param counts: Mapping ``species_id -> coefficient``.
:type counts: Dict[str, int]
"""
counts: Dict[str, int] = field(default_factory=dict)
def __post_init__(self) -> None:
self.counts = _clean_counts(self.counts)
def __bool__(self) -> bool:
return bool(self.counts)
def __len__(self) -> int:
return len(self.counts)
def __iter__(self) -> Iterator[Tuple[str, int]]:
return iter(self.counts.items())
[docs]
def items(self) -> List[Tuple[str, int]]:
"""
Return the side as a list of ``(species_id, coeff)`` pairs.
:returns: Side entries.
:rtype: List[Tuple[str, int]]
"""
return list(self.counts.items())
[docs]
def get(self, species_id: str, default: int = 0) -> int:
"""
Get the coefficient for one species.
:param species_id: Internal species id.
:type species_id: str
:param default: Value to return when species is absent.
:type default: int
:returns: Stoichiometric coefficient.
:rtype: int
"""
return self.counts.get(species_id, default)
[docs]
def to_dict(self) -> Dict[str, int]:
"""
Return a JSON-like dictionary representation.
:returns: Species-to-coefficient mapping.
:rtype: Dict[str, int]
"""
return dict(self.counts)
[docs]
@dataclass
class Reaction:
"""
Canonical concrete reaction instance.
:param id: Stable internal reaction id such as ``r_1``.
:type id: str
:param source_node_id: Original reaction-node id in the source graph.
:type source_node_id: Hashable
:param source_kind: Original source node kind, often ``"rule"``.
:type source_kind: str
:param lhs: Reactant multiset.
:type lhs: RXNSide
:param rhs: Product multiset.
:type rhs: RXNSide
:param label: Optional reaction label.
:type label: Optional[str]
:param step: Optional expansion or generation step.
:type step: Optional[int]
:param rule_index: Optional original rule index.
:type rule_index: Optional[int]
:param app_index: Optional application index.
:type app_index: Optional[int]
:param rule_repr: Optional original rule/template string.
:type rule_repr: Optional[str]
:param rule_id: Optional associated abstract rule id.
:type rule_id: Optional[str]
:param source_attrs: Exact original node attributes from the source graph.
:type source_attrs: Dict[str, Any]
:param metadata: Extra canonical metadata.
:type metadata: Dict[str, Any]
:param reactant_edge_attrs: Original edge attributes for reactant arcs,
keyed by internal species id.
:type reactant_edge_attrs: Dict[str, Dict[str, Any]]
:param product_edge_attrs: Original edge attributes for product arcs,
keyed by internal species id.
:type product_edge_attrs: Dict[str, Dict[str, Any]]
"""
id: str
source_node_id: Hashable
source_kind: str
lhs: RXNSide
rhs: RXNSide
label: Optional[str] = None
step: Optional[int] = None
rule_index: Optional[int] = None
app_index: Optional[int] = None
rule_repr: Optional[str] = None
rule_id: Optional[str] = None
source_attrs: Dict[str, Any] = field(default_factory=dict)
metadata: Dict[str, Any] = field(default_factory=dict)
reactant_edge_attrs: Dict[str, Dict[str, Any]] = field(default_factory=dict)
product_edge_attrs: Dict[str, Dict[str, Any]] = field(default_factory=dict)
[docs]
def to_dict(self) -> Dict[str, Any]:
"""
Return a JSON-like dictionary representation.
:returns: Reaction as a dictionary.
:rtype: Dict[str, Any]
"""
return {
"id": self.id,
"source_node_id": self.source_node_id,
"source_kind": self.source_kind,
"label": self.label,
"lhs": self.lhs.to_dict(),
"rhs": self.rhs.to_dict(),
"step": self.step,
"rule_index": self.rule_index,
"app_index": self.app_index,
"rule_repr": self.rule_repr,
"rule_id": self.rule_id,
"source_attrs": dict(self.source_attrs),
"metadata": dict(self.metadata),
"reactant_edge_attrs": {
k: dict(v) for k, v in self.reactant_edge_attrs.items()
},
"product_edge_attrs": {
k: dict(v) for k, v in self.product_edge_attrs.items()
},
}