# import mod import molfoundry as mf import networkx as nx import random as rd # from mod import BondType from graph import Graph from prettify import header, blue, green, red, warn, bold, underline from typing import Dict, List class Reaction: ''' Can either call constructor with Reaction(leftGraph, rightGraph) or Reaction(rule) (where rule is type mod.Rule) ''' def __init__(self, rule=None, leftGraph=None, rightGraph=None, isMinimal=False, name="no name", context=0): self._name: str = rule.name if rule else name if leftGraph and rightGraph: self._leftGraph = leftGraph self._rightGraph = rightGraph else: self._leftGraph = Graph(rule, left=True) self._rightGraph = Graph(rule, left=False) self._educts = self._leftGraph.connected_components self._products = self._rightGraph.connected_components self._mod_rule = rule if rule else self.get_mod_rule() self._is_minimal: bool = isMinimal self._maximum_subrule: Reaction = self self._minimum_subrule: Reaction = self if isMinimal else self.set_minimum_subrule(add_context=int(context)) self._shells: Reaction = {} self._gml_string = self.to_ruleGML_string() self._mod_rule_with_core = mf.Rule.fromGMLString(self._gml_string) # check that all connected components are used in the reaction core def check_if_proper(self): core_left = self._minimum_subrule.left_graph.nx_graph def get_mod_rule(self): return mf.Rule.fromGMLString(self.to_ruleGML_string(get_from_mod=False)) def to_ruleGML_string(self, get_from_mod=False): if get_from_mod: return self._mod_rule.getGMLString() leftGraph = self._leftGraph.nx_graph rightGraph = self._rightGraph.nx_graph # NODES label_changing_nodes = [node for node in leftGraph.nodes if node in rightGraph.nodes and \ leftGraph.nodes[node]['label']!=rightGraph.nodes[node]['label']] shared_nodes = [node for node in leftGraph.nodes if node in rightGraph.nodes and node not in label_changing_nodes] only_right_nodes = [node for node in rightGraph.nodes if node not in shared_nodes and node not in label_changing_nodes] only_left_nodes = [node for node in leftGraph.nodes if node not in shared_nodes and node not in label_changing_nodes] # EDGES shared_edges = [] for (u, v, attrs1) in leftGraph.edges(data=True): for (p,q,attrs2) in rightGraph.edges(data=True): if {u,v}=={p,q} and attrs1['bond']==attrs2['bond']: shared_edges.append({u,v}) shared_edges_list = [list(s) for s in shared_edges] bond_changing_edges = [] for (u,v,attrs) in leftGraph.edges(data=True): for (p,q,attrs2) in rightGraph.edges(data=True): if {u,v}=={p,q} and attrs['bond']!=attrs2['bond']: bond_changing_edges.append({u,v}) bond_changing_edges_list = [list(s) for s in bond_changing_edges] only_left_edges = [] for (u,v) in leftGraph.edges: if {u,v} not in shared_edges and {u,v} not in bond_changing_edges: only_left_edges.append({u,v}) only_left_edges_list = [list(s) for s in only_left_edges] only_right_edges = [] for (u,v) in rightGraph.edges: if {u,v} not in shared_edges and {u,v} not in bond_changing_edges: only_right_edges.append({u,v}) only_right_edges_list = [list(s) for s in only_right_edges] out = [] out.append("rule [") out.append(f'\truleID "{self._name}"') out.append("\tleft [") out.extend([f"\t\tnode [ id {leftGraph.nodes[node]['modID']} label \"{leftGraph.nodes[node]['label']}\" ]" for node in only_left_nodes]) out.extend([f"\t\tnode [ id {leftGraph.nodes[node]['modID']} label \"{leftGraph.nodes[node]['label']}\" ]" for node in label_changing_nodes]) out.extend([f"\t\tedge [ source {leftGraph.nodes[u]['modID']} target {leftGraph.nodes[v]['modID']} label \"{leftGraph.edges[u,v]['bond']}\" ]" for [u,v] in only_left_edges_list]) out.extend([f"\t\tedge [ source {leftGraph.nodes[u]['modID']} target {leftGraph.nodes[v]['modID']} label \"{leftGraph.edges[u,v]['bond']}\" ]" for [u,v] in bond_changing_edges_list]) out.append("\t]") out.append("\tcontext [") out.extend([f"\t\tnode [ id {leftGraph.nodes[node]['modID']} label \"{leftGraph.nodes[node]['label']}\" ]" for node in shared_nodes]) # could take from left or right, arbitrarily chose leftGraph out.extend([f"\t\tedge [ source {leftGraph.nodes[u]['modID']} target {leftGraph.nodes[v]['modID']} label \"{leftGraph.edges[u,v]['bond']}\" ]" for [u,v] in shared_edges_list]) out.append("\t]") out.append("\tright [") out.extend([f"\t\tnode [ id {rightGraph.nodes[node]['modID']} label \"{rightGraph.nodes[node]['label']}\" ]" for node in only_right_nodes]) out.extend([f"\t\tnode [ id {rightGraph.nodes[node]['modID']} label \"{rightGraph.nodes[node]['label']}\" ]" for node in label_changing_nodes]) out.extend([f"\t\tedge [ source {rightGraph.nodes[u]['modID']} target {rightGraph.nodes[v]['modID']} label \"{rightGraph.edges[u,v]['bond']}\" ]" for [u,v] in only_right_edges_list]) out.extend([f"\t\tedge [ source {rightGraph.nodes[u]['modID']} target {rightGraph.nodes[v]['modID']} label \"{rightGraph.edges[u,v]['bond']}\" ]" for [u,v] in bond_changing_edges_list]) out.append("\t]") out.append("]") return "\n".join(out) def set_minimum_subrule(self, add_context=0): core_left_nodes = [] core_right_nodes = [] left_graph = self._leftGraph.nx_graph left_nodes = list(left_graph.nodes) right_graph = self._rightGraph.nx_graph right_nodes = list(right_graph.nodes) # label changing nodes left_labels = { v : left_graph.nodes[v]['label'] for v in left_nodes } right_labels = { v : right_graph.nodes[v]['label'] for v in right_nodes } for v in set(left_nodes+right_nodes): if v in left_labels and v in right_labels and right_labels[v]!=left_labels[v]: core_left_nodes.append((v, left_graph.nodes[v])) core_right_nodes.append((v, right_graph.nodes[v])) # node in leftGraph, not in rightGraph for v in left_nodes: if v not in right_nodes: core_left_nodes.append((v, left_graph.nodes[v])) # node in rightGraph, not in leftGraph for v in right_nodes: if v not in left_nodes: core_right_nodes.append((v, right_graph.nodes[v])) # edge in leftGraph, no edge in rightGraph core_left_edges = [] for u,v,attrs in left_graph.edges(data=True): try: right_graph[u][v] except KeyError: core_left_edges.append((u,v,attrs)) u_tuple = (u, left_graph.nodes[u]) v_tuple = (v, left_graph.nodes[v]) if u_tuple not in core_left_nodes: core_left_nodes.append(u_tuple) if v_tuple not in core_left_nodes: core_left_nodes.append(v_tuple) # edge in rightGraph, no edge in left graph core_right_edges = [] for u,v,attrs in right_graph.edges(data=True): try: left_graph[u][v] # bond type changes; both left and right must have these nodes if left_graph[u][v]['bond'] != right_graph[u][v]['bond']: core_left_edges.append((u,v,left_graph[u][v])) core_right_edges.append((u,v,attrs)) left_u = (u, left_graph.nodes[u]) left_v = (v, left_graph.nodes[v]) right_u = (u, right_graph.nodes[u]) right_v = (v, right_graph.nodes[v]) if left_u not in core_left_nodes: core_left_nodes.append(left_u) if left_v not in core_left_nodes: core_left_nodes.append(left_v) if right_u not in core_right_nodes: core_right_nodes.append(right_u) if right_v not in core_right_nodes: core_right_nodes.append(right_v) except KeyError: core_right_edges.append((u,v,attrs)) u_tuple = (u, right_graph.nodes[u]) v_tuple = (v, right_graph.nodes[v]) if u_tuple not in core_right_nodes: core_right_nodes.append(u_tuple) if v_tuple not in core_right_nodes: core_right_nodes.append(v_tuple) # nothing changes if core_left_nodes==[] and core_right_nodes==[] and not core_left_edges and not core_right_edges: print(red(f"Cannot set minimum subrule for reaction {self._name} as no bonds change.")) return False # ADDING EXTRA NODES FOR CONTEXT while add_context>0: added=False int_nodes = [v for v,_ in core_left_nodes] for v in int_nodes: if left_graph.degree[v] > 1: nbs = list(left_graph.neighbors(v)) for neighbour in nbs: if neighbour not in int_nodes: # print(blue(f"added context: {left_graph.nodes[neighbour]} | {right_graph.nodes[neighbour]}")) core_left_nodes.append((neighbour, {'label':left_graph.nodes[neighbour]['label'], 'modID':left_graph.nodes[neighbour]['modID']})) core_right_nodes.append((neighbour, {'label':right_graph.nodes[neighbour]['label'], 'modID':right_graph.nodes[neighbour]['modID']})) add_context-=1 added=True if added==False: break core_left_graph = nx.Graph() core_left_graph.add_nodes_from(core_left_nodes) core_left_graph.add_edges_from(core_left_edges) core_right_graph = nx.Graph() core_right_graph.add_nodes_from(core_right_nodes) core_right_graph.add_edges_from(core_right_edges) new_left_graph = Graph(core_left_graph) new_right_graph = Graph(core_right_graph) # if parent reaction does not synthesize nor fragment, minimum subrule should not either # aka. minimum subrule should stay connected if parent reaction is connected # solution: take the shortest path between connected components '''if len(self._educts)==1 and len(self._products)==1 and (new_left_graph.num_components>1 or new_right_graph.num_components>1): comp_nodes = [] if new_left_graph.num_components > 1: ccps = new_left_graph.connected_components else: ccps = new_right_graph.connected_components for component in ccps: acceptable_nodes = [ n for n in list(component.nx_graph.nodes()) if (n in left_graph and n in right_graph) ] comp_nodes.append(acceptable_nodes[0]) tsp = nx.approximation.traveling_salesman_problem nodes_to_add = tsp(left_graph, nodes=comp_nodes, cycle=False) for n in nodes_to_add: core_left_graph.add_node(n, label=left_graph.nodes[n]['label'], modID=left_graph.nodes[n]['modID']) core_right_graph.add_node(n, label=right_graph.nodes[n]['label'], modID=right_graph.nodes[n]['modID']) for u in left_graph.neighbors(n): if u in core_left_graph.nodes(): core_left_graph.add_edge(u,n, bond=left_graph[u][n]['bond']) for u in right_graph.neighbors(n): if u in core_right_graph.nodes(): core_right_graph.add_edge(u,n, bond=right_graph[u][n]['bond']) new_left_graph = Graph(core_left_graph) new_right_graph = Graph(core_right_graph)''' # relabel core nodes in original graph for n in core_left_graph.nodes: for g in [self._leftGraph, core_left_graph]: label = g.nodes[n]['label'] if 'core' not in label: g.nodes[n]['label'] = f"{label}_core" for n in core_right_graph.nodes: for g in [self._rightGraph, core_right_graph]: label = g.nodes[n]['label'] if 'core' not in label: g.nodes[n]['label'] = f"{label}_core" return Reaction(leftGraph=new_left_graph, rightGraph=new_right_graph, isMinimal=True, name=(str(self._name)+"_min")) def copy(self, exact=False): #if exact: # new_left_graph = Graph(self._leftGraph.gml_string) # new_right_graph = Graph(self._rightGraph.gml_string) #else: new_left_graph = Graph(self._leftGraph.gml_string) new_right_graph = Graph(self._rightGraph.gml_string) return Reaction(leftGraph=new_left_graph, rightGraph=new_right_graph, name=self._name) def get_shell(self, k:int): if k<=0: return self._minimum_subrule try: reac_k = self._shells[k] return reac_k except KeyError: pass prev_shell = self.get_shell(k-1).copy() lgraph = prev_shell.left_graph.nx_graph rgraph = prev_shell.right_graph.nx_graph max_lgraph = self._leftGraph.nx_graph max_rgraph = self._rightGraph.nx_graph lnodes = list(lgraph.nodes) rnodes = list(rgraph.nodes) for n in lnodes: for neigh in list(max_lgraph.neighbors(n)): lgraph.add_node(neigh, label=max_lgraph.nodes[neigh]['label'], modID=max_lgraph.nodes[neigh]['modID']) lgraph.add_edge(n, neigh, bond=max_lgraph[n][neigh]['bond']) for n in rnodes: for neigh in list(max_rgraph.neighbors(n)): rgraph.add_node(neigh, label=max_rgraph.nodes[neigh]['label'], modID=max_rgraph.nodes[neigh]['modID']) rgraph.add_edge(n, neigh, bond=max_rgraph[n][neigh]['bond']) new_shell = Reaction(leftGraph=Graph(lgraph), rightGraph=Graph(rgraph), isMinimal=True, name=f"{self._name}_k{k}") self._shells[k] = new_shell return new_shell @property def size(self): return self.left_graph.nx_graph.number_of_nodes() @property def minimum_subrule(self): return self._minimum_subrule @property def maximum_subrule(self): return self._maximum_subrule @property def mod_rule(self): return self._mod_rule @property def educts(self): return self._educts @property def products(self): return self._products @property def left_graph(self): return self._leftGraph @property def right_graph(self): return self._rightGraph @property def name(self): return self._name @name.setter def name(self, val): self._name = val @property def gml(self): return self._gml_string @property def mod_rule_with_core(self): return self._mod_rule_with_core def is_minimal(self): return self._is_minimal def __eq__(self, other: 'Reaction') -> bool: return hash(self) == hash(other) def __lt__(self, other): return hash(self) < hash(other) def __le__(self, other): return hash(self) <= hash(other) def __gt__(self, other): return hash(self) > hash(other) def __ge__(self, other): return hash(self) >= hash(other) def __ne__(self, other): return not self==other def __str__(self) -> str: return self._name def __repr__(self) -> str: return self._name def __hash__(self) -> int: return (hash(self.left_graph)-hash(self.right_graph)) def _self_check_bond_counts(self): for nid in self._leftGraph.nx_graph.nodes: left_count = count_bonds(self._leftGraph.nx_graph, nid) right_count = count_bonds(self._rightGraph.nx_graph, nid) if left_count != right_count: print(red(f"BOND COUNT HAS CHANGED! from {left_count} to {right_count}")) def count_bonds(graph: nx.Graph, v_id): count = 0 for (u, v) in graph.edges([v_id]): bt = graph[u][v]['bond'] if bt == BondType.Single: count+=1 elif bt == BondType.Double: count+=2 elif bt == BondType.Triple: count+=3 elif bt == BondType.Aromatic: count+=1.5 else: print(red("BOND DETECTED OTHER THAN EXPECTED")) return count