Source code for synkit.CRN.Symmetry.canon

from __future__ import annotations

from typing import Any, Dict, List, Optional, Set

import networkx as nx

from ._common import CanonicalResult, SymmetryConfig
from ._ir import IRCanonicalEngine
from .wl_canon import WLCanonicalizer


[docs] class CRNCanonicalizer: """ Exact canonicalizer and symmetry analyzer backed by one shared IR engine. This is the preferred high-level entry point when a chemical reaction network or related graph-like source needs both: - a canonical form - exact automorphism information - orbit information for symmetric nodes The class combines a fast Weisfeiler--Lehman (WL) based pre-analysis with an exact IR-based canonicalization backend. The underlying exact engine is shared across methods so intermediate work can be reused. :param source: Input object to canonicalize. This is typically a SynCRN-like object or a graph representation accepted by the internal canonicalization engine. :type source: Any :param include_rule: Whether rule / reaction nodes should be included explicitly in the canonicalization model. :type include_rule: bool :param include_stoich: Whether stoichiometric information should be included in the canonical representation and symmetry analysis. :type include_stoich: bool :param wl_iters: Number of WL refinement iterations used by the approximate canonicalizer. :type wl_iters: int :param wl_digest_size: Digest size used for WL hashing. :type wl_digest_size: int :param config: Optional symmetry / semantic configuration. If ``None``, a semantic default configuration is used. :type config: Optional[SymmetryConfig] Example ------- .. code-block:: python canon = CRNCanonicalizer( syncrn, include_rule=True, include_stoich=True, ) key = canon.canonical_key() orbits = canon.orbits() has_symmetry = canon.has_nontrivial_automorphism() """ def __init__( self, source: Any, *, include_rule: bool = True, include_stoich: bool = True, wl_iters: int = 20, wl_digest_size: int = 16, config: Optional[SymmetryConfig] = None, ) -> None: """ Initialize the canonicalizer and its shared exact engine. :param source: Input object to canonicalize. :type source: Any :param include_rule: Whether rule / reaction nodes are included in the internal model. :type include_rule: bool :param include_stoich: Whether stoichiometric information is included. :type include_stoich: bool :param wl_iters: Number of WL refinement iterations. :type wl_iters: int :param wl_digest_size: Digest size used in WL hashing. :type wl_digest_size: int :param config: Optional symmetry configuration. :type config: Optional[SymmetryConfig] :returns: None :rtype: None """ self.config = config or SymmetryConfig.topological() self.wl = WLCanonicalizer( source, include_rule=include_rule, include_stoich=include_stoich, n_iter=wl_iters, digest_size=wl_digest_size, config=self.config, ) self._engine = IRCanonicalEngine( source, include_rule=include_rule, include_stoich=include_stoich, wl_iters=wl_iters, wl_digest_size=wl_digest_size, config=self.config, ) @property def G(self) -> nx.DiGraph: """ Return the internal directed graph used by the exact engine. :returns: Internal graph representation. :rtype: nx.DiGraph """ return self._engine.G @property def graph_type(self) -> str: """ Return a string describing the interpreted graph type. :returns: Graph type label reported by the engine. :rtype: str """ return self._engine.graph_type @property def engine(self) -> IRCanonicalEngine: """ Return the shared exact IR canonicalization engine. :returns: Exact canonicalization / symmetry engine. :rtype: IRCanonicalEngine """ return self._engine
[docs] def canonical_result( self, *, timeout_sec: Optional[float] = None ) -> CanonicalResult: """ Compute or retrieve the exact canonicalization result. This method delegates to the shared exact engine and returns the full canonicalization result object, which typically includes the canonical order, canonical key, and timing information. :param timeout_sec: Optional timeout in seconds for the exact canonicalization search. If ``None``, the engine default behavior is used. :type timeout_sec: Optional[float] :returns: Exact canonicalization result. :rtype: CanonicalResult Example ------- .. code-block:: python res = canon.canonical_result(timeout_sec=5.0) print(res.canonical_order) print(res.canonical_key) """ return self._engine.canonical_result(timeout_sec=timeout_sec)
[docs] def canonical_order(self, *, timeout_sec: Optional[float] = None) -> List[Any]: """ Return the exact canonical node order. :param timeout_sec: Optional timeout in seconds. :type timeout_sec: Optional[float] :returns: Canonical ordering of nodes. :rtype: List[Any] """ return self.canonical_result(timeout_sec=timeout_sec).canonical_order
[docs] def canonical_graph(self, *, timeout_sec: Optional[float] = None) -> nx.DiGraph: """ Return a canonically relabeled graph. Nodes are relabeled according to the exact canonical order using consecutive integer labels starting from 1. :param timeout_sec: Optional timeout in seconds. :type timeout_sec: Optional[float] :returns: Canonically relabeled copy of the internal graph. :rtype: nx.DiGraph Example ------- .. code-block:: python g_canon = canon.canonical_graph() print(g_canon.nodes()) """ order = self.canonical_order(timeout_sec=timeout_sec) relabel = {v: i + 1 for i, v in enumerate(order)} return nx.relabel_nodes(self.G, relabel, copy=True)
[docs] def canonical_key(self, *, timeout_sec: Optional[float] = None): """ Return the exact canonical key. The exact type depends on the underlying canonical engine. :param timeout_sec: Optional timeout in seconds. :type timeout_sec: Optional[float] :returns: Canonical key representing the isomorphism class of the source. """ return self.canonical_result(timeout_sec=timeout_sec).canonical_key
[docs] def has_nontrivial_automorphism( self, *, timeout_sec: Optional[float] = 5.0 ) -> bool: """ Test whether the source has a nontrivial automorphism. A fast WL orbit partition is used as an early filter. If all WL orbits are singletons, the method immediately returns ``False``. Otherwise an exact search is performed and the source is considered symmetric if the automorphism count is greater than 1. :param timeout_sec: Timeout in seconds for the exact symmetry check. :type timeout_sec: Optional[float] :returns: ``True`` if a non-identity automorphism exists, else ``False``. :rtype: bool Example ------- .. code-block:: python if canon.has_nontrivial_automorphism(): print("Symmetry detected") """ if all(len(cell) == 1 for cell in self.wl.orbits()): return False return ( self._engine.run( max_count=2, timeout_sec=timeout_sec, stop_after_two=True ).automorphism_count > 1 )
[docs] def automorphism_result( self, *, max_count: int = 100, timeout_sec: Optional[float] = 5.0, ): """ Return the exact automorphism analysis result. The returned object depends on the internal engine and usually contains automorphism count, sample permutations, sample mappings, orbit information, and timing metadata. :param max_count: Maximum number of automorphisms to enumerate or track. :type max_count: int :param timeout_sec: Timeout in seconds for the exact search. :type timeout_sec: Optional[float] :returns: Exact automorphism analysis result from the engine. """ return self._engine.automorphism_result( max_count=max_count, timeout_sec=timeout_sec )
[docs] def orbits( self, *, max_count: int = 1000, timeout_sec: Optional[float] = 5.0, ) -> List[Set[Any]]: """ Return exact node orbits under the automorphism group. Nodes are in the same orbit if an automorphism can map one node to the other. :param max_count: Maximum number of automorphisms considered during the exact search. :type max_count: int :param timeout_sec: Timeout in seconds for the exact search. :type timeout_sec: Optional[float] :returns: List of exact orbit sets. :rtype: List[Set[Any]] Example ------- .. code-block:: python for orbit in canon.orbits(): print(sorted(orbit)) """ return list( self._engine.run(max_count=max_count, timeout_sec=timeout_sec).orbits )
[docs] def wl_orbits(self) -> List[Set[Any]]: """ Return WL-refined approximate orbit classes. These are not guaranteed to equal the exact automorphism orbits, but they are often useful as a fast symmetry approximation or as a filter before running exact search. :returns: Approximate orbit partition from WL refinement. :rtype: List[Set[Any]] """ return self.wl.orbits()
[docs] def summary( self, *, max_count: int = 100, timeout_sec: Optional[float] = 5.0, include_automorphisms: bool = True, ) -> Dict[str, Any]: """ Return a summary dictionary for canonicalization and symmetry analysis. If ``include_automorphisms`` is ``True``, the summary includes exact automorphism information, sample permutations, orbit data, and early-stop metadata. Otherwise only canonicalization data is returned. :param max_count: Maximum number of automorphisms to enumerate or track. :type max_count: int :param timeout_sec: Timeout in seconds for the exact search. :type timeout_sec: Optional[float] :param include_automorphisms: Whether to include exact automorphism-related information. :type include_automorphisms: bool :returns: Summary dictionary containing canonical and optionally symmetry information. :rtype: Dict[str, Any] Example ------- .. code-block:: python info = canon.summary(include_automorphisms=True) print(info["canonical_key"]) print(info["automorphism_count"]) """ if include_automorphisms: res = self._engine.run(max_count=max_count, timeout_sec=timeout_sec) relabel = {v: i + 1 for i, v in enumerate(res.canonical_order)} return { "graph_type": self.graph_type, "canonical_perm": res.canonical_order, "canonical_key": res.canonical_key, "canon_graph": nx.relabel_nodes(self.G, relabel, copy=True), "automorphism_count": res.automorphism_count, "sample_permutations": res.sample_permutations, "mappings": res.sample_mappings, "orbits": res.orbits, "early_stop": res.stopped_early, "elapsed_seconds": res.elapsed_seconds, } cres = self.canonical_result(timeout_sec=timeout_sec) relabel = {v: i + 1 for i, v in enumerate(cres.canonical_order)} return { "graph_type": self.graph_type, "canonical_perm": cres.canonical_order, "canonical_key": cres.canonical_key, "canon_graph": nx.relabel_nodes(self.G, relabel, copy=True), "elapsed_seconds": cres.elapsed_seconds, }
[docs] def canonical(source: Any, **kwargs: Any) -> nx.DiGraph: """ Return the canonically relabeled graph for a source object. This is a convenience wrapper around :class:`CRNCanonicalizer`. :param source: Input object to canonicalize. :type source: Any :param kwargs: Additional keyword arguments forwarded to :class:`CRNCanonicalizer`. :type kwargs: Any :returns: Canonically relabeled directed graph. :rtype: nx.DiGraph Example ------- .. code-block:: python g_canon = canonical( syncrn, include_rule=True, include_stoich=True, ) """ return CRNCanonicalizer(source, **kwargs).canonical_graph()