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,725 @@
1
+ """Electron-flow primitives and validated move groups."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import Counter
6
+ from dataclasses import dataclass, field
7
+ from typing import Any, ClassVar, Literal, Mapping, Sequence
8
+
9
+ from .symbols import (
10
+ CanonicalLocus,
11
+ LONE_PAIR,
12
+ PI,
13
+ RADICAL,
14
+ SIGMA,
15
+ normalize_locus_symbol,
16
+ )
17
+
18
+ LocusKind = CanonicalLocus
19
+ ArrowType = Literal["curved", "fishhook"]
20
+
21
+
22
+ class MechanismModelError(ValueError):
23
+ """Raised when a public mechanism record is structurally invalid."""
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class ElectronLocus:
28
+ """A mapped electron resource or destination.
29
+
30
+ Atom loci (``lp`` and ``∙``) reference one atom map; bond loci
31
+ (``σ`` and ``π``) reference two distinct atom maps in canonical
32
+ ascending order.
33
+ """
34
+
35
+ kind: LocusKind
36
+ atom_maps: tuple[int, ...]
37
+
38
+ _ATOM_KINDS: ClassVar[frozenset[str]] = frozenset({LONE_PAIR, RADICAL})
39
+ _BOND_KINDS: ClassVar[frozenset[str]] = frozenset({SIGMA, PI})
40
+
41
+ def __post_init__(self) -> None:
42
+ try:
43
+ kind = normalize_locus_symbol(self.kind)
44
+ except (TypeError, ValueError) as exc:
45
+ raise MechanismModelError(str(exc)) from exc
46
+ maps = tuple(int(atom_map) for atom_map in self.atom_maps)
47
+ if any(atom_map <= 0 for atom_map in maps):
48
+ raise MechanismModelError("Electron-locus atom maps must be positive.")
49
+ expected = 1 if kind in self._ATOM_KINDS else 2
50
+ if len(maps) != expected or (expected == 2 and maps[0] == maps[1]):
51
+ raise MechanismModelError(
52
+ f"{kind!r} loci require exactly {expected} "
53
+ f"{'distinct ' if expected == 2 else ''}atom map(s)."
54
+ )
55
+ object.__setattr__(self, "kind", kind)
56
+ object.__setattr__(
57
+ self, "atom_maps", tuple(sorted(maps)) if expected == 2 else maps
58
+ )
59
+
60
+ @classmethod
61
+ def atom(cls, kind: str, *, atom_map: int) -> "ElectronLocus":
62
+ return cls(kind=kind, atom_maps=(atom_map,))
63
+
64
+ @classmethod
65
+ def bond(cls, kind: str, *, atom_maps: Sequence[int]) -> "ElectronLocus":
66
+ return cls(kind=kind, atom_maps=tuple(atom_maps))
67
+
68
+ def to_dict(self) -> dict[str, Any]:
69
+ return {"locus": self.kind, "atom_maps": list(self.atom_maps)}
70
+
71
+ @classmethod
72
+ def from_dict(cls, value: Mapping[str, Any]) -> "ElectronLocus":
73
+ return cls(kind=value["locus"], atom_maps=tuple(value["atom_maps"]))
74
+
75
+
76
+ @dataclass(frozen=True)
77
+ class ElectronMove:
78
+ """One curved-arrow or fishhook annotation.
79
+
80
+ A move is syntactically valid on its own. Coupling and resource checks are
81
+ performed by :class:`ElectronMoveGroup`, because all moves in a group read
82
+ the same pre-state and commit atomically.
83
+ """
84
+
85
+ source: ElectronLocus
86
+ target: ElectronLocus
87
+ electron_count: Literal[1, 2]
88
+ arrow_type: ArrowType
89
+ group_id: str
90
+ event_id: str | None = None
91
+ coupling_id: str | None = None
92
+ metadata: Mapping[str, Any] = field(default_factory=dict)
93
+
94
+ def __post_init__(self) -> None:
95
+ if self.electron_count not in (1, 2):
96
+ raise MechanismModelError(
97
+ "INVALID_ELECTRON_COUNT: electron_count must be 1 or 2."
98
+ )
99
+ expected = "fishhook" if self.electron_count == 1 else "curved"
100
+ if self.arrow_type != expected:
101
+ raise MechanismModelError(
102
+ f"ARROW_ELECTRON_COUNT_MISMATCH: {self.electron_count} electron "
103
+ f"moves require arrow_type={expected!r}."
104
+ )
105
+ if not str(self.group_id).strip():
106
+ raise MechanismModelError("Electron moves require a non-empty group_id.")
107
+ if self.source == self.target:
108
+ raise MechanismModelError(
109
+ "Electron moves must change locus; source and target are identical."
110
+ )
111
+ if self.electron_count == 2 and RADICAL in {
112
+ self.source.kind,
113
+ self.target.kind,
114
+ }:
115
+ raise MechanismModelError(
116
+ "RADICAL_LOCUS_REQUIRES_FISHHOOK: radical loci carry "
117
+ "one-electron moves."
118
+ )
119
+ object.__setattr__(self, "metadata", dict(self.metadata))
120
+
121
+ def to_dict(self) -> dict[str, Any]:
122
+ return {
123
+ "event_id": self.event_id,
124
+ "source": self.source.to_dict(),
125
+ "target": self.target.to_dict(),
126
+ "electron_count": self.electron_count,
127
+ "arrow_type": self.arrow_type,
128
+ "group_id": self.group_id,
129
+ "coupling_id": self.coupling_id,
130
+ "metadata": dict(self.metadata),
131
+ }
132
+
133
+ def reversed(self) -> "ElectronMove":
134
+ """Return the same electron transfer in the opposite direction."""
135
+ return ElectronMove(
136
+ source=self.target,
137
+ target=self.source,
138
+ electron_count=self.electron_count,
139
+ arrow_type=self.arrow_type,
140
+ group_id=self.group_id,
141
+ event_id=self.event_id,
142
+ coupling_id=self.coupling_id,
143
+ metadata=self.metadata,
144
+ )
145
+
146
+ @classmethod
147
+ def from_dict(cls, value: Mapping[str, Any]) -> "ElectronMove":
148
+ return cls(
149
+ event_id=value.get("event_id"),
150
+ source=ElectronLocus.from_dict(value["source"]),
151
+ target=ElectronLocus.from_dict(value["target"]),
152
+ electron_count=int(value["electron_count"]),
153
+ arrow_type=value["arrow_type"],
154
+ group_id=value["group_id"],
155
+ coupling_id=value.get("coupling_id"),
156
+ metadata=value.get("metadata", {}),
157
+ )
158
+
159
+
160
+ @dataclass(frozen=True)
161
+ class VerificationIssue:
162
+ """A deterministic, machine-readable mechanism validation finding."""
163
+
164
+ code: str
165
+ message: str
166
+ severity: Literal["error", "warning", "info"] = "error"
167
+ step_id: str | None = None
168
+ group_id: str | None = None
169
+ atom_maps: tuple[int, ...] = ()
170
+ expected: Any = None
171
+ observed: Any = None
172
+
173
+ def to_dict(self) -> dict[str, Any]:
174
+ return {
175
+ "code": self.code,
176
+ "message": self.message,
177
+ "severity": self.severity,
178
+ "step_id": self.step_id,
179
+ "group_id": self.group_id,
180
+ "atom_maps": list(self.atom_maps),
181
+ "expected": self.expected,
182
+ "observed": self.observed,
183
+ }
184
+
185
+ @classmethod
186
+ def from_dict(cls, value: Mapping[str, Any]) -> "VerificationIssue":
187
+ return cls(
188
+ code=value["code"],
189
+ message=value["message"],
190
+ severity=value.get("severity", "error"),
191
+ step_id=value.get("step_id"),
192
+ group_id=value.get("group_id"),
193
+ atom_maps=tuple(value.get("atom_maps", ())),
194
+ expected=value.get("expected"),
195
+ observed=value.get("observed"),
196
+ )
197
+
198
+
199
+ @dataclass(frozen=True)
200
+ class ElectronMoveGroup:
201
+ """Simultaneous electron moves with optional named radical macro."""
202
+
203
+ group_id: str
204
+ moves: tuple[ElectronMove, ...]
205
+ macro: str | None = None
206
+ metadata: Mapping[str, Any] = field(default_factory=dict)
207
+
208
+ SUPPORTED_MACROS: ClassVar[frozenset[str]] = frozenset(
209
+ {
210
+ "HOMOLYSIS",
211
+ "RECOMBINATION",
212
+ "RADICAL_ADDITION",
213
+ "BETA_SCISSION",
214
+ "H_ABSTRACTION",
215
+ "RADICAL_RESONANCE",
216
+ "LONE_PAIR_RADICAL_RELOCATION",
217
+ }
218
+ )
219
+ REVERSE_MACRO: ClassVar[Mapping[str, str]] = {
220
+ "HOMOLYSIS": "RECOMBINATION",
221
+ "RECOMBINATION": "HOMOLYSIS",
222
+ "RADICAL_ADDITION": "BETA_SCISSION",
223
+ "BETA_SCISSION": "RADICAL_ADDITION",
224
+ "H_ABSTRACTION": "H_ABSTRACTION",
225
+ "RADICAL_RESONANCE": "RADICAL_RESONANCE",
226
+ "LONE_PAIR_RADICAL_RELOCATION": "LONE_PAIR_RADICAL_RELOCATION",
227
+ }
228
+
229
+ def __post_init__(self) -> None:
230
+ if not self.moves:
231
+ raise MechanismModelError("ElectronMoveGroup requires at least one move.")
232
+ if not str(self.group_id).strip():
233
+ raise MechanismModelError(
234
+ "ElectronMoveGroup requires a non-empty group_id."
235
+ )
236
+ if any(move.group_id != self.group_id for move in self.moves):
237
+ raise MechanismModelError("Every move must use its containing group_id.")
238
+ macro = self.macro.upper() if self.macro else None
239
+ if macro and macro not in self.SUPPORTED_MACROS:
240
+ raise MechanismModelError(f"UNSUPPORTED_RADICAL_MACRO: {self.macro!r}")
241
+ object.__setattr__(self, "macro", macro)
242
+ object.__setattr__(self, "metadata", dict(self.metadata))
243
+
244
+ def issues(self) -> tuple[VerificationIssue, ...]:
245
+ """Return grammar issues independent of a particular molecular state."""
246
+ issues: list[VerificationIssue] = []
247
+ seen: set[tuple[Any, ...]] = set()
248
+ coupled: dict[str, list[ElectronMove]] = {}
249
+ for move in self.moves:
250
+ if not self._move_support_is_local(move):
251
+ issues.append(
252
+ VerificationIssue(
253
+ code="NONLOCAL_ELECTRON_MOVE",
254
+ message=(
255
+ "Source and target loci must share a reacting atom; "
256
+ "atom-to-atom lone-pair relocation is checked against "
257
+ "the pre-state adjacency separately."
258
+ ),
259
+ group_id=self.group_id,
260
+ atom_maps=tuple(
261
+ sorted(set(move.source.atom_maps + move.target.atom_maps))
262
+ ),
263
+ )
264
+ )
265
+ signature = (move.source, move.target, move.electron_count, move.arrow_type)
266
+ if signature in seen:
267
+ issues.append(
268
+ VerificationIssue(
269
+ code=(
270
+ "DUPLICATE_FISHHOOK"
271
+ if move.electron_count == 1
272
+ else "DUPLICATE_ELECTRON_MOVE"
273
+ ),
274
+ message="Duplicate electron move in one simultaneous group.",
275
+ group_id=self.group_id,
276
+ )
277
+ )
278
+ seen.add(signature)
279
+ if move.electron_count == 1:
280
+ if not move.coupling_id:
281
+ if self.macro != "LONE_PAIR_RADICAL_RELOCATION":
282
+ issues.append(
283
+ VerificationIssue(
284
+ code="MISSING_COUPLED_FISHHOOK",
285
+ message=(
286
+ "Fishhook moves require a coupling_id before commit."
287
+ ),
288
+ group_id=self.group_id,
289
+ )
290
+ )
291
+ else:
292
+ coupled.setdefault(move.coupling_id, []).append(move)
293
+ for coupling_id, moves in coupled.items():
294
+ if len(moves) < 2 and self.macro != "LONE_PAIR_RADICAL_RELOCATION":
295
+ issues.append(
296
+ VerificationIssue(
297
+ code="MISSING_COUPLED_FISHHOOK",
298
+ message=f"Coupling {coupling_id!r} has fewer than two fishhooks.",
299
+ group_id=self.group_id,
300
+ )
301
+ )
302
+ issues.extend(self._macro_issues())
303
+ return tuple(issues)
304
+
305
+ @staticmethod
306
+ def _move_support_is_local(move: ElectronMove) -> bool:
307
+ """Require each primitive arrow to connect incident locus supports."""
308
+ source_support = set(move.source.atom_maps)
309
+ target_support = set(move.target.atom_maps)
310
+ if source_support & target_support:
311
+ return True
312
+ return move.source.kind == move.target.kind == LONE_PAIR
313
+
314
+ def _macro_issues(self) -> list[VerificationIssue]: # noqa: C901
315
+ if not self.macro:
316
+ return []
317
+ fishhooks = [move for move in self.moves if move.electron_count == 1]
318
+ pairs = Counter((move.source.kind, move.target.kind) for move in fishhooks)
319
+
320
+ def pair_counts(value: Counter) -> dict[str, int]:
321
+ return {
322
+ f"{source}->{target}": count
323
+ for (source, target), count in value.items()
324
+ }
325
+
326
+ if self.macro == "LONE_PAIR_RADICAL_RELOCATION":
327
+ valid = (
328
+ len(fishhooks) == 1
329
+ and fishhooks[0].source.kind == LONE_PAIR
330
+ and fishhooks[0].target.kind == LONE_PAIR
331
+ and fishhooks[0].source.atom_maps != fishhooks[0].target.atom_maps
332
+ )
333
+ if valid:
334
+ return []
335
+ return [
336
+ VerificationIssue(
337
+ code="UNBALANCED_EVENT_GROUP",
338
+ message=(
339
+ "LONE_PAIR_RADICAL_RELOCATION requires one fishhook "
340
+ "between distinct atom-local lone-pair loci."
341
+ ),
342
+ group_id=self.group_id,
343
+ expected={"lp->lp": 1},
344
+ observed={
345
+ "fishhook_count": len(fishhooks),
346
+ "locus_pairs": pair_counts(pairs),
347
+ },
348
+ )
349
+ ]
350
+
351
+ if len(fishhooks) < 2:
352
+ return [
353
+ VerificationIssue(
354
+ code="UNBALANCED_EVENT_GROUP",
355
+ message=f"{self.macro} requires coupled one-electron moves.",
356
+ group_id=self.group_id,
357
+ expected={"minimum_fishhooks": 2},
358
+ observed={
359
+ "fishhook_count": len(fishhooks),
360
+ "locus_pairs": pair_counts(pairs),
361
+ },
362
+ )
363
+ ]
364
+
365
+ if self.macro == "HOMOLYSIS":
366
+ sources = {move.source for move in fishhooks}
367
+ targets = {move.target for move in fishhooks}
368
+ valid = (
369
+ len(sources) == 1
370
+ and next(iter(sources)).kind == SIGMA
371
+ and len(targets) == 2
372
+ and all(target.kind == RADICAL for target in targets)
373
+ and {target.atom_maps[0] for target in targets}
374
+ == set(next(iter(sources)).atom_maps)
375
+ )
376
+ if not valid:
377
+ return [
378
+ VerificationIssue(
379
+ "UNBALANCED_EVENT_GROUP",
380
+ "HOMOLYSIS requires one sigma source and two radical targets.",
381
+ group_id=self.group_id,
382
+ expected={"σ->∙": 2},
383
+ observed=pair_counts(pairs),
384
+ )
385
+ ]
386
+ if self.macro == "RECOMBINATION":
387
+ sources = {move.source for move in fishhooks}
388
+ targets = {move.target for move in fishhooks}
389
+ valid = (
390
+ len(sources) == 2
391
+ and all(source.kind == RADICAL for source in sources)
392
+ and len(targets) == 1
393
+ and next(iter(targets)).kind == SIGMA
394
+ and {source.atom_maps[0] for source in sources}
395
+ == set(next(iter(targets)).atom_maps)
396
+ )
397
+ if not valid:
398
+ return [
399
+ VerificationIssue(
400
+ "UNBALANCED_EVENT_GROUP",
401
+ "RECOMBINATION requires two radical sources and one sigma target.",
402
+ group_id=self.group_id,
403
+ expected={"∙->σ": 2},
404
+ observed=pair_counts(pairs),
405
+ )
406
+ ]
407
+ exact_patterns = {
408
+ "RADICAL_ADDITION": Counter(
409
+ {(RADICAL, SIGMA): 1, (PI, SIGMA): 1, (PI, RADICAL): 1}
410
+ ),
411
+ "BETA_SCISSION": Counter(
412
+ {(RADICAL, PI): 1, (SIGMA, PI): 1, (SIGMA, RADICAL): 1}
413
+ ),
414
+ "RADICAL_RESONANCE": Counter(
415
+ {(RADICAL, PI): 1, (PI, PI): 1, (PI, RADICAL): 1}
416
+ ),
417
+ }
418
+ expected = exact_patterns.get(self.macro)
419
+ if expected is not None and pairs != expected:
420
+ return [
421
+ VerificationIssue(
422
+ "UNBALANCED_EVENT_GROUP",
423
+ f"{self.macro} fishhook locus pattern is invalid.",
424
+ group_id=self.group_id,
425
+ expected=pair_counts(expected),
426
+ observed=pair_counts(pairs),
427
+ )
428
+ ]
429
+ if expected is not None and not self._macro_incidence_is_valid(fishhooks):
430
+ return [
431
+ VerificationIssue(
432
+ "MACRO_INCIDENCE_MISMATCH",
433
+ f"{self.macro} loci do not share the required reacting atoms.",
434
+ group_id=self.group_id,
435
+ )
436
+ ]
437
+ if self.macro == "H_ABSTRACTION":
438
+ simple_h_abstraction = Counter(
439
+ {
440
+ (RADICAL, SIGMA): 1,
441
+ (SIGMA, SIGMA): 1,
442
+ (SIGMA, RADICAL): 1,
443
+ }
444
+ )
445
+ expected_h_abstraction = Counter(
446
+ {
447
+ (RADICAL, SIGMA): 1,
448
+ (SIGMA, SIGMA): 1,
449
+ (SIGMA, PI): 1,
450
+ (RADICAL, PI): 1,
451
+ }
452
+ )
453
+ reverse_h_abstraction = Counter(
454
+ {
455
+ (target, source): count
456
+ for (source, target), count in expected_h_abstraction.items()
457
+ }
458
+ )
459
+ valid = pairs in (
460
+ simple_h_abstraction,
461
+ expected_h_abstraction,
462
+ reverse_h_abstraction,
463
+ ) and self._h_abstraction_incidence_is_valid(fishhooks)
464
+ if not valid:
465
+ return [
466
+ VerificationIssue(
467
+ "UNBALANCED_EVENT_GROUP",
468
+ "H_ABSTRACTION requires a radical source plus coupled old/new bond electron pairs.",
469
+ group_id=self.group_id,
470
+ expected={
471
+ "allowed": [
472
+ pair_counts(simple_h_abstraction),
473
+ pair_counts(expected_h_abstraction),
474
+ pair_counts(reverse_h_abstraction),
475
+ ]
476
+ },
477
+ observed=pair_counts(pairs),
478
+ )
479
+ ]
480
+ return []
481
+
482
+ @staticmethod
483
+ def _single_move(
484
+ moves: Sequence[ElectronMove], source: str, target: str
485
+ ) -> ElectronMove:
486
+ return next(
487
+ move
488
+ for move in moves
489
+ if move.source.kind == source and move.target.kind == target
490
+ )
491
+
492
+ def _macro_incidence_is_valid(self, moves: Sequence[ElectronMove]) -> bool:
493
+ """Check atom incidence for the three allylic radical macros."""
494
+ if self.macro == "RADICAL_ADDITION":
495
+ radical_to_new = self._single_move(moves, RADICAL, SIGMA)
496
+ old_to_new = self._single_move(moves, PI, SIGMA)
497
+ old_to_radical = self._single_move(moves, PI, RADICAL)
498
+ elif self.macro == "BETA_SCISSION":
499
+ radical_to_new = self._single_move(moves, RADICAL, PI)
500
+ old_to_new = self._single_move(moves, SIGMA, PI)
501
+ old_to_radical = self._single_move(moves, SIGMA, RADICAL)
502
+ elif self.macro == "RADICAL_RESONANCE":
503
+ radical_to_new = self._single_move(moves, RADICAL, PI)
504
+ old_to_new = self._single_move(moves, PI, PI)
505
+ old_to_radical = self._single_move(moves, PI, RADICAL)
506
+ else:
507
+ return True
508
+
509
+ if radical_to_new.target != old_to_new.target:
510
+ return False
511
+ if old_to_new.source != old_to_radical.source:
512
+ return False
513
+ old_atoms = set(old_to_new.source.atom_maps)
514
+ new_atoms = set(old_to_new.target.atom_maps)
515
+ shared = old_atoms & new_atoms
516
+ return (
517
+ len(shared) == 1
518
+ and {radical_to_new.source.atom_maps[0]} == new_atoms - shared
519
+ and {old_to_radical.target.atom_maps[0]} == old_atoms - shared
520
+ )
521
+
522
+ @classmethod
523
+ def _h_abstraction_incidence_is_valid(cls, moves: Sequence[ElectronMove]) -> bool:
524
+ pairs = Counter((move.source.kind, move.target.kind) for move in moves)
525
+ reverse_conjugated = Counter(
526
+ {
527
+ (SIGMA, RADICAL): 1,
528
+ (SIGMA, SIGMA): 1,
529
+ (PI, SIGMA): 1,
530
+ (PI, RADICAL): 1,
531
+ }
532
+ )
533
+ if pairs == reverse_conjugated:
534
+ return cls._h_abstraction_incidence_is_valid(
535
+ tuple(move.reversed() for move in moves)
536
+ )
537
+
538
+ radical_to_new_sigma = cls._single_move(moves, RADICAL, SIGMA)
539
+ old_to_new_sigma = cls._single_move(moves, SIGMA, SIGMA)
540
+
541
+ if radical_to_new_sigma.target != old_to_new_sigma.target:
542
+ return False
543
+
544
+ old_sigma = set(old_to_new_sigma.source.atom_maps)
545
+ new_sigma = set(old_to_new_sigma.target.atom_maps)
546
+ hydrogen = old_sigma & new_sigma
547
+ if len(hydrogen) != 1:
548
+ return False
549
+ donor = old_sigma - hydrogen
550
+ acceptor = new_sigma - hydrogen
551
+ if {radical_to_new_sigma.source.atom_maps[0]} != acceptor:
552
+ return False
553
+
554
+ if pairs[(SIGMA, RADICAL)] == 1:
555
+ old_to_radical = cls._single_move(moves, SIGMA, RADICAL)
556
+ return (
557
+ old_to_radical.source == old_to_new_sigma.source
558
+ and {old_to_radical.target.atom_maps[0]} == donor
559
+ )
560
+
561
+ old_to_new_pi = cls._single_move(moves, SIGMA, PI)
562
+ radical_to_new_pi = cls._single_move(moves, RADICAL, PI)
563
+ if (
564
+ old_to_new_sigma.source != old_to_new_pi.source
565
+ or old_to_new_pi.target != radical_to_new_pi.target
566
+ ):
567
+ return False
568
+ new_pi = set(old_to_new_pi.target.atom_maps)
569
+ return (
570
+ donor <= new_pi
571
+ and {radical_to_new_pi.source.atom_maps[0]} == new_pi - donor
572
+ )
573
+
574
+ def canonical_signature(self) -> tuple[Any, ...]:
575
+ """Return an order-invariant signature for simultaneous events."""
576
+ moves = tuple(
577
+ sorted(
578
+ (
579
+ move.source.kind,
580
+ move.source.atom_maps,
581
+ move.target.kind,
582
+ move.target.atom_maps,
583
+ move.electron_count,
584
+ move.arrow_type,
585
+ move.coupling_id or "",
586
+ )
587
+ for move in self.moves
588
+ )
589
+ )
590
+ return (self.macro or "", moves)
591
+
592
+ def reversed(self) -> "ElectronMoveGroup":
593
+ """Reverse every move and select the inverse radical macro."""
594
+ return ElectronMoveGroup(
595
+ group_id=self.group_id,
596
+ moves=tuple(move.reversed() for move in self.moves),
597
+ macro=self.REVERSE_MACRO[self.macro] if self.macro else None,
598
+ metadata=self.metadata,
599
+ )
600
+
601
+ @property
602
+ def read_loci(self) -> frozenset[ElectronLocus]:
603
+ return frozenset(move.source for move in self.moves)
604
+
605
+ @property
606
+ def write_loci(self) -> frozenset[ElectronLocus]:
607
+ return frozenset(move.target for move in self.moves)
608
+
609
+ def validate_pre_state(self, graph: Any) -> tuple[VerificationIssue, ...]:
610
+ """Check source resources and simultaneous overconsumption on a graph.
611
+
612
+ The graph is expected to use SynKit's scalar molecular-graph fields.
613
+ This method does not mutate it; replay is introduced in Sprint 4.
614
+ """
615
+ issues = list(self.issues())
616
+ by_map = {
617
+ int(attrs.get("atom_map", 0) or 0): node
618
+ for node, attrs in graph.nodes(data=True)
619
+ if int(attrs.get("atom_map", 0) or 0) > 0
620
+ }
621
+ requested: dict[ElectronLocus, int] = {}
622
+ for move in self.moves:
623
+ requested[move.source] = requested.get(move.source, 0) + move.electron_count
624
+ if move.source.kind == move.target.kind == LONE_PAIR and all(
625
+ atom_map in by_map
626
+ for atom_map in move.source.atom_maps + move.target.atom_maps
627
+ ):
628
+ source_node = by_map[move.source.atom_maps[0]]
629
+ target_node = by_map[move.target.atom_maps[0]]
630
+ if not graph.has_edge(source_node, target_node):
631
+ issues.append(
632
+ VerificationIssue(
633
+ "NONLOCAL_ELECTRON_MOVE",
634
+ "Lone-pair relocation requires adjacent donor and acceptor atoms.",
635
+ group_id=self.group_id,
636
+ atom_maps=tuple(
637
+ sorted(move.source.atom_maps + move.target.atom_maps)
638
+ ),
639
+ )
640
+ )
641
+ for locus, is_source in ((move.source, True), (move.target, False)):
642
+ if any(atom_map not in by_map for atom_map in locus.atom_maps):
643
+ issues.append(
644
+ VerificationIssue(
645
+ (
646
+ "SOURCE_LOCUS_ABSENT"
647
+ if is_source
648
+ else "TARGET_LOCUS_INVALID"
649
+ ),
650
+ f"{locus.kind} locus references an atom absent from the pre-state.",
651
+ group_id=self.group_id,
652
+ atom_maps=locus.atom_maps,
653
+ )
654
+ )
655
+ continue
656
+ if is_source and self._available_electrons(graph, by_map, locus) <= 0:
657
+ issues.append(
658
+ VerificationIssue(
659
+ "SOURCE_LOCUS_ABSENT",
660
+ f"Source {locus.kind} resource is absent.",
661
+ group_id=self.group_id,
662
+ atom_maps=locus.atom_maps,
663
+ )
664
+ )
665
+ for locus, count in requested.items():
666
+ if any(atom_map not in by_map for atom_map in locus.atom_maps):
667
+ continue
668
+ available = self._available_electrons(graph, by_map, locus)
669
+ if count > available:
670
+ issues.append(
671
+ VerificationIssue(
672
+ "LOCUS_OVERCONSUMED",
673
+ f"{count} electrons requested from {locus.kind}; only {available} available.",
674
+ group_id=self.group_id,
675
+ atom_maps=locus.atom_maps,
676
+ expected=available,
677
+ observed=count,
678
+ )
679
+ )
680
+ if self.macro == "LONE_PAIR_RADICAL_RELOCATION" and len(self.moves) == 1:
681
+ move = self.moves[0]
682
+ target_radical = ElectronLocus(RADICAL, move.target.atom_maps)
683
+ if all(atom_map in by_map for atom_map in target_radical.atom_maps) and (
684
+ self._available_electrons(graph, by_map, target_radical) < 1
685
+ ):
686
+ issues.append(
687
+ VerificationIssue(
688
+ "TARGET_RADICAL_ABSENT",
689
+ "Lone-pair radical relocation requires a radical acceptor.",
690
+ group_id=self.group_id,
691
+ atom_maps=move.target.atom_maps,
692
+ )
693
+ )
694
+ return tuple(issues)
695
+
696
+ @staticmethod
697
+ def _available_electrons(
698
+ graph: Any, by_map: Mapping[int, Any], locus: ElectronLocus
699
+ ) -> int:
700
+ if locus.kind == LONE_PAIR:
701
+ return 2 * int(graph.nodes[by_map[locus.atom_maps[0]]].get("lone_pairs", 0))
702
+ if locus.kind == RADICAL:
703
+ return int(graph.nodes[by_map[locus.atom_maps[0]]].get("radical", 0))
704
+ left, right = (by_map[atom_map] for atom_map in locus.atom_maps)
705
+ if not graph.has_edge(left, right):
706
+ return 0
707
+ field = "sigma_order" if locus.kind == SIGMA else "pi_order"
708
+ return int(round(2 * float(graph.edges[left, right].get(field, 0))))
709
+
710
+ def to_dict(self) -> dict[str, Any]:
711
+ return {
712
+ "group_id": self.group_id,
713
+ "macro": self.macro,
714
+ "moves": [move.to_dict() for move in self.moves],
715
+ "metadata": dict(self.metadata),
716
+ }
717
+
718
+ @classmethod
719
+ def from_dict(cls, value: Mapping[str, Any]) -> "ElectronMoveGroup":
720
+ return cls(
721
+ group_id=value["group_id"],
722
+ macro=value.get("macro"),
723
+ moves=tuple(ElectronMove.from_dict(move) for move in value["moves"]),
724
+ metadata=value.get("metadata", {}),
725
+ )