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,1060 @@
1
+ """Typed validation contracts for wildcard-based reaction fusion.
2
+
3
+ The validator in this module is deliberately independent of a particular
4
+ fusion search strategy. It checks invariants that every RBL exit path must
5
+ obey and returns structured issues instead of relying on logs or exceptions.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections import Counter
11
+ from dataclasses import dataclass, field
12
+ from enum import Enum
13
+ from typing import Any, Mapping, Sequence
14
+
15
+ import networkx as nx
16
+ from rdkit import Chem
17
+
18
+ from synkit.Graph.Morphism.constraints import (
19
+ NodeStateKind,
20
+ WildcardRole,
21
+ adapt_legacy_node_state,
22
+ )
23
+ from synkit.Graph.Fusion.identity import (
24
+ graph_identity_digest,
25
+ graphs_exactly_equivalent,
26
+ )
27
+ from synkit.Graph.Stereo import (
28
+ mapped_stereo_subgraph_registries_match,
29
+ stereo_registry_layers,
30
+ )
31
+ from synkit.IO.mol_to_graph import MolToGraph
32
+ from synkit.Synthesis.Reactor import Strategy
33
+
34
+ _ENDPOINT_NODE_ATTRS = (
35
+ "element",
36
+ "isotope",
37
+ "charge",
38
+ "aromatic",
39
+ "hcount",
40
+ "lone_pairs",
41
+ "radical",
42
+ )
43
+ _ENDPOINT_EDGE_ATTRS = ("order", "sigma_order", "pi_order", "aromatic")
44
+
45
+
46
+ def _mapped_smiles_mol(smiles: str) -> Chem.Mol | None:
47
+ """Parse mapped SMILES without discarding heavy-atom-bound H identities."""
48
+ parameters = Chem.SmilesParserParams()
49
+ parameters.removeHs = False
50
+ return Chem.MolFromSmiles(smiles, parameters)
51
+
52
+
53
+ class FusionIssueCode(str, Enum):
54
+ """Stable machine-readable issue codes emitted by fusion validation."""
55
+
56
+ INVALID_REACTION = "FUSION_INVALID_REACTION"
57
+ PARSE_FAILURE = "FUSION_PARSE_FAILURE"
58
+ DANGLING_WILDCARD = "FUSION_DANGLING_WILDCARD"
59
+ SIDE_ONLY_STANDALONE_HYDROGEN = "FUSION_SIDE_ONLY_STANDALONE_HYDROGEN"
60
+ DUPLICATE_ATOM_MAP = "FUSION_DUPLICATE_ATOM_MAP"
61
+ ATOM_MAP_IMBALANCE = "FUSION_ATOM_MAP_IMBALANCE"
62
+ ELEMENT_MAP_CONFLICT = "FUSION_ELEMENT_MAP_CONFLICT"
63
+ ISOTOPE_MAP_CONFLICT = "FUSION_ISOTOPE_MAP_CONFLICT"
64
+ HYDROGEN_MAP_IMBALANCE = "FUSION_HYDROGEN_MAP_IMBALANCE"
65
+ WILDCARD_ROLE_CONFLICT = "FUSION_WILDCARD_ROLE_CONFLICT"
66
+ INTERFACE_INVALID = "FUSION_INTERFACE_INVALID"
67
+ CONSTRUCTION_INVALID = "FUSION_CONSTRUCTION_INVALID"
68
+ PROOF_FAILED = "FUSION_PROOF_FAILED"
69
+ OPERATION_FAILED = "FUSION_OPERATION_FAILED"
70
+ SERIALIZATION_FAILED = "FUSION_SERIALIZATION_FAILED"
71
+ POSTPROCESS_FAILED = "FUSION_POSTPROCESS_FAILED"
72
+ REACTANT_ENDPOINT_NOT_PRESERVED = "FUSION_REACTANT_ENDPOINT_NOT_PRESERVED"
73
+ PRODUCT_ENDPOINT_NOT_PRESERVED = "FUSION_PRODUCT_ENDPOINT_NOT_PRESERVED"
74
+ REACTANT_COMPONENT_NOT_PRESERVED = (
75
+ "FUSION_REACTANT_COMPONENT_NOT_PRESERVED"
76
+ )
77
+ PRODUCT_COMPONENT_NOT_PRESERVED = "FUSION_PRODUCT_COMPONENT_NOT_PRESERVED"
78
+ ELEMENT_ISOTOPE_IMBALANCE = "FUSION_ELEMENT_ISOTOPE_IMBALANCE"
79
+ CHARGE_IMBALANCE = "FUSION_CHARGE_IMBALANCE"
80
+ UNMAPPED_MATERIAL_ATOM = "FUSION_UNMAPPED_MATERIAL_ATOM"
81
+ ENVIRONMENT_DELTA_MISMATCH = "FUSION_ENVIRONMENT_DELTA_MISMATCH"
82
+
83
+
84
+ @dataclass(frozen=True)
85
+ class FusionIssue:
86
+ """One failed fusion invariant."""
87
+
88
+ code: FusionIssueCode
89
+ stage: str
90
+ message: str
91
+ context: Mapping[str, Any] = field(default_factory=dict)
92
+
93
+ def to_dict(self) -> dict[str, Any]:
94
+ """Return a JSON-serialisable representation."""
95
+ return {
96
+ "code": self.code.value,
97
+ "stage": self.stage,
98
+ "message": self.message,
99
+ "context": dict(self.context),
100
+ }
101
+
102
+
103
+ @dataclass(frozen=True)
104
+ class FusionValidation:
105
+ """Validation outcome shared by every RBL execution mode."""
106
+
107
+ valid: bool
108
+ stage: str = "fusion"
109
+ issues: tuple[FusionIssue, ...] = ()
110
+ evidence: Mapping[str, Any] = field(default_factory=dict)
111
+
112
+ def to_dict(self) -> dict[str, Any]:
113
+ """Return a JSON-serialisable representation."""
114
+ return {
115
+ "valid": self.valid,
116
+ "stage": self.stage,
117
+ "issues": [issue.to_dict() for issue in self.issues],
118
+ "evidence": dict(self.evidence),
119
+ }
120
+
121
+
122
+ def _issue(
123
+ code: FusionIssueCode,
124
+ message: str,
125
+ **context: Any,
126
+ ) -> FusionIssue:
127
+ return FusionIssue(
128
+ code=code,
129
+ stage="fusion",
130
+ message=message,
131
+ context=context,
132
+ )
133
+
134
+
135
+ def _mapped_elements(mol: Chem.Mol) -> tuple[dict[int, str], list[int]]:
136
+ elements: dict[int, str] = {}
137
+ maps: list[int] = []
138
+ for atom in mol.GetAtoms():
139
+ atom_map = int(atom.GetAtomMapNum())
140
+ if atom_map <= 0:
141
+ continue
142
+ maps.append(atom_map)
143
+ elements[atom_map] = atom.GetSymbol()
144
+ return elements, maps
145
+
146
+
147
+ def _mapped_isotopes(mol: Chem.Mol) -> dict[int, int]:
148
+ return {
149
+ int(atom.GetAtomMapNum()): int(atom.GetIsotope())
150
+ for atom in mol.GetAtoms()
151
+ if atom.GetAtomMapNum() > 0
152
+ }
153
+
154
+
155
+ def _unmapped_standalone_hydrogens(mol: Chem.Mol) -> Counter[str]:
156
+ """Count untraceable isolated H atoms by isotope/charge/radical state.
157
+
158
+ A mapped H may legitimately move from a standalone proton or hydride into
159
+ a bond; its conserved atom-map identity proves that it was transferred.
160
+ An unmapped isolated H has no such identity and must remain endpoint
161
+ symmetric to cross the verified representation boundary.
162
+ """
163
+ return Counter(
164
+ f"isotope={atom.GetIsotope()},charge={atom.GetFormalCharge()},"
165
+ f"radical={atom.GetNumRadicalElectrons()}"
166
+ for atom in mol.GetAtoms()
167
+ if (
168
+ atom.GetAtomicNum() == 1
169
+ and atom.GetDegree() == 0
170
+ and atom.GetAtomMapNum() <= 0
171
+ )
172
+ )
173
+
174
+
175
+ def _mapped_identity_issues(
176
+ endpoints: Sequence[tuple[str, Chem.Mol]],
177
+ ) -> list[FusionIssue]:
178
+ """Validate uniqueness, side symmetry, and element-stable mapped atoms."""
179
+ issues: list[FusionIssue] = []
180
+ endpoint_elements: list[dict[int, str]] = []
181
+ for side, mol in endpoints:
182
+ elements, maps = _mapped_elements(mol)
183
+ endpoint_elements.append(elements)
184
+ duplicates = sorted(
185
+ atom_map for atom_map, count in Counter(maps).items() if count > 1
186
+ )
187
+ if duplicates:
188
+ issues.append(
189
+ _issue(
190
+ FusionIssueCode.DUPLICATE_ATOM_MAP,
191
+ "An atom-map identity occurs more than once on one endpoint.",
192
+ side=side,
193
+ atom_maps=duplicates,
194
+ )
195
+ )
196
+
197
+ reactant_elements, product_elements = endpoint_elements
198
+ reactant_maps = set(reactant_elements)
199
+ product_maps = set(product_elements)
200
+ if reactant_maps != product_maps:
201
+ issues.append(
202
+ _issue(
203
+ FusionIssueCode.ATOM_MAP_IMBALANCE,
204
+ "Mapped atom identities are not side-symmetric.",
205
+ reactants_only=sorted(reactant_maps - product_maps),
206
+ products_only=sorted(product_maps - reactant_maps),
207
+ )
208
+ )
209
+ conflicts = {
210
+ atom_map: (reactant_elements[atom_map], product_elements[atom_map])
211
+ for atom_map in sorted(reactant_maps & product_maps)
212
+ if reactant_elements[atom_map] != product_elements[atom_map]
213
+ }
214
+ if conflicts:
215
+ issues.append(
216
+ _issue(
217
+ FusionIssueCode.ELEMENT_MAP_CONFLICT,
218
+ "Mapped atom identities change element across the reaction.",
219
+ conflicts=conflicts,
220
+ )
221
+ )
222
+
223
+ endpoint_isotopes = [_mapped_isotopes(mol) for _, mol in endpoints]
224
+ isotope_conflicts = {
225
+ atom_map: (endpoint_isotopes[0][atom_map], endpoint_isotopes[1][atom_map])
226
+ for atom_map in sorted(reactant_maps & product_maps)
227
+ if endpoint_isotopes[0][atom_map] != endpoint_isotopes[1][atom_map]
228
+ }
229
+ if isotope_conflicts:
230
+ issues.append(
231
+ _issue(
232
+ FusionIssueCode.ISOTOPE_MAP_CONFLICT,
233
+ "Mapped atom identities change isotope across the reaction.",
234
+ conflicts=isotope_conflicts,
235
+ )
236
+ )
237
+
238
+ hydrogen_maps = [
239
+ {atom_map for atom_map, element in elements.items() if element == "H"}
240
+ for elements in endpoint_elements
241
+ ]
242
+ if hydrogen_maps[0] != hydrogen_maps[1]:
243
+ issues.append(
244
+ _issue(
245
+ FusionIssueCode.HYDROGEN_MAP_IMBALANCE,
246
+ "Mapped explicit hydrogen identities are not side-symmetric.",
247
+ reactants=sorted(hydrogen_maps[0]),
248
+ products=sorted(hydrogen_maps[1]),
249
+ )
250
+ )
251
+ return issues
252
+
253
+
254
+ def validate_fusion_rsmi(
255
+ rsmi: str,
256
+ *,
257
+ allow_wildcards: bool = False,
258
+ ) -> FusionValidation:
259
+ """Validate endpoint invariants of a fused reaction SMILES.
260
+
261
+ This is not a reaction-balancing oracle. It guards the representation
262
+ boundary most vulnerable during RBL fusion: parseability, unique and
263
+ side-symmetric atom-map identities, element preservation, no side-only
264
+ *unmapped* isolated H, and no unresolved wildcard unless explicitly
265
+ requested by the caller. A mapped isolated H may become bound because its
266
+ symmetric map identity already certifies atom conservation.
267
+ """
268
+ if not isinstance(rsmi, str) or rsmi.count(">>") != 1:
269
+ issue = _issue(
270
+ FusionIssueCode.INVALID_REACTION,
271
+ "Expected exactly one '>>' reaction separator.",
272
+ )
273
+ return FusionValidation(valid=False, issues=(issue,))
274
+
275
+ reactants, products = rsmi.split(">>", 1)
276
+ mols: list[Chem.Mol] = []
277
+ issues: list[FusionIssue] = []
278
+ for side, smiles in (("reactants", reactants), ("products", products)):
279
+ mol = _mapped_smiles_mol(smiles)
280
+ if mol is None:
281
+ issues.append(
282
+ _issue(
283
+ FusionIssueCode.PARSE_FAILURE,
284
+ f"RDKit could not parse the {side} endpoint.",
285
+ side=side,
286
+ )
287
+ )
288
+ else:
289
+ mols.append(mol)
290
+
291
+ if issues:
292
+ return FusionValidation(valid=False, issues=tuple(issues))
293
+
294
+ reactant_mol, product_mol = mols
295
+ if not allow_wildcards:
296
+ for side, mol in (
297
+ ("reactants", reactant_mol),
298
+ ("products", product_mol),
299
+ ):
300
+ wildcard_maps = [
301
+ int(atom.GetAtomMapNum())
302
+ for atom in mol.GetAtoms()
303
+ if atom.GetAtomicNum() == 0
304
+ ]
305
+ if wildcard_maps:
306
+ issues.append(
307
+ _issue(
308
+ FusionIssueCode.DANGLING_WILDCARD,
309
+ "An unresolved wildcard crossed the final fusion boundary.",
310
+ side=side,
311
+ atom_maps=wildcard_maps,
312
+ )
313
+ )
314
+
315
+ issues.extend(
316
+ _mapped_identity_issues(
317
+ (("reactants", reactant_mol), ("products", product_mol))
318
+ )
319
+ )
320
+
321
+ reactant_standalone = _unmapped_standalone_hydrogens(reactant_mol)
322
+ product_standalone = _unmapped_standalone_hydrogens(product_mol)
323
+ if reactant_standalone != product_standalone:
324
+ issues.append(
325
+ _issue(
326
+ FusionIssueCode.SIDE_ONLY_STANDALONE_HYDROGEN,
327
+ "Standalone explicit hydrogen fragments differ between endpoints.",
328
+ reactants=dict(reactant_standalone),
329
+ products=dict(product_standalone),
330
+ )
331
+ )
332
+
333
+ return FusionValidation(valid=not issues, issues=tuple(issues))
334
+
335
+
336
+ def _parse_unmapped_endpoint_graph(side: str) -> nx.Graph | None:
337
+ """Parse one endpoint into SynKit's map-independent molecular graph."""
338
+ if not side:
339
+ graph = nx.Graph()
340
+ graph.graph["stereo_descriptors"] = {}
341
+ return graph
342
+ mol = Chem.MolFromSmiles(side)
343
+ if mol is None:
344
+ return None
345
+ mol = Chem.Mol(mol)
346
+ for atom in mol.GetAtoms():
347
+ atom.SetAtomMapNum(0)
348
+ # Endpoint preservation is a molecular-graph claim, independent of
349
+ # whether a bound hydrogen was serialized explicitly to retain its AAM
350
+ # identity. Clear AAM first, then fold only RDKit-removable bound H; H2
351
+ # and standalone hydrogen components remain explicit.
352
+ mol = Chem.RemoveHs(mol)
353
+ return MolToGraph(
354
+ node_attrs=list(_ENDPOINT_NODE_ATTRS),
355
+ edge_attrs=list(_ENDPOINT_EDGE_ATTRS),
356
+ attr_profile="minimal",
357
+ ).transform(mol)
358
+
359
+
360
+ def _endpoint_embedding_proof(
361
+ original_side: str,
362
+ candidate_side: str,
363
+ ) -> list[dict[str, Any]] | None:
364
+ """Find a component-injective exact subgraph embedding.
365
+
366
+ Every original molecule must embed into a distinct candidate molecule.
367
+ This prevents two observed species from being collapsed into one newly
368
+ connected component while still allowing RBL to add missing components.
369
+ Atom and bond queries are exact RDKit graph queries with specified
370
+ chirality enabled; atom-map numbers are representation labels and are
371
+ therefore excluded from the match relation.
372
+ """
373
+ # Keep this import local so validation that never reaches the endpoint
374
+ # proof does not initialize the general subgraph-matching stack.
375
+ from synkit.Graph.Matcher.subgraph_matcher import SubgraphSearchEngine
376
+
377
+ original = _parse_unmapped_endpoint_graph(original_side)
378
+ candidate = _parse_unmapped_endpoint_graph(candidate_side)
379
+ if original is None or candidate is None:
380
+ return None
381
+
382
+ query_has_stereo = bool(stereo_registry_layers(original))
383
+ mappings = SubgraphSearchEngine.find_subgraph_mappings(
384
+ candidate,
385
+ original,
386
+ node_attrs=list(_ENDPOINT_NODE_ATTRS),
387
+ edge_attrs=list(_ENDPOINT_EDGE_ATTRS),
388
+ strategy=Strategy.COMPONENT,
389
+ max_results=None if query_has_stereo else 1,
390
+ strict_cc_count=False,
391
+ threshold=50_000,
392
+ )
393
+ mapping = next(
394
+ (
395
+ structural_mapping
396
+ for structural_mapping in mappings
397
+ if mapped_stereo_subgraph_registries_match(
398
+ original,
399
+ candidate,
400
+ structural_mapping,
401
+ )
402
+ ),
403
+ None,
404
+ )
405
+ if mapping is None:
406
+ return None
407
+
408
+ original_components = [
409
+ set(component) for component in nx.connected_components(original)
410
+ ]
411
+ candidate_components = [
412
+ set(component) for component in nx.connected_components(candidate)
413
+ ]
414
+ candidate_component_by_node = {
415
+ node: component_index
416
+ for component_index, component in enumerate(candidate_components)
417
+ for node in component
418
+ }
419
+ proof: list[dict[str, Any]] = []
420
+ for original_index, component in enumerate(original_components):
421
+ ordered_nodes = sorted(component, key=repr)
422
+ atom_image = [mapping[node] for node in ordered_nodes]
423
+ candidate_component = candidate_component_by_node[atom_image[0]]
424
+ proof.append(
425
+ {
426
+ "original_component": original_index,
427
+ "candidate_component": candidate_component,
428
+ "atom_mapping": [
429
+ [query_node, host_node]
430
+ for query_node, host_node in zip(ordered_nodes, atom_image)
431
+ ],
432
+ }
433
+ )
434
+ return proof
435
+
436
+
437
+ def validate_endpoint_preservation(
438
+ original_rsmi: str,
439
+ candidate_rsmi: str,
440
+ *,
441
+ required_sides: Sequence[str] = ("reactants", "products"),
442
+ ) -> FusionValidation:
443
+ """Prove component-injective endpoint embeddings on selected sides."""
444
+ if original_rsmi.count(">>") != 1 or candidate_rsmi.count(">>") != 1:
445
+ issue = _issue(
446
+ FusionIssueCode.INVALID_REACTION,
447
+ "Endpoint preservation requires two valid reaction separators.",
448
+ )
449
+ return FusionValidation(valid=False, issues=(issue,))
450
+
451
+ original_reactants, original_products = original_rsmi.split(">>", 1)
452
+ candidate_reactants, candidate_products = candidate_rsmi.split(">>", 1)
453
+ required = set(required_sides)
454
+ unknown = required - {"reactants", "products"}
455
+ if unknown:
456
+ raise ValueError(f"Unknown endpoint side(s): {sorted(unknown)!r}")
457
+
458
+ reactant_proof = (
459
+ _endpoint_embedding_proof(original_reactants, candidate_reactants)
460
+ if "reactants" in required
461
+ else []
462
+ )
463
+ product_proof = (
464
+ _endpoint_embedding_proof(original_products, candidate_products)
465
+ if "products" in required
466
+ else []
467
+ )
468
+ issues: list[FusionIssue] = []
469
+ if "reactants" in required and reactant_proof is None:
470
+ issues.append(
471
+ _issue(
472
+ FusionIssueCode.REACTANT_ENDPOINT_NOT_PRESERVED,
473
+ "The original reactant graph does not embed component-wise "
474
+ "in the candidate reactant graph.",
475
+ )
476
+ )
477
+ if "products" in required and product_proof is None:
478
+ issues.append(
479
+ _issue(
480
+ FusionIssueCode.PRODUCT_ENDPOINT_NOT_PRESERVED,
481
+ "The original product graph does not embed component-wise "
482
+ "in the candidate product graph.",
483
+ )
484
+ )
485
+
486
+ evidence: dict[str, Any] = {}
487
+ evidence["matcher"] = "synkit.SubgraphSearchEngine"
488
+ evidence["stereo_policy"] = "synkit.relative_stereo_subgraph"
489
+ if "reactants" in required and reactant_proof is not None:
490
+ evidence["reactant_embeddings"] = reactant_proof
491
+ if "products" in required and product_proof is not None:
492
+ evidence["product_embeddings"] = product_proof
493
+ return FusionValidation(
494
+ valid=not issues,
495
+ issues=tuple(issues),
496
+ evidence=evidence,
497
+ )
498
+
499
+
500
+ def validate_rbl_candidate(
501
+ original_rsmi: str,
502
+ candidate_rsmi: str,
503
+ *,
504
+ allow_wildcards: bool = False,
505
+ preserve_sides: Sequence[str] = ("products",),
506
+ ) -> FusionValidation:
507
+ """Apply the complete, mode-independent RBL acceptance predicate.
508
+
509
+ RBL reconstructs missing reactants and coproducts, so its default target
510
+ invariant preserves the observed product endpoint. Callers that require
511
+ a conservative balancing-only transformation may additionally request the
512
+ reactant side through ``preserve_sides``.
513
+ """
514
+ fusion = validate_fusion_rsmi(
515
+ candidate_rsmi,
516
+ allow_wildcards=allow_wildcards,
517
+ )
518
+ preservation = validate_endpoint_preservation(
519
+ original_rsmi,
520
+ candidate_rsmi,
521
+ required_sides=preserve_sides,
522
+ )
523
+ issues = fusion.issues + preservation.issues
524
+ return FusionValidation(
525
+ valid=not issues,
526
+ issues=issues,
527
+ evidence={
528
+ "endpoint_preservation": dict(preservation.evidence),
529
+ },
530
+ )
531
+
532
+
533
+ def _component_inventory(side: str) -> Counter[str] | None:
534
+ """Return an atom-map-independent exact molecular-component multiset."""
535
+ molecule = _mapped_smiles_mol(side)
536
+ if molecule is None:
537
+ return None
538
+ inventory: Counter[str] = Counter()
539
+ for fragment in Chem.GetMolFrags(molecule, asMols=True, sanitizeFrags=True):
540
+ fragment = Chem.Mol(fragment)
541
+ for atom in fragment.GetAtoms():
542
+ atom.SetAtomMapNum(0)
543
+ try:
544
+ normalized = Chem.RemoveHs(fragment)
545
+ signature = Chem.MolToSmiles(
546
+ normalized,
547
+ canonical=True,
548
+ isomericSmiles=True,
549
+ )
550
+ except (RuntimeError, ValueError):
551
+ return None
552
+ inventory[signature] += 1
553
+ return inventory
554
+
555
+
556
+ def _material_balance(
557
+ rsmi: str,
558
+ ) -> tuple[Counter[tuple[str, int]], Counter[tuple[str, int]], int, int] | None:
559
+ if rsmi.count(">>") != 1:
560
+ return None
561
+ inventories: list[Counter[tuple[str, int]]] = []
562
+ charges: list[int] = []
563
+ for side in rsmi.split(">>"):
564
+ molecule = _mapped_smiles_mol(side)
565
+ if molecule is None:
566
+ return None
567
+ try:
568
+ expanded = Chem.AddHs(molecule)
569
+ except RuntimeError:
570
+ return None
571
+ inventories.append(
572
+ Counter(
573
+ (atom.GetSymbol(), int(atom.GetIsotope()))
574
+ for atom in expanded.GetAtoms()
575
+ )
576
+ )
577
+ charges.append(int(Chem.GetFormalCharge(molecule)))
578
+ return inventories[0], inventories[1], charges[0], charges[1]
579
+
580
+
581
+ def _resource_delta(
582
+ reactants: Counter[tuple[str, int]],
583
+ products: Counter[tuple[str, int]],
584
+ reactant_charge: int,
585
+ product_charge: int,
586
+ ) -> dict[str, int]:
587
+ delta = {
588
+ f"element:{element}:{isotope}": count
589
+ for (element, isotope), count in products.items()
590
+ }
591
+ for (element, isotope), count in reactants.items():
592
+ key = f"element:{element}:{isotope}"
593
+ delta[key] = delta.get(key, 0) - count
594
+ delta = {key: value for key, value in delta.items() if value}
595
+ charge_delta = product_charge - reactant_charge
596
+ if charge_delta:
597
+ delta["formal_charge"] = charge_delta
598
+ return dict(sorted(delta.items()))
599
+
600
+
601
+ def validate_strict_rbl_candidate( # noqa: C901
602
+ original_rsmi: str,
603
+ candidate_rsmi: str,
604
+ *,
605
+ allow_wildcards: bool = False,
606
+ boundary: str = "closed",
607
+ environment_delta: Mapping[str, int] | None = None,
608
+ require_mapped_material: bool = True,
609
+ ) -> FusionValidation:
610
+ """Validate strict component-completion and conservation semantics.
611
+
612
+ Both observed endpoints must occur as exact molecular-component multisets;
613
+ fragment embeddings are deliberately insufficient. A closed boundary
614
+ requires isotope/element and net-formal-charge conservation. An open
615
+ boundary must declare the exact material/charge delta supplied by its
616
+ environment.
617
+ """
618
+ if boundary not in {"closed", "open"}:
619
+ raise ValueError("boundary must be 'closed' or 'open'.")
620
+ base = validate_fusion_rsmi(candidate_rsmi, allow_wildcards=allow_wildcards)
621
+ issues = list(base.issues)
622
+ evidence: dict[str, Any] = {
623
+ "observation_relation": "exact_component_multiset_inclusion",
624
+ "boundary": boundary,
625
+ }
626
+ if original_rsmi.count(">>") != 1 or candidate_rsmi.count(">>") != 1:
627
+ return FusionValidation(valid=False, issues=tuple(issues), evidence=evidence)
628
+
629
+ original_sides = original_rsmi.split(">>")
630
+ candidate_sides = candidate_rsmi.split(">>")
631
+ component_evidence: dict[str, Any] = {}
632
+ for index, side_name in enumerate(("reactants", "products")):
633
+ observed = _component_inventory(original_sides[index])
634
+ completed = _component_inventory(candidate_sides[index])
635
+ if observed is None or completed is None:
636
+ issues.append(
637
+ _issue(
638
+ FusionIssueCode.PARSE_FAILURE,
639
+ f"Could not normalize {side_name} component inventory.",
640
+ side=side_name,
641
+ )
642
+ )
643
+ continue
644
+ missing = observed - completed
645
+ component_evidence[side_name] = {
646
+ "observed": dict(sorted(observed.items())),
647
+ "candidate": dict(sorted(completed.items())),
648
+ "missing": dict(sorted(missing.items())),
649
+ }
650
+ if missing:
651
+ code = (
652
+ FusionIssueCode.REACTANT_COMPONENT_NOT_PRESERVED
653
+ if side_name == "reactants"
654
+ else FusionIssueCode.PRODUCT_COMPONENT_NOT_PRESERVED
655
+ )
656
+ issues.append(
657
+ _issue(
658
+ code,
659
+ f"Observed {side_name} components are not an exact multiset "
660
+ "subset of the candidate endpoint.",
661
+ missing=dict(sorted(missing.items())),
662
+ )
663
+ )
664
+ evidence["component_inventory"] = component_evidence
665
+
666
+ balance = _material_balance(candidate_rsmi)
667
+ if balance is None:
668
+ issues.append(
669
+ _issue(
670
+ FusionIssueCode.PARSE_FAILURE,
671
+ "Could not compute candidate material balance.",
672
+ )
673
+ )
674
+ else:
675
+ reactants, products, reactant_charge, product_charge = balance
676
+ delta = _resource_delta(
677
+ reactants,
678
+ products,
679
+ reactant_charge,
680
+ product_charge,
681
+ )
682
+ evidence["resource_delta"] = delta
683
+ if boundary == "closed":
684
+ element_delta = {
685
+ key: value for key, value in delta.items() if key != "formal_charge"
686
+ }
687
+ if element_delta:
688
+ issues.append(
689
+ _issue(
690
+ FusionIssueCode.ELEMENT_ISOTOPE_IMBALANCE,
691
+ "Closed reconstruction does not conserve element/isotope inventory.",
692
+ delta=element_delta,
693
+ )
694
+ )
695
+ if delta.get("formal_charge", 0):
696
+ issues.append(
697
+ _issue(
698
+ FusionIssueCode.CHARGE_IMBALANCE,
699
+ "Closed reconstruction does not conserve net formal charge.",
700
+ delta=delta["formal_charge"],
701
+ )
702
+ )
703
+ else:
704
+ declared = dict(sorted((environment_delta or {}).items()))
705
+ evidence["environment_delta"] = declared
706
+ if delta != declared:
707
+ issues.append(
708
+ _issue(
709
+ FusionIssueCode.ENVIRONMENT_DELTA_MISMATCH,
710
+ "Open reconstruction delta differs from its environment token.",
711
+ observed=delta,
712
+ declared=declared,
713
+ )
714
+ )
715
+
716
+ if require_mapped_material:
717
+ unmapped: dict[str, list[int]] = {}
718
+ for side_name, side in zip(
719
+ ("reactants", "products"), candidate_sides, strict=True
720
+ ):
721
+ molecule = _mapped_smiles_mol(side)
722
+ if molecule is None:
723
+ continue
724
+ missing_maps = [
725
+ atom.GetIdx()
726
+ for atom in molecule.GetAtoms()
727
+ if atom.GetAtomicNum() > 1 and atom.GetAtomMapNum() <= 0
728
+ ]
729
+ if missing_maps:
730
+ unmapped[side_name] = missing_maps
731
+ if unmapped:
732
+ issues.append(
733
+ _issue(
734
+ FusionIssueCode.UNMAPPED_MATERIAL_ATOM,
735
+ "Every material atom in strict reconstruction requires provenance.",
736
+ atoms=unmapped,
737
+ )
738
+ )
739
+ evidence["unmapped_material_atoms"] = unmapped
740
+
741
+ return FusionValidation(
742
+ valid=not issues,
743
+ issues=tuple(issues),
744
+ evidence=evidence,
745
+ )
746
+
747
+
748
+ def validate_wildcard_mapping_roles(
749
+ graph1: nx.Graph,
750
+ graph2: nx.Graph,
751
+ mapping: Mapping[Any, Any],
752
+ *,
753
+ element_key: str = "element",
754
+ wildcard_element: Any = ("*", "*"),
755
+ role_key: str = "wildcard_role",
756
+ ) -> FusionValidation:
757
+ """Reject an explicit mapping that conflates wildcard semantics.
758
+
759
+ Wildcards pruned before matching never enter this contract. A
760
+ wildcard-to-wildcard mapping requires compatible declared roles. A typed
761
+ wildcard may instead map to a concrete node admitted by its declared
762
+ domain; the verified interface subsequently proves owner incidence and
763
+ substitution. Missing roles fail closed.
764
+ """
765
+ scalar_wildcard = (
766
+ wildcard_element[0] if isinstance(wildcard_element, tuple) else wildcard_element
767
+ )
768
+ wildcard_values = (wildcard_element, scalar_wildcard)
769
+ issues: list[FusionIssue] = []
770
+
771
+ for node1, node2 in mapping.items():
772
+ if node1 not in graph1 or node2 not in graph2:
773
+ continue
774
+ attrs1 = graph1.nodes[node1]
775
+ attrs2 = graph2.nodes[node2]
776
+ wildcard1 = attrs1.get(element_key) in wildcard_values
777
+ wildcard2 = attrs2.get(element_key) in wildcard_values
778
+ if not wildcard1 and not wildcard2:
779
+ continue
780
+
781
+ raw_role1 = attrs1.get(role_key)
782
+ raw_role2 = attrs2.get(role_key)
783
+ try:
784
+ state1 = adapt_legacy_node_state(
785
+ attrs1,
786
+ element_key=element_key,
787
+ role_key=role_key,
788
+ wildcard_values=wildcard_values,
789
+ )
790
+ constraint1 = (
791
+ state1.constraint if state1.kind is NodeStateKind.WILDCARD else None
792
+ )
793
+ except (TypeError, ValueError):
794
+ constraint1 = None
795
+ try:
796
+ state2 = adapt_legacy_node_state(
797
+ attrs2,
798
+ element_key=element_key,
799
+ role_key=role_key,
800
+ wildcard_values=wildcard_values,
801
+ )
802
+ constraint2 = (
803
+ state2.constraint if state2.kind is NodeStateKind.WILDCARD else None
804
+ )
805
+ except (TypeError, ValueError):
806
+ constraint2 = None
807
+
808
+ compatibility = None
809
+ if constraint1 is not None and constraint2 is not None:
810
+ compatibility = constraint1.relabel_owner(mapping).intersect(constraint2)
811
+
812
+ concrete_constraint = constraint1 if wildcard1 else constraint2
813
+ concrete_attrs = attrs2 if wildcard1 else attrs1
814
+ concrete_element = concrete_attrs.get(element_key)
815
+ if isinstance(concrete_element, (tuple, list)) and len(concrete_element) == 2:
816
+ concrete_element = (
817
+ concrete_element[0]
818
+ if concrete_element[0] == concrete_element[1]
819
+ else None
820
+ )
821
+ concrete_charge = concrete_attrs.get("charge", 0)
822
+ concrete_radical = concrete_attrs.get("radical", 0)
823
+ if isinstance(concrete_charge, (tuple, list)) and len(concrete_charge) == 2:
824
+ concrete_charge = (
825
+ concrete_charge[0] if concrete_charge[0] == concrete_charge[1] else None
826
+ )
827
+ if isinstance(concrete_radical, (tuple, list)) and len(concrete_radical) == 2:
828
+ concrete_radical = (
829
+ concrete_radical[0]
830
+ if concrete_radical[0] == concrete_radical[1]
831
+ else None
832
+ )
833
+ concrete_ok = (
834
+ concrete_constraint is not None
835
+ and concrete_constraint.virtual_kind is None
836
+ and (
837
+ concrete_constraint.elements is None
838
+ or concrete_element in concrete_constraint.elements
839
+ )
840
+ and (
841
+ concrete_constraint.charges is None
842
+ or concrete_charge in concrete_constraint.charges
843
+ )
844
+ and (
845
+ concrete_constraint.radicals is None
846
+ or concrete_radical in concrete_constraint.radicals
847
+ )
848
+ )
849
+ valid = (
850
+ compatibility is not None and compatibility.valid
851
+ if wildcard1 and wildcard2
852
+ else concrete_ok
853
+ )
854
+
855
+ if not valid:
856
+ issues.append(
857
+ _issue(
858
+ FusionIssueCode.WILDCARD_ROLE_CONFLICT,
859
+ "Mapped wildcards require compatible typed constraints.",
860
+ graph1_node=node1,
861
+ graph2_node=node2,
862
+ graph1_role=(
863
+ constraint1.role.value if constraint1 is not None else raw_role1
864
+ ),
865
+ graph2_role=(
866
+ constraint2.role.value if constraint2 is not None else raw_role2
867
+ ),
868
+ constraint_issues=(
869
+ [item.to_dict() for item in compatibility.issues]
870
+ if compatibility is not None
871
+ else []
872
+ ),
873
+ )
874
+ )
875
+
876
+ return FusionValidation(valid=not issues, issues=tuple(issues))
877
+
878
+
879
+ def _normalize_completion_materialization(
880
+ source: nx.Graph,
881
+ *,
882
+ element_key: str,
883
+ wildcard_element: Any,
884
+ role_key: str,
885
+ allowed_roles: set[str],
886
+ ) -> tuple[nx.Graph, list[dict[str, str]], list[FusionIssue]]:
887
+ """Materialize typed completion ports on a copy of the source graph."""
888
+ normalized = source.copy()
889
+ scalar_wildcard = (
890
+ wildcard_element[0] if isinstance(wildcard_element, tuple) else wildcard_element
891
+ )
892
+ wildcard_values = (wildcard_element, scalar_wildcard)
893
+ materialized: list[dict[str, str]] = []
894
+ issues: list[FusionIssue] = []
895
+ for node, attributes in normalized.nodes(data=True):
896
+ if attributes.get(element_key) not in wildcard_values:
897
+ continue
898
+ raw_role = attributes.get(role_key)
899
+ role = raw_role.value if isinstance(raw_role, WildcardRole) else raw_role
900
+ if role not in allowed_roles:
901
+ issues.append(
902
+ _issue(
903
+ FusionIssueCode.PROOF_FAILED,
904
+ "Only typed completion wildcards may materialize as hydrogen.",
905
+ node=repr(node),
906
+ wildcard_role=role,
907
+ )
908
+ )
909
+ continue
910
+ attributes[element_key] = (
911
+ ("H", "H") if isinstance(attributes.get(element_key), tuple) else "H"
912
+ )
913
+ types_gh = attributes.get("typesGH")
914
+ if isinstance(types_gh, tuple) and len(types_gh) == 2:
915
+ attributes["typesGH"] = tuple(
916
+ (
917
+ (("H",) + tuple(endpoint[1:]))
918
+ if isinstance(endpoint, tuple) and endpoint
919
+ else endpoint
920
+ )
921
+ for endpoint in types_gh
922
+ )
923
+ attributes.pop(role_key, None)
924
+ materialized.append({"node": repr(node), "role": str(role)})
925
+
926
+ for _, attributes in normalized.nodes(data=True):
927
+ neighbors = attributes.get("neighbors")
928
+ if isinstance(neighbors, tuple) and len(neighbors) == 2:
929
+ attributes["neighbors"] = tuple(
930
+ ["H" if item == scalar_wildcard else item for item in endpoint or ()]
931
+ for endpoint in neighbors
932
+ )
933
+ elif isinstance(neighbors, list):
934
+ attributes["neighbors"] = [
935
+ "H" if item == scalar_wildcard else item for item in neighbors
936
+ ]
937
+ return normalized, materialized, issues
938
+
939
+
940
+ def certify_fusion_postprocessing(
941
+ source: nx.Graph,
942
+ target: nx.Graph,
943
+ *,
944
+ materialize_hydrogen: bool,
945
+ element_key: str = "element",
946
+ wildcard_element: Any = ("*", "*"),
947
+ role_key: str = "wildcard_role",
948
+ ) -> FusionValidation:
949
+ """Certify the narrow representation change allowed after a pushout.
950
+
951
+ An exact graph identity is always accepted. When hydrogen
952
+ materialisation is requested, a wildcard may additionally become H only
953
+ when it has an explicit completion role. The consumed role annotation
954
+ and wildcard tokens in cached neighbour/type fields are updated before an
955
+ exact map-invariant graph comparison. No nodes or edges may be added,
956
+ removed, or otherwise changed.
957
+
958
+ This is deliberately narrower than general reaction standardisation: a
959
+ query atom, attachment port, side-presence placeholder, or untyped legacy
960
+ wildcard is never evidence for a hydrogen atom.
961
+ """
962
+ source_digest = graph_identity_digest(source)
963
+ target_digest = graph_identity_digest(target)
964
+ if graphs_exactly_equivalent(source, target):
965
+ return FusionValidation(
966
+ valid=True,
967
+ evidence={
968
+ "postprocess_proof": {
969
+ "kind": "identity",
970
+ "source_digest": source_digest,
971
+ "normalized_digest": source_digest,
972
+ "target_digest": target_digest,
973
+ "materialized_nodes": [],
974
+ }
975
+ },
976
+ )
977
+
978
+ allowed_roles = {
979
+ WildcardRole.RADICAL_COMPLETION.value,
980
+ WildcardRole.HYDROGEN_COMPLETION.value,
981
+ }
982
+ if not materialize_hydrogen:
983
+ return FusionValidation(
984
+ valid=False,
985
+ issues=(
986
+ _issue(
987
+ FusionIssueCode.PROOF_FAILED,
988
+ "Post-processing changed the certified pushout graph.",
989
+ source_digest=source_digest,
990
+ target_digest=target_digest,
991
+ ),
992
+ ),
993
+ )
994
+
995
+ normalized, materialized, issues = _normalize_completion_materialization(
996
+ source,
997
+ element_key=element_key,
998
+ wildcard_element=wildcard_element,
999
+ role_key=role_key,
1000
+ allowed_roles=allowed_roles,
1001
+ )
1002
+
1003
+ if issues:
1004
+ return FusionValidation(valid=False, issues=tuple(issues))
1005
+ if not materialized:
1006
+ return FusionValidation(
1007
+ valid=False,
1008
+ issues=(
1009
+ _issue(
1010
+ FusionIssueCode.PROOF_FAILED,
1011
+ "The graph changed without a typed wildcard materialisation.",
1012
+ source_digest=source_digest,
1013
+ target_digest=target_digest,
1014
+ ),
1015
+ ),
1016
+ )
1017
+
1018
+ normalized_digest = graph_identity_digest(normalized)
1019
+ if not graphs_exactly_equivalent(normalized, target):
1020
+ return FusionValidation(
1021
+ valid=False,
1022
+ issues=(
1023
+ _issue(
1024
+ FusionIssueCode.PROOF_FAILED,
1025
+ "Post-processing exceeds typed wildcard-to-hydrogen materialisation.",
1026
+ source_digest=source_digest,
1027
+ normalized_digest=normalized_digest,
1028
+ target_digest=target_digest,
1029
+ materialized_nodes=materialized,
1030
+ ),
1031
+ ),
1032
+ )
1033
+
1034
+ return FusionValidation(
1035
+ valid=True,
1036
+ evidence={
1037
+ "postprocess_proof": {
1038
+ "kind": "typed_wildcard_hydrogen_materialization",
1039
+ "source_digest": source_digest,
1040
+ "normalized_digest": normalized_digest,
1041
+ "target_digest": target_digest,
1042
+ "materialized_nodes": materialized,
1043
+ "allowed_roles": sorted(allowed_roles),
1044
+ }
1045
+ },
1046
+ )
1047
+
1048
+
1049
+ __all__: Sequence[str] = (
1050
+ "FusionIssue",
1051
+ "FusionIssueCode",
1052
+ "FusionValidation",
1053
+ "WildcardRole",
1054
+ "certify_fusion_postprocessing",
1055
+ "validate_fusion_rsmi",
1056
+ "validate_endpoint_preservation",
1057
+ "validate_rbl_candidate",
1058
+ "validate_strict_rbl_candidate",
1059
+ "validate_wildcard_mapping_roles",
1060
+ )