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,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