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,812 @@
1
+ """Connected-component-aware subgraph matching utilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Dict, List, Set, Optional, Sequence, Tuple, Callable, Union
6
+ from operator import eq
7
+ import networkx as nx
8
+
9
+ from networkx.algorithms.isomorphism import GraphMatcher
10
+ from networkx.algorithms.isomorphism import generic_node_match, generic_edge_match
11
+
12
+ from synkit.Synthesis.Reactor import Strategy
13
+
14
+ # ---------------------------------------------------------------------------
15
+ # Type aliases
16
+ # ---------------------------------------------------------------------------
17
+ EdgeAttr = Dict[str, Any]
18
+ MappingDict = Dict[int, int]
19
+
20
+ __all__: Sequence[str] = [
21
+ "SubgraphMatch",
22
+ "SubgraphSearchEngine",
23
+ ]
24
+
25
+
26
+ def electron_aware_node_match(
27
+ host_data: EdgeAttr,
28
+ pattern_data: EdgeAttr,
29
+ node_attrs: Sequence[str],
30
+ ) -> bool:
31
+ """Compare node attributes with chemistry-aware cardinality semantics.
32
+
33
+ Attributes in ``node_attrs`` are exact matches except:
34
+
35
+ - ``hcount``: host must be greater than or equal to pattern
36
+ - ``lone_pairs``: host must be greater than or equal to pattern
37
+ - an attribute explicitly marked ``unknown`` by the typed query policy is
38
+ omitted because the template's local context cannot determine it
39
+
40
+ ``radical`` otherwise remains exact whenever the caller includes it.
41
+ """
42
+ for attr in node_attrs:
43
+ policy = pattern_data.get("_query_attribute_policies", {}).get(
44
+ attr,
45
+ "exact",
46
+ )
47
+ if policy == "unknown":
48
+ continue
49
+ host_value = host_data.get(
50
+ attr, 0 if attr in {"hcount", "lone_pairs"} else None
51
+ )
52
+ pattern_value = pattern_data.get(
53
+ attr, 0 if attr in {"hcount", "lone_pairs"} else None
54
+ )
55
+ if attr in {"hcount", "lone_pairs"}:
56
+ if host_value < pattern_value:
57
+ return False
58
+ continue
59
+ if host_value != pattern_value:
60
+ return False
61
+ return True
62
+
63
+
64
+ def electron_aware_edge_match(
65
+ host_data: EdgeAttr,
66
+ pattern_data: EdgeAttr,
67
+ edge_attrs: Sequence[str],
68
+ ) -> bool:
69
+ """Compare edge attrs while treating aromatic Kekule phase as non-semantic.
70
+
71
+ Aromatic presentation bonds are matched by ``order == 1.5``. Their
72
+ particular ``sigma_order`` / ``pi_order`` split depends on the chosen
73
+ Kekule form and is not stable across independently parsed graphs.
74
+ """
75
+ minimum_pi = pattern_data.get("_minimum_pi_order")
76
+ if minimum_pi is not None:
77
+ # Relative pi reduction: C=C means "a bond carrying at least one pi
78
+ # bond" at this explicitly marked reaction locus. Thus the same
79
+ # chemical edit can consume C=C or C#C without weakening matching
80
+ # anywhere else in the pattern.
81
+ if host_data.get("order") == 1.5:
82
+ return False
83
+ if float(host_data.get("pi_order", 0.0)) < float(minimum_pi):
84
+ return False
85
+
86
+ host_is_aromatic = host_data.get("order") == 1.5
87
+ pattern_is_aromatic = pattern_data.get("order") == 1.5
88
+ for attr in edge_attrs:
89
+ if minimum_pi is not None and attr in {"order", "pi_order"}:
90
+ continue
91
+ if (
92
+ attr in {"sigma_order", "pi_order"}
93
+ and host_is_aromatic
94
+ and pattern_is_aromatic
95
+ ):
96
+ continue
97
+ if host_data.get(attr) != pattern_data.get(attr):
98
+ return False
99
+ return True
100
+
101
+
102
+ def explain_node_mismatch(
103
+ host_data: EdgeAttr,
104
+ pattern_data: EdgeAttr,
105
+ node_attrs: Sequence[str],
106
+ ) -> list[str]:
107
+ """Return node-level mismatch reasons using matcher semantics."""
108
+ reasons: list[str] = []
109
+ for attr in node_attrs:
110
+ policy = pattern_data.get("_query_attribute_policies", {}).get(
111
+ attr,
112
+ "exact",
113
+ )
114
+ if policy == "unknown":
115
+ continue
116
+ host_value = host_data.get(
117
+ attr, 0 if attr in {"hcount", "lone_pairs"} else None
118
+ )
119
+ pattern_value = pattern_data.get(
120
+ attr, 0 if attr in {"hcount", "lone_pairs"} else None
121
+ )
122
+ if attr in {"hcount", "lone_pairs"}:
123
+ if host_value < pattern_value:
124
+ reasons.append(f"{attr}: host {host_value} < pattern {pattern_value}")
125
+ continue
126
+ if host_value != pattern_value:
127
+ reasons.append(f"{attr}: host {host_value!r} != pattern {pattern_value!r}")
128
+ return reasons
129
+
130
+
131
+ def resolve_template_match_attrs(
132
+ pattern: nx.Graph,
133
+ *,
134
+ legacy_node_attrs: Sequence[str] = ("element", "charge"),
135
+ legacy_edge_attrs: Sequence[str] = ("order",),
136
+ ) -> tuple[list[str], list[str]]:
137
+ """Choose match attrs from what the template actually carries.
138
+
139
+ Legacy templates keep the legacy attribute set. Electron-aware templates opt
140
+ into extra constraints only when those attrs are present on the template.
141
+ """
142
+ node_attrs = list(legacy_node_attrs)
143
+ edge_attrs = list(legacy_edge_attrs)
144
+
145
+ for attr in (
146
+ "aromatic",
147
+ "hcount",
148
+ "lone_pairs",
149
+ "radical",
150
+ ):
151
+ if any(attr in data for _, data in pattern.nodes(data=True)):
152
+ node_attrs.append(attr)
153
+
154
+ for attr in ("sigma_order", "pi_order"):
155
+ if any(attr in data for _, _, data in pattern.edges(data=True)):
156
+ edge_attrs.append(attr)
157
+
158
+ return node_attrs, edge_attrs
159
+
160
+
161
+ def diagnose_candidate_node_match(
162
+ host_data: EdgeAttr,
163
+ pattern_data: EdgeAttr,
164
+ node_attrs: Sequence[str],
165
+ ) -> dict[str, Any]:
166
+ """Return a compact node-match diagnostic payload."""
167
+ reasons = explain_node_mismatch(host_data, pattern_data, node_attrs)
168
+ return {"matched": not reasons, "reasons": reasons}
169
+
170
+
171
+ # ---------------------------------------------------------------------------
172
+ # Core engine class
173
+ # ---------------------------------------------------------------------------
174
+ class SubgraphMatch:
175
+ """Boolean-only checks for graph isomorphism and subgraph (induced or
176
+ monomorphic) matching.
177
+
178
+ Provides static methods for NetworkX-based checks.
179
+ """
180
+
181
+ @staticmethod
182
+ def subgraph_isomorphism(
183
+ child_graph: nx.Graph,
184
+ parent_graph: nx.Graph,
185
+ node_label_names: List[str] = ["element", "charge"],
186
+ node_label_default: List[Any] = ["*", 0],
187
+ edge_attribute: str = "order",
188
+ use_filter: bool = False,
189
+ check_type: str = "induced", # 'induced' or 'monomorphism'
190
+ node_comparator: Optional[Callable[[Any, Any], bool]] = None,
191
+ edge_comparator: Optional[Callable[[Any, Any], bool]] = None,
192
+ ) -> bool:
193
+ """Enhanced checks if the child graph is a subgraph isomorphic to the
194
+ parent graph based on customizable node and edge attributes."""
195
+ if use_filter:
196
+ if (
197
+ child_graph.number_of_nodes() > parent_graph.number_of_nodes()
198
+ or child_graph.number_of_edges() > parent_graph.number_of_edges()
199
+ ):
200
+ return False
201
+
202
+ node_comparator = node_comparator or eq
203
+ edge_comparator = edge_comparator or eq
204
+
205
+ for _, child_data in child_graph.nodes(data=True):
206
+ found_match = False
207
+ for _, parent_data in parent_graph.nodes(data=True):
208
+ match = True
209
+ for label, default in zip(node_label_names, node_label_default):
210
+ if not node_comparator(
211
+ parent_data.get(label, default),
212
+ child_data.get(label, default),
213
+ ):
214
+ match = False
215
+ break
216
+ if match:
217
+ found_match = True
218
+ break
219
+ if not found_match:
220
+ return False
221
+
222
+ if edge_attribute:
223
+ parent_edge_values = [
224
+ data.get(edge_attribute)
225
+ for _, _, data in parent_graph.edges(data=True)
226
+ ]
227
+ for _, _, child_data in child_graph.edges(data=True):
228
+ child_value = child_data.get(edge_attribute)
229
+ if not any(
230
+ edge_comparator(parent_value, child_value)
231
+ for parent_value in parent_edge_values
232
+ ):
233
+ return False
234
+
235
+ node_comparator = node_comparator or eq
236
+ edge_comparator = edge_comparator or eq
237
+
238
+ node_match = generic_node_match(
239
+ node_label_names,
240
+ node_label_default,
241
+ [node_comparator] * len(node_label_names),
242
+ )
243
+ edge_match = generic_edge_match(edge_attribute, None, edge_comparator)
244
+
245
+ if child_graph.is_directed() != parent_graph.is_directed():
246
+ return False
247
+ if child_graph.is_multigraph() or parent_graph.is_multigraph():
248
+ raise NotImplementedError("SubgraphMatch does not support multigraphs")
249
+ matcher_cls = (
250
+ nx.algorithms.isomorphism.DiGraphMatcher
251
+ if parent_graph.is_directed()
252
+ else GraphMatcher
253
+ )
254
+ matcher = matcher_cls(
255
+ parent_graph, child_graph, node_match=node_match, edge_match=edge_match
256
+ )
257
+
258
+ if check_type == "induced":
259
+ return matcher.subgraph_is_isomorphic()
260
+ else:
261
+ return matcher.subgraph_is_monomorphic()
262
+
263
+ @staticmethod
264
+ def is_subgraph(
265
+ pattern: Union[nx.Graph, str],
266
+ host: Union[nx.Graph, str],
267
+ node_label_names: List[str] = ["element", "charge"],
268
+ node_label_default: List[Any] = ["*", 0],
269
+ edge_attribute: str = "order",
270
+ use_filter: bool = False,
271
+ check_type: str = "induced",
272
+ backend: str = "nx",
273
+ ) -> bool:
274
+ """Run a native NetworkX subgraph or isomorphism check."""
275
+ if backend != "nx":
276
+ raise ValueError(f"Unknown backend: {backend}")
277
+ if not isinstance(pattern, nx.Graph) or not isinstance(host, nx.Graph):
278
+ raise TypeError("NetworkX backend expects graph inputs.")
279
+ return SubgraphMatch.subgraph_isomorphism(
280
+ pattern,
281
+ host,
282
+ node_label_names,
283
+ node_label_default,
284
+ edge_attribute,
285
+ use_filter,
286
+ check_type,
287
+ )
288
+
289
+
290
+ # -----------------------------------------------------------------------------
291
+ # Sub‑graph search engine
292
+ # -----------------------------------------------------------------------------
293
+
294
+
295
+ class SubgraphSearchEngine:
296
+ """Static helper routines for sub-graph monomorphism search.
297
+
298
+ :cvar DEFAULT_THRESHOLD: default cap on embedding enumeration (5000)
299
+ """
300
+
301
+ DEFAULT_THRESHOLD: int = 5_000
302
+ _COMPILED_MATCH_CAP: int = 1_000_000
303
+
304
+ @staticmethod
305
+ def _quick_pre_filter(
306
+ host: nx.Graph,
307
+ pattern: nx.Graph,
308
+ node_attrs: List[str],
309
+ ) -> bool:
310
+ """Return whether a degree-aware node domain proves no embedding exists.
311
+
312
+ A Cartesian product of candidate-domain sizes is only a loose upper
313
+ bound on the number of embeddings. It must not be compared with the
314
+ result threshold: graph connectivity can reduce a very large product
315
+ to only a handful of valid mappings. The enumerator itself enforces
316
+ ``threshold`` safely.
317
+ """
318
+ # Pre-compute pattern degrees
319
+ pat_degrees = {n: pattern.degree(n) for n in pattern.nodes()}
320
+ for p_node, pat_data in pattern.nodes(data=True):
321
+ pat_deg = pat_degrees[p_node]
322
+ # count host nodes matching attributes and degree
323
+ count = sum(
324
+ 1
325
+ for _, host_data in host.nodes(data=True)
326
+ if electron_aware_node_match(host_data, pat_data, node_attrs)
327
+ and host.degree(_) >= pat_deg
328
+ )
329
+ # if no candidates; impossible match
330
+ if count == 0:
331
+ return True
332
+ return False
333
+
334
+ @staticmethod
335
+ def _find_compiled_domain_mappings(
336
+ host: nx.Graph,
337
+ pattern: nx.Graph,
338
+ node_attrs: List[str],
339
+ edge_attrs: List[str],
340
+ ) -> Optional[List[MappingDict]]:
341
+ """Enumerate an exact node-domain query with RDKit's compiled matcher.
342
+
343
+ Each host atom receives an isotope identifying the complete set of
344
+ pattern nodes accepted by ``electron_aware_node_match``. A pattern
345
+ query atom is the OR of precisely those isotope domains that contain
346
+ it. Thus the compiled query preserves the authoritative node predicate
347
+ exactly; edge attributes are checked by the established predicate on
348
+ every returned topological embedding.
349
+
350
+ ``None`` requests the NetworkX fallback for unsupported graph kinds or
351
+ if the defensive raw-match cap is reached. Consequently the cap can
352
+ never truncate a returned mapping population.
353
+ """
354
+ if (
355
+ host.is_directed()
356
+ or pattern.is_directed()
357
+ or host.is_multigraph()
358
+ or pattern.is_multigraph()
359
+ or nx.number_of_selfloops(host)
360
+ or nx.number_of_selfloops(pattern)
361
+ ):
362
+ return None
363
+ if not pattern:
364
+ return [{}]
365
+
366
+ from rdkit import Chem
367
+ from rdkit.Chem import rdqueries
368
+
369
+ pattern_nodes = tuple(pattern)
370
+ domain_ids: Dict[Tuple[Any, ...], int] = {}
371
+ host_domains: Dict[Any, int] = {}
372
+ for host_node, host_attrs in host.nodes(data=True):
373
+ domain = tuple(
374
+ pattern_node
375
+ for pattern_node in pattern_nodes
376
+ if electron_aware_node_match(
377
+ host_attrs,
378
+ pattern.nodes[pattern_node],
379
+ node_attrs,
380
+ )
381
+ )
382
+ if not domain:
383
+ host_domains[host_node] = 0
384
+ continue
385
+ domain_id = domain_ids.get(domain)
386
+ if domain_id is None:
387
+ domain_id = len(domain_ids) + 1
388
+ if domain_id > 65535:
389
+ return None
390
+ domain_ids[domain] = domain_id
391
+ host_domains[host_node] = domain_id
392
+
393
+ allowed_domains = {
394
+ pattern_node: tuple(
395
+ domain_id
396
+ for domain, domain_id in domain_ids.items()
397
+ if pattern_node in domain
398
+ )
399
+ for pattern_node in pattern_nodes
400
+ }
401
+ if any(not domains for domains in allowed_domains.values()):
402
+ return []
403
+
404
+ host_builder = Chem.RWMol()
405
+ host_nodes = tuple(host)
406
+ host_index = {}
407
+ for node in host_nodes:
408
+ atom = Chem.Atom(0)
409
+ atom.SetIsotope(host_domains[node])
410
+ host_index[node] = host_builder.AddAtom(atom)
411
+ for left, right in host.edges():
412
+ host_builder.AddBond(
413
+ host_index[left],
414
+ host_index[right],
415
+ Chem.BondType.SINGLE,
416
+ )
417
+
418
+ query_builder = Chem.RWMol()
419
+ query_index = {}
420
+ for node in pattern_nodes:
421
+ domains = allowed_domains[node]
422
+ query = rdqueries.IsotopeEqualsQueryAtom(domains[0])
423
+ for domain_id in domains[1:]:
424
+ query.ExpandQuery(
425
+ rdqueries.IsotopeEqualsQueryAtom(domain_id),
426
+ Chem.CompositeQueryType.COMPOSITE_OR,
427
+ )
428
+ query_index[node] = query_builder.AddAtom(query)
429
+ for left, right in pattern.edges():
430
+ query_builder.AddBond(
431
+ query_index[left],
432
+ query_index[right],
433
+ Chem.BondType.SINGLE,
434
+ )
435
+
436
+ matches = host_builder.GetMol().GetSubstructMatches(
437
+ query_builder.GetMol(),
438
+ uniquify=False,
439
+ useChirality=False,
440
+ maxMatches=SubgraphSearchEngine._COMPILED_MATCH_CAP,
441
+ )
442
+ if len(matches) == SubgraphSearchEngine._COMPILED_MATCH_CAP:
443
+ return None
444
+
445
+ results = []
446
+ for match in matches:
447
+ mapping = {
448
+ pattern_nodes[index]: host_nodes[host_atom_index]
449
+ for index, host_atom_index in enumerate(match)
450
+ }
451
+ if all(
452
+ electron_aware_edge_match(
453
+ host.edges[mapping[left], mapping[right]],
454
+ attrs,
455
+ edge_attrs,
456
+ )
457
+ for left, right, attrs in pattern.edges(data=True)
458
+ ):
459
+ results.append(mapping)
460
+ return results
461
+
462
+ @staticmethod
463
+ def find_subgraph_mappings(
464
+ host: nx.Graph,
465
+ pattern: nx.Graph,
466
+ *,
467
+ node_attrs: List[str],
468
+ edge_attrs: List[str],
469
+ strategy: Union[str, Strategy] = Strategy.COMPONENT,
470
+ max_results: Optional[int] = None,
471
+ strict_cc_count: bool = True,
472
+ threshold: Optional[int] = DEFAULT_THRESHOLD,
473
+ pre_filter: bool = False,
474
+ ) -> List[MappingDict]:
475
+ """Dispatch to a subgraph-matching strategy with optional guards.
476
+
477
+ :param host: NetworkX graphs (host ≥ pattern).
478
+ :param pattern: NetworkX graphs (host ≥ pattern).
479
+ :param node_attrs: Keys of attributes to match; ``hcount`` and ``lone_pairs`` use
480
+ host-greater-or-equal semantics, while the rest are exact.
481
+ :param edge_attrs: Keys of attributes to match; ``hcount`` and ``lone_pairs`` use
482
+ host-greater-or-equal semantics, while the rest are exact.
483
+ :param strategy: Matching strategy code or enum ("all", "comp", "bt").
484
+ :param max_results: Stop after this many embeddings (None = no limit).
485
+ :param strict_cc_count: If True, host CC count must ≤ pattern CC count for COMPONENT/BACKTRACK.
486
+ :param threshold: Embedding cap. Passing ``None`` disables the cap; omitting the
487
+ argument uses ``DEFAULT_THRESHOLD``.
488
+ :param pre_filter: If True, reject patterns having an empty candidate-node domain.
489
+
490
+ :return: * *List of dictionaries mapping pattern node→host node. Empty if none or*
491
+ * *if any guard (pre-filter or enumeration) exceeds the threshold.*
492
+ """
493
+ strat = Strategy.from_string(strategy)
494
+ if strat is Strategy.PARTIAL:
495
+ raise NotImplementedError("PARTIAL strategy not implemented yet.")
496
+ if host.is_directed() != pattern.is_directed():
497
+ return []
498
+ if host.is_multigraph() or pattern.is_multigraph():
499
+ raise NotImplementedError(
500
+ "SubgraphSearchEngine does not support multigraphs"
501
+ )
502
+ if max_results is not None and max_results < 0:
503
+ raise ValueError("max_results must be non-negative or None")
504
+ if threshold is not None and threshold < 0:
505
+ raise ValueError("threshold must be non-negative or None")
506
+ if max_results == 0:
507
+ return []
508
+
509
+ thresh = threshold
510
+
511
+ # All matching strategies below treat the host and pattern as
512
+ # read-only. Avoid copying both complete graphs for every search;
513
+ # component-specific working graphs are still materialized where a
514
+ # strategy needs them.
515
+
516
+ # The compiled matcher constructs the complete candidate-node domains
517
+ # itself. Running the Python pre-filter first would evaluate the same
518
+ # authoritative predicate twice. If compilation is unsupported or
519
+ # reaches its defensive cap, retain the established guarded fallback.
520
+ compiled_results = (
521
+ SubgraphSearchEngine._find_compiled_domain_mappings(
522
+ host,
523
+ pattern,
524
+ node_attrs,
525
+ edge_attrs,
526
+ )
527
+ if strat is Strategy.ALL and max_results is None and thresh is None
528
+ else None
529
+ )
530
+ if compiled_results is not None:
531
+ return compiled_results
532
+
533
+ # quick pre-filter
534
+ if pre_filter and SubgraphSearchEngine._quick_pre_filter(
535
+ host, pattern, node_attrs
536
+ ):
537
+ return []
538
+
539
+ # dispatch
540
+ if strat is Strategy.ALL:
541
+ components = list(
542
+ nx.weakly_connected_components(pattern)
543
+ if pattern.is_directed()
544
+ else nx.connected_components(pattern)
545
+ )
546
+ if len(components) > 1:
547
+ results = SubgraphSearchEngine._find_disconnected_all_mappings(
548
+ host,
549
+ pattern,
550
+ components,
551
+ node_attrs,
552
+ edge_attrs,
553
+ max_results,
554
+ thresh,
555
+ )
556
+ else:
557
+ results = SubgraphSearchEngine._find_all_subgraph_mappings(
558
+ host, pattern, node_attrs, edge_attrs, max_results, thresh
559
+ )
560
+ elif strat is Strategy.COMPONENT:
561
+ results = SubgraphSearchEngine._find_component_aware_subgraph_mappings(
562
+ host,
563
+ pattern,
564
+ node_attrs,
565
+ edge_attrs,
566
+ max_results,
567
+ strict_cc_count,
568
+ thresh,
569
+ )
570
+ else: # BACKTRACK
571
+ results = SubgraphSearchEngine._find_bt_subgraph_mappings(
572
+ host,
573
+ pattern,
574
+ node_attrs,
575
+ edge_attrs,
576
+ max_results,
577
+ strict_cc_count,
578
+ thresh,
579
+ )
580
+
581
+ # final threshold guard
582
+ return [] if thresh is not None and len(results) > thresh else results
583
+
584
+ @staticmethod
585
+ def _find_all_subgraph_mappings(
586
+ host: nx.Graph,
587
+ pattern: nx.Graph,
588
+ node_attrs: List[str],
589
+ edge_attrs: List[str],
590
+ max_results: Optional[int],
591
+ threshold: Optional[int],
592
+ ) -> List[MappingDict]:
593
+ """Classic VF2 over the whole host graph."""
594
+
595
+ def node_match(nh: EdgeAttr, np: EdgeAttr) -> bool:
596
+ return electron_aware_node_match(nh, np, node_attrs)
597
+
598
+ def edge_match(eh: EdgeAttr, ep: EdgeAttr) -> bool:
599
+ return electron_aware_edge_match(eh, ep, edge_attrs)
600
+
601
+ matcher_cls = (
602
+ nx.algorithms.isomorphism.DiGraphMatcher
603
+ if host.is_directed()
604
+ else GraphMatcher
605
+ )
606
+ gm = matcher_cls(host, pattern, node_match=node_match, edge_match=edge_match)
607
+ results: List[MappingDict] = []
608
+ for iso in gm.subgraph_monomorphisms_iter():
609
+ results.append({p: h for h, p in iso.items()})
610
+ if max_results is not None and len(results) >= max_results:
611
+ break
612
+ if threshold is not None and len(results) > threshold:
613
+ return []
614
+ return results
615
+
616
+ @staticmethod
617
+ def _find_disconnected_all_mappings(
618
+ host: nx.Graph,
619
+ pattern: nx.Graph,
620
+ components: List[Set[Any]],
621
+ node_attrs: List[str],
622
+ edge_attrs: List[str],
623
+ max_results: Optional[int],
624
+ threshold: Optional[int],
625
+ ) -> List[MappingDict]:
626
+ """Join exact component embeddings with global injectivity.
627
+
628
+ VF2's whole-pattern search repeatedly traverses unrelated host context
629
+ for disconnected queries. Component embeddings are independent except
630
+ for injectivity, so their disjoint Cartesian join is exactly the same
631
+ monomorphism set, including placements in one host component.
632
+ """
633
+ per_component = []
634
+ for nodes in components:
635
+ component = pattern.subgraph(nodes).copy()
636
+ mappings = SubgraphSearchEngine._find_all_subgraph_mappings(
637
+ host,
638
+ component,
639
+ node_attrs,
640
+ edge_attrs,
641
+ None,
642
+ None,
643
+ )
644
+ if not mappings:
645
+ return []
646
+ per_component.append((component.number_of_nodes(), mappings))
647
+ per_component.sort(key=lambda item: (len(item[1]), -item[0]))
648
+
649
+ results: List[MappingDict] = []
650
+
651
+ def join(level: int, combined: MappingDict, used: Set[Any]) -> None:
652
+ if max_results is not None and len(results) >= max_results:
653
+ return
654
+ if threshold is not None and len(results) > threshold:
655
+ return
656
+ if level == len(per_component):
657
+ results.append(combined.copy())
658
+ return
659
+ for mapping in per_component[level][1]:
660
+ image = set(mapping.values())
661
+ if image & used:
662
+ continue
663
+ combined.update(mapping)
664
+ join(level + 1, combined, used | image)
665
+ for node in mapping:
666
+ combined.pop(node)
667
+ if max_results is not None and len(results) >= max_results:
668
+ return
669
+ if threshold is not None and len(results) > threshold:
670
+ return
671
+
672
+ join(0, {}, set())
673
+ return results
674
+
675
+ @staticmethod
676
+ def _find_component_aware_subgraph_mappings(
677
+ host: nx.Graph,
678
+ pattern: nx.Graph,
679
+ node_attrs: List[str],
680
+ edge_attrs: List[str],
681
+ max_results: Optional[int],
682
+ strict_cc_count: bool,
683
+ threshold: Optional[int],
684
+ ) -> List[MappingDict]:
685
+ """Component-aware VF2 split by connected components."""
686
+ host_components = (
687
+ nx.weakly_connected_components(host)
688
+ if host.is_directed()
689
+ else nx.connected_components(host)
690
+ )
691
+ pattern_components = (
692
+ nx.weakly_connected_components(pattern)
693
+ if pattern.is_directed()
694
+ else nx.connected_components(pattern)
695
+ )
696
+ host_ccs = [host.subgraph(component).copy() for component in host_components]
697
+ pat_ccs = [
698
+ pattern.subgraph(component).copy() for component in pattern_components
699
+ ]
700
+ hcc, pcc = len(host_ccs), len(pat_ccs)
701
+ if pcc == 0:
702
+ return [{}]
703
+ if hcc < pcc:
704
+ return SubgraphSearchEngine._find_all_subgraph_mappings(
705
+ host, pattern, node_attrs, edge_attrs, max_results, threshold
706
+ )
707
+ if hcc > pcc and strict_cc_count:
708
+ return []
709
+
710
+ def node_match(nh: EdgeAttr, np: EdgeAttr) -> bool:
711
+ return electron_aware_node_match(nh, np, node_attrs)
712
+
713
+ def edge_match(eh: EdgeAttr, ep: EdgeAttr) -> bool:
714
+ return electron_aware_edge_match(eh, ep, edge_attrs)
715
+
716
+ per_cc: List[List[Tuple[int, MappingDict]]] = []
717
+ for pc in pat_ccs:
718
+ sz = pc.number_of_nodes()
719
+ cand = [i for i, hc in enumerate(host_ccs) if hc.number_of_nodes() >= sz]
720
+ if not cand:
721
+ return []
722
+ maps: List[Tuple[int, MappingDict]] = []
723
+ for i in cand:
724
+ matcher_cls = (
725
+ nx.algorithms.isomorphism.DiGraphMatcher
726
+ if host.is_directed()
727
+ else GraphMatcher
728
+ )
729
+ gm = matcher_cls(
730
+ host_ccs[i], pc, node_match=node_match, edge_match=edge_match
731
+ )
732
+ # ``max_results`` bounds complete joined embeddings, not the
733
+ # number of candidate host components considered here. Keep
734
+ # up to that many local embeddings *per component pair* so a
735
+ # later injective assignment can still choose another host CC.
736
+ host_maps = 0
737
+ for iso in gm.subgraph_monomorphisms_iter():
738
+ maps.append((i, {p: h for h, p in iso.items()}))
739
+ host_maps += 1
740
+ if max_results is not None and host_maps >= max_results:
741
+ break
742
+ if threshold is not None and len(maps) > threshold:
743
+ return []
744
+ if not maps:
745
+ return []
746
+ per_cc.append(maps)
747
+
748
+ order = sorted(range(pcc), key=lambda i: len(per_cc[i]))
749
+ ordered = [per_cc[i] for i in order]
750
+ results: List[MappingDict] = []
751
+ used: Set[int] = set()
752
+
753
+ def backtrack(level: int, acc: MappingDict):
754
+ if max_results is not None and len(results) >= max_results:
755
+ return
756
+ if threshold is not None and len(results) > threshold:
757
+ return
758
+ if level == pcc:
759
+ results.append(acc.copy())
760
+ return
761
+ for hi, m in ordered[level]:
762
+ if hi in used or any(p in acc for p in m):
763
+ continue
764
+ used.add(hi)
765
+ acc.update(m)
766
+ backtrack(level + 1, acc)
767
+ for p in m:
768
+ acc.pop(p)
769
+ used.remove(hi)
770
+ if max_results is not None and len(results) >= max_results:
771
+ return
772
+ if threshold is not None and len(results) > threshold:
773
+ return
774
+
775
+ backtrack(0, {})
776
+ return results
777
+
778
+ @staticmethod
779
+ def _find_bt_subgraph_mappings(
780
+ host: nx.Graph,
781
+ pattern: nx.Graph,
782
+ node_attrs: List[str],
783
+ edge_attrs: List[str],
784
+ max_results: Optional[int],
785
+ strict_cc_count: bool,
786
+ threshold: Optional[int],
787
+ ) -> List[MappingDict]:
788
+ primary = SubgraphSearchEngine._find_component_aware_subgraph_mappings(
789
+ host,
790
+ pattern,
791
+ node_attrs,
792
+ edge_attrs,
793
+ max_results,
794
+ strict_cc_count,
795
+ threshold,
796
+ )
797
+ if primary:
798
+ return primary
799
+ return SubgraphSearchEngine._find_all_subgraph_mappings(
800
+ host, pattern, node_attrs, edge_attrs, max_results, threshold
801
+ )
802
+
803
+ def __repr__(self) -> str:
804
+ return "<SubgraphSearchEngine – use `find_subgraph_mappings`>"
805
+
806
+ __str__ = __repr__
807
+
808
+ @property
809
+ def help(self) -> str: # noqa: D401 – property for convenience
810
+ """Return the full module docstring."""
811
+
812
+ return __doc__