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,918 @@
1
+ """Versioned public models for supplied reaction mechanisms.
2
+
3
+ The models in this module deliberately describe *annotations supplied by a
4
+ user or dataset*. They do not score, enumerate, or select mechanisms.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ import json
11
+ from pathlib import Path
12
+ from typing import Any, Iterable, Literal, Mapping
13
+
14
+ from .electron_models import ( # noqa: F401
15
+ ElectronLocus,
16
+ ElectronMove,
17
+ ElectronMoveGroup,
18
+ MechanismModelError,
19
+ VerificationIssue,
20
+ )
21
+ from .symbols import CanonicalLocus
22
+
23
+ SCHEMA_VERSION = "2.0.0-draft1"
24
+ LocusKind = CanonicalLocus
25
+ ArrowType = Literal["curved", "fishhook"]
26
+ StereoEffectKind = Literal[
27
+ "PRESERVE", "INVERT", "BREAK", "FORM", "FLEETING", "UNSPECIFIED"
28
+ ]
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class StereoDescriptor:
33
+ """Serializable envelope for SynKit's permutation-aware descriptors."""
34
+
35
+ descriptor_class: Literal[
36
+ "tetrahedral",
37
+ "square_planar",
38
+ "trigonal_bipyramidal",
39
+ "octahedral",
40
+ "planar_bond",
41
+ "atrop_bond",
42
+ "unknown",
43
+ ]
44
+ atoms: tuple[int | str, ...]
45
+ parity: int | None = None
46
+ state: Literal["specified", "unknown", "unspecified", "absent"] = "specified"
47
+ provenance: str | None = None
48
+
49
+ def __post_init__(self) -> None:
50
+ expected = {
51
+ "tetrahedral": 5,
52
+ "square_planar": 5,
53
+ "trigonal_bipyramidal": 6,
54
+ "octahedral": 7,
55
+ "planar_bond": 6,
56
+ "atrop_bond": 6,
57
+ "unknown": 0,
58
+ }[self.descriptor_class]
59
+ if expected and len(self.atoms) != expected:
60
+ raise MechanismModelError(
61
+ f"{self.descriptor_class} descriptors require {expected} atom maps."
62
+ )
63
+ allowed = (
64
+ (0, None)
65
+ if self.descriptor_class in {"square_planar", "planar_bond"}
66
+ else (-1, 1, None)
67
+ )
68
+ if self.parity not in allowed:
69
+ raise MechanismModelError(
70
+ f"Invalid parity for {self.descriptor_class} stereo."
71
+ )
72
+ if self.state == "specified" and self.parity is None:
73
+ raise MechanismModelError("Specified stereo descriptors require a parity.")
74
+ if self.state != "specified" and self.parity is not None:
75
+ raise MechanismModelError(
76
+ "Unknown, unspecified, and absent stereo states cannot carry parity."
77
+ )
78
+ if self.descriptor_class == "unknown" and self.state == "specified":
79
+ raise MechanismModelError(
80
+ "Unknown descriptor classes cannot declare specified stereo."
81
+ )
82
+ if self.descriptor_class != "unknown":
83
+ # Reuse the executable descriptor contract so mechanism envelopes
84
+ # cannot reintroduce StereoMolGraph's untyped ``None`` or attach a
85
+ # virtual ligand to the wrong stereochemical owner.
86
+ from synkit.Graph.Stereo.descriptors import stereo_from_dict
87
+
88
+ try:
89
+ stereo_from_dict(
90
+ {
91
+ "descriptor_class": self.descriptor_class,
92
+ "atoms": self.atoms,
93
+ "parity": self.parity,
94
+ "provenance": self.provenance,
95
+ }
96
+ )
97
+ except (TypeError, ValueError) as error:
98
+ raise MechanismModelError(str(error)) from error
99
+
100
+ def to_dict(self) -> dict[str, Any]:
101
+ return {
102
+ "descriptor_class": self.descriptor_class,
103
+ "atoms": list(self.atoms),
104
+ "parity": self.parity,
105
+ "state": self.state,
106
+ "provenance": self.provenance,
107
+ }
108
+
109
+ def inverted(self) -> "StereoDescriptor":
110
+ """Return the opposite relative orientation of specified stereo."""
111
+ if self.descriptor_class == "unknown" or self.state != "specified":
112
+ raise MechanismModelError(
113
+ "Only specified executable stereo descriptors can be inverted."
114
+ )
115
+ from synkit.Graph.Stereo import stereo_from_dict
116
+
117
+ value = stereo_from_dict(self.to_dict()).invert().to_dict()
118
+ value["state"] = self.state
119
+ return StereoDescriptor.from_dict(value)
120
+
121
+ @property
122
+ def target_key(self) -> str:
123
+ """Return the canonical registry key owned by this descriptor."""
124
+ if self.descriptor_class in {
125
+ "tetrahedral",
126
+ "square_planar",
127
+ "trigonal_bipyramidal",
128
+ "octahedral",
129
+ }:
130
+ return f"atom:{self.atoms[0]}"
131
+ if self.descriptor_class in {"planar_bond", "atrop_bond"}:
132
+ return f"bond:{min(self.atoms[2:4])}-{max(self.atoms[2:4])}"
133
+ raise MechanismModelError(
134
+ "Unknown descriptor classes do not own an executable endpoint locus."
135
+ )
136
+
137
+ @classmethod
138
+ def from_dict(cls, value: Mapping[str, Any]) -> "StereoDescriptor":
139
+ return cls(
140
+ descriptor_class=value["descriptor_class"],
141
+ atoms=tuple(value.get("atoms", ())),
142
+ parity=value.get("parity"),
143
+ state=value.get("state", "specified"),
144
+ provenance=value.get("provenance"),
145
+ )
146
+
147
+
148
+ @dataclass(frozen=True)
149
+ class StereoEffect:
150
+ descriptor_target: tuple[str, int | tuple[int, int]]
151
+ effect: StereoEffectKind
152
+ before: StereoDescriptor | None = None
153
+ after: StereoDescriptor | None = None
154
+ provenance: str = "annotated"
155
+
156
+ def __post_init__(self) -> None:
157
+ if len(self.descriptor_target) != 2:
158
+ raise MechanismModelError(
159
+ "Stereo descriptor targets require a kind and mapped locus."
160
+ )
161
+ kind, reference = self.descriptor_target
162
+ if kind == "atom":
163
+ if type(reference) is not int or reference <= 0:
164
+ raise MechanismModelError(
165
+ "Atom stereo targets require one positive atom map."
166
+ )
167
+ normalized_reference: int | tuple[int, int] = reference
168
+ elif kind == "bond":
169
+ if (
170
+ not isinstance(reference, (tuple, list))
171
+ or len(reference) != 2
172
+ or any(type(value) is not int or value <= 0 for value in reference)
173
+ or reference[0] == reference[1]
174
+ ):
175
+ raise MechanismModelError(
176
+ "Bond stereo targets require two distinct positive atom maps."
177
+ )
178
+ normalized_reference = tuple(sorted(reference))
179
+ else:
180
+ raise MechanismModelError("Stereo target kind must be 'atom' or 'bond'.")
181
+ if self.effect not in {
182
+ "PRESERVE",
183
+ "INVERT",
184
+ "BREAK",
185
+ "FORM",
186
+ "FLEETING",
187
+ "UNSPECIFIED",
188
+ }:
189
+ raise MechanismModelError(f"Unsupported stereo effect: {self.effect!r}.")
190
+ object.__setattr__(self, "descriptor_target", (kind, normalized_reference))
191
+
192
+ target_key = (
193
+ f"atom:{normalized_reference}"
194
+ if kind == "atom"
195
+ else f"bond:{normalized_reference[0]}-{normalized_reference[1]}"
196
+ )
197
+ for descriptor in (self.before, self.after):
198
+ if descriptor is None or descriptor.descriptor_class == "unknown":
199
+ continue
200
+ descriptor_key = (
201
+ f"atom:{descriptor.atoms[0]}"
202
+ if descriptor.descriptor_class
203
+ in {
204
+ "tetrahedral",
205
+ "square_planar",
206
+ "trigonal_bipyramidal",
207
+ "octahedral",
208
+ }
209
+ else f"bond:{min(descriptor.atoms[2:4])}-{max(descriptor.atoms[2:4])}"
210
+ )
211
+ if descriptor_key != target_key:
212
+ raise MechanismModelError(
213
+ f"Stereo descriptor belongs to {descriptor_key}, not {target_key}."
214
+ )
215
+
216
+ def to_dict(self) -> dict[str, Any]:
217
+ target_kind, target_reference = self.descriptor_target
218
+ return {
219
+ "descriptor_target": [
220
+ target_kind,
221
+ (
222
+ list(target_reference)
223
+ if isinstance(target_reference, tuple)
224
+ else target_reference
225
+ ),
226
+ ],
227
+ "effect": self.effect,
228
+ "before": self.before.to_dict() if self.before else None,
229
+ "after": self.after.to_dict() if self.after else None,
230
+ "provenance": self.provenance,
231
+ }
232
+
233
+ def reversed(self) -> "StereoEffect":
234
+ """Return the inverse endpoint transition when it is well-defined.
235
+
236
+ ``UNSPECIFIED`` is deliberately non-invertible: discarding an
237
+ orientation does not contain enough information to reconstruct it.
238
+ """
239
+ if self.effect == "UNSPECIFIED":
240
+ raise MechanismModelError(
241
+ "NONREVERSIBLE_STEREO_EFFECT: UNSPECIFIED stereo cannot be "
242
+ "reversed without a supplied recovery descriptor."
243
+ )
244
+ if self.effect == "FORM":
245
+ return StereoEffect(
246
+ self.descriptor_target,
247
+ "BREAK",
248
+ before=self.after,
249
+ provenance=self.provenance,
250
+ )
251
+ if self.effect == "BREAK":
252
+ return StereoEffect(
253
+ self.descriptor_target,
254
+ "FORM",
255
+ after=self.before,
256
+ provenance=self.provenance,
257
+ )
258
+ if self.effect == "INVERT":
259
+ if self.before is None:
260
+ raise MechanismModelError(
261
+ "NONREVERSIBLE_STEREO_EFFECT: INVERT requires a before "
262
+ "descriptor."
263
+ )
264
+ reverse_before = self.after or self.before.inverted()
265
+ return StereoEffect(
266
+ self.descriptor_target,
267
+ "INVERT",
268
+ before=reverse_before,
269
+ after=self.before,
270
+ provenance=self.provenance,
271
+ )
272
+ if self.effect == "PRESERVE" and self.after is None:
273
+ return self
274
+ return StereoEffect(
275
+ self.descriptor_target,
276
+ self.effect,
277
+ before=self.after,
278
+ after=self.before,
279
+ provenance=self.provenance,
280
+ )
281
+
282
+ @classmethod
283
+ def from_dict(cls, value: Mapping[str, Any]) -> "StereoEffect":
284
+ before = value.get("before")
285
+ after = value.get("after")
286
+ return cls(
287
+ descriptor_target=tuple(value["descriptor_target"]),
288
+ effect=value["effect"],
289
+ before=StereoDescriptor.from_dict(before) if before else None,
290
+ after=StereoDescriptor.from_dict(after) if after else None,
291
+ provenance=value.get("provenance", "annotated"),
292
+ )
293
+
294
+
295
+ @dataclass(frozen=True)
296
+ class ElectrocyclicStereoMotion:
297
+ """Correlated terminal-orbital motion for a supplied electrocyclic step."""
298
+
299
+ mode: Literal["CONROTATORY", "DISROTATORY"]
300
+ direction: Literal["RING_CLOSURE", "RING_OPENING"]
301
+ termini: tuple[int, int]
302
+ substituents: tuple[int | str, int | str]
303
+ terminal_motion: tuple[int, int]
304
+ pi_electrons: int
305
+ activation: Literal["THERMAL", "PHOTOCHEMICAL"] | None
306
+ provenance: str = "annotated"
307
+
308
+ def __post_init__(self) -> None:
309
+ if self.mode not in {"CONROTATORY", "DISROTATORY"}:
310
+ raise MechanismModelError("Unsupported electrocyclic rotation mode.")
311
+ if self.direction not in {"RING_CLOSURE", "RING_OPENING"}:
312
+ raise MechanismModelError("Unsupported electrocyclic direction.")
313
+ if (
314
+ len(self.termini) != 2
315
+ or len(set(self.termini)) != 2
316
+ or any(type(value) is not int or value <= 0 for value in self.termini)
317
+ ):
318
+ raise MechanismModelError(
319
+ "Electrocyclic motion requires two distinct mapped termini."
320
+ )
321
+ if len(self.substituents) != 2:
322
+ raise MechanismModelError(
323
+ "Electrocyclic motion requires one tracked substituent per terminus."
324
+ )
325
+ if len(set(self.substituents)) != 2:
326
+ raise MechanismModelError(
327
+ "Electrocyclic terminal substituents must be distinct."
328
+ )
329
+ from synkit.Graph.Stereo import parse_virtual_reference
330
+
331
+ for terminus, substituent in zip(self.termini, self.substituents):
332
+ if type(substituent) is int:
333
+ if substituent <= 0 or substituent == terminus:
334
+ raise MechanismModelError(
335
+ "Mapped terminal substituents must be positive and "
336
+ "distinct from their termini."
337
+ )
338
+ continue
339
+ virtual = parse_virtual_reference(substituent)
340
+ if virtual is None or virtual.center != terminus:
341
+ raise MechanismModelError(
342
+ "A virtual terminal substituent must belong to its terminus."
343
+ )
344
+ if len(self.terminal_motion) != 2 or any(
345
+ value not in {-1, 1} for value in self.terminal_motion
346
+ ):
347
+ raise MechanismModelError(
348
+ "Terminal motions must be a pair of +1/-1 rotations."
349
+ )
350
+ if (
351
+ type(self.pi_electrons) is not int
352
+ or self.pi_electrons < 2
353
+ or self.pi_electrons % 2
354
+ ):
355
+ raise MechanismModelError(
356
+ "Electrocyclic pi_electrons must be a positive even integer."
357
+ )
358
+ if self.activation not in {None, "THERMAL", "PHOTOCHEMICAL"}:
359
+ raise MechanismModelError(
360
+ "Activation must be THERMAL, PHOTOCHEMICAL, or absent."
361
+ )
362
+
363
+ @property
364
+ def expected_mode(self) -> str | None:
365
+ """Return the Woodward-Hoffmann mode for supplied electron/context."""
366
+ if self.activation is None:
367
+ return None
368
+ thermal_conrotatory = self.pi_electrons % 4 == 0
369
+ conrotatory = (
370
+ thermal_conrotatory
371
+ if self.activation == "THERMAL"
372
+ else not thermal_conrotatory
373
+ )
374
+ return "CONROTATORY" if conrotatory else "DISROTATORY"
375
+
376
+ def verification_issues(
377
+ self,
378
+ *,
379
+ step_id: str,
380
+ before_graph: Any | None = None,
381
+ graph: Any | None = None,
382
+ ) -> tuple[VerificationIssue, ...]:
383
+ """Validate context, orbital rule, and correlation as one assertion."""
384
+ _changes, issues = self.verification_evidence(
385
+ step_id=step_id,
386
+ before_graph=before_graph,
387
+ graph=graph,
388
+ )
389
+ return issues
390
+
391
+ def verification_evidence(
392
+ self,
393
+ *,
394
+ step_id: str,
395
+ before_graph: Any | None = None,
396
+ graph: Any | None = None,
397
+ ) -> tuple[tuple[Any, ...], tuple[VerificationIssue, ...]]:
398
+ """Return canonical local-neighbor changes and validation issues."""
399
+ issues: list[VerificationIssue] = []
400
+ if self.activation is None:
401
+ issues.append(
402
+ VerificationIssue(
403
+ "ELECTROCYCLIC_CONTEXT_REQUIRED",
404
+ "Electrocyclic mode requires thermal or photochemical context.",
405
+ step_id=step_id,
406
+ )
407
+ )
408
+ elif self.mode != self.expected_mode:
409
+ issues.append(
410
+ VerificationIssue(
411
+ "ELECTROCYCLIC_MODE_MISMATCH",
412
+ "Declared rotation mode violates the supplied activation "
413
+ "and pi-electron count.",
414
+ step_id=step_id,
415
+ expected=self.expected_mode,
416
+ observed=self.mode,
417
+ )
418
+ )
419
+ changes: tuple[Any, ...] = ()
420
+ if before_graph is not None and graph is not None:
421
+ from .neighbor_stereo import derive_relative_neighbor_changes
422
+
423
+ changes, neighbor_issues = derive_relative_neighbor_changes(
424
+ self,
425
+ before_graph,
426
+ graph,
427
+ step_id=step_id,
428
+ )
429
+ issues.extend(neighbor_issues)
430
+ physical_rotations = (
431
+ tuple(change.physical_rotation for change in changes)
432
+ if len(changes) == 2
433
+ else self.terminal_motion
434
+ )
435
+ same_sense = physical_rotations[0] == physical_rotations[1]
436
+ if same_sense != (self.mode == "CONROTATORY"):
437
+ issues.append(
438
+ VerificationIssue(
439
+ "ELECTROCYCLIC_MOTION_INCONSISTENT",
440
+ "Terminal rotations do not realize the declared correlated "
441
+ "mode.",
442
+ step_id=step_id,
443
+ expected=self.mode,
444
+ observed=physical_rotations,
445
+ )
446
+ )
447
+ if graph is not None:
448
+ by_map = {
449
+ attrs.get("atom_map") or node: node
450
+ for node, attrs in graph.nodes(data=True)
451
+ }
452
+ if not set(self.termini) <= set(by_map):
453
+ issues.append(
454
+ VerificationIssue(
455
+ "ELECTROCYCLIC_SUPPORT_INVALID",
456
+ "A correlated terminal map is absent after the step.",
457
+ step_id=step_id,
458
+ expected=self.termini,
459
+ observed=tuple(sorted(by_map)),
460
+ )
461
+ )
462
+ else:
463
+ left, right = (by_map[value] for value in self.termini)
464
+ bonded = graph.has_edge(left, right)
465
+ expected_bonded = self.direction == "RING_CLOSURE"
466
+ if bonded != expected_bonded:
467
+ issues.append(
468
+ VerificationIssue(
469
+ "ELECTROCYCLIC_SUPPORT_INVALID",
470
+ "Terminal bond topology contradicts the declared "
471
+ "ring direction.",
472
+ step_id=step_id,
473
+ expected=expected_bonded,
474
+ observed=bonded,
475
+ )
476
+ )
477
+ return changes, tuple(issues)
478
+
479
+ def reversed(self) -> "ElectrocyclicStereoMotion":
480
+ """Reverse closure/opening and both physical terminal rotations."""
481
+ return ElectrocyclicStereoMotion(
482
+ self.mode,
483
+ ("RING_OPENING" if self.direction == "RING_CLOSURE" else "RING_CLOSURE"),
484
+ self.termini,
485
+ self.substituents,
486
+ tuple(-value for value in self.terminal_motion),
487
+ self.pi_electrons,
488
+ self.activation,
489
+ self.provenance,
490
+ )
491
+
492
+ def to_dict(self) -> dict[str, Any]:
493
+ return {
494
+ "kind": "ELECTROCYCLIC",
495
+ "mode": self.mode,
496
+ "direction": self.direction,
497
+ "termini": list(self.termini),
498
+ "substituents": list(self.substituents),
499
+ "terminal_motion": list(self.terminal_motion),
500
+ "pi_electrons": self.pi_electrons,
501
+ "activation": self.activation,
502
+ "provenance": self.provenance,
503
+ }
504
+
505
+ @classmethod
506
+ def from_dict(
507
+ cls,
508
+ value: Mapping[str, Any],
509
+ ) -> "ElectrocyclicStereoMotion":
510
+ if value.get("kind", "ELECTROCYCLIC") != "ELECTROCYCLIC":
511
+ raise MechanismModelError("Unsupported correlated stereo motion.")
512
+ return cls(
513
+ value["mode"],
514
+ value["direction"],
515
+ tuple(value["termini"]),
516
+ tuple(value["substituents"]),
517
+ tuple(value["terminal_motion"]),
518
+ int(value["pi_electrons"]),
519
+ value.get("activation"),
520
+ value.get("provenance", "annotated"),
521
+ )
522
+
523
+
524
+ @dataclass(frozen=True)
525
+ class MechanisticStep:
526
+ step_id: str
527
+ groups: tuple[ElectronMoveGroup, ...]
528
+ stereo_effects: tuple[StereoEffect, ...] = ()
529
+ stereo_motions: tuple[ElectrocyclicStereoMotion, ...] = ()
530
+ metadata: Mapping[str, Any] = field(default_factory=dict)
531
+
532
+ def __post_init__(self) -> None:
533
+ if not self.step_id:
534
+ raise MechanismModelError("MechanisticStep requires a step_id.")
535
+ object.__setattr__(self, "metadata", dict(self.metadata))
536
+
537
+ def to_dict(self) -> dict[str, Any]:
538
+ return {
539
+ "step_id": self.step_id,
540
+ "groups": [group.to_dict() for group in self.groups],
541
+ "stereo_effects": [effect.to_dict() for effect in self.stereo_effects],
542
+ "stereo_motions": [motion.to_dict() for motion in self.stereo_motions],
543
+ "metadata": dict(self.metadata),
544
+ }
545
+
546
+ def reversed(self) -> "MechanisticStep":
547
+ """Reverse group order, electron flow, and stereo transitions."""
548
+ return MechanisticStep(
549
+ step_id=self.step_id,
550
+ groups=tuple(group.reversed() for group in reversed(self.groups)),
551
+ stereo_effects=tuple(
552
+ effect.reversed() for effect in reversed(self.stereo_effects)
553
+ ),
554
+ stereo_motions=tuple(
555
+ motion.reversed() for motion in reversed(self.stereo_motions)
556
+ ),
557
+ metadata=self.metadata,
558
+ )
559
+
560
+ @classmethod
561
+ def from_dict(cls, value: Mapping[str, Any]) -> "MechanisticStep":
562
+ return cls(
563
+ step_id=value["step_id"],
564
+ groups=tuple(
565
+ ElectronMoveGroup.from_dict(group) for group in value["groups"]
566
+ ),
567
+ stereo_effects=tuple(
568
+ StereoEffect.from_dict(effect)
569
+ for effect in value.get("stereo_effects", ())
570
+ ),
571
+ stereo_motions=tuple(
572
+ ElectrocyclicStereoMotion.from_dict(motion)
573
+ for motion in value.get("stereo_motions", ())
574
+ ),
575
+ metadata=value.get("metadata", {}),
576
+ )
577
+
578
+
579
+ @dataclass(frozen=True)
580
+ class VerificationCertificate:
581
+ status: Literal["VALID", "INVALID", "INCOMPLETE", "UNSUPPORTED"]
582
+ schema_version: str = SCHEMA_VERSION
583
+ verification_level: Literal[
584
+ "EXACT", "DELTA_CONSISTENT", "NORMALIZED", "INVALID"
585
+ ] = "INVALID"
586
+ transition_valid: bool = False
587
+ delta_charge_match: bool = False
588
+ endpoint_match: bool = False
589
+ absolute_lwg_valid: bool = False
590
+ initial_absolute_residuals: Mapping[int, int | float] = field(
591
+ default_factory=dict
592
+ )
593
+ product_absolute_residuals: Mapping[int, int | float] = field(
594
+ default_factory=dict
595
+ )
596
+ initial_global_electron_residual: int | float = 0
597
+ product_global_electron_residual: int | float = 0
598
+ absolute_residuals_invariant: bool = False
599
+ endpoint_resource_policy: str = "strict"
600
+ normalization_evidence: tuple[Mapping[str, Any], ...] = ()
601
+ diagnostics: tuple[str, ...] = ()
602
+ step_reports: tuple[Mapping[str, Any], ...] = ()
603
+ issues: tuple[VerificationIssue, ...] = ()
604
+ final_match: Mapping[str, Any] = field(default_factory=dict)
605
+ repaired: bool = False
606
+
607
+ def to_dict(self) -> dict[str, Any]:
608
+ return {
609
+ "status": self.status,
610
+ "schema_version": self.schema_version,
611
+ "verification_level": self.verification_level,
612
+ "transition_valid": self.transition_valid,
613
+ "delta_charge_match": self.delta_charge_match,
614
+ "endpoint_match": self.endpoint_match,
615
+ "absolute_lwg_valid": self.absolute_lwg_valid,
616
+ "initial_absolute_residuals": dict(self.initial_absolute_residuals),
617
+ "product_absolute_residuals": dict(self.product_absolute_residuals),
618
+ "initial_global_electron_residual": self.initial_global_electron_residual,
619
+ "product_global_electron_residual": self.product_global_electron_residual,
620
+ "absolute_residuals_invariant": self.absolute_residuals_invariant,
621
+ "endpoint_resource_policy": self.endpoint_resource_policy,
622
+ "normalization_evidence": [
623
+ dict(item) for item in self.normalization_evidence
624
+ ],
625
+ "diagnostics": list(self.diagnostics),
626
+ "step_reports": [dict(report) for report in self.step_reports],
627
+ "issues": [issue.to_dict() for issue in self.issues],
628
+ "final_match": dict(self.final_match),
629
+ "repaired": self.repaired,
630
+ }
631
+
632
+ @classmethod
633
+ def from_dict(cls, value: Mapping[str, Any]) -> "VerificationCertificate":
634
+ return cls(
635
+ status=value["status"],
636
+ schema_version=value.get("schema_version", SCHEMA_VERSION),
637
+ verification_level=value.get("verification_level", "INVALID"),
638
+ transition_valid=bool(value.get("transition_valid", False)),
639
+ delta_charge_match=bool(value.get("delta_charge_match", False)),
640
+ endpoint_match=bool(value.get("endpoint_match", False)),
641
+ absolute_lwg_valid=bool(value.get("absolute_lwg_valid", False)),
642
+ initial_absolute_residuals={
643
+ int(atom_map): residual
644
+ for atom_map, residual in value.get(
645
+ "initial_absolute_residuals", {}
646
+ ).items()
647
+ },
648
+ product_absolute_residuals={
649
+ int(atom_map): residual
650
+ for atom_map, residual in value.get(
651
+ "product_absolute_residuals", {}
652
+ ).items()
653
+ },
654
+ initial_global_electron_residual=value.get(
655
+ "initial_global_electron_residual", 0
656
+ ),
657
+ product_global_electron_residual=value.get(
658
+ "product_global_electron_residual", 0
659
+ ),
660
+ absolute_residuals_invariant=bool(
661
+ value.get("absolute_residuals_invariant", False)
662
+ ),
663
+ endpoint_resource_policy=value.get("endpoint_resource_policy", "strict"),
664
+ normalization_evidence=tuple(value.get("normalization_evidence", ())),
665
+ diagnostics=tuple(value.get("diagnostics", ())),
666
+ step_reports=tuple(value.get("step_reports", ())),
667
+ issues=tuple(
668
+ VerificationIssue.from_dict(issue) for issue in value.get("issues", ())
669
+ ),
670
+ final_match=value.get("final_match", {}),
671
+ repaired=bool(value.get("repaired", False)),
672
+ )
673
+
674
+
675
+ @dataclass(frozen=True)
676
+ class MechanismRecord:
677
+ """Versioned record for a supplied, mapped mechanism annotation."""
678
+
679
+ mapped_reaction: str
680
+ steps: tuple[MechanisticStep, ...]
681
+ schema_version: str = SCHEMA_VERSION
682
+ provenance: Mapping[str, Any] = field(default_factory=dict)
683
+ metadata: Mapping[str, Any] = field(default_factory=dict)
684
+ endpoint_stereo: Mapping[str, Mapping[str, StereoDescriptor]] = field(
685
+ default_factory=dict
686
+ )
687
+
688
+ def __post_init__(self) -> None:
689
+ if self.mapped_reaction.count(">>") != 1:
690
+ raise MechanismModelError("mapped_reaction must contain exactly one '>>'.")
691
+ if len({step.step_id for step in self.steps}) != len(self.steps):
692
+ raise MechanismModelError("Mechanistic step IDs must be unique.")
693
+ object.__setattr__(self, "provenance", dict(self.provenance))
694
+ object.__setattr__(self, "metadata", dict(self.metadata))
695
+ endpoint_stereo: dict[str, dict[str, StereoDescriptor]] = {}
696
+ for side, registry in self.endpoint_stereo.items():
697
+ if side not in {"reactant", "product"}:
698
+ raise MechanismModelError(
699
+ "Endpoint stereo side must be 'reactant' or 'product'."
700
+ )
701
+ endpoint_stereo[side] = {}
702
+ for key, descriptor in registry.items():
703
+ if not isinstance(descriptor, StereoDescriptor):
704
+ raise MechanismModelError(
705
+ "Endpoint stereo registries require StereoDescriptor values."
706
+ )
707
+ if descriptor.target_key != key:
708
+ raise MechanismModelError(
709
+ f"Endpoint descriptor belongs to {descriptor.target_key}, "
710
+ f"not {key}."
711
+ )
712
+ endpoint_stereo[side][key] = descriptor
713
+ object.__setattr__(self, "endpoint_stereo", endpoint_stereo)
714
+
715
+ @classmethod
716
+ def from_ef_smirks(cls, text: str, **kwargs: Any) -> "MechanismRecord":
717
+ from .adapters import mechanism_from_ef_smirks
718
+
719
+ return mechanism_from_ef_smirks(text, **kwargs)
720
+
721
+ def grammar_issues(self) -> tuple[VerificationIssue, ...]:
722
+ return tuple(
723
+ issue
724
+ for step in self.steps
725
+ for group in step.groups
726
+ for issue in group.issues()
727
+ )
728
+
729
+ def reversed(self) -> "MechanismRecord":
730
+ """Return the typed mechanism operating from product to reactant."""
731
+ reactants, products = self.mapped_reaction.split(">>", 1)
732
+ return MechanismRecord(
733
+ mapped_reaction=f"{products}>>{reactants}",
734
+ steps=tuple(step.reversed() for step in reversed(self.steps)),
735
+ schema_version=self.schema_version,
736
+ provenance=self.provenance,
737
+ metadata=self.metadata,
738
+ endpoint_stereo={
739
+ side: self.endpoint_stereo[other]
740
+ for side, other in (
741
+ ("reactant", "product"),
742
+ ("product", "reactant"),
743
+ )
744
+ if other in self.endpoint_stereo
745
+ },
746
+ )
747
+
748
+ def verify(
749
+ self,
750
+ *,
751
+ electron: Literal["strict", "diagnostic"] = "strict",
752
+ stereo: Literal["off", "endpoint", "stepwise"] = "off",
753
+ repair: bool = False,
754
+ ) -> VerificationCertificate:
755
+ """Replay this supplied mechanism and return its certificate."""
756
+ from .replay import MechanismReplayer
757
+
758
+ return (
759
+ MechanismReplayer(
760
+ validation=electron,
761
+ verify_stereo=stereo,
762
+ repair=repair,
763
+ )
764
+ .replay(self)
765
+ .certificate
766
+ )
767
+
768
+ def to_mtg(
769
+ self,
770
+ *,
771
+ electron: Literal["strict", "diagnostic"] = "strict",
772
+ stereo: Literal["off", "endpoint", "stepwise"] = "stepwise",
773
+ ) -> Any:
774
+ """Replay and return the mechanism trajectory graph."""
775
+ from .replay import MechanismReplayer
776
+
777
+ return (
778
+ MechanismReplayer(validation=electron, verify_stereo=stereo)
779
+ .replay(self)
780
+ .mtg
781
+ )
782
+
783
+ def to_rule(
784
+ self,
785
+ *,
786
+ core: bool = False,
787
+ implicit_h: bool = False,
788
+ name: str | None = None,
789
+ ) -> Any:
790
+ """Build a tuple :class:`SynRule` from the same mapped endpoints.
791
+
792
+ Explicit endpoint stereo sidecars replace the corresponding SMILES
793
+ registry, which keeps unknown stereo and backend-independent relative
794
+ descriptors available at the rule boundary. ``core=False`` is the
795
+ conservative default because it preserves the complete reviewed
796
+ endpoint context used for rule/replay agreement checks.
797
+ """
798
+ from synkit.Graph.Stereo import (
799
+ StereoChange,
800
+ classify_stereo_change,
801
+ stereo_from_dict,
802
+ )
803
+ from synkit.IO.chem_converter import rsmi_to_its
804
+ from synkit.Rule import SynRule
805
+
806
+ its = rsmi_to_its(
807
+ self.mapped_reaction,
808
+ core=core,
809
+ format="tuple",
810
+ drop_non_aam=False,
811
+ use_index_as_atom_map=True,
812
+ )
813
+ registries = {
814
+ side: dict(registry)
815
+ for side, registry in its.graph.get("stereo_descriptors", {}).items()
816
+ }
817
+ for side, registry in self.endpoint_stereo.items():
818
+ registries[side] = {
819
+ key: stereo_from_dict(descriptor.to_dict())
820
+ for key, descriptor in registry.items()
821
+ }
822
+ registries.setdefault("reactant", {})
823
+ registries.setdefault("product", {})
824
+ its.graph["stereo_descriptors"] = registries
825
+ its.graph["stereo_changes"] = {
826
+ key: StereoChange(
827
+ classify_stereo_change(
828
+ registries["reactant"].get(key),
829
+ registries["product"].get(key),
830
+ ),
831
+ registries["reactant"].get(key),
832
+ registries["product"].get(key),
833
+ )
834
+ for key in set(registries["reactant"]) | set(registries["product"])
835
+ }
836
+ return SynRule(
837
+ its,
838
+ name=name,
839
+ format="tuple",
840
+ implicit_h=implicit_h,
841
+ )
842
+
843
+ def draw(
844
+ self,
845
+ *,
846
+ show_certificate: bool = False,
847
+ certificate: VerificationCertificate | None = None,
848
+ path: str | Path | None = None,
849
+ ) -> Any:
850
+ """Create a headless static mechanism overview figure."""
851
+ from .drawing import draw_mechanism_record
852
+
853
+ if show_certificate and certificate is None:
854
+ certificate = self.verify(stereo="stepwise")
855
+ return draw_mechanism_record(self, certificate=certificate, path=path)
856
+
857
+ def to_json(self, path: str | Path | None = None, *, indent: int = 2) -> str:
858
+ """Serialize the versioned record and optionally write it to disk."""
859
+ payload = json.dumps(
860
+ self.to_dict(), ensure_ascii=False, indent=indent, sort_keys=True
861
+ )
862
+ if path is not None:
863
+ Path(path).write_text(payload + "\n", encoding="utf-8")
864
+ return payload
865
+
866
+ @classmethod
867
+ def from_json(cls, value: str | Path) -> "MechanismRecord":
868
+ """Read a JSON string or an existing JSON path."""
869
+ if isinstance(value, Path):
870
+ text = value.read_text(encoding="utf-8")
871
+ else:
872
+ stripped = value.lstrip()
873
+ if stripped.startswith(("{", "[")):
874
+ text = value
875
+ else:
876
+ candidate = Path(value)
877
+ text = (
878
+ candidate.read_text(encoding="utf-8")
879
+ if candidate.exists()
880
+ else value
881
+ )
882
+ return cls.from_dict(json.loads(text))
883
+
884
+ def to_dict(self) -> dict[str, Any]:
885
+ return {
886
+ "schema_version": self.schema_version,
887
+ "mapped_reaction": self.mapped_reaction,
888
+ "steps": [step.to_dict() for step in self.steps],
889
+ "provenance": dict(self.provenance),
890
+ "metadata": dict(self.metadata),
891
+ "endpoint_stereo": {
892
+ side: {
893
+ key: descriptor.to_dict() for key, descriptor in registry.items()
894
+ }
895
+ for side, registry in self.endpoint_stereo.items()
896
+ },
897
+ }
898
+
899
+ @classmethod
900
+ def from_dict(cls, value: Mapping[str, Any]) -> "MechanismRecord":
901
+ return cls(
902
+ mapped_reaction=value["mapped_reaction"],
903
+ steps=tuple(MechanisticStep.from_dict(step) for step in value["steps"]),
904
+ schema_version=value.get("schema_version", SCHEMA_VERSION),
905
+ provenance=value.get("provenance", {}),
906
+ metadata=value.get("metadata", {}),
907
+ endpoint_stereo={
908
+ side: {
909
+ key: StereoDescriptor.from_dict(descriptor)
910
+ for key, descriptor in registry.items()
911
+ }
912
+ for side, registry in value.get("endpoint_stereo", {}).items()
913
+ },
914
+ )
915
+
916
+
917
+ def issues_are_errors(issues: Iterable[VerificationIssue]) -> bool:
918
+ return any(issue.severity == "error" for issue in issues)