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,1050 @@
1
+ """
2
+ Symmetry-distinct enumeration of exact kernel optima.
3
+
4
+ The exact kernel solvers can enumerate every optimal assignment over the small
5
+ uncertainty region. This module stitches those kernel assignments back into full
6
+ atom mappings and removes duplicates under graph automorphisms. For mapped
7
+ reaction SMILES, ITS canonical hashes provide the chemistry-aware canonical form;
8
+ for plain graph matching, mappings are canonicalised under ``Aut(R) x Aut(P)``.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import itertools
14
+ from collections import defaultdict, deque
15
+ from typing import Optional
16
+
17
+ import numpy as np
18
+ from scipy.optimize import linear_sum_assignment
19
+
20
+ from ....chem.its import (
21
+ dedup_mapped_rxns,
22
+ mapped_rxn_is_electron_balanced,
23
+ reaction_center_atom_maps,
24
+ )
25
+ from ....chem.smiles import get_numbered_rxn_smiles, expand_reaction_center_hydrogens
26
+ from ....graph.automorphism import to_nx
27
+ from ...slap.lap import _adjacency_and_elements, chemical_distance, recover_mapping
28
+ from .branching import solve_kernel_blockwise
29
+ from .enumeration_result import EnumerationResult, mapping_to_lgp
30
+ from .kernel import apply_kernel_solution
31
+
32
+ _MAX_MAPPED_ENUMERATION_RESULTS = 1000
33
+ _MAX_SYNKIT_DEDUP_RESULTS = 64
34
+ _MAX_AUTOMORPHISM_DEDUP_RESULTS = 64
35
+ _MAX_PAIR_REPAIR_ATOMS = 80
36
+ _PAIR_REPAIR_MIN_CD = 10.0
37
+ _MAX_PAIR_REPAIR_BOND_PAIRS = 120
38
+ _MAX_PAIR_REPAIR_DESCENT_STEPS = 3
39
+ _MAX_HCOUNT_PERMUTE_BLOCK = 6
40
+ _MAX_HCOUNT_PERMUTATION_PRODUCTS = 5000
41
+ _LOCAL_SWAP_DEFAULT_MAX_ALL_ATOMS = 48
42
+ _LOCAL_SWAP_DEFAULT_MAX_POOL_PER_ELEMENT = 14
43
+ _LOCAL_SWAP_DEFAULT_MAX_DESCENT_STEPS = 8
44
+
45
+
46
+ def improve_results_by_pair_swaps(lgp, results, binary=False, valfactor=2):
47
+ """Append greedy coupled same-element bond-pair repairs."""
48
+ if (
49
+ not results
50
+ or len(lgp[0].labels) > _MAX_PAIR_REPAIR_ATOMS
51
+ or not _has_balancing_dummies(lgp)
52
+ or _best_result_cd(results, valfactor) < _PAIR_REPAIR_MIN_CD
53
+ ):
54
+ return results
55
+
56
+ improved = []
57
+ seen = set()
58
+ for result in results:
59
+ mapping = complete_mapping(
60
+ lgp,
61
+ recover_mapping(result["lgp"]),
62
+ binary=binary,
63
+ )
64
+ key = tuple(mapping)
65
+ if key not in seen:
66
+ seen.add(key)
67
+ original = dict(result)
68
+ original.setdefault("mapping", list(mapping))
69
+ improved.append(original)
70
+
71
+ repaired, cd = _pair_swap_descent(lgp, mapping, binary=binary)
72
+ key = tuple(repaired)
73
+ if key in seen:
74
+ continue
75
+ seen.add(key)
76
+ updated = dict(result)
77
+ updated["lgp"] = mapping_to_lgp(lgp, repaired)
78
+ updated["mapping"] = repaired
79
+ updated["cd"] = cd
80
+ updated["val"] = cd * valfactor
81
+ updated["repair"] = "pair-swap"
82
+ improved.append(updated)
83
+
84
+ improved.sort(
85
+ key=lambda r: (
86
+ float(r.get("cd", r.get("val", 0) / valfactor)),
87
+ r.get("repair") or "",
88
+ )
89
+ )
90
+ return improved
91
+
92
+
93
+ def expand_results_by_local_swaps(
94
+ lgp,
95
+ results,
96
+ binary=False,
97
+ valfactor=2,
98
+ depth=0,
99
+ cap=128,
100
+ slack=0.0,
101
+ min_cd=4.0,
102
+ max_all_atoms=_LOCAL_SWAP_DEFAULT_MAX_ALL_ATOMS,
103
+ max_pool_per_element=_LOCAL_SWAP_DEFAULT_MAX_POOL_PER_ELEMENT,
104
+ max_descent_steps=_LOCAL_SWAP_DEFAULT_MAX_DESCENT_STEPS,
105
+ ):
106
+ """Add same-or-better CD maps from bounded same-element swaps."""
107
+ if (
108
+ depth <= 0
109
+ or cap <= 0
110
+ or not results
111
+ or not _has_balancing_dummies(lgp)
112
+ or _best_result_cd(results, valfactor) < min_cd
113
+ ):
114
+ return results
115
+
116
+ seeds = []
117
+ seed_seen = set()
118
+ for result in results:
119
+ mapping = tuple(
120
+ complete_mapping(
121
+ lgp,
122
+ recover_mapping(result["lgp"]),
123
+ binary=binary,
124
+ )
125
+ )
126
+ if mapping in seed_seen:
127
+ continue
128
+ seed_seen.add(mapping)
129
+ cd = float(result.get("cd", chemical_distance(lgp, mapping, binary)))
130
+ seeds.append((list(mapping), cd))
131
+ if not seeds:
132
+ return results
133
+
134
+ original_keys = {tuple(mapping) for mapping, _ in seeds}
135
+ records = _bounded_local_swap_records(
136
+ lgp,
137
+ seeds,
138
+ binary=binary,
139
+ depth=depth,
140
+ slack=slack,
141
+ cap=cap,
142
+ max_all_atoms=max_all_atoms,
143
+ max_pool_per_element=max_pool_per_element,
144
+ max_descent_steps=max_descent_steps,
145
+ )
146
+ if not records:
147
+ return results
148
+
149
+ best = min(cd for _, cd in records)
150
+ filtered = [(mapping, cd) for mapping, cd in records if cd <= best + slack + 1e-9][
151
+ :cap
152
+ ]
153
+ if not filtered:
154
+ return results
155
+ filtered_keys = {tuple(mapping) for mapping, _ in filtered}
156
+ if filtered_keys <= original_keys:
157
+ return results
158
+
159
+ expanded = []
160
+ seen = set()
161
+ for result in results:
162
+ mapping = tuple(
163
+ complete_mapping(
164
+ lgp,
165
+ recover_mapping(result["lgp"]),
166
+ binary=binary,
167
+ )
168
+ )
169
+ seen.add(mapping)
170
+ updated = dict(result)
171
+ updated.setdefault("mapping", list(mapping))
172
+ expanded.append(updated)
173
+
174
+ for mapping, cd in filtered:
175
+ key = tuple(mapping)
176
+ if key in seen:
177
+ continue
178
+ seen.add(key)
179
+ expanded.append(
180
+ {
181
+ "lgp": mapping_to_lgp(lgp, mapping),
182
+ "mapping": list(mapping),
183
+ "cd": cd,
184
+ "val": cd * valfactor,
185
+ "repair": "local-swap",
186
+ }
187
+ )
188
+ return expanded or results
189
+
190
+
191
+ def improve_results_by_hcount_permutations(
192
+ lgp,
193
+ results,
194
+ binary=False,
195
+ valfactor=2,
196
+ hcount_weight=0.0,
197
+ ):
198
+ """Append small-block optima ranked by CD plus H-count change."""
199
+ if hcount_weight <= 0 or not results:
200
+ return results
201
+
202
+ context = _hcount_scoring_context(lgp, binary)
203
+ improved = []
204
+ seen = set()
205
+ for result in results:
206
+ mapping = complete_mapping(
207
+ lgp,
208
+ recover_mapping(result["lgp"]),
209
+ binary=binary,
210
+ )
211
+ base_mapping, cd, hdelta, role, score = _hcount_mapping_record(
212
+ lgp,
213
+ mapping,
214
+ binary,
215
+ hcount_weight,
216
+ context=context,
217
+ )
218
+ base = dict(result)
219
+ base["mapping"] = base_mapping
220
+ base["cd"] = cd
221
+ base["val"] = cd * valfactor
222
+ base["hcount_delta"] = hdelta
223
+ base["hcount_score"] = score
224
+ base["role_delta"] = role
225
+ key = tuple(base_mapping)
226
+ if key not in seen:
227
+ seen.add(key)
228
+ improved.append(base)
229
+
230
+ optima = _hcount_permutation_optima(
231
+ lgp,
232
+ mapping,
233
+ binary=binary,
234
+ hcount_weight=hcount_weight,
235
+ context=context,
236
+ )
237
+ for repaired, cd, hdelta, role, score in optima:
238
+ key = tuple(repaired)
239
+ if key in seen:
240
+ continue
241
+ seen.add(key)
242
+ updated = dict(result)
243
+ updated["lgp"] = mapping_to_lgp(lgp, repaired)
244
+ updated["mapping"] = repaired
245
+ updated["cd"] = cd
246
+ updated["val"] = cd * valfactor
247
+ updated["hcount_delta"] = hdelta
248
+ updated["hcount_score"] = score
249
+ updated["role_delta"] = role
250
+ improved.append(updated)
251
+
252
+ improved.sort(
253
+ key=lambda r: (
254
+ float(r.get("hcount_score", 0.0)),
255
+ float(r.get("cd", 0.0)),
256
+ float(r.get("role_delta", 0.0)),
257
+ r.get("repair") or "",
258
+ )
259
+ )
260
+ return improved
261
+
262
+
263
+ def annotate_hcount_scores(lgp, results, binary=False, hcount_weight=0.0):
264
+ """Attach H-count score fields to final result dictionaries."""
265
+ if hcount_weight <= 0:
266
+ return
267
+ for result in results:
268
+ mapping = result.get("mapping")
269
+ if mapping is None:
270
+ mapping = recover_mapping(result["lgp"])
271
+ cd = result.get("cd")
272
+ if cd is None:
273
+ cd = chemical_distance(lgp, mapping, binary)
274
+ result["cd"] = cd
275
+ hdelta = _hcount_delta(lgp, mapping)
276
+ result["hcount_delta"] = hdelta
277
+ result["hcount_score"] = cd + hcount_weight * hdelta
278
+ result["role_delta"] = _role_delta(lgp, mapping)
279
+
280
+
281
+ def _best_result_cd(results, valfactor):
282
+ best = float("inf")
283
+ for result in results:
284
+ cd = result.get("cd")
285
+ if cd is None and "val" in result:
286
+ cd = result["val"] / valfactor
287
+ if cd is not None and cd < best:
288
+ best = cd
289
+ return best
290
+
291
+
292
+ def _has_balancing_dummies(lgp):
293
+ for graph in lgp:
294
+ slices = graph.props.get("natoms slices")
295
+ if slices and len(slices) >= 2 and slices[-1] != slices[-2]:
296
+ return True
297
+ return False
298
+
299
+
300
+ def _hcount_permutation_optima(
301
+ lgp,
302
+ mapping,
303
+ binary=False,
304
+ hcount_weight=1.0,
305
+ context=None,
306
+ ):
307
+ elements = _elements(lgp[0])
308
+ prod_elements = _elements(lgp[1])
309
+ blocks = _small_element_blocks(elements, prod_elements)
310
+ option_lists = []
311
+ option_count = 1
312
+ for block in blocks:
313
+ product_pool = [mapping[i] for i in block]
314
+ options = list(itertools.permutations(product_pool))
315
+ option_count *= len(options)
316
+ if option_count > _MAX_HCOUNT_PERMUTATION_PRODUCTS:
317
+ return [
318
+ _hcount_mapping_record(
319
+ lgp,
320
+ mapping,
321
+ binary,
322
+ hcount_weight,
323
+ context=context,
324
+ )
325
+ ]
326
+ option_lists.append((block, options))
327
+
328
+ if context is None:
329
+ context = _hcount_scoring_context(lgp, binary)
330
+ best_score = None
331
+ optima = []
332
+ combos = (
333
+ itertools.product(*(options for _, options in option_lists))
334
+ if option_lists
335
+ else [()]
336
+ )
337
+ for combo in combos:
338
+ candidate = list(mapping)
339
+ for (block, _), perm in zip(option_lists, combo):
340
+ for i, p in zip(block, perm):
341
+ candidate[i] = p
342
+ if len(set(candidate)) != len(candidate):
343
+ continue
344
+ record = _hcount_mapping_record(
345
+ lgp,
346
+ candidate,
347
+ binary,
348
+ hcount_weight,
349
+ context=context,
350
+ )
351
+ score = record[-1]
352
+ if best_score is None or score < best_score - 1e-9:
353
+ best_score = score
354
+ optima = [record]
355
+ elif abs(score - best_score) <= 1e-9:
356
+ optima.append(record)
357
+ return optima or [
358
+ _hcount_mapping_record(
359
+ lgp,
360
+ mapping,
361
+ binary,
362
+ hcount_weight,
363
+ context=context,
364
+ )
365
+ ]
366
+
367
+
368
+ def _hcount_scoring_context(lgp, binary):
369
+ A, _ = _adjacency_and_elements(lgp[0], binary=binary)
370
+ B, _ = _adjacency_and_elements(lgp[1], binary=binary)
371
+ h_r, h_p = _atom_hcounts(lgp)
372
+ return A, B, np.asarray(h_r, dtype=int), np.asarray(h_p, dtype=int)
373
+
374
+
375
+ def _hcount_mapping_record(lgp, mapping, binary, hcount_weight, context=None):
376
+ if context is None:
377
+ context = _hcount_scoring_context(lgp, binary)
378
+ A, B, h_r, h_p = context
379
+ cd = _chemical_distance_arrays(A, B, mapping)
380
+ hdelta = _hcount_delta_arrays(h_r, h_p, mapping)
381
+ role = _role_delta_arrays(A, B, mapping)
382
+ score = cd + hcount_weight * hdelta
383
+ return list(mapping), cd, hdelta, role, score
384
+
385
+
386
+ def _chemical_distance_arrays(A, B, mapping):
387
+ mapping = np.asarray(mapping, dtype=int)
388
+ return 0.5 * float(np.abs(A - B[np.ix_(mapping, mapping)]).sum())
389
+
390
+
391
+ def _hcount_delta_arrays(h_r, h_p, mapping):
392
+ mapping = np.asarray(mapping, dtype=int)
393
+ return int(np.abs(h_r - h_p[mapping]).sum())
394
+
395
+
396
+ def _role_delta_arrays(A, B, mapping):
397
+ total = 0.0
398
+ for i, p in enumerate(mapping):
399
+ left = sorted(float(w) for w in A[i] if w != 0)
400
+ right = sorted(float(w) for w in B[p] if w != 0)
401
+ n = max(len(left), len(right))
402
+ left.extend([0.0] * (n - len(left)))
403
+ right.extend([0.0] * (n - len(right)))
404
+ total += sum(abs(a - b) for a, b in zip(left, right))
405
+ return total
406
+
407
+
408
+ def _small_element_blocks(elements_r, elements_p):
409
+ blocks = []
410
+ for element in sorted(set(elements_r)):
411
+ r_block = [i for i, e in enumerate(elements_r) if e == element]
412
+ p_count = sum(1 for e in elements_p if e == element)
413
+ if len(r_block) == p_count and 1 < len(r_block) <= _MAX_HCOUNT_PERMUTE_BLOCK:
414
+ blocks.append(r_block)
415
+ return blocks
416
+
417
+
418
+ def _atom_hcounts(lgp):
419
+ out = []
420
+ for side in (0, 1):
421
+ mol = lgp[side].props["sources"][-1]
422
+ base_natoms = lgp[side].props["natoms slices"][0]
423
+ hcounts = []
424
+ for i, atom in enumerate(mol.GetAtoms()):
425
+ hcounts.append(
426
+ atom.GetTotalNumHs(includeNeighbors=True) if i < base_natoms else 0
427
+ )
428
+ out.append(hcounts)
429
+ return out
430
+
431
+
432
+ def _hcount_delta(lgp, mapping):
433
+ h_r, h_p = _atom_hcounts(lgp)
434
+ return sum(abs(h_r[i] - h_p[p]) for i, p in enumerate(mapping))
435
+
436
+
437
+ def _role_delta(lgp, mapping):
438
+ A, _ = _adjacency_and_elements(lgp[0], binary=False)
439
+ B, _ = _adjacency_and_elements(lgp[1], binary=False)
440
+ return _role_delta_arrays(A, B, mapping)
441
+
442
+
443
+ def _pair_swap_descent(lgp, mapping, binary=False):
444
+ A, _ = _adjacency_and_elements(lgp[0], binary=binary)
445
+ B, _ = _adjacency_and_elements(lgp[1], binary=binary)
446
+ elements = _elements(lgp[0])
447
+ current = list(mapping)
448
+ current_cd = chemical_distance(lgp, current, binary)
449
+ pairs = _budgeted_oriented_bond_pairs(A, B, current, elements)
450
+
451
+ for _ in range(_MAX_PAIR_REPAIR_DESCENT_STEPS):
452
+ best = None
453
+ for x, (i, j, ei, ej) in enumerate(pairs):
454
+ for k, ell, ek, el in pairs[x + 1 :]:
455
+ if ei != ek or ej != el or len({i, j, k, ell}) < 4:
456
+ continue
457
+ cd = _pair_swap_cd(A, B, current, current_cd, i, j, k, ell)
458
+ if cd < current_cd - 1e-9 and (best is None or cd < best[0]):
459
+ best = (cd, i, j, k, ell)
460
+ if best is None:
461
+ return current, current_cd
462
+ current_cd, i, j, k, ell = best
463
+ current[i], current[k] = current[k], current[i]
464
+ current[j], current[ell] = current[ell], current[j]
465
+ return current, current_cd
466
+
467
+
468
+ def _budgeted_oriented_bond_pairs(A, B, mapping, elements):
469
+ pairs = _oriented_bond_pairs(A, elements)
470
+ if len(pairs) <= _MAX_PAIR_REPAIR_BOND_PAIRS:
471
+ return pairs
472
+
473
+ scores = _mismatch_scores(A, B, mapping)
474
+ pool = _expanded_swap_pool(A, B, mapping, scores)
475
+ focused = [pair for pair in pairs if pair[0] in pool or pair[1] in pool]
476
+ if len(focused) <= _MAX_PAIR_REPAIR_BOND_PAIRS:
477
+ return focused
478
+
479
+ focused.sort(
480
+ key=lambda pair: (-(scores[pair[0]] + scores[pair[1]]), pair[0], pair[1])
481
+ )
482
+ return focused[:_MAX_PAIR_REPAIR_BOND_PAIRS]
483
+
484
+
485
+ def _pair_swap_cd(A, B, mapping, current_cd, i, j, k, ell):
486
+ changed = (i, j, k, ell)
487
+ image = {
488
+ i: mapping[k],
489
+ k: mapping[i],
490
+ j: mapping[ell],
491
+ ell: mapping[j],
492
+ }
493
+
494
+ def mapped(atom):
495
+ return image.get(atom, mapping[atom])
496
+
497
+ old = 0.0
498
+ new = 0.0
499
+ n = len(mapping)
500
+ changed_set = set(changed)
501
+ for u in changed:
502
+ mu = mapping[u]
503
+ nmu = mapped(u)
504
+ for v in range(n):
505
+ mv = mapping[v]
506
+ nmv = mapped(v)
507
+ old += abs(A[u, v] - B[mu, mv])
508
+ new += abs(A[u, v] - B[nmu, nmv])
509
+ if v not in changed_set:
510
+ old += abs(A[v, u] - B[mv, mu])
511
+ new += abs(A[v, u] - B[nmv, nmu])
512
+ return current_cd + 0.5 * (new - old)
513
+
514
+
515
+ def _oriented_bond_pairs(adjacency, elements):
516
+ pairs = []
517
+ n = len(elements)
518
+ for i in range(n):
519
+ for j in range(n):
520
+ if i != j and adjacency[i, j] != 0:
521
+ pairs.append((i, j, elements[i], elements[j]))
522
+ return pairs
523
+
524
+
525
+ def _swap_cd(A, B, mapping, current_cd, i, k):
526
+ image = {i: mapping[k], k: mapping[i]}
527
+ changed = (i, k)
528
+ changed_set = set(changed)
529
+ old = 0.0
530
+ new = 0.0
531
+ n = len(mapping)
532
+
533
+ def mapped(atom):
534
+ return image.get(atom, mapping[atom])
535
+
536
+ for u in changed:
537
+ mu = mapping[u]
538
+ nmu = mapped(u)
539
+ for v in range(n):
540
+ mv = mapping[v]
541
+ nmv = mapped(v)
542
+ old += abs(A[u, v] - B[mu, mv])
543
+ new += abs(A[u, v] - B[nmu, nmv])
544
+ if v not in changed_set:
545
+ old += abs(A[v, u] - B[mv, mu])
546
+ new += abs(A[v, u] - B[nmv, nmu])
547
+ return current_cd + 0.5 * (new - old)
548
+
549
+
550
+ def _mismatch_scores(A, B, mapping):
551
+ mapped_product = B[np.ix_(mapping, mapping)]
552
+ return np.abs(A - mapped_product).sum(axis=1).tolist()
553
+
554
+
555
+ def _expanded_swap_pool(A, B, mapping, scores):
556
+ inv = [-1] * len(mapping)
557
+ for i, p in enumerate(mapping):
558
+ if 0 <= p < len(inv):
559
+ inv[p] = i
560
+
561
+ pool = {i for i, score in enumerate(scores) if score > 0}
562
+ for i in list(pool):
563
+ pool.update(j for j, value in enumerate(A[i]) if value)
564
+ p = mapping[i]
565
+ for q, value in enumerate(B[p]):
566
+ if value and inv[q] >= 0:
567
+ pool.add(inv[q])
568
+ return pool
569
+
570
+
571
+ def _local_swap_pool(lgp, mapping, binary, max_all_atoms, max_pool_per_element):
572
+ A, _ = _adjacency_and_elements(lgp[0], binary)
573
+ B, _ = _adjacency_and_elements(lgp[1], binary)
574
+ elements = _elements(lgp[0])
575
+ n = len(mapping)
576
+ if n <= max_all_atoms:
577
+ return set(range(n)), A, B
578
+
579
+ scores = _mismatch_scores(A, B, mapping)
580
+ pool = _expanded_swap_pool(A, B, mapping, scores)
581
+ by_element = defaultdict(list)
582
+ for i in pool:
583
+ by_element[elements[i]].append(i)
584
+
585
+ trimmed = set()
586
+ for idxs in by_element.values():
587
+ idxs.sort(key=lambda i: (-scores[i], i))
588
+ trimmed.update(idxs[:max_pool_per_element])
589
+ return trimmed, A, B
590
+
591
+
592
+ def _same_element_swaps(mapping, pool, elements, product_elements):
593
+ pool = sorted(pool)
594
+ for pos, i in enumerate(pool):
595
+ element = elements[i]
596
+ if product_elements[mapping[i]] != element:
597
+ continue
598
+ for k in pool[pos + 1 :]:
599
+ if elements[k] == element and product_elements[mapping[k]] == element:
600
+ yield i, k
601
+
602
+
603
+ def _greedy_local_swap_descent(lgp, mapping, pool, A, B, binary, max_steps):
604
+ elements = _elements(lgp[0])
605
+ product_elements = _elements(lgp[1])
606
+ current = list(mapping)
607
+ current_cd = chemical_distance(lgp, current, binary)
608
+ out = [(tuple(current), current_cd)]
609
+
610
+ for _ in range(max_steps):
611
+ best = None
612
+ for i, k in _same_element_swaps(current, pool, elements, product_elements):
613
+ cd = _swap_cd(A, B, current, current_cd, i, k)
614
+ if cd < current_cd - 1e-9 and (best is None or cd < best[0]):
615
+ best = (cd, i, k)
616
+ if best is None:
617
+ break
618
+ current_cd, i, k = best
619
+ current[i], current[k] = current[k], current[i]
620
+ out.append((tuple(current), current_cd))
621
+ return out
622
+
623
+
624
+ def _bounded_local_swap_records(
625
+ lgp,
626
+ seeds,
627
+ binary,
628
+ depth,
629
+ slack,
630
+ cap,
631
+ max_all_atoms,
632
+ max_pool_per_element,
633
+ max_descent_steps,
634
+ ):
635
+ start_best = min(cd for _, cd in seeds)
636
+ elements = _elements(lgp[0])
637
+ product_elements = _elements(lgp[1])
638
+
639
+ seen = set()
640
+ records = []
641
+ queue = deque()
642
+ for mapping, cd in seeds:
643
+ mapping_tuple = tuple(mapping)
644
+ if cd > start_best + slack + 1e-9 or mapping_tuple in seen:
645
+ continue
646
+ seen.add(mapping_tuple)
647
+ pool, A, B = _local_swap_pool(
648
+ lgp,
649
+ mapping,
650
+ binary,
651
+ max_all_atoms,
652
+ max_pool_per_element,
653
+ )
654
+ queue.append((list(mapping), cd, 0, pool, A, B))
655
+ records.append((mapping_tuple, cd))
656
+
657
+ for descended, descent_cd in _greedy_local_swap_descent(
658
+ lgp,
659
+ mapping,
660
+ pool,
661
+ A,
662
+ B,
663
+ binary,
664
+ max_descent_steps,
665
+ ):
666
+ if descent_cd > start_best + slack + 1e-9 or descended in seen:
667
+ continue
668
+ seen.add(descended)
669
+ records.append((descended, descent_cd))
670
+ if len(records) >= cap:
671
+ break
672
+
673
+ while queue and len(records) < cap:
674
+ mapping, cd, current_depth, pool, A, B = queue.popleft()
675
+ if current_depth >= depth:
676
+ continue
677
+
678
+ scored = []
679
+ for i, k in _same_element_swaps(mapping, pool, elements, product_elements):
680
+ next_cd = _swap_cd(A, B, mapping, cd, i, k)
681
+ if next_cd <= start_best + slack + 1e-9:
682
+ scored.append((next_cd, i, k))
683
+ scored.sort(key=lambda item: (item[0], item[1], item[2]))
684
+
685
+ for next_cd, i, k in scored:
686
+ next_mapping = list(mapping)
687
+ next_mapping[i], next_mapping[k] = next_mapping[k], next_mapping[i]
688
+ next_tuple = tuple(next_mapping)
689
+ if next_tuple in seen:
690
+ continue
691
+ seen.add(next_tuple)
692
+ records.append((next_tuple, next_cd))
693
+ queue.append((next_mapping, next_cd, current_depth + 1, pool, A, B))
694
+ if len(records) >= cap:
695
+ break
696
+
697
+ records.sort(key=lambda item: (item[1], item[0]))
698
+ return records
699
+
700
+
701
+ def _elements(lg):
702
+ elements = lg.props.get("atomic numbers")
703
+ if elements is None:
704
+ elements = list(lg.labels)
705
+ return list(elements)
706
+
707
+
708
+ def complete_mapping(lgp, mapping, binary=False): # noqa: C901
709
+ """
710
+ Complete a partial/duplicate mapping to an element-compatible bijection.
711
+
712
+ SLAP's default chemistry front-end breaks heavy-atom symmetry only, so
713
+ hydrogens can remain tied by identical labels. Kernel enumeration fixes the
714
+ chemically relevant atoms; this helper assigns any unresolved duplicate
715
+ atoms, typically hydrogens, to the remaining same-element product atoms.
716
+ """
717
+ mapping = list(mapping)
718
+ n = len(mapping)
719
+ elem_r = _elements(lgp[0])
720
+ elem_p = _elements(lgp[1])
721
+ out = [-1] * n
722
+ used = set()
723
+
724
+ for i, p in enumerate(mapping):
725
+ if not isinstance(p, int) or p < 0 or p >= n or p in used:
726
+ continue
727
+ if elem_r[i] != elem_p[p]:
728
+ continue
729
+ out[i] = p
730
+ used.add(p)
731
+
732
+ A, _ = _adjacency_and_elements(lgp[0], binary=binary)
733
+ B, _ = _adjacency_and_elements(lgp[1], binary=binary)
734
+
735
+ remaining_by_element = {}
736
+ for p in range(n):
737
+ if p not in used:
738
+ remaining_by_element.setdefault(elem_p[p], []).append(p)
739
+
740
+ unresolved_by_element = {}
741
+ for i, p in enumerate(out):
742
+ if p == -1:
743
+ unresolved_by_element.setdefault(elem_r[i], []).append(i)
744
+
745
+ fixed = [i for i, p in enumerate(out) if p != -1]
746
+ for element, idxs in unresolved_by_element.items():
747
+ candidates = remaining_by_element.get(element, [])
748
+ if len(candidates) != len(idxs):
749
+ raise ValueError("Could not complete element-compatible mapping")
750
+
751
+ cost = np.zeros((len(idxs), len(candidates)), dtype=float)
752
+ for row, i in enumerate(idxs):
753
+ for col, p in enumerate(candidates):
754
+ val = 0.0
755
+ for j in fixed:
756
+ pj = out[j]
757
+ val += abs(A[i, j] - B[p, pj])
758
+ val += abs(A[j, i] - B[pj, p])
759
+ cost[row, col] = val
760
+
761
+ rows, cols = linear_sum_assignment(cost)
762
+ for row, col in zip(rows, cols):
763
+ out[idxs[row]] = candidates[col]
764
+ used.add(candidates[col])
765
+ fixed.extend(idxs)
766
+
767
+ return out
768
+
769
+
770
+ def _identity_permutation(n):
771
+ return tuple(range(n))
772
+
773
+
774
+ def _automorphism_permutations(lg, binary=False, limit=10000):
775
+ """Enumerate graph automorphisms as permutations ``old_index -> new_index``."""
776
+ n = len(lg.labels)
777
+ try:
778
+ import networkx as nx
779
+ from networkx.algorithms import isomorphism as iso
780
+
781
+ g = to_nx(lg, binary=binary)
782
+ node_match = iso.categorical_node_match("element", None)
783
+ edge_match = iso.categorical_edge_match("order", None)
784
+ matcher = nx.algorithms.isomorphism.GraphMatcher(
785
+ g, g, node_match=node_match, edge_match=edge_match
786
+ )
787
+ except Exception:
788
+ return [_identity_permutation(n)]
789
+
790
+ perms = []
791
+ for mapping in matcher.isomorphisms_iter():
792
+ perms.append(tuple(mapping[i] for i in range(n)))
793
+ if len(perms) >= limit:
794
+ break
795
+ return perms or [_identity_permutation(n)]
796
+
797
+
798
+ def canonical_mapping_key(lgp, mapping, binary=False, max_automorphisms=10000):
799
+ """
800
+ Canonical tuple for a full mapping modulo ``Aut(R) x Aut(P)``.
801
+
802
+ If ``alpha`` is a reactant automorphism and ``beta`` is a product
803
+ automorphism, the equivalent mapping sends ``alpha(i)`` to
804
+ ``beta(mapping[i])``. The lexicographically smallest transformed mapping is
805
+ used as the canonical representative.
806
+ """
807
+ mapping = list(mapping)
808
+ r_aut = _automorphism_permutations(lgp[0], binary, max_automorphisms)
809
+ p_aut = _automorphism_permutations(lgp[1], binary, max_automorphisms)
810
+
811
+ best = None
812
+ for alpha in r_aut:
813
+ for beta in p_aut:
814
+ transformed = [None] * len(mapping)
815
+ for i, p in enumerate(mapping):
816
+ transformed[alpha[i]] = beta[p]
817
+ key = tuple(transformed)
818
+ if best is None or key < best:
819
+ best = key
820
+ return best if best is not None else tuple(mapping)
821
+
822
+
823
+ def canonical_partial_mapping_key(lgp, pairs, binary=False, max_automorphisms=10000):
824
+ """
825
+ Canonical key for fixed mapping pairs modulo ``Aut(R) x Aut(P)``.
826
+
827
+ ``pairs`` may be a dict or iterable of ``(reactant_index, product_index)``
828
+ assignments. This is useful for partially resolved SLAP/GraphMatcher states
829
+ where only the symmetry-breaking choices, rather than every atom, are unique.
830
+ """
831
+ if isinstance(pairs, dict):
832
+ pairs = list(pairs.items())
833
+ else:
834
+ pairs = list(pairs)
835
+ if not pairs:
836
+ return ()
837
+
838
+ r_aut = _automorphism_permutations(lgp[0], binary, max_automorphisms)
839
+ p_aut = _automorphism_permutations(lgp[1], binary, max_automorphisms)
840
+
841
+ best = None
842
+ for alpha in r_aut:
843
+ for beta in p_aut:
844
+ key = tuple(sorted((alpha[i], beta[p]) for i, p in pairs))
845
+ if best is None or key < best:
846
+ best = key
847
+ return best if best is not None else tuple(sorted(pairs))
848
+
849
+
850
+ def dedup_mappings_by_automorphism(lgp, mappings, binary=False):
851
+ """Keep one representative per ``Aut(R) x Aut(P)`` mapping class."""
852
+ seen = set()
853
+ distinct = []
854
+ for mapping in mappings:
855
+ key = canonical_mapping_key(lgp, mapping, binary=binary)
856
+ if key in seen:
857
+ continue
858
+ seen.add(key)
859
+ distinct.append(list(mapping))
860
+ return distinct
861
+
862
+
863
+ def _mapping_to_rxn_smiles(
864
+ rxn_smiles,
865
+ mapping,
866
+ explicit_hs=False,
867
+ explicit_h_atoms_pair=None,
868
+ ):
869
+ react_nums = list(range(1, len(mapping) + 1))
870
+ prod_nums = [0] * len(mapping)
871
+ for i, p in enumerate(mapping):
872
+ prod_nums[p] = i + 1
873
+ return get_numbered_rxn_smiles(
874
+ rxn_smiles,
875
+ [react_nums, prod_nums],
876
+ explicit_hs=explicit_hs,
877
+ explicit_h_atoms_pair=explicit_h_atoms_pair,
878
+ )
879
+
880
+
881
+ def _dedup_result_dicts_by_mapping(lgp, results, binary):
882
+ seen = set()
883
+ distinct = []
884
+ use_canonical = len(results) <= _MAX_AUTOMORPHISM_DEDUP_RESULTS
885
+ for result in results:
886
+ if use_canonical:
887
+ key = canonical_mapping_key(lgp, result["mapping"], binary=binary)
888
+ else:
889
+ key = tuple(result["mapping"])
890
+ if key in seen:
891
+ continue
892
+ seen.add(key)
893
+ distinct.append(result)
894
+ return distinct
895
+
896
+
897
+ def enumerate_kernel_optima( # noqa: C901
898
+ kernel,
899
+ rxn_smiles: Optional[str] = None,
900
+ unique: bool = True,
901
+ electron_balance: bool = False,
902
+ explicit_hs: bool = False,
903
+ reaction_center_hs: bool = False,
904
+ ):
905
+ """Enumerate exact, symmetry-distinct optima for a kernel.
906
+
907
+ :param kernel: Uncertainty-region kernel.
908
+ :type kernel: Kernel
909
+ :param rxn_smiles: Original reaction SMILES. If supplied, each result receives mapped
910
+ reaction SMILES and ITS-hash deduplication is used when available.
911
+ :type rxn_smiles: str, optional
912
+ :param unique: Collapse automorphism/ITS-equivalent optima.
913
+ :type unique: bool, optional
914
+ :param electron_balance: If True, prefer maps not rejected by the explicit-H ITS heuristic.
915
+ :type electron_balance: bool, optional
916
+ :param explicit_hs: If True, include explicit mapped hydrogens in output reaction SMILES.
917
+ :type explicit_hs: bool, optional
918
+ :param reaction_center_hs: If True, expand hydrogens only on heavy atoms incident to changed ITS
919
+ edges. This is intended for heavy-atom optimization with lightweight
920
+ reaction-center hydrogen display.
921
+ :type reaction_center_hs: bool, optional
922
+ """
923
+ solution = solve_kernel_blockwise(kernel, enumerate_all=True)
924
+ enumeration_complete = True
925
+ if not solution.sub_mappings:
926
+ fallback = solve_kernel_blockwise(kernel, enumerate_all=False)
927
+ if fallback.sub_mappings:
928
+ solution = fallback
929
+ enumeration_complete = False
930
+ else:
931
+ enumeration_complete = False
932
+
933
+ if (
934
+ rxn_smiles is not None
935
+ and len(solution.sub_mappings) > _MAX_MAPPED_ENUMERATION_RESULTS
936
+ ):
937
+ fallback = solve_kernel_blockwise(kernel, enumerate_all=False)
938
+ if fallback.sub_mappings:
939
+ solution = fallback
940
+ enumeration_complete = False
941
+ else:
942
+ return EnumerationResult(
943
+ cost=solution.cost,
944
+ mappings=[],
945
+ sub_mappings=[],
946
+ results=[],
947
+ proven_optimal=solution.proven_optimal,
948
+ enumeration_complete=False,
949
+ )
950
+
951
+ results = []
952
+ rejected_results = []
953
+ for sub_mapping in solution.sub_mappings:
954
+ mapping = complete_mapping(
955
+ kernel.lgp,
956
+ apply_kernel_solution(kernel, sub_mapping),
957
+ binary=kernel.binary,
958
+ )
959
+ result_lgp = mapping_to_lgp(kernel.lgp, mapping)
960
+ result = {
961
+ "lgp": result_lgp,
962
+ "mapping": mapping,
963
+ "sub_mapping": dict(sub_mapping),
964
+ "cd": chemical_distance(kernel.lgp, mapping, kernel.binary),
965
+ }
966
+ if rxn_smiles is not None:
967
+ result["its_smiles"] = _mapping_to_rxn_smiles(
968
+ rxn_smiles,
969
+ mapping,
970
+ explicit_hs=False,
971
+ )
972
+ if reaction_center_hs:
973
+ selected_maps = reaction_center_atom_maps(result["its_smiles"])
974
+ react_nums = list(range(1, len(mapping) + 1))
975
+ prod_nums = [0] * len(mapping)
976
+ for i, p in enumerate(mapping):
977
+ prod_nums[p] = i + 1
978
+ expanded_rxn = expand_reaction_center_hydrogens(
979
+ rxn_smiles,
980
+ [react_nums, prod_nums],
981
+ selected_maps,
982
+ )
983
+ try:
984
+ from ....chem.aam import AAMapper
985
+
986
+ center_mapper = AAMapper(binary=kernel.binary)
987
+ center_mapper.map_smiles(
988
+ expanded_rxn,
989
+ add_Hs=False,
990
+ break_sym="all",
991
+ unique=True,
992
+ certify=False,
993
+ electron_balance=False,
994
+ enumerate_exact=True,
995
+ )
996
+ center_smiles = [r["smiles"] for r in center_mapper.results]
997
+ except Exception:
998
+ center_smiles = []
999
+ if electron_balance:
1000
+ ok = mapped_rxn_is_electron_balanced(result["its_smiles"])
1001
+ result["electron_balanced"] = ok
1002
+ else:
1003
+ ok = None
1004
+ target = (
1005
+ rejected_results if electron_balance and ok is False else results
1006
+ )
1007
+ for mapped_smiles in center_smiles or [expanded_rxn]:
1008
+ rr = dict(result)
1009
+ rr["smiles"] = mapped_smiles
1010
+ target.append(rr)
1011
+ continue
1012
+ else:
1013
+ result["smiles"] = _mapping_to_rxn_smiles(
1014
+ rxn_smiles,
1015
+ mapping,
1016
+ explicit_hs=explicit_hs,
1017
+ )
1018
+ if electron_balance:
1019
+ ok = mapped_rxn_is_electron_balanced(result["its_smiles"])
1020
+ result["electron_balanced"] = ok
1021
+ if ok is False:
1022
+ rejected_results.append(result)
1023
+ continue
1024
+ results.append(result)
1025
+
1026
+ if electron_balance and not results and rejected_results:
1027
+ results = rejected_results
1028
+
1029
+ if (
1030
+ unique
1031
+ and rxn_smiles is not None
1032
+ and 1 < len(results) <= _MAX_SYNKIT_DEDUP_RESULTS
1033
+ ):
1034
+ results = dedup_mapped_rxns(results, smiles_key="its_smiles")
1035
+ elif unique and len(results) > 1:
1036
+ results = _dedup_result_dicts_by_mapping(kernel.lgp, results, kernel.binary)
1037
+
1038
+ return EnumerationResult(
1039
+ cost=solution.cost,
1040
+ mappings=[r["mapping"] for r in results],
1041
+ sub_mappings=[r["sub_mapping"] for r in results],
1042
+ results=results,
1043
+ proven_optimal=solution.proven_optimal,
1044
+ enumeration_complete=enumeration_complete,
1045
+ )
1046
+
1047
+
1048
+ def enumerate_symmetry_distinct_optima(*args, **kwargs):
1049
+ """Alias for :func:`enumerate_kernel_optima`."""
1050
+ return enumerate_kernel_optima(*args, **kwargs)