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,884 @@
1
+ """Proof-bearing extraction of conservative generic stereo rules.
2
+
3
+ The extractor in this module is additive: ordinary :class:`SynRule`
4
+ construction remains exact. Generalization replaces only graph-backed,
5
+ unchanged peripheral stereo references under an explicit policy and records
6
+ enough evidence to replay the source reaction.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass, field
12
+ from enum import Enum
13
+ import hashlib
14
+ import json
15
+ from types import MappingProxyType
16
+ from typing import Any, Iterable, Mapping
17
+
18
+ import networkx as nx
19
+
20
+ from synkit.Graph.ITS.its_reverter import ITSReverter
21
+ from synkit.Graph.Morphism import WildcardConstraint, WildcardRole
22
+ from synkit.Graph.syn_graph import SynGraph
23
+ from synkit.IO.chem_converter import rsmi_to_graph, rsmi_to_its
24
+
25
+ from .syn_rule import NonInvertibleStereoEffectError, SynRule
26
+
27
+ EXTRACTION_SCHEMA = "synkit.stereo-rule-extraction/1"
28
+
29
+
30
+ class GenericStereoDomainSource(str, Enum):
31
+ """Authority used to define one extracted wildcard domain."""
32
+
33
+ EXACT = "exact"
34
+ OBSERVED = "observed"
35
+ CLASS = "class"
36
+ CORPUS = "corpus"
37
+
38
+
39
+ class GenericStereoExtractionIssueCode(str, Enum):
40
+ """Stable refusal codes for generic stereo extraction."""
41
+
42
+ INPUT_NOT_MAPPED = "GENERIC_STEREO_INPUT_NOT_MAPPED"
43
+ NO_STEREO = "GENERIC_STEREO_NO_STEREO"
44
+ REFERENCE_NOT_FOUND = "GENERIC_STEREO_REFERENCE_NOT_FOUND"
45
+ REFERENCE_NOT_ELIGIBLE = "GENERIC_STEREO_REFERENCE_NOT_ELIGIBLE"
46
+ AMBIGUOUS_BINDING = "GENERIC_STEREO_AMBIGUOUS_BINDING"
47
+ DOMAIN_REQUIRED = "GENERIC_STEREO_DOMAIN_REQUIRED"
48
+ SOURCE_REPLAY_FAILED = "GENERIC_STEREO_SOURCE_REPLAY_FAILED"
49
+ REVERSE_REPLAY_FAILED = "GENERIC_STEREO_REVERSE_REPLAY_FAILED"
50
+ INVALID_STEREO_SUPPORT = "GENERIC_STEREO_INVALID_SUPPORT"
51
+ CERTIFICATE_INVALID = "GENERIC_STEREO_CERTIFICATE_INVALID"
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class GenericStereoExtractionIssue:
56
+ """One structured extraction refusal."""
57
+
58
+ code: GenericStereoExtractionIssueCode
59
+ message: str
60
+ context: Mapping[str, Any] = field(default_factory=dict)
61
+
62
+ def to_dict(self) -> dict[str, Any]:
63
+ """Return a JSON-compatible issue record."""
64
+ return {
65
+ "code": self.code.value,
66
+ "message": self.message,
67
+ "context": dict(self.context),
68
+ }
69
+
70
+
71
+ class GenericStereoExtractionError(ValueError):
72
+ """Raised when conservative generic extraction cannot be certified."""
73
+
74
+ def __init__(self, *issues: GenericStereoExtractionIssue):
75
+ self.issues = tuple(issues)
76
+ super().__init__("; ".join(issue.message for issue in self.issues))
77
+
78
+
79
+ def _freeze_domain_mapping(
80
+ values: Mapping[int, Mapping[str, Any]],
81
+ ) -> Mapping[int, Mapping[str, Any]]:
82
+ frozen = {}
83
+ for reference, domains in values.items():
84
+ normalized = {}
85
+ for name, value in domains.items():
86
+ if name in {"elements", "charges", "radicals", "bond_orders"}:
87
+ if isinstance(value, (set, frozenset, tuple, list)):
88
+ value = frozenset(value)
89
+ else:
90
+ value = frozenset({value})
91
+ normalized[str(name)] = value
92
+ frozen[int(reference)] = MappingProxyType(normalized)
93
+ return MappingProxyType(frozen)
94
+
95
+
96
+ @dataclass(frozen=True)
97
+ class GenericStereoRulePolicy:
98
+ """Explicit policy controlling which spectator references generalize."""
99
+
100
+ domain_source: GenericStereoDomainSource | str = GenericStereoDomainSource.OBSERVED
101
+ selected_references: frozenset[int] | Iterable[int] | None = None
102
+ explicit_domains: Mapping[int, Mapping[str, Any]] = field(default_factory=dict)
103
+ domain_evidence: Mapping[int, Iterable[str]] = field(default_factory=dict)
104
+ verify_source: bool = True
105
+ verify_reverse: bool = True
106
+
107
+ def __post_init__(self) -> None:
108
+ source = GenericStereoDomainSource(self.domain_source)
109
+ selected = self.selected_references
110
+ if selected is not None and not isinstance(selected, frozenset):
111
+ selected = frozenset(int(value) for value in selected)
112
+ object.__setattr__(self, "domain_source", source)
113
+ object.__setattr__(self, "selected_references", selected)
114
+ object.__setattr__(
115
+ self,
116
+ "explicit_domains",
117
+ _freeze_domain_mapping(self.explicit_domains),
118
+ )
119
+ evidence = {}
120
+ for reference, records in self.domain_evidence.items():
121
+ values = (records,) if isinstance(records, str) else records
122
+ evidence[int(reference)] = tuple(sorted(map(str, values)))
123
+ object.__setattr__(self, "domain_evidence", MappingProxyType(evidence))
124
+ if (
125
+ source
126
+ in {
127
+ GenericStereoDomainSource.EXACT,
128
+ GenericStereoDomainSource.OBSERVED,
129
+ }
130
+ and self.explicit_domains
131
+ ):
132
+ raise ValueError(
133
+ "Explicit domains require domain_source='class' or 'corpus'."
134
+ )
135
+
136
+
137
+ @dataclass(frozen=True)
138
+ class ExtractedStereoPort:
139
+ """One generalized peripheral ligand and its authoritative binding."""
140
+
141
+ reference: int
142
+ target: str
143
+ owner: int
144
+ stereo_slot: int
145
+ constraint: WildcardConstraint
146
+ domain_source: GenericStereoDomainSource
147
+ domain_evidence: tuple[str, ...] = ()
148
+
149
+ def to_dict(self) -> dict[str, Any]:
150
+ """Return a stable JSON-compatible port record."""
151
+ normalized = self.constraint.normalized()
152
+ return {
153
+ "reference": self.reference,
154
+ "target": self.target,
155
+ "owner": self.owner,
156
+ "stereo_slot": self.stereo_slot,
157
+ "domain_source": self.domain_source.value,
158
+ "domain_evidence": list(self.domain_evidence),
159
+ "constraint": {
160
+ "role": normalized[0],
161
+ "elements": normalized[1],
162
+ "charges": normalized[2],
163
+ "radicals": normalized[3],
164
+ "bond_orders": normalized[4],
165
+ "side": normalized[5],
166
+ "owner": self.owner,
167
+ "capacity": normalized[7],
168
+ "resource_budget": normalized[8],
169
+ "stereo_slot": normalized[9],
170
+ "virtual_kind": normalized[10],
171
+ "mapped_identity": normalized[11],
172
+ "materialization": normalized[12],
173
+ },
174
+ }
175
+
176
+ @classmethod
177
+ def from_dict(
178
+ cls,
179
+ value: Mapping[str, Any],
180
+ ) -> "ExtractedStereoPort":
181
+ """Read one typed port contract from certificate JSON."""
182
+ constraint = value["constraint"]
183
+ return cls(
184
+ reference=int(value["reference"]),
185
+ target=str(value["target"]),
186
+ owner=int(value["owner"]),
187
+ stereo_slot=int(value["stereo_slot"]),
188
+ constraint=WildcardConstraint(
189
+ role=constraint["role"],
190
+ elements=constraint.get("elements"),
191
+ charges=constraint.get("charges"),
192
+ radicals=constraint.get("radicals"),
193
+ bond_orders=constraint.get("bond_orders"),
194
+ side=constraint.get("side", "any"),
195
+ owner=constraint.get("owner"),
196
+ capacity=int(constraint.get("capacity", 1)),
197
+ resource_budget=constraint.get("resource_budget"),
198
+ stereo_slot=constraint.get("stereo_slot"),
199
+ virtual_kind=constraint.get("virtual_kind"),
200
+ mapped_identity=constraint.get("mapped_identity"),
201
+ materialization=constraint.get("materialization"),
202
+ ),
203
+ domain_source=GenericStereoDomainSource(value["domain_source"]),
204
+ domain_evidence=tuple(
205
+ str(item) for item in value.get("domain_evidence", ())
206
+ ),
207
+ )
208
+
209
+
210
+ @dataclass(frozen=True)
211
+ class RuleExtractionCertificate:
212
+ """Immutable source-to-generic extraction and replay evidence."""
213
+
214
+ source_rule_digest: str
215
+ generic_rule_digest: str
216
+ ports: tuple[ExtractedStereoPort, ...]
217
+ source_mapping_count: int
218
+ source_unique_products: int
219
+ source_replay_exact: bool
220
+ reverse_status: str
221
+ reverse_mapping_count: int = 0
222
+ schema: str = EXTRACTION_SCHEMA
223
+
224
+ def to_dict(self) -> dict[str, Any]:
225
+ """Return stable certificate payload."""
226
+ payload = {
227
+ "schema": self.schema,
228
+ "source_rule_digest": self.source_rule_digest,
229
+ "generic_rule_digest": self.generic_rule_digest,
230
+ "ports": [port.to_dict() for port in self.ports],
231
+ "source_replay": {
232
+ "mapping_count": self.source_mapping_count,
233
+ "unique_products": self.source_unique_products,
234
+ "exact": self.source_replay_exact,
235
+ },
236
+ "reverse_replay": {
237
+ "status": self.reverse_status,
238
+ "mapping_count": self.reverse_mapping_count,
239
+ },
240
+ }
241
+ payload["digest"] = self.digest
242
+ return payload
243
+
244
+ @property
245
+ def digest(self) -> str:
246
+ """Return a deterministic atom-map-invariant certificate digest."""
247
+ port_contracts = []
248
+ for port in self.ports:
249
+ normalized = list(port.constraint.normalized())
250
+ normalized[6] = None
251
+ port_contracts.append(
252
+ (
253
+ port.stereo_slot,
254
+ port.domain_source.value,
255
+ port.domain_evidence,
256
+ tuple(normalized),
257
+ )
258
+ )
259
+ payload = json.dumps(
260
+ {
261
+ "schema": self.schema,
262
+ "source_rule_digest": self.source_rule_digest,
263
+ "generic_rule_digest": self.generic_rule_digest,
264
+ "ports": sorted(port_contracts, key=repr),
265
+ "source_replay": (
266
+ self.source_mapping_count,
267
+ self.source_unique_products,
268
+ self.source_replay_exact,
269
+ ),
270
+ "reverse_replay": (self.reverse_status, self.reverse_mapping_count),
271
+ },
272
+ sort_keys=True,
273
+ separators=(",", ":"),
274
+ )
275
+ return hashlib.sha256(payload.encode("utf-8")).hexdigest()
276
+
277
+ @classmethod
278
+ def from_dict(
279
+ cls,
280
+ value: Mapping[str, Any],
281
+ ) -> "RuleExtractionCertificate":
282
+ """Read and verify one extraction certificate."""
283
+ if value.get("schema") != EXTRACTION_SCHEMA:
284
+ raise GenericStereoExtractionError(
285
+ GenericStereoExtractionIssue(
286
+ GenericStereoExtractionIssueCode.CERTIFICATE_INVALID,
287
+ "Unsupported stereo-rule extraction certificate schema.",
288
+ {"schema": value.get("schema")},
289
+ )
290
+ )
291
+ source = value.get("source_replay", {})
292
+ reverse = value.get("reverse_replay", {})
293
+ certificate = cls(
294
+ source_rule_digest=str(value["source_rule_digest"]),
295
+ generic_rule_digest=str(value["generic_rule_digest"]),
296
+ ports=tuple(
297
+ ExtractedStereoPort.from_dict(item) for item in value.get("ports", ())
298
+ ),
299
+ source_mapping_count=int(source.get("mapping_count", 0)),
300
+ source_unique_products=int(source.get("unique_products", 0)),
301
+ source_replay_exact=bool(source.get("exact", False)),
302
+ reverse_status=str(reverse.get("status", "not_checked")),
303
+ reverse_mapping_count=int(reverse.get("mapping_count", 0)),
304
+ )
305
+ declared = value.get("digest")
306
+ if declared is not None and declared != certificate.digest:
307
+ raise GenericStereoExtractionError(
308
+ GenericStereoExtractionIssue(
309
+ GenericStereoExtractionIssueCode.CERTIFICATE_INVALID,
310
+ "Stereo-rule extraction certificate digest mismatch.",
311
+ {
312
+ "declared": declared,
313
+ "computed": certificate.digest,
314
+ },
315
+ )
316
+ )
317
+ references = [port.reference for port in certificate.ports]
318
+ contracts = [(port.target, port.stereo_slot) for port in certificate.ports]
319
+ if len(references) != len(set(references)) or len(contracts) != len(
320
+ set(contracts)
321
+ ):
322
+ raise GenericStereoExtractionError(
323
+ GenericStereoExtractionIssue(
324
+ GenericStereoExtractionIssueCode.CERTIFICATE_INVALID,
325
+ "Extraction certificate contains duplicate port bindings.",
326
+ )
327
+ )
328
+ return certificate
329
+
330
+
331
+ @dataclass(frozen=True)
332
+ class GenericStereoRuleResult:
333
+ """Extracted rule paired with its replay certificate."""
334
+
335
+ rule: SynRule
336
+ certificate: RuleExtractionCertificate
337
+
338
+
339
+ @dataclass(frozen=True)
340
+ class _PortBinding:
341
+ reference: int
342
+ target: str
343
+ owner: int
344
+ slot: int
345
+
346
+
347
+ _ATOM_CENTERED = {
348
+ "tetrahedral",
349
+ "square_planar",
350
+ "trigonal_bipyramidal",
351
+ "octahedral",
352
+ }
353
+
354
+
355
+ def _descriptor_bindings(target: str, descriptor: Any) -> tuple[_PortBinding, ...]:
356
+ # ``stereo_slot`` is defined on the authoritative orbit configuration,
357
+ # not on the parser's raw atom order. For example, tetrahedral parity can
358
+ # exchange two peripheral positions while preserving the same descriptor.
359
+ frame = descriptor.configuration.frame
360
+ bindings = []
361
+ if descriptor.descriptor_class in _ATOM_CENTERED:
362
+ owner = frame[0]
363
+ if not isinstance(owner, int):
364
+ return ()
365
+ for slot, reference in enumerate(frame[1:]):
366
+ if isinstance(reference, int):
367
+ bindings.append(_PortBinding(reference, target, owner, slot))
368
+ return tuple(bindings)
369
+ left_owner, right_owner = frame[2:4]
370
+ if isinstance(left_owner, int):
371
+ for slot, reference in enumerate(frame[:2]):
372
+ if isinstance(reference, int):
373
+ bindings.append(_PortBinding(reference, target, left_owner, slot))
374
+ if isinstance(right_owner, int):
375
+ for slot, reference in enumerate(frame[4:]):
376
+ if isinstance(reference, int):
377
+ bindings.append(_PortBinding(reference, target, right_owner, slot))
378
+ return tuple(bindings)
379
+
380
+
381
+ def _side_value(value: Any, side: int) -> Any:
382
+ if isinstance(value, (tuple, list)) and len(value) == 2:
383
+ return value[side]
384
+ return value
385
+
386
+
387
+ def _node_by_atom_map(graph: nx.Graph) -> dict[int, Any]:
388
+ result = {}
389
+ for node, attrs in graph.nodes(data=True):
390
+ atom_map = attrs.get("atom_map")
391
+ values = atom_map if isinstance(atom_map, (tuple, list)) else (atom_map,)
392
+ for value in values:
393
+ if not isinstance(value, int) or value <= 0:
394
+ continue
395
+ if value in result and result[value] != node:
396
+ raise GenericStereoExtractionError(
397
+ GenericStereoExtractionIssue(
398
+ GenericStereoExtractionIssueCode.INPUT_NOT_MAPPED,
399
+ "A generic stereo source requires unique positive atom maps.",
400
+ {"atom_map": value},
401
+ )
402
+ )
403
+ result[value] = node
404
+ return result
405
+
406
+
407
+ def _validate_mapped_reaction(reaction: str) -> None:
408
+ reactant, product = rsmi_to_graph(
409
+ reaction,
410
+ drop_non_aam=False,
411
+ use_index_as_atom_map=False,
412
+ )
413
+ layers = {"reactant": reactant, "product": product}
414
+ layer_maps = {}
415
+ for layer, graph in layers.items():
416
+ if graph is None:
417
+ raise GenericStereoExtractionError(
418
+ GenericStereoExtractionIssue(
419
+ GenericStereoExtractionIssueCode.INPUT_NOT_MAPPED,
420
+ "Generic stereo extraction requires a valid mapped reaction.",
421
+ {"layer": layer},
422
+ )
423
+ )
424
+ values = [attrs.get("atom_map") for _, attrs in graph.nodes(data=True)]
425
+ if any(type(value) is not int or value <= 0 for value in values) or (
426
+ len(set(values)) != len(values)
427
+ ):
428
+ raise GenericStereoExtractionError(
429
+ GenericStereoExtractionIssue(
430
+ GenericStereoExtractionIssueCode.INPUT_NOT_MAPPED,
431
+ "Every source atom requires one unique positive atom map.",
432
+ {"layer": layer},
433
+ )
434
+ )
435
+ layer_maps[layer] = set(values)
436
+ if layer_maps["reactant"] != layer_maps["product"]:
437
+ raise GenericStereoExtractionError(
438
+ GenericStereoExtractionIssue(
439
+ GenericStereoExtractionIssueCode.INPUT_NOT_MAPPED,
440
+ "Reactant and product atom-map domains must be identical.",
441
+ )
442
+ )
443
+
444
+
445
+ def _rule_digest(rule: SynRule) -> str:
446
+ payload = (
447
+ rule.canonical_smiles,
448
+ rule._stereo_signature(),
449
+ )
450
+ return hashlib.sha256(repr(payload).encode("utf-8")).hexdigest()
451
+
452
+
453
+ def _unique_graphs(graphs: Iterable[nx.Graph]) -> list[SynGraph]:
454
+ unique: list[SynGraph] = []
455
+ for graph in graphs:
456
+ wrapped = SynGraph(graph)
457
+ if any(wrapped == existing for existing in unique):
458
+ continue
459
+ unique.append(wrapped)
460
+ return unique
461
+
462
+
463
+ class GenericStereoRuleExtractor:
464
+ """Extract conservative typed-wildcard stereo rules from mapped reactions."""
465
+
466
+ def __init__(self, policy: GenericStereoRulePolicy | None = None):
467
+ self.policy = policy or GenericStereoRulePolicy()
468
+
469
+ @staticmethod
470
+ def _eligible_bindings(
471
+ its: nx.Graph,
472
+ by_map: Mapping[int, Any],
473
+ ) -> tuple[dict[int, _PortBinding], frozenset[int]]:
474
+ stereo = its.graph.get("stereo_descriptors", {})
475
+ reactant = stereo.get("reactant", {})
476
+ product = stereo.get("product", {})
477
+ changes = its.graph.get("stereo_changes", {})
478
+ candidates: dict[int, list[_PortBinding]] = {}
479
+ for target, descriptor in reactant.items():
480
+ after = product.get(target)
481
+ if after is None or descriptor.descriptor_class != after.descriptor_class:
482
+ continue
483
+ after_references = set(after.atoms)
484
+ alignment = changes.get(target)
485
+ replaced = (
486
+ set(alignment.alignment.removed) | set(alignment.alignment.added)
487
+ if alignment is not None
488
+ else set()
489
+ )
490
+ for binding in _descriptor_bindings(target, descriptor):
491
+ if binding.reference not in after_references or (
492
+ binding.reference in replaced
493
+ ):
494
+ continue
495
+ node = by_map.get(binding.reference)
496
+ owner_node = by_map.get(binding.owner)
497
+ if (
498
+ node is None
499
+ or owner_node is None
500
+ or not its.has_edge(node, owner_node)
501
+ ):
502
+ continue
503
+ attrs = its.nodes[node]
504
+ stable_node = all(
505
+ _side_value(attrs.get(name), 0) == _side_value(attrs.get(name), 1)
506
+ for name in ("element", "charge", "radical", "present")
507
+ )
508
+ order = its.edges[node, owner_node].get("order")
509
+ stable_bond = _side_value(order, 0) == _side_value(order, 1)
510
+ if stable_node and stable_bond:
511
+ candidates.setdefault(binding.reference, []).append(binding)
512
+ result = {}
513
+ ambiguous = set()
514
+ for reference, bindings in candidates.items():
515
+ distinct = set(bindings)
516
+ if len(distinct) == 1:
517
+ result[reference] = next(iter(distinct))
518
+ else:
519
+ ambiguous.add(reference)
520
+ return result, frozenset(ambiguous)
521
+
522
+ def _selected_bindings(
523
+ self,
524
+ its: nx.Graph,
525
+ by_map: Mapping[int, Any],
526
+ ) -> tuple[_PortBinding, ...]:
527
+ eligible, ambiguous = self._eligible_bindings(its, by_map)
528
+ selected = self.policy.selected_references
529
+ if selected is None:
530
+ if ambiguous:
531
+ raise GenericStereoExtractionError(
532
+ GenericStereoExtractionIssue(
533
+ GenericStereoExtractionIssueCode.AMBIGUOUS_BINDING,
534
+ "A peripheral reference binds multiple stereo frames.",
535
+ {"references": tuple(sorted(ambiguous))},
536
+ )
537
+ )
538
+ references = set(eligible)
539
+ else:
540
+ references = set(selected)
541
+ issues = []
542
+ for reference in sorted(references):
543
+ if reference not in by_map:
544
+ issues.append(
545
+ GenericStereoExtractionIssue(
546
+ GenericStereoExtractionIssueCode.REFERENCE_NOT_FOUND,
547
+ "A selected stereo reference is absent from the source rule.",
548
+ {"reference": reference},
549
+ )
550
+ )
551
+ elif reference in ambiguous:
552
+ issues.append(
553
+ GenericStereoExtractionIssue(
554
+ GenericStereoExtractionIssueCode.AMBIGUOUS_BINDING,
555
+ "A selected reference binds multiple stereo frames.",
556
+ {"reference": reference},
557
+ )
558
+ )
559
+ elif reference not in eligible:
560
+ issues.append(
561
+ GenericStereoExtractionIssue(
562
+ GenericStereoExtractionIssueCode.REFERENCE_NOT_ELIGIBLE,
563
+ "A selected reference is not one unchanged peripheral ligand.",
564
+ {"reference": reference},
565
+ )
566
+ )
567
+ if issues:
568
+ raise GenericStereoExtractionError(*issues)
569
+ return tuple(eligible[reference] for reference in sorted(references))
570
+
571
+ def _constraint(
572
+ self,
573
+ its: nx.Graph,
574
+ by_map: Mapping[int, Any],
575
+ binding: _PortBinding,
576
+ ) -> WildcardConstraint:
577
+ node = by_map[binding.reference]
578
+ owner = by_map[binding.owner]
579
+ attrs = its.nodes[node]
580
+ observed = {
581
+ "elements": {_side_value(attrs.get("element"), 0)},
582
+ "charges": {int(_side_value(attrs.get("charge", 0), 0))},
583
+ "radicals": {int(_side_value(attrs.get("radical", 0), 0))},
584
+ "bond_orders": {float(_side_value(its.edges[node, owner].get("order"), 0))},
585
+ "materialization": "concrete",
586
+ }
587
+ source = self.policy.domain_source
588
+ supplied = self.policy.explicit_domains.get(binding.reference)
589
+ if (
590
+ source
591
+ in {
592
+ GenericStereoDomainSource.CLASS,
593
+ GenericStereoDomainSource.CORPUS,
594
+ }
595
+ and supplied is None
596
+ ):
597
+ raise GenericStereoExtractionError(
598
+ GenericStereoExtractionIssue(
599
+ GenericStereoExtractionIssueCode.DOMAIN_REQUIRED,
600
+ "Class/corpus generalization requires an explicit domain.",
601
+ {"reference": binding.reference, "source": source.value},
602
+ )
603
+ )
604
+ if source is GenericStereoDomainSource.CORPUS and not (
605
+ self.policy.domain_evidence.get(binding.reference)
606
+ ):
607
+ raise GenericStereoExtractionError(
608
+ GenericStereoExtractionIssue(
609
+ GenericStereoExtractionIssueCode.DOMAIN_REQUIRED,
610
+ "Corpus generalization requires aligned record evidence.",
611
+ {"reference": binding.reference, "source": source.value},
612
+ )
613
+ )
614
+ values = observed if supplied is None else {**observed, **dict(supplied)}
615
+ return WildcardConstraint(
616
+ WildcardRole.STEREO_LIGAND_PORT,
617
+ elements=values.get("elements"),
618
+ charges=values.get("charges"),
619
+ radicals=values.get("radicals"),
620
+ bond_orders=values.get("bond_orders"),
621
+ owner=binding.owner,
622
+ stereo_slot=binding.slot,
623
+ virtual_kind=values.get("virtual_kind"),
624
+ materialization=values.get("materialization", "concrete"),
625
+ capacity=int(values.get("capacity", 1)),
626
+ resource_budget=values.get("resource_budget"),
627
+ )
628
+
629
+ @staticmethod
630
+ def _make_wildcard(attrs: dict[str, Any], constraint: WildcardConstraint) -> None:
631
+ atom_map = attrs.get("atom_map")
632
+ maps = atom_map if isinstance(atom_map, tuple) else (atom_map, atom_map)
633
+ neighbors = attrs.get("neighbors", ([], []))
634
+ attrs.update(
635
+ element=("*", "*"),
636
+ aromatic=(False, False),
637
+ hcount=(0, 0),
638
+ charge=(0, 0),
639
+ radical=(0, 0),
640
+ lone_pairs=(0, 0),
641
+ valence_electrons=(0, 0),
642
+ typesGH=(
643
+ ("*", False, 0, 0, 0, 0, 0, neighbors[0], maps[0]),
644
+ ("*", False, 0, 0, 0, 0, 0, neighbors[1], maps[1]),
645
+ ),
646
+ wildcard_role=constraint.role.value,
647
+ owner=constraint.owner,
648
+ stereo_slot=constraint.stereo_slot,
649
+ elements=set(constraint.elements) if constraint.elements else None,
650
+ charges=set(constraint.charges) if constraint.charges else None,
651
+ radicals=set(constraint.radicals) if constraint.radicals else None,
652
+ bond_orders=(
653
+ set(constraint.bond_orders) if constraint.bond_orders else None
654
+ ),
655
+ side=constraint.side.value,
656
+ capacity=constraint.capacity,
657
+ resource_budget=constraint.resource_budget,
658
+ virtual_kind=constraint.virtual_kind,
659
+ mapped_identity=constraint.mapped_identity,
660
+ materialization=constraint.materialization,
661
+ )
662
+ for key in (
663
+ "elements",
664
+ "charges",
665
+ "radicals",
666
+ "bond_orders",
667
+ "resource_budget",
668
+ "virtual_kind",
669
+ "mapped_identity",
670
+ ):
671
+ if attrs[key] is None:
672
+ attrs.pop(key)
673
+
674
+ @staticmethod
675
+ def _source_replay(
676
+ rule: SynRule,
677
+ concrete_its: nx.Graph,
678
+ ) -> tuple[int, int, bool]:
679
+ from synkit.Synthesis.Reactor import SynReactor
680
+
681
+ reverter = ITSReverter(concrete_its)
682
+ reactant = reverter.to_reactant_graph()
683
+ expected = SynGraph(reverter.to_product_graph())
684
+ reactor = SynReactor(
685
+ reactant,
686
+ rule,
687
+ template_format="tuple",
688
+ explicit_h=False,
689
+ stereo_mode="strict",
690
+ dedup_its=False,
691
+ )
692
+ products = [ITSReverter(its).to_product_graph() for its in reactor.its_list]
693
+ unique = _unique_graphs(products)
694
+ exact = bool(products) and all(
695
+ SynGraph(product) == expected for product in products
696
+ )
697
+ return reactor.mapping_count, len(unique), exact
698
+
699
+ @staticmethod
700
+ def _reverse_replay(
701
+ rule: SynRule,
702
+ concrete_its: nx.Graph,
703
+ ) -> tuple[str, int, bool]:
704
+ from synkit.Synthesis.Reactor import SynReactor
705
+
706
+ try:
707
+ reverse = rule.reversed()
708
+ except NonInvertibleStereoEffectError:
709
+ return "non_invertible", 0, True
710
+ reverter = ITSReverter(concrete_its)
711
+ product = reverter.to_product_graph()
712
+ expected = SynGraph(reverter.to_reactant_graph())
713
+ reactor = SynReactor(
714
+ product,
715
+ reverse,
716
+ template_format="tuple",
717
+ explicit_h=False,
718
+ stereo_mode="strict",
719
+ dedup_its=False,
720
+ )
721
+ reactants = [ITSReverter(its).to_product_graph() for its in reactor.its_list]
722
+ exact = bool(reactants) and all(
723
+ SynGraph(candidate) == expected for candidate in reactants
724
+ )
725
+ return "exact" if exact else "failed", reactor.mapping_count, exact
726
+
727
+ def extract(self, reaction: str | nx.Graph) -> GenericStereoRuleResult:
728
+ """Return a certified generic rule for one mapped reaction or ITS."""
729
+ if isinstance(reaction, str):
730
+ _validate_mapped_reaction(reaction)
731
+ concrete_its = (
732
+ rsmi_to_its(
733
+ reaction,
734
+ format="tuple",
735
+ drop_non_aam=False,
736
+ use_index_as_atom_map=True,
737
+ )
738
+ if isinstance(reaction, str)
739
+ else reaction.copy()
740
+ )
741
+ from synkit.Graph.ITS.stereo import (
742
+ StereoITSValidationError,
743
+ validate_stereo_support,
744
+ )
745
+
746
+ reverter = ITSReverter(concrete_its)
747
+ try:
748
+ validate_stereo_support(
749
+ reverter.to_reactant_graph(),
750
+ side="reactant",
751
+ )
752
+ validate_stereo_support(
753
+ reverter.to_product_graph(),
754
+ side="product",
755
+ )
756
+ except StereoITSValidationError as error:
757
+ raise GenericStereoExtractionError(
758
+ GenericStereoExtractionIssue(
759
+ GenericStereoExtractionIssueCode.INVALID_STEREO_SUPPORT,
760
+ "Generic extraction requires valid endpoint stereo " "supports.",
761
+ {"refusals": [refusal.to_dict() for refusal in error.refusals]},
762
+ )
763
+ ) from error
764
+ concrete_rule = SynRule(concrete_its, format="tuple", implicit_h=False)
765
+ if not concrete_rule.stereo_guards and not concrete_rule.stereo_effects:
766
+ raise GenericStereoExtractionError(
767
+ GenericStereoExtractionIssue(
768
+ GenericStereoExtractionIssueCode.NO_STEREO,
769
+ "Generic stereo extraction requires a stereo-bearing reaction.",
770
+ )
771
+ )
772
+ by_map = _node_by_atom_map(concrete_its)
773
+ if len(by_map) != concrete_its.number_of_nodes():
774
+ raise GenericStereoExtractionError(
775
+ GenericStereoExtractionIssue(
776
+ GenericStereoExtractionIssueCode.INPUT_NOT_MAPPED,
777
+ "Every concrete ITS node requires one unique positive atom map.",
778
+ )
779
+ )
780
+ bindings = (
781
+ ()
782
+ if self.policy.domain_source is GenericStereoDomainSource.EXACT
783
+ else self._selected_bindings(concrete_its, by_map)
784
+ )
785
+ if (
786
+ self.policy.domain_source is not GenericStereoDomainSource.EXACT
787
+ and not bindings
788
+ ):
789
+ raise GenericStereoExtractionError(
790
+ GenericStereoExtractionIssue(
791
+ GenericStereoExtractionIssueCode.REFERENCE_NOT_ELIGIBLE,
792
+ "No unchanged peripheral stereo reference is eligible.",
793
+ )
794
+ )
795
+ generalized = concrete_its.copy()
796
+ generalized_by_map = _node_by_atom_map(generalized)
797
+ ports = []
798
+ for binding in bindings:
799
+ constraint = self._constraint(concrete_its, by_map, binding)
800
+ self._make_wildcard(
801
+ generalized.nodes[generalized_by_map[binding.reference]],
802
+ constraint,
803
+ )
804
+ ports.append(
805
+ ExtractedStereoPort(
806
+ binding.reference,
807
+ binding.target,
808
+ binding.owner,
809
+ binding.slot,
810
+ constraint,
811
+ self.policy.domain_source,
812
+ self.policy.domain_evidence.get(binding.reference, ()),
813
+ )
814
+ )
815
+ generalized.graph["generic_stereo_extraction"] = {
816
+ "schema": EXTRACTION_SCHEMA,
817
+ "domain_source": self.policy.domain_source.value,
818
+ "generalized_references": [port.reference for port in ports],
819
+ }
820
+ rule = SynRule(generalized, format="tuple", implicit_h=False)
821
+
822
+ mapping_count = unique_count = 0
823
+ source_exact = True
824
+ if self.policy.verify_source:
825
+ mapping_count, unique_count, source_exact = self._source_replay(
826
+ rule,
827
+ concrete_its,
828
+ )
829
+ if not source_exact:
830
+ raise GenericStereoExtractionError(
831
+ GenericStereoExtractionIssue(
832
+ GenericStereoExtractionIssueCode.SOURCE_REPLAY_FAILED,
833
+ "The extracted generic rule did not exactly replay its source.",
834
+ {
835
+ "mapping_count": mapping_count,
836
+ "unique_products": unique_count,
837
+ },
838
+ )
839
+ )
840
+
841
+ reverse_status = "not_checked"
842
+ reverse_count = 0
843
+ if self.policy.verify_reverse:
844
+ reverse_status, reverse_count, reverse_exact = self._reverse_replay(
845
+ rule,
846
+ concrete_its,
847
+ )
848
+ if not reverse_exact:
849
+ raise GenericStereoExtractionError(
850
+ GenericStereoExtractionIssue(
851
+ GenericStereoExtractionIssueCode.REVERSE_REPLAY_FAILED,
852
+ "The extracted generic rule failed exact reverse replay.",
853
+ {"mapping_count": reverse_count},
854
+ )
855
+ )
856
+
857
+ certificate = RuleExtractionCertificate(
858
+ source_rule_digest=_rule_digest(concrete_rule),
859
+ generic_rule_digest=_rule_digest(rule),
860
+ ports=tuple(ports),
861
+ source_mapping_count=mapping_count,
862
+ source_unique_products=unique_count,
863
+ source_replay_exact=source_exact,
864
+ reverse_status=reverse_status,
865
+ reverse_mapping_count=reverse_count,
866
+ )
867
+ rule.rc.raw.graph["generic_stereo_extraction_certificate"] = (
868
+ certificate.to_dict()
869
+ )
870
+ return GenericStereoRuleResult(rule, certificate)
871
+
872
+
873
+ __all__ = [
874
+ "EXTRACTION_SCHEMA",
875
+ "ExtractedStereoPort",
876
+ "GenericStereoDomainSource",
877
+ "GenericStereoExtractionError",
878
+ "GenericStereoExtractionIssue",
879
+ "GenericStereoExtractionIssueCode",
880
+ "GenericStereoRuleExtractor",
881
+ "GenericStereoRulePolicy",
882
+ "GenericStereoRuleResult",
883
+ "RuleExtractionCertificate",
884
+ ]