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,254 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import networkx as nx
|
|
3
|
+
from operator import eq
|
|
4
|
+
from itertools import combinations
|
|
5
|
+
from joblib import Parallel, delayed
|
|
6
|
+
from typing import Dict, List, Tuple, Union, Optional
|
|
7
|
+
from networkx.algorithms.isomorphism import generic_node_match, generic_edge_match
|
|
8
|
+
|
|
9
|
+
from synkit.ITS.its_construction import ITSConstruction
|
|
10
|
+
from synkit.IO.chem_converter import rsmi_to_graph
|
|
11
|
+
from synkit.ITS._misc import get_rc, enumerate_tautomers, mapping_success_rate
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AAMValidator:
|
|
15
|
+
def __init__(self):
|
|
16
|
+
"""Initializes the AAMValidator class."""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
@staticmethod
|
|
20
|
+
def check_equivariant_graph(
|
|
21
|
+
its_graphs: List[nx.Graph],
|
|
22
|
+
) -> Tuple[List[Tuple[int, int]], int]:
|
|
23
|
+
"""
|
|
24
|
+
Checks for isomorphism among a list of ITS graphs and
|
|
25
|
+
identifies all pairs of isomorphic graphs.
|
|
26
|
+
|
|
27
|
+
Parameters:
|
|
28
|
+
- its_graphs (List[nx.Graph]): A list of ITS graphs.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
- List[Tuple[int, int]]: A list of tuples representing
|
|
32
|
+
pairs of indices of isomorphic graphs.
|
|
33
|
+
- int: The count of unique isomorphic graph pairs found.
|
|
34
|
+
"""
|
|
35
|
+
nodeLabelNames = ["typesGH"]
|
|
36
|
+
nodeLabelDefault = ["*", False, 0, 0, ()]
|
|
37
|
+
nodeLabelOperator = [eq, eq, eq, eq, eq]
|
|
38
|
+
nodeMatch = generic_node_match(
|
|
39
|
+
nodeLabelNames, nodeLabelDefault, nodeLabelOperator
|
|
40
|
+
)
|
|
41
|
+
edgeMatch = generic_edge_match("order", 1, eq)
|
|
42
|
+
|
|
43
|
+
classified = []
|
|
44
|
+
for i, j in combinations(range(len(its_graphs)), 2):
|
|
45
|
+
if nx.is_isomorphic(
|
|
46
|
+
its_graphs[i], its_graphs[j], node_match=nodeMatch, edge_match=edgeMatch
|
|
47
|
+
):
|
|
48
|
+
classified.append((i, j))
|
|
49
|
+
|
|
50
|
+
return classified, len(classified)
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def smiles_check(
|
|
54
|
+
mapped_smile: str,
|
|
55
|
+
ground_truth: str,
|
|
56
|
+
check_method: str = "RC", # or 'ITS'
|
|
57
|
+
ignore_aromaticity: bool = False,
|
|
58
|
+
) -> bool:
|
|
59
|
+
"""
|
|
60
|
+
Checks the equivalence of mapped SMILES against ground truth
|
|
61
|
+
using reaction center (RC) or ITS graph method.
|
|
62
|
+
|
|
63
|
+
Parameters:
|
|
64
|
+
- mapped_smile (str): The mapped SMILES string.
|
|
65
|
+
- ground_truth (str): The ground truth SMILES string.
|
|
66
|
+
- check_method (str): The method used for validation ('RC' or 'ITS').
|
|
67
|
+
- ignore_aromaticity (bool): Flag to ignore aromaticity in ITS graph construction.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
- bool: True if the mapped SMILES is equivalent to the ground truth,
|
|
71
|
+
False otherwise.
|
|
72
|
+
"""
|
|
73
|
+
its_graphs = []
|
|
74
|
+
rc_graphs = []
|
|
75
|
+
try:
|
|
76
|
+
for rsmi in [mapped_smile, ground_truth]:
|
|
77
|
+
G, H = rsmi_to_graph(
|
|
78
|
+
rsmi=rsmi, sanitize=True, drop_non_aam=True, light_weight=True
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
ITS = ITSConstruction.ITSGraph(G, H, ignore_aromaticity)
|
|
82
|
+
its_graphs.append(ITS)
|
|
83
|
+
rc = get_rc(ITS)
|
|
84
|
+
rc_graphs.append(rc)
|
|
85
|
+
|
|
86
|
+
_, equivariant = AAMValidator.check_equivariant_graph(
|
|
87
|
+
rc_graphs if check_method == "RC" else its_graphs
|
|
88
|
+
)
|
|
89
|
+
return equivariant == 1
|
|
90
|
+
|
|
91
|
+
except Exception as e:
|
|
92
|
+
print("An error occurred:", str(e))
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def smiles_check_tautomer(
|
|
97
|
+
mapped_smile: str,
|
|
98
|
+
ground_truth: str,
|
|
99
|
+
check_method: str = "RC", # or 'ITS'
|
|
100
|
+
ignore_aromaticity: bool = False,
|
|
101
|
+
) -> Optional[bool]:
|
|
102
|
+
"""
|
|
103
|
+
Determines if a given mapped SMILE string is equivalent to any tautomer of
|
|
104
|
+
a ground truth SMILES string using a specified comparison method.
|
|
105
|
+
|
|
106
|
+
Parameters:
|
|
107
|
+
- mapped_smile (str): The mapped SMILES string to check against the tautomers of
|
|
108
|
+
the ground truth.
|
|
109
|
+
- ground_truth (str): The reference SMILES string for generating possible
|
|
110
|
+
tautomers.
|
|
111
|
+
- check_method (str): The method used for checking equivalence. Default is 'RC'.
|
|
112
|
+
Possible values are 'RC' for reaction center or 'ITS'.
|
|
113
|
+
- ignore_aromaticity (bool): Flag to ignore differences in aromaticity between
|
|
114
|
+
the mapped SMILE and the tautomers.Default is False.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
- Optional[bool]: True if the mapped SMILE matches any of the enumerated tautomers
|
|
118
|
+
of the ground truth according to the specified check method.
|
|
119
|
+
Returns False if no match is found.
|
|
120
|
+
Returns None if an error occurs during processing.
|
|
121
|
+
|
|
122
|
+
Raises:
|
|
123
|
+
- Exception: If an error occurs during the tautomer enumeration
|
|
124
|
+
or the comparison process.
|
|
125
|
+
"""
|
|
126
|
+
try:
|
|
127
|
+
ground_truth_tautomers = enumerate_tautomers(ground_truth)
|
|
128
|
+
return any(
|
|
129
|
+
AAMValidator.smiles_check(
|
|
130
|
+
mapped_smile, t, check_method, ignore_aromaticity
|
|
131
|
+
)
|
|
132
|
+
for t in ground_truth_tautomers
|
|
133
|
+
)
|
|
134
|
+
except Exception as e:
|
|
135
|
+
print(f"An error occurred: {e}")
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
@staticmethod
|
|
139
|
+
def check_pair(
|
|
140
|
+
mapping: Dict[str, str],
|
|
141
|
+
mapped_col: str,
|
|
142
|
+
ground_truth_col: str,
|
|
143
|
+
check_method: str = "RC",
|
|
144
|
+
ignore_aromaticity: bool = False,
|
|
145
|
+
ignore_tautomers: bool = True,
|
|
146
|
+
) -> bool:
|
|
147
|
+
"""
|
|
148
|
+
Checks the equivalence between the mapped and ground truth
|
|
149
|
+
values within a given mapping dictionary, using a specified check method.
|
|
150
|
+
The check can optionally ignore aromaticity.
|
|
151
|
+
|
|
152
|
+
Parameters:
|
|
153
|
+
- mapping (Dict[str, str]): A dictionary containing the data entries to check.
|
|
154
|
+
- mapped_col (str): The key in the mapping dictionary corresponding
|
|
155
|
+
to the mapped value.
|
|
156
|
+
- ground_truth_col (str): The key in the mapping dictionary corresponding
|
|
157
|
+
to the ground truth value.
|
|
158
|
+
- check_method (str, optional): The method used for checking the equivalence.
|
|
159
|
+
Defaults to 'RC'.
|
|
160
|
+
- ignore_aromaticity (bool, optional): Flag to indicate whether aromaticity
|
|
161
|
+
should be ignored during the check. Defaults to False.
|
|
162
|
+
- ignore_tautomers (bool, optional): Flag to indicate whether tautomers
|
|
163
|
+
should be ignored during the check. Defaults to False.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
- bool: The result of the check, indicating whether the mapped value is
|
|
167
|
+
equivalent to the ground truth according to the specified method
|
|
168
|
+
and considerations regarding aromaticity.
|
|
169
|
+
"""
|
|
170
|
+
if ignore_tautomers:
|
|
171
|
+
return AAMValidator.smiles_check(
|
|
172
|
+
mapping[mapped_col],
|
|
173
|
+
mapping[ground_truth_col],
|
|
174
|
+
check_method,
|
|
175
|
+
ignore_aromaticity,
|
|
176
|
+
)
|
|
177
|
+
else:
|
|
178
|
+
return AAMValidator.smiles_check_tautomer(
|
|
179
|
+
mapping[mapped_col],
|
|
180
|
+
mapping[ground_truth_col],
|
|
181
|
+
check_method,
|
|
182
|
+
ignore_aromaticity,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
@staticmethod
|
|
186
|
+
def validate_smiles(
|
|
187
|
+
data: Union[pd.DataFrame, List[Dict[str, str]]],
|
|
188
|
+
ground_truth_col: str = "ground_truth",
|
|
189
|
+
mapped_cols: List[str] = ["rxn_mapper", "graphormer", "local_mapper"],
|
|
190
|
+
check_method: str = "RC",
|
|
191
|
+
ignore_aromaticity: bool = False,
|
|
192
|
+
n_jobs: int = 1,
|
|
193
|
+
verbose: int = 0,
|
|
194
|
+
ignore_tautomers=True,
|
|
195
|
+
) -> List[Dict[str, Union[str, float, List[bool]]]]:
|
|
196
|
+
"""
|
|
197
|
+
Validates collections of mapped SMILES against their ground truths for
|
|
198
|
+
multiple mappers and calculates the accuracy.
|
|
199
|
+
|
|
200
|
+
Parameters:
|
|
201
|
+
- data (Union[pd.DataFrame, List[Dict[str, str]]]):
|
|
202
|
+
The input data containing mapped and ground truth SMILES.
|
|
203
|
+
- id_col (str): The name of the column or key containing the reaction ID.
|
|
204
|
+
- ground_truth_col (str): The name of the column or key containing
|
|
205
|
+
the ground truth SMILES.
|
|
206
|
+
- mapped_cols (List[str]): The list of columns or keys containing
|
|
207
|
+
the mapped SMILES for different mappers.
|
|
208
|
+
- check_method (str): The method used for validation ('RC' or 'ITS').
|
|
209
|
+
- ignore_aromaticity (bool): Flag to ignore aromaticity in ITS graph construction.
|
|
210
|
+
- n_jobs (int): The number of parallel jobs to run.
|
|
211
|
+
- verbose (int): The verbosity level for joblib's parallel execution.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
- List[Dict[str, Union[str, float, List[bool]]]]: A list of dictionaries, each
|
|
215
|
+
containing the mapper name, accuracy, and individual results for each SMILES pair.
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
validation_results = []
|
|
219
|
+
|
|
220
|
+
for mapped_col in mapped_cols:
|
|
221
|
+
|
|
222
|
+
if isinstance(data, pd.DataFrame):
|
|
223
|
+
mappings = data.to_dict("records")
|
|
224
|
+
elif isinstance(data, list):
|
|
225
|
+
mappings = data
|
|
226
|
+
else:
|
|
227
|
+
raise ValueError(
|
|
228
|
+
"Data must be either a pandas DataFrame or a list of dictionaries."
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
results = Parallel(n_jobs=n_jobs, verbose=verbose)(
|
|
232
|
+
delayed(AAMValidator.check_pair)(
|
|
233
|
+
mapping,
|
|
234
|
+
mapped_col,
|
|
235
|
+
ground_truth_col,
|
|
236
|
+
check_method,
|
|
237
|
+
ignore_aromaticity,
|
|
238
|
+
ignore_tautomers,
|
|
239
|
+
)
|
|
240
|
+
for mapping in mappings
|
|
241
|
+
)
|
|
242
|
+
accuracy = sum(results) / len(mappings) if mappings else 0
|
|
243
|
+
mapped_data = [value[mapped_col] for value in mappings]
|
|
244
|
+
|
|
245
|
+
validation_results.append(
|
|
246
|
+
{
|
|
247
|
+
"mapper": mapped_col,
|
|
248
|
+
"accuracy": round(100 * accuracy, 2),
|
|
249
|
+
"results": results,
|
|
250
|
+
"success_rate": mapping_success_rate(mapped_data),
|
|
251
|
+
}
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
return validation_results
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import networkx as nx
|
|
2
|
+
from copy import deepcopy
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ITSBuilder:
|
|
6
|
+
@staticmethod
|
|
7
|
+
def update_atom_map(graph: nx.Graph) -> None:
|
|
8
|
+
"""
|
|
9
|
+
Update the 'atom_map' of each node in a graph to match its node index.
|
|
10
|
+
Parameters:
|
|
11
|
+
- graph (nx.Graph): The graph whose node attributes are to be updated.
|
|
12
|
+
"""
|
|
13
|
+
for node in graph.nodes():
|
|
14
|
+
graph.nodes[node]["atom_map"] = node
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def ITSGraph(G: nx.Graph, RC: nx.Graph) -> nx.Graph:
|
|
18
|
+
"""
|
|
19
|
+
Creates an ITS graph based on graph G and the reaction center RC.
|
|
20
|
+
|
|
21
|
+
This function:
|
|
22
|
+
- Copies graph G to initialize ITS.
|
|
23
|
+
- Initializes 'typesGH' and edge orders for ITS.
|
|
24
|
+
- Establishes a mapping from RC's 'atom_map' to G's node indices.
|
|
25
|
+
- Updates nodes and edges in ITS based on attributes from RC using the established mapping.
|
|
26
|
+
|
|
27
|
+
Parameters:
|
|
28
|
+
- G (nx.Graph): The initial graph.
|
|
29
|
+
- RC (nx.Graph): The reaction center graph with modifications.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
- nx.Graph: The ITS graph with updated node and edge attributes based on RC.
|
|
33
|
+
"""
|
|
34
|
+
# Step 1: Copy Graph G to form the initial ITS
|
|
35
|
+
ITS = deepcopy(G)
|
|
36
|
+
|
|
37
|
+
# Step 2: Initialize 'typesGH' for each node in ITS using attributes from G
|
|
38
|
+
for node in ITS.nodes():
|
|
39
|
+
node_attr = ITS.nodes[node]
|
|
40
|
+
typesGH = (
|
|
41
|
+
(
|
|
42
|
+
node_attr.get("element", "*"),
|
|
43
|
+
node_attr.get("aromatic", False),
|
|
44
|
+
node_attr.get("hcount", 0),
|
|
45
|
+
node_attr.get("charge", 0),
|
|
46
|
+
node_attr.get("neighbors", []),
|
|
47
|
+
),
|
|
48
|
+
(
|
|
49
|
+
node_attr.get("element", "*"),
|
|
50
|
+
node_attr.get("aromatic", False),
|
|
51
|
+
node_attr.get("hcount", 0),
|
|
52
|
+
node_attr.get("charge", 0),
|
|
53
|
+
node_attr.get("neighbors", []),
|
|
54
|
+
),
|
|
55
|
+
)
|
|
56
|
+
ITS.nodes[node]["typesGH"] = typesGH
|
|
57
|
+
|
|
58
|
+
# Step 3: Set edge orders in ITS as (order, order) and 'standard_order' as 0
|
|
59
|
+
for u, v in ITS.edges():
|
|
60
|
+
edge_attr = ITS[u][v]
|
|
61
|
+
order = edge_attr.get("order", 1.0)
|
|
62
|
+
ITS[u][v]["order"] = (order, order)
|
|
63
|
+
ITS[u][v]["standard_order"] = 0.0
|
|
64
|
+
|
|
65
|
+
# Mapping from atom_map in RC to node indices in G
|
|
66
|
+
atom_map_to_node = {
|
|
67
|
+
G.nodes[n]["atom_map"]: n for n in G.nodes if G.nodes[n]["atom_map"] != 0
|
|
68
|
+
}
|
|
69
|
+
# print(atom_map_to_node)
|
|
70
|
+
|
|
71
|
+
# Step 4: Update nodes in ITS based on RC
|
|
72
|
+
for rc_node, rc_attr in RC.nodes(data=True):
|
|
73
|
+
atom_map = rc_attr.get("atom_map")
|
|
74
|
+
if atom_map in atom_map_to_node:
|
|
75
|
+
target_node = atom_map_to_node[atom_map]
|
|
76
|
+
ITS.nodes[target_node].update(rc_attr)
|
|
77
|
+
|
|
78
|
+
# Step 5: Update and add edges based on RC
|
|
79
|
+
for rc_u, rc_v, rc_edge_attr in RC.edges(data=True):
|
|
80
|
+
rc_u_map = RC.nodes[rc_u].get("atom_map", rc_u)
|
|
81
|
+
rc_v_map = RC.nodes[rc_v].get("atom_map", rc_v)
|
|
82
|
+
|
|
83
|
+
rc_u_target = atom_map_to_node.get(rc_u_map)
|
|
84
|
+
rc_v_target = atom_map_to_node.get(rc_v_map)
|
|
85
|
+
|
|
86
|
+
if rc_u_target is not None and rc_v_target is not None:
|
|
87
|
+
if ITS.has_edge(rc_u_target, rc_v_target):
|
|
88
|
+
ITS[rc_u_target][rc_v_target].update(rc_edge_attr)
|
|
89
|
+
else:
|
|
90
|
+
ITS.add_edge(rc_u_target, rc_v_target, **rc_edge_attr)
|
|
91
|
+
|
|
92
|
+
# Update atom_map for all nodes to reflect their indices
|
|
93
|
+
ITSBuilder.update_atom_map(ITS)
|
|
94
|
+
return ITS
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import networkx as nx
|
|
2
|
+
from typing import Tuple, Dict, Any
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ITSConstruction:
|
|
7
|
+
@staticmethod
|
|
8
|
+
def ITSGraph(
|
|
9
|
+
G: nx.Graph,
|
|
10
|
+
H: nx.Graph,
|
|
11
|
+
ignore_aromaticity: bool = False,
|
|
12
|
+
attributes_defaults: Dict[str, Any] = None,
|
|
13
|
+
balance_its: bool = True,
|
|
14
|
+
) -> nx.Graph:
|
|
15
|
+
"""
|
|
16
|
+
Creates a Combined Graph Representation (CGR) from two input graphs G and H.
|
|
17
|
+
|
|
18
|
+
This function merges the nodes of G and H, preserving their attributes. Edges are
|
|
19
|
+
added based on their presence in G and/or H, with special labeling for edges
|
|
20
|
+
unique to one graph.
|
|
21
|
+
|
|
22
|
+
Parameters:
|
|
23
|
+
- G (nx.Graph): The first input graph.
|
|
24
|
+
- H (nx.Graph): The second input graph.
|
|
25
|
+
- ignore_aromaticity (bool): Whether to ignore aromaticity in the graphs.
|
|
26
|
+
Defaults to False.
|
|
27
|
+
- attributes_defaults (Dict[str, Any]): A dictionary of default attributes
|
|
28
|
+
to use for nodes that are not present in either G or H.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
- nx.Graph: The Combined Graph Representation as a new graph instance.
|
|
32
|
+
"""
|
|
33
|
+
# Create a null graph from a copy of G to preserve attributes
|
|
34
|
+
if (balance_its and len(G.nodes()) <= len(H.nodes())) or (
|
|
35
|
+
not balance_its and len(G.nodes()) >= len(H.nodes())
|
|
36
|
+
):
|
|
37
|
+
ITS = deepcopy(G)
|
|
38
|
+
else:
|
|
39
|
+
ITS = deepcopy(H)
|
|
40
|
+
|
|
41
|
+
ITS.remove_edges_from(list(ITS.edges()))
|
|
42
|
+
|
|
43
|
+
# Initialize a dictionary to hold node types
|
|
44
|
+
typesDict = dict()
|
|
45
|
+
|
|
46
|
+
# Add typeG and typeH attributes, or default attributes for "*" unknown elements
|
|
47
|
+
for v in list(ITS.nodes()):
|
|
48
|
+
# Check if v is in both G and H
|
|
49
|
+
if v not in G.nodes() or v not in H.nodes():
|
|
50
|
+
continue
|
|
51
|
+
else:
|
|
52
|
+
typesG = ITSConstruction.get_node_attributes_with_defaults(
|
|
53
|
+
G, v, attributes_defaults
|
|
54
|
+
) # node attribute in reactant graph
|
|
55
|
+
typesH = ITSConstruction.get_node_attributes_with_defaults(
|
|
56
|
+
H, v, attributes_defaults
|
|
57
|
+
) # node attribute in product graph
|
|
58
|
+
typesDict[v] = (typesG, typesH)
|
|
59
|
+
|
|
60
|
+
nx.set_node_attributes(ITS, typesDict, "typesGH")
|
|
61
|
+
|
|
62
|
+
# Add edges from G and H
|
|
63
|
+
ITS = ITSConstruction.add_edges_to_ITS(ITS, G, H, ignore_aromaticity)
|
|
64
|
+
|
|
65
|
+
return ITS
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def get_node_attribute(graph: nx.Graph, node: int, attribute: str, default):
|
|
69
|
+
"""
|
|
70
|
+
Retrieves a specific attribute for a node in a graph, returning a default value if
|
|
71
|
+
the attribute is missing.
|
|
72
|
+
|
|
73
|
+
Parameters:
|
|
74
|
+
- graph (nx.Graph): The graph from which to retrieve the node attribute.
|
|
75
|
+
- node (int): The node identifier.
|
|
76
|
+
- attribute (str): The attribute to retrieve.
|
|
77
|
+
- default: The default value to return if the attribute is missing.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
- The value of the node attribute, or the default value if the attribute is
|
|
81
|
+
missing.
|
|
82
|
+
"""
|
|
83
|
+
try:
|
|
84
|
+
return graph.nodes[node][attribute]
|
|
85
|
+
except KeyError:
|
|
86
|
+
return default
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def get_node_attributes_with_defaults(
|
|
90
|
+
graph: nx.Graph, node: int, attributes_defaults: Dict[str, Any] = None
|
|
91
|
+
) -> Tuple:
|
|
92
|
+
"""
|
|
93
|
+
Retrieves node attributes from a graph, assigning default values if they are
|
|
94
|
+
missing. Allows for an optional dictionary of attribute-default value pairs to
|
|
95
|
+
specify custom attributes and defaults.
|
|
96
|
+
|
|
97
|
+
Parameters:
|
|
98
|
+
- graph (nx.Graph): The graph from which to retrieve node attributes.
|
|
99
|
+
- node (int): The node identifier.
|
|
100
|
+
- attributes_defaults (Dict[str, Any], optional): A dictionary specifying
|
|
101
|
+
attributes and their default values.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
- Tuple: A tuple containing the node attributes in the order specified by
|
|
105
|
+
attributes_defaults.
|
|
106
|
+
"""
|
|
107
|
+
if attributes_defaults is None:
|
|
108
|
+
attributes_defaults = {
|
|
109
|
+
"element": "*",
|
|
110
|
+
"aromatic": False,
|
|
111
|
+
"hcount": 0,
|
|
112
|
+
"charge": 0,
|
|
113
|
+
"neighbors": ["", ""],
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return tuple(
|
|
117
|
+
ITSConstruction.get_node_attribute(graph, node, attr, default)
|
|
118
|
+
for attr, default in attributes_defaults.items()
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def add_edges_to_ITS(
|
|
123
|
+
ITS: nx.Graph, G: nx.Graph, H: nx.Graph, ignore_aromaticity: bool = False
|
|
124
|
+
) -> nx.Graph:
|
|
125
|
+
"""
|
|
126
|
+
Adds edges to the Combined Graph Representation (ITS) based on the edges of G and
|
|
127
|
+
H, and returns a new graph without modifying the original ITS.
|
|
128
|
+
|
|
129
|
+
Parameters:
|
|
130
|
+
- ITS (nx.Graph): The initial combined graph representation.
|
|
131
|
+
- G (nx.Graph): The first input graph.
|
|
132
|
+
- H (nx.Graph): The second input graph.
|
|
133
|
+
- ignore_aromaticity (bool): Whether to ignore aromaticity in the graphs. Defaults
|
|
134
|
+
to False.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
- nx.Graph: The updated graph with added edges.
|
|
138
|
+
"""
|
|
139
|
+
new_ITS = ITS.copy()
|
|
140
|
+
|
|
141
|
+
# Add edges from G and H
|
|
142
|
+
for graph_from, graph_to, reverse in [(G, H, False), (H, G, True)]:
|
|
143
|
+
for u, v in graph_from.edges():
|
|
144
|
+
if not new_ITS.has_edge(u, v):
|
|
145
|
+
if graph_to.has_edge(u, v) or graph_to.has_edge(v, u):
|
|
146
|
+
edge_label = (
|
|
147
|
+
(graph_from[u][v]["order"], graph_to[u][v]["order"])
|
|
148
|
+
if graph_to.has_edge(u, v)
|
|
149
|
+
else (
|
|
150
|
+
(graph_from[v][u]["order"], graph_to[v][u]["order"])
|
|
151
|
+
if reverse
|
|
152
|
+
else (
|
|
153
|
+
graph_from[u][v]["order"],
|
|
154
|
+
graph_to[v][u]["order"],
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
new_ITS.add_edge(u, v, order=edge_label)
|
|
159
|
+
else:
|
|
160
|
+
edge_label = (
|
|
161
|
+
(graph_from[u][v]["order"], 0)
|
|
162
|
+
if not reverse
|
|
163
|
+
else (0, graph_from[u][v]["order"])
|
|
164
|
+
)
|
|
165
|
+
new_ITS.add_edge(u, v, order=edge_label)
|
|
166
|
+
nodes_to_remove = [node for node in new_ITS.nodes() if not new_ITS.nodes[node]]
|
|
167
|
+
new_ITS.remove_nodes_from(nodes_to_remove)
|
|
168
|
+
new_ITS = ITSConstruction.add_standard_order_attribute(
|
|
169
|
+
new_ITS, ignore_aromaticity
|
|
170
|
+
)
|
|
171
|
+
return new_ITS
|
|
172
|
+
|
|
173
|
+
@staticmethod
|
|
174
|
+
def add_standard_order_attribute(
|
|
175
|
+
graph: nx.Graph, ignore_aromaticity: bool = False
|
|
176
|
+
) -> nx.Graph:
|
|
177
|
+
"""
|
|
178
|
+
Adds a 'standard_order' attribute to each edge in the provided NetworkX graph.
|
|
179
|
+
This attribute is calculated based on the existing 'order' attribute, which should
|
|
180
|
+
be a tuple associated with each edge. The 'standard_order' is computed by
|
|
181
|
+
subtracting the second element of the 'order' tuple from the first element.
|
|
182
|
+
If any element of the 'order' tuple is not an integer (e.g., '*'), it is treated
|
|
183
|
+
as 0 for the purpose of this computation.
|
|
184
|
+
|
|
185
|
+
Parameters:
|
|
186
|
+
- graph (NetworkX.Graph): A NetworkX graph where each edge has an 'order'
|
|
187
|
+
attribute formatted as a tuple.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
- NetworkX.Graph: The same graph passed as input, now with a 'standard_order'
|
|
191
|
+
attribute added to each edge, reflecting the computed standard order derived from
|
|
192
|
+
the 'order' attribute.
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
new_graph = graph.copy()
|
|
196
|
+
|
|
197
|
+
for u, v, data in new_graph.edges(data=True):
|
|
198
|
+
if "order" in data and isinstance(data["order"], tuple):
|
|
199
|
+
# Extract order values, replacing non-ints with 0
|
|
200
|
+
first_order = data["order"][0]
|
|
201
|
+
second_order = data["order"][1]
|
|
202
|
+
# Compute standard order
|
|
203
|
+
standard_order = first_order - second_order
|
|
204
|
+
if ignore_aromaticity:
|
|
205
|
+
if abs(standard_order) < 1: # to ignore aromaticity
|
|
206
|
+
standard_order = 0
|
|
207
|
+
# Update the edge data with a new attribute 'standard_order'
|
|
208
|
+
new_graph[u][v]["standard_order"] = standard_order
|
|
209
|
+
else:
|
|
210
|
+
# If 'order' attribute is missing or not a tuple, 'standard_order' to 0
|
|
211
|
+
new_graph[u][v]["standard_order"] = 0
|
|
212
|
+
|
|
213
|
+
return new_graph
|