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.
- synkit/Chem/Fingerprint/__init__.py +0 -0
- synkit/Chem/Fingerprint/fp_calculator.py +122 -0
- synkit/Chem/Fingerprint/smiles_featurizer.py +185 -0
- synkit/Chem/Fingerprint/transformation_fp.py +79 -0
- synkit/Chem/Molecule/__init__.py +0 -0
- synkit/Chem/Molecule/standardize.py +137 -0
- synkit/Chem/Reaction/__init__.py +0 -0
- synkit/Chem/Reaction/balance_check.py +162 -0
- synkit/Chem/Reaction/cleanning.py +59 -0
- synkit/Chem/Reaction/deionize.py +289 -0
- synkit/Chem/Reaction/neutralize.py +256 -0
- synkit/Chem/Reaction/reagent.py +102 -0
- synkit/Chem/Reaction/standardize.py +157 -0
- synkit/Chem/Reaction/tautomerize.py +168 -0
- synkit/Graph/Cluster/__init__.py +0 -0
- synkit/Graph/Cluster/morphism.py +83 -0
- synkit/Graph/Feature/__init__.py +0 -0
- synkit/Graph/Feature/graph_descriptors.py +325 -0
- synkit/Graph/Feature/graph_fps.py +97 -0
- synkit/Graph/Feature/graph_signature.py +236 -0
- synkit/Graph/Feature/hash_fps.py +130 -0
- synkit/Graph/Feature/morgan_fps.py +87 -0
- synkit/Graph/Feature/path_fps.py +82 -0
- synkit/Graph/__init.py +0 -0
- synkit/IO/__init__.py +0 -0
- synkit/IO/chem_converter.py +231 -0
- synkit/IO/data_io.py +277 -0
- synkit/IO/data_process.py +49 -0
- synkit/IO/debug.py +78 -0
- synkit/IO/dg_to_gml.py +124 -0
- synkit/IO/gml_to_nx.py +119 -0
- synkit/IO/graph_to_mol.py +110 -0
- synkit/IO/mol_to_graph.py +282 -0
- synkit/IO/nx_to_gml.py +200 -0
- synkit/IO/parse_rule.py +172 -0
- synkit/IO/smiles_to_id.py +119 -0
- synkit/ITS/_misc.py +280 -0
- synkit/ITS/aam_validator.py +254 -0
- synkit/ITS/its_builder.py +94 -0
- synkit/ITS/its_construction.py +213 -0
- synkit/ITS/normalize_aam.py +183 -0
- synkit/ITS/partial_expand.py +170 -0
- synkit/Reactor/__init__.py +0 -0
- synkit/Reactor/core_engine.py +164 -0
- synkit/Reactor/inference.py +73 -0
- synkit/Reactor/multi_step.py +227 -0
- synkit/Reactor/multi_step_aam.py +82 -0
- synkit/Reactor/reagent.py +95 -0
- synkit/Reactor/rule_apply.py +81 -0
- synkit/Vis/__init__.py +0 -0
- synkit/Vis/chemical_graph_visualizer.py +378 -0
- synkit/Vis/chemical_reaction_visualizer.py +133 -0
- synkit/Vis/chemical_space.py +83 -0
- synkit/Vis/embedding.py +92 -0
- synkit/Vis/graph_visualizer.py +286 -0
- synkit/Vis/pdf_writer.py +143 -0
- synkit/Vis/rsmi_to_fig.py +169 -0
- synkit/__init__.py +0 -0
- synkit/_misc.py +181 -0
- synkit-0.0.1.dist-info/METADATA +148 -0
- synkit-0.0.1.dist-info/RECORD +63 -0
- synkit-0.0.1.dist-info/WHEEL +4 -0
- 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
|