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,621 @@
1
+ """Electron-aware product-state helpers for the reactor facade.
2
+
3
+ This module is intentionally independent of SynReactor so the reactor remains
4
+ an orchestration facade rather than a dependency of its leaf helpers.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections import defaultdict
10
+ from typing import Any, Dict, Iterable, Mapping, Tuple
11
+
12
+ import networkx as nx
13
+ from rdkit import Chem
14
+
15
+ from synkit.Graph.Hyrogen._misc import implicit_hydrogen
16
+ from synkit.Graph.ITS.its_decompose import its_decompose
17
+ from synkit.Graph.ITS.its_reverter import ITSReverter
18
+ from synkit.Graph.Mech.electron_accounting import refresh_electron_fields
19
+ from synkit.IO.chem_converter import _get_preserved_hydrogen_maps
20
+ from synkit.IO.graph_to_mol import GraphToMol
21
+ from synkit.IO.mol_to_graph import MolToGraph
22
+
23
+ ITS_STRUCTURAL_EDGE_ATTRS = ["order", "kekule_order", "sigma_order", "pi_order"]
24
+
25
+
26
+ class ProductStatePerceptionError(RuntimeError):
27
+ """Raised when a dirty aromatic product cannot be safely reperceived."""
28
+
29
+
30
+ def _product_value(value: Any) -> Any:
31
+ """Return the product member of a scalar or endpoint pair."""
32
+ if isinstance(value, tuple) and len(value) == 2:
33
+ return value[1]
34
+ return value
35
+
36
+
37
+ def _electron_refresh_support( # noqa: C901
38
+ its: nx.Graph,
39
+ rewrite_nodes: Iterable[Any],
40
+ ) -> frozenset[Any]:
41
+ """Return the exact one-local support of product electron recomputation.
42
+
43
+ The derived state at a vertex depends only on its intrinsic node fields
44
+ and on the electron orders of incident edges. The rewrite implementation
45
+ writes intrinsic fields only at ``rewrite_nodes`` and writes edges only
46
+ between such nodes. A presence change can additionally remove every
47
+ external incident edge, so neighbors across that boundary are included
48
+ whenever the actual endpoint edge state differs. This is the complete
49
+ support of the local formal-charge functional, not a corpus assumption.
50
+ """
51
+
52
+ def side_values(value: Any) -> Tuple[Any, Any]:
53
+ if isinstance(value, tuple) and len(value) == 2:
54
+ return value
55
+ return value, value
56
+
57
+ def node_exists(node: Any, side: int) -> bool:
58
+ attrs = its.nodes[node]
59
+ present = attrs.get("present")
60
+ if isinstance(present, tuple) and len(present) == 2:
61
+ return bool(present[side])
62
+ return side_values(attrs.get("element"))[side] not in (None, "")
63
+
64
+ def edge_state(left: Any, right: Any, attrs: Mapping[str, Any], side: int) -> Any:
65
+ if not node_exists(left, side) or not node_exists(right, side):
66
+ return None
67
+ values = tuple(
68
+ side_values(attrs.get(name))[side]
69
+ for name in (
70
+ "order",
71
+ "kekule_order",
72
+ "sigma_order",
73
+ "pi_order",
74
+ "bond_type",
75
+ )
76
+ )
77
+ if all(value in (None, "", 0, 0.0) for value in values):
78
+ return None
79
+ return values
80
+
81
+ support = {node for node in rewrite_nodes if node in its}
82
+
83
+ # Every edge written by the rewrite has both endpoints in ``support``.
84
+ # Those endpoints are already scheduled, regardless of the edge delta.
85
+ # An edge crossing out of the mapped locus can therefore change endpoint
86
+ # state only when its mapped endpoint changes presence. Test only those
87
+ # boundary edges instead of rescanning every incident edge of every
88
+ # application. This is the same one-local support, derived directly from
89
+ # the rewrite locality invariant above.
90
+ presence_changes = []
91
+ for node in support:
92
+ if node_exists(node, 0) != node_exists(node, 1):
93
+ presence_changes.append(node)
94
+ for node in presence_changes:
95
+ for left, right, attrs in its.edges(node, data=True):
96
+ if left in support and right in support:
97
+ continue
98
+ if edge_state(left, right, attrs, 0) != edge_state(left, right, attrs, 1):
99
+ support.update((left, right))
100
+ return frozenset(support)
101
+
102
+
103
+ def _template_charge_is_authoritative(
104
+ its: nx.Graph,
105
+ node: Any,
106
+ product_attrs: Mapping[str, Any],
107
+ ) -> bool:
108
+ """Return whether the source state lies outside the local Lewis model.
109
+
110
+ Product charge is derived only when both parsed template endpoints satisfy
111
+ the same ``VE - NBE - B`` identity. An inconsistent endpoint is evidence
112
+ that the ordinary graph omits electronic context, so its exact charge is
113
+ retained in either replay direction. This is a representation invariant
114
+ and does not classify atoms by element.
115
+ """
116
+ if node not in its:
117
+ return True
118
+ attrs = its.nodes[node]
119
+ model_consistent = attrs.get("charge_model_consistent")
120
+ if (
121
+ isinstance(model_consistent, tuple)
122
+ and len(model_consistent) == 2
123
+ and all(isinstance(value, bool) for value in model_consistent)
124
+ ):
125
+ return not all(model_consistent)
126
+ present = attrs.get("present")
127
+ if isinstance(present, tuple) and len(present) == 2 and not present[0]:
128
+ return True
129
+
130
+ def reactant_value(value: Any) -> Any:
131
+ if isinstance(value, tuple) and len(value) == 2:
132
+ return value[0]
133
+ return value
134
+
135
+ required = ("valence_electrons", "lone_pairs", "radical", "hcount", "charge")
136
+ values = {name: reactant_value(attrs.get(name)) for name in required}
137
+ if any(value is None for value in values.values()):
138
+ return True
139
+
140
+ bond_sum = 0.0
141
+ for _, _, edge_attrs in its.edges(node, data=True):
142
+ sigma = reactant_value(edge_attrs.get("sigma_order"))
143
+ pi = reactant_value(edge_attrs.get("pi_order"))
144
+ if sigma is None or pi is None:
145
+ order = reactant_value(edge_attrs.get("kekule_order"))
146
+ if order is None:
147
+ order = reactant_value(edge_attrs.get("order"))
148
+ if order is None:
149
+ return True
150
+ bond_sum += float(order)
151
+ else:
152
+ bond_sum += float(sigma) + float(pi)
153
+
154
+ modeled_charge = (
155
+ float(values["valence_electrons"])
156
+ - 2.0 * float(values["lone_pairs"])
157
+ - float(values["radical"])
158
+ - float(values["hcount"])
159
+ - bond_sum
160
+ )
161
+ return modeled_charge != float(values["charge"])
162
+
163
+
164
+ def _pair_electron_aware_node_attrs(
165
+ host_n: Dict[str, Any],
166
+ rc_n: Dict[str, Any],
167
+ *,
168
+ preserve_unchanged_state: bool = False,
169
+ relative_resources: frozenset[str] = frozenset(),
170
+ ) -> None:
171
+ """Store paired attrs, preserving generic relative-query state locally."""
172
+ _, product_types = host_n["typesGH"]
173
+ rc_present = rc_n.get("present")
174
+ reactant_is_absent = (
175
+ isinstance(rc_present, tuple) and len(rc_present) == 2 and not rc_present[0]
176
+ )
177
+ legacy_product_values = {
178
+ "element": product_types[0],
179
+ "aromatic": product_types[1],
180
+ "hcount": product_types[2],
181
+ "neighbors": product_types[4],
182
+ }
183
+ model_consistent = rc_n.get("charge_model_consistent")
184
+ if isinstance(model_consistent, tuple) and len(model_consistent) == 2:
185
+ host_n["charge_model_consistent"] = (
186
+ bool(model_consistent[0]),
187
+ bool(model_consistent[1]),
188
+ )
189
+
190
+ for key, product_value in legacy_product_values.items():
191
+ left_value = host_n.get(key)
192
+ product_is_absent = (
193
+ isinstance(rc_present, tuple) and len(rc_present) == 2 and not rc_present[1]
194
+ )
195
+ rc_value = rc_n.get(key)
196
+ if reactant_is_absent and isinstance(rc_value, tuple) and len(rc_value) == 2:
197
+ product_value = rc_value[1]
198
+ if key == "element" and product_value == "*" and not product_is_absent:
199
+ product_value = left_value
200
+ host_n[key] = (left_value, product_value)
201
+
202
+ for key in ("radical", "lone_pairs", "valence_electrons"):
203
+ rc_value = rc_n.get(key)
204
+ if isinstance(rc_value, tuple) and len(rc_value) == 2:
205
+ left_value = host_n.get(key)
206
+ if left_value is None:
207
+ left_value = rc_value[0]
208
+ if key in relative_resources:
209
+ product_value = left_value - rc_value[0] + rc_value[1]
210
+ else:
211
+ product_value = (
212
+ left_value
213
+ if preserve_unchanged_state and rc_value[0] == rc_value[1]
214
+ else rc_value[1]
215
+ )
216
+ host_n[key] = (left_value, product_value)
217
+
218
+ host_n["template_charge"] = (host_n.get("charge"), product_types[3])
219
+
220
+ # Electron-authoritative RCs derive charge at the product boundary.
221
+ # Keep the reactant-side value temporarily so mutation does not copy
222
+ # the RC's product charge label.
223
+ host_n["charge"] = (host_n.get("charge"), host_n.get("charge"))
224
+
225
+ if "atom_map" in host_n:
226
+ host_n["atom_map"] = (host_n["atom_map"], host_n["atom_map"])
227
+ if isinstance(rc_present, tuple) and len(rc_present) == 2:
228
+ host_n["present"] = (bool(host_n.get("present", True)), rc_present[1])
229
+ else:
230
+ host_n["present"] = (True, True)
231
+
232
+
233
+ def _ensure_host_atom_maps(host: nx.Graph) -> None:
234
+ """Assign stable fresh atom maps to unmapped host atoms."""
235
+ used: set[int] = set()
236
+ for node, attrs in host.nodes(data=True):
237
+ atom_map = attrs.get("atom_map")
238
+ if not isinstance(atom_map, int) or atom_map <= 0:
239
+ continue
240
+ if atom_map in used:
241
+ raise ValueError(f"Duplicate atom map {atom_map} on host graph.")
242
+ used.add(atom_map)
243
+ fresh = max(used, default=0) + 1
244
+ for node, attrs in host.nodes(data=True):
245
+ if not isinstance(attrs.get("atom_map"), int) or attrs["atom_map"] <= 0:
246
+ while fresh in used:
247
+ fresh += 1
248
+ attrs["atom_map"] = fresh
249
+ used.add(fresh)
250
+ fresh += 1
251
+
252
+
253
+ def _refresh_product_electron_fields(
254
+ its: nx.Graph,
255
+ ) -> None:
256
+ """Refresh product-side electron fields from the scalar product graph."""
257
+ # Direct refresh is valid while the product Kekule phase is unchanged.
258
+ if not its.graph.get("_product_kekule_phase_dirty", True):
259
+ _refresh_product_electron_fields_direct(its)
260
+ return
261
+
262
+ product = _prepared_electron_product_graph(its)
263
+ refreshed = refresh_electron_fields(product)
264
+ structural_dirty_nodes = its.graph.get("_structural_exact_dirty_nodes")
265
+ structural_dirty_edges = its.graph.get("_structural_exact_dirty_edges")
266
+ for node, attrs in refreshed.nodes(data=True):
267
+ its.nodes[node].pop("_structural_exact_node_sig", None)
268
+ if isinstance(structural_dirty_nodes, set):
269
+ structural_dirty_nodes.add(node)
270
+ current_charge = its.nodes[node].get("charge")
271
+ left_charge = (
272
+ current_charge[0]
273
+ if isinstance(current_charge, tuple) and len(current_charge) == 2
274
+ else current_charge
275
+ )
276
+ product_charge = _electron_product_charge(its, node, attrs)
277
+ if product_charge is not None:
278
+ its.nodes[node]["charge"] = (left_charge, product_charge)
279
+
280
+ template_charge = its.nodes[node].get("template_charge")
281
+ if isinstance(template_charge, tuple) and len(template_charge) == 2:
282
+ attrs["charge_mismatch"] = template_charge[1] != attrs.get(
283
+ "recomputed_charge"
284
+ )
285
+
286
+ for key in ("bond_order_sum", "recomputed_charge", "charge_mismatch"):
287
+ if key in attrs:
288
+ current = its.nodes[node].get(key)
289
+ left_value = (
290
+ current[0]
291
+ if isinstance(current, tuple) and len(current) == 2
292
+ else current
293
+ )
294
+ its.nodes[node][key] = (left_value, attrs[key])
295
+ for u, v, attrs in refreshed.edges(data=True):
296
+ its.edges[u, v].pop("_structural_exact_edge_sig", None)
297
+ if isinstance(structural_dirty_edges, set):
298
+ structural_dirty_edges.add((u, v))
299
+ for key in ("kekule_order", "sigma_order", "pi_order"):
300
+ if key not in attrs:
301
+ continue
302
+ current = its.edges[u, v].get(key)
303
+ left_value = (
304
+ current[0]
305
+ if isinstance(current, tuple) and len(current) == 2
306
+ else current
307
+ )
308
+ its.edges[u, v][key] = (left_value, attrs[key])
309
+ its.graph["_product_electron_fields_current"] = True
310
+
311
+
312
+ def _refresh_product_electron_fields_direct(its: nx.Graph) -> None: # noqa: C901
313
+ """Refresh derived product fields without materialising a side graph.
314
+
315
+ This path is exact while the aromatic Kekule phase is unchanged.
316
+ Explicit hydrogen collapse preserves the heavy atom's electron count:
317
+ a removed H--X sigma bond becomes one unit of ``hcount``.
318
+ """
319
+
320
+ def product_node_exists(attrs: Mapping[str, Any]) -> bool:
321
+ present = attrs.get("present")
322
+ if isinstance(present, tuple) and len(present) == 2:
323
+ return bool(present[1])
324
+ return _product_value(attrs.get("element")) not in (None, "")
325
+
326
+ def product_edge_exists(attrs: Mapping[str, Any]) -> bool:
327
+ for name in ("order", "kekule_order", "bond_type"):
328
+ value = _product_value(attrs.get(name))
329
+ if value not in (None, "", 0, 0.0):
330
+ return True
331
+ return False
332
+
333
+ refresh_hint = its.graph.get("_product_refresh_nodes")
334
+ candidate_nodes = (
335
+ tuple(its)
336
+ if refresh_hint is None
337
+ else tuple(node for node in refresh_hint if node in its)
338
+ )
339
+ product_nodes = {
340
+ node for node in candidate_nodes if product_node_exists(its.nodes[node])
341
+ }
342
+
343
+ # Every changed edge is incident to a mapped rule node. Build the exact
344
+ # local edge population once; unchanged edges elsewhere already carry
345
+ # valid scalar host fields and need no tuple-side refresh.
346
+ product_edges = []
347
+ structural_dirty_nodes = its.graph.get("_structural_exact_dirty_nodes")
348
+ structural_dirty_edges = its.graph.get("_structural_exact_dirty_edges")
349
+ seen_edges: set[frozenset[Any]] = set()
350
+ for node in product_nodes:
351
+ for left, right, attrs in its.edges(node, data=True):
352
+ edge_key = frozenset((left, right))
353
+ if edge_key in seen_edges:
354
+ continue
355
+ seen_edges.add(edge_key)
356
+ if (
357
+ left in its
358
+ and right in its
359
+ and product_node_exists(its.nodes[left])
360
+ and product_node_exists(its.nodes[right])
361
+ and product_edge_exists(attrs)
362
+ ):
363
+ product_edges.append((left, right, attrs))
364
+
365
+ bond_sums: Dict[Any, float] = defaultdict(float)
366
+ for left, right, attrs in product_edges:
367
+ attrs.pop("_structural_exact_edge_sig", None)
368
+ if isinstance(structural_dirty_edges, set):
369
+ structural_dirty_edges.add((left, right))
370
+ sigma = float(_product_value(attrs.get("sigma_order", 0.0)) or 0.0)
371
+ pi = float(_product_value(attrs.get("pi_order", 0.0)) or 0.0)
372
+ bond_order = sigma + pi
373
+ bond_sums[left] += bond_order
374
+ bond_sums[right] += bond_order
375
+
376
+ current = attrs.get("kekule_order")
377
+ left_value = (
378
+ current[0] if isinstance(current, tuple) and len(current) == 2 else current
379
+ )
380
+ attrs["kekule_order"] = (left_value, bond_order)
381
+
382
+ for node in product_nodes:
383
+ attrs = its.nodes[node]
384
+ attrs.pop("_structural_exact_node_sig", None)
385
+ if isinstance(structural_dirty_nodes, set):
386
+ structural_dirty_nodes.add(node)
387
+ bond_sum = bond_sums[node]
388
+ current_bond_sum = attrs.get("bond_order_sum")
389
+ left_bond_sum = (
390
+ current_bond_sum[0]
391
+ if isinstance(current_bond_sum, tuple) and len(current_bond_sum) == 2
392
+ else current_bond_sum
393
+ )
394
+ attrs["bond_order_sum"] = (left_bond_sum, bond_sum)
395
+
396
+ valence_electrons = _product_value(attrs.get("valence_electrons"))
397
+ if valence_electrons is None:
398
+ continue
399
+ lone_pairs = float(_product_value(attrs.get("lone_pairs", 0)) or 0)
400
+ radical = float(_product_value(attrs.get("radical", 0)) or 0)
401
+ hcount = float(_product_value(attrs.get("hcount", 0)) or 0)
402
+ recomputed_charge = (
403
+ float(valence_electrons) - 2.0 * lone_pairs - radical - hcount - bond_sum
404
+ )
405
+ if recomputed_charge.is_integer():
406
+ recomputed_charge = int(recomputed_charge)
407
+
408
+ current_recomputed = attrs.get("recomputed_charge")
409
+ left_recomputed = (
410
+ current_recomputed[0]
411
+ if isinstance(current_recomputed, tuple) and len(current_recomputed) == 2
412
+ else current_recomputed
413
+ )
414
+ attrs["recomputed_charge"] = (left_recomputed, recomputed_charge)
415
+
416
+ template_charge = attrs.get("template_charge")
417
+ represented_charge = _product_value(attrs.get("charge", 0))
418
+ mismatch = float(represented_charge or 0) != recomputed_charge
419
+ if isinstance(template_charge, tuple) and len(template_charge) == 2:
420
+ mismatch = template_charge[1] != recomputed_charge
421
+ current_mismatch = attrs.get("charge_mismatch")
422
+ left_mismatch = (
423
+ current_mismatch[0]
424
+ if isinstance(current_mismatch, tuple) and len(current_mismatch) == 2
425
+ else current_mismatch
426
+ )
427
+ attrs["charge_mismatch"] = (left_mismatch, mismatch)
428
+
429
+ current_charge = attrs.get("charge")
430
+ left_charge = (
431
+ current_charge[0]
432
+ if isinstance(current_charge, tuple) and len(current_charge) == 2
433
+ else current_charge
434
+ )
435
+ product_charge = _electron_product_charge(
436
+ its,
437
+ node,
438
+ {
439
+ "aromatic": bool(_product_value(attrs.get("aromatic", False))),
440
+ "element": _product_value(attrs.get("element")),
441
+ "recomputed_charge": recomputed_charge,
442
+ },
443
+ )
444
+ attrs["charge"] = (left_charge, product_charge)
445
+
446
+ its.graph["_product_electron_fields_current"] = True
447
+
448
+
449
+ def _product_kekule_phase_is_dirty(its: nx.Graph) -> bool:
450
+ """Return whether a rewrite can invalidate an aromatic Kekule phase.
451
+
452
+ Substituent and hydrogen-count edits do not alter the alternating phase
453
+ inside an aromatic system. Electronic changes on aromatic atoms and
454
+ edits to bonds within that system do, and therefore still require full
455
+ RDKit re-perception.
456
+ """
457
+
458
+ def side_values(value: Any) -> Tuple[Any, Any]:
459
+ if isinstance(value, tuple) and len(value) == 2:
460
+ return value
461
+ return value, value
462
+
463
+ refresh_hint = its.graph.get("_product_refresh_nodes")
464
+ candidate_nodes = (
465
+ tuple(its)
466
+ if refresh_hint is None
467
+ else tuple(node for node in refresh_hint if node in its)
468
+ )
469
+ aromatic_nodes = {
470
+ node
471
+ for node in candidate_nodes
472
+ if any(
473
+ bool(value) for value in side_values(its.nodes[node].get("aromatic", False))
474
+ )
475
+ }
476
+ if not aromatic_nodes:
477
+ return False
478
+
479
+ for node in aromatic_nodes:
480
+ attrs = its.nodes[node]
481
+ for name in (
482
+ "element",
483
+ "aromatic",
484
+ "radical",
485
+ "lone_pairs",
486
+ "valence_electrons",
487
+ "present",
488
+ "template_charge",
489
+ ):
490
+ left, right = side_values(attrs.get(name))
491
+ if left != right:
492
+ return True
493
+
494
+ seen_edges: set[frozenset[Any]] = set()
495
+ for node in aromatic_nodes:
496
+ for left, right, attrs in its.edges(node, data=True):
497
+ edge_key = frozenset((left, right))
498
+ if edge_key in seen_edges:
499
+ continue
500
+ seen_edges.add(edge_key)
501
+ if left not in aromatic_nodes or right not in aromatic_nodes:
502
+ continue
503
+ for name in ITS_STRUCTURAL_EDGE_ATTRS:
504
+ before, after = side_values(attrs.get(name))
505
+ if before != after:
506
+ return True
507
+ return False
508
+
509
+
510
+ def _prepared_electron_product_graph(its: nx.Graph) -> nx.Graph:
511
+ """Build the scalar product graph used for electron recomputation."""
512
+ product = ITSReverter(its).to_product_graph()
513
+ preserved_hydrogens = _get_preserved_hydrogen_maps(its, "tuple")
514
+ product = implicit_hydrogen(product, set(preserved_hydrogens))
515
+ return _reperceive_product_kekule_phase(product, its)
516
+
517
+
518
+ def _electron_product_charge(
519
+ its: nx.Graph,
520
+ node: Any,
521
+ product_attrs: Mapping[str, Any],
522
+ ) -> Any:
523
+ """Choose the product charge used for electron-aware serialization.
524
+
525
+ Recompute charge when the template endpoints satisfy the local Lewis
526
+ identity. Preserve explicit template charge for aromatic or
527
+ model-inconsistent endpoints.
528
+ """
529
+ if node in its:
530
+ template_charge = its.nodes[node].get("template_charge")
531
+ aromatic = product_attrs.get("aromatic", its.nodes[node].get("aromatic"))
532
+ if isinstance(template_charge, tuple) and len(template_charge) == 2:
533
+ if aromatic is True or _template_charge_is_authoritative(
534
+ its,
535
+ node,
536
+ product_attrs,
537
+ ):
538
+ return template_charge[1]
539
+ else:
540
+ return _product_value(its.nodes[node].get("charge"))
541
+ return product_attrs.get("recomputed_charge")
542
+
543
+
544
+ def _reperceive_product_kekule_phase( # noqa: C901
545
+ product: nx.Graph,
546
+ its: nx.Graph,
547
+ ) -> nx.Graph:
548
+ """Refresh aromatic sigma/pi phase from full product presentation bonds."""
549
+ if not any(data.get("order") == 1.5 for _, _, data in product.edges(data=True)):
550
+ return product
551
+ if not its.graph.get("_product_kekule_phase_dirty", True):
552
+ return product
553
+
554
+ probe = product.copy()
555
+ for node, attrs in probe.nodes(data=True):
556
+ template_charge = its.nodes[node].get("template_charge")
557
+ if isinstance(template_charge, tuple) and len(template_charge) == 2:
558
+ attrs["charge"] = template_charge[1]
559
+
560
+ try:
561
+ mol = GraphToMol(edge_attributes={"order": "order"}).graph_to_mol(
562
+ probe,
563
+ sanitize=True,
564
+ use_h_count=True,
565
+ prefer_kekule_order=False,
566
+ )
567
+ except Exception as exc:
568
+ raise ProductStatePerceptionError(
569
+ "Could not reperceive the dirty aromatic product state."
570
+ ) from exc
571
+
572
+ refreshed = product.copy()
573
+ try:
574
+ # Only three edge fields are needed here. Building a complete
575
+ # MolToGraph representation also derives every atom descriptor and
576
+ # stereo registry, which is substantially more work. Read the
577
+ # Kekule bond phase directly from a sanitized RDKit copy instead.
578
+ kekule = Chem.Mol(mol)
579
+ Chem.Kekulize(kekule, clearAromaticFlags=True)
580
+ node_by_index = tuple(
581
+ atom.GetAtomMapNum() or atom.GetIdx() + 1 for atom in kekule.GetAtoms()
582
+ )
583
+ if len(node_by_index) != len(set(node_by_index)) or set(node_by_index) != set(
584
+ refreshed
585
+ ):
586
+ raise ValueError("Sanitized product atom identities are not bijective.")
587
+ for bond in kekule.GetBonds():
588
+ left = node_by_index[bond.GetBeginAtomIdx()]
589
+ right = node_by_index[bond.GetEndAtomIdx()]
590
+ if not refreshed.has_edge(left, right):
591
+ raise ValueError("Sanitized product bond is absent from the graph.")
592
+ order = float(bond.GetBondTypeAsDouble())
593
+ refreshed[left][right]["kekule_order"] = order
594
+ refreshed[left][right]["sigma_order"] = 1.0 if order > 0 else 0.0
595
+ refreshed[left][right]["pi_order"] = max(0.0, order - 1.0)
596
+ except Exception:
597
+ # Preserve the established conservative path for unusual third-party
598
+ # graphs whose atom maps cannot identify the sanitized RDKit bonds.
599
+ try:
600
+ reperceived = MolToGraph(attr_profile="minimal").transform(
601
+ mol,
602
+ use_index_as_atom_map=True,
603
+ )
604
+ except Exception as exc:
605
+ raise ProductStatePerceptionError(
606
+ "Could not reperceive the dirty aromatic product state."
607
+ ) from exc
608
+ for u, v in refreshed.edges():
609
+ if not reperceived.has_edge(u, v):
610
+ continue
611
+ for key in ("kekule_order", "sigma_order", "pi_order"):
612
+ if key in reperceived[u][v]:
613
+ refreshed[u][v][key] = reperceived[u][v][key]
614
+ return refreshed
615
+
616
+
617
+ def _product_graph_for_diagnostics(its: nx.Graph) -> nx.Graph:
618
+ """Return the product graph matching the rewrite representation."""
619
+ if its.graph.get("electron_aware_rewrite", False):
620
+ return ITSReverter(its).to_product_graph()
621
+ return its_decompose(its)[1]