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,1249 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Union
4
+ import logging
5
+ from time import perf_counter
6
+
7
+ import networkx as nx
8
+
9
+ from synkit.Chem.utils import remove_explicit_H_from_rsmi
10
+ from synkit.Graph.Hyrogen._misc import standardize_hydrogen, h_to_implicit
11
+ from synkit.IO import its_to_rsmi, rsmi_to_its
12
+ from synkit.Chem.Reaction.radical_wildcard import RadicalWildcardAdder
13
+ from synkit.Synthesis.Reactor import SynReactor
14
+ from synkit.Graph.Wildcard.its_merge import fuse_its_graphs
15
+ from synkit.Graph.Fusion import (
16
+ DEFAULT_INTERFACE_EDGE_KEYS,
17
+ DEFAULT_INTERFACE_NODE_KEYS,
18
+ FusionCandidate,
19
+ FusionConstructionError,
20
+ FusionInterface,
21
+ FusionInterfaceError,
22
+ construct_pushout,
23
+ fusion_candidate_from_construction,
24
+ fusion_candidates_exactly_equivalent,
25
+ graph_identity_digest,
26
+ graphs_exactly_equivalent,
27
+ )
28
+ from synkit.Graph.Matcher.mcs_matcher import MCSMatcher
29
+ from synkit.Graph.Matcher.wl_sel import WLSel
30
+
31
+ # from synkit.Graph.Matcher.approx_mcs import ApproxMCSMatcher
32
+ from synkit.Chem.Reaction.standardize import Standardize
33
+ from synkit.Synthesis.RBL.validation import (
34
+ FusionIssueCode,
35
+ FusionValidation,
36
+ certify_fusion_postprocessing,
37
+ validate_fusion_rsmi,
38
+ validate_rbl_candidate,
39
+ validate_strict_rbl_candidate,
40
+ validate_wildcard_mapping_roles,
41
+ )
42
+ from synkit.Synthesis.RBL.policy import (
43
+ AcceptanceTask,
44
+ OverlapScope,
45
+ ProofLevel,
46
+ RBLSearchPolicy,
47
+ SearchScope,
48
+ TerminationPolicy,
49
+ )
50
+ from synkit.Synthesis.RBL.overlap import (
51
+ TypedOverlapLimits,
52
+ enumerate_typed_overlaps,
53
+ )
54
+ from synkit.Synthesis.RBL.proof import RBLReplayCertificate
55
+ from synkit.Synthesis.RBL.matching import RBLMatchingMixin
56
+ from synkit.Synthesis.RBL.pipeline import RBLPipelineMixin
57
+ from synkit.Synthesis.RBL.reaction import RBLReactionMixin
58
+ from synkit.Synthesis.RBL.state import RBLStateMixin
59
+
60
+ ITSLike = Any
61
+
62
+
63
+ class RBLEngine(
64
+ RBLStateMixin, RBLPipelineMixin, RBLReactionMixin, RBLMatchingMixin
65
+ ):
66
+ """Radical-based linking (RBL) engine for bidirectional template
67
+ application and ITS-graph fusion using wildcard-based subgraph
68
+ matching.
69
+
70
+ Overview
71
+ --------
72
+ The RBL engine turns a reaction template (RSMI or ITS graph) into a
73
+ set of fused reaction graphs that link forward and backward template
74
+ applications through a wildcard-aware core. The workflow is:
75
+
76
+ 1. **Template preparation**:
77
+ Convert a template (RSMI or ITS graph) into a standardized ITS
78
+ representation with normalized hydrogen handling.
79
+
80
+ 2. **Forward / backward application**:
81
+ Use :class:`SynReactor` to apply the template to a substrate
82
+ (reactants or products) in forward or inverted mode, convert to
83
+ RSMI, decorate with radical wildcards, and convert back to ITS.
84
+
85
+ 3. **Wildcard-based fusion**:
86
+ For each forward/backward ITS pair, run a matcher
87
+ (:class:`MCSMatcher` or :class:`ApproxMCSMatcher`) to detect a
88
+ core overlap (ignoring wildcard regions) and fuse the graphs via
89
+ :func:`fuse_its_graphs`. The fused ITS graphs are then converted
90
+ back to post-processed RSMI strings.
91
+
92
+ Matching back-ends: exact vs. approximate
93
+ -----------------------------------------
94
+ The engine delegates ITS matching to :attr:`matcher_cls`, which is
95
+ assumed to be API-compatible with :class:`MCSMatcher`:
96
+
97
+ * :class:`MCSMatcher` (default)
98
+ - Exhaustive maximum-common-subgraph search based on
99
+ :class:`networkx.algorithms.isomorphism.GraphMatcher`.
100
+ - Respects ``prune_wc`` and
101
+ ``prune_automorphisms``.
102
+ - Produces exact MCS mappings but can be expensive on large or
103
+ highly symmetric graphs.
104
+
105
+ * :class:`ApproxMCSMatcher`
106
+ - Heuristic / greedy approximate MCS search.
107
+ - Uses seed selection and local greedy growth instead of
108
+ exhaustive enumeration.
109
+ - Much faster on large graphs but only approximate – mappings
110
+ are usually close to optimal in practice but not guaranteed
111
+ to be globally maximal.
112
+
113
+ Any custom matcher can be plugged in as long as it implements the
114
+ :class:`MCSMatcher` public API:
115
+
116
+ * ``__init__(node_attrs, node_defaults, edge_attrs, prune_wc, ...)``
117
+ * :py:meth:`find_rc_mapping`
118
+ * :py:meth:`get_mappings`
119
+
120
+ Early-stop semantics
121
+ --------------------
122
+ The engine exposes two orthogonal control flags:
123
+ ``early_stop`` and ``fast_paths_only``.
124
+
125
+ * If :attr:`early_stop` is ``True``:
126
+
127
+ * A cheap **quick-check** is attempted first via
128
+ :meth:`_quick_check`.
129
+
130
+ * If that fails, the engine looks for **ITS graphs without any
131
+ wildcard atoms** in the forward and backward sets and
132
+ post-processes them directly via
133
+ :meth:`_early_stop_on_nonwildcard`, without any MCS/fusion.
134
+
135
+ For each such candidate, a canonical reactant/product check is
136
+ performed to ensure consistency with the original reaction:
137
+
138
+ * forward candidates must preserve the original *main* product
139
+ component;
140
+ * backward candidates must preserve the original *main*
141
+ reactant component.
142
+
143
+ * Only if both these cheap paths fail, fusion and
144
+ post-processing are run in a **streaming loop**:
145
+ mappings are fused and post-processed one by one, and the
146
+ pipeline stops after the **first successful fused RSMI**.
147
+
148
+ * If :attr:`early_stop` is ``False``, the same loop runs without
149
+ early exit, collecting all fused ITS and fused RSMIs.
150
+
151
+ Fast-track and fast-path-only modes
152
+ -----------------------------------
153
+ ``mode="fast_track"`` runs only the two cheap paths and never enters MCS
154
+ or fusion. ``mode="fast_fusion"`` first tries those paths and then searches
155
+ a bounded, WL-ranked prefix of fusion pairs. WL scores affect order only;
156
+ they are not treated as a semantic filter. The default bound is eight pairs
157
+ and the result always reports ``complete=False`` when fusion is used.
158
+
159
+ * If :attr:`fast_paths_only` is ``True`` (or
160
+ :meth:`process` is called with ``fast_paths_only=True``):
161
+
162
+ * The engine **never** enters the expensive MCS/fusion stage
163
+ (:meth:`_fuse_and_postprocess` is skipped).
164
+
165
+ * It only attempts:
166
+
167
+ 1. :meth:`_quick_check`
168
+ 2. :meth:`_early_stop_on_nonwildcard`
169
+
170
+ * If neither path yields a solution, the engine returns with
171
+ empty :attr:`fused_its` / :attr:`fused_rsmis` and
172
+ ``result['mode'] == "fast_paths_only"`` and
173
+ ``result['reason'] == "fast_paths_no_solution"``.
174
+
175
+ * The flag :attr:`early_stop` is **ignored** for the fusion
176
+ stage in this mode, but still controls behaviour when
177
+ ``fast_paths_only=False``.
178
+
179
+ Reactor / hydrogen control
180
+ --------------------------
181
+ The underlying :class:`SynReactor` is configured via three flags
182
+ that are exposed on the engine:
183
+
184
+ * :attr:`implicit_temp` – forwarded to ``SynReactor(..., implicit_temp=...)``.
185
+ * :attr:`explicit_h` – forwarded to ``SynReactor(..., explicit_h=...)``.
186
+ * :attr:`embed_threshold` – forwarded to ``SynReactor(..., embed_threshold=...)``.
187
+
188
+ This gives fine-grained external control over how templates are
189
+ embedded and how hydrogens are handled during the reaction stage.
190
+ ``mode="verified"`` deliberately overrides these two flags with
191
+ ``implicit_temp=False`` and ``explicit_h=True`` so mapped hydrogen
192
+ identities cannot be erased from a proof-safe rule application.
193
+
194
+ :param wildcard_element: Value used for wildcard atom elements.
195
+ :type wildcard_element: Any
196
+ :param element_key: Node attribute containing the element symbol.
197
+ :type element_key: str
198
+ :param node_attrs: Node attributes used for candidate matching.
199
+ :type node_attrs: Sequence[str] | None
200
+ :param edge_attrs: Edge attributes used for candidate matching.
201
+ :type edge_attrs: Sequence[str] | None
202
+ :param prune_wc: Remove wildcard nodes before matching when supported.
203
+ :type prune_wc: bool
204
+ :param prune_automorphisms: Collapse automorphism-equivalent mappings.
205
+ ``None`` selects the profile default; verified search always disables
206
+ this uncertified quotient.
207
+ :type prune_automorphisms: bool | None
208
+ :param mcs_side: Reaction-center side passed to the MCS matcher.
209
+ :type mcs_side: str
210
+ :param early_stop: Stop after the first valid result.
211
+ :type early_stop: bool
212
+ :param fast_paths_only: Restrict processing to quick-check and
213
+ non-wildcard paths.
214
+ :type fast_paths_only: bool
215
+ :param mode: Compatibility search profile name.
216
+ :type mode: str | None
217
+ :param search_policy: Explicit search scope and termination policy;
218
+ mutually exclusive with ``mode``.
219
+ :type search_policy: RBLSearchPolicy | None
220
+ :param max_pairs: Maximum number of ranked forward/backward pairs.
221
+ :type max_pairs: int | None
222
+ :param max_mappings_per_pair: Maximum mappings evaluated for each pair;
223
+ zero means unbounded and ``None`` selects
224
+ the active profile's default.
225
+ :type max_mappings_per_pair: int | None
226
+ :param overlap_max_states: Maximum incremental states per typed-overlap
227
+ enumeration.
228
+ :type overlap_max_states: int
229
+ :param overlap_max_mappings: Maximum emitted typed overlaps per graph pair.
230
+ :type overlap_max_mappings: int
231
+ :param overlap_timeout_seconds: Optional internal typed-overlap wall time.
232
+ :type overlap_timeout_seconds: float | None
233
+ :param component_matching: Enable component-assignment matching.
234
+ :type component_matching: bool | None
235
+ :param fusion_backend: Fusion materialization backend.
236
+ :type fusion_backend: str | None
237
+ :param implicit_temp: Treat the reactor template as implicit-H.
238
+ :type implicit_temp: bool
239
+ :param explicit_h: Retain explicit hydrogens during reactor application.
240
+ :type explicit_h: bool
241
+ :param electron_diagnostics: Collect electron-accounting diagnostics.
242
+ :type electron_diagnostics: bool
243
+ :param preserve_original_sides: Original reaction sides that accepted
244
+ candidates must preserve.
245
+ :type preserve_original_sides: Sequence[str]
246
+ :param conservation_boundary: Strict-mode ``"closed"`` or ``"open"``
247
+ material boundary.
248
+ :type conservation_boundary: str
249
+ :param environment_delta: Exact declared material/charge delta for an open
250
+ boundary.
251
+ :type environment_delta: Mapping[str, int] | None
252
+ :param embed_threshold: Reactor embedding limit.
253
+ :type embed_threshold: int
254
+ :param reactor_cls: Reactor implementation.
255
+ :type reactor_cls: type
256
+ :param wildcard_adder_cls: Radical-wildcard decorator implementation.
257
+ :type wildcard_adder_cls: type
258
+ :param matcher_cls: ITS matcher implementation.
259
+ :type matcher_cls: type
260
+ :param fuse_fn: Legacy ITS-fusion function.
261
+ :type fuse_fn: Callable[[ITSLike, ITSLike, Dict[Any, Any]], ITSLike]
262
+ :param remove_explicit_H_fn: Reaction explicit-hydrogen remover.
263
+ :type remove_explicit_H_fn: Callable[[str], str]
264
+ :param rsmi_to_its_fn: Reaction-to-ITS converter.
265
+ :type rsmi_to_its_fn: Callable[..., ITSLike]
266
+ :param its_to_rsmi_fn: ITS-to-reaction converter.
267
+ :type its_to_rsmi_fn: Callable[[ITSLike], str]
268
+ :param h_to_implicit_fn: Explicit-to-implicit hydrogen converter.
269
+ :type h_to_implicit_fn: Callable[[ITSLike], ITSLike]
270
+ :param standardize_h_fn: ITS hydrogen standardizer.
271
+ :type standardize_h_fn: Callable[[ITSLike], ITSLike]
272
+ :param standardize_fn: Reaction canonicalizer used for quick checks.
273
+ :type standardize_fn: Callable[[str], str] | None
274
+ :param logger: Diagnostic logger.
275
+ :type logger: logging.Logger | None
276
+
277
+ .. rubric:: Examples
278
+
279
+ Exact MCS back-end
280
+ ~~~~~~~~~~~~~~~~~~
281
+ Use the default :class:`MCSMatcher` for exact MCS fusion:
282
+
283
+ .. code-block:: python
284
+
285
+ from synkit.Synthesis.RBL.engine import RBLEngine
286
+
287
+ rxn = "CCO.CBr>>CCOBr"
288
+ template = "CBr>>C[*]" # toy example
289
+
290
+ engine = RBLEngine(
291
+ early_stop=True,
292
+ fast_paths_only=False,
293
+ implicit_temp=True,
294
+ explicit_h=False,
295
+ embed_threshold=5000,
296
+ )
297
+
298
+ engine = engine.process(rxn, template)
299
+ print(engine.result["mode"])
300
+ print(engine.fused_rsmis)
301
+
302
+ Approximate MCS back-end
303
+ ~~~~~~~~~~~~~~~~~~~~~~~~
304
+ Swap in :class:`ApproxMCSMatcher` to accelerate matching on large
305
+ graphs while retaining the same RBL API:
306
+
307
+ .. code-block:: python
308
+
309
+ from synkit.Graph.Matcher.approx_mcs import ApproxMCSMatcher
310
+ from synkit.Synthesis.RBL.engine import RBLEngine
311
+
312
+ rxn = "CC1=CC=CC=C1.OBr>>CC1=CC=CC=C1OBr"
313
+ template = "OBr>>O[*]"
314
+
315
+ engine = RBLEngine(
316
+ matcher_cls=ApproxMCSMatcher, # use heuristic MCS
317
+ early_stop=False, # collect all fused hits
318
+ fast_paths_only=False,
319
+ )
320
+
321
+ engine = engine.process(rxn, template)
322
+ for fused in engine.fused_rsmis:
323
+ print(fused)
324
+ """
325
+
326
+ def __init__(
327
+ self,
328
+ *,
329
+ wildcard_element: Any = ("*", "*"),
330
+ element_key: str = "element",
331
+ node_attrs: Optional[Sequence[str]] = None,
332
+ edge_attrs: Optional[Sequence[str]] = None,
333
+ prune_wc: bool = True,
334
+ prune_automorphisms: bool | None = None,
335
+ mcs_side: str = "l",
336
+ early_stop: bool = True,
337
+ fast_paths_only: bool = False,
338
+ mode: str | None = None,
339
+ search_policy: RBLSearchPolicy | None = None,
340
+ max_pairs: int | None = None,
341
+ max_mappings_per_pair: int | None = None,
342
+ overlap_max_states: int = 250_000,
343
+ overlap_max_mappings: int = 50_000,
344
+ overlap_timeout_seconds: float | None = None,
345
+ component_matching: bool | None = None,
346
+ fusion_backend: str | None = None,
347
+ implicit_temp: bool = True,
348
+ explicit_h: bool = False,
349
+ electron_diagnostics: bool = False,
350
+ preserve_original_sides: Sequence[str] = ("products",),
351
+ conservation_boundary: str = "closed",
352
+ environment_delta: Mapping[str, int] | None = None,
353
+ embed_threshold: int = 10_000,
354
+ reactor_cls: type = SynReactor,
355
+ wildcard_adder_cls: type = RadicalWildcardAdder,
356
+ matcher_cls: type = MCSMatcher,
357
+ fuse_fn: Callable[[ITSLike, ITSLike, Dict[Any, Any]], ITSLike] = (
358
+ fuse_its_graphs
359
+ ),
360
+ remove_explicit_H_fn: Callable[[str], str] = remove_explicit_H_from_rsmi,
361
+ rsmi_to_its_fn: Callable[..., ITSLike] = rsmi_to_its,
362
+ its_to_rsmi_fn: Callable[[ITSLike], str] = its_to_rsmi,
363
+ h_to_implicit_fn: Callable[[ITSLike], ITSLike] = h_to_implicit,
364
+ standardize_h_fn: Callable[[ITSLike], ITSLike] = standardize_hydrogen,
365
+ standardize_fn: Optional[Callable[[str], str]] = Standardize().fit,
366
+ logger: Optional[logging.Logger] = None,
367
+ ) -> None:
368
+ # Core config
369
+ self.wildcard_element: Any = wildcard_element
370
+ self.element_key: str = element_key
371
+ self.node_attrs: List[str] = (
372
+ list(node_attrs)
373
+ if node_attrs is not None
374
+ else ["element", "aromatic", "charge"]
375
+ )
376
+ self.edge_attrs: List[str] = (
377
+ list(edge_attrs) if edge_attrs is not None else ["order"]
378
+ )
379
+ # Candidate discovery may intentionally use a coarse configurable
380
+ # label set. Proof construction may not: its interface uses the fixed
381
+ # isotope/Lewis contract independently of matcher tuning, while the
382
+ # completed-graph proof handles hydrogen presentation equivalence.
383
+ self.interface_node_attrs = list(DEFAULT_INTERFACE_NODE_KEYS)
384
+ self.interface_edge_attrs = list(DEFAULT_INTERFACE_EDGE_KEYS)
385
+ if mode is not None and search_policy is not None:
386
+ raise ValueError("Specify either mode or search_policy, not both.")
387
+ self.mode = mode
388
+ if search_policy is not None:
389
+ if not isinstance(search_policy, RBLSearchPolicy):
390
+ raise TypeError("search_policy must be an RBLSearchPolicy.")
391
+ self.search_policy = search_policy
392
+ elif mode is not None:
393
+ self.search_policy = RBLSearchPolicy.from_mode(mode)
394
+ elif fast_paths_only:
395
+ self.search_policy = RBLSearchPolicy(
396
+ SearchScope.FAST_PATHS_ONLY,
397
+ TerminationPolicy.FIRST_VALID,
398
+ )
399
+ elif early_stop:
400
+ self.search_policy = RBLSearchPolicy.from_mode("early_stop")
401
+ else:
402
+ self.search_policy = RBLSearchPolicy.from_mode("full")
403
+ verified_mode = self.search_policy.proof_level is ProofLevel.REPLAYABLE
404
+ if prune_automorphisms is None:
405
+ # Orbit pruning is an optional heuristic until the matcher emits a
406
+ # checkable quotient certificate. The verified profile therefore
407
+ # uses the literal mapping universe; compatibility profiles retain
408
+ # their historical bounded behavior and report incompleteness.
409
+ prune_automorphisms = not verified_mode
410
+ if verified_mode:
411
+ prune_automorphisms = False
412
+ # Proof-safe rule application keeps mapped hydrogen as an explicit
413
+ # graph vertex. Folding it into an implicit template destroys the
414
+ # identity needed to certify proton-transfer rules.
415
+ implicit_temp = False
416
+ explicit_h = True
417
+ self.prune_wc = prune_wc
418
+ self.prune_automorphisms = prune_automorphisms
419
+ self.mcs_side = mcs_side
420
+ self.verified_mode = verified_mode
421
+ self.early_stop = (
422
+ self.search_policy.termination is TerminationPolicy.FIRST_VALID
423
+ )
424
+ self.fast_paths_only = self.search_policy.scope is SearchScope.FAST_PATHS_ONLY
425
+ if max_pairs is None and self.search_policy.scope is SearchScope.BOUNDED_FUSION:
426
+ max_pairs = 8
427
+ if max_pairs is not None and int(max_pairs) <= 0:
428
+ raise ValueError("max_pairs must be positive or None.")
429
+ self.max_pairs: int | None = None if max_pairs is None else int(max_pairs)
430
+ mapping_limit = (
431
+ 0 if verified_mode else 1
432
+ ) if max_mappings_per_pair is None else int(max_mappings_per_pair)
433
+ if mapping_limit < 0:
434
+ raise ValueError("max_mappings_per_pair must be non-negative.")
435
+ self.max_mappings_per_pair = mapping_limit
436
+ self.overlap_limits = TypedOverlapLimits(
437
+ max_states=int(overlap_max_states),
438
+ max_overlaps=int(overlap_max_mappings),
439
+ timeout_seconds=overlap_timeout_seconds,
440
+ )
441
+ if component_matching is None:
442
+ component_matching = not (
443
+ not self.prune_automorphisms and self.max_mappings_per_pair == 0
444
+ )
445
+ if verified_mode:
446
+ component_matching = False
447
+ self.component_matching = bool(component_matching)
448
+ if fusion_backend is None:
449
+ fusion_backend = (
450
+ "categorical_pushout"
451
+ if verified_mode
452
+ or self.search_policy.scope is SearchScope.BOUNDED_FUSION
453
+ else "legacy_its_merge"
454
+ )
455
+ if fusion_backend not in {"categorical_pushout", "legacy_its_merge"}:
456
+ raise ValueError(
457
+ "fusion_backend must be 'categorical_pushout' or " "'legacy_its_merge'."
458
+ )
459
+ self.fusion_backend = fusion_backend
460
+
461
+ # Reactor behaviour flags
462
+ self.implicit_temp: bool = bool(implicit_temp)
463
+ self.explicit_h: bool = bool(explicit_h)
464
+ self.electron_diagnostics: bool = bool(electron_diagnostics)
465
+ unknown_preservation_sides = set(preserve_original_sides) - {
466
+ "reactants",
467
+ "products",
468
+ }
469
+ if unknown_preservation_sides:
470
+ raise ValueError(
471
+ "Unknown preserve_original_sides values: "
472
+ f"{sorted(unknown_preservation_sides)!r}."
473
+ )
474
+ self.preserve_original_sides = tuple(dict.fromkeys(preserve_original_sides))
475
+ if conservation_boundary not in {"closed", "open"}:
476
+ raise ValueError("conservation_boundary must be 'closed' or 'open'.")
477
+ self.conservation_boundary = conservation_boundary
478
+ self.environment_delta = dict(environment_delta or {})
479
+ self.embed_threshold: int = int(embed_threshold)
480
+
481
+ # Dependencies (DI)
482
+ self.reactor_cls = reactor_cls
483
+ self.wildcard_adder_cls = wildcard_adder_cls
484
+ self.matcher_cls = matcher_cls
485
+ self.fuse_fn = fuse_fn
486
+ self.remove_explicit_H_fn = remove_explicit_H_fn
487
+ self.rsmi_to_its_fn = rsmi_to_its_fn
488
+ self.its_to_rsmi_fn = its_to_rsmi_fn
489
+ self.h_to_implicit_fn = h_to_implicit_fn
490
+ self.standardize_h_fn = standardize_h_fn
491
+ self.standardize_fn: Callable[[str], str] = (
492
+ standardize_fn if standardize_fn is not None else self._identity_standardize
493
+ )
494
+
495
+ # Logging
496
+ self.logger = logger or logging.getLogger(__name__)
497
+
498
+ # Internal state (fluent-style access via properties)
499
+ self._template_raw: Optional[Union[str, nx.Graph, ITSLike]] = None
500
+ self._template_its: Optional[ITSLike] = None
501
+
502
+ self._last_reaction: Optional[str] = None
503
+ self._last_reactants: Optional[str] = None
504
+ self._last_products: Optional[str] = None
505
+
506
+ self._forward_its: List[ITSLike] = []
507
+ self._backward_its: List[ITSLike] = []
508
+ self._fused_its: List[ITSLike] = []
509
+ self._fused_rsmis: List[str] = []
510
+ self._fusion_candidates: List[FusionCandidate] = []
511
+ self._rbl_proofs: List[RBLReplayCertificate] = []
512
+ self._rbl_proof_digests: set[str] = set()
513
+ self._fusion_search_stats: Dict[str, Any] = {}
514
+ self._latest_postprocessed_its: Optional[ITSLike] = None
515
+ self._latest_output_validation: Optional[FusionValidation] = None
516
+ self._diagnostics: Dict[str, List[Dict[str, Any]]] = {
517
+ "forward": [],
518
+ "backward": [],
519
+ "quick_check": [],
520
+ "fusion": [],
521
+ }
522
+
523
+ # Result / termination bookkeeping
524
+ self._last_stop_mode: str = "not_run"
525
+ self._last_stop_reason: str = "not_run"
526
+ self._last_stop_metadata: Dict[str, Any] = {}
527
+ self._active_search_policy = self.search_policy
528
+
529
+ def _fuse_and_postprocess(
530
+ self,
531
+ fw_its: Sequence[ITSLike],
532
+ bw_its: Sequence[ITSLike],
533
+ *,
534
+ replace_wc: bool,
535
+ early_stop: bool,
536
+ max_pairs: int | None = None,
537
+ initial_graphs: Sequence[ITSLike] = (),
538
+ initial_rsmis: Sequence[str] = (),
539
+ ) -> None:
540
+ """
541
+ Core fusion + post-processing loop.
542
+
543
+ This is where performance matters. Instead of trying all
544
+ ``(fw, bw)`` pairs naively, we first apply a WL-based selector
545
+ (:class:`WLSel`) to rank candidate forward–backward ITS pairs by
546
+ structural similarity. We then process pairs in this order:
547
+
548
+ 1. Use the configured matcher (:class:`MCSMatcher` or
549
+ :class:`ApproxMCSMatcher`) to obtain mappings.
550
+ 2. Take at most :attr:`max_mappings_per_pair` mappings.
551
+ 3. Construct the audited pushout, then use either that graph or the
552
+ configured legacy compatibility fusion backend.
553
+ 4. Immediately post-process the fused graph via
554
+ :meth:`_postprocess_single`.
555
+
556
+ If ``early_stop`` is ``True``, the method returns as soon as a
557
+ successful fused RSMI is obtained. Otherwise, it explores all
558
+ WL-selected pairs/mappings and collects all fused ITS and fused
559
+ RSMIs.
560
+
561
+ On return, :attr:`_fused_its`, :attr:`_fused_rsmis` and the
562
+ result bookkeeping attributes are updated.
563
+
564
+ :param fw_its: Forward ITS graphs.
565
+ :type fw_its: Sequence[ITSLike]
566
+ :param bw_its: Backward ITS graphs.
567
+ :type bw_its: Sequence[ITSLike]
568
+ :param replace_wc: Whether to replace wildcard atoms with H.
569
+ :type replace_wc: bool
570
+ :param early_stop: Whether to stop after the first valid fused RSMI.
571
+ :type early_stop: bool
572
+ :param max_pairs: Optional hard prefix bound after deterministic pair
573
+ ranking. ``None`` explores every pair.
574
+ :type max_pairs: int or None
575
+ :param initial_graphs: Already accepted direct-path candidates that a
576
+ wider search must retain.
577
+ :type initial_graphs: Sequence[ITSLike]
578
+ :param initial_rsmis: Serializations aligned with ``initial_graphs``.
579
+ :type initial_rsmis: Sequence[str]
580
+ """
581
+ if len(initial_graphs) != len(initial_rsmis):
582
+ raise ValueError("initial_graphs and initial_rsmis must be aligned.")
583
+ fused_graphs: List[ITSLike] = list(initial_graphs)
584
+ fused_rsmis: List[str] = list(initial_rsmis)
585
+ rw_adder = self.wildcard_adder_cls()
586
+
587
+ # --- WL-based candidate selection instead of naive nested loops ---
588
+ # The selector is an ordering heuristic only. A similarity threshold
589
+ # is not a sound admissibility predicate for graph fusion.
590
+ pair_order_started = perf_counter()
591
+ selector = WLSel(
592
+ fw_its,
593
+ bw_its,
594
+ element_key=self.element_key,
595
+ node_attrs=self.node_attrs,
596
+ edge_attrs=self.edge_attrs,
597
+ min_score=0.0,
598
+ )
599
+ selector.build_signatures().score_pairs(top_k=max_pairs)
600
+ pairs = selector.pair_indices
601
+ pair_order_seconds = perf_counter() - pair_order_started
602
+ pair_candidate_count = selector.pair_candidate_count
603
+ n_pairs_truncated = pair_candidate_count - len(pairs)
604
+
605
+ n_pairs = 0
606
+ n_mappings_total = 0
607
+ n_mappings_rejected = 0
608
+ n_candidates_deduplicated = 0
609
+ n_mappings_truncated = 0
610
+ n_postprocess_rejections = 0
611
+ overlap_certificates: List[Dict[str, Any]] = []
612
+ overlap_search_incomplete = 0
613
+ n_operational_failures = 0
614
+ overlap_seconds = 0.0
615
+ construction_seconds = 0.0
616
+ postprocess_seconds = 0.0
617
+ candidate_buckets: Dict[str, List[FusionCandidate]] = {}
618
+ accepted_graph_buckets: Dict[str, List[nx.Graph]] = {}
619
+ for graph in initial_graphs:
620
+ if isinstance(graph, nx.Graph):
621
+ accepted_graph_buckets.setdefault(
622
+ graph_identity_digest(graph),
623
+ [],
624
+ ).append(graph)
625
+
626
+ def incomplete_reasons(*, first_valid: bool) -> List[str]:
627
+ reasons = []
628
+ if (
629
+ self._active_search_policy.overlap_scope
630
+ is OverlapScope.MAXIMUM_COMMON_SUBGRAPHS
631
+ ):
632
+ reasons.append("maximum_common_subgraphs_only")
633
+ if first_valid:
634
+ reasons.append("first_valid_termination")
635
+ if n_pairs_truncated:
636
+ reasons.append("pair_limit")
637
+ if n_mappings_truncated:
638
+ reasons.append("mapping_limit")
639
+ if self.prune_automorphisms:
640
+ reasons.append("uncertified_automorphism_pruning")
641
+ if self.component_matching:
642
+ reasons.append("single_component_assignment")
643
+ if (
644
+ self._active_search_policy.overlap_scope
645
+ is OverlapScope.MAXIMUM_COMMON_SUBGRAPHS
646
+ and self.matcher_cls is not MCSMatcher
647
+ ):
648
+ reasons.append("non_exact_matcher")
649
+ if overlap_search_incomplete:
650
+ reasons.append("typed_overlap_limit")
651
+ if n_operational_failures:
652
+ reasons.append("operational_failure")
653
+ return reasons
654
+
655
+ for i_fw, i_bw in pairs:
656
+ fw = fw_its[i_fw]
657
+ bw = bw_its[i_bw]
658
+ n_pairs += 1
659
+
660
+ overlap_started = perf_counter()
661
+ if self._active_search_policy.overlap_scope is OverlapScope.ALL_TYPED:
662
+ overlap_result = enumerate_typed_overlaps(
663
+ fw,
664
+ bw,
665
+ node_keys=self.interface_node_attrs,
666
+ edge_keys=self.interface_edge_attrs,
667
+ element_key=self.element_key,
668
+ wildcard_element=self.wildcard_element,
669
+ limits=self.overlap_limits,
670
+ )
671
+ certificate_payload = overlap_result.certificate.to_dict()
672
+ certificate_payload["pair_index"] = [i_fw, i_bw]
673
+ overlap_certificates.append(certificate_payload)
674
+ if not overlap_result.certificate.complete:
675
+ overlap_search_incomplete += 1
676
+ mappings = [dict(mapping) for mapping in overlap_result.mappings]
677
+ else:
678
+ matcher = self._build_matcher()
679
+ matcher.find_rc_mapping(
680
+ fw,
681
+ bw,
682
+ mcs=True,
683
+ mcs_mol=False,
684
+ component=self.component_matching,
685
+ side=self.mcs_side,
686
+ )
687
+ mappings = matcher.get_mappings(direction="G1_to_G2") or []
688
+ overlap_seconds += perf_counter() - overlap_started
689
+ if not mappings:
690
+ continue
691
+
692
+ if self.verified_mode:
693
+ completed_mappings: List[Dict[Any, Any]] = []
694
+ for mapping in mappings:
695
+ completed_mappings.extend(
696
+ self._complete_typed_wildcard_ports(
697
+ fw,
698
+ bw,
699
+ mapping,
700
+ maximum_only=(
701
+ self._active_search_policy.overlap_scope
702
+ is not OverlapScope.ALL_TYPED
703
+ ),
704
+ )
705
+ )
706
+ mappings = completed_mappings
707
+
708
+ mapping_count = len(mappings)
709
+ if self.max_mappings_per_pair > 0:
710
+ mappings = mappings[: self.max_mappings_per_pair]
711
+ n_mappings_truncated += mapping_count - len(mappings)
712
+
713
+ for i_map, mapping in enumerate(mappings):
714
+ n_mappings_total += 1
715
+ role_validation = validate_wildcard_mapping_roles(
716
+ fw,
717
+ bw,
718
+ mapping,
719
+ element_key=self.element_key,
720
+ wildcard_element=self.wildcard_element,
721
+ )
722
+ if not role_validation.valid:
723
+ n_mappings_rejected += 1
724
+ payload = role_validation.to_dict()
725
+ payload.update(
726
+ {
727
+ "source": "wildcard_mapping",
728
+ "pair_index": (i_fw, i_bw),
729
+ "mapping_index": i_map,
730
+ }
731
+ )
732
+ self._diagnostics["fusion"].append(payload)
733
+ continue
734
+ diagnostic_context = {
735
+ "pair_index": (i_fw, i_bw),
736
+ "mapping_index": i_map,
737
+ }
738
+ construction_started = perf_counter()
739
+ try:
740
+ interface = FusionInterface.from_mapping(
741
+ fw,
742
+ bw,
743
+ mapping,
744
+ node_keys=self.interface_node_attrs,
745
+ edge_keys=self.interface_edge_attrs,
746
+ element_key=self.element_key,
747
+ wildcard_element=self.wildcard_element,
748
+ )
749
+ proof_construction = construct_pushout(
750
+ fw,
751
+ bw,
752
+ interface,
753
+ node_keys=self.interface_node_attrs,
754
+ edge_keys=self.interface_edge_attrs,
755
+ element_key=self.element_key,
756
+ wildcard_element=self.wildcard_element,
757
+ )
758
+ except FusionInterfaceError as exc:
759
+ n_mappings_rejected += 1
760
+ self._record_fusion_failure(
761
+ FusionIssueCode.INTERFACE_INVALID,
762
+ "The proposed Sprint 15 fusion interface is incompatible.",
763
+ source="verified_interface",
764
+ context={
765
+ **diagnostic_context,
766
+ "issues": [issue.to_dict() for issue in exc.issues],
767
+ },
768
+ )
769
+ continue
770
+ except FusionConstructionError as exc:
771
+ n_mappings_rejected += 1
772
+ self._record_fusion_failure(
773
+ FusionIssueCode.CONSTRUCTION_INVALID,
774
+ "The Sprint 15 pushout audit rejected the mapping.",
775
+ source="verified_construction",
776
+ context={
777
+ **diagnostic_context,
778
+ "issues": [issue.to_dict() for issue in exc.issues],
779
+ },
780
+ )
781
+ continue
782
+ finally:
783
+ construction_seconds += perf_counter() - construction_started
784
+ if self.fusion_backend == "categorical_pushout":
785
+ fused_graph = proof_construction.graph.copy()
786
+ else:
787
+ # Compatibility backend. Its output is accepted only as
788
+ # a plain result unless it later proves exactly equivalent
789
+ # to the audited pushout graph.
790
+ try:
791
+ try:
792
+ fused_graph = self.fuse_fn(
793
+ fw,
794
+ bw,
795
+ mapping,
796
+ remove_wildcards=True,
797
+ )
798
+ except TypeError:
799
+ fused_graph = self.fuse_fn( # type: ignore[arg-type]
800
+ fw,
801
+ bw,
802
+ mapping,
803
+ )
804
+ except Exception as exc: # pragma: no cover - defensive
805
+ n_mappings_rejected += 1
806
+ n_operational_failures += 1
807
+ self._record_fusion_failure(
808
+ FusionIssueCode.OPERATION_FAILED,
809
+ "Legacy ITS graph fusion failed for the mapping.",
810
+ source="construction",
811
+ context={
812
+ **diagnostic_context,
813
+ "error": type(exc).__name__,
814
+ },
815
+ )
816
+ continue
817
+
818
+ if self.verified_mode:
819
+ fused_graph = self._normalize_duplicate_atom_maps(fused_graph)
820
+
821
+ # The base implementation populates these caches, while a
822
+ # subclass override safely falls back to reparsing/revalidating.
823
+ # Clear them here so an override can never inherit evidence
824
+ # from the preceding mapping.
825
+ self._latest_postprocessed_its = None
826
+ self._latest_output_validation = None
827
+ diagnostics_before = len(self._diagnostics["fusion"])
828
+ postprocess_started = perf_counter()
829
+ rsmi_final = self._postprocess_single(
830
+ fused_graph,
831
+ replace_wc=replace_wc,
832
+ rw_adder=rw_adder,
833
+ diagnostic_context=diagnostic_context,
834
+ )
835
+ postprocess_seconds += perf_counter() - postprocess_started
836
+ if rsmi_final is None:
837
+ n_mappings_rejected += 1
838
+ n_postprocess_rejections += 1
839
+ new_diagnostics = self._diagnostics["fusion"][
840
+ diagnostics_before:
841
+ ]
842
+ if any(
843
+ issue.get("code")
844
+ in {
845
+ FusionIssueCode.OPERATION_FAILED.value,
846
+ }
847
+ for report in new_diagnostics
848
+ for issue in report.get("issues", ())
849
+ ):
850
+ n_operational_failures += 1
851
+ continue
852
+ final_graph = self._latest_postprocessed_its
853
+ if not isinstance(final_graph, nx.Graph):
854
+ final_graph = self._safe_rsmi_to_its(rsmi_final)
855
+ if not isinstance(final_graph, nx.Graph):
856
+ n_mappings_rejected += 1
857
+ n_operational_failures += 1
858
+ self._record_fusion_failure(
859
+ FusionIssueCode.PROOF_FAILED,
860
+ "The accepted serialization could not be restored as an ITS graph.",
861
+ source="verified_proof",
862
+ context=diagnostic_context,
863
+ )
864
+ continue
865
+ postprocess_proof = certify_fusion_postprocessing(
866
+ proof_construction.graph,
867
+ final_graph,
868
+ materialize_hydrogen=replace_wc,
869
+ element_key=self.element_key,
870
+ wildcard_element=self.wildcard_element,
871
+ )
872
+ proof_equivalent = postprocess_proof.valid
873
+ if not proof_equivalent:
874
+ self._record_fusion_failure(
875
+ FusionIssueCode.PROOF_FAILED,
876
+ "Post-processing changed the graph certified by the pushout "
877
+ "outside the audited normalization contract.",
878
+ source="verified_proof",
879
+ context={
880
+ **diagnostic_context,
881
+ "issues": [
882
+ issue.to_dict() for issue in postprocess_proof.issues
883
+ ],
884
+ },
885
+ )
886
+ if self.verified_mode:
887
+ n_mappings_rejected += 1
888
+ continue
889
+ proof_validation = self._latest_output_validation
890
+ if proof_validation is None:
891
+ if (
892
+ self._last_reaction is not None
893
+ and self._active_search_policy.acceptance_task
894
+ is AcceptanceTask.STRICT_RECONSTRUCTION
895
+ ):
896
+ proof_validation = validate_strict_rbl_candidate(
897
+ self._last_reaction,
898
+ rsmi_final,
899
+ allow_wildcards=not replace_wc,
900
+ boundary=self.conservation_boundary,
901
+ environment_delta=self.environment_delta,
902
+ )
903
+ elif self._last_reaction is not None:
904
+ proof_validation = validate_rbl_candidate(
905
+ self._last_reaction,
906
+ rsmi_final,
907
+ allow_wildcards=not replace_wc,
908
+ preserve_sides=self.preserve_original_sides,
909
+ )
910
+ else:
911
+ proof_validation = validate_fusion_rsmi(
912
+ rsmi_final,
913
+ allow_wildcards=not replace_wc,
914
+ )
915
+ final_digest = graph_identity_digest(final_graph)
916
+ identity_bucket = accepted_graph_buckets.setdefault(
917
+ final_digest,
918
+ [],
919
+ )
920
+ duplicate_outcome = any(
921
+ graphs_exactly_equivalent(final_graph, previous)
922
+ for previous in identity_bucket
923
+ )
924
+ if duplicate_outcome:
925
+ n_candidates_deduplicated += 1
926
+ if proof_equivalent:
927
+ validation_payload = proof_validation.to_dict()
928
+ validation_payload["evidence"] = {
929
+ **validation_payload.get("evidence", {}),
930
+ **postprocess_proof.evidence,
931
+ }
932
+ replay_certificate = None
933
+ if (
934
+ self._active_search_policy.proof_level
935
+ is ProofLevel.REPLAYABLE
936
+ ):
937
+ try:
938
+ replay_certificate = RBLReplayCertificate.create(
939
+ original_rsmi=self._last_reaction or rsmi_final,
940
+ forward=fw,
941
+ backward=bw,
942
+ mapping=mapping,
943
+ pushout=proof_construction.graph,
944
+ final_graph=final_graph,
945
+ final_rsmi=rsmi_final,
946
+ materialize_hydrogen=replace_wc,
947
+ acceptance_task=(
948
+ self._active_search_policy.acceptance_task.value
949
+ ),
950
+ preserve_sides=self.preserve_original_sides,
951
+ conservation_boundary=self.conservation_boundary,
952
+ environment_delta=self.environment_delta,
953
+ wildcard_element=self.wildcard_element,
954
+ node_keys=tuple(self.interface_node_attrs),
955
+ edge_keys=tuple(self.interface_edge_attrs),
956
+ )
957
+ except (TypeError, ValueError) as error:
958
+ n_operational_failures += 1
959
+ self._record_fusion_failure(
960
+ FusionIssueCode.PROOF_FAILED,
961
+ "The end-to-end RBL certificate did not replay.",
962
+ source="rbl_proof",
963
+ context={
964
+ **diagnostic_context,
965
+ "error": str(error),
966
+ },
967
+ )
968
+ n_mappings_rejected += 1
969
+ continue
970
+ candidate = fusion_candidate_from_construction(
971
+ proof_construction,
972
+ rsmi=rsmi_final,
973
+ validation=(validation_payload,),
974
+ graph=final_graph,
975
+ )
976
+ bucket = candidate_buckets.setdefault(
977
+ candidate.canonical_signature,
978
+ [],
979
+ )
980
+ if not any(
981
+ fusion_candidates_exactly_equivalent(candidate, previous)
982
+ for previous in bucket
983
+ ):
984
+ self._fusion_candidates.append(candidate)
985
+ bucket.append(candidate)
986
+ if replay_certificate is not None:
987
+ replay_payload = replay_certificate.to_dict()
988
+ replay_digest = replay_payload["document_digest"]
989
+ if replay_digest not in self._rbl_proof_digests:
990
+ self._rbl_proof_digests.add(replay_digest)
991
+ self._rbl_proofs.append(replay_certificate)
992
+ if duplicate_outcome:
993
+ continue
994
+ identity_bucket.append(final_graph)
995
+ fused_graphs.append(final_graph)
996
+ fused_rsmis.append(rsmi_final)
997
+ if early_stop:
998
+ self.logger.debug(
999
+ "Early-stop: first successful fused RSMI "
1000
+ "at pair=(%d,%d), mapping_index=%d",
1001
+ i_fw,
1002
+ i_bw,
1003
+ i_map,
1004
+ )
1005
+ self._fused_its = fused_graphs
1006
+ self._fused_rsmis = fused_rsmis
1007
+ self._fusion_search_stats = {
1008
+ "complete": False,
1009
+ "complete_within_mapping_scope": False,
1010
+ "termination": "first_valid",
1011
+ "pairs_explored": n_pairs,
1012
+ "mappings_explored": n_mappings_total,
1013
+ "mappings_rejected": n_mappings_rejected,
1014
+ "operational_failures": n_operational_failures,
1015
+ "postprocess_rejections": n_postprocess_rejections,
1016
+ "candidates_valid": len(fused_rsmis),
1017
+ "direct_candidates_valid": len(initial_rsmis),
1018
+ "fusion_candidates_valid": (
1019
+ len(fused_rsmis) - len(initial_rsmis)
1020
+ ),
1021
+ "proof_candidates": len(self._fusion_candidates),
1022
+ "candidates_deduplicated": n_candidates_deduplicated,
1023
+ "mappings_truncated": n_mappings_truncated,
1024
+ "pair_candidates": pair_candidate_count,
1025
+ "pairs_truncated": n_pairs_truncated,
1026
+ "component_matching": self.component_matching,
1027
+ "mapping_scope": self._active_search_policy.overlap_scope.value,
1028
+ "pair_ordering": "wl_no_cutoff",
1029
+ "fusion_backend": self.fusion_backend,
1030
+ "interface_completion": (
1031
+ "all_typed_leaf_port_assignments"
1032
+ if self._active_search_policy.overlap_scope
1033
+ is OverlapScope.ALL_TYPED
1034
+ else (
1035
+ "maximum_typed_leaf_ports"
1036
+ if self.verified_mode
1037
+ else "none"
1038
+ )
1039
+ ),
1040
+ "overlap_scope": (
1041
+ self._active_search_policy.overlap_scope.value
1042
+ ),
1043
+ "globally_complete_over_all_overlaps": False,
1044
+ "incomplete_reasons": incomplete_reasons(first_valid=True),
1045
+ "overlap_certificates": overlap_certificates,
1046
+ "timings_seconds": {
1047
+ "pair_ordering": pair_order_seconds,
1048
+ "overlap_enumeration": overlap_seconds,
1049
+ "construction": construction_seconds,
1050
+ "postprocessing": postprocess_seconds,
1051
+ },
1052
+ }
1053
+ self._record_stop(
1054
+ mode="early_stop",
1055
+ reason="early_stop_first_valid",
1056
+ metadata={
1057
+ "n_fw": len(fw_its),
1058
+ "n_bw": len(bw_its),
1059
+ "n_pairs": n_pairs,
1060
+ "n_mappings_total": n_mappings_total,
1061
+ "n_fused": len(fused_graphs),
1062
+ "n_fused_rsmis": len(fused_rsmis),
1063
+ "early_stop": early_stop,
1064
+ "pair_index": (i_fw, i_bw),
1065
+ "mapping_index": i_map,
1066
+ },
1067
+ )
1068
+ return
1069
+
1070
+ # No early-stop taken: finalise results
1071
+ ranked_outputs = sorted(
1072
+ zip(fused_graphs, fused_rsmis, strict=True),
1073
+ key=lambda record: record[1],
1074
+ )
1075
+ self._fused_its = [record[0] for record in ranked_outputs]
1076
+ self._fused_rsmis = [record[1] for record in ranked_outputs]
1077
+ self._fusion_candidates.sort(
1078
+ key=lambda candidate: (
1079
+ candidate.score.ranking_key if candidate.score is not None else (),
1080
+ candidate.canonical_signature,
1081
+ candidate.proof_digest,
1082
+ )
1083
+ )
1084
+ all_typed_scope = (
1085
+ self._active_search_policy.overlap_scope is OverlapScope.ALL_TYPED
1086
+ )
1087
+ mapping_scope_complete = (
1088
+ n_pairs_truncated == 0
1089
+ and n_mappings_truncated == 0
1090
+ and not self.prune_automorphisms
1091
+ and not self.component_matching
1092
+ and overlap_search_incomplete == 0
1093
+ and n_operational_failures == 0
1094
+ and (all_typed_scope or self.matcher_cls is MCSMatcher)
1095
+ )
1096
+ global_overlap_complete = mapping_scope_complete and all_typed_scope
1097
+ self._fusion_search_stats = {
1098
+ "complete": global_overlap_complete,
1099
+ "complete_within_mapping_scope": mapping_scope_complete,
1100
+ "termination": (
1101
+ "mapping_scope_exhausted" if mapping_scope_complete else "bounded"
1102
+ ),
1103
+ "pairs_explored": n_pairs,
1104
+ "mappings_explored": n_mappings_total,
1105
+ "mappings_rejected": n_mappings_rejected,
1106
+ "operational_failures": n_operational_failures,
1107
+ "postprocess_rejections": n_postprocess_rejections,
1108
+ "candidates_valid": len(self._fused_rsmis),
1109
+ "direct_candidates_valid": len(initial_rsmis),
1110
+ "fusion_candidates_valid": len(self._fused_rsmis) - len(initial_rsmis),
1111
+ "proof_candidates": len(self._fusion_candidates),
1112
+ "candidates_deduplicated": n_candidates_deduplicated,
1113
+ "mappings_truncated": n_mappings_truncated,
1114
+ "automorphism_pruned": self.prune_automorphisms,
1115
+ "pair_candidates": pair_candidate_count,
1116
+ "pairs_truncated": n_pairs_truncated,
1117
+ "component_matching": self.component_matching,
1118
+ "mapping_scope": self._active_search_policy.overlap_scope.value,
1119
+ "pair_ordering": "wl_no_cutoff",
1120
+ "fusion_backend": self.fusion_backend,
1121
+ "interface_completion": (
1122
+ "all_typed_leaf_port_assignments"
1123
+ if all_typed_scope
1124
+ else ("maximum_typed_leaf_ports" if self.verified_mode else "none")
1125
+ ),
1126
+ "overlap_scope": self._active_search_policy.overlap_scope.value,
1127
+ "globally_complete_over_all_overlaps": global_overlap_complete,
1128
+ "incomplete_reasons": incomplete_reasons(first_valid=False),
1129
+ "overlap_certificates": overlap_certificates,
1130
+ "timings_seconds": {
1131
+ "pair_ordering": pair_order_seconds,
1132
+ "overlap_enumeration": overlap_seconds,
1133
+ "construction": construction_seconds,
1134
+ "postprocessing": postprocess_seconds,
1135
+ },
1136
+ }
1137
+
1138
+ if not fused_graphs:
1139
+ self._record_stop(
1140
+ mode="full_pipeline",
1141
+ reason="no_fused_its",
1142
+ metadata={
1143
+ "n_fw": len(fw_its),
1144
+ "n_bw": len(bw_its),
1145
+ "n_pairs": n_pairs,
1146
+ "n_mappings_total": n_mappings_total,
1147
+ "early_stop": early_stop,
1148
+ },
1149
+ )
1150
+ elif not fused_rsmis:
1151
+ self._record_stop(
1152
+ mode="full_pipeline",
1153
+ reason="postprocessing_failed",
1154
+ metadata={
1155
+ "n_fw": len(fw_its),
1156
+ "n_bw": len(bw_its),
1157
+ "n_pairs": n_pairs,
1158
+ "n_mappings_total": n_mappings_total,
1159
+ "n_fused": len(fused_graphs),
1160
+ "n_fused_rsmis": 0,
1161
+ "early_stop": early_stop,
1162
+ },
1163
+ )
1164
+ else:
1165
+ self._record_stop(
1166
+ mode="full_pipeline",
1167
+ reason="fused_its_completed",
1168
+ metadata={
1169
+ "n_fw": len(fw_its),
1170
+ "n_bw": len(bw_its),
1171
+ "n_pairs": n_pairs,
1172
+ "n_mappings_total": n_mappings_total,
1173
+ "n_fused": len(fused_graphs),
1174
+ "n_fused_rsmis": len(fused_rsmis),
1175
+ "early_stop": early_stop,
1176
+ },
1177
+ )
1178
+
1179
+ # ------------------------------------------------------------------
1180
+ # Post-processing helper
1181
+ # ------------------------------------------------------------------
1182
+
1183
+ # ------------------------------------------------------------------
1184
+ # Convenience / diagnostics
1185
+ # ------------------------------------------------------------------
1186
+
1187
+ def help(self) -> str:
1188
+ """Return a short textual description of the current engine state.
1189
+
1190
+ Useful for quick inspection in interactive sessions.
1191
+
1192
+ :return: Multi-line human-readable summary string.
1193
+ :rtype: str
1194
+ """
1195
+ template_ready = self._template_its is not None
1196
+ return (
1197
+ "RBLEngine configuration\n"
1198
+ f" wildcard_element : {self.wildcard_element!r}\n"
1199
+ f" element_key : {self.element_key!r}\n"
1200
+ f" node_attrs : {self.node_attrs!r}\n"
1201
+ f" edge_attrs : {self.edge_attrs!r}\n"
1202
+ f" prune_wc : {self.prune_wc}\n"
1203
+ f" prune_autos : {self.prune_automorphisms}\n"
1204
+ f" mcs_side : {self.mcs_side!r}\n"
1205
+ f" early_stop : {self.early_stop}\n"
1206
+ f" fast_paths_only : {self.fast_paths_only}\n"
1207
+ f" search_policy : {self.search_policy.to_dict()!r}\n"
1208
+ f" max_maps/pair : {self.max_mappings_per_pair}\n"
1209
+ f" max_pairs : {self.max_pairs!r}\n"
1210
+ f" component_matching : {self.component_matching}\n"
1211
+ f" fusion_backend : {self.fusion_backend!r}\n"
1212
+ f" implicit_temp : {self.implicit_temp}\n"
1213
+ f" explicit_h : {self.explicit_h}\n"
1214
+ f" preserve_sides : {self.preserve_original_sides!r}\n"
1215
+ f" embed_threshold : {self.embed_threshold}\n"
1216
+ f" reactor_cls : {self.reactor_cls.__name__}\n"
1217
+ f" matcher_cls : {self.matcher_cls.__name__}\n"
1218
+ f" template_ready : {template_ready}\n"
1219
+ f" last_reaction : {self._last_reaction!r}\n"
1220
+ f" #fw_its : {len(self._forward_its)}\n"
1221
+ f" #bw_its : {len(self._backward_its)}\n"
1222
+ f" #fused_its : {len(self._fused_its)}\n"
1223
+ f" #fused_rsmis : {len(self._fused_rsmis)}\n"
1224
+ f" result_mode : {self._last_stop_mode!r}\n"
1225
+ f" result_reason : {self._last_stop_reason!r}"
1226
+ )
1227
+
1228
+ def __repr__(self) -> str:
1229
+ """Return a concise summary representation of the engine.
1230
+
1231
+ :return: One-line representation string.
1232
+ :rtype: str
1233
+ """
1234
+ return (
1235
+ f"<RBLEngine wildcard_element={self.wildcard_element!r} "
1236
+ f"node_attrs={self.node_attrs!r} edge_attrs={self.edge_attrs!r} "
1237
+ f"prune_wc={self.prune_wc} "
1238
+ f"prune_automorphisms={self.prune_automorphisms} "
1239
+ f"mcs_side={self.mcs_side!r} "
1240
+ f"early_stop={self.early_stop} "
1241
+ f"fast_paths_only={self.fast_paths_only} "
1242
+ f"search_policy={self.search_policy.to_dict()!r} "
1243
+ f"max_mappings_per_pair={self.max_mappings_per_pair} "
1244
+ f"implicit_temp={self.implicit_temp} "
1245
+ f"explicit_h={self.explicit_h} "
1246
+ f"preserve_original_sides={self.preserve_original_sides!r} "
1247
+ f"embed_threshold={self.embed_threshold} "
1248
+ f"reactor_cls={self.reactor_cls.__name__}>"
1249
+ )