synkit 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. synkit/Chem/Fingerprint/__init__.py +0 -0
  2. synkit/Chem/Fingerprint/fp_calculator.py +122 -0
  3. synkit/Chem/Fingerprint/smiles_featurizer.py +185 -0
  4. synkit/Chem/Fingerprint/transformation_fp.py +79 -0
  5. synkit/Chem/Molecule/__init__.py +0 -0
  6. synkit/Chem/Molecule/standardize.py +137 -0
  7. synkit/Chem/Reaction/__init__.py +0 -0
  8. synkit/Chem/Reaction/balance_check.py +162 -0
  9. synkit/Chem/Reaction/cleanning.py +59 -0
  10. synkit/Chem/Reaction/deionize.py +289 -0
  11. synkit/Chem/Reaction/neutralize.py +256 -0
  12. synkit/Chem/Reaction/reagent.py +102 -0
  13. synkit/Chem/Reaction/standardize.py +157 -0
  14. synkit/Chem/Reaction/tautomerize.py +168 -0
  15. synkit/Graph/Cluster/__init__.py +0 -0
  16. synkit/Graph/Cluster/morphism.py +83 -0
  17. synkit/Graph/Feature/__init__.py +0 -0
  18. synkit/Graph/Feature/graph_descriptors.py +325 -0
  19. synkit/Graph/Feature/graph_fps.py +97 -0
  20. synkit/Graph/Feature/graph_signature.py +236 -0
  21. synkit/Graph/Feature/hash_fps.py +130 -0
  22. synkit/Graph/Feature/morgan_fps.py +87 -0
  23. synkit/Graph/Feature/path_fps.py +82 -0
  24. synkit/Graph/__init.py +0 -0
  25. synkit/IO/__init__.py +0 -0
  26. synkit/IO/chem_converter.py +231 -0
  27. synkit/IO/data_io.py +277 -0
  28. synkit/IO/data_process.py +49 -0
  29. synkit/IO/debug.py +78 -0
  30. synkit/IO/dg_to_gml.py +124 -0
  31. synkit/IO/gml_to_nx.py +119 -0
  32. synkit/IO/graph_to_mol.py +110 -0
  33. synkit/IO/mol_to_graph.py +282 -0
  34. synkit/IO/nx_to_gml.py +200 -0
  35. synkit/IO/parse_rule.py +172 -0
  36. synkit/IO/smiles_to_id.py +119 -0
  37. synkit/ITS/_misc.py +280 -0
  38. synkit/ITS/aam_validator.py +254 -0
  39. synkit/ITS/its_builder.py +94 -0
  40. synkit/ITS/its_construction.py +213 -0
  41. synkit/ITS/normalize_aam.py +183 -0
  42. synkit/ITS/partial_expand.py +170 -0
  43. synkit/Reactor/__init__.py +0 -0
  44. synkit/Reactor/core_engine.py +164 -0
  45. synkit/Reactor/inference.py +73 -0
  46. synkit/Reactor/multi_step.py +227 -0
  47. synkit/Reactor/multi_step_aam.py +82 -0
  48. synkit/Reactor/reagent.py +95 -0
  49. synkit/Reactor/rule_apply.py +81 -0
  50. synkit/Vis/__init__.py +0 -0
  51. synkit/Vis/chemical_graph_visualizer.py +378 -0
  52. synkit/Vis/chemical_reaction_visualizer.py +133 -0
  53. synkit/Vis/chemical_space.py +83 -0
  54. synkit/Vis/embedding.py +92 -0
  55. synkit/Vis/graph_visualizer.py +286 -0
  56. synkit/Vis/pdf_writer.py +143 -0
  57. synkit/Vis/rsmi_to_fig.py +169 -0
  58. synkit/__init__.py +0 -0
  59. synkit/_misc.py +181 -0
  60. synkit-0.0.1.dist-info/METADATA +148 -0
  61. synkit-0.0.1.dist-info/RECORD +63 -0
  62. synkit-0.0.1.dist-info/WHEEL +4 -0
  63. synkit-0.0.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,110 @@
1
+ import networkx as nx
2
+ from rdkit import Chem
3
+ from typing import Dict
4
+
5
+
6
+ class GraphToMol:
7
+ """
8
+ Converts a NetworkX graph representation of a molecule into an RDKit molecule object,
9
+ considering specific node and edge attributes for the construction of the molecule.
10
+ This includes handling different bond orders and optional hydrogen counts on nodes.
11
+ """
12
+
13
+ def __init__(
14
+ self,
15
+ node_attributes: Dict[str, str] = {
16
+ "element": "element",
17
+ "charge": "charge",
18
+ "atom_map": "atom_map",
19
+ },
20
+ edge_attributes: Dict[str, str] = {"order": "order"},
21
+ ):
22
+ """
23
+ Initializes the GraphToMol object with mappings for node and edge attributes.
24
+
25
+ Parameters:
26
+ - node_attributes (Dict[str, str]): Mapping of attribute names to node keys in the graph.
27
+ - edge_attributes (Dict[str, str]): Mapping of attribute names to edge keys in the graph.
28
+ """
29
+ self.node_attributes = node_attributes
30
+ self.edge_attributes = edge_attributes
31
+
32
+ def graph_to_mol(
33
+ self,
34
+ graph: nx.Graph,
35
+ ignore_bond_order: bool = False,
36
+ sanitize: bool = True,
37
+ use_h_count: bool = False,
38
+ ) -> Chem.Mol:
39
+ """
40
+ Converts a NetworkX graph into an RDKit molecule.
41
+
42
+ Parameters:
43
+ - graph (nx.Graph): The molecule graph.
44
+ - ignore_bond_order (bool): If True, all bonds are treated as single.
45
+ - sanitize (bool): If True, attempts to sanitize the molecule.
46
+ - use_h_count (bool): If True, adjusts hydrogen counts using the 'hcount' attribute.
47
+
48
+ Returns:
49
+ - Chem.Mol: An RDKit molecule object constructed from the graph.
50
+ """
51
+ mol = Chem.RWMol()
52
+ node_to_idx: Dict[int, int] = {}
53
+
54
+ for node, data in graph.nodes(data=True):
55
+ element = data.get(self.node_attributes["element"], "C")
56
+ charge = data.get(self.node_attributes["charge"], 0)
57
+ atom_map = (
58
+ data.get(self.node_attributes["atom_map"], 0)
59
+ if "atom_map" in data.keys()
60
+ else None
61
+ )
62
+ hcount = (
63
+ data.get("hcount", 0)
64
+ if use_h_count and "hcount" in data.keys()
65
+ else None
66
+ )
67
+
68
+ atom = Chem.Atom(element)
69
+ atom.SetFormalCharge(charge)
70
+ if atom_map is not None:
71
+ atom.SetAtomMapNum(atom_map)
72
+ if hcount is not None:
73
+ atom.SetNoImplicit(True)
74
+ atom.SetNumExplicitHs(hcount)
75
+
76
+ idx = mol.AddAtom(atom)
77
+ node_to_idx[node] = idx
78
+
79
+ for u, v, data in graph.edges(data=True):
80
+ bond_order = (
81
+ 1
82
+ if ignore_bond_order
83
+ else abs(data.get(self.edge_attributes["order"], 1))
84
+ )
85
+ bond_type = self.get_bond_type_from_order(bond_order)
86
+ mol.AddBond(node_to_idx[u], node_to_idx[v], bond_type)
87
+
88
+ if sanitize:
89
+ Chem.SanitizeMol(mol)
90
+
91
+ return mol
92
+
93
+ @staticmethod
94
+ def get_bond_type_from_order(order: float) -> Chem.BondType:
95
+ """
96
+ Converts a numerical bond order into the corresponding RDKit BondType.
97
+
98
+ Parameters:
99
+ - order (float): The bond order.
100
+
101
+ Returns:
102
+ - Chem.BondType: The corresponding RDKit bond type for the given order.
103
+ """
104
+ if order == 1:
105
+ return Chem.BondType.SINGLE
106
+ elif order == 2:
107
+ return Chem.BondType.DOUBLE
108
+ elif order == 3:
109
+ return Chem.BondType.TRIPLE
110
+ return Chem.BondType.AROMATIC
@@ -0,0 +1,282 @@
1
+ from rdkit import Chem
2
+ from rdkit.Chem import AllChem
3
+ import networkx as nx
4
+ from typing import Any, Dict, Optional
5
+ import random
6
+ from synkit.IO.debug import setup_logging
7
+
8
+ logger = setup_logging()
9
+
10
+
11
+ class MolToGraph:
12
+ """
13
+ A class for converting molecules from SMILES strings to graph representations using
14
+ RDKit and NetworkX. It supports creating both lightweight and detailed
15
+ graph representations with customizable atom and bond attributes,
16
+ allowing for exclusion of atoms without atom mapping numbers.
17
+ """
18
+
19
+ def __init__(self) -> None:
20
+ """
21
+ Initialize the MolToGraph class.
22
+ """
23
+ pass
24
+
25
+ @staticmethod
26
+ def add_partial_charges(mol: Chem.Mol) -> None:
27
+ """
28
+ Computes and assigns Gasteiger partial charges to each atom in the given molecule.
29
+
30
+ Parameters:
31
+ - mol (Chem.Mol): An RDKit molecule object.
32
+ """
33
+ try:
34
+ AllChem.ComputeGasteigerCharges(mol)
35
+ except Exception as e:
36
+ logger.error(f"Error computing Gasteiger charges: {e}")
37
+
38
+ @staticmethod
39
+ def get_stereochemistry(atom: Chem.Atom) -> str:
40
+ """
41
+ Determines the stereochemistry (R/S configuration) of a given atom.
42
+
43
+ Parameters:
44
+ - atom (Chem.Atom): An RDKit atom object.
45
+
46
+ Returns:
47
+ - str: The stereochemistry ('R', 'S', or 'N' for non-chiral).
48
+ """
49
+ chiral_tag = atom.GetChiralTag()
50
+ return (
51
+ "S"
52
+ if chiral_tag == Chem.ChiralType.CHI_TETRAHEDRAL_CCW
53
+ else "R" if chiral_tag == Chem.ChiralType.CHI_TETRAHEDRAL_CW else "N"
54
+ )
55
+
56
+ @staticmethod
57
+ def get_bond_stereochemistry(bond: Chem.Bond) -> str:
58
+ """
59
+ Determines the stereochemistry (E/Z configuration) of a given bond.
60
+
61
+ Parameters:
62
+ - bond (Chem.Bond): An RDKit bond object.
63
+
64
+ Returns:
65
+ - str: The stereochemistry ('E', 'Z', or 'N' for non-stereospecific
66
+ or non-double bonds).
67
+ """
68
+ if bond.GetBondType() != Chem.BondType.DOUBLE:
69
+ return "N"
70
+ stereo = bond.GetStereo()
71
+ if stereo == Chem.BondStereo.STEREOE:
72
+ return "E"
73
+ elif stereo == Chem.BondStereo.STEREOZ:
74
+ return "Z"
75
+ return "N"
76
+
77
+ @staticmethod
78
+ def has_atom_mapping(mol: Chem.Mol) -> bool:
79
+ """
80
+ Check if the given molecule has any atom mapping numbers.
81
+
82
+ Parameters:
83
+ - mol (Chem.Mol): An RDKit molecule object.
84
+
85
+ Returns:
86
+ - bool: True if any atom in the molecule has a mapping number, False otherwise.
87
+ """
88
+ return any(atom.HasProp("molAtomMapNumber") for atom in mol.GetAtoms())
89
+
90
+ @staticmethod
91
+ def random_atom_mapping(mol: Chem.Mol) -> Chem.Mol:
92
+ """
93
+ Assigns a random atom mapping number to each atom in the given molecule.
94
+
95
+ Parameters:
96
+ - mol (Chem.Mol): An RDKit molecule object.
97
+
98
+ Returns:
99
+ - Chem.Mol: The RDKit molecule object with random atom mapping numbers assigned.
100
+ """
101
+ atom_indices = list(range(1, mol.GetNumAtoms() + 1))
102
+ random.shuffle(atom_indices)
103
+ for atom, idx in zip(mol.GetAtoms(), atom_indices):
104
+ atom.SetProp("molAtomMapNumber", str(idx))
105
+ return mol
106
+
107
+ @classmethod
108
+ def mol_to_graph(
109
+ cls,
110
+ mol: Chem.Mol,
111
+ drop_non_aam: Optional[bool] = False,
112
+ light_weight: Optional[bool] = False,
113
+ use_index_as_atom_map: Optional[bool] = False,
114
+ ) -> nx.Graph:
115
+ """
116
+ Converts an RDKit molecule object to a NetworkX graph with specified atom and bond
117
+ attributes. Optionally excludes atoms without atom mapping numbers
118
+ if drop_non_aam is True.
119
+
120
+ Parameters:
121
+ - mol (Chem.Mol): An RDKit molecule object.
122
+ - drop_non_aam (bool, optional): If True, nodes without atom mapping numbers will
123
+ be dropped. This option is useful for focusing on labeled parts of a molecule.
124
+ - light_weight (bool, optional): If True, creates a graph with minimal attributes.
125
+ This option is useful for reducing memory footprint or simplifying the graph.
126
+ - use_index_as_atom_map (bool, optional): If True, uses the index of atoms as
127
+ atom map numbers, otherwise uses existing atom map numbers or indices if not set.
128
+
129
+ Raises:
130
+ - ValueError: If `drop_non_aam` and `use_index_as_atom_map` are not both True or
131
+ both False.
132
+
133
+ Returns:
134
+ - nx.Graph: A NetworkX graph representing the molecule.
135
+ """
136
+
137
+ if drop_non_aam and not use_index_as_atom_map:
138
+ raise ValueError(
139
+ "drop_non_aam and use_index_as_atom_map must be both False or both True."
140
+ )
141
+
142
+ if light_weight:
143
+ return cls._create_light_weight_graph(
144
+ mol, drop_non_aam, use_index_as_atom_map
145
+ )
146
+ else:
147
+ return cls._create_detailed_graph(mol, drop_non_aam, use_index_as_atom_map)
148
+
149
+ @classmethod
150
+ def _create_light_weight_graph(
151
+ cls,
152
+ mol: Chem.Mol,
153
+ drop_non_aam: bool = False,
154
+ use_index_as_atom_map: bool = False,
155
+ ) -> nx.Graph:
156
+ graph = nx.Graph()
157
+
158
+ for atom in mol.GetAtoms():
159
+ if use_index_as_atom_map:
160
+ # Use the atom map number if present; otherwise, use index + 1
161
+ atom_id = (
162
+ atom.GetAtomMapNum()
163
+ if atom.GetAtomMapNum() != 0
164
+ else atom.GetIdx() + 1
165
+ )
166
+ else:
167
+ # Always use index + 1
168
+ atom_id = atom.GetIdx() + 1
169
+
170
+ if drop_non_aam and atom.GetAtomMapNum() == 0:
171
+ continue # Skip atoms without atom map numbers if drop_non_aam is True
172
+
173
+ graph.add_node(
174
+ atom_id,
175
+ element=atom.GetSymbol(), # Store atom's element symbol
176
+ aromatic=atom.GetIsAromatic(),
177
+ hcount=atom.GetTotalNumHs(),
178
+ charge=atom.GetFormalCharge(),
179
+ neighbors=sorted(
180
+ neighbor.GetSymbol() for neighbor in atom.GetNeighbors()
181
+ ),
182
+ atom_map=atom.GetAtomMapNum(),
183
+ )
184
+
185
+ # Handle edges based on atom IDs and consistency checks
186
+ for bond in atom.GetBonds():
187
+ neighbor = bond.GetOtherAtom(atom)
188
+ if use_index_as_atom_map:
189
+ # Use the atom map number if present; otherwise, use index + 1
190
+ neighbor_id = (
191
+ neighbor.GetAtomMapNum()
192
+ if neighbor.GetAtomMapNum() != 0
193
+ else neighbor.GetIdx() + 1
194
+ )
195
+ else:
196
+ # Always use index + 1 for the neighbor
197
+ neighbor_id = neighbor.GetIdx() + 1
198
+
199
+ if not drop_non_aam or neighbor.GetAtomMapNum() != 0:
200
+ graph.add_edge(
201
+ atom_id, neighbor_id, order=bond.GetBondTypeAsDouble()
202
+ )
203
+
204
+ return graph
205
+
206
+ @classmethod
207
+ def _create_detailed_graph(
208
+ cls,
209
+ mol: Chem.Mol,
210
+ drop_non_aam: bool = True,
211
+ use_index_as_atom_map: bool = True,
212
+ ) -> nx.Graph:
213
+ cls.add_partial_charges(mol) # Compute charges if not already present
214
+ graph = nx.Graph()
215
+ index_to_id = {}
216
+
217
+ for atom in mol.GetAtoms():
218
+ if use_index_as_atom_map:
219
+ # Use the atom map number if present; otherwise, use index + 1
220
+ atom_id = (
221
+ atom.GetAtomMapNum()
222
+ if atom.GetAtomMapNum() != 0
223
+ else atom.GetIdx() + 1
224
+ )
225
+ else:
226
+ # Always use index + 1
227
+ atom_id = atom.GetIdx() + 1
228
+
229
+ if drop_non_aam and atom.GetAtomMapNum() == 0:
230
+ continue # Skip atoms without atom map numbers if drop_non_aam is True
231
+
232
+ props = cls._gather_atom_properties(atom)
233
+ index_to_id[atom.GetIdx()] = atom_id
234
+ graph.add_node(atom_id, **props)
235
+
236
+ for bond in mol.GetBonds():
237
+ begin_atom_id = index_to_id.get(bond.GetBeginAtomIdx())
238
+ end_atom_id = index_to_id.get(bond.GetEndAtomIdx())
239
+
240
+ if begin_atom_id and end_atom_id:
241
+ # Apply consistent ID handling for edges
242
+ graph.add_edge(
243
+ begin_atom_id, end_atom_id, **cls._gather_bond_properties(bond)
244
+ )
245
+
246
+ return graph
247
+
248
+ @staticmethod
249
+ def _gather_atom_properties(atom: Chem.Atom) -> Dict[str, Any]:
250
+ """Collect all relevant properties from an atom to use
251
+ as graph node attributes."""
252
+ gasteiger_charge = (
253
+ round(float(atom.GetProp("_GasteigerCharge")), 3)
254
+ if atom.HasProp("_GasteigerCharge")
255
+ else 0.0
256
+ )
257
+ return {
258
+ "charge": atom.GetFormalCharge(),
259
+ "hcount": atom.GetTotalNumHs(),
260
+ "aromatic": atom.GetIsAromatic(),
261
+ "element": atom.GetSymbol(),
262
+ "atom_map": atom.GetAtomMapNum(),
263
+ "isomer": MolToGraph.get_stereochemistry(atom),
264
+ "partial_charge": gasteiger_charge,
265
+ "hybridization": str(atom.GetHybridization()),
266
+ "in_ring": atom.IsInRing(),
267
+ "implicit_hcount": atom.GetNumImplicitHs(),
268
+ "neighbors": sorted(
269
+ neighbor.GetSymbol() for neighbor in atom.GetNeighbors()
270
+ ),
271
+ }
272
+
273
+ @staticmethod
274
+ def _gather_bond_properties(bond: Chem.Bond) -> Dict[str, Any]:
275
+ """Collect all relevant properties from a bond to use as graph edge attributes."""
276
+ return {
277
+ "order": bond.GetBondTypeAsDouble(),
278
+ "ez_isomer": MolToGraph.get_bond_stereochemistry(bond),
279
+ "bond_type": str(bond.GetBondType()),
280
+ "conjugated": bond.GetIsConjugated(),
281
+ "in_ring": bond.IsInRing(),
282
+ }
synkit/IO/nx_to_gml.py ADDED
@@ -0,0 +1,200 @@
1
+ import networkx as nx
2
+ from typing import Tuple, Dict, List
3
+ from synkit.ITS._misc import expand_hydrogens
4
+
5
+
6
+ class NXToGML:
7
+
8
+ def __init__(self) -> None:
9
+ pass
10
+
11
+ @staticmethod
12
+ def _charge_to_string(charge):
13
+ """
14
+ Converts an integer charge into a string representation.
15
+
16
+ Parameters:
17
+ - charge (int): The charge value, which can be positive, negative, or zero.
18
+
19
+ Returns:
20
+ - str: The string representation of the charge.
21
+ """
22
+ if charge > 0:
23
+ return (
24
+ "+" if charge == 1 else f"{charge}+"
25
+ ) # '+' for +1, '2+', '3+', etc., for higher values
26
+ elif charge < 0:
27
+ return (
28
+ "-" if charge == -1 else f"{-charge}-"
29
+ ) # '-' for -1, '2-', '3-', etc., for lower values
30
+ else:
31
+ return "" # No charge symbol for neutral atoms
32
+
33
+ @staticmethod
34
+ def _find_changed_nodes(
35
+ graph1: nx.Graph, graph2: nx.Graph, attributes: list = ["charge"]
36
+ ) -> list:
37
+ """
38
+ Identifies nodes with changes in specified attributes between two NetworkX graphs.
39
+
40
+ Parameters:
41
+ - graph1 (nx.Graph): The first NetworkX graph.
42
+ - graph2 (nx.Graph): The second NetworkX graph.
43
+ - attributes (list): A list of attribute names to check for changes.
44
+
45
+ Returns:
46
+ - list: Node identifiers that have changes in the specified attributes.
47
+ """
48
+ changed_nodes = []
49
+
50
+ # Iterate through nodes in the first graph
51
+ for node in graph1.nodes():
52
+ # Ensure the node exists in both graphs
53
+ if node in graph2:
54
+ # Check each specified attribute for changes
55
+ for attr in attributes:
56
+ value1 = graph1.nodes[node].get(attr, None)
57
+ value2 = graph2.nodes[node].get(attr, None)
58
+
59
+ if value1 != value2:
60
+ changed_nodes.append(node)
61
+ break
62
+
63
+ return changed_nodes
64
+
65
+ @staticmethod
66
+ def _convert_graph_to_gml(
67
+ graph: nx.Graph,
68
+ section: str,
69
+ changed_node_ids: List,
70
+ explicit_hydrogen: bool = False,
71
+ ) -> str:
72
+ """
73
+ Convert a NetworkX graph to a GML string representation, focusing on nodes for the
74
+ 'context' section and on nodes and edges for the 'left' or 'right' sections.
75
+
76
+ Parameters:
77
+ - graph (nx.Graph): The NetworkX graph to be converted.
78
+ - section (str): The section name in the GML output, typically "left", "right", or
79
+ "context".
80
+ - changed_node_ids (List): list of nodes change attribute
81
+ - explicit_hydrogen (bool): Whether to explicitly include hydrogen atoms
82
+ in the output.
83
+
84
+
85
+ Returns:
86
+ str: The GML string representation of the graph for the specified section.
87
+ """
88
+ order_to_label = {1: "-", 1.5: ":", 2: "=", 3: "#"}
89
+ gml_str = f" {section} [\n"
90
+
91
+ if section == "context":
92
+ for node in graph.nodes(data=True):
93
+ if node[0] not in changed_node_ids:
94
+ element = node[1].get("element", "X")
95
+ charge = node[1].get("charge", 0)
96
+ charge_str = NXToGML._charge_to_string(charge)
97
+ gml_str += (
98
+ f' node [ id {node[0]} label "{element}{charge_str}" ]\n'
99
+ )
100
+ if explicit_hydrogen:
101
+ for edge in graph.edges(data=True):
102
+ order = edge[2].get("order", (1.0, 1.0))
103
+ standard_order = edge[2].get("standard_order", (0))
104
+ if standard_order == 0:
105
+ label = order_to_label.get(order, "-")
106
+ gml_str += (
107
+ f" edge [ source {edge[0]} target {edge[1]}"
108
+ + f' label "{label}" ]\n'
109
+ )
110
+
111
+ if section != "context":
112
+ for edge in graph.edges(data=True):
113
+ label = order_to_label.get(edge[2].get("order", 1), "-")
114
+ gml_str += f' edge [ source {edge[0]} target {edge[1]} label "{label}" ]\n'
115
+ for node in graph.nodes(data=True):
116
+ if node[0] in changed_node_ids:
117
+ element = node[1].get("element", "X")
118
+ charge = node[1].get("charge", 0)
119
+ charge_str = NXToGML._charge_to_string(charge)
120
+ gml_str += (
121
+ f' node [ id {node[0]} label "{element}{charge_str}" ]\n'
122
+ )
123
+
124
+ gml_str += " ]\n"
125
+ return gml_str
126
+
127
+ @staticmethod
128
+ def _rule_grammar(
129
+ L: nx.Graph,
130
+ R: nx.Graph,
131
+ K: nx.Graph,
132
+ rule_name: str,
133
+ changed_node_ids: List,
134
+ explicit_hydrogen: bool,
135
+ ) -> str:
136
+ """
137
+ Generate a GML string representation for a chemical rule, including its left,
138
+ context, and right graphs.
139
+
140
+ Parameters:
141
+ - L (nx.Graph): The left graph.
142
+ - R (nx.Graph): The right graph.
143
+ - K (nx.Graph): The context graph.
144
+ - rule_name (str): The name of the rule.
145
+ - explicit_hydrogen (bool): Whether to explicitly include hydrogen atoms in the output.
146
+
147
+ Returns:
148
+ - str: The GML string representation of the rule.
149
+ """
150
+ gml_str = "rule [\n"
151
+ gml_str += f' ruleID "{rule_name}"\n'
152
+ gml_str += NXToGML._convert_graph_to_gml(L, "left", changed_node_ids)
153
+ gml_str += NXToGML._convert_graph_to_gml(
154
+ K, "context", changed_node_ids, explicit_hydrogen
155
+ )
156
+ gml_str += NXToGML._convert_graph_to_gml(R, "right", changed_node_ids)
157
+ gml_str += "]"
158
+ return gml_str
159
+
160
+ @staticmethod
161
+ def transform(
162
+ graph_rules: Tuple[nx.Graph, nx.Graph, nx.Graph],
163
+ rule_name: str = "Test",
164
+ reindex: bool = False,
165
+ attributes: List[str] = ["charge"],
166
+ explicit_hydrogen: bool = False,
167
+ ) -> Dict[str, str]:
168
+ """
169
+ Process a dictionary of graph rules to generate GML strings for each rule, with an
170
+ option to reindex nodes and edges.
171
+
172
+ Parameters:
173
+ - graph_rules (Dict[str, Tuple[nx.Graph, nx.Graph, nx.Graph]]): A dictionary
174
+ mapping rule names to tuples of (L, R, K) graphs.
175
+ - reindex (bool): If true, reindex node IDs based on the L graph sequence.
176
+ - explicit_hydrogen (bool): Whether to explicitly include hydrogen atoms in the output.
177
+
178
+
179
+ Returns:
180
+ - Dict[str, str]: A dictionary mapping rule names to their GML string
181
+ representations.
182
+ """
183
+ L, R, K = graph_rules
184
+ if explicit_hydrogen:
185
+ K = expand_hydrogens(K)
186
+ if reindex:
187
+ # Create an index mapping from L graph
188
+ index_mapping = {
189
+ old_id: new_id for new_id, old_id in enumerate(L.nodes(), 1)
190
+ }
191
+
192
+ # Apply the mapping to L, R, and K graphs
193
+ L = nx.relabel_nodes(L, index_mapping)
194
+ R = nx.relabel_nodes(R, index_mapping)
195
+ K = nx.relabel_nodes(K, index_mapping)
196
+ changed_node_ids = NXToGML._find_changed_nodes(L, R, attributes)
197
+ rule_grammar = NXToGML._rule_grammar(
198
+ L, R, K, rule_name, changed_node_ids, explicit_hydrogen
199
+ )
200
+ return rule_grammar