synkit 1.6.5__py3-none-win_amd64.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 (636) hide show
  1. synkit/CRN/Benchmark/__init__.py +53 -0
  2. synkit/CRN/Benchmark/crosschecks.py +13 -0
  3. synkit/CRN/Benchmark/kegg.py +12 -0
  4. synkit/CRN/Benchmark/networks.py +13 -0
  5. synkit/CRN/Benchmark/scaling.py +13 -0
  6. synkit/CRN/Benchmark/validate.py +13 -0
  7. synkit/CRN/Construct/__init__.py +16 -0
  8. synkit/CRN/Construct/abstract.py +668 -0
  9. synkit/CRN/Construct/arity.py +36 -0
  10. synkit/CRN/Construct/builder.py +619 -0
  11. synkit/CRN/Construct/derivation.py +63 -0
  12. synkit/CRN/Construct/flattener.py +100 -0
  13. synkit/CRN/Construct/keys.py +14 -0
  14. synkit/CRN/Construct/mixtures.py +117 -0
  15. synkit/CRN/Construct/rdkit_worker.py +69 -0
  16. synkit/CRN/Construct/smiles.py +99 -0
  17. synkit/CRN/Construct/state.py +31 -0
  18. synkit/CRN/Construct/strategy.py +75 -0
  19. synkit/CRN/Construct/worker.py +42 -0
  20. synkit/CRN/IO/__init__.py +36 -0
  21. synkit/CRN/IO/sbml.py +925 -0
  22. synkit/CRN/Pathway/__init__.py +72 -0
  23. synkit/CRN/Pathway/_adapter.py +333 -0
  24. synkit/CRN/Pathway/pathfinder.py +500 -0
  25. synkit/CRN/Pathway/reachability.py +796 -0
  26. synkit/CRN/Pathway/realizability.py +1322 -0
  27. synkit/CRN/Petrinet/__init__.py +93 -0
  28. synkit/CRN/Petrinet/analyzer.py +486 -0
  29. synkit/CRN/Petrinet/minimal_semiflows.py +436 -0
  30. synkit/CRN/Petrinet/net.py +1041 -0
  31. synkit/CRN/Petrinet/persistence.py +97 -0
  32. synkit/CRN/Petrinet/semiflows.py +483 -0
  33. synkit/CRN/Petrinet/siphon_search.py +402 -0
  34. synkit/CRN/Petrinet/structure.py +286 -0
  35. synkit/CRN/Props/__init__.py +130 -0
  36. synkit/CRN/Props/_rank.py +29 -0
  37. synkit/CRN/Props/deficiency.py +953 -0
  38. synkit/CRN/Props/dynamics.py +832 -0
  39. synkit/CRN/Props/helper.py +151 -0
  40. synkit/CRN/Props/stoich.py +751 -0
  41. synkit/CRN/Props/thermo.py +584 -0
  42. synkit/CRN/Query/__init__.py +66 -0
  43. synkit/CRN/Query/cache.py +41 -0
  44. synkit/CRN/Query/kegg_api.py +99 -0
  45. synkit/CRN/Query/kegg_extract.py +714 -0
  46. synkit/CRN/Query/kegg_impute.py +707 -0
  47. synkit/CRN/Query/kegg_parse.py +480 -0
  48. synkit/CRN/Query/to_syncrn.py +290 -0
  49. synkit/CRN/README.md +234 -0
  50. synkit/CRN/Structure/__init__.py +12 -0
  51. synkit/CRN/Structure/_graph_io.py +713 -0
  52. synkit/CRN/Structure/_matrices.py +88 -0
  53. synkit/CRN/Structure/_parse.py +544 -0
  54. synkit/CRN/Structure/reaction.py +237 -0
  55. synkit/CRN/Structure/rule.py +55 -0
  56. synkit/CRN/Structure/species.py +46 -0
  57. synkit/CRN/Structure/syncrn.py +929 -0
  58. synkit/CRN/Symmetry/__init__.py +20 -0
  59. synkit/CRN/Symmetry/_common.py +1059 -0
  60. synkit/CRN/Symmetry/_ir.py +940 -0
  61. synkit/CRN/Symmetry/automorphism.py +472 -0
  62. synkit/CRN/Symmetry/canon.py +441 -0
  63. synkit/CRN/Symmetry/isomorphism.py +412 -0
  64. synkit/CRN/Symmetry/symmetry.py +61 -0
  65. synkit/CRN/Symmetry/wl_canon.py +967 -0
  66. synkit/CRN/Visualize/__init__.py +11 -0
  67. synkit/CRN/Visualize/labels.py +113 -0
  68. synkit/CRN/Visualize/layout.py +708 -0
  69. synkit/CRN/Visualize/palette.py +292 -0
  70. synkit/CRN/Visualize/validation.py +122 -0
  71. synkit/CRN/Visualize/vis.py +1152 -0
  72. synkit/CRN/__init__.py +233 -0
  73. synkit/CRN/_build.py +50 -0
  74. synkit/CRN/_native_build.py +175 -0
  75. synkit/CRN/analysis.py +629 -0
  76. synkit/CRN/backend.py +74 -0
  77. synkit/CRN/cpp/CMakeLists.txt +58 -0
  78. synkit/CRN/cpp/algebra.cpp +46 -0
  79. synkit/CRN/cpp/closure.cpp +29 -0
  80. synkit/CRN/cpp/exact.h +55 -0
  81. synkit/CRN/cpp/pathway.cpp +46 -0
  82. synkit/CRN/cpp/semiflows.cpp +209 -0
  83. synkit/CRN/cpp/structural.cpp +240 -0
  84. synkit/CRN/cpp/symmetry.cpp +45 -0
  85. synkit/CRN/cpp/write_manifest.cmake +28 -0
  86. synkit/CRN/kinds.py +137 -0
  87. synkit/CRN/lib/synkit_crn.dll +0 -0
  88. synkit/CRN/lib/synkit_crn.json +33 -0
  89. synkit/CRN/native.py +388 -0
  90. synkit/CRN/native_analysis.py +459 -0
  91. synkit/Chem/Cluster/__init__.py +0 -0
  92. synkit/Chem/Cluster/butina.py +140 -0
  93. synkit/Chem/Fingerprint/__init__.py +0 -0
  94. synkit/Chem/Fingerprint/fp_calculator.py +155 -0
  95. synkit/Chem/Fingerprint/smiles_featurizer.py +258 -0
  96. synkit/Chem/Fingerprint/transformation_fp.py +139 -0
  97. synkit/Chem/Mapper/README.rst +43 -0
  98. synkit/Chem/Mapper/__init__.py +255 -0
  99. synkit/Chem/Mapper/_compat.py +130 -0
  100. synkit/Chem/Mapper/adapters/__init__.py +1 -0
  101. synkit/Chem/Mapper/adapters/prediction.py +126 -0
  102. synkit/Chem/Mapper/adapters/template.py +412 -0
  103. synkit/Chem/Mapper/chem/__init__.py +52 -0
  104. synkit/Chem/Mapper/chem/aam.py +673 -0
  105. synkit/Chem/Mapper/chem/blind.py +160 -0
  106. synkit/Chem/Mapper/chem/its.py +309 -0
  107. synkit/Chem/Mapper/chem/smiles.py +564 -0
  108. synkit/Chem/Mapper/graph/__init__.py +66 -0
  109. synkit/Chem/Mapper/graph/automorphism.py +395 -0
  110. synkit/Chem/Mapper/graph/blockcut.py +98 -0
  111. synkit/Chem/Mapper/graph/labeled_graph.py +104 -0
  112. synkit/Chem/Mapper/graph/refine.py +176 -0
  113. synkit/Chem/Mapper/graph/synkit_adapter.py +110 -0
  114. synkit/Chem/Mapper/io/__init__.py +13 -0
  115. synkit/Chem/Mapper/io/idxmapstr.py +82 -0
  116. synkit/Chem/Mapper/mapper.py +685 -0
  117. synkit/Chem/Mapper/reaction_search.py +554 -0
  118. synkit/Chem/Mapper/reporting/__init__.py +1 -0
  119. synkit/Chem/Mapper/reporting/alternatives.py +451 -0
  120. synkit/Chem/Mapper/reporting/analysis.py +681 -0
  121. synkit/Chem/Mapper/reporting/native_analysis.py +485 -0
  122. synkit/Chem/Mapper/reporting/spectrum.py +642 -0
  123. synkit/Chem/Mapper/solvers/__init__.py +1 -0
  124. synkit/Chem/Mapper/solvers/exact/__init__.py +141 -0
  125. synkit/Chem/Mapper/solvers/exact/assignment/__init__.py +1 -0
  126. synkit/Chem/Mapper/solvers/exact/assignment/assignment_certificate.py +113 -0
  127. synkit/Chem/Mapper/solvers/exact/assignment/assignment_domains.py +162 -0
  128. synkit/Chem/Mapper/solvers/exact/assignment/assignment_verification.py +58 -0
  129. synkit/Chem/Mapper/solvers/exact/assignment/incremental_assignment.py +247 -0
  130. synkit/Chem/Mapper/solvers/exact/certificate.py +128 -0
  131. synkit/Chem/Mapper/solvers/exact/core.py +183 -0
  132. synkit/Chem/Mapper/solvers/exact/distance_search/__init__.py +1 -0
  133. synkit/Chem/Mapper/solvers/exact/distance_search/bounds.py +319 -0
  134. synkit/Chem/Mapper/solvers/exact/distance_search/exhaustive.py +179 -0
  135. synkit/Chem/Mapper/solvers/exact/distance_search/inputs.py +283 -0
  136. synkit/Chem/Mapper/solvers/exact/distance_search/records.py +242 -0
  137. synkit/Chem/Mapper/solvers/exact/distance_search/search.py +1025 -0
  138. synkit/Chem/Mapper/solvers/exact/distance_search/verify.py +378 -0
  139. synkit/Chem/Mapper/solvers/exact/edit_support.py +375 -0
  140. synkit/Chem/Mapper/solvers/exact/hybrid.py +224 -0
  141. synkit/Chem/Mapper/solvers/exact/hydrogen.py +264 -0
  142. synkit/Chem/Mapper/solvers/exact/kernel_solvers/__init__.py +1 -0
  143. synkit/Chem/Mapper/solvers/exact/kernel_solvers/branching.py +499 -0
  144. synkit/Chem/Mapper/solvers/exact/kernel_solvers/enumerate.py +1050 -0
  145. synkit/Chem/Mapper/solvers/exact/kernel_solvers/enumeration_result.py +30 -0
  146. synkit/Chem/Mapper/solvers/exact/kernel_solvers/kernel.py +256 -0
  147. synkit/Chem/Mapper/solvers/exact/kernel_solvers/milp.py +194 -0
  148. synkit/Chem/Mapper/solvers/exact/native/__init__.py +1 -0
  149. synkit/Chem/Mapper/solvers/exact/native/build.py +154 -0
  150. synkit/Chem/Mapper/solvers/exact/native/candidates.py +593 -0
  151. synkit/Chem/Mapper/solvers/exact/native/canonical.py +396 -0
  152. synkit/Chem/Mapper/solvers/exact/native/disk_frontier.py +415 -0
  153. synkit/Chem/Mapper/solvers/exact/native/frontier.py +390 -0
  154. synkit/Chem/Mapper/solvers/exact/native/hydrogen_quotient.py +213 -0
  155. synkit/Chem/Mapper/solvers/exact/native/its.py +528 -0
  156. synkit/Chem/Mapper/solvers/exact/native/native_distance.cpp +2803 -0
  157. synkit/Chem/Mapper/solvers/exact/native/orbit_aggregation.py +328 -0
  158. synkit/Chem/Mapper/solvers/exact/native/parallel.py +205 -0
  159. synkit/Chem/Mapper/solvers/exact/native/pmcd.py +203 -0
  160. synkit/Chem/Mapper/solvers/exact/native/runtime.py +40 -0
  161. synkit/Chem/Mapper/solvers/exact/native/search.py +294 -0
  162. synkit/Chem/Mapper/solvers/exact/native/separator.py +224 -0
  163. synkit/Chem/Mapper/solvers/exact/native_build.py +12 -0
  164. synkit/Chem/Mapper/solvers/exact/pabs/__init__.py +1 -0
  165. synkit/Chem/Mapper/solvers/exact/pabs/cost_lattice.py +43 -0
  166. synkit/Chem/Mapper/solvers/exact/pabs/factor_spectrum.py +110 -0
  167. synkit/Chem/Mapper/solvers/exact/pabs/multi_shell.py +368 -0
  168. synkit/Chem/Mapper/solvers/exact/pabs/propagation.py +679 -0
  169. synkit/Chem/Mapper/solvers/exact/pabs/propagation_bounds.py +436 -0
  170. synkit/Chem/Mapper/solvers/exact/pabs/propagation_limits.py +13 -0
  171. synkit/Chem/Mapper/solvers/exact/pabs/propagation_search.py +856 -0
  172. synkit/Chem/Mapper/solvers/exact/pabs/propagation_symmetry.py +66 -0
  173. synkit/Chem/Mapper/solvers/exact/pabs/residual_typed_bonds.py +136 -0
  174. synkit/Chem/Mapper/solvers/exact/pabs/reward_frontier.py +201 -0
  175. synkit/Chem/Mapper/solvers/exact/pabs/separator_bound.py +258 -0
  176. synkit/Chem/Mapper/solvers/exact/pabs/separator_spectrum.py +308 -0
  177. synkit/Chem/Mapper/solvers/exact/pabs/sparse_reward_bound.py +207 -0
  178. synkit/Chem/Mapper/solvers/exact/pabs/suffix_spectrum.py +299 -0
  179. synkit/Chem/Mapper/solvers/exact/search.py +55 -0
  180. synkit/Chem/Mapper/solvers/exact/seeds/__init__.py +1 -0
  181. synkit/Chem/Mapper/solvers/exact/seeds/seed.py +104 -0
  182. synkit/Chem/Mapper/solvers/exact/seeds/seed_fragments.py +148 -0
  183. synkit/Chem/Mapper/solvers/exact/seeds/seed_relaxation.py +181 -0
  184. synkit/Chem/Mapper/solvers/exact/symmetry.py +223 -0
  185. synkit/Chem/Mapper/solvers/native/__init__.py +4 -0
  186. synkit/Chem/Mapper/solvers/native/assignment.py +99 -0
  187. synkit/Chem/Mapper/solvers/native/astar.py +22 -0
  188. synkit/Chem/Mapper/solvers/native/build.py +385 -0
  189. synkit/Chem/Mapper/solvers/native/classification.py +641 -0
  190. synkit/Chem/Mapper/solvers/native/core.py +378 -0
  191. synkit/Chem/Mapper/solvers/native/cpp/CMakeLists.txt +120 -0
  192. synkit/Chem/Mapper/solvers/native/cpp/examples/map_reaction.cpp +29 -0
  193. synkit/Chem/Mapper/solvers/native/cpp/examples/slap_graph.cpp +63 -0
  194. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/assignment/assignment.h +18 -0
  195. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/astar/astar.h +18 -0
  196. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/astar/bound.h +33 -0
  197. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/core/pmcd_witness.h +16 -0
  198. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/core/runtime.h +130 -0
  199. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/hydrogen/hydrogen.h +73 -0
  200. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/mapper.h +175 -0
  201. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/mces/mces.h +9 -0
  202. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/pabs/seed.h +34 -0
  203. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/reaction/reaction.h +30 -0
  204. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/sepdp/sepdp.h +21 -0
  205. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/slap.h +72 -0
  206. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/solvers/solvers.h +16 -0
  207. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/symmetry/classification.h +140 -0
  208. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/symmetry/cpabs.h +33 -0
  209. synkit/Chem/Mapper/solvers/native/cpp/src/assignment/abb.cpp +302 -0
  210. synkit/Chem/Mapper/solvers/native/cpp/src/astar/CMakeLists.txt +12 -0
  211. synkit/Chem/Mapper/solvers/native/cpp/src/astar/README.md +47 -0
  212. synkit/Chem/Mapper/solvers/native/cpp/src/astar/astar.cpp +228 -0
  213. synkit/Chem/Mapper/solvers/native/cpp/src/astar/astar_graph.cpp +37 -0
  214. synkit/Chem/Mapper/solvers/native/cpp/src/astar/bound.cpp +212 -0
  215. synkit/Chem/Mapper/solvers/native/cpp/src/core/dispatch.cpp +272 -0
  216. synkit/Chem/Mapper/solvers/native/cpp/src/core/pmcd_witness.cpp +36 -0
  217. synkit/Chem/Mapper/solvers/native/cpp/src/hydrogen/hydrogen.cpp +380 -0
  218. synkit/Chem/Mapper/solvers/native/cpp/src/mces/README.md +57 -0
  219. synkit/Chem/Mapper/solvers/native/cpp/src/mces/mces.cpp +287 -0
  220. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/bridge.cpp +334 -0
  221. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/chemical_floor.inc +181 -0
  222. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/endpoint_symmetry.inc +104 -0
  223. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/exact_helpers.h +20 -0
  224. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/lp_assignment_certificate.h +165 -0
  225. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/lp_certificate.h +125 -0
  226. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/lp_model.h +365 -0
  227. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/lp_provider.inc +319 -0
  228. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/lp_rounding.inc +170 -0
  229. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/lp_test_hooks.h +342 -0
  230. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/objective_reduction.inc +137 -0
  231. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/overlap_dual.inc +351 -0
  232. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/profile_seed.inc +214 -0
  233. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/seed_heuristics.inc +640 -0
  234. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/sparse_embedding.cpp +430 -0
  235. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/sparse_root_lp.h +176 -0
  236. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/test_hooks.inc +262 -0
  237. synkit/Chem/Mapper/solvers/native/cpp/src/reaction/reaction.cpp +611 -0
  238. synkit/Chem/Mapper/solvers/native/cpp/src/sepdp/README.md +61 -0
  239. synkit/Chem/Mapper/solvers/native/cpp/src/sepdp/sepdp.cpp +253 -0
  240. synkit/Chem/Mapper/solvers/native/cpp/src/slap.cpp +810 -0
  241. synkit/Chem/Mapper/solvers/native/cpp/src/solvers/formulation.h +138 -0
  242. synkit/Chem/Mapper/solvers/native/cpp/src/solvers/maxsat.cpp +526 -0
  243. synkit/Chem/Mapper/solvers/native/cpp/src/solvers/ortools.cpp +475 -0
  244. synkit/Chem/Mapper/solvers/native/cpp/src/solvers/seed.h +80 -0
  245. synkit/Chem/Mapper/solvers/native/cpp/src/solvers/weighted_cnf.h +256 -0
  246. synkit/Chem/Mapper/solvers/native/cpp/src/supporting/README.md +43 -0
  247. synkit/Chem/Mapper/solvers/native/cpp/src/supporting/dfs.cpp +123 -0
  248. synkit/Chem/Mapper/solvers/native/cpp/src/supporting/edit_support.cpp +224 -0
  249. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/classification.cpp +553 -0
  250. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/classification_pipeline.cpp +124 -0
  251. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/classification_stream.h +13 -0
  252. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/cpabs.cpp +455 -0
  253. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/edge_deck.cpp +743 -0
  254. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/COPYRIGHT +44 -0
  255. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/LICENSE-2.0.txt +202 -0
  256. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/naugraph.c +732 -0
  257. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/naurng.c +94 -0
  258. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/naurng.h +49 -0
  259. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/nausparse.c +1771 -0
  260. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/nausparse.h +133 -0
  261. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/nautil.c +750 -0
  262. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/naututil.h +355 -0
  263. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/nauty.c +1256 -0
  264. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/nauty.h +1544 -0
  265. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/schreier.c +1131 -0
  266. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/schreier.h +70 -0
  267. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/sorttemplates.c +580 -0
  268. synkit/Chem/Mapper/solvers/native/hydrogen.py +711 -0
  269. synkit/Chem/Mapper/solvers/native/implicit.py +506 -0
  270. synkit/Chem/Mapper/solvers/native/implicit_certificate.py +187 -0
  271. synkit/Chem/Mapper/solvers/native/implicit_preparation.py +309 -0
  272. synkit/Chem/Mapper/solvers/native/lib/synkit_mapper.dll +0 -0
  273. synkit/Chem/Mapper/solvers/native/mces.py +16 -0
  274. synkit/Chem/Mapper/solvers/native/reaction.py +179 -0
  275. synkit/Chem/Mapper/solvers/native/reaction_build.py +277 -0
  276. synkit/Chem/Mapper/solvers/native/reaction_text.py +26 -0
  277. synkit/Chem/Mapper/solvers/native/runtime.py +242 -0
  278. synkit/Chem/Mapper/solvers/native/sepdp.py +47 -0
  279. synkit/Chem/Mapper/solvers/native/slap.py +266 -0
  280. synkit/Chem/Mapper/solvers/native/solvers.py +67 -0
  281. synkit/Chem/Mapper/solvers/native/stereo.py +86 -0
  282. synkit/Chem/Mapper/solvers/native/supporting.py +34 -0
  283. synkit/Chem/Mapper/solvers/native/symmetry.py +91 -0
  284. synkit/Chem/Mapper/solvers/slap/__init__.py +29 -0
  285. synkit/Chem/Mapper/solvers/slap/lap.py +174 -0
  286. synkit/Chem/Mapper/solvers/slap/sequential.py +696 -0
  287. synkit/Chem/Mapper/validation/__init__.py +1 -0
  288. synkit/Chem/Mapper/validation/aam_validator.py +442 -0
  289. synkit/Chem/Mapper/validation/annotations.py +274 -0
  290. synkit/Chem/Mapper/validation/bonds.py +124 -0
  291. synkit/Chem/Mapper/validation/cohorts.py +72 -0
  292. synkit/Chem/Mapper/validation/identifiability.py +150 -0
  293. synkit/Chem/Mapper/validation/numerical_scope.py +36 -0
  294. synkit/Chem/Mapper/validation/orbits.py +137 -0
  295. synkit/Chem/Molecule/__init__.py +0 -0
  296. synkit/Chem/Molecule/_chirality_loci.py +165 -0
  297. synkit/Chem/Molecule/_chirality_types.py +231 -0
  298. synkit/Chem/Molecule/_helical_loci.py +167 -0
  299. synkit/Chem/Molecule/_stereo_axis_evidence.py +202 -0
  300. synkit/Chem/Molecule/_stereo_element_detection.py +193 -0
  301. synkit/Chem/Molecule/_stereo_orientation_constraints.py +511 -0
  302. synkit/Chem/Molecule/_tetrahedral_carriers.py +63 -0
  303. synkit/Chem/Molecule/atom_features.py +379 -0
  304. synkit/Chem/Molecule/chirality.py +1041 -0
  305. synkit/Chem/Molecule/cip_assignment.py +569 -0
  306. synkit/Chem/Molecule/cip_ranking.py +843 -0
  307. synkit/Chem/Molecule/coordinate_stereo.py +503 -0
  308. synkit/Chem/Molecule/derived_stereo_naming.py +165 -0
  309. synkit/Chem/Molecule/descriptors.py +452 -0
  310. synkit/Chem/Molecule/formula.py +249 -0
  311. synkit/Chem/Molecule/global_stereo.py +175 -0
  312. synkit/Chem/Molecule/graph_annotator.py +300 -0
  313. synkit/Chem/Molecule/standardize.py +817 -0
  314. synkit/Chem/Molecule/stereo_evidence.py +179 -0
  315. synkit/Chem/Molecule/stereo_perception.py +1032 -0
  316. synkit/Chem/Molecule/valence.py +123 -0
  317. synkit/Chem/Reaction/Mapper/__init__.py +44 -0
  318. synkit/Chem/Reaction/__init__.py +20 -0
  319. synkit/Chem/Reaction/aam_validator.py +5 -0
  320. synkit/Chem/Reaction/balance_check.py +138 -0
  321. synkit/Chem/Reaction/canon_rsmi.py +249 -0
  322. synkit/Chem/Reaction/cleaning.py +58 -0
  323. synkit/Chem/Reaction/deionize.py +199 -0
  324. synkit/Chem/Reaction/explicit_h_audit.py +232 -0
  325. synkit/Chem/Reaction/fix_aam.py +64 -0
  326. synkit/Chem/Reaction/neutralize.py +207 -0
  327. synkit/Chem/Reaction/radical_wildcard.py +224 -0
  328. synkit/Chem/Reaction/standardize.py +203 -0
  329. synkit/Chem/Reaction/tautomerize.py +205 -0
  330. synkit/Chem/__init__.py +1 -0
  331. synkit/Chem/utils.py +315 -0
  332. synkit/Data/__init__.py +0 -0
  333. synkit/Data/aldol.json.gz +1 -0
  334. synkit/Data/gen_partial_aam.py +147 -0
  335. synkit/Data/paracetamol.json.gz +1 -0
  336. synkit/Graph/Canon/__init__.py +16 -0
  337. synkit/Graph/Canon/_native/__init__.py +13 -0
  338. synkit/Graph/Canon/_native/build.py +93 -0
  339. synkit/Graph/Canon/_native/lib/synkit_canon.dll +0 -0
  340. synkit/Graph/Canon/_native/prepare.py +49 -0
  341. synkit/Graph/Canon/_native/runtime.py +140 -0
  342. synkit/Graph/Canon/_native/search.py +137 -0
  343. synkit/Graph/Canon/canon_algs.py +228 -0
  344. synkit/Graph/Canon/canon_graph.py +531 -0
  345. synkit/Graph/Canon/cpp/CMakeLists.txt +42 -0
  346. synkit/Graph/Canon/cpp/README.md +123 -0
  347. synkit/Graph/Canon/cpp/examples/canonical_graph.cpp +16 -0
  348. synkit/Graph/Canon/cpp/include/synkit_canon/canon.h +85 -0
  349. synkit/Graph/Canon/cpp/src/canonical.cpp +391 -0
  350. synkit/Graph/Canon/cpp/src/symmetry.h +83 -0
  351. synkit/Graph/Canon/cpp/tests/symmetry.cpp +93 -0
  352. synkit/Graph/Canon/exact.py +1020 -0
  353. synkit/Graph/Canon/exact_refinement.py +368 -0
  354. synkit/Graph/Canon/nauty.py +143 -0
  355. synkit/Graph/Context/__init__.py +0 -0
  356. synkit/Graph/Context/hier_context.py +244 -0
  357. synkit/Graph/Context/radius_expand.py +252 -0
  358. synkit/Graph/FG/__init__.py +19 -0
  359. synkit/Graph/FG/api.py +39 -0
  360. synkit/Graph/FG/audit.py +155 -0
  361. synkit/Graph/FG/catalog.py +919 -0
  362. synkit/Graph/FG/detector.py +288 -0
  363. synkit/Graph/FG/model.py +97 -0
  364. synkit/Graph/FG/recognizers.py +274 -0
  365. synkit/Graph/FG/ring_system.py +154 -0
  366. synkit/Graph/Feature/Descriptors/topology.py +857 -0
  367. synkit/Graph/Feature/Fingerprint/__init__.py +0 -0
  368. synkit/Graph/Feature/Fingerprint/wl_rxn_fps.py +222 -0
  369. synkit/Graph/Feature/__init__.py +5 -0
  370. synkit/Graph/Feature/graph_descriptors.py +323 -0
  371. synkit/Graph/Feature/graph_fps.py +96 -0
  372. synkit/Graph/Feature/graph_signature.py +239 -0
  373. synkit/Graph/Feature/hash_fps.py +133 -0
  374. synkit/Graph/Feature/morgan_fps.py +88 -0
  375. synkit/Graph/Feature/path_fps.py +81 -0
  376. synkit/Graph/Feature/wl_hash.py +134 -0
  377. synkit/Graph/Fusion/__init__.py +85 -0
  378. synkit/Graph/Fusion/candidate.py +559 -0
  379. synkit/Graph/Fusion/constructor.py +877 -0
  380. synkit/Graph/Fusion/identity.py +184 -0
  381. synkit/Graph/Fusion/interface.py +700 -0
  382. synkit/Graph/Fusion/search.py +241 -0
  383. synkit/Graph/Hyrogen/__init__.py +6 -0
  384. synkit/Graph/Hyrogen/_misc.py +591 -0
  385. synkit/Graph/Hyrogen/hcomplete.py +995 -0
  386. synkit/Graph/Hyrogen/hcomplete_legacy.py +368 -0
  387. synkit/Graph/Hyrogen/hcompletion_algorithms.py +713 -0
  388. synkit/Graph/Hyrogen/hextend.py +570 -0
  389. synkit/Graph/Hyrogen/hextend_legacy.py +215 -0
  390. synkit/Graph/ITS/__init__.py +4 -0
  391. synkit/Graph/ITS/its_builder.py +120 -0
  392. synkit/Graph/ITS/its_construction.py +719 -0
  393. synkit/Graph/ITS/its_decompose.py +572 -0
  394. synkit/Graph/ITS/its_destruction.py +331 -0
  395. synkit/Graph/ITS/its_expand.py +1251 -0
  396. synkit/Graph/ITS/its_relabel.py +187 -0
  397. synkit/Graph/ITS/its_reverter.py +356 -0
  398. synkit/Graph/ITS/normalize_aam.py +145 -0
  399. synkit/Graph/ITS/partial_its.py +238 -0
  400. synkit/Graph/ITS/rc_extractor.py +656 -0
  401. synkit/Graph/ITS/stereo.py +74 -0
  402. synkit/Graph/MTG/__init__.py +107 -0
  403. synkit/Graph/MTG/gluing.py +451 -0
  404. synkit/Graph/MTG/history.py +928 -0
  405. synkit/Graph/MTG/inventory.py +118 -0
  406. synkit/Graph/MTG/local.py +447 -0
  407. synkit/Graph/MTG/mcs_matcher.py +246 -0
  408. synkit/Graph/MTG/mtg.py +611 -0
  409. synkit/Graph/MTG/mtg_explore.py +74 -0
  410. synkit/Graph/MTG/observation.py +209 -0
  411. synkit/Graph/MTG/process.py +577 -0
  412. synkit/Graph/MTG/queries.py +66 -0
  413. synkit/Graph/MTG/realization.py +95 -0
  414. synkit/Graph/MTG/series_parallel.py +107 -0
  415. synkit/Graph/MTG/utils.py +424 -0
  416. synkit/Graph/Matcher/__init__.py +10 -0
  417. synkit/Graph/Matcher/approx_mcs.py +1264 -0
  418. synkit/Graph/Matcher/auto_est.py +736 -0
  419. synkit/Graph/Matcher/automorphism.py +314 -0
  420. synkit/Graph/Matcher/batch_cluster.py +224 -0
  421. synkit/Graph/Matcher/dedup_matches.py +247 -0
  422. synkit/Graph/Matcher/graph_cluster.py +191 -0
  423. synkit/Graph/Matcher/graph_matcher.py +312 -0
  424. synkit/Graph/Matcher/graph_morphism.py +421 -0
  425. synkit/Graph/Matcher/mcs_matcher.py +804 -0
  426. synkit/Graph/Matcher/multi_turbo_iso.py +186 -0
  427. synkit/Graph/Matcher/orbit.py +284 -0
  428. synkit/Graph/Matcher/partial_matcher.py +694 -0
  429. synkit/Graph/Matcher/sing.py +544 -0
  430. synkit/Graph/Matcher/subgraph_matcher.py +812 -0
  431. synkit/Graph/Matcher/turbo_iso.py +272 -0
  432. synkit/Graph/Matcher/wl_sel.py +537 -0
  433. synkit/Graph/Mech/__init__.py +26 -0
  434. synkit/Graph/Mech/conversion.py +1466 -0
  435. synkit/Graph/Mech/conversion_debug.py +237 -0
  436. synkit/Graph/Mech/electron_accounting.py +200 -0
  437. synkit/Graph/Mech/lwg_editor.py +406 -0
  438. synkit/Graph/Mech/lwg_ops.py +313 -0
  439. synkit/Graph/Morphism/__init__.py +99 -0
  440. synkit/Graph/Morphism/constraints.py +460 -0
  441. synkit/Graph/Morphism/llg.py +999 -0
  442. synkit/Graph/Morphism/morphism.py +349 -0
  443. synkit/Graph/Morphism/stereo.py +258 -0
  444. synkit/Graph/Morphism/stereo_morphism.py +383 -0
  445. synkit/Graph/Morphism/stereo_morphism_helpers.py +957 -0
  446. synkit/Graph/Morphism/stereo_morphism_types.py +117 -0
  447. synkit/Graph/Stereo/__init__.py +376 -0
  448. synkit/Graph/Stereo/_descriptor_core.py +207 -0
  449. synkit/Graph/Stereo/_descriptor_serialization.py +138 -0
  450. synkit/Graph/Stereo/canonical.py +648 -0
  451. synkit/Graph/Stereo/changes.py +964 -0
  452. synkit/Graph/Stereo/composition.py +391 -0
  453. synkit/Graph/Stereo/configured.py +946 -0
  454. synkit/Graph/Stereo/couplings.py +245 -0
  455. synkit/Graph/Stereo/descriptors.py +1030 -0
  456. synkit/Graph/Stereo/enumeration.py +597 -0
  457. synkit/Graph/Stereo/extended_descriptors.py +406 -0
  458. synkit/Graph/Stereo/global_configured.py +152 -0
  459. synkit/Graph/Stereo/global_stereo.py +304 -0
  460. synkit/Graph/Stereo/identity.py +648 -0
  461. synkit/Graph/Stereo/interchange.py +496 -0
  462. synkit/Graph/Stereo/legacy.py +460 -0
  463. synkit/Graph/Stereo/matching.py +899 -0
  464. synkit/Graph/Stereo/native.py +154 -0
  465. synkit/Graph/Stereo/orbits.py +668 -0
  466. synkit/Graph/Stereo/outcomes.py +99 -0
  467. synkit/Graph/Stereo/pairwise.py +350 -0
  468. synkit/Graph/Stereo/rdkit_adapter.py +662 -0
  469. synkit/Graph/Stereo/rdkit_identity.py +118 -0
  470. synkit/Graph/Stereo/rdkit_resonance.py +140 -0
  471. synkit/Graph/Stereo/reaction_interchange.py +266 -0
  472. synkit/Graph/Stereo/semantics.py +296 -0
  473. synkit/Graph/Stereo/supports.py +257 -0
  474. synkit/Graph/Stereo/wire.py +339 -0
  475. synkit/Graph/Wildcard/__init__.py +0 -0
  476. synkit/Graph/Wildcard/fuse_graph.py +156 -0
  477. synkit/Graph/Wildcard/graph_wc.py +303 -0
  478. synkit/Graph/Wildcard/its_merge.py +857 -0
  479. synkit/Graph/Wildcard/radwc.py +114 -0
  480. synkit/Graph/Wildcard/wc_matcher.py +576 -0
  481. synkit/Graph/Wildcard/wildcard.py +225 -0
  482. synkit/Graph/__init__.py +17 -0
  483. synkit/Graph/canon_graph.py +14 -0
  484. synkit/Graph/syn_graph.py +199 -0
  485. synkit/Graph/utils.py +324 -0
  486. synkit/IO/__init__.py +4 -0
  487. synkit/IO/chem_converter.py +1059 -0
  488. synkit/IO/combinatorial/__init__.py +8 -0
  489. synkit/IO/combinatorial/gml_to_graph.py +223 -0
  490. synkit/IO/combinatorial/graph_to_gml.py +280 -0
  491. synkit/IO/combinatorial/graph_to_smarts.py +186 -0
  492. synkit/IO/combinatorial/smarts_expander.py +147 -0
  493. synkit/IO/combinatorial/smarts_generalizer.py +133 -0
  494. synkit/IO/combinatorial/smarts_to_graph.py +179 -0
  495. synkit/IO/conversion.py +42 -0
  496. synkit/IO/data_io.py +269 -0
  497. synkit/IO/data_process.py +143 -0
  498. synkit/IO/debug.py +73 -0
  499. synkit/IO/gml_to_nx.py +150 -0
  500. synkit/IO/graph_to_mol.py +199 -0
  501. synkit/IO/mol_to_graph.py +1105 -0
  502. synkit/IO/mol_to_graph_chemistry.py +545 -0
  503. synkit/IO/nx_to_gml.py +209 -0
  504. synkit/IO/smiles_to_id.py +123 -0
  505. synkit/Mechanism/__init__.py +151 -0
  506. synkit/Mechanism/adapters.py +136 -0
  507. synkit/Mechanism/audit.py +323 -0
  508. synkit/Mechanism/benchmark.py +638 -0
  509. synkit/Mechanism/compatibility.py +13 -0
  510. synkit/Mechanism/drawing.py +85 -0
  511. synkit/Mechanism/electron_models.py +725 -0
  512. synkit/Mechanism/equivalence.py +521 -0
  513. synkit/Mechanism/evidence.py +262 -0
  514. synkit/Mechanism/interchange.py +141 -0
  515. synkit/Mechanism/model.py +918 -0
  516. synkit/Mechanism/neighbor_stereo.py +312 -0
  517. synkit/Mechanism/radical_data.py +740 -0
  518. synkit/Mechanism/replay.py +986 -0
  519. synkit/Mechanism/schema.py +264 -0
  520. synkit/Mechanism/stereo_state.py +416 -0
  521. synkit/Mechanism/symbols.py +91 -0
  522. synkit/Rule/Apply/__init__.py +35 -0
  523. synkit/Rule/Apply/conditions.py +45 -0
  524. synkit/Rule/Apply/dpo.py +998 -0
  525. synkit/Rule/Apply/rule_matcher.py +218 -0
  526. synkit/Rule/Apply/validation.py +354 -0
  527. synkit/Rule/Compose/__init__.py +106 -0
  528. synkit/Rule/Compose/_identity.py +43 -0
  529. synkit/Rule/Compose/composition.py +976 -0
  530. synkit/Rule/Compose/laws.py +439 -0
  531. synkit/Rule/Compose/search.py +771 -0
  532. synkit/Rule/Compose/synrule.py +170 -0
  533. synkit/Rule/Modify/__init__.py +0 -0
  534. synkit/Rule/Modify/implict_rule.py +56 -0
  535. synkit/Rule/Modify/longest_path.py +92 -0
  536. synkit/Rule/Modify/molecule_rule.py +115 -0
  537. synkit/Rule/Modify/prune_templates.py +78 -0
  538. synkit/Rule/Modify/rule_utils.py +194 -0
  539. synkit/Rule/Modify/strip_rule.py +97 -0
  540. synkit/Rule/__init__.py +28 -0
  541. synkit/Rule/generic_stereo.py +884 -0
  542. synkit/Rule/stereo_identity.py +271 -0
  543. synkit/Rule/syn_rule.py +1090 -0
  544. synkit/Synthesis/MSR/__init__.py +0 -0
  545. synkit/Synthesis/MSR/path_finder.py +239 -0
  546. synkit/Synthesis/Metrics/__init__.py +0 -0
  547. synkit/Synthesis/Metrics/_base.py +52 -0
  548. synkit/Synthesis/Metrics/_plot.py +129 -0
  549. synkit/Synthesis/Metrics/_ranking.py +179 -0
  550. synkit/Synthesis/RBL/__init__.py +80 -0
  551. synkit/Synthesis/RBL/engine.py +1249 -0
  552. synkit/Synthesis/RBL/matching.py +652 -0
  553. synkit/Synthesis/RBL/overlap.py +336 -0
  554. synkit/Synthesis/RBL/pipeline.py +376 -0
  555. synkit/Synthesis/RBL/policy.py +139 -0
  556. synkit/Synthesis/RBL/proof.py +370 -0
  557. synkit/Synthesis/RBL/reaction.py +506 -0
  558. synkit/Synthesis/RBL/state.py +169 -0
  559. synkit/Synthesis/RBL/validation.py +1060 -0
  560. synkit/Synthesis/Reactor/__init__.py +60 -0
  561. synkit/Synthesis/Reactor/core/__init__.py +1 -0
  562. synkit/Synthesis/Reactor/core/engine.py +967 -0
  563. synkit/Synthesis/Reactor/core/product.py +621 -0
  564. synkit/Synthesis/Reactor/core/rewrite.py +561 -0
  565. synkit/Synthesis/Reactor/core/strategy.py +44 -0
  566. synkit/Synthesis/Reactor/matching/__init__.py +1 -0
  567. synkit/Synthesis/Reactor/matching/mixin.py +920 -0
  568. synkit/Synthesis/Reactor/matching/policy.py +345 -0
  569. synkit/Synthesis/Reactor/matching/symmetry.py +309 -0
  570. synkit/Synthesis/Reactor/output/__init__.py +1 -0
  571. synkit/Synthesis/Reactor/output/deduplication.py +703 -0
  572. synkit/Synthesis/Reactor/output/policy.py +16 -0
  573. synkit/Synthesis/Reactor/output/serialization.py +485 -0
  574. synkit/Synthesis/Reactor/output/structural.py +893 -0
  575. synkit/Synthesis/Reactor/stereo/__init__.py +1 -0
  576. synkit/Synthesis/Reactor/stereo/assignment.py +35 -0
  577. synkit/Synthesis/Reactor/stereo/mixin.py +780 -0
  578. synkit/Synthesis/Reactor/variants/__init__.py +1 -0
  579. synkit/Synthesis/Reactor/variants/imbalanced.py +180 -0
  580. synkit/Synthesis/Reactor/variants/partial.py +84 -0
  581. synkit/Synthesis/Reactor/workflow/__init__.py +1 -0
  582. synkit/Synthesis/Reactor/workflow/batch.py +550 -0
  583. synkit/Synthesis/Reactor/workflow/benchmark.py +143 -0
  584. synkit/Synthesis/Reactor/workflow/postprocess.py +267 -0
  585. synkit/Synthesis/Reactor/workflow/rule_filter.py +210 -0
  586. synkit/Synthesis/__init__.py +0 -0
  587. synkit/Synthesis/reactor_utils.py +316 -0
  588. synkit/Utils/__init__.py +0 -0
  589. synkit/Utils/utils.py +172 -0
  590. synkit/Vis/__init__.py +91 -0
  591. synkit/Vis/crn/__init__.py +40 -0
  592. synkit/Vis/epd/__init__.py +20 -0
  593. synkit/Vis/epd/arrows.py +425 -0
  594. synkit/Vis/epd/chem.py +197 -0
  595. synkit/Vis/epd/constants.py +83 -0
  596. synkit/Vis/epd/layout.py +283 -0
  597. synkit/Vis/epd/mapping.py +51 -0
  598. synkit/Vis/epd/models.py +136 -0
  599. synkit/Vis/epd/render.py +543 -0
  600. synkit/Vis/epd/utils.py +108 -0
  601. synkit/Vis/epd/visualizer.py +811 -0
  602. synkit/Vis/graph_visualizer.py +375 -0
  603. synkit/Vis/its/__init__.py +19 -0
  604. synkit/Vis/its/drawer.py +615 -0
  605. synkit/Vis/its_drawer.py +35 -0
  606. synkit/Vis/molecule/__init__.py +31 -0
  607. synkit/Vis/molecule/drawer.py +563 -0
  608. synkit/Vis/molecule/reaction.py +285 -0
  609. synkit/Vis/molecule_drawer.py +33 -0
  610. synkit/Vis/mtg/__init__.py +17 -0
  611. synkit/Vis/mtg/drawer.py +769 -0
  612. synkit/Vis/mtg_drawer.py +34 -0
  613. synkit/Vis/pdf_writer.py +146 -0
  614. synkit/Vis/reaction/__init__.py +19 -0
  615. synkit/Vis/reaction/rule.py +141 -0
  616. synkit/Vis/reaction/rxn.py +532 -0
  617. synkit/Vis/reaction_drawer.py +41 -0
  618. synkit/Vis/space/__init__.py +17 -0
  619. synkit/Vis/space/chemical.py +80 -0
  620. synkit/Vis/space/embedding.py +87 -0
  621. synkit/Vis/vis_synedu/Vis/__init__.py +7 -0
  622. synkit/Vis/vis_synedu/Vis/dpo.py +862 -0
  623. synkit/Vis/vis_synedu/rxn_vis.py +377 -0
  624. synkit/Vis/vis_synedu/vis.py +501 -0
  625. synkit/Vis/visual_drawer.py +215 -0
  626. synkit/Vis/visual_model.py +520 -0
  627. synkit/__init__.py +3 -0
  628. synkit/_native.py +91 -0
  629. synkit/examples.py +50 -0
  630. synkit/version.py +23 -0
  631. synkit-1.6.5.dist-info/METADATA +156 -0
  632. synkit-1.6.5.dist-info/RECORD +636 -0
  633. synkit-1.6.5.dist-info/WHEEL +4 -0
  634. synkit-1.6.5.dist-info/licenses/LICENSE +21 -0
  635. synkit-1.6.5.dist-info/licenses/synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/COPYRIGHT +44 -0
  636. synkit-1.6.5.dist-info/licenses/synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/LICENSE-2.0.txt +202 -0
@@ -0,0 +1,857 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Optional, Tuple, Union, Set
4
+ import logging
5
+
6
+ import networkx as nx
7
+
8
+ GraphType = Union[nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph]
9
+
10
+ __all__ = ["ITSMerge", "fuse_its_graphs"]
11
+
12
+
13
+ class ITSMerge:
14
+ """Merge two ITS graphs given a node mapping between them.
15
+
16
+ This class encapsulates the logic of fusing two ITS graphs (e.g. from
17
+ wildcard pattern matching) in an object-oriented way. The result is a
18
+ **fused** graph that:
19
+
20
+ * starts as a copy of the **host** graph,
21
+ * merges ITS typing (``typesGH``) on mapped node pairs,
22
+ * adds leftover (non-mapped, non-wildcard) pattern nodes and edges, and
23
+ * optionally removes all wildcard nodes in the final fused graph.
24
+
25
+ Orientation
26
+ -----------
27
+ The graph whose nodes appear as **values** of the mapping is treated as
28
+ the **host**; the other graph is the **pattern**. If the mapping is
29
+ given in the opposite direction (host → pattern), the class detects this
30
+ and automatically inverts the mapping.
31
+
32
+ ITS merging semantics
33
+ ---------------------
34
+ * For mapped node pairs (p → h), the ``typesGH`` attribute is merged:
35
+ the hydrogen count entries (index 2 in each inner tuple) are set to the
36
+ **maximum** of host vs pattern.
37
+ * Leftover pattern nodes:
38
+ - If ``element == wildcard_element``, they are **ignored**.
39
+ - Otherwise they are added as new nodes with new IDs and edges are
40
+ created according to the pattern topology.
41
+ * If ``remove_wildcards`` is ``True`` (default), **all wildcard
42
+ nodes** (``element == wildcard_element``) are removed from the fused
43
+ graph; their incident edges disappear. If ``False``, wildcard nodes are
44
+ kept.
45
+
46
+ .. rubric:: Examples
47
+
48
+ Simple usage with integer-labeled graphs:
49
+
50
+ .. code-block:: python
51
+
52
+ import networkx as nx
53
+ from synkit.Graph.ITS.its_merge import ITSMerge
54
+
55
+ G1 = nx.Graph()
56
+ G2 = nx.Graph()
57
+
58
+ # Example: two ITS graphs with 'typesGH' and 'element' attributes
59
+ G1.add_node(1, element="C", typesGH=(("C", False, 2, 0, ["O"]),
60
+ ("C", False, 2, 0, ["O"])))
61
+ G2.add_node(10, element="C", typesGH=(("C", False, 1, 0, ["O"]),
62
+ ("C", False, 1, 0, ["O"])))
63
+
64
+ mapping = {1: 10} # pattern node → host node
65
+
66
+ merger = ITSMerge(G1, G2, mapping, remove_wildcards=True).merge()
67
+ F = merger.fused_graph
68
+ print("Fused nodes:", F.nodes(data=True))
69
+
70
+ :param G1: First input ITS graph.
71
+ :type G1: GraphType
72
+ :param G2: Second input ITS graph.
73
+ :type G2: GraphType
74
+ :param mapping: Node mapping between the graphs. Must be a bijection
75
+ either from pattern → host or host → pattern; the class detects
76
+ orientation automatically.
77
+ :type mapping: dict[Any, Any]
78
+ :param types_key: Node attribute key holding the ITS typing tuple,
79
+ e.g. ``(('C', False, 3, 0, ['O']), ('C', False, 3, 0, ['O']))``.
80
+ :type types_key: str
81
+ :param element_key: Node attribute key for element / atom type.
82
+ :type element_key: str
83
+ :param wildcard_element: Value of ``element_key`` that denotes
84
+ wildcard nodes.
85
+ :type wildcard_element: str
86
+ :param remove_wildcards: If ``True``, remove wildcard nodes in the final
87
+ fused graph. If ``False``, wildcard nodes are kept.
88
+ :type remove_wildcards: bool
89
+ :param logger: Optional logger for debug output.
90
+ :type logger: logging.Logger | None
91
+ """
92
+
93
+ def __init__(
94
+ self,
95
+ G1: GraphType,
96
+ G2: GraphType,
97
+ mapping: Dict[Any, Any],
98
+ *,
99
+ types_key: str = "typesGH",
100
+ element_key: str = "element",
101
+ wildcard_element: str = "*",
102
+ remove_wildcards: bool = True,
103
+ logger: Optional[logging.Logger] = None,
104
+ ) -> None:
105
+ self._G1 = G1
106
+ self._G2 = G2
107
+ self._mapping_input: Dict[Any, Any] = dict(mapping)
108
+ self._types_key = types_key
109
+ self._element_key = element_key
110
+ self._wildcard_element = wildcard_element
111
+ self._remove_wildcards_flag = remove_wildcards
112
+ self._logger = logger or logging.getLogger(__name__)
113
+
114
+ self._host: GraphType
115
+ self._pattern: GraphType
116
+ self._pat_is_G1: bool
117
+ self._pat_to_host: Dict[Any, Any]
118
+
119
+ self._fused: Optional[GraphType] = None
120
+
121
+ self._orient_graphs_and_mapping()
122
+
123
+ # ------------------------------------------------------------------
124
+ # Public API
125
+ # ------------------------------------------------------------------
126
+ def merge(self) -> "ITSMerge":
127
+ """Execute the ITS fusion process.
128
+
129
+ The method:
130
+
131
+ 1. Starts from a copy of the host graph.
132
+ 2. Merges ``typesGH`` attributes on mapped node pairs.
133
+ 3. Adds leftover non-wildcard pattern nodes.
134
+ 4. Adds pattern edges between mapped/added nodes.
135
+ 5. Optionally removes wildcard nodes from the fused graph.
136
+
137
+ :return: Self, with :attr:`fused_graph` updated.
138
+ :rtype: ITSMerge
139
+ """
140
+ fused = self._host.copy()
141
+ self._merge_anchor_nodes(fused)
142
+ leftover_map = self._add_leftover_pattern_nodes(fused)
143
+ self._add_pattern_edges(fused, leftover_map)
144
+ if self._remove_wildcards_flag:
145
+ self._remove_wildcards(fused)
146
+ self._normalize_atom_maps(fused)
147
+ self._fused = fused
148
+ return self
149
+
150
+ # ------------------------------------------------------------------
151
+ # Properties
152
+ # ------------------------------------------------------------------
153
+ @property
154
+ def fused_graph(self) -> GraphType:
155
+ """Fused ITS graph.
156
+
157
+ The graph is in the **host's node ID space**, plus any extra IDs
158
+ for leftover pattern nodes. Wildcard nodes may have been removed,
159
+ depending on ``remove_wildcards``.
160
+
161
+ :return: Fused ITS graph.
162
+ :rtype: GraphType
163
+ :raises RuntimeError: If :meth:`merge` has not been called yet.
164
+ """
165
+ if self._fused is None:
166
+ raise RuntimeError(
167
+ "ITSMerge.merge() must be called before accessing fused_graph."
168
+ )
169
+ return self._fused
170
+
171
+ @property
172
+ def host_graph(self) -> GraphType:
173
+ """Graph that was treated as the **host** for merging.
174
+
175
+ :return: Host graph.
176
+ :rtype: GraphType
177
+ """
178
+ return self._host
179
+
180
+ @property
181
+ def pattern_graph(self) -> GraphType:
182
+ """Graph that was treated as the **pattern** for merging.
183
+
184
+ :return: Pattern graph.
185
+ :rtype: GraphType
186
+ """
187
+ return self._pattern
188
+
189
+ @property
190
+ def pattern_to_host(self) -> Dict[Any, Any]:
191
+ """Mapping from pattern node IDs to host node IDs.
192
+
193
+ Orientation is resolved automatically during construction.
194
+
195
+ :return: Pattern → host node mapping.
196
+ :rtype: dict[Any, Any]
197
+ """
198
+ return dict(self._pat_to_host)
199
+
200
+ def __repr__(self) -> str:
201
+ """Short textual representation for debugging.
202
+
203
+ :return: Summary string with node/edge counts when available.
204
+ :rtype: str
205
+ """
206
+ fused_info = "unmerged"
207
+ if self._fused is not None:
208
+ fused_info = (
209
+ f"fused_nodes={self._fused.number_of_nodes()}, "
210
+ f"fused_edges={self._fused.number_of_edges()}"
211
+ )
212
+ return (
213
+ f"<ITSMerge host_nodes={self._host.number_of_nodes()} "
214
+ f"pattern_nodes={self._pattern.number_of_nodes()} "
215
+ f"remove_wildcards={self._remove_wildcards_flag} "
216
+ f"{fused_info}>"
217
+ )
218
+
219
+ # ------------------------------------------------------------------
220
+ # Internal: orientation & mapping
221
+ # ------------------------------------------------------------------
222
+ def _orient_graphs_and_mapping(self) -> None:
223
+ """
224
+ Decide which graph is host and which is pattern, and orient mapping.
225
+
226
+ Uses score-based voting: for each of the four candidate orientations
227
+ (direct/inverted mapping × G1-as-pattern/G2-as-pattern) count how many
228
+ (key, value) pairs in the candidate mapping are consistent with
229
+ key∈pattern and value∈host. The highest-scoring orientation wins; ties
230
+ are broken by preferring G1-as-pattern with the direct mapping.
231
+ """
232
+ mapping = self._mapping_input
233
+ inv = {v: k for k, v in mapping.items()}
234
+
235
+ def _score(m: Dict[Any, Any], pattern: GraphType, host: GraphType) -> int:
236
+ return sum(1 for k, v in m.items() if k in pattern and v in host)
237
+
238
+ candidates = [
239
+ (mapping, self._G1, self._G2, True),
240
+ (inv, self._G1, self._G2, True),
241
+ (mapping, self._G2, self._G1, False),
242
+ (inv, self._G2, self._G1, False),
243
+ ]
244
+
245
+ best_score = -1
246
+ best = None
247
+ for m, pat, host, pat_is_G1 in candidates:
248
+ score = _score(m, pat, host)
249
+ if score > best_score:
250
+ best_score = score
251
+ best = (m, pat, host, pat_is_G1)
252
+
253
+ if best is None or best_score == 0:
254
+ raise ValueError(
255
+ "ITSMerge: cannot orient mapping; keys/values do not consistently "
256
+ "belong to G1/G2."
257
+ )
258
+
259
+ mapping, pattern, host, pat_is_G1 = best
260
+ self._pattern, self._host = pattern, host
261
+ self._pat_is_G1 = pat_is_G1
262
+ self._pat_to_host = dict(mapping)
263
+
264
+ # ------------------------------------------------------------------
265
+ # Internal: wildcard detection helper
266
+ # ------------------------------------------------------------------
267
+ def _is_wc_node(self, attrs: Dict[str, Any]) -> bool:
268
+ """Return True if a node's element is the wildcard element."""
269
+ el = attrs.get(self._element_key)
270
+ wc = self._wildcard_element
271
+ wc0 = wc[0] if isinstance(wc, (tuple, list)) else wc
272
+ if isinstance(el, (tuple, list)):
273
+ return el[0] == wc0
274
+ return el == wc0
275
+
276
+ def _host_has_wc_neighbor(self, h_node: Any) -> bool:
277
+ """Return True if a host node has at least one wildcard neighbor."""
278
+ if h_node not in self._host:
279
+ return False
280
+ return any(
281
+ self._is_wc_node(self._host.nodes[nb])
282
+ for nb in self._host.neighbors(h_node)
283
+ )
284
+
285
+ # ------------------------------------------------------------------
286
+ # Internal: ITS types merging
287
+ # ------------------------------------------------------------------
288
+ def _merge_types_gh(
289
+ self,
290
+ host_data: Dict[str, Any],
291
+ pat_data: Dict[str, Any],
292
+ ) -> Optional[Tuple[Tuple[Any, ...], Tuple[Any, ...]]]:
293
+ """Merge host vs pattern ``typesGH``, keeping max hydrogen counts.
294
+
295
+ Only index 2 (H count) of each side is altered; remaining entries are
296
+ taken from the host when present.
297
+
298
+ :param host_data: Host node attribute dictionary.
299
+ :type host_data: dict[str, Any]
300
+ :param pat_data: Pattern node attribute dictionary.
301
+ :type pat_data: dict[str, Any]
302
+ :return: Merged ``typesGH`` tuple (left, right) or ``None`` if both
303
+ are missing.
304
+ :rtype: tuple[tuple[Any, ...], tuple[Any, ...]] | None
305
+ """
306
+ t_host = host_data.get(self._types_key)
307
+ t_pat = pat_data.get(self._types_key)
308
+
309
+ if t_host is None and t_pat is None:
310
+ return None
311
+ if t_host is None:
312
+ t_host = t_pat
313
+ if t_host is None:
314
+ return None
315
+
316
+ try:
317
+ left_h = list(t_host[0])
318
+ right_h = list(t_host[1])
319
+ except Exception: # pragma: no cover - defensive
320
+ self._logger.debug(
321
+ "ITSMerge: host typesGH not a 2-tuple, leaving as-is: %r", t_host
322
+ )
323
+ return t_host
324
+
325
+ if t_pat is not None:
326
+ try:
327
+ left_p = t_pat[0]
328
+ right_p = t_pat[1]
329
+ if len(left_h) > 2 and len(left_p) > 2:
330
+ left_h[2] = max(left_h[2], left_p[2])
331
+ if len(right_h) > 2 and len(right_p) > 2:
332
+ right_h[2] = max(right_h[2], right_p[2])
333
+ except Exception: # pragma: no cover - defensive
334
+ self._logger.debug(
335
+ "ITSMerge: pattern typesGH not mergeable, keeping host: %r",
336
+ t_pat,
337
+ )
338
+
339
+ return (tuple(left_h), tuple(right_h))
340
+
341
+ def _merge_tuple_format(
342
+ self,
343
+ host_data: Dict[str, Any],
344
+ pat_data: Dict[str, Any],
345
+ ) -> Dict[str, Any]:
346
+ """Merge tuple-format ITS node attributes (``hcount``, ``sigma_order``,
347
+ ``pi_order``), each stored as a 2-tuple ``(reactant_side, product_side)``.
348
+
349
+ ``hcount`` keeps the host value; bond orders keep the host value.
350
+ The host (bw ITS) has the correct hcounts from the actual substrate
351
+ molecule, so we never let the pattern override them. Taking max()
352
+ was wrong when MCS maps a pattern atom with more Hs onto a host atom
353
+ with fewer Hs (e.g. wrong cross-molecule match due to incompatible
354
+ fw/bw atom_map numbering).
355
+
356
+ :param host_data: Host node attribute dictionary.
357
+ :param pat_data: Pattern node attribute dictionary.
358
+ :return: Dict of updated attributes (empty if no tuple-format attrs found).
359
+ """
360
+ updates: Dict[str, Any] = {}
361
+ for attr in ("hcount", "sigma_order", "pi_order"):
362
+ h_val = host_data.get(attr)
363
+ p_val = pat_data.get(attr)
364
+ if h_val is None or p_val is None:
365
+ continue
366
+ if not (
367
+ isinstance(h_val, (tuple, list)) and isinstance(p_val, (tuple, list))
368
+ ):
369
+ continue
370
+ if len(h_val) != 2 or len(p_val) != 2:
371
+ continue
372
+ updates[attr] = tuple(h_val)
373
+ return updates
374
+
375
+ def _merge_anchor_nodes(self, fused: GraphType) -> None:
376
+ """
377
+ Merge ITS typing for mapped pattern→host node pairs.
378
+
379
+ When the host node is a wildcard but the pattern node is a real atom,
380
+ the wildcard placeholder is replaced entirely by the pattern's data so
381
+ that the fused graph carries the correct atom identity and hydrogen
382
+ counts from the forward ITS instead of the wildcard stub.
383
+
384
+ :param fused: Graph being constructed (copy of host).
385
+ :type fused: GraphType
386
+ """
387
+ for p_node, h_node in self._pat_to_host.items():
388
+ if h_node not in fused or p_node not in self._pattern:
389
+ continue
390
+
391
+ host_attrs = fused.nodes[h_node]
392
+ pat_attrs = self._pattern.nodes[p_node]
393
+
394
+ # If the host node is a wildcard placeholder and the pattern node is
395
+ # a real atom, replace the wildcard's attributes with the real atom's
396
+ # data so the fused ITS carries the actual element/hcount/charge.
397
+ # Preserve the host node's original atom_map so the fused-graph node
398
+ # ID space stays consistent (the pattern's atom_map would collide with
399
+ # other host nodes that legitimately share that atom_map number).
400
+ if self._is_wc_node(host_attrs) and not self._is_wc_node(pat_attrs):
401
+ old_amap = host_attrs.get("atom_map")
402
+ host_attrs.clear()
403
+ host_attrs.update(pat_attrs)
404
+ if old_amap is not None:
405
+ host_attrs["atom_map"] = old_amap
406
+ continue
407
+
408
+ merged_t = self._merge_types_gh(host_attrs, pat_attrs)
409
+ if merged_t is not None:
410
+ host_attrs[self._types_key] = merged_t
411
+ tuple_updates = self._merge_tuple_format(host_attrs, pat_attrs)
412
+ host_attrs.update(tuple_updates)
413
+
414
+ # ------------------------------------------------------------------
415
+ # Internal: leftover nodes & edges
416
+ # ------------------------------------------------------------------
417
+ def _next_int_id(self, fused: GraphType) -> Optional[int]:
418
+ """Determine starting integer ID for new nodes, if applicable.
419
+
420
+ :param fused: Graph being constructed.
421
+ :type fused: GraphType
422
+ :return: Next integer ID or ``None`` if node IDs are not all ints.
423
+ :rtype: int | None
424
+ """
425
+ if not fused.nodes:
426
+ return 0
427
+ if all(isinstance(n, int) for n in fused.nodes):
428
+ return max(fused.nodes) + 1
429
+ return None
430
+
431
+ def _add_leftover_pattern_nodes(self, fused: GraphType) -> Dict[Any, Any]:
432
+ """Add leftover non-wildcard pattern nodes to the fused graph.
433
+
434
+ Pattern nodes whose atom_map already appears in the host are
435
+ treated as implicitly mapped (they are not added as new nodes
436
+ and their host counterpart is recorded in :attr:`_pat_to_host`
437
+ so that edges between them are handled correctly).
438
+
439
+ :param fused: Graph being constructed.
440
+ :type fused: GraphType
441
+ :return: Mapping from pattern node IDs to new fused node IDs.
442
+ :rtype: dict[Any, Any]
443
+ """
444
+ mapped_p_nodes = set(self._pat_to_host.keys())
445
+ leftover_map: Dict[Any, Any] = {}
446
+
447
+ # Build reverse lookup: host atom_map → host node id
448
+ host_amap_to_node: Dict[Any, Any] = {}
449
+ for h_node in self._host.nodes():
450
+ amap = self._host.nodes[h_node].get("atom_map")
451
+ if amap is not None:
452
+ host_amap_to_node[amap] = h_node
453
+
454
+ # Host nodes already occupied by the explicit MCS mapping must not be
455
+ # re-used for implicit atom_map-based matching. fw and bw ITS use
456
+ # independent atom_map numbering (reactant vs product SMILES), so the
457
+ # same atom_map number can refer to chemically different atoms — the
458
+ # element-equality check below is not sufficient to distinguish them
459
+ # when the MCS has already assigned the host node to a different pattern
460
+ # atom.
461
+ used_host_nodes: Set[Any] = set(self._pat_to_host.values())
462
+
463
+ next_int_id = self._next_int_id(fused)
464
+ tag = "p1" if self._pat_is_G1 else "p2"
465
+
466
+ for p_node, p_data in self._pattern.nodes(data=True):
467
+ if p_node in mapped_p_nodes:
468
+ continue
469
+ if p_data.get(self._element_key) == self._wildcard_element:
470
+ continue
471
+
472
+ # If this pattern atom's atom_map already exists in the host
473
+ # AND the elements match AND the host node is not already occupied
474
+ # by the explicit MCS mapping, record the host counterpart instead
475
+ # of creating a duplicate node. The element check and the
476
+ # used_host_nodes guard are both required because fw and bw ITS use
477
+ # independent atom_map numbering (reactant vs product SMILES
478
+ # ordering), so the same atom_map number can refer to chemically
479
+ # different atoms.
480
+ p_amap = p_data.get("atom_map")
481
+ if p_amap is not None and p_amap in host_amap_to_node:
482
+ h_node = host_amap_to_node[p_amap]
483
+ h_elem = self._host.nodes[h_node].get(self._element_key)
484
+ p_elem = p_data.get(self._element_key)
485
+ if h_elem == p_elem and h_node not in used_host_nodes:
486
+ self._pat_to_host[p_node] = h_node
487
+ mapped_p_nodes.add(p_node)
488
+ used_host_nodes.add(h_node)
489
+ continue
490
+ # Element mismatch or host node already used: atom_maps are from
491
+ # incompatible numbering schemes — fall through to genuine
492
+ # leftover handling.
493
+
494
+ if next_int_id is not None:
495
+ f_node = next_int_id
496
+ next_int_id += 1
497
+ else:
498
+ f_node = (tag, p_node)
499
+
500
+ leftover_map[p_node] = f_node
501
+ fused.add_node(f_node, **p_data)
502
+ # Reassign atom_map to the new unique fused node ID so that
503
+ # tuple-format SMILES generation does not collide with existing
504
+ # host atom_maps (fw and bw ITS use independent atom_map
505
+ # numbering, so the same number may already be taken by a host
506
+ # atom with a different identity).
507
+ nd = fused.nodes[f_node]
508
+ am_val = nd.get("atom_map")
509
+ if am_val is not None:
510
+ if isinstance(am_val, (tuple, list)):
511
+ nd["atom_map"] = type(am_val)([f_node] * len(am_val))
512
+ else:
513
+ nd["atom_map"] = f_node
514
+ tgh = nd.get("typesGH")
515
+ if tgh and len(tgh) == 2:
516
+
517
+ def _upd_amap(side: tuple, new_am: int) -> tuple:
518
+ lst = list(side)
519
+ if lst:
520
+ lst[-1] = new_am
521
+ return tuple(lst)
522
+
523
+ nd["typesGH"] = (_upd_amap(tgh[0], f_node), _upd_amap(tgh[1], f_node))
524
+ # Propagate aromatic/charge from the nearest mapped anchor neighbour
525
+ # when those attributes are absent from the pattern node's own data.
526
+ for p_nb in self._pattern.neighbors(p_node):
527
+ h_nb = self._pat_to_host.get(p_nb)
528
+ if h_nb is None or h_nb not in fused:
529
+ continue
530
+ h_nb_data = fused.nodes[h_nb]
531
+ for attr in ("aromatic", "charge"):
532
+ if attr in h_nb_data and attr not in p_data:
533
+ fused.nodes[f_node][attr] = h_nb_data[attr]
534
+ break
535
+
536
+ return leftover_map
537
+
538
+ def _add_pattern_edges(
539
+ self,
540
+ fused: GraphType,
541
+ leftover_map: Dict[Any, Any],
542
+ ) -> None:
543
+ """
544
+ Add edges from the pattern graph into the fused graph.
545
+
546
+ Edges are added between:
547
+
548
+ * mapped pattern nodes, via their host IDs, and
549
+ * leftover pattern nodes that were newly added.
550
+
551
+ :param fused: Graph being constructed.
552
+ :type fused: GraphType
553
+ :param leftover_map: Mapping from pattern node IDs to new fused node IDs.
554
+ :type leftover_map: dict[Any, Any]
555
+ """
556
+
557
+ def map_p_to_f(n: Any) -> Optional[Any]:
558
+ if n in self._pat_to_host:
559
+ return self._pat_to_host[n]
560
+ return leftover_map.get(n)
561
+
562
+ is_multi = isinstance(fused, (nx.MultiGraph, nx.MultiDiGraph))
563
+
564
+ for u, v, data in self._pattern.edges(data=True):
565
+ fu = map_p_to_f(u)
566
+ fv = map_p_to_f(v)
567
+ if fu is None or fv is None:
568
+ continue
569
+
570
+ # Fix 2b: only add edges that involve at least one genuinely new
571
+ # leftover node. Edges between two host-already-present atoms are
572
+ # skipped: the host ITS already encodes the correct bonds between
573
+ # its own atoms, and adding pattern bonds would create duplicate or
574
+ # invalid connectivity (valence errors).
575
+ if u not in leftover_map and v not in leftover_map:
576
+ continue
577
+
578
+ # Fix 2d: for edges between exactly one leftover node and one
579
+ # host-mapped node, skip if the bond is static (no order change
580
+ # between reactant and product sides, i.e. standard_order == 0).
581
+ # Static bonds from leftover nodes are phantom bonds produced by
582
+ # the fw-ITS double-representation artifact — the host ITS already
583
+ # encodes any real bond to the mapped atom, so adding these would
584
+ # create wrong connectivity. Only reaction bonds (forming/breaking,
585
+ # standard_order != 0) should be added.
586
+ #
587
+ # Exception: if the mapped host node IS a wildcard or HAS a wildcard
588
+ # neighbor, the leftover pattern atom was expected to bond there (the
589
+ # wildcard was a placeholder for the missing group), so even static
590
+ # bonds must be preserved.
591
+ if (u in leftover_map) != (v in leftover_map):
592
+ std_ord = data.get("standard_order")
593
+ is_static: bool
594
+ if std_ord is not None:
595
+ is_static = abs(float(std_ord)) < 1e-9
596
+ else:
597
+ ord_val = data.get("order")
598
+ if isinstance(ord_val, (tuple, list)) and len(ord_val) == 2:
599
+ is_static = abs(float(ord_val[0]) - float(ord_val[1])) < 1e-9
600
+ else:
601
+ is_static = False
602
+ if is_static:
603
+ # Check whether the mapped host node is/has a wildcard:
604
+ # if so, the static bond is genuine and must be kept.
605
+ mapped_end_node = None
606
+ for lo, me in ((u, v), (v, u)):
607
+ if lo in leftover_map:
608
+ mapped_end_node = self._pat_to_host.get(me)
609
+ break
610
+ if mapped_end_node is not None and mapped_end_node in self._host:
611
+ wc_placeholder = self._is_wc_node(
612
+ self._host.nodes[mapped_end_node]
613
+ ) or self._host_has_wc_neighbor(mapped_end_node)
614
+ else:
615
+ wc_placeholder = False
616
+ if not wc_placeholder:
617
+ continue
618
+
619
+ # Fix 2c: for edges between a leftover atom and a non-leftover
620
+ # (host-mapped) atom, skip if the corresponding HOST atom has
621
+ # no wildcard neighbor and no neighbor of the leftover's element.
622
+ # This prevents phantom bonds when the MCS maps a pattern atom to
623
+ # a host atom that is structurally similar but chemically different
624
+ # (e.g. mapping a ylide-C adjacent to P onto an aldehyde-C that
625
+ # should never bond to P).
626
+ # Exception: if the mapped host node itself is a wildcard, any
627
+ # leftover neighbor is chemically valid by construction.
628
+ for leftover_end, mapped_end in ((u, v), (v, u)):
629
+ if leftover_end not in leftover_map:
630
+ continue
631
+ h_mapped = self._pat_to_host.get(mapped_end)
632
+ if h_mapped is None or h_mapped not in self._host:
633
+ break
634
+ # If the mapped host node is itself a wildcard placeholder,
635
+ # any leftover pattern neighbor is chemically valid.
636
+ if self._is_wc_node(self._host.nodes[h_mapped]):
637
+ break
638
+ left_elem = self._pattern.nodes[leftover_end].get(self._element_key)
639
+ wc = self._wildcard_element
640
+ host_neighbors = list(self._host.neighbors(h_mapped))
641
+ has_wc = any(
642
+ self._host.nodes[nb].get(self._element_key) == wc
643
+ for nb in host_neighbors
644
+ )
645
+ has_elem = any(
646
+ self._host.nodes[nb].get(self._element_key) == left_elem
647
+ for nb in host_neighbors
648
+ )
649
+ # Only block phantom bonds when the host node already has real
650
+ # (non-wildcard) neighbors: if it is isolated, the leftover edge
651
+ # is legitimate by construction (nothing to protect against).
652
+ has_real_neighbors = any(
653
+ not self._is_wc_node(self._host.nodes[nb]) for nb in host_neighbors
654
+ )
655
+ if has_real_neighbors and not has_wc and not has_elem:
656
+ fu = None # signal to skip
657
+ break
658
+
659
+ if fu is None or fv is None:
660
+ continue
661
+
662
+ if is_multi:
663
+ fused.add_edge(fu, fv, **data)
664
+ else:
665
+ if fused.has_edge(fu, fv):
666
+ continue
667
+ fused.add_edge(fu, fv, **data)
668
+
669
+ # ------------------------------------------------------------------
670
+ # Internal: atom_map normalization
671
+ # ------------------------------------------------------------------
672
+ def _normalize_atom_maps(self, fused: GraphType) -> None:
673
+ """
674
+ Reassign every node's ``atom_map`` to its node index.
675
+
676
+ After merging fw and bw ITS graphs, the two graphs' independent
677
+ atom_map numbering schemes can produce duplicate atom_map values in
678
+ the fused graph (e.g. fw node 29 and bw node 29 both end up in the
679
+ fused graph with ``atom_map=(29,29)``). Using the node index as the
680
+ canonical atom_map guarantees uniqueness.
681
+
682
+ :param fused: Fused graph to normalize in-place.
683
+ :type fused: GraphType
684
+ """
685
+ for n, d in fused.nodes(data=True):
686
+ amap = d.get("atom_map")
687
+ if amap is None:
688
+ continue
689
+ if isinstance(amap, (tuple, list)):
690
+ d["atom_map"] = type(amap)([n] * len(amap))
691
+ else:
692
+ d["atom_map"] = n
693
+
694
+ # ------------------------------------------------------------------
695
+ # Internal: wildcard removal
696
+ # ------------------------------------------------------------------
697
+ def _remove_wildcards(self, fused: GraphType) -> None:
698
+ """
699
+ Remove wildcard nodes (and incident edges) from the fused graph.
700
+
701
+ Before removal, aromatic atoms that will lose their last ring bond
702
+ to a wildcard are dearomatized so that RDKit sanitization does not
703
+ fail with "non-ring atom marked aromatic".
704
+
705
+ :param fused: Graph being constructed.
706
+ :type fused: GraphType
707
+ """
708
+ wc_nodes = {
709
+ n
710
+ for n, d in fused.nodes(data=True)
711
+ if d.get(self._element_key) == self._wildcard_element
712
+ }
713
+
714
+ if wc_nodes:
715
+ self._logger.debug(
716
+ "ITSMerge: removing %d wildcard nodes from fused graph",
717
+ len(wc_nodes),
718
+ )
719
+
720
+ # Dearomatize aromatic atoms that lack enough aromatic-bond neighbors
721
+ # to form a valid ring. This check runs unconditionally (even when
722
+ # there are no wildcards) because leftover pattern atoms can be
723
+ # marked aromatic but have no ring partners in the fused graph.
724
+ # An aromatic atom needs at least 2 aromatic-bond neighbors to form
725
+ # a valid ring. If fewer than 2 aromatic-bond neighbors remain after
726
+ # wildcard removal, the atom can no longer be in a ring and must be
727
+ # dearomatized to avoid RDKit "non-ring atom marked aromatic" errors.
728
+ AROMATIC_ORDER = 1.5
729
+
730
+ def _edge_order_r(node: Any, nb: Any) -> float:
731
+ val = fused.edges[node, nb].get("order", 0)
732
+ if isinstance(val, (tuple, list)):
733
+ return float(val[0])
734
+ return float(val or 0)
735
+
736
+ def _aromatic_degree(node: Any) -> int:
737
+ return sum(
738
+ 1
739
+ for nb in fused.neighbors(node)
740
+ if nb not in wc_nodes
741
+ and abs(_edge_order_r(node, nb) - AROMATIC_ORDER) < 1e-9
742
+ )
743
+
744
+ def _is_aromatic(attr: Any) -> bool:
745
+ if isinstance(attr, (tuple, list)):
746
+ return any(attr)
747
+ return bool(attr)
748
+
749
+ SINGLE_ORDER = 1.0
750
+
751
+ def _dearom_side(side: tuple) -> tuple:
752
+ lst = list(side)
753
+ if len(lst) > 1:
754
+ lst[1] = False
755
+ return tuple(lst)
756
+
757
+ def _fix_bonds(n: Any) -> None:
758
+ for nb in fused.neighbors(n):
759
+ if nb in wc_nodes:
760
+ continue
761
+ edata = fused.edges[n, nb]
762
+ for key in ("order", "kekule_order", "sigma_order"):
763
+ val = edata.get(key)
764
+ if val is None:
765
+ continue
766
+ if isinstance(val, (tuple, list)):
767
+ edata[key] = tuple(
768
+ SINGLE_ORDER if abs(v - AROMATIC_ORDER) < 1e-9 else v
769
+ for v in val
770
+ )
771
+ elif abs(val - AROMATIC_ORDER) < 1e-9:
772
+ edata[key] = SINGLE_ORDER
773
+
774
+ dearom_nodes: set = set()
775
+
776
+ # Iterative passes: dearomatizing a node changes its bond orders,
777
+ # which can reduce the aromatic degree of its neighbors and trigger
778
+ # further dearomatization (cascade effect). Repeat until convergence.
779
+ while True:
780
+ newly_dearom: set = set()
781
+ for n, d in fused.nodes(data=True):
782
+ if n in wc_nodes or n in dearom_nodes:
783
+ continue
784
+ aromatic = d.get("aromatic")
785
+ if not _is_aromatic(aromatic):
786
+ continue
787
+ if _aromatic_degree(n) < 2:
788
+ if isinstance(aromatic, (tuple, list)):
789
+ d["aromatic"] = tuple(False for _ in aromatic)
790
+ else:
791
+ d["aromatic"] = False
792
+ tgh = d.get("typesGH")
793
+ if tgh and len(tgh) == 2:
794
+ d["typesGH"] = (_dearom_side(tgh[0]), _dearom_side(tgh[1]))
795
+ newly_dearom.add(n)
796
+
797
+ if not newly_dearom:
798
+ break
799
+
800
+ # Fix bond orders immediately so the next iteration sees updated
801
+ # aromatic degrees for nodes that lost an aromatic neighbour.
802
+ for n in newly_dearom:
803
+ _fix_bonds(n)
804
+
805
+ dearom_nodes.update(newly_dearom)
806
+
807
+ fused.remove_nodes_from(wc_nodes)
808
+
809
+
810
+ # ----------------------------------------------------------------------
811
+ # Functional wrapper for backwards compatibility
812
+ # ----------------------------------------------------------------------
813
+ def fuse_its_graphs(
814
+ G1: GraphType,
815
+ G2: GraphType,
816
+ mapping: Dict[Any, Any],
817
+ *,
818
+ types_key: str = "typesGH",
819
+ element_key: str = "element",
820
+ wildcard_element: str = "*",
821
+ remove_wildcards: bool = True,
822
+ logger: Optional[logging.Logger] = None,
823
+ ) -> GraphType:
824
+ """Functional wrapper around :class:`ITSMerge`.
825
+
826
+ :param G1: First input ITS graph.
827
+ :type G1: GraphType
828
+ :param G2: Second input ITS graph.
829
+ :type G2: GraphType
830
+ :param mapping: Node mapping between the graphs.
831
+ :type mapping: dict[Any, Any]
832
+ :param types_key: Node attribute key holding ITS typing information.
833
+ :type types_key: str
834
+ :param element_key: Node attribute key for element / atom type.
835
+ :type element_key: str
836
+ :param wildcard_element: Value of ``element_key`` that denotes
837
+ wildcard nodes.
838
+ :type wildcard_element: str
839
+ :param remove_wildcards: If ``True``, remove wildcard nodes from the
840
+ fused graph. If ``False``, keep them.
841
+ :type remove_wildcards: bool
842
+ :param logger: Optional logger for debug output.
843
+ :type logger: logging.Logger | None
844
+ :return: Fused ITS graph.
845
+ :rtype: GraphType
846
+ """
847
+ merger = ITSMerge(
848
+ G1,
849
+ G2,
850
+ mapping,
851
+ types_key=types_key,
852
+ element_key=element_key,
853
+ wildcard_element=wildcard_element,
854
+ remove_wildcards=remove_wildcards,
855
+ logger=logger,
856
+ ).merge()
857
+ return merger.fused_graph