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,986 @@
1
+ """Atomic replay and formal verification of supplied electron-flow groups."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import Counter
6
+ from copy import deepcopy
7
+ from dataclasses import dataclass
8
+ from typing import Any
9
+
10
+ import networkx as nx
11
+
12
+ from synkit.Graph.Stereo import stereo_from_dict
13
+ from synkit.Graph.Mech.electron_accounting import (
14
+ recompute_charge,
15
+ refresh_electron_fields,
16
+ )
17
+ from synkit.Graph.Mech.lwg_ops import normalize_lwg_graph
18
+ from synkit.IO.chem_converter import DEFAULT_NODE_ATTRS, smiles_to_graph
19
+
20
+ from .audit import audit_local_electron_state
21
+ from .model import (
22
+ ElectronLocus,
23
+ ElectronMoveGroup,
24
+ MechanismRecord,
25
+ VerificationCertificate,
26
+ VerificationIssue,
27
+ StereoDescriptor,
28
+ )
29
+ from .stereo_state import (
30
+ StereoStateTimeline,
31
+ apply_stereo_effects,
32
+ descriptor_support_errors,
33
+ stereo_timeline,
34
+ )
35
+ from .symbols import LONE_PAIR, PI, RADICAL, SIGMA
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class GroupReplayReport:
40
+ step_id: str
41
+ group_id: str
42
+ status: str
43
+ changed_atom_maps: tuple[int, ...]
44
+ issues: tuple[VerificationIssue, ...] = ()
45
+ charge_updates: tuple[dict[str, Any], ...] = ()
46
+
47
+ def to_dict(self) -> dict[str, Any]:
48
+ return {
49
+ "step_id": self.step_id,
50
+ "group_id": self.group_id,
51
+ "status": self.status,
52
+ "changed_atom_maps": list(self.changed_atom_maps),
53
+ "issues": [issue.to_dict() for issue in self.issues],
54
+ "charge_updates": [dict(update) for update in self.charge_updates],
55
+ }
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class MechanismReplayResult:
60
+ final_graph: nx.Graph
61
+ intermediates: tuple[nx.Graph, ...]
62
+ certificate: VerificationCertificate
63
+ mtg: nx.DiGraph
64
+
65
+ @property
66
+ def stereo_timeline(self) -> StereoStateTimeline:
67
+ """Return the typed proof carried by the replay MTG."""
68
+ return self.mtg.graph["stereo_timeline_proof"]
69
+
70
+
71
+ class MechanismReplayer:
72
+ """Replay simultaneous groups against one common pre-state per group.
73
+
74
+ Formal charges are propagated from committed sigma, pi, lone-pair, and
75
+ radical deltas. Absolute Lewis-state residuals are retained as an
76
+ independent certificate claim. Endpoint resource comparison is strict by
77
+ default; ``closed_shell_pair`` is an explicit, atom-selected comparison
78
+ policy for a two-radical/one-lone-pair serialization boundary.
79
+ """
80
+
81
+ def __init__(
82
+ self,
83
+ *,
84
+ validation: str = "strict",
85
+ aromatic_policy: str = "presentation",
86
+ repair: bool = False,
87
+ verify_stereo: str = "off",
88
+ endpoint_resource_policy: str | None = None,
89
+ closed_shell_atom_maps: set[int] | tuple[int, ...] | list[int] | None = None,
90
+ ) -> None:
91
+ if validation not in {"strict", "diagnostic"}:
92
+ raise ValueError("validation must be 'strict' or 'diagnostic'.")
93
+ if aromatic_policy not in {"kekule", "presentation"}:
94
+ raise ValueError("Unsupported aromatic policy.")
95
+ self.validation = validation
96
+ self.aromatic_policy = aromatic_policy
97
+ self.repair = repair
98
+ if verify_stereo not in {"off", "endpoint", "stepwise"}:
99
+ raise ValueError("verify_stereo must be 'off', 'endpoint', or 'stepwise'.")
100
+ self.verify_stereo = verify_stereo
101
+ if endpoint_resource_policy not in {None, "strict", "closed_shell_pair"}:
102
+ raise ValueError("Unsupported endpoint resource policy.")
103
+ self.endpoint_resource_policy = endpoint_resource_policy
104
+ self.closed_shell_atom_maps = frozenset(
105
+ int(atom_map) for atom_map in (closed_shell_atom_maps or ())
106
+ )
107
+ if any(atom_map <= 0 for atom_map in self.closed_shell_atom_maps):
108
+ raise ValueError("Closed-shell atom maps must be positive integers.")
109
+
110
+ def replay(self, record: MechanismRecord) -> MechanismReplayResult: # noqa: C901
111
+ reactants, products = record.mapped_reaction.split(">>", 1)
112
+ current = self._parse_side(reactants)
113
+ expected = self._parse_side(products)
114
+ self._seed_mechanism_stereo(current)
115
+ self._seed_mechanism_stereo(expected)
116
+ self._select_endpoint_stereo(current, record, side="reactant")
117
+ self._select_endpoint_stereo(expected, record, side="product")
118
+ endpoint_policy, closed_shell_atom_maps = self._resolve_endpoint_policy(record)
119
+ intermediates: list[nx.Graph] = []
120
+ reports: list[GroupReplayReport] = []
121
+ canonical_neighbor_changes: list[dict[str, Any]] = []
122
+ issues: list[VerificationIssue] = list(record.grammar_issues())
123
+ issues.extend(self._graph_mapping_issues(current, side="reactant"))
124
+ issues.extend(self._graph_mapping_issues(expected, side="product"))
125
+ initial_absolute_audit = audit_local_electron_state(current)
126
+ product_absolute_audit = audit_local_electron_state(expected)
127
+ issues.extend(self._blocking_audit_issues(initial_absolute_audit.issues))
128
+ issues.extend(self._blocking_audit_issues(product_absolute_audit.issues))
129
+ issues.extend(
130
+ self._endpoint_policy_issues(
131
+ current,
132
+ expected,
133
+ policy=endpoint_policy,
134
+ atom_maps=closed_shell_atom_maps,
135
+ )
136
+ )
137
+ issues.extend(self._endpoint_stereo_issues(current, side="reactant"))
138
+ issues.extend(self._endpoint_stereo_issues(expected, side="product"))
139
+ initial_absolute = self._absolute_state(current)
140
+ product_absolute = self._absolute_state(expected)
141
+ committed_absolute_states = [initial_absolute]
142
+ mtg = nx.DiGraph(schema="synkit-mtg-2.0-draft1")
143
+ mtg.add_node(0, graph=deepcopy(current), status="INITIAL")
144
+
145
+ if not issues:
146
+ state_index = 0
147
+ for step in record.steps:
148
+ step_start = deepcopy(current)
149
+ step_state_index = state_index
150
+ step_intermediate_count = len(intermediates)
151
+ step_failed = False
152
+ for group in step.groups:
153
+ next_graph, group_report = self._apply_group(
154
+ current, group, step_id=step.step_id
155
+ )
156
+ reports.append(group_report)
157
+ issues.extend(group_report.issues)
158
+ if group_report.status != "VALID":
159
+ if self.validation == "strict":
160
+ step_failed = True
161
+ break
162
+ continue
163
+ current = next_graph
164
+ committed_absolute_states.append(self._absolute_state(current))
165
+ intermediates.append(deepcopy(current))
166
+ state_index += 1
167
+ mtg.add_node(state_index, graph=deepcopy(current), status="VALID")
168
+ mtg.add_edge(
169
+ state_index - 1,
170
+ state_index,
171
+ step_id=step.step_id,
172
+ group_id=group.group_id,
173
+ macro=group.macro,
174
+ event_signature=group.canonical_signature(),
175
+ )
176
+ if not step_failed:
177
+ motion_evidence = tuple(
178
+ motion.verification_evidence(
179
+ step_id=step.step_id,
180
+ before_graph=step_start,
181
+ graph=current,
182
+ )
183
+ for motion in step.stereo_motions
184
+ )
185
+ step_neighbor_changes = [
186
+ change.to_dict()
187
+ for changes, _motion_issues in motion_evidence
188
+ for change in changes
189
+ ]
190
+ motion_issues = tuple(
191
+ issue
192
+ for _changes, result_issues in motion_evidence
193
+ for issue in result_issues
194
+ )
195
+ if self.verify_stereo == "stepwise":
196
+ issues.extend(motion_issues)
197
+ if motion_issues and self.validation == "strict":
198
+ step_failed = True
199
+ before_stereo = dict(
200
+ current.graph.get("mechanism_stereo_descriptors", {})
201
+ )
202
+ current, stereo_issues = apply_stereo_effects(
203
+ current,
204
+ step.stereo_effects,
205
+ step_id=step.step_id,
206
+ )
207
+ if self.verify_stereo == "stepwise":
208
+ issues.extend(stereo_issues)
209
+ if stereo_issues and self.validation == "strict":
210
+ step_failed = True
211
+ after_stereo = current.graph.get("mechanism_stereo_descriptors", {})
212
+ if not step_failed:
213
+ canonical_neighbor_changes.extend(step_neighbor_changes)
214
+ if not step_failed and state_index > step_state_index:
215
+ intermediates[-1] = deepcopy(current)
216
+ mtg.nodes[state_index]["graph"] = deepcopy(current)
217
+ if step.stereo_effects:
218
+ mtg.edges[state_index - 1, state_index][
219
+ "stereo_effects"
220
+ ] = [effect.to_dict() for effect in step.stereo_effects]
221
+ if step.stereo_motions:
222
+ mtg.edges[state_index - 1, state_index][
223
+ "stereo_motions"
224
+ ] = [motion.to_dict() for motion in step.stereo_motions]
225
+ if step_neighbor_changes:
226
+ mtg.edges[state_index - 1, state_index][
227
+ "canonical_neighbor_changes"
228
+ ] = step_neighbor_changes
229
+ elif not step_failed and (
230
+ step.stereo_effects
231
+ or step.stereo_motions
232
+ or before_stereo != after_stereo
233
+ ):
234
+ intermediates.append(deepcopy(current))
235
+ state_index += 1
236
+ mtg.add_node(
237
+ state_index, graph=deepcopy(current), status="VALID"
238
+ )
239
+ mtg.add_edge(
240
+ state_index - 1,
241
+ state_index,
242
+ step_id=step.step_id,
243
+ group_id=None,
244
+ macro=None,
245
+ event_signature=(),
246
+ stereo_effects=[
247
+ effect.to_dict() for effect in step.stereo_effects
248
+ ],
249
+ stereo_motions=[
250
+ motion.to_dict() for motion in step.stereo_motions
251
+ ],
252
+ canonical_neighbor_changes=step_neighbor_changes,
253
+ )
254
+ if step_failed and self.validation == "strict":
255
+ current = step_start
256
+ del intermediates[step_intermediate_count:]
257
+ mtg.remove_nodes_from(
258
+ node for node in tuple(mtg.nodes) if node > step_state_index
259
+ )
260
+ state_index = step_state_index
261
+ break
262
+
263
+ final_match = self._compare_graphs(
264
+ current,
265
+ expected,
266
+ include_stereo=self.verify_stereo in {"endpoint", "stepwise"},
267
+ endpoint_resource_policy=endpoint_policy,
268
+ closed_shell_atom_maps=closed_shell_atom_maps,
269
+ )
270
+ final_match["stereo_verification"] = self.verify_stereo
271
+ final_match["stereo_verification_performed"] = self.verify_stereo != "off"
272
+ final_match["canonical_neighbor_changes"] = canonical_neighbor_changes
273
+ transition_valid = not any(issue.severity == "error" for issue in issues)
274
+ observed_charges = self._mapped_charges(current)
275
+ expected_charges = self._mapped_charges(expected)
276
+ delta_charge_match = observed_charges == expected_charges
277
+ residuals_invariant = all(
278
+ state == initial_absolute
279
+ for state in (*committed_absolute_states[1:], product_absolute)
280
+ )
281
+ if transition_valid and not delta_charge_match:
282
+ issues.append(
283
+ VerificationIssue(
284
+ "DELTA_CHARGE_MISMATCH",
285
+ "Delta-derived final charges do not match the mapped product.",
286
+ atom_maps=tuple(
287
+ sorted(
288
+ atom_map
289
+ for atom_map in set(observed_charges) | set(expected_charges)
290
+ if observed_charges.get(atom_map)
291
+ != expected_charges.get(atom_map)
292
+ )
293
+ ),
294
+ expected=expected_charges,
295
+ observed=observed_charges,
296
+ )
297
+ )
298
+ if transition_valid and not residuals_invariant:
299
+ issues.append(
300
+ VerificationIssue(
301
+ "ABSOLUTE_RESIDUAL_CHANGED",
302
+ "Absolute Lewis residuals changed across delta replay.",
303
+ expected=initial_absolute,
304
+ observed={
305
+ "committed_states": committed_absolute_states[1:],
306
+ "product": product_absolute,
307
+ },
308
+ )
309
+ )
310
+ if transition_valid and not final_match["matches"]:
311
+ issues.append(
312
+ VerificationIssue(
313
+ "FINAL_PRODUCT_MISMATCH",
314
+ "Replayed Lewis state does not match the supplied mapped product.",
315
+ expected=final_match["expected"],
316
+ observed=final_match["observed"],
317
+ )
318
+ )
319
+ status = (
320
+ "VALID"
321
+ if not any(issue.severity == "error" for issue in issues)
322
+ else "INVALID"
323
+ )
324
+ absolute_lwg_valid = initial_absolute_audit.valid and product_absolute_audit.valid
325
+ normalization_required = bool(
326
+ final_match.get("matches")
327
+ and not final_match.get("raw_matches", final_match["matches"])
328
+ and final_match.get("normalization_evidence")
329
+ )
330
+ if status == "INVALID":
331
+ verification_level = "INVALID"
332
+ elif normalization_required:
333
+ verification_level = "NORMALIZED"
334
+ elif absolute_lwg_valid:
335
+ verification_level = "EXACT"
336
+ else:
337
+ verification_level = "DELTA_CONSISTENT"
338
+ diagnostics = []
339
+ if not absolute_lwg_valid:
340
+ diagnostics.append("ABSOLUTE_LEWIS_STATE_INCOMPLETE")
341
+ if normalization_required:
342
+ diagnostics.append("ENDPOINT_RESOURCE_NORMALIZED")
343
+ certificate = VerificationCertificate(
344
+ status=status,
345
+ verification_level=verification_level,
346
+ transition_valid=transition_valid,
347
+ delta_charge_match=delta_charge_match,
348
+ endpoint_match=bool(final_match["matches"]),
349
+ absolute_lwg_valid=absolute_lwg_valid,
350
+ initial_absolute_residuals=initial_absolute["atom_residuals"],
351
+ product_absolute_residuals=product_absolute["atom_residuals"],
352
+ initial_global_electron_residual=initial_absolute["global_residual"],
353
+ product_global_electron_residual=product_absolute["global_residual"],
354
+ absolute_residuals_invariant=residuals_invariant,
355
+ endpoint_resource_policy=endpoint_policy,
356
+ normalization_evidence=tuple(final_match.get("normalization_evidence", ())),
357
+ diagnostics=tuple(diagnostics),
358
+ step_reports=tuple(report.to_dict() for report in reports),
359
+ issues=tuple(issues),
360
+ final_match=final_match,
361
+ repaired=self.repair
362
+ and any(issue.code.endswith("_REPAIRED") for issue in issues),
363
+ )
364
+ ordered_states = [mtg.nodes[node]["graph"] for node in sorted(mtg.nodes)]
365
+ mtg.graph["stereo_timeline"] = stereo_timeline(ordered_states)
366
+ mtg.graph["verify_stereo"] = self.verify_stereo
367
+ mtg.graph["stereo_timeline_proof"] = StereoStateTimeline.create(
368
+ ordered_states,
369
+ verification_mode=self.verify_stereo,
370
+ )
371
+ return MechanismReplayResult(current, tuple(intermediates), certificate, mtg)
372
+
373
+ def _apply_group(
374
+ self, pre_state: nx.Graph, group: ElectronMoveGroup, *, step_id: str
375
+ ) -> tuple[nx.Graph, GroupReplayReport]:
376
+ issues = [
377
+ VerificationIssue(
378
+ issue.code,
379
+ issue.message,
380
+ issue.severity,
381
+ step_id=step_id,
382
+ group_id=group.group_id,
383
+ atom_maps=issue.atom_maps,
384
+ expected=issue.expected,
385
+ observed=issue.observed,
386
+ )
387
+ for issue in group.validate_pre_state(pre_state)
388
+ ]
389
+ changed = tuple(
390
+ sorted(
391
+ {
392
+ atom_map
393
+ for move in group.moves
394
+ for locus in (move.source, move.target)
395
+ for atom_map in locus.atom_maps
396
+ }
397
+ )
398
+ )
399
+ if issues:
400
+ return pre_state, GroupReplayReport(
401
+ step_id, group.group_id, "INVALID", changed, tuple(issues)
402
+ )
403
+
404
+ graph = deepcopy(pre_state)
405
+ deltas: Counter[ElectronLocus] = Counter()
406
+ for move in group.moves:
407
+ if (
408
+ group.macro == "LONE_PAIR_RADICAL_RELOCATION"
409
+ and move.electron_count == 1
410
+ and move.source.kind == LONE_PAIR
411
+ and move.target.kind == LONE_PAIR
412
+ ):
413
+ # A fishhook can split one donor lone pair and pair with one
414
+ # acceptor radical. Pair/ radical resources must remain whole
415
+ # at the committed boundary even though only one electron
416
+ # moves between atoms.
417
+ deltas[move.source] -= 2
418
+ deltas[ElectronLocus(RADICAL, move.source.atom_maps)] += 1
419
+ deltas[ElectronLocus(RADICAL, move.target.atom_maps)] -= 1
420
+ deltas[move.target] += 2
421
+ else:
422
+ deltas[move.source] -= move.electron_count
423
+ deltas[move.target] += move.electron_count
424
+
425
+ try:
426
+ lookup = self._atom_map_lookup(graph)
427
+ charge_deltas = self._charge_deltas(deltas)
428
+ charge_updates = self._commit_charge_deltas(
429
+ graph,
430
+ lookup,
431
+ charge_deltas,
432
+ )
433
+ self._commit_bond_deltas(graph, lookup, deltas)
434
+ self._commit_atom_deltas(graph, lookup, deltas)
435
+ refresh_electron_fields(graph, in_place=True)
436
+ electron_audit = audit_local_electron_state(graph)
437
+ issues.extend(self._blocking_audit_issues(electron_audit.issues))
438
+ except (KeyError, ValueError) as exc:
439
+ charge_updates = ()
440
+ issues.append(
441
+ VerificationIssue(
442
+ "UNBALANCED_EVENT_GROUP",
443
+ str(exc),
444
+ step_id=step_id,
445
+ group_id=group.group_id,
446
+ atom_maps=changed,
447
+ )
448
+ )
449
+
450
+ status = (
451
+ "INVALID" if any(issue.severity == "error" for issue in issues) else "VALID"
452
+ )
453
+ return graph if status == "VALID" else pre_state, GroupReplayReport(
454
+ step_id,
455
+ group.group_id,
456
+ status,
457
+ changed,
458
+ tuple(issues),
459
+ tuple(charge_updates),
460
+ )
461
+
462
+ @staticmethod
463
+ def _charge_deltas(
464
+ deltas: Counter[ElectronLocus],
465
+ ) -> Counter[int]:
466
+ """Derive formal-charge deltas from committed electron resources."""
467
+ result: Counter[int] = Counter()
468
+ for locus, electron_delta in deltas.items():
469
+ if electron_delta == 0:
470
+ continue
471
+ if locus.kind in {SIGMA, PI}:
472
+ charge_delta = -electron_delta / 2
473
+ for atom_map in locus.atom_maps:
474
+ result[atom_map] += charge_delta
475
+ elif locus.kind in {LONE_PAIR, RADICAL}:
476
+ result[locus.atom_maps[0]] -= electron_delta
477
+ return result
478
+
479
+ @staticmethod
480
+ def _commit_charge_deltas(
481
+ graph: nx.Graph,
482
+ lookup: dict[int, Any],
483
+ charge_deltas: Counter[int],
484
+ ) -> tuple[dict[str, Any], ...]:
485
+ """Apply delta-authoritative charges without an absolute Lewis rewrite."""
486
+ updates = []
487
+ for atom_map in sorted(charge_deltas):
488
+ delta = charge_deltas[atom_map]
489
+ if isinstance(delta, float) and delta.is_integer():
490
+ delta = int(delta)
491
+ node = lookup[atom_map]
492
+ before = graph.nodes[node].get("charge", 0)
493
+ after = before + delta
494
+ if isinstance(after, float) and after.is_integer():
495
+ after = int(after)
496
+ graph.nodes[node]["charge"] = after
497
+ updates.append(
498
+ {
499
+ "atom_map": atom_map,
500
+ "before": before,
501
+ "delta": delta,
502
+ "after": after,
503
+ }
504
+ )
505
+ return tuple(updates)
506
+
507
+ @staticmethod
508
+ def _atom_map_lookup(graph: nx.Graph) -> dict[int, Any]:
509
+ lookup: dict[int, Any] = {}
510
+ for node, attrs in graph.nodes(data=True):
511
+ atom_map = int(attrs.get("atom_map", 0) or 0)
512
+ if atom_map <= 0:
513
+ continue
514
+ if atom_map in lookup:
515
+ raise ValueError(f"Duplicate atom map {atom_map}.")
516
+ lookup[atom_map] = node
517
+ return lookup
518
+
519
+ @staticmethod
520
+ def _graph_mapping_issues(
521
+ graph: nx.Graph,
522
+ *,
523
+ side: str,
524
+ ) -> tuple[VerificationIssue, ...]:
525
+ """Require complete, unique AAM for formal mapped endpoint replay."""
526
+ issues: list[VerificationIssue] = []
527
+ seen: dict[int, Any] = {}
528
+ for node, attrs in graph.nodes(data=True):
529
+ atom_map = attrs.get("atom_map", 0)
530
+ if type(atom_map) is not int or atom_map <= 0:
531
+ issues.append(
532
+ VerificationIssue(
533
+ "MISSING_ATOM_MAP",
534
+ f"Every {side} atom requires a positive atom map.",
535
+ observed={"node": node, "atom_map": atom_map},
536
+ )
537
+ )
538
+ continue
539
+ if atom_map in seen:
540
+ issues.append(
541
+ VerificationIssue(
542
+ "DUPLICATE_ATOM_MAP",
543
+ f"The {side} graph contains duplicate atom map {atom_map}.",
544
+ atom_maps=(atom_map,),
545
+ observed=(seen[atom_map], node),
546
+ )
547
+ )
548
+ else:
549
+ seen[atom_map] = node
550
+ return tuple(issues)
551
+
552
+ @staticmethod
553
+ def _commit_bond_deltas(
554
+ graph: nx.Graph,
555
+ lookup: dict[int, Any],
556
+ deltas: Counter[ElectronLocus],
557
+ ) -> None:
558
+ for locus, electron_delta in deltas.items():
559
+ if locus.kind not in {SIGMA, PI} or electron_delta == 0:
560
+ continue
561
+ left, right = (lookup[atom_map] for atom_map in locus.atom_maps)
562
+ if not graph.has_edge(left, right):
563
+ if electron_delta < 0:
564
+ raise ValueError(f"Cannot consume absent {locus.kind} bond.")
565
+ graph.add_edge(
566
+ left,
567
+ right,
568
+ order=0.0,
569
+ kekule_order=0.0,
570
+ sigma_order=0.0,
571
+ pi_order=0.0,
572
+ )
573
+ field = "sigma_order" if locus.kind == SIGMA else "pi_order"
574
+ current_electrons = 2 * float(graph.edges[left, right].get(field, 0.0))
575
+ new_electrons = current_electrons + electron_delta
576
+ if new_electrons < 0 or abs(new_electrons % 2) > 1e-9:
577
+ raise ValueError(
578
+ f"Committed {locus.kind} resource is not a whole electron pair."
579
+ )
580
+ graph.edges[left, right][field] = new_electrons / 2
581
+
582
+ for left, right in list(graph.edges):
583
+ edge = graph.edges[left, right]
584
+ sigma = float(edge.get("sigma_order", 0.0))
585
+ pi = float(edge.get("pi_order", 0.0))
586
+ if sigma == 0 and pi == 0:
587
+ graph.remove_edge(left, right)
588
+ else:
589
+ edge["kekule_order"] = sigma + pi
590
+ edge["order"] = sigma + pi
591
+
592
+ @staticmethod
593
+ def _commit_atom_deltas(
594
+ graph: nx.Graph,
595
+ lookup: dict[int, Any],
596
+ deltas: Counter[ElectronLocus],
597
+ ) -> None:
598
+ per_atom: dict[int, Counter[str]] = {}
599
+ for locus, electron_delta in deltas.items():
600
+ if locus.kind not in {LONE_PAIR, RADICAL} or electron_delta == 0:
601
+ continue
602
+ per_atom.setdefault(locus.atom_maps[0], Counter())[
603
+ locus.kind
604
+ ] += electron_delta
605
+ for atom_map, atom_deltas in per_atom.items():
606
+ attrs = graph.nodes[lookup[atom_map]]
607
+ lp_electrons = 2 * int(attrs.get("lone_pairs", 0)) + atom_deltas[LONE_PAIR]
608
+ radicals = int(attrs.get("radical", 0)) + atom_deltas[RADICAL]
609
+ if lp_electrons < 0 or lp_electrons % 2:
610
+ raise ValueError("Lone-pair resource must commit as complete pairs.")
611
+ if radicals < 0:
612
+ raise ValueError("Committed radical resource cannot be negative.")
613
+ attrs["lone_pairs"] = lp_electrons // 2
614
+ attrs["radical"] = radicals
615
+
616
+ def _compare_graphs(
617
+ self,
618
+ observed: nx.Graph,
619
+ expected: nx.Graph,
620
+ *,
621
+ include_stereo: bool = False,
622
+ endpoint_resource_policy: str | None = None,
623
+ closed_shell_atom_maps: frozenset[int] | None = None,
624
+ ) -> dict[str, Any]:
625
+ presentation_edges: set[frozenset[int]] | None = None
626
+ if self.aromatic_policy == "presentation":
627
+ presentation_edges = self._mapped_aromatic_edges(
628
+ observed
629
+ ) | self._mapped_aromatic_edges(expected)
630
+ raw_observed_sig = self._graph_signature(
631
+ observed,
632
+ include_stereo=include_stereo,
633
+ presentation_edges=presentation_edges,
634
+ )
635
+ raw_expected_sig = self._graph_signature(
636
+ expected,
637
+ include_stereo=include_stereo,
638
+ presentation_edges=presentation_edges,
639
+ )
640
+ policy = endpoint_resource_policy or self.endpoint_resource_policy or "strict"
641
+ selected_maps = (
642
+ self.closed_shell_atom_maps
643
+ if closed_shell_atom_maps is None
644
+ else closed_shell_atom_maps
645
+ )
646
+ normalization_evidence: tuple[dict[str, Any], ...] = ()
647
+ observed_sig = raw_observed_sig
648
+ expected_sig = raw_expected_sig
649
+ if policy == "closed_shell_pair":
650
+ observed_working, observed_evidence = self._closed_shell_pair_copy(
651
+ observed,
652
+ selected_maps,
653
+ side="observed",
654
+ )
655
+ expected_working, expected_evidence = self._closed_shell_pair_copy(
656
+ expected,
657
+ selected_maps,
658
+ side="expected",
659
+ )
660
+ normalization_evidence = (*observed_evidence, *expected_evidence)
661
+ observed_sig = self._graph_signature(
662
+ observed_working,
663
+ include_stereo=include_stereo,
664
+ presentation_edges=presentation_edges,
665
+ )
666
+ expected_sig = self._graph_signature(
667
+ expected_working,
668
+ include_stereo=include_stereo,
669
+ presentation_edges=presentation_edges,
670
+ )
671
+ return {
672
+ "matches": observed_sig == expected_sig,
673
+ "raw_matches": raw_observed_sig == raw_expected_sig,
674
+ "observed": observed_sig,
675
+ "expected": expected_sig,
676
+ "raw_observed": raw_observed_sig,
677
+ "raw_expected": raw_expected_sig,
678
+ "aromatic_policy": self.aromatic_policy,
679
+ "endpoint_resource_policy": policy,
680
+ "normalization_evidence": list(normalization_evidence),
681
+ }
682
+
683
+ @classmethod
684
+ def _closed_shell_pair_copy(
685
+ cls,
686
+ graph: nx.Graph,
687
+ atom_maps: frozenset[int],
688
+ *,
689
+ side: str,
690
+ ) -> tuple[nx.Graph, tuple[dict[str, Any], ...]]:
691
+ """Normalize explicitly selected diradical scalars on a graph copy."""
692
+ working = deepcopy(graph)
693
+ lookup = cls._atom_map_lookup(working)
694
+ evidence = []
695
+ for atom_map in sorted(atom_maps):
696
+ node = lookup.get(atom_map)
697
+ if node is None:
698
+ continue
699
+ attrs = working.nodes[node]
700
+ if int(attrs.get("radical", 0)) != 2:
701
+ continue
702
+ before = {
703
+ "charge": attrs.get("charge", 0),
704
+ "lone_pairs": int(attrs.get("lone_pairs", 0)),
705
+ "radical": 2,
706
+ }
707
+ attrs["lone_pairs"] = before["lone_pairs"] + 1
708
+ attrs["radical"] = 0
709
+ after = {
710
+ "charge": attrs.get("charge", 0),
711
+ "lone_pairs": attrs["lone_pairs"],
712
+ "radical": 0,
713
+ }
714
+ evidence.append(
715
+ {
716
+ "atom_map": atom_map,
717
+ "element": str(attrs.get("element", "*")),
718
+ "side": side,
719
+ "before": before,
720
+ "after": after,
721
+ "policy_name": "closed_shell_pair",
722
+ "policy_version": "1.0",
723
+ }
724
+ )
725
+ return working, tuple(evidence)
726
+
727
+ def _resolve_endpoint_policy(
728
+ self,
729
+ record: MechanismRecord,
730
+ ) -> tuple[str, frozenset[int]]:
731
+ policy = self.endpoint_resource_policy
732
+ atom_maps = self.closed_shell_atom_maps
733
+ if policy is None:
734
+ policy = str(record.metadata.get("endpoint_resource_policy", "strict"))
735
+ atom_maps = frozenset(
736
+ int(atom_map)
737
+ for atom_map in record.metadata.get("closed_shell_atom_maps", atom_maps)
738
+ )
739
+ if policy not in {"strict", "closed_shell_pair"}:
740
+ raise ValueError(f"Unsupported endpoint resource policy: {policy!r}.")
741
+ return policy, atom_maps
742
+
743
+ @classmethod
744
+ def _endpoint_policy_issues(
745
+ cls,
746
+ reactant: nx.Graph,
747
+ product: nx.Graph,
748
+ *,
749
+ policy: str,
750
+ atom_maps: frozenset[int],
751
+ ) -> tuple[VerificationIssue, ...]:
752
+ if policy != "closed_shell_pair":
753
+ return ()
754
+ if not atom_maps:
755
+ return (
756
+ VerificationIssue(
757
+ "CLOSED_SHELL_ATOM_MAPS_REQUIRED",
758
+ "The closed-shell-pair policy requires explicit mapped atoms.",
759
+ ),
760
+ )
761
+ reactant_maps = set(cls._atom_map_lookup(reactant))
762
+ product_maps = set(cls._atom_map_lookup(product))
763
+ missing = atom_maps - (reactant_maps & product_maps)
764
+ if not missing:
765
+ return ()
766
+ return (
767
+ VerificationIssue(
768
+ "CLOSED_SHELL_ATOM_MAP_MISSING",
769
+ "Every selected closed-shell atom must occur on both endpoints.",
770
+ atom_maps=tuple(sorted(missing)),
771
+ ),
772
+ )
773
+
774
+ @staticmethod
775
+ def _blocking_audit_issues(
776
+ issues: tuple[VerificationIssue, ...],
777
+ ) -> tuple[VerificationIssue, ...]:
778
+ """Keep malformed resources blocking while absolute mismatch is diagnostic."""
779
+ absolute_codes = {
780
+ "LOCAL_ELECTRON_MISMATCH",
781
+ "GLOBAL_ELECTRON_MISMATCH",
782
+ }
783
+ return tuple(issue for issue in issues if issue.code not in absolute_codes)
784
+
785
+ @staticmethod
786
+ def _number(value: float) -> int | float:
787
+ return int(value) if float(value).is_integer() else value
788
+
789
+ @classmethod
790
+ def _absolute_state(cls, graph: nx.Graph) -> dict[str, Any]:
791
+ atom_residuals = {}
792
+ expected_total = 0.0
793
+ represented_total = 0.0
794
+ for node, attrs in graph.nodes(data=True):
795
+ atom_map = int(attrs.get("atom_map", 0) or 0)
796
+ if "valence_electrons" in attrs:
797
+ residual = float(attrs.get("charge", 0)) - float(
798
+ recompute_charge(graph, node)
799
+ )
800
+ if residual:
801
+ atom_residuals[atom_map] = cls._number(residual)
802
+ expected_total += (
803
+ float(attrs["valence_electrons"])
804
+ + float(attrs.get("hcount", 0))
805
+ - float(attrs.get("charge", 0))
806
+ )
807
+ represented_total += (
808
+ 2 * float(attrs.get("lone_pairs", 0))
809
+ + float(attrs.get("radical", 0))
810
+ + 2 * float(attrs.get("hcount", 0))
811
+ )
812
+ represented_total += 2 * sum(
813
+ float(attrs.get("sigma_order", 0.0))
814
+ + float(attrs.get("pi_order", 0.0))
815
+ for _, _, attrs in graph.edges(data=True)
816
+ )
817
+ return {
818
+ "atom_residuals": atom_residuals,
819
+ "global_residual": cls._number(represented_total - expected_total),
820
+ }
821
+
822
+ @staticmethod
823
+ def _mapped_charges(graph: nx.Graph) -> dict[int, int | float]:
824
+ return {
825
+ int(attrs["atom_map"]): attrs.get("charge", 0)
826
+ for _, attrs in graph.nodes(data=True)
827
+ if int(attrs.get("atom_map", 0) or 0) > 0
828
+ }
829
+
830
+ def _graph_signature(
831
+ self,
832
+ graph: nx.Graph,
833
+ *,
834
+ include_stereo: bool = False,
835
+ presentation_edges: set[frozenset[int]] | None = None,
836
+ ) -> dict[str, Any]:
837
+ # ``normalize_lwg_graph`` deliberately replaces the stable aromatic
838
+ # presentation order (1.5) with the editable Kekule sigma/pi order.
839
+ # Remember which input edges were aromatic before normalization so
840
+ # presentation comparison remains independent of the arbitrary
841
+ # alternating Kekule phase selected by RDKit.
842
+ aromatic_edges = {
843
+ frozenset((left, right))
844
+ for left, right, attrs in graph.edges(data=True)
845
+ if attrs.get("aromatic") is True or attrs.get("order") == 1.5
846
+ }
847
+ graph = normalize_lwg_graph(graph)
848
+ lookup: dict[int, Any] = {}
849
+ node_keys: dict[Any, Any] = {}
850
+ duplicate_counts: Counter[int] = Counter()
851
+ for node, attrs in graph.nodes(data=True):
852
+ atom_map = attrs.get("atom_map", 0)
853
+ if type(atom_map) is int and atom_map > 0 and atom_map not in lookup:
854
+ lookup[atom_map] = node
855
+ node_keys[node] = atom_map
856
+ elif type(atom_map) is int and atom_map > 0:
857
+ duplicate_counts[atom_map] += 1
858
+ node_keys[node] = f"duplicate:{atom_map}:{duplicate_counts[atom_map]}"
859
+ else:
860
+ node_keys[node] = f"unmapped:{node}"
861
+ nodes = {
862
+ node_keys[node]: (
863
+ str(graph.nodes[node].get("element", "*")),
864
+ int(graph.nodes[node].get("isotope", 0) or 0),
865
+ int(graph.nodes[node].get("charge", 0)),
866
+ int(graph.nodes[node].get("radical", 0)),
867
+ int(graph.nodes[node].get("hcount", 0)),
868
+ int(graph.nodes[node].get("lone_pairs", 0)),
869
+ int(graph.nodes[node].get("valence_electrons", 0)),
870
+ )
871
+ for node in graph.nodes
872
+ }
873
+ edges = {}
874
+ for left, right, attrs in graph.edges(data=True):
875
+ endpoints = tuple(sorted((node_keys[left], node_keys[right]), key=repr))
876
+ key = f"{endpoints[0]!r}|{endpoints[1]!r}"
877
+ mapped_key = (
878
+ frozenset((node_keys[left], node_keys[right]))
879
+ if all(type(node_keys[node]) is int for node in (left, right))
880
+ else None
881
+ )
882
+ if self.aromatic_policy == "presentation" and (
883
+ frozenset((left, right)) in aromatic_edges
884
+ or (
885
+ mapped_key is not None
886
+ and presentation_edges is not None
887
+ and mapped_key in presentation_edges
888
+ )
889
+ ):
890
+ edges[key] = ("aromatic",)
891
+ else:
892
+ edges[key] = (
893
+ float(attrs.get("sigma_order", 0.0)),
894
+ float(attrs.get("pi_order", 0.0)),
895
+ )
896
+ signature = {"nodes": nodes, "edges": edges}
897
+ if include_stereo:
898
+ signature["stereo"] = {
899
+ key: self._stereo_descriptor_signature(descriptor)
900
+ for key, descriptor in sorted(
901
+ graph.graph.get("mechanism_stereo_descriptors", {}).items()
902
+ )
903
+ }
904
+ return signature
905
+
906
+ @staticmethod
907
+ def _mapped_aromatic_edges(graph: nx.Graph) -> set[frozenset[int]]:
908
+ """Return mapped bonds rendered aromatically by the input toolkit."""
909
+ edges: set[frozenset[int]] = set()
910
+ for left, right, attrs in graph.edges(data=True):
911
+ if not (attrs.get("aromatic") is True or attrs.get("order") == 1.5):
912
+ continue
913
+ atom_maps = (
914
+ graph.nodes[left].get("atom_map"),
915
+ graph.nodes[right].get("atom_map"),
916
+ )
917
+ if all(type(atom_map) is int and atom_map > 0 for atom_map in atom_maps):
918
+ edges.add(frozenset(atom_maps))
919
+ return edges
920
+
921
+ @staticmethod
922
+ def _stereo_descriptor_signature(descriptor: StereoDescriptor) -> tuple[Any, ...]:
923
+ if descriptor.descriptor_class == "unknown":
924
+ return ("unknown", descriptor.state)
925
+ native = stereo_from_dict(descriptor.to_dict())
926
+ return (descriptor.state, *native.canonical_form())
927
+
928
+ @staticmethod
929
+ def _seed_mechanism_stereo(graph: nx.Graph) -> None:
930
+ registry = {}
931
+ for key, descriptor in graph.graph.get("stereo_descriptors", {}).items():
932
+ value = descriptor.to_dict()
933
+ value["state"] = (
934
+ "specified" if value.get("parity") is not None else "unknown"
935
+ )
936
+ registry[key] = StereoDescriptor.from_dict(value)
937
+ graph.graph["mechanism_stereo_descriptors"] = registry
938
+
939
+ @staticmethod
940
+ def _select_endpoint_stereo(
941
+ graph: nx.Graph,
942
+ record: MechanismRecord,
943
+ *,
944
+ side: str,
945
+ ) -> None:
946
+ """Use an explicit endpoint sidecar when SMILES cannot encode state."""
947
+ if side in record.endpoint_stereo:
948
+ graph.graph["mechanism_stereo_descriptors"] = dict(
949
+ record.endpoint_stereo[side]
950
+ )
951
+
952
+ @staticmethod
953
+ def _endpoint_stereo_issues(
954
+ graph: nx.Graph,
955
+ *,
956
+ side: str,
957
+ ) -> tuple[VerificationIssue, ...]:
958
+ issues = []
959
+ for key, descriptor in graph.graph.get(
960
+ "mechanism_stereo_descriptors", {}
961
+ ).items():
962
+ for reason in descriptor_support_errors(
963
+ graph,
964
+ descriptor,
965
+ registry_key=key,
966
+ ):
967
+ issues.append(
968
+ VerificationIssue(
969
+ "INVALID_ENDPOINT_STEREO",
970
+ f"Invalid {side} descriptor {key}: {reason}.",
971
+ )
972
+ )
973
+ return tuple(issues)
974
+
975
+ @staticmethod
976
+ def _parse_side(smiles: str) -> nx.Graph:
977
+ graph = smiles_to_graph(
978
+ smiles,
979
+ drop_non_aam=False,
980
+ sanitize=True,
981
+ use_index_as_atom_map=False,
982
+ node_attrs=(*DEFAULT_NODE_ATTRS, "isotope"),
983
+ )
984
+ if graph is None:
985
+ raise ValueError(f"Could not parse mapped mechanism side: {smiles!r}")
986
+ return normalize_lwg_graph(graph, in_place=True)