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,893 @@
1
+ """Structural ITS signatures, certificates, and clustering."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import Counter, defaultdict
6
+ from typing import Any, Dict, List, Tuple
7
+
8
+ import networkx as nx
9
+
10
+ from synkit.Graph.Matcher.graph_cluster import GraphCluster
11
+ from ..core import product as _product_state
12
+ from .deduplication import (
13
+ _PRIMITIVE_IDENTITY_TYPES,
14
+ _freeze_typed_identity,
15
+ _merge_application_orbits,
16
+ )
17
+
18
+ ITS_STRUCTURAL_NODE_ATTRS = [
19
+ "element",
20
+ "aromatic",
21
+ "hcount",
22
+ "charge",
23
+ "radical",
24
+ "lone_pairs",
25
+ "valence_electrons",
26
+ "present",
27
+ "_legacy_typesgh_sig",
28
+ ]
29
+ ITS_STRUCTURAL_EDGE_ATTRS = ["order", "kekule_order", "sigma_order", "pi_order"]
30
+ _EXACT_NODE_SIG = "_structural_exact_node_sig"
31
+ _REFINED_NODE_COLOUR = "_structural_refined_node_colour"
32
+ _EXACT_EDGE_SIG = "_structural_exact_edge_sig"
33
+ _REFINED_EDGE_COLOUR = "_structural_refined_edge_colour"
34
+ _EXACT_NODE_PALETTE = "_structural_exact_node_palette"
35
+ _EXACT_EDGE_PALETTE = "_structural_exact_edge_palette"
36
+ _EXACT_IDENTITY_CACHE = "_structural_exact_identity_cache"
37
+ _EXACT_DIRTY_NODES = "_structural_exact_dirty_nodes"
38
+ _EXACT_DIRTY_EDGES = "_structural_exact_dirty_edges"
39
+
40
+
41
+ def _attach_exact_structural_signatures( # noqa: C901
42
+ graph: nx.Graph,
43
+ identity_cache: Dict[int, Tuple[Any, Any]] | None = None,
44
+ ) -> None:
45
+ """Attach the normalized exact labels used by structural isomorphism."""
46
+ node_palette = graph.graph.get(_EXACT_NODE_PALETTE)
47
+ edge_palette = graph.graph.get(_EXACT_EDGE_PALETTE)
48
+ if identity_cache is None:
49
+ identity_cache = graph.graph.get(_EXACT_IDENTITY_CACHE)
50
+
51
+ dirty_nodes = graph.graph.get(_EXACT_DIRTY_NODES)
52
+ dirty_edges = graph.graph.get(_EXACT_DIRTY_EDGES)
53
+ incrementally_seeded = (
54
+ graph.graph.get("_structural_signatures_seeded", False)
55
+ and isinstance(dirty_nodes, set)
56
+ and isinstance(dirty_edges, set)
57
+ )
58
+ if incrementally_seeded:
59
+ # The prepared host already carries exact labels. Every subsequent
60
+ # mutation records its support, so evaluating the same label function
61
+ # on that support is equivalent to rescanning the complete graph.
62
+ node_records = (
63
+ (node, graph.nodes[node]) for node in dirty_nodes if node in graph
64
+ )
65
+ edge_records = (
66
+ (left, right, graph.edges[left, right])
67
+ for edge in dirty_edges
68
+ for left, right in (edge,)
69
+ if graph.has_edge(left, right)
70
+ )
71
+ adjacency = graph._adj
72
+ aromatic_nodes = {
73
+ node
74
+ for node in dirty_nodes
75
+ if node in adjacency
76
+ and any(
77
+ attrs.get("order") == (1.5, 1.5) for attrs in adjacency[node].values()
78
+ )
79
+ }
80
+ else:
81
+ node_records = graph.nodes(data=True)
82
+ edge_records = graph.edges(data=True)
83
+ aromatic_nodes = {
84
+ node
85
+ for u, v, attrs in graph.edges(data=True)
86
+ if attrs.get("order") == (1.5, 1.5)
87
+ for node in (u, v)
88
+ }
89
+ electron_aware = bool(graph.graph.get("electron_aware_rewrite", False))
90
+ for node, attrs in node_records:
91
+ if _EXACT_NODE_SIG in attrs:
92
+ continue
93
+ values = []
94
+ for name in ITS_STRUCTURAL_NODE_ATTRS:
95
+ if name == "_legacy_typesgh_sig":
96
+ value = () if electron_aware else attrs.get("typesGH", ())
97
+ elif name == "charge" and node in aromatic_nodes:
98
+ template_charge = attrs.get("template_charge")
99
+ value = (
100
+ template_charge
101
+ if isinstance(template_charge, tuple) and len(template_charge) == 2
102
+ else attrs.get(name)
103
+ )
104
+ else:
105
+ value = attrs.get(name)
106
+ value_type = type(value)
107
+ values.append(
108
+ (value_type, value)
109
+ if value_type in _PRIMITIVE_IDENTITY_TYPES
110
+ else _freeze_typed_identity(value, identity_cache)
111
+ )
112
+ signature = tuple(values)
113
+ if node_palette is None:
114
+ attrs[_EXACT_NODE_SIG] = signature
115
+ else:
116
+ colour = node_palette.get(signature)
117
+ if colour is None:
118
+ colour = len(node_palette) + 1
119
+ node_palette[signature] = colour
120
+ attrs[_EXACT_NODE_SIG] = colour
121
+ for _, _, attrs in edge_records:
122
+ if _EXACT_EDGE_SIG in attrs:
123
+ continue
124
+ edge_values = []
125
+ aromatic_unchanged = attrs.get("order") == (1.5, 1.5)
126
+ for name in ITS_STRUCTURAL_EDGE_ATTRS:
127
+ value = attrs.get(name)
128
+ if aromatic_unchanged and name in {
129
+ "kekule_order",
130
+ "sigma_order",
131
+ "pi_order",
132
+ }:
133
+ value = "aromatic_phase"
134
+ value_type = type(value)
135
+ edge_values.append(
136
+ (value_type, value)
137
+ if value_type in _PRIMITIVE_IDENTITY_TYPES
138
+ else _freeze_typed_identity(value, identity_cache)
139
+ )
140
+ signature = tuple(edge_values)
141
+ if edge_palette is None:
142
+ attrs[_EXACT_EDGE_SIG] = signature
143
+ else:
144
+ colour = edge_palette.get(signature)
145
+ if colour is None:
146
+ colour = len(edge_palette) + 1
147
+ edge_palette[signature] = colour
148
+ attrs[_EXACT_EDGE_SIG] = colour
149
+ if incrementally_seeded:
150
+ dirty_nodes.clear()
151
+ dirty_edges.clear()
152
+
153
+
154
+ def _prepare_its_for_structural_cluster(
155
+ its: nx.Graph,
156
+ *,
157
+ refresh_electrons: bool = True,
158
+ hash_iterations: int = 5,
159
+ ) -> nx.Graph:
160
+ """Attach invariant signatures that accelerate exact ITS clustering."""
161
+ prepared = its.copy()
162
+ if (
163
+ refresh_electrons
164
+ and prepared.graph.get("electron_aware_rewrite", False)
165
+ and not prepared.graph.get("_product_electron_fields_current", False)
166
+ ):
167
+ _product_state._refresh_product_electron_fields(prepared)
168
+ _attach_exact_structural_signatures(prepared)
169
+ # Retain the established diagnostic labels on copied prepared graphs.
170
+ for _, attrs in prepared.nodes(data=True):
171
+ attrs["_its_node_sig"] = attrs[_EXACT_NODE_SIG]
172
+ for _, _, attrs in prepared.edges(data=True):
173
+ attrs["_its_edge_sig"] = attrs[_EXACT_EDGE_SIG]
174
+ node_hashes = nx.weisfeiler_lehman_subgraph_hashes(
175
+ prepared,
176
+ node_attr=_EXACT_NODE_SIG,
177
+ edge_attr=_EXACT_EDGE_SIG,
178
+ iterations=hash_iterations,
179
+ digest_size=16,
180
+ )
181
+ for node, hashes in node_hashes.items():
182
+ prepared.nodes[node][_REFINED_NODE_COLOUR] = hashes[-1] if hashes else ""
183
+ return prepared
184
+
185
+
186
+ def _refine_structural_colours_in_place( # noqa: C901
187
+ graphs: List[nx.Graph],
188
+ *,
189
+ iterations: int,
190
+ node_signature_attr: str = _EXACT_NODE_SIG,
191
+ edge_signature_attr: str = _EXACT_EDGE_SIG,
192
+ node_colour_attr: str = _REFINED_NODE_COLOUR,
193
+ edge_colour_attr: str = _REFINED_EDGE_COLOUR,
194
+ ) -> None:
195
+ """Run joint exact 1-WL refinement using interned integer colours.
196
+
197
+ Integer interning avoids cryptographic hashing. The refinement is used
198
+ only as a necessary isomorphism invariant; exact component comparison is
199
+ still the sole authority for merging a collision bucket.
200
+ """
201
+
202
+ def assign(signatures: List[Tuple[nx.Graph, Any, Any]]) -> int:
203
+ palette: Dict[Any, int] = {}
204
+ for graph, node, signature in signatures:
205
+ colour = palette.get(signature)
206
+ if colour is None:
207
+ colour = len(palette) + 1
208
+ palette[signature] = colour
209
+ graph.nodes[node][node_colour_attr] = colour
210
+ return len(palette)
211
+
212
+ labels_are_interned_integers = all(
213
+ type(attrs[node_signature_attr]) is int
214
+ for graph in graphs
215
+ for _, attrs in graph.nodes(data=True)
216
+ ) and all(
217
+ type(attrs[edge_signature_attr]) is int
218
+ for graph in graphs
219
+ for _, _, attrs in graph.edges(data=True)
220
+ )
221
+ if labels_are_interned_integers:
222
+ node_colours = set()
223
+ for graph in graphs:
224
+ for _, attrs in graph.nodes(data=True):
225
+ colour = attrs[node_signature_attr]
226
+ attrs[node_colour_attr] = colour
227
+ node_colours.add(colour)
228
+ for _, _, attrs in graph.edges(data=True):
229
+ attrs[edge_colour_attr] = attrs[edge_signature_attr]
230
+ colour_count = len(node_colours)
231
+ else:
232
+ edge_palette: Dict[Any, int] = {}
233
+ for graph in graphs:
234
+ for _, _, attrs in graph.edges(data=True):
235
+ signature = attrs[edge_signature_attr]
236
+ colour = edge_palette.get(signature)
237
+ if colour is None:
238
+ colour = len(edge_palette) + 1
239
+ edge_palette[signature] = colour
240
+ attrs[edge_colour_attr] = colour
241
+
242
+ colour_count = assign(
243
+ [
244
+ (graph, node, attrs[node_signature_attr])
245
+ for graph in graphs
246
+ for node, attrs in graph.nodes(data=True)
247
+ ]
248
+ )
249
+ for _ in range(iterations):
250
+ signatures = []
251
+ for graph in graphs:
252
+ for node in graph:
253
+ neighbourhood = tuple(
254
+ sorted(
255
+ (
256
+ graph.edges[node, neighbor][edge_colour_attr],
257
+ graph.nodes[neighbor][node_colour_attr],
258
+ )
259
+ for neighbor in graph.neighbors(node)
260
+ )
261
+ )
262
+ signatures.append(
263
+ (
264
+ graph,
265
+ node,
266
+ (
267
+ graph.nodes[node][node_colour_attr],
268
+ neighbourhood,
269
+ ),
270
+ )
271
+ )
272
+ refined_count = assign(signatures)
273
+ if refined_count == colour_count:
274
+ break
275
+ colour_count = refined_count
276
+
277
+
278
+ def _remove_structural_signatures(graphs: List[nx.Graph]) -> None:
279
+ """Remove temporary exact/refinement labels from returned ITS graphs."""
280
+ for graph in graphs:
281
+ graph.graph.pop(_EXACT_NODE_PALETTE, None)
282
+ graph.graph.pop(_EXACT_EDGE_PALETTE, None)
283
+ graph.graph.pop(_EXACT_IDENTITY_CACHE, None)
284
+ graph.graph.pop(_EXACT_DIRTY_NODES, None)
285
+ graph.graph.pop(_EXACT_DIRTY_EDGES, None)
286
+ graph.graph.pop("_structural_signatures_seeded", None)
287
+ for _, attrs in graph.nodes(data=True):
288
+ attrs.pop(_EXACT_NODE_SIG, None)
289
+ attrs.pop(_REFINED_NODE_COLOUR, None)
290
+ for _, _, attrs in graph.edges(data=True):
291
+ attrs.pop(_EXACT_EDGE_SIG, None)
292
+ attrs.pop(_REFINED_EDGE_COLOUR, None)
293
+
294
+
295
+ def _exact_inventory_key(graph: nx.Graph) -> Any:
296
+ """Return a cheap necessary invariant for attributed isomorphism.
297
+
298
+ Equal attributed graphs necessarily have equal multisets of exact node
299
+ and edge labels. This key is therefore allowed to reject a comparison,
300
+ but never to authorize a merge.
301
+ """
302
+ return (
303
+ graph.is_directed(),
304
+ graph.is_multigraph(),
305
+ graph.number_of_nodes(),
306
+ graph.number_of_edges(),
307
+ frozenset(
308
+ Counter(
309
+ attrs[_EXACT_NODE_SIG] for _, attrs in graph.nodes(data=True)
310
+ ).items()
311
+ ),
312
+ frozenset(
313
+ Counter(
314
+ attrs[_EXACT_EDGE_SIG] for _, _, attrs in graph.edges(data=True)
315
+ ).items()
316
+ ),
317
+ )
318
+
319
+
320
+ def _active_neighbourhood_invariant(
321
+ graph: nx.Graph,
322
+ *,
323
+ radius: int = 2,
324
+ ) -> Any:
325
+ """Return an exact local invariant rooted at endpoint-paired vertices.
326
+
327
+ Electron-aware rewrites store ``present`` as an endpoint pair precisely on
328
+ their mapped vertices. Container type and value are part of
329
+ ``_EXACT_NODE_SIG``, so every attributed isomorphism preserves this root
330
+ set. It consequently preserves graph distance from that set and every
331
+ labelled neighbourhood recorded below. Unequal results prove
332
+ non-isomorphism; equality is never used to merge.
333
+
334
+ This sparse refinement visits only the reaction locus and its immediate
335
+ context. Graphs without such intrinsic roots fall back to one full exact
336
+ 1-WL round in the caller.
337
+ """
338
+ nodes = graph._node
339
+ adjacency = graph._adj
340
+ roots = {
341
+ node
342
+ for node, attrs in nodes.items()
343
+ if isinstance(attrs.get("present"), tuple) and len(attrs["present"]) == 2
344
+ }
345
+ if not roots:
346
+ return None
347
+
348
+ records = []
349
+ frontier = roots
350
+ seen = set(roots)
351
+ for depth in range(radius):
352
+ next_frontier = set()
353
+ for node in frontier:
354
+ node_signature = nodes[node][_EXACT_NODE_SIG]
355
+ neighbourhood = []
356
+ for neighbour, edge_attrs in adjacency[node].items():
357
+ next_frontier.add(neighbour)
358
+ neighbourhood.append(
359
+ (
360
+ edge_attrs[_EXACT_EDGE_SIG],
361
+ nodes[neighbour][_EXACT_NODE_SIG],
362
+ )
363
+ )
364
+ records.append(
365
+ (
366
+ depth,
367
+ node_signature,
368
+ tuple(sorted(neighbourhood)),
369
+ )
370
+ )
371
+ frontier = next_frontier - seen
372
+ seen.update(next_frontier)
373
+ if not frontier:
374
+ break
375
+ return len(roots), tuple(sorted(records))
376
+
377
+
378
+ def _canonical_attributed_graph_certificate(
379
+ graph: nx.Graph,
380
+ *,
381
+ node_attribute: str,
382
+ edge_attribute: str,
383
+ node_palette: Dict[Any, int],
384
+ edge_palette: Dict[Any, int],
385
+ topology_cache: Dict[Any, Any] | None = None,
386
+ ) -> str | None:
387
+ """Return an injective canonical encoding of a simple attributed graph.
388
+
389
+ Original vertices become isotope-labelled dummy atoms. Every labelled
390
+ edge is subdivided by an isotope-labelled helium atom. The two atom types
391
+ make this transformation injective: decoding contracts every helium atom
392
+ and recovers the original vertex- and edge-labelled graph. Consequently,
393
+ equal SMILES certificates are sufficient proof of attributed isomorphism,
394
+ rather than a probabilistic hash match.
395
+
396
+ RDKit supplies compiled canonical traversal. Unsupported graph kinds or
397
+ an exhausted isotope label space return ``None`` and retain the exact
398
+ NetworkX tree/VF2 path.
399
+ """
400
+ if graph.is_directed() or graph.is_multigraph() or nx.number_of_selfloops(graph):
401
+ return None
402
+
403
+ from rdkit import Chem
404
+
405
+ node_order = tuple(graph)
406
+ edge_order = tuple(graph.edges())
407
+ topology_key = (node_order, edge_order)
408
+ skeleton = topology_cache.get(topology_key) if topology_cache is not None else None
409
+ if skeleton is None:
410
+ builder = Chem.RWMol()
411
+ node_index = {node: builder.AddAtom(Chem.Atom(0)) for node in node_order}
412
+ for left, right in edge_order:
413
+ edge_node = builder.AddAtom(Chem.Atom(2))
414
+ builder.AddBond(node_index[left], edge_node, Chem.BondType.SINGLE)
415
+ builder.AddBond(edge_node, node_index[right], Chem.BondType.SINGLE)
416
+ skeleton = builder.GetMol()
417
+ if topology_cache is not None:
418
+ topology_cache[topology_key] = skeleton
419
+
420
+ mol = Chem.Mol(skeleton)
421
+ for atom_index, node in enumerate(node_order):
422
+ attrs = graph.nodes[node]
423
+ label = attrs[node_attribute]
424
+ colour = node_palette.get(label)
425
+ if colour is None:
426
+ colour = len(node_palette) + 1
427
+ if colour > 65535:
428
+ return None
429
+ node_palette[label] = colour
430
+ mol.GetAtomWithIdx(atom_index).SetIsotope(colour)
431
+
432
+ edge_offset = len(node_order)
433
+ for edge_index, (left, right) in enumerate(edge_order):
434
+ attrs = graph.edges[left, right]
435
+ label = attrs[edge_attribute]
436
+ colour = edge_palette.get(label)
437
+ if colour is None:
438
+ colour = len(edge_palette) + 1
439
+ if colour > 65535:
440
+ return None
441
+ edge_palette[label] = colour
442
+ mol.GetAtomWithIdx(edge_offset + edge_index).SetIsotope(colour)
443
+
444
+ try:
445
+ return Chem.MolToSmiles(
446
+ mol,
447
+ canonical=True,
448
+ allBondsExplicit=True,
449
+ allHsExplicit=True,
450
+ )
451
+ except Exception:
452
+ return None
453
+
454
+
455
+ def _order_prepared_its_for_vf2(prepared: nx.Graph) -> nx.Graph:
456
+ """Put rare refined environments first before an exact VF2 comparison."""
457
+ # VF2 selects pattern nodes in insertion order. Put rare refined
458
+ # environments first so a small reaction locus anchors the match
459
+ # before traversal enters a large symmetric scaffold.
460
+ frequencies = Counter(
461
+ attrs[_REFINED_NODE_COLOUR] for _, attrs in prepared.nodes(data=True)
462
+ )
463
+ node_order = sorted(
464
+ prepared.nodes,
465
+ key=lambda node: (
466
+ frequencies[prepared.nodes[node][_REFINED_NODE_COLOUR]],
467
+ prepared.nodes[node][_REFINED_NODE_COLOUR],
468
+ repr(node),
469
+ ),
470
+ )
471
+ ordered = prepared.__class__()
472
+ ordered.graph.update(prepared.graph)
473
+ ordered.add_nodes_from((node, prepared.nodes[node].copy()) for node in node_order)
474
+ if prepared.is_multigraph():
475
+ ordered.add_edges_from(
476
+ (u, v, key, attrs.copy())
477
+ for u, v, key, attrs in prepared.edges(keys=True, data=True)
478
+ )
479
+ else:
480
+ ordered.add_edges_from(
481
+ (u, v, attrs.copy()) for u, v, attrs in prepared.edges(data=True)
482
+ )
483
+ return ordered
484
+
485
+
486
+ def _attributed_tree_code(component: nx.Graph) -> Any | None:
487
+ """Return a complete canonical code for an undirected attributed tree."""
488
+ if (
489
+ component.is_directed()
490
+ or component.is_multigraph()
491
+ or component.number_of_edges() + 1 != len(component)
492
+ ):
493
+ return None
494
+ neighbours = {node: set(component.neighbors(node)) for node in component}
495
+ remaining = set(component)
496
+ leaves = {node for node in remaining if len(neighbours[node]) <= 1}
497
+ while len(remaining) > 2:
498
+ remaining.difference_update(leaves)
499
+ next_leaves = set()
500
+ for leaf in leaves:
501
+ for neighbor in neighbours[leaf]:
502
+ neighbours[neighbor].discard(leaf)
503
+ if neighbor in remaining and len(neighbours[neighbor]) <= 1:
504
+ next_leaves.add(neighbor)
505
+ leaves = next_leaves
506
+
507
+ def rooted_code(node: Any, parent: Any | None) -> Any:
508
+ children = [
509
+ (
510
+ component.edges[node, child][_EXACT_EDGE_SIG],
511
+ rooted_code(child, node),
512
+ )
513
+ for child in component.neighbors(node)
514
+ if child != parent
515
+ ]
516
+ node_role = (
517
+ component.nodes[node][_EXACT_NODE_SIG],
518
+ component.nodes[node][_REFINED_NODE_COLOUR],
519
+ )
520
+ return node_role, tuple(sorted(children, key=repr))
521
+
522
+ return min((rooted_code(center, None) for center in remaining), key=repr)
523
+
524
+
525
+ def _component_inventory( # noqa: C901
526
+ graphs: List[nx.Graph],
527
+ cluster: GraphCluster,
528
+ ) -> List[Tuple[int, ...]]:
529
+ """Classify disconnected non-stereo ITS components exactly."""
530
+ tree_classes: Dict[Any, int] = {}
531
+ representatives: Dict[Any, List[List[Any]]] = defaultdict(list)
532
+ multigraph_representatives: Dict[Any, List[Tuple[int, nx.Graph]]] = defaultdict(
533
+ list
534
+ )
535
+ literal_classes: Dict[Any, int] = {}
536
+ inventories: List[Tuple[int, ...]] = []
537
+ next_class = 0
538
+ for graph in graphs:
539
+ components = (
540
+ nx.weakly_connected_components(graph)
541
+ if graph.is_directed()
542
+ else nx.connected_components(graph)
543
+ )
544
+ classes = []
545
+ for nodes in components:
546
+ component = graph.subgraph(nodes)
547
+ node_records = tuple(
548
+ sorted(
549
+ [
550
+ (
551
+ node,
552
+ (
553
+ attrs[_EXACT_NODE_SIG],
554
+ attrs[_REFINED_NODE_COLOUR],
555
+ ),
556
+ )
557
+ for node, attrs in component.nodes(data=True)
558
+ ],
559
+ key=lambda record: repr(record[0]),
560
+ )
561
+ )
562
+ edge_records = tuple(
563
+ sorted(
564
+ [
565
+ (
566
+ tuple(sorted((left, right), key=repr)),
567
+ attrs[_EXACT_EDGE_SIG],
568
+ )
569
+ for left, right, attrs in component.edges(data=True)
570
+ ],
571
+ key=repr,
572
+ )
573
+ )
574
+ literal_signature = (node_records, edge_records)
575
+ component_class = literal_classes.get(literal_signature)
576
+ if component_class is not None:
577
+ classes.append(component_class)
578
+ continue
579
+ tree_key = _attributed_tree_code(component)
580
+ if tree_key is not None:
581
+ component_class = tree_classes.get(tree_key)
582
+ if component_class is None:
583
+ component_class = next_class
584
+ next_class += 1
585
+ tree_classes[tree_key] = component_class
586
+ elif component.is_multigraph():
587
+ fingerprint = (
588
+ component.number_of_nodes(),
589
+ component.number_of_edges(),
590
+ tuple(
591
+ sorted(
592
+ attrs[_REFINED_NODE_COLOUR]
593
+ for _, attrs in component.nodes(data=True)
594
+ )
595
+ ),
596
+ )
597
+ for class_id, representative in multigraph_representatives[fingerprint]:
598
+ if nx.is_isomorphic(
599
+ component,
600
+ representative,
601
+ node_match=cluster.nodeMatch,
602
+ edge_match=cluster.edgeMatch,
603
+ ):
604
+ component_class = class_id
605
+ break
606
+ if component_class is None:
607
+ component_class = next_class
608
+ next_class += 1
609
+ multigraph_representatives[fingerprint].append(
610
+ (component_class, component)
611
+ )
612
+ else:
613
+ fingerprint = (
614
+ component.number_of_nodes(),
615
+ component.number_of_edges(),
616
+ tuple(
617
+ sorted(
618
+ attrs[_REFINED_NODE_COLOUR]
619
+ for _, attrs in component.nodes(data=True)
620
+ )
621
+ ),
622
+ tuple(
623
+ sorted(
624
+ (
625
+ attrs[_EXACT_EDGE_SIG]
626
+ for _, _, attrs in component.edges(data=True)
627
+ ),
628
+ key=repr,
629
+ )
630
+ ),
631
+ )
632
+ bucket = representatives[fingerprint]
633
+ if not bucket:
634
+ component_class = next_class
635
+ next_class += 1
636
+ bucket.append([component_class, component])
637
+ else:
638
+ for record in bucket:
639
+ if nx.is_isomorphic(
640
+ component,
641
+ record[1],
642
+ node_match=cluster.nodeMatch,
643
+ edge_match=cluster.edgeMatch,
644
+ ):
645
+ component_class = record[0]
646
+ break
647
+ if component_class is None:
648
+ component_class = next_class
649
+ next_class += 1
650
+ bucket.append([component_class, component])
651
+ literal_classes[literal_signature] = component_class
652
+ classes.append(component_class)
653
+ inventories.append(tuple(sorted(classes)))
654
+ return inventories
655
+
656
+
657
+ def _cluster_prepared_structural_its(
658
+ its_graphs: List[nx.Graph],
659
+ prepared_records: List[Tuple[int, nx.Graph]],
660
+ ) -> List[nx.Graph]:
661
+ """Classify prepared ITS graphs, with exact tests deciding every merge."""
662
+ from synkit.Graph.Stereo import stereo_identity_signature
663
+
664
+ buckets: Dict[Any, List[Tuple[int, nx.Graph]]] = defaultdict(list)
665
+ for index, prepared in prepared_records:
666
+ signature = (
667
+ prepared.is_directed(),
668
+ prepared.is_multigraph(),
669
+ prepared.number_of_nodes(),
670
+ prepared.number_of_edges(),
671
+ tuple(
672
+ sorted(
673
+ attrs[_REFINED_NODE_COLOUR]
674
+ for _, attrs in prepared.nodes(data=True)
675
+ )
676
+ ),
677
+ tuple(
678
+ sorted(
679
+ (
680
+ attrs[_EXACT_EDGE_SIG]
681
+ for _, _, attrs in prepared.edges(data=True)
682
+ ),
683
+ key=repr,
684
+ )
685
+ ),
686
+ )
687
+ stereo_signature = stereo_identity_signature(prepared)
688
+ buckets[(signature, stereo_signature)].append((index, prepared))
689
+
690
+ cluster = GraphCluster(
691
+ node_label_names=[_EXACT_NODE_SIG, _REFINED_NODE_COLOUR],
692
+ node_label_default=[(), ""],
693
+ edge_attribute=_EXACT_EDGE_SIG,
694
+ )
695
+
696
+ representative_indices: List[int] = []
697
+ for (_, stereo_signature), bucket in buckets.items():
698
+ if len(bucket) == 1:
699
+ representative_indices.append(bucket[0][0])
700
+ continue
701
+ prepared = [_order_prepared_its_for_vf2(prepared) for _, prepared in bucket]
702
+ if stereo_signature is None:
703
+ by_inventory: Dict[Tuple[int, ...], set[int]] = defaultdict(set)
704
+ for index, inventory in enumerate(_component_inventory(prepared, cluster)):
705
+ by_inventory[inventory].add(index)
706
+ classes = list(by_inventory.values())
707
+ else:
708
+ classes, _ = cluster.iterative_cluster(
709
+ prepared,
710
+ nodeMatch=cluster.nodeMatch,
711
+ edgeMatch=cluster.edgeMatch,
712
+ )
713
+ for cls in classes:
714
+ member_indices = [bucket[index][0] for index in sorted(cls)]
715
+ representative_index = member_indices[0]
716
+ representative_indices.append(representative_index)
717
+ _merge_application_orbits(
718
+ its_graphs[representative_index],
719
+ [its_graphs[index] for index in member_indices],
720
+ )
721
+
722
+ representative_indices.sort()
723
+ return [its_graphs[index] for index in representative_indices]
724
+
725
+
726
+ def _cluster_structural_its( # noqa: C901
727
+ its_graphs: List[nx.Graph],
728
+ *,
729
+ refresh_electrons: bool,
730
+ hash_iterations: int = 5,
731
+ ) -> List[nx.Graph]:
732
+ """Run one exact structural/stereo clustering pass.
733
+
734
+ Finalized simple graphs first use an exact radius-two invariant around the
735
+ intrinsically endpoint-paired vertices to prove that unequal buckets
736
+ cannot be isomorphic. Graphs without such roots retain one exact joint
737
+ 1-WL round. Singleton buckets need no canonical work. Collisions are merged
738
+ only by the injective attributed-graph certificate, with complete tree/VF2
739
+ comparison as the unsupported-graph fallback.
740
+ """
741
+ if len(its_graphs) < 2:
742
+ if any(
743
+ _EXACT_NODE_SIG in attrs
744
+ for graph in its_graphs
745
+ for _, attrs in graph.nodes(data=True)
746
+ ):
747
+ _remove_structural_signatures(its_graphs)
748
+ return its_graphs
749
+
750
+ use_integer_refinement = not refresh_electrons and all(
751
+ not graph.is_directed() and not graph.is_multigraph() for graph in its_graphs
752
+ )
753
+ if not use_integer_refinement:
754
+ prepared_records = [
755
+ (
756
+ index,
757
+ _prepare_its_for_structural_cluster(
758
+ its,
759
+ refresh_electrons=refresh_electrons,
760
+ hash_iterations=hash_iterations,
761
+ ),
762
+ )
763
+ for index, its in enumerate(its_graphs)
764
+ ]
765
+ return _cluster_prepared_structural_its(its_graphs, prepared_records)
766
+
767
+ seeded_node_palette = its_graphs[0].graph.get(_EXACT_NODE_PALETTE)
768
+ seeded_edge_palette = its_graphs[0].graph.get(_EXACT_EDGE_PALETTE)
769
+ seeded_identity_cache = its_graphs[0].graph.get(_EXACT_IDENTITY_CACHE)
770
+ palettes_are_shared = (
771
+ isinstance(seeded_node_palette, dict)
772
+ and isinstance(seeded_edge_palette, dict)
773
+ and isinstance(seeded_identity_cache, dict)
774
+ and all(
775
+ graph.graph.get(_EXACT_NODE_PALETTE) is seeded_node_palette
776
+ and graph.graph.get(_EXACT_EDGE_PALETTE) is seeded_edge_palette
777
+ and graph.graph.get(_EXACT_IDENTITY_CACHE) is seeded_identity_cache
778
+ for graph in its_graphs
779
+ )
780
+ )
781
+ if palettes_are_shared:
782
+ node_label_palette = seeded_node_palette
783
+ edge_label_palette = seeded_edge_palette
784
+ identity_cache = seeded_identity_cache
785
+ else:
786
+ node_label_palette: Dict[Any, int] = {}
787
+ edge_label_palette: Dict[Any, int] = {}
788
+ identity_cache: Dict[int, Tuple[Any, Any]] = {}
789
+ # Existing labels without the same palette are not mutually
790
+ # comparable. Recompute them once in the new quotient-local palette.
791
+ _remove_structural_signatures(its_graphs)
792
+ for graph in its_graphs:
793
+ graph.graph[_EXACT_NODE_PALETTE] = node_label_palette
794
+ graph.graph[_EXACT_EDGE_PALETTE] = edge_label_palette
795
+ graph.graph[_EXACT_IDENTITY_CACHE] = identity_cache
796
+ for graph in its_graphs:
797
+ _attach_exact_structural_signatures(graph, identity_cache)
798
+ try:
799
+ from synkit.Graph.Stereo import stereo_identity_signature
800
+
801
+ # The endpoint-paired rewrite locus is intrinsic to the exact node
802
+ # labels. Its radius-two attributed neighbourhood separates most
803
+ # placements without traversing the complete molecular graph. If any
804
+ # candidate lacks that intrinsic root, retain the full exact 1-WL
805
+ # invariant. Neither path is allowed to authorize a merge.
806
+ local_invariants = [
807
+ _active_neighbourhood_invariant(graph) for graph in its_graphs
808
+ ]
809
+ use_local_invariant = all(
810
+ invariant is not None for invariant in local_invariants
811
+ )
812
+ if not use_local_invariant:
813
+ _refine_structural_colours_in_place(its_graphs, iterations=1)
814
+ coarse_buckets: Dict[Any, List[Tuple[int, nx.Graph]]] = defaultdict(list)
815
+ for index, (graph, local_invariant) in enumerate(
816
+ zip(its_graphs, local_invariants)
817
+ ):
818
+ structural_invariant = (
819
+ local_invariant
820
+ if use_local_invariant
821
+ else (
822
+ graph.number_of_edges(),
823
+ tuple(
824
+ sorted(
825
+ attrs[_REFINED_NODE_COLOUR]
826
+ for _, attrs in graph.nodes(data=True)
827
+ )
828
+ ),
829
+ )
830
+ )
831
+ coarse_buckets[
832
+ (
833
+ structural_invariant,
834
+ stereo_identity_signature(graph),
835
+ )
836
+ ].append((index, graph))
837
+
838
+ certificate_node_palette: Dict[Any, int] = {}
839
+ certificate_edge_palette: Dict[Any, int] = {}
840
+ certificate_topology_cache: Dict[Any, Any] = {}
841
+ representative_indices: List[int] = []
842
+ fallback_records: List[Tuple[int, nx.Graph]] = []
843
+ for bucket in coarse_buckets.values():
844
+ if len(bucket) == 1:
845
+ representative_indices.append(bucket[0][0])
846
+ continue
847
+ certificate_buckets: Dict[str, List[int]] = defaultdict(list)
848
+ supported = True
849
+ for index, graph in bucket:
850
+ certificate = _canonical_attributed_graph_certificate(
851
+ graph,
852
+ node_attribute=_EXACT_NODE_SIG,
853
+ edge_attribute=_EXACT_EDGE_SIG,
854
+ node_palette=certificate_node_palette,
855
+ edge_palette=certificate_edge_palette,
856
+ topology_cache=certificate_topology_cache,
857
+ )
858
+ if certificate is None:
859
+ supported = False
860
+ break
861
+ certificate_buckets[certificate].append(index)
862
+ if not supported:
863
+ fallback_records.extend(bucket)
864
+ continue
865
+ for members in certificate_buckets.values():
866
+ representative_index = members[0]
867
+ representative_indices.append(representative_index)
868
+ _merge_application_orbits(
869
+ its_graphs[representative_index],
870
+ [its_graphs[index] for index in members],
871
+ )
872
+
873
+ if fallback_records:
874
+ if use_local_invariant:
875
+ # Complete fallback routines consume refined colours. Compute
876
+ # them only for the rare graph kinds that the compiled
877
+ # certificate cannot represent.
878
+ _refine_structural_colours_in_place(
879
+ [graph for _, graph in fallback_records],
880
+ iterations=hash_iterations,
881
+ )
882
+ fallback_representatives = _cluster_prepared_structural_its(
883
+ its_graphs,
884
+ fallback_records,
885
+ )
886
+ representative_indices.extend(
887
+ its_graphs.index(graph) for graph in fallback_representatives
888
+ )
889
+
890
+ representative_indices.sort()
891
+ return [its_graphs[index] for index in representative_indices]
892
+ finally:
893
+ _remove_structural_signatures(its_graphs)