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,740 @@
1
+ """Audited adapter for SynKit's partial-AAM radical mechanism dataset."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import csv
6
+ import re
7
+ from collections import Counter, defaultdict
8
+ from dataclasses import dataclass
9
+ from itertools import combinations
10
+ from pathlib import Path
11
+ from typing import Any, Iterable, Sequence
12
+
13
+ from rdkit import Chem
14
+
15
+ from synkit.Graph.ITS.its_expand import ITSExpand
16
+ from synkit.Graph.Mech.conversion import (
17
+ build_its_from_rsmi,
18
+ typed_convert_arrow_code,
19
+ validate_arrow_maps,
20
+ )
21
+
22
+ from .model import (
23
+ ElectronLocus,
24
+ ElectronMove,
25
+ ElectronMoveGroup,
26
+ MechanismRecord,
27
+ MechanisticStep,
28
+ VerificationIssue,
29
+ )
30
+
31
+ RADICAL_CLASS_TO_MACRO = {
32
+ "homolyze": "HOMOLYSIS",
33
+ "recombine": "RECOMBINATION",
34
+ "addition": "RADICAL_ADDITION",
35
+ "retroaddition": "BETA_SCISSION",
36
+ "abstraction": "H_ABSTRACTION",
37
+ "resonance": "RADICAL_RESONANCE",
38
+ "ha resonance": "LONE_PAIR_RADICAL_RELOCATION",
39
+ }
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class RadicalAAMCompletionResult:
44
+ """Fail-closed evidence for arrow-independent radical AAM completion."""
45
+
46
+ source_reaction: str
47
+ mapped_reaction: str | None
48
+ status: str
49
+ method: str | None
50
+ source_was_complete: bool
51
+ all_atoms_mapped: bool = False
52
+ unique_atom_maps: bool = False
53
+ balanced_atom_maps: bool = False
54
+ source_anchors_preserved: bool = False
55
+ constitution_preserved: bool = False
56
+ expansion_side: str | None = None
57
+ fallback_used: bool = False
58
+ unmapped_explicit_hydrogens_folded: bool = False
59
+ folded_unmapped_explicit_hydrogen_count: int = 0
60
+ explicit_hydrogen_serialization: bool = False
61
+ stereochemistry_ignored_for_expansion: bool = False
62
+ radical_state_preserved: bool = False
63
+ failure_reason: str | None = None
64
+
65
+ @property
66
+ def usable(self) -> bool:
67
+ """Whether the result is a validated complete atom mapping."""
68
+ return self.status in {"ALREADY_COMPLETE", "COMPLETED"}
69
+
70
+ def to_dict(self) -> dict[str, Any]:
71
+ return {
72
+ "source_reaction": self.source_reaction,
73
+ "mapped_reaction": self.mapped_reaction,
74
+ "status": self.status,
75
+ "method": self.method,
76
+ "source_was_complete": self.source_was_complete,
77
+ "usable": self.usable,
78
+ "all_atoms_mapped": self.all_atoms_mapped,
79
+ "unique_atom_maps": self.unique_atom_maps,
80
+ "balanced_atom_maps": self.balanced_atom_maps,
81
+ "source_anchors_preserved": self.source_anchors_preserved,
82
+ "constitution_preserved": self.constitution_preserved,
83
+ "expansion_side": self.expansion_side,
84
+ "fallback_used": self.fallback_used,
85
+ "unmapped_explicit_hydrogens_folded": (
86
+ self.unmapped_explicit_hydrogens_folded
87
+ ),
88
+ "folded_unmapped_explicit_hydrogen_count": (
89
+ self.folded_unmapped_explicit_hydrogen_count
90
+ ),
91
+ "explicit_hydrogen_serialization": (self.explicit_hydrogen_serialization),
92
+ "stereochemistry_ignored_for_expansion": (
93
+ self.stereochemistry_ignored_for_expansion
94
+ ),
95
+ "radical_state_preserved": self.radical_state_preserved,
96
+ "failure_reason": self.failure_reason,
97
+ }
98
+
99
+
100
+ @dataclass(frozen=True)
101
+ class RadicalNormalizationReport:
102
+ row_number: int
103
+ status: str
104
+ source_class: str
105
+ macro: str | None = None
106
+ aam_expanded: bool = False
107
+ aam_expansion_side: str | None = None
108
+ aam_fallback_used: bool = False
109
+ constitution_checked: bool = False
110
+ unmapped_explicit_hydrogens_folded: bool = False
111
+ folded_unmapped_explicit_hydrogen_count: int = 0
112
+ stereochemistry_ignored_for_expansion: bool = False
113
+ radical_state_preserved: bool = False
114
+ equivalent_map_swap: tuple[int, int] | None = None
115
+ arrow_repair_assessment: str | None = None
116
+ spin_assessment: str = "UNASSESSED_NO_SOURCE_EVIDENCE"
117
+ aliases_normalized: tuple[str, ...] = ()
118
+ issues: tuple[VerificationIssue, ...] = ()
119
+
120
+ def to_dict(self) -> dict[str, Any]:
121
+ return {
122
+ "row_number": self.row_number,
123
+ "status": self.status,
124
+ "source_class": self.source_class,
125
+ "macro": self.macro,
126
+ "aam_expanded": self.aam_expanded,
127
+ "aam_expansion_side": self.aam_expansion_side,
128
+ "aam_fallback_used": self.aam_fallback_used,
129
+ "constitution_checked": self.constitution_checked,
130
+ "unmapped_explicit_hydrogens_folded": (
131
+ self.unmapped_explicit_hydrogens_folded
132
+ ),
133
+ "folded_unmapped_explicit_hydrogen_count": (
134
+ self.folded_unmapped_explicit_hydrogen_count
135
+ ),
136
+ "stereochemistry_ignored_for_expansion": (
137
+ self.stereochemistry_ignored_for_expansion
138
+ ),
139
+ "radical_state_preserved": self.radical_state_preserved,
140
+ "equivalent_map_swap": (
141
+ list(self.equivalent_map_swap) if self.equivalent_map_swap else None
142
+ ),
143
+ "arrow_repair_assessment": self.arrow_repair_assessment,
144
+ "spin_assessment": self.spin_assessment,
145
+ "aliases_normalized": list(self.aliases_normalized),
146
+ "issues": [issue.to_dict() for issue in self.issues],
147
+ }
148
+
149
+
150
+ @dataclass(frozen=True)
151
+ class RadicalDatasetRecord:
152
+ source_row: tuple[str, ...]
153
+ report: RadicalNormalizationReport
154
+ mechanism: MechanismRecord | None = None
155
+
156
+ @property
157
+ def accepted(self) -> bool:
158
+ return self.mechanism is not None and self.report.status == "NORMALIZED"
159
+
160
+
161
+ def _split_dataset_text(text: str) -> tuple[str, str]:
162
+ try:
163
+ reaction, flow = text.strip().split(None, 1)
164
+ except ValueError as exc:
165
+ raise ValueError(
166
+ "Radical record must contain reaction SMILES followed by electron flow."
167
+ ) from exc
168
+ if reaction.count(">>") != 1:
169
+ raise ValueError("Radical record must contain one reaction '>>'.")
170
+ return reaction, flow
171
+
172
+
173
+ def _normalize_dataset_flow(flow: str) -> tuple[str, tuple[str, ...]]:
174
+ """Normalize unambiguous legacy flow separators to internal equals form."""
175
+ steps: list[str] = []
176
+ aliases: list[str] = []
177
+ atom_list = r"\d+(?:\s*,\s*\d+)*"
178
+ if any(raw_step != raw_step.strip() for raw_step in flow.split(";")):
179
+ aliases.append("FlowWhitespace→canonical")
180
+
181
+ for raw_step in flow.split(";"):
182
+ step = raw_step.strip()
183
+ if not step:
184
+ continue
185
+
186
+ if step.count("=") == 1 and "-" not in step:
187
+ lhs, rhs = step.split("=", 1)
188
+ aliases.append("FlowSeparator=→-")
189
+ elif step.count("-") == 1 and "=" not in step:
190
+ lhs, rhs = step.split("-", 1)
191
+ elif step.count("-") == 2 and "=" not in step:
192
+ match = re.fullmatch(rf"\s*({atom_list})\s*-\s*(\d+)\s*-\s*(\d+)\s*", step)
193
+ if match is None:
194
+ raise ValueError(
195
+ f"MALFORMED_FLOW_SEPARATOR: cannot normalize {step!r}."
196
+ )
197
+ lhs, rhs_a, rhs_b = match.groups()
198
+ rhs = f"{rhs_a},{rhs_b}"
199
+ aliases.append("FlowPairSeparator-→,")
200
+ else:
201
+ raise ValueError(f"MALFORMED_FLOW_SEPARATOR: cannot normalize {step!r}.")
202
+
203
+ lhs = lhs.strip()
204
+ rhs = rhs.strip()
205
+ if not re.fullmatch(atom_list, lhs) or not re.fullmatch(atom_list, rhs):
206
+ raise ValueError(f"MALFORMED_FLOW_LOCUS: cannot parse {step!r}.")
207
+ steps.append(f"{lhs}={rhs}")
208
+
209
+ if not steps:
210
+ raise ValueError("MALFORMED_FLOW_SEPARATOR: electron flow is empty.")
211
+ return ";".join(steps), tuple(dict.fromkeys(aliases))
212
+
213
+
214
+ def _mapped_atoms_complete(reaction: str) -> bool:
215
+ for side in reaction.split(">>"):
216
+ mol = Chem.MolFromSmiles(side, sanitize=False)
217
+ if mol is None:
218
+ return False
219
+ if any(
220
+ atom.GetAtomicNum() > 0 and atom.GetAtomMapNum() == 0
221
+ for atom in mol.GetAtoms()
222
+ ):
223
+ return False
224
+ return True
225
+
226
+
227
+ def _side_atom_map_identities(
228
+ smiles: str, *, require_complete: bool
229
+ ) -> dict[int, tuple[int, int]]:
230
+ """Return positive map identities while rejecting unsafe map topology."""
231
+ parser = Chem.SmilesParserParams()
232
+ parser.removeHs = False
233
+ mol = Chem.MolFromSmiles(smiles, parser)
234
+ if mol is None:
235
+ raise ValueError(f"Cannot parse reaction side {smiles!r}.")
236
+
237
+ atoms = [atom for atom in mol.GetAtoms() if atom.GetAtomicNum() > 0]
238
+ atom_maps = [atom.GetAtomMapNum() for atom in atoms]
239
+ if require_complete and any(atom_map <= 0 for atom_map in atom_maps):
240
+ raise ValueError("Completed AAM contains an unmapped atom.")
241
+ positive_maps = [atom_map for atom_map in atom_maps if atom_map > 0]
242
+ if len(positive_maps) != len(set(positive_maps)):
243
+ raise ValueError("A reaction side contains duplicate positive atom maps.")
244
+ return {
245
+ atom.GetAtomMapNum(): (atom.GetAtomicNum(), atom.GetIsotope())
246
+ for atom in atoms
247
+ if atom.GetAtomMapNum() > 0
248
+ }
249
+
250
+
251
+ def _validate_completed_radical_aam(source: str, candidate: str) -> None:
252
+ """Validate completeness, map balance, identity, anchors, and constitution."""
253
+ if source.count(">>") != 1 or candidate.count(">>") != 1:
254
+ raise ValueError("AAM validation requires one reaction '>>'.")
255
+ if not ITSExpand.endpoint_constitutions_match(source, candidate):
256
+ raise ValueError("Completed AAM changed an endpoint constitution.")
257
+
258
+ source_sides = [
259
+ _side_atom_map_identities(side, require_complete=False)
260
+ for side in source.split(">>")
261
+ ]
262
+ candidate_sides = [
263
+ _side_atom_map_identities(side, require_complete=True)
264
+ for side in candidate.split(">>")
265
+ ]
266
+ if set(candidate_sides[0]) != set(candidate_sides[1]):
267
+ raise ValueError("Completed AAM has different reactant and product map sets.")
268
+ for atom_map, identity in candidate_sides[0].items():
269
+ if candidate_sides[1][atom_map] != identity:
270
+ raise ValueError(
271
+ f"Completed AAM map {atom_map} changes atom identity across endpoints."
272
+ )
273
+ for atom_map in set(source_sides[0]) | set(source_sides[1]):
274
+ for source_side, candidate_side in zip(source_sides, candidate_sides):
275
+ if (
276
+ atom_map in source_side
277
+ and candidate_side.get(atom_map) != source_side[atom_map]
278
+ ):
279
+ raise ValueError(
280
+ f"Completed AAM moved or removed source anchor map {atom_map}."
281
+ )
282
+
283
+
284
+ def complete_radical_aam(reaction: str) -> RadicalAAMCompletionResult:
285
+ """Complete and validate a radical AAM without consulting electron arrows.
286
+
287
+ Existing positive map numbers are hard anchors. Partial mappings are expanded
288
+ from the ITS with endpoint-constitution, radical-state, and explicit-H guards.
289
+ The returned result is usable only after all validation gates pass.
290
+ """
291
+ source_was_complete = _mapped_atoms_complete(reaction)
292
+ try:
293
+ if source_was_complete:
294
+ candidate = reaction
295
+ expansion = None
296
+ status = "ALREADY_COMPLETE"
297
+ method = "source"
298
+ else:
299
+ expansion = ITSExpand.expand_aam_with_its_report(
300
+ reaction,
301
+ preserve_older_map=True,
302
+ fallback_to_other_side=True,
303
+ require_constitution_preservation=True,
304
+ fold_unmapped_explicit_hydrogens=True,
305
+ ignore_stereochemistry=True,
306
+ explicit_hydrogen=True,
307
+ preserve_radical_state=True,
308
+ )
309
+ candidate = expansion.rsmi
310
+ status = "COMPLETED"
311
+ method = "its"
312
+
313
+ _validate_completed_radical_aam(reaction, candidate)
314
+ return RadicalAAMCompletionResult(
315
+ reaction,
316
+ candidate,
317
+ status,
318
+ method,
319
+ source_was_complete,
320
+ all_atoms_mapped=True,
321
+ unique_atom_maps=True,
322
+ balanced_atom_maps=True,
323
+ source_anchors_preserved=True,
324
+ constitution_preserved=True,
325
+ expansion_side=expansion.selected_side if expansion else None,
326
+ fallback_used=expansion.fallback_used if expansion else False,
327
+ unmapped_explicit_hydrogens_folded=(
328
+ expansion.unmapped_explicit_hydrogens_folded if expansion else False
329
+ ),
330
+ folded_unmapped_explicit_hydrogen_count=(
331
+ expansion.folded_unmapped_explicit_hydrogen_count if expansion else 0
332
+ ),
333
+ explicit_hydrogen_serialization=(
334
+ expansion.explicit_hydrogen_serialization if expansion else False
335
+ ),
336
+ stereochemistry_ignored_for_expansion=(
337
+ expansion.stereochemistry_ignored_for_expansion if expansion else False
338
+ ),
339
+ radical_state_preserved=(
340
+ expansion.radical_state_preserved if expansion else True
341
+ ),
342
+ )
343
+ except Exception as exc:
344
+ return RadicalAAMCompletionResult(
345
+ reaction,
346
+ None,
347
+ "FAILED",
348
+ None,
349
+ source_was_complete,
350
+ failure_reason=str(exc),
351
+ )
352
+
353
+
354
+ def _positive_source_atom_maps(reaction: str) -> set[int]:
355
+ """Collect supplied positive maps without treating completed maps as evidence."""
356
+ atom_maps: set[int] = set()
357
+ parser = Chem.SmilesParserParams()
358
+ parser.removeHs = False
359
+ for side in reaction.split(">>"):
360
+ mol = Chem.MolFromSmiles(side, parser)
361
+ if mol is None:
362
+ continue
363
+ atom_maps.update(
364
+ atom.GetAtomMapNum() for atom in mol.GetAtoms() if atom.GetAtomMapNum() > 0
365
+ )
366
+ return atom_maps
367
+
368
+
369
+ def _reactant_symmetry_map_pairs(
370
+ mapped_reaction: str, *, allowed_maps: set[int]
371
+ ) -> tuple[tuple[int, int], ...]:
372
+ """Find supplied map pairs in the same RDKit reactant symmetry class."""
373
+ reactants, _products = mapped_reaction.split(">>")
374
+ parser = Chem.SmilesParserParams()
375
+ parser.removeHs = False
376
+ mol = Chem.MolFromSmiles(reactants, parser)
377
+ if mol is None:
378
+ return ()
379
+
380
+ source_maps = [atom.GetAtomMapNum() for atom in mol.GetAtoms()]
381
+ map_free = Chem.Mol(mol)
382
+ for atom in map_free.GetAtoms():
383
+ atom.SetAtomMapNum(0)
384
+ ranks = Chem.CanonicalRankAtoms(
385
+ map_free,
386
+ breakTies=False,
387
+ includeChirality=True,
388
+ includeIsotopes=True,
389
+ )
390
+ classes: dict[tuple[int, int, int], list[int]] = defaultdict(list)
391
+ for atom, atom_map, rank in zip(mol.GetAtoms(), source_maps, ranks):
392
+ if atom_map in allowed_maps:
393
+ classes[(rank, atom.GetAtomicNum(), atom.GetIsotope())].append(atom_map)
394
+ return tuple(
395
+ pair
396
+ for symmetry_class in classes.values()
397
+ for pair in combinations(symmetry_class, 2)
398
+ )
399
+
400
+
401
+ def _swap_reaction_atom_maps(mapped_reaction: str, atom_maps: tuple[int, int]) -> str:
402
+ """Swap two map labels globally without changing endpoint constitution."""
403
+ left_map, right_map = atom_maps
404
+ parser = Chem.SmilesParserParams()
405
+ parser.removeHs = False
406
+ swapped_sides: list[str] = []
407
+ for side in mapped_reaction.split(">>"):
408
+ mol = Chem.MolFromSmiles(side, parser)
409
+ if mol is None:
410
+ raise ValueError(f"Cannot parse reaction side {side!r} for map swap.")
411
+ for atom in mol.GetAtoms():
412
+ if atom.GetAtomMapNum() == left_map:
413
+ atom.SetAtomMapNum(right_map)
414
+ elif atom.GetAtomMapNum() == right_map:
415
+ atom.SetAtomMapNum(left_map)
416
+ swapped_sides.append(Chem.MolToSmiles(mol, canonical=True, isomericSmiles=True))
417
+ return ">>".join(swapped_sides)
418
+
419
+
420
+ def _normalization_failure_code(message: str) -> str:
421
+ """Classify fail-closed adapter errors without claiming chemical repair."""
422
+ if "MALFORMED_FLOW_" in message:
423
+ return "MALFORMED_ELECTRON_FLOW"
424
+ if "missing_arrow_maps" in message:
425
+ return "FLOW_ATOM_MAP_MISSING"
426
+ if "ITS graph has no edge" in message:
427
+ return "FLOW_BOND_ABSENT_FROM_ITS"
428
+ if "Cannot align a reconstructed endpoint" in message:
429
+ return "AAM_RADICAL_STATE_UNALIGNABLE"
430
+ if "changed an endpoint constitution" in message:
431
+ return "AAM_ENDPOINT_CONSTITUTION_CHANGED"
432
+ if "Unsupported electron locus" in message:
433
+ return "FLOW_LOCUS_UNTYPABLE"
434
+ if "ATOM_TRANSFER_STATE_MISMATCH" in message:
435
+ return "FLOW_ATOM_TRANSFER_STATE_MISMATCH"
436
+ return "RADICAL_DATA_NORMALIZATION_FAILED"
437
+
438
+
439
+ def _fishhook_move(
440
+ typed_row: Sequence[Any],
441
+ *,
442
+ group_id: str,
443
+ coupling_id: str | None,
444
+ event_id: str,
445
+ ) -> ElectronMove:
446
+ action, source_maps, target_maps = typed_row[:3]
447
+ source_name, target_name = str(action).split("-/", 1)
448
+ target_name = target_name.removesuffix("+")
449
+
450
+ # In typed polar EPD a one-atom endpoint is called LP. In this reviewed
451
+ # radical dataset the same shape denotes the SOMO/radical endpoint; bond
452
+ # endpoints retain the σ/π type inferred from the ITS bond-order change.
453
+ if len(source_maps) == 1 and len(target_maps) == 1:
454
+ # State-aware atom-to-atom typing is authoritative when both compact
455
+ # endpoints resolve to explicit nonbonding resource transitions.
456
+ source_kind = source_name
457
+ target_kind = target_name
458
+ else:
459
+ source_kind = "∙" if len(source_maps) == 1 else source_name
460
+ target_kind = "∙" if len(target_maps) == 1 else target_name
461
+ return ElectronMove(
462
+ source=ElectronLocus(source_kind, tuple(source_maps)),
463
+ target=ElectronLocus(target_kind, tuple(target_maps)),
464
+ electron_count=1,
465
+ arrow_type="fishhook",
466
+ group_id=group_id,
467
+ coupling_id=coupling_id,
468
+ event_id=event_id,
469
+ metadata={"inferred_from_typed_epd": str(action)},
470
+ )
471
+
472
+
473
+ def normalize_radical_row( # noqa: C901
474
+ row: Sequence[str],
475
+ *,
476
+ row_number: int = 0,
477
+ enforce_source_macro: bool = True,
478
+ ) -> RadicalDatasetRecord:
479
+ """Normalize one four-column radical CSV row or return a quarantine report.
480
+
481
+ ``enforce_source_macro=False`` validates and reconstructs the typed
482
+ fishhook group independently of the dataset's reaction-class label. The
483
+ state-resolved single-fishhook transition retains its operational marker
484
+ because replay needs its paired lone-pair/radical commit semantics.
485
+ """
486
+ source_row = tuple(str(value) for value in row)
487
+ source_class = source_row[3].strip().lower() if len(source_row) >= 4 else ""
488
+ macro = RADICAL_CLASS_TO_MACRO.get(source_class)
489
+ issues: list[VerificationIssue] = []
490
+
491
+ if len(source_row) != 4:
492
+ issues.append(
493
+ VerificationIssue("INVALID_DATASET_ROW", "Expected four CSV columns.")
494
+ )
495
+ if enforce_source_macro and macro is None:
496
+ issues.append(
497
+ VerificationIssue(
498
+ "UNREVIEWED_RADICAL_CLASS",
499
+ f"Radical class {source_class!r} has no approved macro mapping.",
500
+ )
501
+ )
502
+ if issues:
503
+ return RadicalDatasetRecord(
504
+ source_row,
505
+ RadicalNormalizationReport(
506
+ row_number, "QUARANTINED", source_class, macro, issues=tuple(issues)
507
+ ),
508
+ )
509
+
510
+ flow_aliases: tuple[str, ...] = ()
511
+ try:
512
+ reaction, flow = _split_dataset_text(source_row[0])
513
+ arrow_code, flow_aliases = _normalize_dataset_flow(flow)
514
+ was_complete = _mapped_atoms_complete(reaction)
515
+ # Validate flow references against supplied maps before completion.
516
+ # Otherwise a newly assigned map could accidentally legitimize a
517
+ # source arrow that originally named a nonexistent atom.
518
+ validate_arrow_maps(
519
+ reaction,
520
+ arrow_code,
521
+ raise_on_arrow_duplicates=True,
522
+ raise_on_missing_arrow_maps=True,
523
+ )
524
+ aam_completion = complete_radical_aam(reaction)
525
+ if not aam_completion.usable or aam_completion.mapped_reaction is None:
526
+ raise ValueError(
527
+ "Arrow-independent AAM completion failed before flow typing: "
528
+ f"{aam_completion.failure_reason}"
529
+ )
530
+ equivalent_map_swap: tuple[int, int] | None = None
531
+ arrow_repair_assessment: str | None = None
532
+ try:
533
+ its, expanded, _cleaned, _diagnostics = build_its_from_rsmi(
534
+ aam_completion.mapped_reaction,
535
+ arrow_code,
536
+ expand_aam=False,
537
+ remove_non_arrow_maps=False,
538
+ )
539
+ typed = typed_convert_arrow_code(arrow_code, its, electron_count=1)
540
+ except Exception as arrow_exc:
541
+ if "ITS graph has no edge" not in str(arrow_exc):
542
+ raise
543
+ repair_candidates = []
544
+ allowed_maps = _positive_source_atom_maps(reaction)
545
+ for atom_map_pair in _reactant_symmetry_map_pairs(
546
+ aam_completion.mapped_reaction,
547
+ allowed_maps=allowed_maps,
548
+ ):
549
+ swapped = _swap_reaction_atom_maps(
550
+ aam_completion.mapped_reaction, atom_map_pair
551
+ )
552
+ if not ITSExpand.endpoint_constitutions_match(
553
+ aam_completion.mapped_reaction, swapped
554
+ ):
555
+ continue
556
+ try:
557
+ candidate_its, candidate_rsmi, candidate_cleaned, candidate_diag = (
558
+ build_its_from_rsmi(
559
+ swapped,
560
+ arrow_code,
561
+ expand_aam=False,
562
+ remove_non_arrow_maps=False,
563
+ )
564
+ )
565
+ candidate_typed = typed_convert_arrow_code(
566
+ arrow_code, candidate_its, electron_count=1
567
+ )
568
+ except Exception:
569
+ continue
570
+ repair_candidates.append(
571
+ (
572
+ atom_map_pair,
573
+ candidate_its,
574
+ candidate_rsmi,
575
+ candidate_cleaned,
576
+ candidate_diag,
577
+ candidate_typed,
578
+ )
579
+ )
580
+ if len(repair_candidates) != 1:
581
+ raise arrow_exc
582
+ (
583
+ equivalent_map_swap,
584
+ its,
585
+ expanded,
586
+ _cleaned,
587
+ _diagnostics,
588
+ typed,
589
+ ) = repair_candidates[0]
590
+ arrow_repair_assessment = "REACTANT_SYMMETRY_UNIQUE"
591
+ expansion_side = aam_completion.expansion_side
592
+ fallback_used = aam_completion.fallback_used
593
+ constitution_checked = aam_completion.constitution_preserved
594
+ hydrogens_folded = aam_completion.unmapped_explicit_hydrogens_folded
595
+ folded_hydrogen_count = aam_completion.folded_unmapped_explicit_hydrogen_count
596
+ stereochemistry_ignored = aam_completion.stereochemistry_ignored_for_expansion
597
+ radical_state_preserved = aam_completion.radical_state_preserved
598
+ is_state_relocation = (
599
+ len(typed) == 1
600
+ and len(typed[0][1]) == 1
601
+ and len(typed[0][2]) == 1
602
+ and str(typed[0][0]).lower().startswith("lp-/lp")
603
+ )
604
+ operational_macro = (
605
+ "LONE_PAIR_RADICAL_RELOCATION"
606
+ if is_state_relocation
607
+ else macro if enforce_source_macro else None
608
+ )
609
+ aliases = (
610
+ "LP→∙",
611
+ "Sigma→σ",
612
+ "Pi→π",
613
+ *flow_aliases,
614
+ *(
615
+ ("CompactAtomArrow→state-resolved:lp→lp",)
616
+ if is_state_relocation
617
+ else ()
618
+ ),
619
+ *(("Stereo→constitution-only",) if stereochemistry_ignored else ()),
620
+ *(
621
+ (
622
+ "EquivalentReactantMapSwap:"
623
+ f"{equivalent_map_swap[0]}↔{equivalent_map_swap[1]}",
624
+ )
625
+ if equivalent_map_swap
626
+ else ()
627
+ ),
628
+ )
629
+ group_id = f"g{row_number or 1}"
630
+ coupling_id = (
631
+ None
632
+ if operational_macro == "LONE_PAIR_RADICAL_RELOCATION"
633
+ else f"{source_class}-{row_number or 1}"
634
+ )
635
+ moves = tuple(
636
+ _fishhook_move(
637
+ typed_row,
638
+ group_id=group_id,
639
+ coupling_id=coupling_id,
640
+ event_id=f"e{index}",
641
+ )
642
+ for index, typed_row in enumerate(typed, start=1)
643
+ )
644
+ group = ElectronMoveGroup(
645
+ group_id,
646
+ moves,
647
+ macro=operational_macro,
648
+ metadata={"source_class": source_class},
649
+ )
650
+ grammar_issues = group.issues()
651
+ if grammar_issues:
652
+ return RadicalDatasetRecord(
653
+ source_row,
654
+ RadicalNormalizationReport(
655
+ row_number,
656
+ "QUARANTINED",
657
+ source_class,
658
+ macro,
659
+ aam_expanded=not was_complete,
660
+ aam_expansion_side=expansion_side,
661
+ aam_fallback_used=fallback_used,
662
+ constitution_checked=constitution_checked,
663
+ unmapped_explicit_hydrogens_folded=hydrogens_folded,
664
+ folded_unmapped_explicit_hydrogen_count=folded_hydrogen_count,
665
+ stereochemistry_ignored_for_expansion=stereochemistry_ignored,
666
+ radical_state_preserved=radical_state_preserved,
667
+ equivalent_map_swap=equivalent_map_swap,
668
+ arrow_repair_assessment=arrow_repair_assessment,
669
+ aliases_normalized=aliases,
670
+ issues=grammar_issues,
671
+ ),
672
+ )
673
+ report = RadicalNormalizationReport(
674
+ row_number,
675
+ "NORMALIZED",
676
+ source_class,
677
+ macro,
678
+ aam_expanded=not was_complete,
679
+ aam_expansion_side=expansion_side,
680
+ aam_fallback_used=fallback_used,
681
+ constitution_checked=constitution_checked,
682
+ unmapped_explicit_hydrogens_folded=hydrogens_folded,
683
+ folded_unmapped_explicit_hydrogen_count=folded_hydrogen_count,
684
+ stereochemistry_ignored_for_expansion=stereochemistry_ignored,
685
+ radical_state_preserved=radical_state_preserved,
686
+ equivalent_map_swap=equivalent_map_swap,
687
+ arrow_repair_assessment=arrow_repair_assessment,
688
+ aliases_normalized=aliases,
689
+ )
690
+ mechanism = MechanismRecord(
691
+ expanded,
692
+ (MechanisticStep("s1", (group,)),),
693
+ provenance={
694
+ "format": "synkit_radical_csv",
695
+ "source_row": row_number,
696
+ "temperature": source_row[1],
697
+ "stage": source_row[2],
698
+ "normalization": report.to_dict(),
699
+ },
700
+ )
701
+ return RadicalDatasetRecord(source_row, report, mechanism)
702
+ except Exception as exc:
703
+ message = str(exc)
704
+ issue = VerificationIssue(
705
+ _normalization_failure_code(message),
706
+ message,
707
+ observed=source_row[0] if source_row else None,
708
+ )
709
+ return RadicalDatasetRecord(
710
+ source_row,
711
+ RadicalNormalizationReport(
712
+ row_number,
713
+ "QUARANTINED",
714
+ source_class,
715
+ macro,
716
+ aliases_normalized=flow_aliases,
717
+ issues=(issue,),
718
+ ),
719
+ )
720
+
721
+
722
+ def iter_radical_csv(path: str | Path) -> Iterable[RadicalDatasetRecord]:
723
+ with Path(path).open(newline="", encoding="utf-8-sig") as handle:
724
+ for row_number, row in enumerate(csv.reader(handle), start=1):
725
+ if row:
726
+ yield normalize_radical_row(row, row_number=row_number)
727
+
728
+
729
+ def radical_dataset_summary(records: Iterable[RadicalDatasetRecord]) -> dict[str, Any]:
730
+ records = list(records)
731
+ return {
732
+ "total": len(records),
733
+ "accepted": sum(record.accepted for record in records),
734
+ "quarantined": sum(not record.accepted for record in records),
735
+ "classes": dict(Counter(record.report.source_class for record in records)),
736
+ "expanded_aam": sum(record.report.aam_expanded for record in records),
737
+ "issue_codes": dict(
738
+ Counter(issue.code for record in records for issue in record.report.issues)
739
+ ),
740
+ }