Source code for synkit.CRN.Petrinet.analyzer

from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Set, Tuple

import numpy as np

from .net import PetriNet
from .persistence import (
    PersistenceCheckResult,
    siphon_persistence_condition,
    siphon_persistence_details,
)
from .semiflows import find_p_semiflows, find_t_semiflows, stoichiometric_matrix
from .structure import find_siphons, find_traps


[docs] @dataclass class PetriSummary: """ Structured container summarizing Petri-style SynCRN diagnostics. :param p_semiflows: Numerical basis of P-semiflows, arranged as columns. :type p_semiflows: numpy.ndarray :param t_semiflows: Numerical basis of T-semiflows, arranged as columns. :type t_semiflows: numpy.ndarray :param siphons: Detected siphons as sets of place labels. :type siphons: List[Set[str]] :param traps: Detected traps as sets of place labels. :type traps: List[Set[str]] :param persistence_ok: Whether the tested siphon-based persistence condition is satisfied. :type persistence_ok: bool :param place_order: Place ordering associated with the Petri/stoichiometric representation. :type place_order: List[str] :param transition_order: Transition ordering associated with the Petri/stoichiometric representation. :type transition_order: List[str] """ p_semiflows: np.ndarray t_semiflows: np.ndarray siphons: List[Set[str]] traps: List[Set[str]] persistence_ok: bool place_order: List[str] transition_order: List[str]
[docs] class PetriAnalyzer: """ OOP wrapper for Petri-net style analysis on SynCRN-like inputs. Accepted inputs are canonical SynCRN objects, SynCRN bipartite digraphs, and :class:`PetriNet` objects. The analyzer caches computed results so repeated access to already computed diagnostics does not trigger recomputation. :param crn: SynCRN-like object or :class:`PetriNet`. :type crn: Any :param rtol: Relative tolerance used in numerical nullspace calculations. :type rtol: float :param max_siphon_size: Optional maximum siphon/trap size considered during enumeration. :type max_siphon_size: Optional[int] Examples -------- .. code-block:: python analyzer = PetriAnalyzer(crn, rtol=1e-12, max_siphon_size=4) analyzer.compute_all() print(analyzer.summary) print(analyzer.as_dict()) print(analyzer.explain()) """ def __init__( self, crn: Any, *, rtol: float = 1e-12, max_siphon_size: Optional[int] = None, ) -> None: """ Initialize the Petri analyzer. :param crn: SynCRN-like object or :class:`PetriNet`. :type crn: Any :param rtol: Relative tolerance used in numerical nullspace calculations. :type rtol: float :param max_siphon_size: Optional maximum siphon/trap size considered during enumeration. :type max_siphon_size: Optional[int] """ self._crn = crn self._petri = crn if isinstance(crn, PetriNet) else PetriNet.from_syncrn(crn) self._rtol = float(rtol) self._max_siphon_size = max_siphon_size self._p_semiflows: Optional[np.ndarray] = None self._t_semiflows: Optional[np.ndarray] = None self._siphons: Optional[List[Set[str]]] = None self._traps: Optional[List[Set[str]]] = None self._persistence_ok: Optional[bool] = None self._persistence_details: Optional[PersistenceCheckResult] = None @property def petri(self) -> PetriNet: """ Return the internal Petri-net representation. :returns: Internal Petri-net view used for structural analysis. :rtype: PetriNet """ return self._petri def _orders(self) -> Tuple[List[str], List[str]]: """ Return place and transition orders from the stoichiometric view. :returns: Tuple ``(place_order, transition_order)``. :rtype: Tuple[List[str], List[str]] """ places, transitions, _ = stoichiometric_matrix(self._crn) return places, transitions def _ensure_persistence_computed(self) -> bool: """ Check whether persistence has already been computed. :returns: ``True`` if persistence results are available. :rtype: bool """ return self._persistence_ok is not None def _persistence_details_as_dict(self) -> Optional[Dict[str, Any]]: """ Convert cached persistence details into a serializable dictionary. :returns: Dictionary form of persistence details, or ``None`` if unavailable. :rtype: Optional[Dict[str, Any]] """ if self._persistence_details is None: return None return { "persistence_ok": self._persistence_details.persistence_ok, "siphons": [sorted(x) for x in self._persistence_details.siphons], "semiflow_supports": [ sorted(x) for x in self._persistence_details.semiflow_supports ], "uncovered_siphons": [ sorted(x) for x in self._persistence_details.uncovered_siphons ], } def _summary_ready(self) -> bool: """ Check whether all ingredients required for :attr:`summary` are present. :returns: ``True`` if a full summary can be constructed. :rtype: bool """ return ( self._p_semiflows is not None and self._t_semiflows is not None and self._siphons is not None and self._traps is not None and self._persistence_ok is not None )
[docs] def compute_semiflows(self) -> "PetriAnalyzer": """ Compute and cache P-semiflows and T-semiflows. :returns: The analyzer itself, enabling method chaining. :rtype: PetriAnalyzer Examples -------- .. code-block:: python analyzer = PetriAnalyzer(crn).compute_semiflows() print(analyzer.p_semiflows) print(analyzer.t_semiflows) """ self._p_semiflows = find_p_semiflows(self._crn, rtol=self._rtol) self._t_semiflows = find_t_semiflows(self._crn, rtol=self._rtol) return self
[docs] def compute_siphons_traps(self) -> "PetriAnalyzer": """ Compute and cache siphons and traps. :returns: The analyzer itself, enabling method chaining. :rtype: PetriAnalyzer Examples -------- .. code-block:: python analyzer = PetriAnalyzer(crn).compute_siphons_traps() print(analyzer.siphons) print(analyzer.traps) """ self._siphons = find_siphons( self._petri, max_size=self._max_siphon_size, names="label" ) self._traps = find_traps( self._petri, max_size=self._max_siphon_size, names="label" ) return self
[docs] def check_persistence(self) -> "PetriAnalyzer": """ Evaluate and cache the siphon-based persistence condition. Both the boolean persistence condition and the detailed explanation structure are computed and stored. :returns: The analyzer itself, enabling method chaining. :rtype: PetriAnalyzer Examples -------- .. code-block:: python analyzer = PetriAnalyzer(crn).check_persistence() print(analyzer.persistence_ok) print(analyzer.persistence_details) """ self._persistence_ok = siphon_persistence_condition( self._crn, rtol=self._rtol, max_siphon_size=self._max_siphon_size, ) self._persistence_details = siphon_persistence_details( self._crn, rtol=self._rtol, max_siphon_size=self._max_siphon_size, ) return self
[docs] def compute_all(self) -> "PetriAnalyzer": """ Compute all supported Petri-style diagnostics. This is equivalent to calling :meth:`compute_semiflows`, :meth:`compute_siphons_traps`, and :meth:`check_persistence` in sequence. :returns: The analyzer itself, enabling method chaining. :rtype: PetriAnalyzer Examples -------- .. code-block:: python analyzer = PetriAnalyzer(crn).compute_all() print(analyzer.summary) """ return self.compute_semiflows().compute_siphons_traps().check_persistence()
@property def p_semiflows(self) -> Optional[np.ndarray]: """ Return cached P-semiflows. :returns: Cached P-semiflow basis, or ``None`` if not yet computed. :rtype: Optional[numpy.ndarray] """ return self._p_semiflows @property def t_semiflows(self) -> Optional[np.ndarray]: """ Return cached T-semiflows. :returns: Cached T-semiflow basis, or ``None`` if not yet computed. :rtype: Optional[numpy.ndarray] """ return self._t_semiflows @property def siphons(self) -> Optional[List[Set[str]]]: """ Return cached siphons. :returns: Cached siphons, or ``None`` if not yet computed. :rtype: Optional[List[Set[str]]] """ return self._siphons @property def traps(self) -> Optional[List[Set[str]]]: """ Return cached traps. :returns: Cached traps, or ``None`` if not yet computed. :rtype: Optional[List[Set[str]]] """ return self._traps @property def persistence_ok(self) -> Optional[bool]: """ Return cached persistence status. :returns: Cached persistence status, or ``None`` if not yet computed. :rtype: Optional[bool] """ return self._persistence_ok @property def persistence_details(self) -> Optional[PersistenceCheckResult]: """ Return cached detailed persistence analysis. :returns: Cached persistence detail object, or ``None`` if not yet computed. :rtype: Optional[PersistenceCheckResult] """ return self._persistence_details @property def summary(self) -> Optional[PetriSummary]: """ Return a structured summary if all diagnostics are available. :returns: A :class:`PetriSummary` if all components are computed, otherwise ``None``. :rtype: Optional[PetriSummary] """ if not self._summary_ready(): return None places, transitions = self._orders() return PetriSummary( p_semiflows=self._p_semiflows, t_semiflows=self._t_semiflows, siphons=self._siphons, traps=self._traps, persistence_ok=bool(self._persistence_ok), place_order=places, transition_order=transitions, )
[docs] def as_dict(self) -> Dict[str, Any]: """ Convert the current analyzer state into a serializable dictionary. :returns: Dictionary containing cached analysis results and metadata. :rtype: Dict[str, Any] Examples -------- .. code-block:: python analyzer = PetriAnalyzer(crn).compute_all() payload = analyzer.as_dict() print(payload["persistence_ok"]) """ places, transitions = self._orders() return { "place_order": places, "transition_order": transitions, "p_semiflows": ( None if self._p_semiflows is None else self._p_semiflows.tolist() ), "t_semiflows": ( None if self._t_semiflows is None else self._t_semiflows.tolist() ), "siphons": ( None if self._siphons is None else [sorted(x) for x in self._siphons] ), "traps": None if self._traps is None else [sorted(x) for x in self._traps], "persistence_ok": self._persistence_ok, "persistence_details": self._persistence_details_as_dict(), }
[docs] def explain(self) -> str: """ Return a compact human-readable explanation of current results. :returns: Summary string describing persistence and the number of computed objects, or a message indicating that no computation has been run. :rtype: str Examples -------- .. code-block:: python analyzer = PetriAnalyzer(crn).compute_all() print(analyzer.explain()) """ if not self._ensure_persistence_computed(): return ( "No Petri computations performed yet. " "Call compute_all() or individual compute_* methods." ) n_siph = 0 if self._siphons is None else len(self._siphons) n_trap = 0 if self._traps is None else len(self._traps) kp = 0 if self._p_semiflows is None else self._p_semiflows.shape[1] kt = 0 if self._t_semiflows is None else self._t_semiflows.shape[1] return ( f"persistence_ok={self._persistence_ok}, " f"p_semiflows={kp}, t_semiflows={kt}, " f"siphons={n_siph}, traps={n_trap}" )
def __repr__(self) -> str: """ Return a concise developer-facing representation. :returns: String representation of the analyzer status. :rtype: str """ status = ( "NA" if self._persistence_ok is None else ("True" if self._persistence_ok else "False") ) return f"<PetriAnalyzer persistence_ok={status}>"