Source code for synkit.Graph.ITS.its_construction

import networkx as nx
from typing import Tuple, Dict, Any, Optional, List, Hashable
from copy import deepcopy


[docs] class ITSConstruction: """ Utility class for constructing an ITS graph from two input graphs. Nodes store paired state information through the ``typesGH`` attribute. Edges store direct paired attributes such as ``order=(g, h)`` without an edge-level ``typesGH``. The main public entry point is :meth:`construct`. """ CORE_NODE_DEFAULTS: Dict[str, Any] = { "element": "*", "charge": 0, "atom_map": 0, "hcount": 0, "aromatic": False, "neighbors": lambda: ["", ""], "partial_charge": 0, "hybridization": "", "lone_pairs": 0, "radical": 0, "valence_electrons": 0, } CORE_EDGE_DEFAULTS: Dict[str, Any] = { "order": 0.0, "kekule_order": 0.0, "sigma_order": 0.0, "pi_order": 0.0, "ez_isomer": "", "bond_type": "", "conjugated": False, "in_ring": False, } @staticmethod def _resolve_defaults( user_defaults: Optional[Dict[str, Any]], core_defaults: Dict[str, Any] ) -> Dict[str, Any]: """ Merge user-provided defaults with built-in defaults. :param user_defaults: Optional mapping of user overrides. :type user_defaults: Optional[Dict[str, Any]] :param core_defaults: Built-in defaults. Callable values are treated as factories. :type core_defaults: Dict[str, Any] :returns: Resolved defaults with fresh copies for mutable values. :rtype: Dict[str, Any] """ resolved: Dict[str, Any] = {} user_defaults = user_defaults or {} for key, core_val in core_defaults.items(): if key in user_defaults: resolved[key] = deepcopy(user_defaults[key]) elif callable(core_val): resolved[key] = core_val() else: resolved[key] = deepcopy(core_val) return resolved @staticmethod def _compute_standard_order( its: nx.Graph, ignore_aromaticity: bool = False ) -> None: """ Compute ``standard_order`` for each edge from ``order=(g, h)``. :param its: ITS graph whose edges contain an ``order`` tuple. :type its: nx.Graph :param ignore_aromaticity: If ``True``, absolute differences smaller than ``1`` are set to ``0``. :type ignore_aromaticity: bool """ for u, v, data in its.edges(data=True): order_tuple = data.get("order", (0.0, 0.0)) try: o_g, o_h = order_tuple except Exception: o_g, o_h = 0.0, 0.0 standard_order = o_g - o_h if ignore_aromaticity and abs(standard_order) < 1: standard_order = 0 its[u][v]["standard_order"] = standard_order @staticmethod def _select_base_graph(G: nx.Graph, H: nx.Graph, balance_its: bool) -> nx.Graph: """ Select the base graph used to initialize the ITS graph. :param G: First input graph. :type G: nx.Graph :param H: Second input graph. :type H: nx.Graph :param balance_its: If ``True``, prefer the smaller graph; otherwise prefer the larger. :type balance_its: bool :returns: Selected base graph. :rtype: nx.Graph """ if (balance_its and len(G.nodes) <= len(H.nodes)) or ( not balance_its and len(G.nodes) >= len(H.nodes) ): return G return H @staticmethod def _initialize_its(base: nx.Graph) -> nx.Graph: """ Deep-copy the base graph and remove all edges. :param base: Graph chosen as ITS initialization template. :type base: nx.Graph :returns: Edge-free copy of the base graph. :rtype: nx.Graph """ its = deepcopy(base) its.remove_edges_from(list(its.edges())) return its @staticmethod def _ensure_union_nodes(its: nx.Graph, G: nx.Graph, H: nx.Graph) -> None: """ Ensure the ITS graph contains the union of nodes from both input graphs. :param its: ITS graph to update in place. :type its: nx.Graph :param G: First input graph. :type G: nx.Graph :param H: Second input graph. :type H: nx.Graph """ all_nodes = set(G.nodes()) | set(H.nodes()) for n in all_nodes: if n in its: continue source_attrs: Dict[str, Any] = {} if n in G: source_attrs = deepcopy(G.nodes[n]) elif n in H: source_attrs = deepcopy(H.nodes[n]) its.add_node(n, **source_attrs) @staticmethod def _build_node_side_tuple( graph: nx.Graph, node: Hashable, attrs: List[str], defaults: Dict[str, Any], ) -> Tuple[Any, ...]: """ Build one side of a node tuple for ``typesGH``. :param graph: Source graph. :type graph: nx.Graph :param node: Node identifier. :type node: Hashable :param attrs: Ordered node attributes. :type attrs: List[str] :param defaults: Default values for missing attributes. :type defaults: Dict[str, Any] :returns: Attribute tuple for the requested node. :rtype: Tuple[Any, ...] """ if node not in graph: return tuple(defaults.get(attr) for attr in attrs) return tuple(graph.nodes[node].get(attr, defaults.get(attr)) for attr in attrs) @staticmethod def _populate_node_attributes( its: nx.Graph, G: nx.Graph, H: nx.Graph, node_attrs: List[str], node_defaults: Dict[str, Any], store: bool, ) -> None: """ Populate node-level ``typesGH`` and per-attribute node storage. :param its: ITS graph to update in place. :type its: nx.Graph :param G: First input graph. :type G: nx.Graph :param H: Second input graph. :type H: nx.Graph :param node_attrs: Ordered node attributes included in ``typesGH``. :type node_attrs: List[str] :param node_defaults: Default values for missing node attributes. :type node_defaults: Dict[str, Any] :param store: If ``True``, store per-attribute ``(G, H)`` tuples. Otherwise store only the ``G``-side value. :type store: bool """ for n in its.nodes(): g_tuple = ITSConstruction._build_node_side_tuple( G, n, node_attrs, node_defaults ) h_tuple = ITSConstruction._build_node_side_tuple( H, n, node_attrs, node_defaults ) its.nodes[n]["typesGH"] = (g_tuple, h_tuple) its.nodes[n]["present"] = (n in G, n in H) for i, attr in enumerate(node_attrs): its.nodes[n][attr] = (g_tuple[i], h_tuple[i]) if store else g_tuple[i] @staticmethod def _edge_keys(G: nx.Graph, H: nx.Graph) -> List[Tuple[Hashable, Hashable]]: """ Compute the union of undirected edges from ``G`` and ``H``. :param G: First input graph. :type G: nx.Graph :param H: Second input graph. :type H: nx.Graph :returns: List of unique edge pairs. :rtype: List[Tuple[Hashable, Hashable]] """ edge_keys = {frozenset((u, v)) for u, v in G.edges()} | { frozenset((u, v)) for u, v in H.edges() } return [tuple(fs) for fs in edge_keys] @staticmethod def _build_edge_pair_data( G: nx.Graph, H: nx.Graph, u: Hashable, v: Hashable, edge_attrs: List[str], edge_defaults: Dict[str, Any], ) -> Dict[str, Any]: """ Build direct paired edge attributes for one ITS edge. Each requested edge attribute is stored as ``(G_value, H_value)``. The ``order`` attribute is always guaranteed to exist. :param G: First input graph. :type G: nx.Graph :param H: Second input graph. :type H: nx.Graph :param u: First edge endpoint. :type u: Hashable :param v: Second edge endpoint. :type v: Hashable :param edge_attrs: Edge attributes to store as paired tuples. :type edge_attrs: List[str] :param edge_defaults: Default values for missing edge attributes. :type edge_defaults: Dict[str, Any] :returns: Edge attribute mapping for ITS storage. :rtype: Dict[str, Any] """ g_edge = G[u][v] if G.has_edge(u, v) else {} h_edge = H[u][v] if H.has_edge(u, v) else {} edge_data: Dict[str, Any] = {} for attr in edge_attrs: default = edge_defaults.get(attr) g_val = g_edge.get(attr, default) h_val = h_edge.get(attr, default) edge_data[attr] = (g_val, h_val) if "order" not in edge_data: g_order = g_edge.get("order", edge_defaults.get("order", 0.0)) h_order = h_edge.get("order", edge_defaults.get("order", 0.0)) edge_data["order"] = (g_order, h_order) if "kekule_order" in edge_data: g_order = g_edge.get("kekule_order", edge_defaults.get("order", 0.0)) h_order = h_edge.get("kekule_order", edge_defaults.get("order", 0.0)) edge_data["kekule_order"] = (g_order, h_order) return edge_data @staticmethod def _populate_edge_attributes( its: nx.Graph, G: nx.Graph, H: nx.Graph, edge_attrs: List[str], edge_defaults: Dict[str, Any], ) -> None: """ Populate ITS edges with direct paired edge attributes. :param its: ITS graph to update in place. :type its: nx.Graph :param G: First input graph. :type G: nx.Graph :param H: Second input graph. :type H: nx.Graph :param edge_attrs: Edge attributes to store as ``(G, H)`` tuples. :type edge_attrs: List[str] :param edge_defaults: Default values for missing edge attributes. :type edge_defaults: Dict[str, Any] """ for u, v in ITSConstruction._edge_keys(G, H): edge_data = ITSConstruction._build_edge_pair_data( G, H, u, v, edge_attrs, edge_defaults ) its.add_edge(u, v, **edge_data)
[docs] @staticmethod def construct( G: nx.Graph, H: nx.Graph, *, ignore_aromaticity: bool = False, balance_its: bool = True, store: bool = True, node_attrs: Optional[List[str]] = None, edge_attrs: Optional[List[str]] = None, attributes_defaults: Optional[Dict[str, Any]] = None, ) -> nx.Graph: """ Construct an ITS graph from two input graphs. Nodes store ``typesGH`` as paired tuples over ``node_attrs``. Requested edge attributes are stored directly as paired values such as ``order=(g, h)`` and ``bond_type=(g, h)``. No edge-level ``typesGH`` is created. :param G: First input graph, typically the reactant-side graph. :type G: nx.Graph :param H: Second input graph, typically the product-side graph. :type H: nx.Graph :param ignore_aromaticity: If ``True``, bond-order differences with absolute value smaller than ``1`` are treated as zero when computing ``standard_order``. :type ignore_aromaticity: bool :param balance_its: If ``True``, initialize from the smaller graph; otherwise from the larger. :type balance_its: bool :param store: Controls node attribute storage only. If ``True``, node attributes are stored as ``(G, H)`` tuples. If ``False``, only the ``G``-side value is stored. Edge attributes are always stored as paired tuples. :type store: bool :param node_attrs: Ordered list of node attributes included in node-level ``typesGH``. :type node_attrs: Optional[List[str]] :param edge_attrs: Ordered list of edge attributes stored directly as ``(G, H)`` tuples. :type edge_attrs: Optional[List[str]] :param attributes_defaults: Optional overrides for node attribute defaults. :type attributes_defaults: Optional[Dict[str, Any]] :returns: ITS graph with merged nodes, paired node/edge annotations, and derived ``standard_order``. :rtype: nx.Graph Example ------- .. code-block:: python node_attrs = [ "element", "aromatic", "hcount", "charge", "neighbors", "hybridization", "atom_map", "lone_pairs", ] edge_attrs = [ "kekule_order", "order", "bond_type", "conjugated", "in_ring", ] its = ITSConstruction.construct( r_graph, p_graph, node_attrs=node_attrs, edge_attrs=edge_attrs, store=True, ) print(its.edges[12, 30]["order"]) print(its.edges[12, 30]["bond_type"]) print(its.edges[12, 30]["standard_order"]) """ node_attrs = node_attrs or [ "element", "aromatic", "hcount", "charge", "neighbors", "lone_pairs", "radical", "valence_electrons", ] edge_attrs = edge_attrs or [ "order", "kekule_order", "sigma_order", "pi_order", ] node_defaults = ITSConstruction._resolve_defaults( attributes_defaults, ITSConstruction.CORE_NODE_DEFAULTS ) edge_defaults = ITSConstruction._resolve_defaults( None, ITSConstruction.CORE_EDGE_DEFAULTS ) base = ITSConstruction._select_base_graph(G, H, balance_its) its = ITSConstruction._initialize_its(base) ITSConstruction._ensure_union_nodes(its, G, H) ITSConstruction._populate_node_attributes( its, G, H, node_attrs, node_defaults, store ) ITSConstruction._populate_edge_attributes(its, G, H, edge_attrs, edge_defaults) ITSConstruction._compute_standard_order( its, ignore_aromaticity=ignore_aromaticity ) return its
[docs] @staticmethod def ITSGraph( G: nx.Graph, H: nx.Graph, ignore_aromaticity: bool = False, attributes_defaults: Optional[Dict[str, Any]] = None, balance_its: bool = False, store: bool = False, ) -> nx.Graph: """ Backward-compatible wrapper around :meth:`construct`. :param G: First input graph. :type G: nx.Graph :param H: Second input graph. :type H: nx.Graph :param ignore_aromaticity: If ``True``, small bond-order differences are ignored. :type ignore_aromaticity: bool :param attributes_defaults: Optional node defaults for missing values. :type attributes_defaults: Optional[Dict[str, Any]] :param balance_its: If ``True``, prefer the smaller graph as base. :type balance_its: bool :param store: If ``True``, node attributes are stored as paired tuples. :type store: bool :returns: Constructed ITS graph using legacy node and edge attribute defaults. :rtype: nx.Graph """ return ITSConstruction.construct( G, H, ignore_aromaticity=ignore_aromaticity, balance_its=balance_its, store=store, node_attrs=["element", "aromatic", "hcount", "charge", "neighbors"], edge_attrs=["order"], attributes_defaults=attributes_defaults, )
[docs] @staticmethod def typesGH_info( node_attrs: Optional[List[str]] = None, edge_attrs: Optional[List[str]] = None ) -> Dict[str, Dict[str, Tuple[type, Any]]]: """ Provide expected types and defaults for node and edge attributes. :param node_attrs: Node attributes expected in node-level ``typesGH``. :type node_attrs: Optional[List[str]] :param edge_attrs: Edge attributes expected as direct paired edge tuples. :type edge_attrs: Optional[List[str]] :returns: Nested mapping describing ``(type, default)`` for each selected attribute. :rtype: Dict[str, Dict[str, Tuple[type, Any]]] """ node_attrs = node_attrs or [ "element", "aromatic", "hcount", "charge", "neighbors", ] edge_attrs = edge_attrs or ["order"] node_prop_types: Dict[str, type] = { "element": str, "aromatic": bool, "hcount": int, "charge": int, "neighbors": list, } edge_prop_types: Dict[str, type] = { "order": float, "ez_isomer": str, "bond_type": str, "conjugated": bool, "in_ring": bool, } node_defaults = { attr: ( node_prop_types.get(attr, object), ( ITSConstruction.CORE_NODE_DEFAULTS.get(attr)() if callable(ITSConstruction.CORE_NODE_DEFAULTS.get(attr)) else ITSConstruction.CORE_NODE_DEFAULTS.get(attr) ), ) for attr in node_attrs } edge_defaults = { attr: ( edge_prop_types.get(attr, object), ITSConstruction.CORE_EDGE_DEFAULTS.get(attr), ) for attr in edge_attrs } return {"node": node_defaults, "edge": edge_defaults}
[docs] @staticmethod def get_node_attribute( graph: nx.Graph, node: Hashable, attribute: str, default: Any ) -> Any: """ Retrieve a node attribute or return a default if missing. :param graph: Input graph. :type graph: nx.Graph :param node: Node identifier. :type node: Hashable :param attribute: Attribute name. :type attribute: str :param default: Fallback value. :type default: Any :returns: Stored node attribute or fallback default. :rtype: Any """ try: return graph.nodes[node][attribute] except KeyError: return default
[docs] @staticmethod def get_node_attributes_with_defaults( graph: nx.Graph, node: Hashable, attributes_defaults: Dict[str, Any] = None ) -> Tuple: """ Retrieve multiple node attributes using provided defaults. :param graph: Input graph. :type graph: nx.Graph :param node: Node identifier. :type node: Hashable :param attributes_defaults: Mapping from attribute names to fallback values. :type attributes_defaults: Optional[Dict[str, Any]] :returns: Tuple of node attributes in mapping order. :rtype: Tuple Example ------- .. code-block:: python attrs = ITSConstruction.get_node_attributes_with_defaults( graph=G, node=1, attributes_defaults={ "element": "*", "aromatic": False, "hcount": 0, "charge": 0, "neighbors": ["", ""], }, ) """ if attributes_defaults is None: attributes_defaults = { "element": "*", "aromatic": False, "hcount": 0, "charge": 0, "neighbors": ["", ""], } return tuple( ITSConstruction.get_node_attribute(graph, node, attr, default) for attr, default in attributes_defaults.items() )