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,1090 @@
1
+ """syn_rule.py
2
+ ================
3
+ Immutable description of a reaction template (SynRule) with canonical forms
4
+ and optional implicit‐hydrogen stripping.
5
+
6
+ Key features
7
+ ------------
8
+ * **Fragment decomposition** – splits the ITS graph into rc, left, and right.
9
+ * **Implicit H‐handling** – converts explicit H nodes into hcount + h_pairs.
10
+ * **Canonicalisation** – wraps rc/left/right in SynGraph for stable signatures.
11
+ * **Value‑object semantics** – `__eq__`/`__hash__` use fragment signatures.
12
+
13
+ Quick start
14
+ -----------
15
+ >>> from synkit.Graph.syn_rule import SynRule
16
+ >>> rule = SynRule.from_smart("[CH3:1]C>>[CH2:1]C")
17
+ >>> rule.left.signature, rule.right.signature
18
+ ('abc123...', 'def456...')
19
+
20
+ """
21
+
22
+ from __future__ import annotations
23
+ from numbers import Real
24
+ from typing import Any, Mapping, Optional, Tuple
25
+
26
+ import networkx as nx
27
+
28
+ from synkit.Graph.syn_graph import SynGraph
29
+ from synkit.Graph.canon_graph import GraphCanonicaliser
30
+ from synkit.Graph.ITS.its_decompose import its_decompose
31
+ from synkit.Graph.ITS.its_construction import ITSConstruction
32
+ from synkit.Graph.ITS.its_reverter import ITSReverter
33
+ from synkit.Graph.Hyrogen._misc import normalize_h_pair_graph
34
+ from synkit.Graph.Morphism.constraints import NodeStateKind, adapt_legacy_node_state
35
+ from synkit.Graph.Stereo import (
36
+ NonInvertibleStereoEffectError,
37
+ StereoCoupling,
38
+ StereoOutcome,
39
+ )
40
+ from synkit.Rule.stereo_identity import stereo_rule_isomorphic
41
+ from synkit.IO.chem_converter import (
42
+ ITSFormat,
43
+ detect_its_format,
44
+ rsmi_to_its,
45
+ gml_to_its,
46
+ )
47
+
48
+ __all__ = ["NonInvertibleStereoEffectError", "SynRule"]
49
+
50
+ _WILDCARD_CONTRACT_KEYS = (
51
+ "wildcard_role",
52
+ "elements",
53
+ "charges",
54
+ "radicals",
55
+ "bond_orders",
56
+ "side",
57
+ "owner",
58
+ "capacity",
59
+ "resource_budget",
60
+ "stereo_slot",
61
+ "virtual_kind",
62
+ "mapped_identity",
63
+ "materialization",
64
+ )
65
+ _RELATIVE_NODE_RESOURCES = ("lone_pairs",)
66
+
67
+
68
+ class SynRule:
69
+ """Immutable reaction template: rc, left, and right fragments as SynGraph Object.
70
+
71
+ :param rc_graph: Raw reaction-centre (RC) graph.
72
+ :type rc_graph: nx.Graph
73
+ :param name: Identifier for the rule.
74
+ :type name: str, default ``"rule"``
75
+ :param canonicaliser: Custom canonicaliser; if *None* a default is created.
76
+ :type canonicaliser: Optional[GraphCanonicaliser]
77
+ :param canon: If *True*, build canonical forms and SHA-256 signatures.
78
+ :type canon: bool, default ``True``
79
+ :param implicit_h: Convert explicit hydrogens in the **rc/left/right** fragments to an
80
+ integer ``hcount`` attribute and record cross-fragment hydrogen pairs
81
+ in a ``h_pairs`` attribute.
82
+ :type implicit_h: bool, default ``True``
83
+ :param stereo_outcomes: Explicit product-branch declarations keyed by descriptor target. A
84
+ ``RACEMIC`` outcome uses the stored product descriptor as a reference
85
+ orientation and makes application emit it and its inverse.
86
+ :type stereo_outcomes: typing.Mapping or None
87
+ :param stereo_couplings: Coupled rule operations keyed by their central bond target. A vicinal
88
+ addition may be declared compactly as ``{"bond:2-3": "ANTI"}``;
89
+ centers and delivered ligands are inferred from the structural edit.
90
+ :type stereo_couplings: typing.Mapping or None
91
+
92
+ :ivar rc: Wrapped reaction‐centre graph.
93
+ :vartype rc: SynGraph
94
+ :ivar left: Wrapped left fragment.
95
+ :vartype left: SynGraph
96
+ :ivar right: Wrapped right fragment.
97
+ :vartype right: SynGraph
98
+ :ivar canonical_smiles: Pair of left/right fragment SHA‐256 signatures (or None if canon=False).
99
+ :vartype canonical_smiles: Optional[Tuple[str,str]]
100
+ """
101
+
102
+ # Alternate constructors
103
+ @classmethod
104
+ def from_smart(
105
+ cls,
106
+ smart: str,
107
+ name: str = "rule",
108
+ canonicaliser: Optional[GraphCanonicaliser] = None,
109
+ *,
110
+ canon: bool = True,
111
+ implicit_h: bool = True,
112
+ format: ITSFormat = "typesGH",
113
+ stereo_outcomes: Optional[
114
+ Mapping[str, StereoOutcome | str | Mapping[str, Any]]
115
+ ] = None,
116
+ stereo_couplings: Optional[
117
+ Mapping[str, StereoCoupling | str | Mapping[str, Any]]
118
+ ] = None,
119
+ ) -> "SynRule":
120
+ """Instantiate from a SMARTS string."""
121
+ return cls(
122
+ rsmi_to_its(smart, format=format),
123
+ name=name,
124
+ canonicaliser=canonicaliser,
125
+ canon=canon,
126
+ implicit_h=implicit_h,
127
+ format=format,
128
+ stereo_outcomes=stereo_outcomes,
129
+ stereo_couplings=stereo_couplings,
130
+ )
131
+
132
+ @classmethod
133
+ def from_gml(
134
+ cls,
135
+ gml: str,
136
+ name: str = "rule",
137
+ canonicaliser: Optional[GraphCanonicaliser] = None,
138
+ *,
139
+ canon: bool = True,
140
+ implicit_h: bool = True,
141
+ stereo_couplings: Optional[
142
+ Mapping[str, StereoCoupling | str | Mapping[str, Any]]
143
+ ] = None,
144
+ ) -> "SynRule":
145
+ """Instantiate from a GML string."""
146
+ return cls(
147
+ gml_to_its(gml),
148
+ name=name,
149
+ canonicaliser=canonicaliser,
150
+ canon=canon,
151
+ implicit_h=implicit_h,
152
+ stereo_couplings=stereo_couplings,
153
+ )
154
+
155
+ # Initializer
156
+ def __init__(
157
+ self,
158
+ rc: nx.Graph,
159
+ name: str = "rule",
160
+ canonicaliser: Optional[GraphCanonicaliser] = None,
161
+ *,
162
+ canon: bool = True,
163
+ implicit_h: bool = True,
164
+ format: Optional[ITSFormat] = None,
165
+ stereo_outcomes: Optional[
166
+ Mapping[str, StereoOutcome | str | Mapping[str, Any]]
167
+ ] = None,
168
+ stereo_couplings: Optional[
169
+ Mapping[str, StereoCoupling | str | Mapping[str, Any]]
170
+ ] = None,
171
+ ) -> None:
172
+ self._name = name
173
+ self._canon_enabled = canon
174
+ self._implicit_h = implicit_h
175
+ self._canonicaliser = canonicaliser or GraphCanonicaliser()
176
+
177
+ # Fragment decomposition
178
+ rc_graph = rc.copy()
179
+ self._format = format or detect_its_format(rc_graph)
180
+ if self._implicit_h:
181
+ rc_graph = normalize_h_pair_graph(rc_graph)
182
+
183
+ left_graph, right_graph = self._decompose(rc_graph, self._format)
184
+
185
+ # Optional H-stripping
186
+ if self._implicit_h and self._format == "typesGH":
187
+ self._strip_explicit_h(rc_graph, left_graph, right_graph)
188
+ # Update typesGH tuples with new hcount.
189
+ for node, att in rc_graph.nodes(data=True):
190
+ t0, t1 = att["typesGH"]
191
+ new_t0 = (
192
+ t0[0],
193
+ t0[1],
194
+ left_graph.nodes[node]["hcount"] + t0[2],
195
+ t0[3],
196
+ t0[4],
197
+ )
198
+ new_t1 = (
199
+ t1[0],
200
+ t1[1],
201
+ right_graph.nodes[node]["hcount"] + t1[2],
202
+ t1[3],
203
+ t1[4],
204
+ )
205
+ att["typesGH"] = (new_t0, new_t1)
206
+ left_graph, right_graph = self._decompose(rc_graph, self._format)
207
+ elif self._implicit_h and self._format == "tuple":
208
+ self._strip_explicit_h_tuple(rc_graph, left_graph, right_graph)
209
+ left_graph, right_graph = self._decompose(rc_graph, self._format)
210
+ if self._format == "tuple":
211
+ self._normalize_relative_node_resources(rc_graph)
212
+ left_graph, right_graph = self._decompose(rc_graph, self._format)
213
+ self._project_wildcard_contracts(rc_graph, left_graph, right_graph)
214
+ stereo_sides = rc_graph.graph.get("stereo_descriptors", {})
215
+ self.stereo_guards = dict(stereo_sides.get("reactant", {}))
216
+ self.stereo_effects = dict(rc_graph.graph.get("stereo_changes", {}))
217
+ raw_outcomes = (
218
+ stereo_outcomes
219
+ if stereo_outcomes is not None
220
+ else rc_graph.graph.get("stereo_outcomes", {})
221
+ )
222
+ self.stereo_outcomes = {
223
+ key: StereoOutcome.from_value(value) for key, value in raw_outcomes.items()
224
+ }
225
+ raw_couplings = (
226
+ stereo_couplings
227
+ if stereo_couplings is not None
228
+ else rc_graph.graph.get("stereo_couplings", {})
229
+ )
230
+ self.stereo_couplings = self._normalize_stereo_couplings(
231
+ raw_couplings,
232
+ left_graph,
233
+ right_graph,
234
+ )
235
+ self.stereo_query_policies = dict(
236
+ rc_graph.graph.get("stereo_query_policies", {})
237
+ )
238
+ invalid_query_policies = {
239
+ value
240
+ for value in self.stereo_query_policies.values()
241
+ if value not in {"exact", "wildcard", "either"}
242
+ }
243
+ if invalid_query_policies:
244
+ raise ValueError(
245
+ "Stereo query policies must be 'exact', 'wildcard', or "
246
+ f"'either'; received {sorted(invalid_query_policies)!r}."
247
+ )
248
+ self._reverse_stereo_outcomes = {
249
+ key: StereoOutcome.from_value(value)
250
+ for key, value in rc_graph.graph.get("stereo_reverse_outcomes", {}).items()
251
+ }
252
+ self._reverse_stereo_query_policies = dict(
253
+ rc_graph.graph.get("stereo_reverse_query_policies", {})
254
+ )
255
+ self._validate_stereo_outcomes()
256
+ self._validate_stereo_couplings(rc_graph)
257
+ rc_graph.graph["stereo_outcomes"] = {
258
+ key: outcome.to_dict() for key, outcome in self.stereo_outcomes.items()
259
+ }
260
+ rc_graph.graph["stereo_query_policies"] = dict(self.stereo_query_policies)
261
+ rc_graph.graph["stereo_couplings"] = {
262
+ key: coupling.relation for key, coupling in self.stereo_couplings.items()
263
+ }
264
+ if self._reverse_stereo_outcomes:
265
+ rc_graph.graph["stereo_reverse_outcomes"] = {
266
+ key: outcome.to_dict()
267
+ for key, outcome in self._reverse_stereo_outcomes.items()
268
+ }
269
+ rc_graph.graph["stereo_reverse_query_policies"] = dict(
270
+ self._reverse_stereo_query_policies
271
+ )
272
+
273
+ # Wrap graphs.
274
+ self.rc = SynGraph(rc_graph, self._canonicaliser, canon=canon)
275
+ self.left = SynGraph(left_graph, self._canonicaliser, canon=canon)
276
+ self.right = SynGraph(right_graph, self._canonicaliser, canon=canon)
277
+
278
+ self.stereo_mode = "ignore"
279
+
280
+ self.canonical_smiles: Optional[Tuple[str, str]] = (
281
+ (self.left.signature, self.right.signature) if canon else None
282
+ )
283
+
284
+ # Private utilities
285
+ def _validate_stereo_outcomes(self) -> None:
286
+ """Require explicit branching declarations to match stored effects."""
287
+ for key, outcome in self.stereo_outcomes.items():
288
+ if key not in self.stereo_effects:
289
+ raise ValueError(f"Stereo outcome target {key!r} has no rule effect.")
290
+ change = self.stereo_effects[key]
291
+ if outcome.kind == "SINGLE":
292
+ continue
293
+ if (
294
+ change.change != "FORMED"
295
+ or change.after is None
296
+ or change.after.parity not in (-1, 1)
297
+ ):
298
+ raise ValueError(
299
+ f"{outcome.kind} outcome is only valid for a specified, "
300
+ "newly formed chiral descriptor."
301
+ )
302
+
303
+ @staticmethod
304
+ def _mapped_neighbors(graph: nx.Graph, atom_map: int) -> set[int]:
305
+ """Return mapped neighbors for one rule atom on one endpoint."""
306
+ by_map = {
307
+ attrs.get("atom_map", node): node
308
+ for node, attrs in graph.nodes(data=True)
309
+ if isinstance(attrs.get("atom_map", node), int)
310
+ }
311
+ node = by_map.get(atom_map)
312
+ if node is None:
313
+ return set()
314
+ result = set()
315
+ for neighbor in graph.neighbors(node):
316
+ reference = graph.nodes[neighbor].get("atom_map", neighbor)
317
+ if isinstance(reference, int) and reference > 0:
318
+ result.add(reference)
319
+ return result
320
+
321
+ @classmethod
322
+ def _infer_vicinal_coupling(
323
+ cls,
324
+ target: str,
325
+ relation: str,
326
+ left: nx.Graph,
327
+ right: nx.Graph,
328
+ ) -> StereoCoupling:
329
+ """Infer operation direction and ligand maps from a structural edit."""
330
+ try:
331
+ prefix, values = target.split(":", 1)
332
+ centers = tuple(int(value) for value in values.split("-"))
333
+ except (TypeError, ValueError) as error:
334
+ raise ValueError(
335
+ f"Compact stereo coupling target {target!r} must be 'bond:i-j'."
336
+ ) from error
337
+ if prefix != "bond" or len(centers) != 2:
338
+ raise ValueError(
339
+ f"Compact stereo coupling target {target!r} must be 'bond:i-j'."
340
+ )
341
+
342
+ added_by_center = []
343
+ removed_by_center = []
344
+ for center in centers:
345
+ left_neighbors = cls._mapped_neighbors(left, center)
346
+ right_neighbors = cls._mapped_neighbors(right, center)
347
+ added = right_neighbors - left_neighbors
348
+ removed = left_neighbors - right_neighbors
349
+ added -= set(centers)
350
+ removed -= set(centers)
351
+ added_by_center.append(added)
352
+ removed_by_center.append(removed)
353
+
354
+ if all(len(values) == 1 for values in added_by_center) and all(
355
+ not values for values in removed_by_center
356
+ ):
357
+ kind = "VICINAL_ADDITION"
358
+ ligand_sets = added_by_center
359
+ elif all(len(values) == 1 for values in removed_by_center) and all(
360
+ not values for values in added_by_center
361
+ ):
362
+ kind = "VICINAL_ELIMINATION"
363
+ ligand_sets = removed_by_center
364
+ else:
365
+ raise ValueError(
366
+ f"Compact stereo coupling {target!r} requires exactly one "
367
+ "mapped ligand bond to be formed or broken at each center."
368
+ )
369
+ return StereoCoupling(
370
+ kind,
371
+ relation,
372
+ centers, # type: ignore[arg-type]
373
+ tuple(next(iter(values)) for values in ligand_sets), # type: ignore[arg-type]
374
+ )
375
+
376
+ @classmethod
377
+ def _normalize_stereo_couplings(
378
+ cls,
379
+ values: Mapping[str, StereoCoupling | str | Mapping[str, Any]],
380
+ left: nx.Graph,
381
+ right: nx.Graph,
382
+ ) -> dict[str, StereoCoupling]:
383
+ """Expand compact relation-only declarations into mapped values."""
384
+ result = {}
385
+ for target, value in values.items():
386
+ coupling = (
387
+ cls._infer_vicinal_coupling(target, value, left, right)
388
+ if isinstance(value, str)
389
+ else StereoCoupling.from_value(value)
390
+ )
391
+ result[target] = coupling
392
+ return result
393
+
394
+ def _validate_stereo_couplings(self, rc_graph: nx.Graph) -> None:
395
+ """Require chemistry-level couplings without encoded endpoints."""
396
+ atom_maps = set()
397
+ for node, attrs in rc_graph.nodes(data=True):
398
+ atom_map = attrs.get("atom_map", node)
399
+ if isinstance(atom_map, tuple):
400
+ atom_maps.update(
401
+ value for value in atom_map if isinstance(value, int) and value > 0
402
+ )
403
+ elif isinstance(atom_map, int) and atom_map > 0:
404
+ atom_maps.add(atom_map)
405
+
406
+ for key, coupling in self.stereo_couplings.items():
407
+ if key != coupling.target:
408
+ raise ValueError(
409
+ f"Stereo coupling key {key!r} must equal its center target "
410
+ f"{coupling.target!r}."
411
+ )
412
+ missing = set(coupling.dependencies) - atom_maps
413
+ if missing:
414
+ raise ValueError(
415
+ f"Stereo coupling {key!r} references absent atom maps "
416
+ f"{sorted(missing)!r}."
417
+ )
418
+ coupled_targets = {
419
+ key,
420
+ *(f"atom:{center}" for center in coupling.centers),
421
+ }
422
+ redundant = coupled_targets & (
423
+ set(self.stereo_guards) | set(self.stereo_effects)
424
+ )
425
+ if redundant:
426
+ raise ValueError(
427
+ f"Stereo coupling {key!r} derives endpoint stereo; remove "
428
+ "stereo marks from the addition SMARTS instead of storing "
429
+ f"coupled endpoint descriptors at {sorted(redundant)!r}."
430
+ )
431
+
432
+ @staticmethod
433
+ def _decompose(rc: nx.Graph, format: ITSFormat) -> tuple[nx.Graph, nx.Graph]:
434
+ """Return left/right fragments for either supported ITS representation."""
435
+ if format == "tuple":
436
+ reverter = ITSReverter(rc)
437
+ return reverter.to_reactant_graph(), reverter.to_product_graph()
438
+ return its_decompose(rc)
439
+
440
+ @staticmethod
441
+ def _normalize_relative_node_resources(rc: nx.Graph) -> None:
442
+ """Store tuple resource edits as reactant demand and product supply."""
443
+ normalized_resources: list[str] = []
444
+ for resource in _RELATIVE_NODE_RESOURCES:
445
+ normalized = False
446
+ for _, attrs in rc.nodes(data=True):
447
+ value = attrs.get(resource)
448
+ if not (
449
+ isinstance(value, tuple)
450
+ and len(value) == 2
451
+ and all(
452
+ isinstance(endpoint, Real) and not isinstance(endpoint, bool)
453
+ for endpoint in value
454
+ )
455
+ ):
456
+ continue
457
+ reactant, product = value
458
+ consumed = max(reactant - product, 0)
459
+ produced = max(product - reactant, 0)
460
+ attrs[resource] = (consumed, produced)
461
+ normalized = True
462
+ if normalized:
463
+ normalized_resources.append(resource)
464
+ if normalized_resources:
465
+ rc.graph["relative_node_resources"] = tuple(normalized_resources)
466
+
467
+ @staticmethod
468
+ def _project_wildcard_contracts(
469
+ rc: nx.Graph,
470
+ left: nx.Graph,
471
+ right: nx.Graph,
472
+ ) -> None:
473
+ """Preserve additive typed-wildcard metadata on rule endpoints."""
474
+ for node, attrs in rc.nodes(data=True):
475
+ contract = {
476
+ key: attrs[key] for key in _WILDCARD_CONTRACT_KEYS if key in attrs
477
+ }
478
+ if not contract:
479
+ continue
480
+ for endpoint in (left, right):
481
+ if node in endpoint:
482
+ endpoint.nodes[node].update(contract)
483
+
484
+ @staticmethod
485
+ def _reverse_wildcard_contracts(
486
+ original: nx.Graph,
487
+ reversed_graph: nx.Graph,
488
+ reactant_descriptors: Mapping[str, Any],
489
+ ) -> None:
490
+ """Transport typed wildcard contracts to the reversed source frame."""
491
+ atom_shapes = {
492
+ "tetrahedral",
493
+ "square_planar",
494
+ "trigonal_bipyramidal",
495
+ "octahedral",
496
+ }
497
+
498
+ def binding(descriptor: Any, reference: Any) -> tuple[Any, int] | None:
499
+ frame = descriptor.configuration.frame
500
+ positions = [
501
+ index for index, candidate in enumerate(frame) if candidate == reference
502
+ ]
503
+ if len(positions) != 1:
504
+ return None
505
+ position = positions[0]
506
+ if descriptor.descriptor_class in atom_shapes:
507
+ return None if position == 0 else (frame[0], position - 1)
508
+ if position in {0, 1}:
509
+ return frame[2], position
510
+ if position in {4, 5}:
511
+ return frame[3], position - 4
512
+ return None
513
+
514
+ for node, attrs in original.nodes(data=True):
515
+ if "wildcard_role" not in attrs or node not in reversed_graph:
516
+ continue
517
+ contract = {
518
+ key: attrs[key] for key in _WILDCARD_CONTRACT_KEYS if key in attrs
519
+ }
520
+ side = contract.get("side")
521
+ if side == "reactant":
522
+ contract["side"] = "product"
523
+ elif side == "product":
524
+ contract["side"] = "reactant"
525
+ atom_map = attrs.get("atom_map", node)
526
+ if isinstance(atom_map, (tuple, list)) and len(atom_map) == 2:
527
+ atom_map = atom_map[1]
528
+ bindings = [
529
+ result
530
+ for descriptor in reactant_descriptors.values()
531
+ if (result := binding(descriptor, atom_map)) is not None
532
+ ]
533
+ if contract.get("wildcard_role") == "stereo_ligand_port" and (
534
+ len(bindings) == 1
535
+ ):
536
+ contract["owner"], contract["stereo_slot"] = bindings[0]
537
+ reversed_graph.nodes[node].update(contract)
538
+
539
+ @staticmethod
540
+ def _strip_explicit_h(
541
+ rc: nx.Graph,
542
+ left: nx.Graph,
543
+ right: nx.Graph,
544
+ ) -> None:
545
+ """Remove explicit hydrogens from rc, left, right—but only when *both*
546
+ left & right agree the H should be implicit.
547
+
548
+ Otherwise an H remains explicit in all three graphs.
549
+ """
550
+
551
+ def _removable_on(graph: nx.Graph, h: str) -> bool:
552
+ # H+ (no neighbors) ⇒ not removable
553
+ nbrs = list(graph.neighbors(h))
554
+ if not nbrs:
555
+ return False
556
+ # H–H only ⇒ not removable
557
+ if all(graph.nodes[n].get("element") == "H" for n in nbrs):
558
+ return False
559
+ # otherwise bonded to ≥1 heavy ⇒ removable
560
+ return True
561
+
562
+ def _fully_removable(h: str) -> bool:
563
+ # only remove if BOTH left and right say removable
564
+ return _removable_on(left, h) and _removable_on(right, h)
565
+
566
+ # 1) initialize hcount & h_pairs
567
+ for g in (rc, left, right):
568
+ for n, data in g.nodes(data=True):
569
+ data["hcount"] = 0
570
+ if data.get("element") != "H":
571
+ data.setdefault("h_pairs", [])
572
+
573
+ # 2) shared H: only those removable on both sides
574
+ shared = sorted(
575
+ n
576
+ for n, d in left.nodes(data=True)
577
+ if d.get("element") == "H" and right.has_node(n) and _fully_removable(n)
578
+ )
579
+
580
+ pair_id = 1
581
+ for h in shared:
582
+ for g in (left, right, rc):
583
+ if not g.has_node(h):
584
+ continue
585
+ for nbr in list(g.neighbors(h)):
586
+ if g.nodes[nbr].get("element") != "H":
587
+ g.nodes[nbr]["hcount"] += 1
588
+ # only shared H get pair-IDs
589
+ g.nodes[nbr].setdefault("h_pairs", []).append(pair_id)
590
+ g.remove_node(h)
591
+ pair_id += 1
592
+
593
+ # 3) remaining explicit H in any graph: strip only if fully_removable
594
+ for g in (rc, left, right):
595
+ for h in [n for n, d in g.nodes(data=True) if d.get("element") == "H"]:
596
+ if not _fully_removable(h):
597
+ # at least one side wants to keep it explicit → skip
598
+ continue
599
+ # else both agree → convert to implicit
600
+ for nbr in list(g.neighbors(h)):
601
+ if g.nodes[nbr].get("element") != "H":
602
+ g.nodes[nbr]["hcount"] += 1
603
+ g.remove_node(h)
604
+
605
+ @staticmethod
606
+ def _strip_explicit_h_tuple(
607
+ rc: nx.Graph,
608
+ left: nx.Graph,
609
+ right: nx.Graph,
610
+ ) -> None:
611
+ """Tuple-style equivalent of legacy explicit-H stripping."""
612
+
613
+ def _removable_on(graph: nx.Graph, h: int) -> bool:
614
+ if not graph.has_node(h):
615
+ return False
616
+ nbrs = list(graph.neighbors(h))
617
+ if not nbrs:
618
+ return False
619
+ return not all(graph.nodes[n].get("element") == "H" for n in nbrs)
620
+
621
+ def _fully_removable(h: int) -> bool:
622
+ return _removable_on(left, h) and _removable_on(right, h)
623
+
624
+ for graph in (left, right):
625
+ for _, data in graph.nodes(data=True):
626
+ if data.get("element") != "H":
627
+ data.setdefault("h_pairs", [])
628
+ data.setdefault("h_pairs_left", [])
629
+ data.setdefault("h_pairs_right", [])
630
+ data.setdefault("h_pair_atom_maps", {})
631
+
632
+ for _, data in rc.nodes(data=True):
633
+ element = data.get("element")
634
+ is_h = (
635
+ isinstance(element, tuple)
636
+ and len(element) == 2
637
+ and all(value == "H" for value in element)
638
+ )
639
+ if not is_h:
640
+ data.setdefault("h_pairs", [])
641
+ data.setdefault("h_pairs_left", [])
642
+ data.setdefault("h_pairs_right", [])
643
+ data.setdefault("h_pair_atom_maps", {})
644
+
645
+ removable = sorted(
646
+ node
647
+ for node, attrs in left.nodes(data=True)
648
+ if attrs.get("element") == "H"
649
+ and right.has_node(node)
650
+ and _fully_removable(node)
651
+ )
652
+
653
+ for pair_id, h in enumerate(removable, start=1):
654
+ atom_map = left.nodes[h].get("atom_map", h)
655
+ for side, graph in (("left", left), ("right", right)):
656
+ for nbr in list(graph.neighbors(h)):
657
+ if graph.nodes[nbr].get("element") != "H":
658
+ graph.nodes[nbr]["hcount"] += 1
659
+ graph.nodes[nbr].setdefault("h_pairs", []).append(pair_id)
660
+ graph.nodes[nbr].setdefault(f"h_pairs_{side}", []).append(
661
+ pair_id
662
+ )
663
+ graph.nodes[nbr].setdefault("h_pair_atom_maps", {})[
664
+ pair_id
665
+ ] = atom_map
666
+ graph.remove_node(h)
667
+ if rc.has_node(h):
668
+ rc.remove_node(h)
669
+
670
+ for node, attrs in rc.nodes(data=True):
671
+ if node not in left or node not in right:
672
+ continue
673
+ if attrs.get("element") == ("H", "H"):
674
+ continue
675
+ left_h = left.nodes[node].get("hcount", 0)
676
+ right_h = right.nodes[node].get("hcount", 0)
677
+ attrs["hcount"] = (left_h, right_h)
678
+ attrs["h_pairs"] = sorted(
679
+ set(left.nodes[node].get("h_pairs", []))
680
+ | set(right.nodes[node].get("h_pairs", []))
681
+ )
682
+ attrs["h_pairs_left"] = sorted(left.nodes[node].get("h_pairs_left", []))
683
+ attrs["h_pairs_right"] = sorted(right.nodes[node].get("h_pairs_right", []))
684
+ attrs["h_pair_atom_maps"] = {
685
+ **left.nodes[node].get("h_pair_atom_maps", {}),
686
+ **right.nodes[node].get("h_pair_atom_maps", {}),
687
+ }
688
+ typesgh = attrs.get("typesGH")
689
+ if typesgh and len(typesgh) == 2:
690
+ react_attr, prod_attr = typesgh
691
+ attrs["typesGH"] = (
692
+ tuple(list(react_attr[:2]) + [left_h] + list(react_attr[3:])),
693
+ tuple(list(prod_attr[:2]) + [right_h] + list(prod_attr[3:])),
694
+ )
695
+
696
+ # Dunder methods
697
+ def __eq__(self, other: object) -> bool:
698
+ return (
699
+ isinstance(other, SynRule)
700
+ and self.canonical_smiles == other.canonical_smiles
701
+ and self._stereo_signature() == other._stereo_signature()
702
+ and stereo_rule_isomorphic(self, other)
703
+ )
704
+
705
+ def __hash__(self) -> int:
706
+ return hash((self.canonical_smiles, self._stereo_signature()))
707
+
708
+ def _stereo_signature(self) -> tuple:
709
+ """Return a coarse map-invariant prefilter for exact rule equality."""
710
+ guards = tuple(
711
+ sorted(
712
+ descriptor.descriptor_class
713
+ for descriptor in self.stereo_guards.values()
714
+ )
715
+ )
716
+ effects = tuple(
717
+ sorted(change.change for change in self.stereo_effects.values())
718
+ )
719
+ outcomes = tuple(
720
+ sorted(outcome.signature() for outcome in self.stereo_outcomes.values())
721
+ )
722
+ couplings = tuple(
723
+ sorted(
724
+ (coupling.kind, coupling.relation)
725
+ for coupling in self.stereo_couplings.values()
726
+ )
727
+ )
728
+ query_policies = tuple(sorted(self.stereo_query_policies.values()))
729
+ reverse_outcomes = tuple(
730
+ sorted(
731
+ outcome.signature()
732
+ for outcome in self._reverse_stereo_outcomes.values()
733
+ )
734
+ )
735
+ reverse_queries = tuple(sorted(self._reverse_stereo_query_policies.values()))
736
+ wildcard_contracts = []
737
+ for _, attrs in self.rc.raw.nodes(data=True):
738
+ if "wildcard_role" not in attrs:
739
+ continue
740
+ state = adapt_legacy_node_state(attrs)
741
+ if state.kind is not NodeStateKind.WILDCARD or state.constraint is None:
742
+ continue
743
+ values = list(state.constraint.normalized())
744
+ values[6] = state.constraint.owner is not None
745
+ wildcard_contracts.append(tuple(values))
746
+ return (
747
+ guards,
748
+ effects,
749
+ outcomes,
750
+ couplings,
751
+ query_policies,
752
+ reverse_outcomes,
753
+ reverse_queries,
754
+ tuple(sorted(wildcard_contracts, key=repr)),
755
+ )
756
+
757
+ def __str__(self) -> str:
758
+ if self._canon_enabled and self.canonical_smiles:
759
+ ls, rs = self.canonical_smiles
760
+ return f"<SynRule {self._name!r} left={ls[:8]}… right={rs[:8]}…>"
761
+ return f"<SynRule {self._name!r} (raw only)>"
762
+
763
+ @staticmethod
764
+ def _summary_descriptor(descriptor: Any) -> dict[str, Any]:
765
+ """Return the chemically relevant part of one stored descriptor."""
766
+ return {
767
+ "atoms": list(descriptor.atoms),
768
+ "parity": descriptor.parity,
769
+ }
770
+
771
+ @staticmethod
772
+ def _reference_delta(before: Any, after: Any) -> dict[str, list[Any]]:
773
+ """Report reference replacements while respecting multiplicity/order."""
774
+ remaining_after = list(after.atoms)
775
+ removed = []
776
+ for reference in before.atoms:
777
+ if reference in remaining_after:
778
+ remaining_after.remove(reference)
779
+ else:
780
+ removed.append(reference)
781
+ return {"removed": removed, "added": remaining_after}
782
+
783
+ def stereo_summary(self) -> dict[str, dict[str, Any]]:
784
+ """Return a compact, per-target view of active stereo rule semantics.
785
+
786
+ The lossless graph metadata stores a reactant descriptor both as a
787
+ matching guard and as the ``before`` state of its change. This view
788
+ presents that descriptor once, alongside the product state, change,
789
+ reference replacement, matching policy, and optional branch outcome.
790
+ Parser provenance is intentionally omitted from this inspection view.
791
+ """
792
+ targets = sorted(
793
+ set(self.stereo_guards)
794
+ | set(self.stereo_effects)
795
+ | set(self.stereo_outcomes)
796
+ | set(self.stereo_couplings)
797
+ | set(self.stereo_query_policies)
798
+ )
799
+ summary: dict[str, dict[str, Any]] = {}
800
+ for target in targets:
801
+ guard = self.stereo_guards.get(target)
802
+ effect = self.stereo_effects.get(target)
803
+ before = guard or (effect.before if effect else None)
804
+ after = effect.after if effect else None
805
+ transition = effect.transition if effect else None
806
+ descriptor = before or after or transition
807
+
808
+ item: dict[str, Any] = {}
809
+ if descriptor is not None:
810
+ item["descriptor"] = descriptor.descriptor_class
811
+ if target in self.stereo_query_policies:
812
+ item["query"] = self.stereo_query_policies[target]
813
+ if effect is not None:
814
+ item["change"] = effect.change
815
+ item["relation"] = effect.relation_evidence()
816
+ if before is not None:
817
+ item["reactant"] = self._summary_descriptor(before)
818
+ if after is not None:
819
+ item["product"] = self._summary_descriptor(after)
820
+ if before is not None and after is not None:
821
+ delta = self._reference_delta(before, after)
822
+ if delta["removed"] or delta["added"]:
823
+ item["reference_delta"] = delta
824
+ if transition is not None:
825
+ item["transition"] = self._summary_descriptor(transition)
826
+ outcome = self.stereo_outcomes.get(target)
827
+ if outcome is not None:
828
+ item["outcome"] = outcome.to_dict()
829
+ coupling = self.stereo_couplings.get(target)
830
+ if coupling is not None:
831
+ item["coupling"] = coupling.to_dict()
832
+ summary[target] = item
833
+ return summary
834
+
835
+ @staticmethod
836
+ def _repr_counts(values: tuple[str, ...]) -> str:
837
+ """Return a compact, deterministic ``{value:count}`` summary."""
838
+ counts: dict[str, int] = {}
839
+ for value in values:
840
+ counts[value] = counts.get(value, 0) + 1
841
+ return (
842
+ "{" + ",".join(f"{value}:{counts[value]}" for value in sorted(counts)) + "}"
843
+ )
844
+
845
+ def _stereo_repr(self) -> str:
846
+ """Summarize active rule-level stereochemical semantics for ``repr``."""
847
+ if not (
848
+ self.stereo_guards
849
+ or self.stereo_effects
850
+ or self.stereo_outcomes
851
+ or self.stereo_couplings
852
+ or self.stereo_query_policies
853
+ ):
854
+ return "none"
855
+
856
+ guard_kinds = tuple(key.partition(":")[0] for key in self.stereo_guards)
857
+ effect_kinds = tuple(change.change for change in self.stereo_effects.values())
858
+ parts = [
859
+ f"guards={self._repr_counts(guard_kinds) if guard_kinds else '0'}",
860
+ f"effects={self._repr_counts(effect_kinds) if effect_kinds else '0'}",
861
+ ]
862
+ if self.stereo_outcomes:
863
+ parts.append(
864
+ "outcomes="
865
+ + self._repr_counts(
866
+ tuple(outcome.kind for outcome in self.stereo_outcomes.values())
867
+ )
868
+ )
869
+ if self.stereo_couplings:
870
+ parts.append(
871
+ "couplings="
872
+ + self._repr_counts(
873
+ tuple(
874
+ coupling.relation for coupling in self.stereo_couplings.values()
875
+ )
876
+ )
877
+ )
878
+ if self.stereo_query_policies:
879
+ parts.append(
880
+ "queries="
881
+ + self._repr_counts(tuple(self.stereo_query_policies.values()))
882
+ )
883
+ return "(" + ",".join(parts) + ")"
884
+
885
+ def __repr__(self) -> str:
886
+ try:
887
+ v_rc, e_rc = self.rc.raw.number_of_nodes(), self.rc.raw.number_of_edges()
888
+ v_l, e_l = self.left.raw.number_of_nodes(), self.left.raw.number_of_edges()
889
+ v_r, e_r = (
890
+ self.right.raw.number_of_nodes(),
891
+ self.right.raw.number_of_edges(),
892
+ )
893
+ except Exception:
894
+ v_rc = e_rc = v_l = e_l = v_r = e_r = 0
895
+ return (
896
+ f"SynRule(name={self._name!r}, "
897
+ f"rc=(|V|={v_rc},|E|={e_rc}), "
898
+ f"left=(|V|={v_l},|E|={e_l}), "
899
+ f"right=(|V|={v_r},|E|={e_r}), "
900
+ f"stereo={self._stereo_repr()})"
901
+ )
902
+
903
+ # Public API
904
+ def to_rule_span(
905
+ self,
906
+ *,
907
+ electron_complete: bool | None = None,
908
+ boundary: str = "abstract",
909
+ environment: Any = None,
910
+ ) -> Any:
911
+ """Return the native non-stereo LLG/DPO view of this rule."""
912
+ from synkit.Rule.Compose import synrule_to_span
913
+
914
+ return synrule_to_span(
915
+ self,
916
+ electron_complete=electron_complete,
917
+ boundary=boundary,
918
+ environment=environment,
919
+ )
920
+
921
+ def compose(
922
+ self,
923
+ other: "SynRule",
924
+ overlap: Mapping[Any, Any],
925
+ *,
926
+ overlap_edges: set[frozenset[Any]] | None = None,
927
+ electron_complete: bool | None = None,
928
+ ) -> Any:
929
+ """Natively compose with ``other`` along one explicit overlap map."""
930
+ from synkit.Rule.Compose import compose_synrules
931
+
932
+ options = {
933
+ "overlap_edges": overlap_edges,
934
+ "electron_complete": electron_complete,
935
+ }
936
+ return compose_synrules(self, other, overlap, **options)
937
+
938
+ def composition_candidates(
939
+ self,
940
+ other: "SynRule",
941
+ *,
942
+ limits: Any = None,
943
+ electron_complete: bool | None = None,
944
+ ) -> Any:
945
+ """Return every bounded native composition class and witness."""
946
+ from synkit.Rule.Compose import search_synrule_compositions
947
+
948
+ return search_synrule_compositions(
949
+ self,
950
+ other,
951
+ limits=limits,
952
+ electron_complete=electron_complete,
953
+ )
954
+
955
+ def non_invertible_stereo_targets(self) -> tuple[str, ...]:
956
+ """Return targets whose stereo relation has no unique inverse."""
957
+ return tuple(
958
+ sorted(
959
+ target
960
+ for target, change in self.stereo_effects.items()
961
+ if change.non_invertible
962
+ )
963
+ )
964
+
965
+ @property
966
+ def is_stereo_reversible(self) -> bool:
967
+ """Whether reverse construction can preserve stereo semantics."""
968
+ return not self.non_invertible_stereo_targets()
969
+
970
+ def reversed(
971
+ self,
972
+ *,
973
+ balance_its: bool = False,
974
+ semantics: str = "orbit",
975
+ diagnostics: list[Any] | None = None,
976
+ ) -> "SynRule":
977
+ """Return a rule with reactant/product stereo semantics reversed.
978
+
979
+ A forward two-enantiomer product outcome becomes an ``either``
980
+ reactant guard in reverse, because the reverse rule consumes either
981
+ enantiomer but does not create two achiral products. Reversing again
982
+ restores the original product outcome and query policies.
983
+ """
984
+ non_invertible = self.non_invertible_stereo_targets()
985
+ if non_invertible:
986
+ raise NonInvertibleStereoEffectError(non_invertible)
987
+
988
+ # These are the authoritative endpoint graphs produced from the
989
+ # normalized reaction centre during construction. Reusing them avoids
990
+ # a redundant full ITS reversion on every backward replay.
991
+ left_graph, right_graph = self.left.raw, self.right.raw
992
+ if self._format == "tuple":
993
+ reversed_graph = ITSConstruction.construct(
994
+ right_graph,
995
+ left_graph,
996
+ balance_its=balance_its,
997
+ )
998
+ for node in reversed_graph:
999
+ consistency = self.rc.raw.nodes[node].get("charge_model_consistent")
1000
+ if isinstance(consistency, tuple) and len(consistency) == 2:
1001
+ reversed_graph.nodes[node]["charge_model_consistent"] = (
1002
+ consistency[1],
1003
+ consistency[0],
1004
+ )
1005
+ else:
1006
+ reversed_graph = ITSConstruction.ITSGraph(
1007
+ right_graph,
1008
+ left_graph,
1009
+ balance_its=balance_its,
1010
+ )
1011
+
1012
+ reversed_graph.graph["stereo_changes"] = {
1013
+ key: change.reverse(
1014
+ semantics=semantics,
1015
+ diagnostics=diagnostics,
1016
+ )
1017
+ for key, change in self.stereo_effects.items()
1018
+ }
1019
+ reversed_graph.graph["stereo_couplings"] = {
1020
+ key: coupling.relation for key, coupling in self.stereo_couplings.items()
1021
+ }
1022
+ sides = self.rc.raw.graph.get("stereo_descriptors", {})
1023
+ reversed_graph.graph["stereo_descriptors"] = {
1024
+ "reactant": dict(sides.get("product", {})),
1025
+ "product": dict(sides.get("reactant", {})),
1026
+ }
1027
+ if "transition" in sides:
1028
+ reversed_graph.graph["stereo_descriptors"]["transition"] = dict(
1029
+ sides["transition"]
1030
+ )
1031
+ self._reverse_wildcard_contracts(
1032
+ self.rc.raw,
1033
+ reversed_graph,
1034
+ reversed_graph.graph["stereo_descriptors"]["reactant"],
1035
+ )
1036
+
1037
+ if self._reverse_stereo_outcomes:
1038
+ # This rule is already the reverse view of a branching rule.
1039
+ reversed_graph.graph["stereo_outcomes"] = {
1040
+ key: outcome.to_dict()
1041
+ for key, outcome in self._reverse_stereo_outcomes.items()
1042
+ }
1043
+ reversed_graph.graph["stereo_query_policies"] = dict(
1044
+ self._reverse_stereo_query_policies
1045
+ )
1046
+ else:
1047
+ reversed_graph.graph["stereo_outcomes"] = {}
1048
+ reverse_policies = dict(self.stereo_query_policies)
1049
+ for key, outcome in self.stereo_outcomes.items():
1050
+ if outcome.kind != "SINGLE":
1051
+ reverse_policies[key] = "either"
1052
+ reversed_graph.graph["stereo_query_policies"] = reverse_policies
1053
+ if self.stereo_outcomes:
1054
+ reversed_graph.graph["stereo_reverse_outcomes"] = {
1055
+ key: outcome.to_dict()
1056
+ for key, outcome in self.stereo_outcomes.items()
1057
+ }
1058
+ reversed_graph.graph["stereo_reverse_query_policies"] = dict(
1059
+ self.stereo_query_policies
1060
+ )
1061
+
1062
+ return SynRule(
1063
+ reversed_graph,
1064
+ name=self._name,
1065
+ canonicaliser=self._canonicaliser,
1066
+ canon=self._canon_enabled,
1067
+ implicit_h=self._implicit_h,
1068
+ format=self._format,
1069
+ )
1070
+
1071
+ def help(self) -> None:
1072
+ """Pretty-print raw / canonical contents for quick inspection."""
1073
+ print(f"SynRule name={self._name!r}")
1074
+ print("→ Full (raw) rc_graph edges:")
1075
+ for u, v, d in self.rc.raw.edges(data=True):
1076
+ print(f" ({u}, {v}): {d}")
1077
+
1078
+ if not self._canon_enabled:
1079
+ print("→ Canonicalisation disabled.")
1080
+ return
1081
+
1082
+ print("\n→ Full canonical_graph edges:")
1083
+ for u, v, d in self.rc.canonical.edges(data=True): # type: ignore[attr-defined]
1084
+ print(f" ({u}, {v}): {d}")
1085
+
1086
+ print("\n→ Left fragment:")
1087
+ self.left.help()
1088
+ print("\n→ Right fragment:")
1089
+ self.right.help()
1090
+ print("\n→ Fragment signatures:", self.canonical_smiles)