Source code for synkit.Graph.ITS.its_builder

import networkx as nx
from copy import deepcopy


[docs] class ITSBuilder: """Build and annotate an Imaginary Transition State (ITS) graph from a base graph and a reaction-center (RC) graph. :cvar None: This class only provides static methods and does not maintain state. """
[docs] @staticmethod def update_atom_map(graph: nx.Graph) -> None: """Reset and renumber the 'atom_map' attribute of every node to match its node index. :param graph: The graph whose nodes will be renumbered. :type graph: nx.Graph :returns: None :rtype: NoneType :example: >>> G = nx.Graph() >>> G.add_node(5) >>> ITSBuilder.update_atom_map(G) >>> G.nodes[5]['atom_map'] 5 """ for node in graph.nodes(): graph.nodes[node]["atom_map"] = node
[docs] @staticmethod def ITSGraph(G: nx.Graph, RC: nx.Graph) -> nx.Graph: """Create an ITS graph by merging attributes from a reaction-center graph (RC) into a copy of the base graph G and initializing transition- state metadata. The returned ITS graph will have: 1. A deep copy of G’s nodes and edges. 2. A new node attribute 'typesGH' storing G‑side and H‑side element/aromaticity/etc. 3. Edge attributes: - 'order': tuple of the original order replicated for G and H. - 'standard_order': initialized to 0.0. 4. All node and edge attributes from RC grafted onto corresponding nodes/edges in the copy of G, matched by RC’s 'atom_map' values. 5. A final renumbering of 'atom_map' to each node’s index. :param G: The original molecular graph representing either reactants or products. :type G: nx.Graph :param RC: The reaction-center graph containing updated atom and bond changes. :type RC: nx.Graph :returns: A new graph representing the ITS, with merged and initialized attributes. :rtype: nx.Graph :raises KeyError: If a required attribute is missing from G or RC during merging. :example: >>> from synkit.Graph.ITS.its_construction import ITSConstruction >>> base = nx.Graph() >>> # ... populate base with 'atom_map' and other attrs ... >>> rc = ITSConstruction().ITSGraph(base, some_other_graph) >>> its = ITSBuilder.ITSGraph(base, rc) >>> isinstance(its, nx.Graph) True """ # 1) Copy base graph its = deepcopy(G) # 2) Initialize 'typesGH' for each node for node, attrs in its.nodes(data=True): common = ( attrs.get("element", "*"), attrs.get("aromatic", False), attrs.get("hcount", 0), attrs.get("charge", 0), attrs.get("neighbors", []), ) its.nodes[node]["typesGH"] = (common, common) # 3) Initialize edge orders and standard_order for u, v, edge_attrs in its.edges(data=True): order = edge_attrs.get("order", 1.0) its[u][v]["order"] = (order, order) its[u][v]["standard_order"] = 0.0 # 4) Build mapping from RC atom_map to its node index in G atom_map_to_node = { attrs["atom_map"]: node for node, attrs in G.nodes(data=True) if attrs.get("atom_map", 0) != 0 } # 5) Merge node attributes from RC for rc_node, rc_attrs in RC.nodes(data=True): amap = rc_attrs.get("atom_map") target = atom_map_to_node.get(amap) if target is not None: its.nodes[target].update(rc_attrs) # 6) Merge or add edges from RC for u_rc, v_rc, rc_edge_attrs in RC.edges(data=True): u_map = RC.nodes[u_rc].get("atom_map", u_rc) v_map = RC.nodes[v_rc].get("atom_map", v_rc) u_target = atom_map_to_node.get(u_map) v_target = atom_map_to_node.get(v_map) if u_target is None or v_target is None: continue if its.has_edge(u_target, v_target): its[u_target][v_target].update(rc_edge_attrs) else: its.add_edge(u_target, v_target, **rc_edge_attrs) # 7) Renumber atom_map to node indices ITSBuilder.update_atom_map(its) return its