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,920 @@
1
+ """Matching, automorphism, and mapping-deduplication behavior for SynReactor."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import defaultdict
6
+ from typing import Any, Dict, List, Mapping, Optional, Tuple
7
+
8
+ import networkx as nx
9
+
10
+ from synkit.Graph import has_wildcard_node, remove_wildcard_nodes
11
+ from synkit.Graph.Hyrogen._misc import h_to_implicit, has_XH
12
+ from synkit.Graph.Matcher.partial_matcher import PartialMatcher
13
+ from synkit.Graph.Matcher.subgraph_matcher import (
14
+ SubgraphSearchEngine,
15
+ resolve_template_match_attrs,
16
+ )
17
+ from synkit.IO import setup_logging
18
+ from ..stereo.assignment import StereoWildcardAssignmentLimitError
19
+ from .policy import (
20
+ contextual_electron_pattern_graph,
21
+ deduplicate_joint_rule_mappings,
22
+ has_heavy_cross_component_correlation,
23
+ )
24
+ from .symmetry import (
25
+ deduplicate_exact_pattern_mappings,
26
+ deduplicate_free_host_component_mappings,
27
+ deduplicate_port_witness_mappings,
28
+ deduplicate_pure_coupling_mappings,
29
+ )
30
+ from ..core.strategy import Strategy
31
+
32
+ NodeId = Any
33
+ MappingDict = Dict[NodeId, NodeId]
34
+ log = setup_logging(task_type="synreactor")
35
+
36
+
37
+ class ReactorMatchingMixin:
38
+ """Internal matching behavior shared by the SynReactor facade."""
39
+
40
+ @staticmethod
41
+ def _filter_radical_lower_bound(
42
+ mappings: List[MappingDict], pattern: nx.Graph, host: nx.Graph
43
+ ) -> List[MappingDict]:
44
+ """Keep mappings whose host provides at least pattern radical count.
45
+
46
+ Exact radical comparison remains the concrete-mechanism default. This
47
+ opt-in policy is for intentionally generalized rule templates only.
48
+ """
49
+ return [
50
+ mapping
51
+ for mapping in mappings
52
+ if all(
53
+ int(host.nodes[host_node].get("radical", 0))
54
+ >= int(pattern.nodes[pattern_node].get("radical", 0))
55
+ for pattern_node, host_node in mapping.items()
56
+ )
57
+ ]
58
+
59
+ # ------------------------------------------------------------------
60
+ # Mapping / ITS / SMARTS (computed once, cached) --------------------
61
+ # ------------------------------------------------------------------
62
+ @property
63
+ def mappings(self) -> List[MappingDict]:
64
+ """Return unique sub‑graph mappings, optionally pruned via automorphisms."""
65
+ if self._mappings is None:
66
+ log.debug("Finding sub‑graph mappings (strategy=%s)", self.strategy)
67
+ full_pattern_graph = self._relative_pi_pattern_graph(self.rule.left.raw)
68
+ full_pattern_graph = contextual_electron_pattern_graph(
69
+ full_pattern_graph,
70
+ reaction_center=self.rule.rc.raw,
71
+ template_format=getattr(self.rule, "_format", None),
72
+ )
73
+ # Handle explicit‑H constraints
74
+ if has_XH(full_pattern_graph):
75
+ self._flag_pattern_has_explicit_H = True
76
+ full_pattern_graph = h_to_implicit(full_pattern_graph)
77
+ pattern_graph = full_pattern_graph
78
+ # Handle wildcard‑node patterns
79
+ if has_wildcard_node(pattern_graph):
80
+ pattern_graph = remove_wildcard_nodes(pattern_graph, inplace=False)
81
+
82
+ matching_host = self._matching_host_graph()
83
+ node_attrs, edge_attrs = resolve_template_match_attrs(pattern_graph)
84
+ if self.radical_policy in {"lower_bound", "ignore"}:
85
+ node_attrs = [attr for attr in node_attrs if attr != "radical"]
86
+
87
+ # --- Choose matcher ------------------------------------------------
88
+ if self.partial:
89
+ max_results = (
90
+ self.embed_threshold / 100 if self.embed_threshold else None
91
+ )
92
+ matcher = PartialMatcher(
93
+ host=matching_host,
94
+ pattern=pattern_graph,
95
+ node_attrs=node_attrs,
96
+ edge_attrs=edge_attrs,
97
+ strategy=Strategy.from_string(self.strategy),
98
+ threshold=self.embed_threshold,
99
+ pre_filter=self.embed_pre_filter,
100
+ max_results=max_results,
101
+ # WL colour classes are not automorphism orbits in
102
+ # general. Keep the exhaustive partial population here;
103
+ # exact rule/product quotients below remain authoritative.
104
+ prune_auto=False,
105
+ )
106
+ raw_maps = matcher.get_mappings()
107
+ else:
108
+ raw_maps = SubgraphSearchEngine.find_subgraph_mappings(
109
+ host=matching_host,
110
+ pattern=pattern_graph,
111
+ node_attrs=node_attrs,
112
+ edge_attrs=edge_attrs,
113
+ strategy=Strategy.from_string(self.strategy),
114
+ threshold=self.embed_threshold,
115
+ pre_filter=self.embed_pre_filter,
116
+ )
117
+
118
+ raw_maps.sort(
119
+ key=lambda candidate: self._mapping_atom_map_alignment(
120
+ pattern_graph,
121
+ matching_host,
122
+ candidate,
123
+ ),
124
+ reverse=True,
125
+ )
126
+
127
+ stereo_substitutions = self._typed_stereo_wildcard_substitutions(
128
+ full_pattern_graph
129
+ )
130
+ certified_generic = bool(
131
+ self.rule.rc.raw.graph.get("generic_stereo_extraction")
132
+ )
133
+ raw_maps = self._expand_stereo_wildcard_mappings(
134
+ raw_maps,
135
+ full_pattern_graph,
136
+ matching_host,
137
+ substitutions=stereo_substitutions,
138
+ )
139
+
140
+ if self.radical_policy == "lower_bound":
141
+ raw_maps = self._filter_radical_lower_bound(
142
+ raw_maps, pattern_graph, matching_host
143
+ )
144
+ if self.stereo_mode in {"require", "strict"}:
145
+ from synkit.Graph.Stereo import candidate_mapping_stereo_matches
146
+
147
+ presence_mode = (
148
+ "require"
149
+ if self.stereo_mode == "strict" and self.rule.stereo_couplings
150
+ else self.stereo_mode
151
+ )
152
+ raw_maps = [
153
+ mapping
154
+ for mapping in raw_maps
155
+ if candidate_mapping_stereo_matches(
156
+ full_pattern_graph,
157
+ matching_host,
158
+ mapping,
159
+ mode=presence_mode,
160
+ unknown_policy=self.stereo_query_mode,
161
+ query_policies=self.rule.stereo_query_policies,
162
+ substitutions=stereo_substitutions,
163
+ semantics=self.stereo_semantics,
164
+ diagnostics=self._stereo_semantic_diagnostics,
165
+ morphism_issues=self._stereo_morphism_issues,
166
+ )
167
+ ]
168
+ elif self.stereo_mode == "propagate":
169
+ if certified_generic and stereo_substitutions:
170
+ from synkit.Graph.Stereo import candidate_mapping_stereo_matches
171
+
172
+ raw_maps = [
173
+ mapping
174
+ for mapping in raw_maps
175
+ if candidate_mapping_stereo_matches(
176
+ full_pattern_graph,
177
+ matching_host,
178
+ mapping,
179
+ mode="propagate",
180
+ unknown_policy=self.stereo_query_mode,
181
+ query_policies=self.rule.stereo_query_policies,
182
+ substitutions=stereo_substitutions,
183
+ semantics=self.stereo_semantics,
184
+ diagnostics=self._stereo_semantic_diagnostics,
185
+ morphism_issues=self._stereo_morphism_issues,
186
+ )
187
+ ]
188
+ exact_targets = self._noncovariant_propagation_targets()
189
+ if exact_targets:
190
+ from synkit.Graph.Stereo import candidate_mapping_stereo_matches
191
+
192
+ selective_pattern = full_pattern_graph.copy()
193
+ selective_pattern.graph["stereo_descriptors"] = {
194
+ key: self.rule.stereo_guards[key] for key in exact_targets
195
+ }
196
+ raw_maps = [
197
+ mapping
198
+ for mapping in raw_maps
199
+ if candidate_mapping_stereo_matches(
200
+ selective_pattern,
201
+ matching_host,
202
+ mapping,
203
+ mode="require",
204
+ unknown_policy="exact",
205
+ query_policies={key: "exact" for key in exact_targets},
206
+ semantics=self.stereo_semantics,
207
+ diagnostics=self._stereo_semantic_diagnostics,
208
+ )
209
+ ]
210
+
211
+ if self.rule.stereo_couplings and self.stereo_mode != "ignore":
212
+ raw_maps = [
213
+ mapping
214
+ for mapping in raw_maps
215
+ if self._coupling_mapping_inputs_match(
216
+ matching_host,
217
+ mapping,
218
+ strict=self.stereo_mode == "strict",
219
+ )
220
+ ]
221
+
222
+ stereo_sensitive = bool(
223
+ self.rule.stereo_guards
224
+ or self.rule.stereo_effects
225
+ or self.rule.stereo_outcomes
226
+ or self.rule.stereo_couplings
227
+ )
228
+ if len(raw_maps) < 2:
229
+ self._mappings = raw_maps
230
+ elif certified_generic:
231
+ # Certified generic rules retain the complete injective port
232
+ # assignment population. Exact product quotienting happens
233
+ # only after rewrite, where multiplicity and provenance can
234
+ # be preserved. A future stabilizer optimization must prove
235
+ # population equivalence before entering this branch.
236
+ self._mappings = raw_maps
237
+ log.debug(
238
+ "Certified generic stereo rule retained %d exhaustive mapping(s)",
239
+ len(raw_maps),
240
+ )
241
+ elif self.automorphism and not stereo_sensitive:
242
+ # Build transition roles only when a nontrivial mapping
243
+ # population can actually benefit from the exact quotient.
244
+ automorphism_pattern = self._automorphism_pattern_graph(pattern_graph)
245
+ components = [
246
+ frozenset(component)
247
+ for component in nx.connected_components(automorphism_pattern)
248
+ ]
249
+ correlated = has_heavy_cross_component_correlation(
250
+ automorphism_pattern,
251
+ self.rule.rc.raw,
252
+ )
253
+ fixed_nodes = (
254
+ frozenset()
255
+ if correlated or len(components) < 2
256
+ else max(components, key=len)
257
+ )
258
+ self._mappings = deduplicate_joint_rule_mappings(
259
+ raw_maps,
260
+ automorphism_pattern,
261
+ self.rule.rc.raw,
262
+ node_attrs=self._automorphism_node_attrs(
263
+ automorphism_pattern,
264
+ node_attrs,
265
+ ),
266
+ edge_attrs=edge_attrs,
267
+ fixed_nodes=fixed_nodes,
268
+ )
269
+ log.debug(
270
+ "Exact rule symmetry: %d → %d mapping(s)",
271
+ len(raw_maps),
272
+ len(self._mappings),
273
+ )
274
+ elif stereo_sensitive:
275
+ # Wildcard ports are existential stereo witnesses rather than
276
+ # rewrite resources. Once a witness has passed the typed
277
+ # stereo predicate, retain one per identical structural map.
278
+ # Exact host-component and coupling group actions then remove
279
+ # only jointly realizable symmetry variants.
280
+ stereo_pattern = self._stereo_automorphism_pattern_graph(pattern_graph)
281
+ stereo_attrs = self._automorphism_node_attrs(
282
+ stereo_pattern,
283
+ node_attrs,
284
+ )
285
+ if "_stereo_role" not in stereo_attrs:
286
+ stereo_attrs.append("_stereo_role")
287
+ port_unique = deduplicate_port_witness_mappings(
288
+ raw_maps,
289
+ stereo_pattern,
290
+ )
291
+ # Preserve the established application-provenance population
292
+ # for reactant-side stereo guards/destruction. Product-side
293
+ # formation is covariant under the decorated pattern action,
294
+ # so it can use the exact quotient before rewrite.
295
+ product_side_stereo = (
296
+ bool(self.rule.stereo_effects)
297
+ and not self.rule.stereo_guards
298
+ and not self.rule.stereo_couplings
299
+ and all(
300
+ change.before is None
301
+ for change in self.rule.stereo_effects.values()
302
+ )
303
+ )
304
+ pattern_unique = (
305
+ deduplicate_exact_pattern_mappings(
306
+ port_unique,
307
+ stereo_pattern,
308
+ node_attrs=stereo_attrs,
309
+ edge_attrs=edge_attrs,
310
+ )
311
+ if product_side_stereo
312
+ else port_unique
313
+ )
314
+ host_unique = deduplicate_free_host_component_mappings(
315
+ pattern_unique,
316
+ matching_host,
317
+ node_attrs=stereo_attrs,
318
+ edge_attrs=edge_attrs,
319
+ )
320
+ self._mappings = deduplicate_pure_coupling_mappings(
321
+ host_unique,
322
+ stereo_pattern,
323
+ self.rule,
324
+ node_attrs=stereo_attrs,
325
+ edge_attrs=edge_attrs,
326
+ )
327
+ log.debug(
328
+ "Stereo witness/exact symmetry: %d → %d → %d → %d "
329
+ "→ %d mapping(s)",
330
+ len(raw_maps),
331
+ len(port_unique),
332
+ len(pattern_unique),
333
+ len(host_unique),
334
+ len(self._mappings),
335
+ )
336
+ else:
337
+ self._mappings = raw_maps
338
+
339
+ log.info("%d mapping(s) discovered", len(self._mappings))
340
+ return self._mappings
341
+
342
+ def _expand_stereo_wildcard_mappings(
343
+ self,
344
+ mappings: List[MappingDict],
345
+ pattern: nx.Graph,
346
+ host: nx.Graph,
347
+ *,
348
+ substitutions: Mapping[NodeId, Any] | None = None,
349
+ ) -> List[MappingDict]:
350
+ """Bind wildcard stereo references to actual host neighbors.
351
+
352
+ Wildcard ligand nodes are omitted from structural matching, but a
353
+ tetrahedral descriptor still needs their concrete identities for
354
+ parity propagation. Only wildcards referenced by stored stereo state
355
+ are expanded; ordinary wildcard regions retain their legacy behavior.
356
+ """
357
+ wildcard_nodes = {
358
+ node
359
+ for node, attrs in pattern.nodes(data=True)
360
+ if attrs.get("element") == "*"
361
+ }
362
+ if not mappings or not wildcard_nodes:
363
+ return mappings
364
+
365
+ referenced_maps = {
366
+ reference
367
+ for descriptor in self.rule.stereo_guards.values()
368
+ for reference in descriptor.atoms
369
+ if isinstance(reference, int)
370
+ }
371
+ referenced_maps.update(
372
+ reference
373
+ for change in self.rule.stereo_effects.values()
374
+ for descriptor in (change.before, change.after, change.transition)
375
+ if descriptor is not None
376
+ for reference in descriptor.atoms
377
+ if isinstance(reference, int)
378
+ )
379
+ by_map = self._nodes_by_atom_map(pattern)
380
+ stereo_wildcards = sorted(
381
+ (
382
+ by_map[atom_map]
383
+ for atom_map in referenced_maps
384
+ if atom_map in by_map and by_map[atom_map] in wildcard_nodes
385
+ ),
386
+ key=repr,
387
+ )
388
+ if not stereo_wildcards:
389
+ return mappings
390
+
391
+ expanded: List[MappingDict] = []
392
+ assignment_count = 0
393
+ substitutions = substitutions or {}
394
+ from synkit.Graph.Morphism import (
395
+ StereoMorphismIssue,
396
+ StereoMorphismIssueCode,
397
+ )
398
+
399
+ def reject(
400
+ code: Any,
401
+ message: str,
402
+ wildcard: NodeId,
403
+ candidate: NodeId | None = None,
404
+ ) -> None:
405
+ self._stereo_morphism_issues.append(
406
+ StereoMorphismIssue(
407
+ code,
408
+ message,
409
+ {
410
+ "source_node": repr(wildcard),
411
+ "target_node": repr(candidate),
412
+ },
413
+ )
414
+ )
415
+
416
+ for mapping in mappings:
417
+ partials = [dict(mapping)]
418
+ for wildcard in stereo_wildcards:
419
+ next_partials: List[MappingDict] = []
420
+ anchors = [
421
+ neighbor
422
+ for neighbor in pattern.neighbors(wildcard)
423
+ if neighbor not in wildcard_nodes
424
+ ]
425
+ for partial in partials:
426
+ mapped_anchors = [
427
+ partial[anchor] for anchor in anchors if anchor in partial
428
+ ]
429
+ if len(mapped_anchors) != len(anchors) or not mapped_anchors:
430
+ continue
431
+ candidates = set(host.neighbors(mapped_anchors[0]))
432
+ for anchor in mapped_anchors[1:]:
433
+ candidates &= set(host.neighbors(anchor))
434
+ candidates -= set(partial.values())
435
+ for candidate in sorted(candidates, key=repr):
436
+ constraint = substitutions.get(wildcard)
437
+ if constraint is not None:
438
+ owner = constraint.owner
439
+ owner_node = (
440
+ owner if owner in pattern else by_map.get(owner)
441
+ )
442
+ target_owner = (
443
+ partial.get(owner_node)
444
+ if owner_node is not None
445
+ else None
446
+ )
447
+ if target_owner is None or not host.has_edge(
448
+ target_owner, candidate
449
+ ):
450
+ reject(
451
+ StereoMorphismIssueCode.PORT_OWNER_MISMATCH,
452
+ "A typed stereo port owner is absent from the candidate mapping.",
453
+ wildcard,
454
+ candidate,
455
+ )
456
+ continue
457
+ attrs = host.nodes[candidate]
458
+ element = attrs.get("element")
459
+ virtual_kind = (
460
+ "H"
461
+ if element == "H"
462
+ else ("LP" if element in {"LP", "Lp", "lp"} else None)
463
+ )
464
+ concrete = {
465
+ "element": element,
466
+ "charge": attrs.get("charge", 0),
467
+ "radical": attrs.get("radical", 0),
468
+ "bond_order": host.edges[target_owner, candidate].get(
469
+ "order"
470
+ ),
471
+ "owner": constraint.owner,
472
+ "side": "reactant",
473
+ "stereo_slot": constraint.stereo_slot,
474
+ "virtual_kind": virtual_kind,
475
+ "mapped_identity": attrs.get("atom_map", candidate),
476
+ "materialization": attrs.get(
477
+ "materialization",
478
+ (
479
+ "virtual"
480
+ if virtual_kind is not None
481
+ else "concrete"
482
+ ),
483
+ ),
484
+ "capacity": 1,
485
+ "resource_usage": 1,
486
+ }
487
+ if constraint.virtual_kind is not None and (
488
+ virtual_kind != constraint.virtual_kind
489
+ ):
490
+ reject(
491
+ StereoMorphismIssueCode.PORT_VIRTUAL_KIND_MISMATCH,
492
+ "A stereo port candidate has the wrong virtual kind.",
493
+ wildcard,
494
+ candidate,
495
+ )
496
+ continue
497
+ if constraint.capacity < 1 or (
498
+ constraint.resource_budget is not None
499
+ and constraint.resource_budget < 1
500
+ ):
501
+ reject(
502
+ StereoMorphismIssueCode.PORT_CAPACITY_MISMATCH,
503
+ "A stereo port has no materialization capacity.",
504
+ wildcard,
505
+ candidate,
506
+ )
507
+ continue
508
+ if not constraint.satisfies(concrete):
509
+ reject(
510
+ StereoMorphismIssueCode.PORT_DOMAIN_MISMATCH,
511
+ "A stereo port candidate violates its typed domain.",
512
+ wildcard,
513
+ candidate,
514
+ )
515
+ continue
516
+ branch = dict(partial)
517
+ branch[wildcard] = candidate
518
+ next_partials.append(branch)
519
+ partials = next_partials
520
+ assignment_count += len(partials)
521
+ if (
522
+ self.stereo_assignment_limit is not None
523
+ and assignment_count > self.stereo_assignment_limit
524
+ ):
525
+ raise StereoWildcardAssignmentLimitError(
526
+ self.stereo_assignment_limit,
527
+ assignment_count,
528
+ )
529
+ expanded.extend(partials)
530
+ return expanded
531
+
532
+ @staticmethod
533
+ def _typed_stereo_wildcard_substitutions(
534
+ pattern: nx.Graph,
535
+ ) -> Dict[NodeId, Any]:
536
+ """Adapt explicitly typed stereo wildcard nodes at the Reactor boundary."""
537
+ from synkit.Graph.Morphism import (
538
+ NodeStateKind,
539
+ WildcardRole,
540
+ adapt_legacy_node_state,
541
+ )
542
+
543
+ substitutions: Dict[NodeId, Any] = {}
544
+ for node, attrs in pattern.nodes(data=True):
545
+ if attrs.get("element") not in {"*", ("*", "*")} or (
546
+ "wildcard_role" not in attrs
547
+ ):
548
+ continue
549
+ state = adapt_legacy_node_state(attrs)
550
+ if (
551
+ state.kind is NodeStateKind.WILDCARD
552
+ and state.constraint is not None
553
+ and state.constraint.role is WildcardRole.STEREO_LIGAND_PORT
554
+ ):
555
+ substitutions[node] = state.constraint
556
+ return substitutions
557
+
558
+ @staticmethod
559
+ def _nodes_by_atom_map(graph: nx.Graph) -> Dict[int, Any]:
560
+ """Return graph node identifiers keyed by positive atom-map value."""
561
+ result: Dict[int, Any] = {}
562
+ for node, attrs in graph.nodes(data=True):
563
+ atom_map = attrs.get("atom_map", node)
564
+ if isinstance(atom_map, tuple) and atom_map:
565
+ atom_map = atom_map[0]
566
+ if isinstance(atom_map, int) and atom_map > 0:
567
+ if atom_map in result and result[atom_map] != node:
568
+ raise ValueError(f"Duplicate atom map {atom_map} in graph.")
569
+ result[atom_map] = node
570
+ return result
571
+
572
+ def _relative_pi_rewrite_edges(self, rc: nx.Graph) -> set[frozenset[Any]]:
573
+ """Infer coupled bonds whose edit is one relative pi reduction.
574
+
575
+ This is intentionally restricted to declared vicinal additions. A
576
+ normal structural rule continues to match and write absolute bond
577
+ orders exactly as before.
578
+ """
579
+ by_map = self._nodes_by_atom_map(rc)
580
+ result: set[frozenset[Any]] = set()
581
+ for coupling in self.rule.stereo_couplings.values():
582
+ if coupling.kind != "VICINAL_ADDITION":
583
+ continue
584
+ try:
585
+ left, right = (by_map[value] for value in coupling.centers)
586
+ except KeyError:
587
+ continue
588
+ if not rc.has_edge(left, right):
589
+ continue
590
+ attrs = rc.edges[left, right]
591
+ pi_order = attrs.get("pi_order")
592
+ sigma_order = attrs.get("sigma_order")
593
+ if not (
594
+ isinstance(pi_order, tuple)
595
+ and len(pi_order) == 2
596
+ and float(pi_order[0]) - float(pi_order[1]) == 1.0
597
+ and isinstance(sigma_order, tuple)
598
+ and len(sigma_order) == 2
599
+ and float(sigma_order[0]) == float(sigma_order[1]) == 1.0
600
+ ):
601
+ continue
602
+ result.add(frozenset((left, right)))
603
+ return result
604
+
605
+ def _relative_pi_pattern_graph(self, pattern: nx.Graph) -> nx.Graph:
606
+ """Decorate only inferred relative pi-addition query positions."""
607
+ relative_edges = self._relative_pi_rewrite_edges(self.rule.rc.raw)
608
+ if not relative_edges:
609
+ return pattern
610
+ decorated = pattern.copy()
611
+ for edge in relative_edges:
612
+ left, right = tuple(edge)
613
+ if not decorated.has_edge(left, right):
614
+ continue
615
+ attrs = decorated.edges[left, right]
616
+ minimum_pi = float(attrs.get("pi_order", 0.0))
617
+ if minimum_pi < 1.0:
618
+ continue
619
+ attrs["_minimum_pi_order"] = minimum_pi
620
+ for node in (left, right):
621
+ policies = dict(
622
+ decorated.nodes[node].get(
623
+ "_query_attribute_policies",
624
+ {},
625
+ )
626
+ )
627
+ policies["radical"] = "unknown"
628
+ decorated.nodes[node]["_query_attribute_policies"] = policies
629
+ return decorated
630
+
631
+ @staticmethod
632
+ def _automorphism_node_attrs(
633
+ pattern: nx.Graph,
634
+ node_attrs: List[str],
635
+ ) -> List[str]:
636
+ """Keep pruning at least as role-aware as the stored template data."""
637
+ attrs = list(node_attrs)
638
+ for attr in (
639
+ "aromatic",
640
+ "neighbors",
641
+ "_query_attribute_policies",
642
+ "_rewrite_role",
643
+ ):
644
+ if attr not in attrs and any(
645
+ attr in data for _, data in pattern.nodes(data=True)
646
+ ):
647
+ attrs.append(attr)
648
+ return attrs
649
+
650
+ def _automorphism_pattern_graph(self, pattern: nx.Graph) -> nx.Graph:
651
+ """Decorate patterns with complete local rewrite roles for pruning.
652
+
653
+ Pattern connectivity contains only the source endpoint. Two source
654
+ atoms can therefore be symmetric even when the rule reconnects them
655
+ to different leaving groups on the other endpoint. Automorphism
656
+ pruning must include that transition incidence or it can discard the
657
+ only application that reconstructs the reference precursors.
658
+ """
659
+
660
+ def freeze(value: Any) -> Any:
661
+ if isinstance(value, dict):
662
+ return tuple(sorted((key, freeze(item)) for key, item in value.items()))
663
+ if isinstance(value, (list, tuple)):
664
+ shallow = tuple(value)
665
+ try:
666
+ hash(shallow)
667
+ except TypeError:
668
+ return tuple(freeze(item) for item in value)
669
+ return shallow
670
+ if isinstance(value, set):
671
+ return tuple(sorted((freeze(item) for item in value), key=repr))
672
+ return value
673
+
674
+ def node_transition(attrs: Mapping[str, Any]) -> Any:
675
+ types = attrs.get("typesGH")
676
+ if isinstance(types, tuple) and len(types) == 2:
677
+ return tuple(
678
+ freeze(self._chemical_rewrite_role(side)) for side in types
679
+ )
680
+ return freeze(
681
+ tuple(
682
+ (key, attrs.get(key))
683
+ for key in (
684
+ "element",
685
+ "aromatic",
686
+ "hcount",
687
+ "charge",
688
+ "lone_pairs",
689
+ "radical",
690
+ "valence_electrons",
691
+ )
692
+ )
693
+ )
694
+
695
+ decorated = pattern.copy()
696
+ rc = self.rule.rc.raw
697
+ for node, attrs in decorated.nodes(data=True):
698
+ rc_attrs = rc.nodes.get(node)
699
+ if not rc_attrs:
700
+ continue
701
+ incidence = []
702
+ for neighbor in rc.neighbors(node):
703
+ edge = rc.edges[node, neighbor]
704
+ incidence.append(
705
+ (
706
+ node_transition(rc.nodes[neighbor]),
707
+ freeze(edge.get("order")),
708
+ freeze(edge.get("standard_order")),
709
+ )
710
+ )
711
+ attrs["_rewrite_role"] = (
712
+ node_transition(rc_attrs),
713
+ tuple(sorted(incidence, key=repr)),
714
+ )
715
+ return decorated
716
+
717
+ def _stereo_automorphism_pattern_graph(self, pattern: nx.Graph) -> nx.Graph:
718
+ """Decorate pattern atoms by their ordered rule-descriptor roles.
719
+
720
+ Exact positions are deliberately conservative: an automorphism may
721
+ still exchange atoms absent from all stereo states (for example H2),
722
+ but cannot exchange two references whose permutation could invert a
723
+ stored descriptor.
724
+ """
725
+ decorated = self._automorphism_pattern_graph(pattern)
726
+ by_map: Dict[int, Any] = {}
727
+ for node, attrs in decorated.nodes(data=True):
728
+ atom_map = attrs.get("atom_map", node)
729
+ if isinstance(atom_map, int):
730
+ by_map[atom_map] = node
731
+
732
+ roles: Dict[Any, List[Tuple[str, str, int]]] = defaultdict(list)
733
+
734
+ def role_position(descriptor: Any, position: int) -> int:
735
+ # Planar-bond identity permits reversal of the complete bond and
736
+ # simultaneous end swaps. Molecular connectivity/atom labels
737
+ # couple those moves, while a coarse center/reference role lets
738
+ # the legitimate whole-bond reversal survive automorphism.
739
+ if getattr(descriptor, "descriptor_class", None) == "planar_bond":
740
+ return -2 if position in {2, 3} else -1
741
+ # Tetrahedral odd/even permutation parity requires exact slots.
742
+ return position
743
+
744
+ for key, change in self.rule.stereo_effects.items():
745
+ for state_name, descriptor in (
746
+ ("before", change.before),
747
+ ("after", change.after),
748
+ ("transition", change.transition),
749
+ ):
750
+ if descriptor is None:
751
+ continue
752
+ for position, atom_map in enumerate(descriptor.atoms):
753
+ if isinstance(atom_map, int) and atom_map in by_map:
754
+ roles[by_map[atom_map]].append(
755
+ (key, state_name, role_position(descriptor, position))
756
+ )
757
+ for key, descriptor in self.rule.stereo_guards.items():
758
+ for position, atom_map in enumerate(descriptor.atoms):
759
+ if isinstance(atom_map, int) and atom_map in by_map:
760
+ roles[by_map[atom_map]].append(
761
+ (key, "guard", role_position(descriptor, position))
762
+ )
763
+ for node in decorated.nodes:
764
+ decorated.nodes[node]["_stereo_role"] = tuple(
765
+ sorted(set(roles.get(node, [])))
766
+ )
767
+ return decorated
768
+
769
+ @staticmethod
770
+ def _mapping_provenance(
771
+ mapping: MappingDict,
772
+ pattern: nx.Graph,
773
+ host: nx.Graph,
774
+ ) -> Tuple[Tuple[Any, Any], ...]:
775
+ """Return a deterministic template-map to substrate-map application trace."""
776
+ pairs = []
777
+ for pattern_node, host_node in mapping.items():
778
+ pattern_ref = pattern.nodes[pattern_node].get("atom_map", pattern_node)
779
+ host_ref = host.nodes[host_node].get("atom_map", host_node)
780
+ if not isinstance(pattern_ref, int) or pattern_ref <= 0:
781
+ pattern_ref = pattern_node
782
+ if not isinstance(host_ref, int) or host_ref <= 0:
783
+ host_ref = host_node
784
+ pairs.append((pattern_ref, host_ref))
785
+ return tuple(sorted(pairs, key=lambda pair: (repr(pair[0]), repr(pair[1]))))
786
+
787
+ @staticmethod
788
+ def _configuration_provenance(configuration: Any | None) -> Any:
789
+ """Return deterministic JSON-compatible local-frame evidence."""
790
+ if configuration is None:
791
+ return None
792
+ shape, specification, frame = configuration.canonical_form()
793
+ return {
794
+ "shape": shape,
795
+ "specification": specification,
796
+ "canonical_frame": [repr(reference) for reference in frame],
797
+ }
798
+
799
+ def _stereo_application_provenance(
800
+ self,
801
+ pattern: nx.Graph,
802
+ host: nx.Graph,
803
+ mapping: MappingDict,
804
+ substitutions: Mapping[NodeId, Any],
805
+ ) -> Dict[str, Any]:
806
+ """Build replayable local stereo evidence for one accepted mapping."""
807
+ from synkit.Graph.Stereo import candidate_mapping_stereo_morphism
808
+
809
+ proof = candidate_mapping_stereo_morphism(
810
+ pattern,
811
+ host,
812
+ mapping,
813
+ mode=self.stereo_mode,
814
+ unknown_policy=self.stereo_query_mode,
815
+ query_policies=self.rule.stereo_query_policies,
816
+ substitutions=substitutions,
817
+ )
818
+ certificates = []
819
+ for certificate in proof.certificates:
820
+ relation = certificate.relation
821
+ witness = certificate.witness
822
+ certificates.append(
823
+ {
824
+ "layer": certificate.layer,
825
+ "status": certificate.status.value,
826
+ "information_policy": certificate.information_policy.value,
827
+ "source": self._configuration_provenance(
828
+ certificate.source_configuration
829
+ ),
830
+ "target": self._configuration_provenance(
831
+ certificate.target_configuration
832
+ ),
833
+ "relation": (
834
+ None
835
+ if relation is None
836
+ else {
837
+ "kind": relation.kind.value,
838
+ "shape": relation.shape,
839
+ "class_id": relation.class_id,
840
+ }
841
+ ),
842
+ "witness": (
843
+ None if witness is None else tuple(witness.permutation.image)
844
+ ),
845
+ }
846
+ )
847
+ return {
848
+ "presence_mode": proof.presence_mode.value,
849
+ "information_policy": proof.information_policy.value,
850
+ "certificates": certificates,
851
+ }
852
+
853
+ def _deduplicate_equivalent_free_components(
854
+ self,
855
+ mappings: List[MappingDict],
856
+ pattern: nx.Graph,
857
+ anchor: Optional[frozenset[NodeId]],
858
+ node_attrs: List[str],
859
+ edge_attrs: List[str],
860
+ ) -> List[MappingDict]:
861
+ """Collapse swaps of equivalent disconnected non-anchor tuple components."""
862
+ if getattr(self.rule, "_format", None) != "tuple" or len(mappings) < 2:
863
+ return mappings
864
+
865
+ anchor = anchor or frozenset()
866
+ free_components = [
867
+ frozenset(component)
868
+ for component in nx.connected_components(pattern)
869
+ if not set(component) & set(anchor)
870
+ ]
871
+ if len(free_components) < 2:
872
+ return mappings
873
+
874
+ component_groups: List[List[frozenset[NodeId]]] = []
875
+ for component in free_components:
876
+ for group in component_groups:
877
+ if self._components_are_equivalent(
878
+ pattern,
879
+ component,
880
+ group[0],
881
+ node_attrs,
882
+ edge_attrs,
883
+ ):
884
+ group.append(component)
885
+ break
886
+ else:
887
+ component_groups.append([component])
888
+
889
+ swappable_groups = [group for group in component_groups if len(group) > 1]
890
+ if not swappable_groups:
891
+ return mappings
892
+
893
+ swappable_nodes = set().union(
894
+ *[set().union(*group) for group in swappable_groups]
895
+ )
896
+ seen = set()
897
+ unique: List[MappingDict] = []
898
+ for mapping in mappings:
899
+ fixed = tuple(
900
+ sorted(
901
+ (node, host)
902
+ for node, host in mapping.items()
903
+ if node not in swappable_nodes
904
+ )
905
+ )
906
+ component_bags = tuple(
907
+ tuple(
908
+ sorted(
909
+ tuple(sorted(mapping[node] for node in component))
910
+ for component in group
911
+ )
912
+ )
913
+ for group in swappable_groups
914
+ )
915
+ signature = (fixed, component_bags)
916
+ if signature in seen:
917
+ continue
918
+ seen.add(signature)
919
+ unique.append(mapping)
920
+ return unique