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,1264 @@
1
+ """
2
+ approx_mcs_matcher.py — Heuristic / Approximate MCS Matcher
3
+ ===========================================================
4
+
5
+ A compact, OOP-style wrapper that mimics the public behaviour of
6
+ :class:`MCSMatcher`, but uses a **greedy, heuristic** search instead of
7
+ exhaustive subgraph enumeration.
8
+
9
+ Key ideas
10
+ ---------
11
+ * Use :func:`generic_node_match` for flexible node-attribute comparison.
12
+ * Optionally augment structural similarity with a 1-WL-style
13
+ color-refinement term (``use_wl=True``).
14
+ * Score node pairs based on attribute compatibility, local degrees,
15
+ neighbour-degree overlap and (optionally) WL colors.
16
+ * Select a small set of high-scoring **seed pairs**.
17
+ * For each seed, **greedily grow** a mapping along neighbouring nodes,
18
+ enforcing local edge compatibility along the way.
19
+ * Cache the resulting (approximate) mappings in the same orientation
20
+ scheme as :class:`MCSMatcher`:
21
+
22
+ - Internal cache: **pattern → host** (pattern = smaller graph after
23
+ optional wildcard pruning).
24
+ - :py:meth:`ApproxMCSMatcher.get_mappings` converts to ``G1→G2`` or
25
+ ``G2→G1``.
26
+
27
+ Public API
28
+ ~~~~~~~~~~
29
+ .. code-block:: python
30
+
31
+ from synkit.Graph.Matcher.approx_mcs import ApproxMCSMatcher
32
+ import networkx as nx
33
+
34
+ # build two toy molecular graphs
35
+ g1 = nx.Graph()
36
+ g1.add_node(0, element="C")
37
+ g1.add_node(1, element="O")
38
+ g1.add_edge(0, 1, order=1)
39
+
40
+ g2 = nx.Graph()
41
+ g2.add_node(10, element="C")
42
+ g2.add_node(11, element="O")
43
+ g2.add_edge(10, 11, order=1)
44
+
45
+ matcher = ApproxMCSMatcher(use_wl=True).find_common_subgraph(g1, g2)
46
+ g1_to_g2 = matcher.get_mappings(direction="G1_to_G2")[0]
47
+ print(g1_to_g2) # e.g. {0: 10, 1: 11}
48
+
49
+ """
50
+
51
+ from __future__ import annotations
52
+
53
+ from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple
54
+
55
+ import networkx as nx
56
+ from networkx.algorithms.isomorphism import generic_node_match
57
+
58
+ try: # pragma: no cover - optional dependency
59
+ from synkit.Graph.ITS import its_decompose
60
+ except ImportError: # pragma: no cover
61
+ its_decompose = None # type: ignore[assignment]
62
+
63
+ __all__ = ["ApproxMCSMatcher"]
64
+
65
+ MappingDict = Dict[int, int]
66
+
67
+
68
+ class ApproxMCSMatcher:
69
+ """Heuristic / approximate common-subgraph matcher.
70
+
71
+ This class provides a **fast, approximate** alternative to
72
+ :class:`MCSMatcher`. It does *not* enumerate all subgraph
73
+ isomorphisms. Instead, it:
74
+
75
+ 1. Picks a set of high-scoring node pairs as **seeds** based on
76
+ attribute and local-structure similarity.
77
+ 2. For each seed, **greedily grows** a subgraph isomorphism by
78
+ extending along neighbouring nodes while maintaining local
79
+ adjacency consistency.
80
+ 3. Collects the resulting partial mappings and reports them in the
81
+ same orientation style as :class:`MCSMatcher`.
82
+
83
+ Optionally, a 1-WL-style color refinement (``use_wl=True``) is used
84
+ to produce coarse structural colors; matching nodes with identical
85
+ WL colors receive a small similarity bonus.
86
+
87
+ The result is an approximate maximum-common-subgraph (MCS) mapping
88
+ that is usually close to optimal for molecular graphs, but can be
89
+ computed much faster and can be bounded by simple iteration
90
+ parameters.
91
+
92
+ Orientation and storage
93
+ -----------------------
94
+ Internally, mappings are stored in **pattern → host** orientation,
95
+ where the pattern is the smaller of the two graphs after optional
96
+ wildcard pruning. The :py:meth:`get_mappings` helper converts these
97
+ into ``G1→G2`` or ``G2→G1`` orientation based on which original
98
+ graph served as the pattern.
99
+
100
+ API compatibility
101
+ -----------------
102
+ The constructor and public methods mirror :class:`MCSMatcher` as
103
+ closely as possible so that :class:`ApproxMCSMatcher` can drop in as
104
+ a faster, heuristic substitute in many workflows.
105
+
106
+ :param node_attrs: Node attribute keys to compare. If ``None``, defaults to
107
+ ``["element"]``.
108
+ :type node_attrs: list[str] or None, optional
109
+ :param node_defaults: Fallback values for each node attribute when missing. If
110
+ ``None``, defaults to a list of ``"*"``
111
+ with the same length as ``node_attrs``.
112
+ :type node_defaults: list[Any] or None, optional
113
+ :param allow_shift: Placeholder for future asymmetric rules. Kept for API
114
+ compatibility with :class:`MCSMatcher`.
115
+ :type allow_shift: bool, optional
116
+ :param edge_attrs: Edge attribute keys to use for scalar comparison
117
+ (e.g. ``["order"]``). If ``None``, defaults to ``["order"]``.
118
+ :type edge_attrs: list[str] or None, optional
119
+ :param prune_wc: If ``True``, strip wildcard nodes (see
120
+ ``wildcard_element``, ``element_key``) from both
121
+ graphs before searching.
122
+ :type prune_wc: bool, optional
123
+ :param prune_automorphisms: If ``True``, collapse mappings that have the same host node set
124
+ (automorphism pruning).
125
+ :type prune_automorphisms: bool, optional
126
+ :param wildcard_element: Attribute value denoting wildcard nodes (typically ``"*"``,
127
+ used together with ``element_key``).
128
+ :type wildcard_element: Any, optional
129
+ :param element_key: Node attribute key used to detect wildcard nodes when
130
+ ``prune_wc`` is ``True``.
131
+ :type element_key: str, optional
132
+ :param use_wl: If ``True``, run a simple 1-WL-style color refinement on both
133
+ graphs and include the resulting colors in the node similarity
134
+ score.
135
+ :type use_wl: bool, optional
136
+ :param wl_max_iter: Maximum number of WL refinement iterations.
137
+ :type wl_max_iter: int, optional
138
+ """
139
+
140
+ # ------------------------------------------------------------------
141
+ # Construction
142
+ # ------------------------------------------------------------------
143
+ def __init__(
144
+ self,
145
+ node_attrs: Optional[List[str]] = None,
146
+ node_defaults: Optional[List[Any]] = None,
147
+ allow_shift: bool = True,
148
+ *,
149
+ edge_attrs: Optional[List[str]] = None,
150
+ prune_wc: bool = False,
151
+ prune_automorphisms: bool = False,
152
+ wildcard_element: Any = "*",
153
+ element_key: str = "element",
154
+ use_wl: bool = False,
155
+ wl_max_iter: int = 3,
156
+ wildcard_aware: bool = False,
157
+ ) -> None:
158
+ if node_attrs is None:
159
+ node_attrs = ["element"]
160
+ if node_defaults is None:
161
+ node_defaults = ["*"] * len(node_attrs)
162
+ if len(node_defaults) != len(node_attrs):
163
+ raise ValueError(
164
+ "ApproxMCSMatcher: node_defaults must have the same length "
165
+ "as node_attrs."
166
+ )
167
+
168
+ self._node_attrs: List[str] = node_attrs
169
+ self._node_defaults: List[Any] = node_defaults
170
+ self._edge_attrs: List[str] = (
171
+ ["order"] if edge_attrs is None else list(edge_attrs)
172
+ )
173
+ self.allow_shift: bool = allow_shift
174
+
175
+ self.wildcard_aware: bool = wildcard_aware
176
+ # wildcard_aware requires wildcards to stay in the graph for matching
177
+ self.prune_wc: bool = False if wildcard_aware else prune_wc
178
+ self.prune_automorphisms: bool = prune_automorphisms
179
+ self.wildcard_element: Any = wildcard_element
180
+ self.element_key: str = element_key
181
+
182
+ self.use_wl: bool = use_wl
183
+ self.wl_max_iter: int = wl_max_iter
184
+
185
+ comparators: List[Callable[[Any, Any], bool]] = [
186
+ (lambda x, y: x == y) for _ in node_attrs
187
+ ]
188
+ self.node_match: Callable[[Dict[str, Any], Dict[str, Any]], bool] = (
189
+ generic_node_match(
190
+ node_attrs,
191
+ node_defaults,
192
+ comparators,
193
+ )
194
+ )
195
+
196
+ # Internal cache (pattern → host)
197
+ self._mappings: List[MappingDict] = []
198
+ self._last_size: int = 0
199
+ self._last_pattern_is_G1: Optional[bool] = None
200
+
201
+ # ------------------------------------------------------------------
202
+ # Internal helpers
203
+ # ------------------------------------------------------------------
204
+
205
+ def _is_wildcard_node(self, data: Dict[str, Any]) -> bool:
206
+ """Return True if this node is a wildcard atom."""
207
+ el = data.get(self.element_key)
208
+ wc = self.wildcard_element
209
+ wc0 = wc[0] if isinstance(wc, tuple) else wc
210
+ if isinstance(el, tuple):
211
+ return el[0] == wc0
212
+ return el == wc0
213
+
214
+ def _node_compatible(self, pdata: Dict[str, Any], hdata: Dict[str, Any]) -> bool:
215
+ """Node compatibility check, optionally treating wildcards as universal."""
216
+ if self.wildcard_aware and (
217
+ self._is_wildcard_node(pdata) or self._is_wildcard_node(hdata)
218
+ ):
219
+ return True
220
+ return self.node_match(pdata, hdata)
221
+
222
+ def _prune_graph(self, G: nx.Graph) -> nx.Graph:
223
+ """Remove wildcard nodes from ``G`` (non-inplace) if applicable.
224
+
225
+ When :attr:`prune_wc` is ``False``, the input graph is returned
226
+ as-is.
227
+
228
+ :param G: Input graph.
229
+ :type G: nx.Graph
230
+ :return: Possibly pruned graph.
231
+ :rtype: nx.Graph
232
+ """
233
+ if not self.prune_wc:
234
+ return G
235
+
236
+ key = self.element_key
237
+ wc = self.wildcard_element
238
+ keep_nodes = [n for n, d in G.nodes(data=True) if d.get(key) != wc]
239
+ return G.subgraph(keep_nodes).copy()
240
+
241
+ @staticmethod
242
+ def _prepare_orientation(
243
+ G1: nx.Graph,
244
+ G2: nx.Graph,
245
+ ) -> Tuple[nx.Graph, nx.Graph, bool]:
246
+ """Ensure the smaller graph is used as pattern.
247
+
248
+ :param G1: First input graph.
249
+ :type G1: nx.Graph
250
+ :param G2: Second input graph.
251
+ :type G2: nx.Graph
252
+ :return: Tuple ``(pattern, host, pattern_is_G1)``.
253
+ :rtype: tuple[nx.Graph, nx.Graph, bool]
254
+ """
255
+ if G1.number_of_nodes() <= G2.number_of_nodes():
256
+ return G1, G2, True
257
+ return G2, G1, False
258
+
259
+ @staticmethod
260
+ def _invert_mapping(mapping: MappingDict) -> MappingDict:
261
+ """Invert a mapping from host→pattern to pattern→host or vice versa.
262
+
263
+ :param mapping: Mapping to invert.
264
+ :type mapping: dict[int, int]
265
+ :return: Inverted mapping.
266
+ :rtype: dict[int, int]
267
+ """
268
+ return {v: k for k, v in mapping.items()}
269
+
270
+ def _edge_match(
271
+ self,
272
+ attrs1: Dict[str, Any],
273
+ attrs2: Dict[str, Any],
274
+ ) -> bool:
275
+ """Compare edge attributes listed in :attr:`_edge_attrs`.
276
+
277
+ For each attribute name:
278
+
279
+ * If both values can be cast to ``float``, use numeric equality.
280
+ * Otherwise, fall back to direct ``==`` comparison.
281
+ * Missing values on both sides are ignored.
282
+
283
+ :param attrs1: Edge attributes of the first edge.
284
+ :type attrs1: dict[str, Any]
285
+ :param attrs2: Edge attributes of the second edge.
286
+ :type attrs2: dict[str, Any]
287
+ :return: ``True`` if the attributes are compatible.
288
+ :rtype: bool
289
+ """
290
+ for name in self._edge_attrs:
291
+ v1 = attrs1.get(name)
292
+ v2 = attrs2.get(name)
293
+ if v1 is None and v2 is None:
294
+ continue
295
+ try:
296
+ if float(v1) != float(v2):
297
+ return False
298
+ except (TypeError, ValueError):
299
+ if v1 != v2:
300
+ return False
301
+ return True
302
+
303
+ def _compute_wl_colors(self, G: nx.Graph) -> Dict[int, int]:
304
+ """Run a simple 1-WL color refinement on ``G``.
305
+
306
+ Initial colors are based on the configured node attributes and
307
+ the node degree; refinement iteratively refines colors using the
308
+ multiset of neighbour colors.
309
+
310
+ :param G: Input graph.
311
+ :type G: nx.Graph
312
+ :return: Mapping from node id to WL color id.
313
+ :rtype: dict[int, int]
314
+ """
315
+ colors: Dict[int, int] = {}
316
+ for n, data in G.nodes(data=True):
317
+ attr_vals: List[Any] = []
318
+ for key, default in zip(self._node_attrs, self._node_defaults):
319
+ attr_vals.append(data.get(key, default))
320
+ colors[n] = hash((tuple(attr_vals), G.degree[n]))
321
+
322
+ for _ in range(self.wl_max_iter):
323
+ signatures: Dict[Tuple[int, Tuple[int, ...]], int] = {}
324
+ new_colors: Dict[int, int] = {}
325
+ for n in G.nodes():
326
+ neigh = [colors[nb] for nb in G.neighbors(n)]
327
+ neigh.sort()
328
+ sig = (colors[n], tuple(neigh))
329
+ if sig not in signatures:
330
+ signatures[sig] = len(signatures)
331
+ new_colors[n] = signatures[sig]
332
+ if new_colors == colors:
333
+ break
334
+ colors = new_colors
335
+
336
+ return colors
337
+
338
+ def _node_similarity(
339
+ self,
340
+ p: int,
341
+ h: int,
342
+ pattern: nx.Graph,
343
+ host: nx.Graph,
344
+ wl_pattern: Dict[int, int],
345
+ wl_host: Dict[int, int],
346
+ ) -> float:
347
+ """Compute a simple structural similarity score for a node pair.
348
+
349
+ The score is based on:
350
+
351
+ * Node attribute compatibility via :attr:`node_match`.
352
+ * Degree difference between ``p`` and ``h``.
353
+ * Overlap in neighbour degrees.
354
+ * Optional 1-WL color agreement (if :attr:`use_wl` is ``True``).
355
+
356
+ :param p: Node id in the pattern graph.
357
+ :type p: int
358
+ :param h: Node id in the host graph.
359
+ :type h: int
360
+ :param pattern: Pattern graph.
361
+ :type pattern: nx.Graph
362
+ :param host: Host graph.
363
+ :type host: nx.Graph
364
+ :param wl_pattern: WL colors for the pattern graph.
365
+ :type wl_pattern: dict[int, int]
366
+ :param wl_host: WL colors for the host graph.
367
+ :type wl_host: dict[int, int]
368
+ :return: Similarity score (larger is better; negative for
369
+ incompatible pairs).
370
+ :rtype: float
371
+ """
372
+ pdata = pattern.nodes[p]
373
+ hdata = host.nodes[h]
374
+ if not self._node_compatible(pdata, hdata):
375
+ return -1.0
376
+
377
+ deg_p = pattern.degree[p]
378
+ deg_h = host.degree[h]
379
+ base = 2.0
380
+ penalty = float(abs(deg_p - deg_h))
381
+
382
+ neigh_deg_p = {pattern.degree[n] for n in pattern.neighbors(p)}
383
+ neigh_deg_h = {host.degree[n] for n in host.neighbors(h)}
384
+ overlap = float(len(neigh_deg_p & neigh_deg_h))
385
+
386
+ color_term = 0.0
387
+ if self.use_wl and wl_pattern and wl_host:
388
+ if wl_pattern.get(p) == wl_host.get(h):
389
+ color_term = 1.0
390
+ else:
391
+ color_term = -1.0
392
+
393
+ return base - penalty + 0.5 * overlap + color_term
394
+
395
+ def _generate_seeds(
396
+ self,
397
+ pattern: nx.Graph,
398
+ host: nx.Graph,
399
+ max_seeds: int,
400
+ wl_pattern: Dict[int, int],
401
+ wl_host: Dict[int, int],
402
+ ) -> List[Tuple[int, int]]:
403
+ """Generate a list of high-scoring seed node pairs.
404
+
405
+ :param pattern: Pattern graph.
406
+ :type pattern: nx.Graph
407
+ :param host: Host graph.
408
+ :type host: nx.Graph
409
+ :param max_seeds: Maximum number of seed pairs to keep.
410
+ :type max_seeds: int
411
+ :param wl_pattern: WL colors for ``pattern`` (may be empty).
412
+ :type wl_pattern: dict[int, int]
413
+ :param wl_host: WL colors for ``host`` (may be empty).
414
+ :type wl_host: dict[int, int]
415
+ :return: List of ``(pattern_node, host_node)`` pairs.
416
+ :rtype: list[tuple[int, int]]
417
+ """
418
+ candidates: List[Tuple[float, int, int]] = []
419
+ for p in pattern.nodes():
420
+ for h in host.nodes():
421
+ score = self._node_similarity(
422
+ p,
423
+ h,
424
+ pattern,
425
+ host,
426
+ wl_pattern,
427
+ wl_host,
428
+ )
429
+ if score <= 0.0:
430
+ continue
431
+ candidates.append((score, p, h))
432
+
433
+ candidates.sort(reverse=True, key=lambda t: t[0])
434
+ top = candidates[:max_seeds]
435
+ return [(p, h) for _, p, h in top]
436
+
437
+ def _respects_edges(
438
+ self,
439
+ p: int,
440
+ h: int,
441
+ pattern: nx.Graph,
442
+ host: nx.Graph,
443
+ mapping: MappingDict,
444
+ ) -> bool:
445
+ """Check whether extending mapping with ``p → h`` is locally valid.
446
+
447
+ This ensures that for any already-mapped neighbour ``p_n`` of
448
+ ``p``, the candidate host node ``h`` is adjacent to the mapped
449
+ host node and that edge attributes are compatible.
450
+
451
+ :param p: Pattern node to extend with.
452
+ :type p: int
453
+ :param h: Candidate host node.
454
+ :type h: int
455
+ :param pattern: Pattern graph.
456
+ :type pattern: nx.Graph
457
+ :param host: Host graph.
458
+ :type host: nx.Graph
459
+ :param mapping: Current partial mapping (pattern→host).
460
+ :type mapping: dict[int, int]
461
+ :return: ``True`` if the extension is feasible.
462
+ :rtype: bool
463
+ """
464
+ for p_n, h_n in mapping.items():
465
+ if not pattern.has_edge(p, p_n):
466
+ continue
467
+ if not host.has_edge(h, h_n):
468
+ return False
469
+ attrs_p = pattern[p][p_n]
470
+ attrs_h = host[h][h_n]
471
+ if not self._edge_match(attrs_p, attrs_h):
472
+ return False
473
+ return True
474
+
475
+ def _candidate_hosts(
476
+ self,
477
+ p: int,
478
+ pattern: nx.Graph,
479
+ host: nx.Graph,
480
+ mapping: MappingDict,
481
+ wl_pattern: Dict[int, int],
482
+ wl_host: Dict[int, int],
483
+ ) -> List[int]:
484
+ """Enumerate host nodes that can be matched to pattern node ``p``.
485
+
486
+ :param p: Pattern node.
487
+ :type p: int
488
+ :param pattern: Pattern graph.
489
+ :type pattern: nx.Graph
490
+ :param host: Host graph.
491
+ :type host: nx.Graph
492
+ :param mapping: Current partial mapping (pattern→host).
493
+ :type mapping: dict[int, int]
494
+ :param wl_pattern: WL colors for ``pattern`` (may be empty).
495
+ :type wl_pattern: dict[int, int]
496
+ :param wl_host: WL colors for ``host`` (may be empty).
497
+ :type wl_host: dict[int, int]
498
+ :return: List of feasible host node ids.
499
+ :rtype: list[int]
500
+ """
501
+ mapped_hosts = set(mapping.values())
502
+ pdata = pattern.nodes[p]
503
+ candidates: List[int] = []
504
+
505
+ for h in host.nodes():
506
+ if h in mapped_hosts:
507
+ continue
508
+ hdata = host.nodes[h]
509
+ if not self._node_compatible(pdata, hdata):
510
+ continue
511
+ if not self._respects_edges(p, h, pattern, host, mapping):
512
+ continue
513
+ if self.use_wl and wl_pattern and wl_host:
514
+ score = self._node_similarity(
515
+ p,
516
+ h,
517
+ pattern,
518
+ host,
519
+ wl_pattern,
520
+ wl_host,
521
+ )
522
+ if score <= 0.0:
523
+ continue
524
+ candidates.append(h)
525
+
526
+ return candidates
527
+
528
+ def _grow_from_seed(
529
+ self,
530
+ pattern: nx.Graph,
531
+ host: nx.Graph,
532
+ seed_p: int,
533
+ seed_h: int,
534
+ max_steps: int,
535
+ wl_pattern: Dict[int, int],
536
+ wl_host: Dict[int, int],
537
+ ) -> MappingDict:
538
+ """Greedily grow a subgraph mapping starting from a single seed.
539
+
540
+ :param pattern: Pattern graph.
541
+ :type pattern: nx.Graph
542
+ :param host: Host graph.
543
+ :type host: nx.Graph
544
+ :param seed_p: Seed node in pattern graph.
545
+ :type seed_p: int
546
+ :param seed_h: Seed node in host graph.
547
+ :type seed_h: int
548
+ :param max_steps: Maximum number of growth steps.
549
+ :type max_steps: int
550
+ :param wl_pattern: WL colors for ``pattern`` (may be empty).
551
+ :type wl_pattern: dict[int, int]
552
+ :param wl_host: WL colors for ``host`` (may be empty).
553
+ :type wl_host: dict[int, int]
554
+ :return: Completed partial mapping (pattern→host).
555
+ :rtype: dict[int, int]
556
+ """
557
+ mapping: MappingDict = {seed_p: seed_h}
558
+ frontier: Set[int] = {n for n in pattern.neighbors(seed_p)}
559
+ steps = 0
560
+
561
+ while frontier and steps < max_steps:
562
+ steps += 1
563
+ p = max(frontier, key=pattern.degree.__getitem__)
564
+ frontier.remove(p)
565
+
566
+ candidates = self._candidate_hosts(
567
+ p,
568
+ pattern,
569
+ host,
570
+ mapping,
571
+ wl_pattern,
572
+ wl_host,
573
+ )
574
+ if not candidates:
575
+ continue
576
+
577
+ best_h = max(
578
+ candidates,
579
+ key=lambda h: self._node_similarity(
580
+ p,
581
+ h,
582
+ pattern,
583
+ host,
584
+ wl_pattern,
585
+ wl_host,
586
+ ),
587
+ )
588
+ mapping[p] = best_h
589
+
590
+ for q in pattern.neighbors(p):
591
+ if q in mapping or q in frontier:
592
+ continue
593
+ frontier.add(q)
594
+
595
+ return mapping
596
+
597
+ def _approximate_mappings(
598
+ self,
599
+ pattern: nx.Graph,
600
+ host: nx.Graph,
601
+ max_seeds: int,
602
+ max_steps: int,
603
+ ) -> Tuple[List[MappingDict], int]:
604
+ """Core heuristic search between ``pattern`` and ``host``.
605
+
606
+ This is factored out so that it can be reused for whole-graph,
607
+ component-wise, and reaction-centre searches.
608
+
609
+ :param pattern: Pattern graph (smaller or equal).
610
+ :type pattern: nx.Graph
611
+ :param host: Host graph (larger or equal).
612
+ :type host: nx.Graph
613
+ :param max_seeds: Maximum number of seed node pairs to explore.
614
+ :type max_seeds: int
615
+ :param max_steps: Maximum number of growth steps per seed.
616
+ :type max_steps: int
617
+ :return: Tuple ``(mappings, best_size)`` where ``mappings`` are
618
+ pattern→host and ``best_size`` is the size of the largest
619
+ mapping found.
620
+ :rtype: tuple[list[dict[int, int]], int]
621
+ """
622
+ wl_pattern: Dict[int, int] = {}
623
+ wl_host: Dict[int, int] = {}
624
+ if self.use_wl:
625
+ wl_pattern = self._compute_wl_colors(pattern)
626
+ wl_host = self._compute_wl_colors(host)
627
+
628
+ seeds = self._generate_seeds(
629
+ pattern,
630
+ host,
631
+ max_seeds=max_seeds,
632
+ wl_pattern=wl_pattern,
633
+ wl_host=wl_host,
634
+ )
635
+ if not seeds:
636
+ return [], 0
637
+
638
+ best_size = 0
639
+ all_maps: List[MappingDict] = []
640
+
641
+ for seed_p, seed_h in seeds:
642
+ mapping = self._grow_from_seed(
643
+ pattern,
644
+ host,
645
+ seed_p,
646
+ seed_h,
647
+ max_steps=max_steps,
648
+ wl_pattern=wl_pattern,
649
+ wl_host=wl_host,
650
+ )
651
+ all_maps.append(mapping)
652
+ size = len(mapping)
653
+ if size > best_size:
654
+ best_size = size
655
+
656
+ if self.prune_automorphisms:
657
+ filtered: List[MappingDict] = []
658
+ seen_host_sets: Set[frozenset[int]] = set()
659
+ all_maps.sort(
660
+ key=lambda mapping: (
661
+ -len(mapping),
662
+ repr(tuple(sorted(mapping.items(), key=repr))),
663
+ )
664
+ )
665
+ for mp in all_maps:
666
+ hset = frozenset(mp.values())
667
+ if hset in seen_host_sets:
668
+ continue
669
+ seen_host_sets.add(hset)
670
+ filtered.append(mp)
671
+ all_maps = filtered
672
+
673
+ return all_maps, best_size
674
+
675
+ def _componentwise_approx(
676
+ self,
677
+ G1: nx.Graph,
678
+ G2: nx.Graph,
679
+ *,
680
+ max_seeds: int,
681
+ max_steps: int,
682
+ ) -> MappingDict:
683
+ """Component-wise approximate matching between ``G1`` and ``G2``.
684
+
685
+ Connected components of each graph are sorted by size
686
+ (descending) and matched pairwise (largest with largest, etc.).
687
+ For each pair, an approximate mapping is computed and the best
688
+ mapping is merged into a combined **G1 → G2** mapping.
689
+
690
+ :param G1: First input graph.
691
+ :type G1: nx.Graph
692
+ :param G2: Second input graph.
693
+ :type G2: nx.Graph
694
+ :param max_seeds: Maximum number of seed pairs per component pair.
695
+ :type max_seeds: int
696
+ :param max_steps: Maximum growth steps per seed.
697
+ :type max_steps: int
698
+ :return: Combined mapping from nodes of ``G1`` to nodes of ``G2``.
699
+ :rtype: dict[int, int]
700
+ """
701
+ comps1 = [G1.subgraph(c).copy() for c in nx.connected_components(G1)]
702
+ comps2 = [G2.subgraph(c).copy() for c in nx.connected_components(G2)]
703
+
704
+ comps1.sort(key=lambda g: g.number_of_nodes(), reverse=True)
705
+ comps2.sort(key=lambda g: g.number_of_nodes(), reverse=True)
706
+
707
+ limit = min(len(comps1), len(comps2))
708
+ combined: MappingDict = {}
709
+
710
+ for idx in range(limit):
711
+ sub1 = comps1[idx]
712
+ sub2 = comps2[idx]
713
+
714
+ pattern, host, pattern_is_G1 = self._prepare_orientation(sub1, sub2)
715
+ maps, _ = self._approximate_mappings(
716
+ pattern,
717
+ host,
718
+ max_seeds=max_seeds,
719
+ max_steps=max_steps,
720
+ )
721
+ if not maps:
722
+ continue
723
+
724
+ best = maps[0]
725
+ if pattern_is_G1:
726
+ combined.update(best)
727
+ else:
728
+ combined.update(self._invert_mapping(best))
729
+
730
+ return combined
731
+
732
+ def _bipartite_assign_components(
733
+ self,
734
+ G1: nx.Graph,
735
+ G2: nx.Graph,
736
+ *,
737
+ max_seeds: int,
738
+ max_steps: int,
739
+ ) -> MappingDict:
740
+ """Hungarian-optimal component pairing followed by per-pair approx MCS.
741
+
742
+ Builds an n₁×n₂ score matrix (best approximate MCS size for each
743
+ component pair), solves the linear assignment problem to maximise
744
+ total overlap, then returns the combined **G1 → G2** mapping for the
745
+ optimally assigned pairs.
746
+
747
+ Falls back to :py:meth:`_componentwise_approx` when scipy is not
748
+ available.
749
+
750
+ :param G1: First input graph (after wildcard pruning).
751
+ :type G1: nx.Graph
752
+ :param G2: Second input graph (after wildcard pruning).
753
+ :type G2: nx.Graph
754
+ :param max_seeds: Maximum number of seed pairs per component pair.
755
+ :type max_seeds: int
756
+ :param max_steps: Maximum growth steps per seed.
757
+ :type max_steps: int
758
+ :return: Combined mapping from nodes of ``G1`` to nodes of ``G2``.
759
+ :rtype: dict[int, int]
760
+ """
761
+ try:
762
+ import numpy as np
763
+ from scipy.optimize import linear_sum_assignment
764
+ except ImportError: # pragma: no cover
765
+ return self._componentwise_approx(
766
+ G1, G2, max_seeds=max_seeds, max_steps=max_steps
767
+ )
768
+
769
+ comps1 = [G1.subgraph(c).copy() for c in nx.connected_components(G1)]
770
+ comps2 = [G2.subgraph(c).copy() for c in nx.connected_components(G2)]
771
+ n1, n2 = len(comps1), len(comps2)
772
+
773
+ # Build score matrix and cache results in one pass
774
+ score = np.zeros((n1, n2), dtype=float)
775
+ cache: Dict[Tuple[int, int], Tuple[List[MappingDict], bool]] = {}
776
+ for i, sub1 in enumerate(comps1):
777
+ for j, sub2 in enumerate(comps2):
778
+ pattern, host, pat_is_g1 = self._prepare_orientation(sub1, sub2)
779
+ maps, sz = self._approximate_mappings(
780
+ pattern, host, max_seeds=max_seeds, max_steps=max_steps
781
+ )
782
+ score[i, j] = float(sz)
783
+ cache[(i, j)] = (maps, pat_is_g1)
784
+
785
+ rows, cols = linear_sum_assignment(-score)
786
+ combined: MappingDict = {}
787
+ for i, j in zip(rows, cols):
788
+ if score[i, j] == 0.0:
789
+ continue
790
+ maps, pattern_is_g1 = cache[(i, j)]
791
+ if not maps:
792
+ continue
793
+ best = maps[0]
794
+ if pattern_is_g1:
795
+ combined.update(best)
796
+ else:
797
+ combined.update(self._invert_mapping(best))
798
+
799
+ return combined
800
+
801
+ def _find_mcs_mol_approx(
802
+ self,
803
+ G1: nx.Graph,
804
+ G2: nx.Graph,
805
+ *,
806
+ max_seeds: int,
807
+ max_steps: int,
808
+ ) -> MappingDict:
809
+ """Approximate molecule-level (component) matching in G1→G2.
810
+
811
+ This mirrors :py:meth:`MCSMatcher._find_mcs_mol` but uses the
812
+ heuristic search rather than exact isomorphism checks.
813
+
814
+ :param G1: First graph (treated as source of components).
815
+ :type G1: nx.Graph
816
+ :param G2: Second graph (target for component mapping).
817
+ :type G2: nx.Graph
818
+ :param max_seeds: Maximum number of seeds per component pair.
819
+ :type max_seeds: int
820
+ :param max_steps: Maximum growth steps per seed.
821
+ :type max_steps: int
822
+ :return: Combined mapping from nodes of ``G1`` to nodes of ``G2``.
823
+ :rtype: dict[int, int]
824
+ """
825
+ comps1 = sorted(nx.connected_components(G1), key=len, reverse=True)
826
+ comps2 = sorted(nx.connected_components(G2), key=len, reverse=True)
827
+
828
+ used2: Set[frozenset[int]] = set()
829
+ combined: MappingDict = {}
830
+
831
+ for comp1 in comps1:
832
+ size1 = len(comp1)
833
+ sub1 = G1.subgraph(comp1)
834
+
835
+ best_map: MappingDict = {}
836
+ best_size = 0
837
+ best_key2: Optional[frozenset[int]] = None
838
+
839
+ for comp2 in comps2:
840
+ if len(comp2) != size1:
841
+ continue
842
+ key2 = frozenset(comp2)
843
+ if key2 in used2:
844
+ continue
845
+
846
+ sub2 = G2.subgraph(comp2)
847
+ pattern, host, pattern_is_G1 = self._prepare_orientation(
848
+ sub1,
849
+ sub2,
850
+ )
851
+ maps, local_best = self._approximate_mappings(
852
+ pattern,
853
+ host,
854
+ max_seeds=max_seeds,
855
+ max_steps=max_steps,
856
+ )
857
+ if not maps or local_best == 0:
858
+ continue
859
+
860
+ if local_best > best_size:
861
+ best_size = local_best
862
+ best = maps[0]
863
+ if pattern_is_G1:
864
+ best_map = best
865
+ else:
866
+ best_map = self._invert_mapping(best)
867
+ best_key2 = key2
868
+
869
+ if best_map and best_key2 is not None:
870
+ combined.update(best_map)
871
+ used2.add(best_key2)
872
+
873
+ return combined
874
+
875
+ def _select_graphs_from_rc(
876
+ self,
877
+ rc1: Any,
878
+ rc2: Any,
879
+ side: str,
880
+ ) -> Tuple[Any, Any]:
881
+ """Select ITS sides or treat inputs as graphs.
882
+
883
+ :param rc1: First reaction-centre or ITS-like graph object.
884
+ :type rc1: Any
885
+ :param rc2: Second reaction-centre or ITS-like graph object.
886
+ :type rc2: Any
887
+ :param side: Which ITS sides to compare.
888
+ :type side: str
889
+ :return: Tuple ``(G1, G2)`` as graphs.
890
+ :rtype: tuple[Any, Any]
891
+ :raises ImportError: If ITS utilities are required but missing.
892
+ :raises ValueError: If ``side`` is invalid.
893
+ """
894
+ side_norm = side.lower()
895
+
896
+ if side_norm == "its":
897
+ return rc1, rc2
898
+
899
+ if its_decompose is None:
900
+ raise ImportError(
901
+ "synkit is not available; cannot decompose reaction centres "
902
+ "for side values 'r', 'l' or 'op'."
903
+ )
904
+
905
+ l1, r1 = its_decompose(rc1)
906
+ l2, r2 = its_decompose(rc2)
907
+
908
+ if side_norm == "r":
909
+ return r1, r2
910
+ if side_norm == "l":
911
+ return l1, l2
912
+ if side_norm == "op":
913
+ return r1, l2
914
+
915
+ raise ValueError(
916
+ "ApproxMCSMatcher.find_rc_mapping: side must be one of "
917
+ "'r', 'l', 'op', 'its', got "
918
+ f"{side!r}."
919
+ )
920
+
921
+ # ------------------------------------------------------------------
922
+ # Public approximate search – graph level
923
+ # ------------------------------------------------------------------
924
+ def find_common_subgraph_approx(
925
+ self,
926
+ G1: nx.Graph,
927
+ G2: nx.Graph,
928
+ *,
929
+ max_seeds: int = 16,
930
+ max_steps: int = 256,
931
+ ) -> "ApproxMCSMatcher":
932
+ """Heuristically search for approximate common subgraphs.
933
+
934
+ This is a lightweight wrapper that ignores molecule-level
935
+ options and simply runs the greedy approximate search on the
936
+ whole (possibly wildcard-pruned) graphs.
937
+
938
+ :param G1: First input graph.
939
+ :type G1: nx.Graph
940
+ :param G2: Second input graph.
941
+ :type G2: nx.Graph
942
+ :param max_seeds: Maximum number of seed node pairs to explore.
943
+ :type max_seeds: int
944
+ :param max_steps: Maximum number of growth steps per seed.
945
+ :type max_steps: int
946
+ :return: The matcher instance (with cache updated).
947
+ :rtype: ApproxMCSMatcher
948
+ """
949
+ return self.find_common_subgraph(
950
+ G1,
951
+ G2,
952
+ mcs=False,
953
+ mcs_mol=False,
954
+ max_seeds=max_seeds,
955
+ max_steps=max_steps,
956
+ )
957
+
958
+ def find_common_subgraph(
959
+ self,
960
+ G1: nx.Graph,
961
+ G2: nx.Graph,
962
+ *,
963
+ mcs: bool = False,
964
+ mcs_mol: bool = False,
965
+ max_seeds: int = 16,
966
+ max_steps: int = 256,
967
+ ) -> "ApproxMCSMatcher":
968
+ """Approximate analogue of :py:meth:`MCSMatcher.find_common_subgraph`.
969
+
970
+ The signature mirrors the exact matcher, but the implementation
971
+ is greedy/heuristic:
972
+
973
+ 1. Optionally prunes wildcard nodes from both graphs.
974
+ 2. If ``mcs_mol`` is ``True``, performs component-level
975
+ (molecule-level) approximate matching with
976
+ :py:meth:`_find_mcs_mol_approx`.
977
+ 3. Otherwise, orients the pair so that the smaller graph is the
978
+ pattern and runs the heuristic search.
979
+
980
+ The ``mcs`` flag is accepted for API compatibility but
981
+ has no distinct effect here; the heuristic always aims for large
982
+ mappings.
983
+
984
+ :param G1: First input graph.
985
+ :type G1: nx.Graph
986
+ :param G2: Second input graph.
987
+ :type G2: nx.Graph
988
+ :param mcs: Ignored (kept for API compatibility).
989
+ :type mcs: bool
990
+ :param mcs_mol: If ``True``, perform approximate
991
+ connected-component (molecule-level) matching.
992
+ :type mcs_mol: bool
993
+ :param max_seeds: Maximum number of seed pairs.
994
+ :type max_seeds: int
995
+ :param max_steps: Maximum growth steps per seed.
996
+ :type max_steps: int
997
+ :return: The matcher instance (with internal cache updated).
998
+ :rtype: ApproxMCSMatcher
999
+ """
1000
+ del mcs # unused, kept for signature compatibility
1001
+
1002
+ self._mappings = []
1003
+ self._last_size = 0
1004
+ self._last_pattern_is_G1 = None
1005
+
1006
+ G1_use = self._prune_graph(G1)
1007
+ G2_use = self._prune_graph(G2)
1008
+
1009
+ if mcs_mol:
1010
+ combined = self._find_mcs_mol_approx(
1011
+ G1_use,
1012
+ G2_use,
1013
+ max_seeds=max_seeds,
1014
+ max_steps=max_steps,
1015
+ )
1016
+ self._mappings = [combined] # G1 -> G2
1017
+ self._last_size = len(combined)
1018
+ self._last_pattern_is_G1 = True
1019
+ return self
1020
+
1021
+ pattern, host, pattern_is_G1 = self._prepare_orientation(G1_use, G2_use)
1022
+ self._last_pattern_is_G1 = pattern_is_G1
1023
+
1024
+ maps, best_size = self._approximate_mappings(
1025
+ pattern,
1026
+ host,
1027
+ max_seeds=max_seeds,
1028
+ max_steps=max_steps,
1029
+ )
1030
+ self._mappings = maps
1031
+ self._last_size = best_size
1032
+
1033
+ return self
1034
+
1035
+ # ------------------------------------------------------------------
1036
+ # Public approximate search – ITS / reaction-centre level
1037
+ # ------------------------------------------------------------------
1038
+ def find_rc_mapping(
1039
+ self,
1040
+ rc1: Any,
1041
+ rc2: Any,
1042
+ *,
1043
+ side: str = "op",
1044
+ mcs: bool = True,
1045
+ mcs_mol: bool = False,
1046
+ component: bool = True,
1047
+ max_seeds: int = 16,
1048
+ max_steps: int = 256,
1049
+ ) -> "ApproxMCSMatcher":
1050
+ """Convenience wrapper for ITS reaction-centre or ITS-like graph
1051
+ objects, analogous to :py:meth:`MCSMatcher.find_rc_mapping` but
1052
+ using the heuristic search internally.
1053
+
1054
+ Depending on ``side``, this either uses
1055
+ :func:`synkit.Graph.ITS.its_decompose` to obtain left/right
1056
+ graphs or treats the inputs directly as graphs.
1057
+
1058
+ Side selection
1059
+ --------------
1060
+ * ``'r'`` → compare right sides: ``r1`` vs ``r2``.
1061
+ * ``'l'`` → compare left sides: ``l1`` vs ``l2``.
1062
+ * ``'op'`` → compare opposite: ``r1`` vs ``l2``.
1063
+ * ``'its'`` → treat ``rc1`` and ``rc2`` directly as graphs
1064
+ (no decomposition), useful when the inputs are already ITS
1065
+ (or ITS-like) :class:`networkx.Graph` objects.
1066
+
1067
+ Component-wise mode
1068
+ -------------------
1069
+ If ``component`` is ``True``, the selected graphs are
1070
+ decomposed into connected components, sorted by size
1071
+ (descending), and matched pairwise using
1072
+ :py:meth:`_componentwise_approx`. The resulting mappings are
1073
+ combined into a single **G1 → G2** mapping in terms of the
1074
+ original node ids. In this mode, ``mcs_mol`` is ignored.
1075
+
1076
+ :param rc1: First reaction-centre or ITS-like graph object.
1077
+ :type rc1: Any
1078
+ :param rc2: Second reaction-centre or ITS-like graph object.
1079
+ :type rc2: Any
1080
+ :param side: Which ITS sides to compare (``'r'``, ``'l'``,
1081
+ ``'op'``, or ``'its'``).
1082
+ :type side: str
1083
+ :param mcs: Ignored (kept for compatibility with
1084
+ :class:`MCSMatcher`).
1085
+ :type mcs: bool
1086
+ :param mcs_mol: If ``True`` and ``component`` is
1087
+ ``False``, use approximate molecule-level matching via
1088
+ :py:meth:`find_common_subgraph` with
1089
+ ``mcs_mol=True``.
1090
+ :type mcs_mol: bool
1091
+ :param component: If ``True``, perform size-sorted,
1092
+ component-wise approximate matching between the selected
1093
+ sides and combine the per-component mappings into a single
1094
+ mapping.
1095
+ :type component: bool
1096
+ :param max_seeds: Maximum number of seeds per call.
1097
+ :type max_seeds: int
1098
+ :param max_steps: Maximum growth steps per seed.
1099
+ :type max_steps: int
1100
+ :return: The matcher instance (with internal cache updated).
1101
+ :rtype: ApproxMCSMatcher
1102
+ :raises ImportError: If :mod:`synkit` ITS utilities are not
1103
+ available for ``side`` in ``{'r', 'l', 'op'}``.
1104
+ :raises ValueError: If ``side`` is not one of
1105
+ ``'r'``, ``'l'``, ``'op'``, ``'its'``.
1106
+ """
1107
+ del mcs # unused, kept for signature compatibility
1108
+
1109
+ self._mappings = []
1110
+ self._last_size = 0
1111
+ self._last_pattern_is_G1 = None
1112
+
1113
+ G1, G2 = self._select_graphs_from_rc(rc1, rc2, side)
1114
+
1115
+ G1_use = self._prune_graph(G1)
1116
+ G2_use = self._prune_graph(G2)
1117
+
1118
+ if component:
1119
+ combined = self._bipartite_assign_components(
1120
+ G1_use,
1121
+ G2_use,
1122
+ max_seeds=max_seeds,
1123
+ max_steps=max_steps,
1124
+ )
1125
+ self._mappings = [combined] # G1 -> G2
1126
+ self._last_size = len(combined)
1127
+ self._last_pattern_is_G1 = True
1128
+ return self
1129
+
1130
+ return self.find_common_subgraph(
1131
+ G1_use,
1132
+ G2_use,
1133
+ mcs=False,
1134
+ mcs_mol=mcs_mol,
1135
+ max_seeds=max_seeds,
1136
+ max_steps=max_steps,
1137
+ )
1138
+
1139
+ # ------------------------------------------------------------------
1140
+ # Accessors / properties
1141
+ # ------------------------------------------------------------------
1142
+ def get_mappings(self, direction: str = "pattern_to_host") -> List[MappingDict]:
1143
+ """Return a copy of the cached mapping list in the requested
1144
+ orientation.
1145
+
1146
+ Internal orientation is **pattern → host**. This method can
1147
+ convert to ``G1→G2`` or ``G2→G1`` based on the last call to
1148
+ :py:meth:`find_common_subgraph`,
1149
+ :py:meth:`find_common_subgraph_approx` or
1150
+ :py:meth:`find_rc_mapping`.
1151
+
1152
+ :param direction: Orientation of the returned mappings. One of:
1153
+ * ``"pattern_to_host"`` (default)
1154
+ * ``"G1_to_G2"``
1155
+ * ``"G2_to_G1"``
1156
+ :type direction: str
1157
+ :return: List of mapping dictionaries.
1158
+ :rtype: list[dict[int, int]]
1159
+ :raises ValueError: If the direction is not supported.
1160
+ """
1161
+ if direction == "pattern_to_host" or self._last_pattern_is_G1 is None:
1162
+ return [dict(m) for m in self._mappings]
1163
+
1164
+ if direction not in {"G1_to_G2", "G2_to_G1"}:
1165
+ raise ValueError(
1166
+ "ApproxMCSMatcher.get_mappings: direction must be one of "
1167
+ "'pattern_to_host', 'G1_to_G2', 'G2_to_G1'."
1168
+ )
1169
+
1170
+ pattern_is_G1 = self._last_pattern_is_G1
1171
+ result: List[MappingDict] = []
1172
+
1173
+ for m in self._mappings:
1174
+ if direction == "G1_to_G2":
1175
+ if pattern_is_G1:
1176
+ result.append(dict(m))
1177
+ else:
1178
+ result.append(self._invert_mapping(m))
1179
+ else:
1180
+ if pattern_is_G1:
1181
+ result.append(self._invert_mapping(m))
1182
+ else:
1183
+ result.append(dict(m))
1184
+
1185
+ return result
1186
+
1187
+ @property
1188
+ def mappings(self) -> List[MappingDict]:
1189
+ """Cached approximate mappings from the most recent search.
1190
+
1191
+ The orientation is pattern→host. For ``G1→G2`` or ``G2→G1``,
1192
+ use :py:meth:`get_mappings`.
1193
+
1194
+ :return: List of cached mapping dictionaries.
1195
+ :rtype: list[dict[int, int]]
1196
+ """
1197
+ return self.get_mappings(direction="pattern_to_host")
1198
+
1199
+ @property
1200
+ def last_size(self) -> int:
1201
+ """Size of the largest approximate mapping from the last search.
1202
+
1203
+ :return: Size of the best mapping.
1204
+ :rtype: int
1205
+ """
1206
+ return self._last_size
1207
+
1208
+ @property
1209
+ def num_mappings(self) -> int:
1210
+ """Number of approximate mappings stored from the last search.
1211
+
1212
+ :return: Count of mappings.
1213
+ :rtype: int
1214
+ """
1215
+ return len(self._mappings)
1216
+
1217
+ @property
1218
+ def mapping_direction(self) -> str:
1219
+ """Human-readable description of internal mapping orientation.
1220
+
1221
+ :return: ``"G1_to_G2"``, ``"G2_to_G1"``, or ``"unknown"`` if
1222
+ no search has been run.
1223
+ :rtype: str
1224
+ """
1225
+ if self._last_pattern_is_G1 is None:
1226
+ return "unknown"
1227
+ return "G1_to_G2" if self._last_pattern_is_G1 else "G2_to_G1"
1228
+
1229
+ # ------------------------------------------------------------------
1230
+ # Iteration & niceties
1231
+ # ------------------------------------------------------------------
1232
+ def __iter__(self) -> Iterable[MappingDict]:
1233
+ """Iterate over cached mappings in pattern→host orientation.
1234
+
1235
+ :return: Iterator over mapping dictionaries.
1236
+ :rtype: Iterable[dict[int, int]]
1237
+ """
1238
+ return iter(self._mappings)
1239
+
1240
+ def __repr__(self) -> str:
1241
+ """Short textual representation for debugging.
1242
+
1243
+ :return: Summary string with key attributes.
1244
+ :rtype: str
1245
+ """
1246
+ return (
1247
+ f"<ApproxMCSMatcher mappings={self.num_mappings} "
1248
+ f"last_size={self.last_size} "
1249
+ f"prune_wc={self.prune_wc} "
1250
+ f"prune_automorphisms={self.prune_automorphisms} "
1251
+ f"use_wl={self.use_wl} "
1252
+ f"direction={self.mapping_direction}>"
1253
+ )
1254
+
1255
+ __str__ = __repr__
1256
+
1257
+ @property
1258
+ def help(self) -> str:
1259
+ """Return the module-level documentation string.
1260
+
1261
+ :return: The full module docstring, if available.
1262
+ :rtype: str
1263
+ """
1264
+ return __doc__ or ""