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,1020 @@
1
+ """Exact native canonical labeling for finite coloured graphs.
2
+
3
+ The default implementation is intentionally small and unpruned. It provides
4
+ the correctness oracle for an opt-in automorphism-pruned
5
+ individualization--refinement search. Canonical identity is available only
6
+ after a complete search.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections import Counter
12
+ from collections.abc import Callable, Mapping, Sequence
13
+ from dataclasses import dataclass, field
14
+ from enum import Enum
15
+ import hashlib
16
+ import json
17
+ import math
18
+ from time import perf_counter
19
+ from typing import Any, Hashable
20
+
21
+ import networkx as nx
22
+
23
+ from .exact_refinement import ExactRefinementMixin
24
+
25
+ ColorToken = tuple[Any, ...]
26
+ ColorSelector = str | Sequence[str] | Callable[[Mapping[str, Any]], Any] | None
27
+
28
+ _STABILIZER_WORK_LIMIT = 4096
29
+
30
+
31
+ class CanonicalSearchIncomplete(RuntimeError):
32
+ """Raised when a bounded search cannot establish canonical identity."""
33
+
34
+
35
+ def _encode_color(value: Any) -> ColorToken:
36
+ """Return an exact, totally orderable token for supported colour values."""
37
+ if value is None:
38
+ return ("none",)
39
+ if isinstance(value, Enum):
40
+ return (
41
+ "enum",
42
+ type(value).__module__,
43
+ type(value).__qualname__,
44
+ _encode_color(value.value),
45
+ )
46
+ if isinstance(value, bool):
47
+ return ("bool", int(value))
48
+ if isinstance(value, int):
49
+ return ("int", str(value))
50
+ if isinstance(value, float):
51
+ if math.isnan(value):
52
+ return ("float", "nan")
53
+ return ("float", value.hex())
54
+ if isinstance(value, str):
55
+ return ("str", value)
56
+ if isinstance(value, bytes):
57
+ return ("bytes", value.hex())
58
+ if isinstance(value, Mapping):
59
+ items = sorted(
60
+ ((_encode_color(key), _encode_color(item)) for key, item in value.items())
61
+ )
62
+ return ("mapping", *items)
63
+ if isinstance(value, tuple):
64
+ return ("tuple", *(_encode_color(item) for item in value))
65
+ if isinstance(value, list):
66
+ return ("list", *(_encode_color(item) for item in value))
67
+ if isinstance(value, (set, frozenset)):
68
+ return ("set", *sorted(_encode_color(item) for item in value))
69
+ raise TypeError(
70
+ "Canonical colours require None, bool, number, string, bytes, enum, "
71
+ f"mapping, or finite container values; received {type(value).__name__}."
72
+ )
73
+
74
+
75
+ def _selector_value(
76
+ attributes: Mapping[str, Any],
77
+ selector: ColorSelector,
78
+ ) -> Any:
79
+ if selector is None:
80
+ return None
81
+ if isinstance(selector, str):
82
+ return attributes.get(selector)
83
+ if callable(selector):
84
+ return selector(attributes)
85
+ return tuple(attributes.get(key) for key in selector)
86
+
87
+
88
+ @dataclass(frozen=True)
89
+ class AutomorphismWitness:
90
+ """One verified source-to-image automorphism."""
91
+
92
+ pairs: tuple[tuple[Hashable, Hashable], ...]
93
+
94
+ def as_dict(self) -> dict[Hashable, Hashable]:
95
+ """Return the witness as a mutable convenience mapping."""
96
+ return dict(self.pairs)
97
+
98
+
99
+ @dataclass(frozen=True)
100
+ class CanonicalSearchStatistics:
101
+ """Observable search effort for one exact or bounded run."""
102
+
103
+ visited_nodes: int
104
+ leaves: int
105
+ refinement_rounds: int
106
+ elapsed_seconds: float
107
+
108
+
109
+ @dataclass(frozen=True)
110
+ class ExactCanonicalResult:
111
+ """Complete canonical identity and symmetry evidence.
112
+
113
+ ``canonical_key``, ``certificate_text``, and ``canonical_code`` are
114
+ invariant under every colour-preserving relabeling and construction-order
115
+ permutation. ``canonical_digest`` is only a compact index of that exact
116
+ certificate.
117
+
118
+ ``canonical_order`` and mappings derived from it retain original node
119
+ handles. They are labeling witnesses, not relabeling-invariant choices
120
+ among vertices in one non-trivial automorphism orbit. Compare such
121
+ witnesses after transport and modulo :attr:`automorphisms`.
122
+ """
123
+
124
+ canonical_order: tuple[Hashable, ...]
125
+ canonical_key: tuple[Any, ...]
126
+ certificate_text: str
127
+ canonical_code: str
128
+ canonical_digest: str
129
+ automorphisms: tuple[AutomorphismWitness, ...]
130
+ orbits: tuple[frozenset[Hashable], ...]
131
+ statistics: CanonicalSearchStatistics
132
+ automorphisms_complete: bool = True
133
+ complete: bool = True
134
+ exact: bool = True
135
+
136
+ @property
137
+ def canonical_mapping(self) -> dict[Hashable, int]:
138
+ """Map original nodes to consecutive one-based canonical labels."""
139
+ return {
140
+ node: position
141
+ for position, node in enumerate(self.canonical_order, start=1)
142
+ }
143
+
144
+ def same_canonical_graph(self, other: object) -> bool:
145
+ """Compare exact certificates, never their finite digests."""
146
+ return (
147
+ isinstance(other, ExactCanonicalResult)
148
+ and self.canonical_key == other.canonical_key
149
+ )
150
+
151
+
152
+ @dataclass(frozen=True)
153
+ class IncompleteCanonicalResult:
154
+ """Diagnostic evidence from a search that did not prove canonicality."""
155
+
156
+ reason: str
157
+ statistics: CanonicalSearchStatistics
158
+ complete: bool = False
159
+ exact: bool = False
160
+ automorphisms: tuple[AutomorphismWitness, ...] = ()
161
+
162
+ def require_complete(self) -> ExactCanonicalResult:
163
+ """Refuse promotion of bounded-search evidence to canonical identity."""
164
+ raise CanonicalSearchIncomplete(
165
+ f"Canonical search is incomplete: {self.reason}."
166
+ )
167
+
168
+
169
+ CanonicalResult = ExactCanonicalResult | IncompleteCanonicalResult
170
+
171
+
172
+ @dataclass
173
+ class _SearchState:
174
+ """Mutable state owned by one depth-first canonical search."""
175
+
176
+ started: float
177
+ timeout_seconds: float | None
178
+ max_search_nodes: int | None
179
+ max_depth: int | None
180
+ visited_nodes: int = 0
181
+ leaves: int = 0
182
+ termination_reason: str | None = None
183
+ best_key: tuple[Any, ...] | None = None
184
+ best_order: tuple[Hashable, ...] | None = None
185
+ equal_orders: list[tuple[Hashable, ...]] = field(default_factory=list)
186
+ generators: list[dict[Hashable, Hashable]] = field(default_factory=list)
187
+ generator_signatures: set[tuple[Hashable, ...]] = field(default_factory=set)
188
+ generator_supports: list[tuple[tuple[Hashable, Hashable], ...]] = field(default_factory=list)
189
+ stabilizer_cache: dict[
190
+ tuple[int, tuple[Hashable, ...]],
191
+ tuple[dict[Hashable, Hashable], ...],
192
+ ] = field(default_factory=dict)
193
+
194
+
195
+ class ExactColoredGraphCanonicalizer(ExactRefinementMixin):
196
+ """Complete individualization--refinement for finite coloured graphs.
197
+
198
+ Graph structure and semantic colours are snapshotted during construction.
199
+ Node identifiers are retained only as witness handles and never enter the
200
+ canonical certificate. ``prune_automorphisms`` enables verified orbit
201
+ pruning together with incremental refinement, adaptive stabilizer chains,
202
+ nonuniform-component recursion, and trace-guided certificate pruning. The
203
+ unpruned default remains the small exhaustive correctness oracle.
204
+ """
205
+
206
+ def __init__(
207
+ self,
208
+ graph: nx.Graph,
209
+ *,
210
+ node_color: ColorSelector = "color",
211
+ edge_color: ColorSelector = "color",
212
+ prune_automorphisms: bool = False,
213
+ enumerate_automorphism_group: bool = True,
214
+ ) -> None:
215
+ if not isinstance(graph, (nx.Graph, nx.DiGraph)):
216
+ raise TypeError("Exact canonicalization requires a NetworkX graph.")
217
+ if graph.is_multigraph():
218
+ raise TypeError(
219
+ "Encode parallel edges as coloured incidence vertices before "
220
+ "exact canonicalization."
221
+ )
222
+ self._graph = graph.copy()
223
+ self._directed = self._graph.is_directed()
224
+ self._nodes = tuple(self._graph.nodes())
225
+ self._node_colors = {
226
+ node: _encode_color(_selector_value(self._graph.nodes[node], node_color))
227
+ for node in self._nodes
228
+ }
229
+ self._edge_colors = {
230
+ (left, right): _encode_color(_selector_value(attributes, edge_color))
231
+ for left, right, attributes in self._graph.edges(data=True)
232
+ }
233
+ self._edge_lookup = dict(self._edge_colors)
234
+ if not self._directed:
235
+ self._edge_lookup.update(
236
+ {
237
+ (right, left): token
238
+ for (left, right), token in self._edge_colors.items()
239
+ }
240
+ )
241
+ outgoing: dict[Hashable, list[tuple[Hashable, ColorToken]]] = {
242
+ node: [] for node in self._nodes
243
+ }
244
+ incoming: dict[Hashable, list[tuple[Hashable, ColorToken]]] = {
245
+ node: [] for node in self._nodes
246
+ }
247
+ for (left, right), token in self._edge_colors.items():
248
+ outgoing[left].append((right, token))
249
+ incoming[right].append((left, token))
250
+ if not self._directed and left != right:
251
+ outgoing[right].append((left, token))
252
+ incoming[left].append((right, token))
253
+ self._outgoing = {
254
+ node: tuple(neighbours) for node, neighbours in outgoing.items()
255
+ }
256
+ self._incoming = {
257
+ node: tuple(neighbours) for node, neighbours in incoming.items()
258
+ }
259
+ self._refinement_rounds = 0
260
+ self._complete_cache: ExactCanonicalResult | None = None
261
+ self._prune_automorphisms = prune_automorphisms
262
+ self._enumerate_automorphism_group = enumerate_automorphism_group
263
+
264
+ @property
265
+ def graph(self) -> nx.Graph:
266
+ """Return a defensive copy of the snapshotted graph."""
267
+ return self._graph.copy()
268
+
269
+ def _edge_token(
270
+ self,
271
+ left: Hashable,
272
+ right: Hashable,
273
+ ) -> ColorToken | None:
274
+ return self._edge_lookup.get((left, right))
275
+
276
+ @staticmethod
277
+ def _multiset(values: list[ColorToken]) -> tuple[Any, ...]:
278
+ counts = Counter(values)
279
+ return tuple(sorted(counts.items()))
280
+
281
+ def _cell_signature(
282
+ self,
283
+ node: Hashable,
284
+ partition: tuple[tuple[Hashable, ...], ...],
285
+ cell_index: Mapping[Hashable, int] | None = None,
286
+ ) -> tuple[Any, ...]:
287
+ if cell_index is None:
288
+ cell_index = {
289
+ member: index for index, cell in enumerate(partition) for member in cell
290
+ }
291
+ outgoing: dict[int, list[ColorToken]] = {}
292
+ for other, token in self._outgoing[node]:
293
+ outgoing.setdefault(cell_index[other], []).append(token)
294
+ # Empty cells sort before non-empty cells in the former dense
295
+ # signature. Omitting them and negating the cell index preserves
296
+ # that lexicographic order without materializing one entry per cell.
297
+ if self._directed:
298
+ incoming: dict[int, list[ColorToken]] = {}
299
+ for other, token in self._incoming[node]:
300
+ incoming.setdefault(cell_index[other], []).append(token)
301
+ per_cell = tuple(
302
+ (
303
+ -index,
304
+ (
305
+ self._multiset(outgoing.get(index, [])),
306
+ self._multiset(incoming.get(index, [])),
307
+ ),
308
+ )
309
+ for index in sorted(outgoing.keys() | incoming.keys())
310
+ )
311
+ else:
312
+ per_cell = tuple(
313
+ (-index, self._multiset(tokens))
314
+ for index, tokens in sorted(outgoing.items())
315
+ )
316
+ return (self._node_colors[node], per_cell)
317
+
318
+ def _refine(
319
+ self,
320
+ partition: tuple[tuple[Hashable, ...], ...],
321
+ ) -> tuple[tuple[Hashable, ...], ...]:
322
+ current = partition
323
+ while True:
324
+ changed = False
325
+ refined: list[tuple[Hashable, ...]] = []
326
+ cell_index = {
327
+ node: index for index, cell in enumerate(current) for node in cell
328
+ }
329
+ for cell in current:
330
+ buckets: dict[tuple[Any, ...], list[Hashable]] = {}
331
+ for node in cell:
332
+ signature = self._cell_signature(
333
+ node,
334
+ current,
335
+ cell_index,
336
+ )
337
+ buckets.setdefault(signature, []).append(node)
338
+ if len(buckets) > 1:
339
+ changed = True
340
+ refined.extend(
341
+ tuple(buckets[signature]) for signature in sorted(buckets)
342
+ )
343
+ self._refinement_rounds += 1
344
+ current = tuple(refined)
345
+ if not changed:
346
+ return current
347
+
348
+ def _initial_partition(self) -> tuple[tuple[Hashable, ...], ...]:
349
+ buckets: dict[ColorToken, list[Hashable]] = {}
350
+ for node in self._nodes:
351
+ buckets.setdefault(self._node_colors[node], []).append(node)
352
+ return tuple(tuple(buckets[color]) for color in sorted(buckets))
353
+
354
+ def _canonical_key(
355
+ self,
356
+ order: tuple[Hashable, ...],
357
+ ) -> tuple[Any, ...]:
358
+ node_part = tuple(self._node_colors[node] for node in order)
359
+ absent = ("absent",)
360
+ size = len(order)
361
+ positions = {node: index for index, node in enumerate(order)}
362
+ if self._directed:
363
+ adjacency = [absent] * (size * size)
364
+ for (left, right), token in self._edge_colors.items():
365
+ adjacency[positions[left] * size + positions[right]] = (
366
+ "edge",
367
+ token,
368
+ )
369
+ else:
370
+ adjacency = [absent] * (size * (size + 1) // 2)
371
+ for (left, right), token in self._edge_colors.items():
372
+ left_position, right_position = positions[left], positions[right]
373
+ if left_position > right_position:
374
+ left_position, right_position = right_position, left_position
375
+ row_start = (
376
+ left_position * size - left_position * (left_position - 1) // 2
377
+ )
378
+ adjacency[row_start + right_position - left_position] = (
379
+ "edge",
380
+ token,
381
+ )
382
+ return (
383
+ ("directed", int(self._directed)),
384
+ ("nodes", node_part),
385
+ ("adjacency", tuple(adjacency)),
386
+ )
387
+
388
+ def _is_automorphism(
389
+ self,
390
+ mapping: Mapping[Hashable, Hashable],
391
+ ) -> bool:
392
+ if set(mapping) != set(self._nodes) or set(mapping.values()) != set(
393
+ self._nodes
394
+ ):
395
+ return False
396
+ if any(
397
+ self._node_colors[node] != self._node_colors[mapping[node]]
398
+ for node in self._nodes
399
+ ):
400
+ return False
401
+ # A vertex bijection maps distinct source edges to distinct pairs.
402
+ # Since source and target have the same edge count, preserving every
403
+ # coloured edge also proves that non-edges are preserved.
404
+ return all(
405
+ token == self._edge_token(mapping[left], mapping[right])
406
+ for (left, right), token in self._edge_colors.items()
407
+ )
408
+
409
+ def _orbits(
410
+ self,
411
+ witnesses: tuple[AutomorphismWitness, ...],
412
+ ) -> tuple[frozenset[Hashable], ...]:
413
+ parent = {node: node for node in self._nodes}
414
+
415
+ def find(node: Hashable) -> Hashable:
416
+ while parent[node] != node:
417
+ parent[node] = parent[parent[node]]
418
+ node = parent[node]
419
+ return node
420
+
421
+ def union(left: Hashable, right: Hashable) -> None:
422
+ left_root, right_root = find(left), find(right)
423
+ if left_root != right_root:
424
+ parent[right_root] = left_root
425
+
426
+ for witness in witnesses:
427
+ for left, right in witness.pairs:
428
+ union(left, right)
429
+ classes: dict[Hashable, set[Hashable]] = {}
430
+ for node in self._nodes:
431
+ classes.setdefault(find(node), set()).add(node)
432
+ position = {node: index for index, node in enumerate(self._nodes)}
433
+ return tuple(
434
+ sorted(
435
+ (frozenset(values) for values in classes.values()),
436
+ key=lambda cell: min(position[node] for node in cell),
437
+ )
438
+ )
439
+
440
+ @staticmethod
441
+ def _stop_reason(state: _SearchState, depth: int) -> str | None:
442
+ if (
443
+ state.timeout_seconds is not None
444
+ and perf_counter() - state.started >= state.timeout_seconds
445
+ ):
446
+ return "timeout"
447
+ if (
448
+ state.max_search_nodes is not None
449
+ and state.visited_nodes >= state.max_search_nodes
450
+ ):
451
+ return "search_node_budget"
452
+ if state.max_depth is not None and depth > state.max_depth:
453
+ return "depth_budget"
454
+ return None
455
+
456
+ def _record_leaf(
457
+ self,
458
+ partition: tuple[tuple[Hashable, ...], ...],
459
+ state: _SearchState,
460
+ ) -> tuple[tuple[Any, ...], tuple[Hashable, ...]]:
461
+ state.leaves += 1
462
+ order = tuple(cell[0] for cell in partition)
463
+ key = self._canonical_key(order)
464
+ if state.best_key is None or key < state.best_key:
465
+ state.best_key = key
466
+ state.best_order = order
467
+ state.equal_orders = [order]
468
+ elif key == state.best_key:
469
+ state.equal_orders.append(order)
470
+ return key, order
471
+
472
+ def _visit_partition(
473
+ self,
474
+ partition: tuple[tuple[Hashable, ...], ...],
475
+ depth: int,
476
+ state: _SearchState,
477
+ ) -> None:
478
+ if state.termination_reason is not None:
479
+ return
480
+ reason = self._stop_reason(state, depth)
481
+ if reason is not None:
482
+ state.termination_reason = reason
483
+ return
484
+ state.visited_nodes += 1
485
+ refined = self._refine(partition)
486
+ ambiguous = tuple(
487
+ (index, cell) for index, cell in enumerate(refined) if len(cell) > 1
488
+ )
489
+ if not ambiguous:
490
+ self._record_leaf(refined, state)
491
+ return
492
+ cell_index, target = min(
493
+ ambiguous,
494
+ key=lambda item: (len(item[1]), item[0]),
495
+ )
496
+ for chosen in target:
497
+ remainder = tuple(node for node in target if node != chosen)
498
+ child = list(refined)
499
+ child[cell_index : cell_index + 1] = (
500
+ [(chosen,), remainder] if remainder else [(chosen,)]
501
+ )
502
+ self._visit_partition(tuple(child), depth + 1, state)
503
+ if state.termination_reason is not None:
504
+ return
505
+
506
+ def _register_generator(
507
+ self,
508
+ left_order: tuple[Hashable, ...],
509
+ right_order: tuple[Hashable, ...],
510
+ state: _SearchState,
511
+ ) -> None:
512
+ mapping = {
513
+ left_order[index]: right_order[index] for index in range(len(left_order))
514
+ }
515
+ signature = tuple(mapping[node] for node in self._nodes)
516
+ if signature in state.generator_signatures:
517
+ return
518
+ if not self._is_automorphism(mapping):
519
+ raise RuntimeError(
520
+ "Equal canonical subtrees did not induce an automorphism."
521
+ )
522
+ state.generator_signatures.add(signature)
523
+ state.generators.append(mapping)
524
+
525
+ def _seed_twin_generators(self, state: _SearchState) -> None:
526
+ """Seed verified swaps of identically colored nonadjacent twins.
527
+
528
+ Exact incoming and outgoing neighborhoods define candidates, including
529
+ for directed graphs. Every swap is checked before it can prune search.
530
+ Adjacent swaps generate each twin cell without expanding its group.
531
+ """
532
+ groups = {}
533
+ for node in self._nodes:
534
+ if self._stop_reason(state, 0) is not None:
535
+ return
536
+ signature = (
537
+ self._node_colors[node],
538
+ frozenset(self._outgoing[node]),
539
+ frozenset(self._incoming[node]),
540
+ )
541
+ groups.setdefault(signature, []).append(node)
542
+ for nodes in groups.values():
543
+ for left, right in zip(nodes, nodes[1:]):
544
+ if len(state.generators) >= 256 or self._stop_reason(state, 0):
545
+ return
546
+ mapping = self._identity_mapping()
547
+ mapping[left], mapping[right] = right, left
548
+ if not self._is_automorphism(mapping):
549
+ continue
550
+ signature = self._mapping_signature(mapping)
551
+ if signature not in state.generator_signatures:
552
+ state.generator_signatures.add(signature)
553
+ state.generators.append(mapping)
554
+
555
+ def _identity_mapping(self) -> dict[Hashable, Hashable]:
556
+ return {node: node for node in self._nodes}
557
+
558
+ def _mapping_signature(
559
+ self,
560
+ mapping: Mapping[Hashable, Hashable],
561
+ ) -> tuple[Hashable, ...]:
562
+ return tuple(mapping[node] for node in self._nodes)
563
+
564
+ def _compose_mappings(
565
+ self,
566
+ first: Mapping[Hashable, Hashable],
567
+ second: Mapping[Hashable, Hashable],
568
+ ) -> dict[Hashable, Hashable]:
569
+ """Return ``second`` after ``first``."""
570
+ return {node: second[first[node]] for node in self._nodes}
571
+
572
+ def _inverse_mapping(
573
+ self,
574
+ mapping: Mapping[Hashable, Hashable],
575
+ ) -> dict[Hashable, Hashable]:
576
+ return {image: source for source, image in mapping.items()}
577
+
578
+ def _deduplicate_mappings(
579
+ self,
580
+ mappings: Sequence[Mapping[Hashable, Hashable]],
581
+ *,
582
+ include_identity: bool = False,
583
+ ) -> tuple[dict[Hashable, Hashable], ...]:
584
+ identity_signature = tuple(self._nodes)
585
+ unique: dict[tuple[Hashable, ...], dict[Hashable, Hashable]] = {}
586
+ for mapping in mappings:
587
+ signature = self._mapping_signature(mapping)
588
+ if not include_identity and signature == identity_signature:
589
+ continue
590
+ unique.setdefault(signature, dict(mapping))
591
+ return tuple(unique.values())
592
+
593
+ def _point_stabilizer_generators(
594
+ self,
595
+ generators: Sequence[Mapping[Hashable, Hashable]],
596
+ point: Hashable,
597
+ ) -> tuple[dict[Hashable, Hashable], ...]:
598
+ """Return Schreier generators for the subgroup fixing ``point``."""
599
+ seeds = self._deduplicate_mappings(generators)
600
+ if not seeds:
601
+ return ()
602
+ if all(generator[point] == point for generator in seeds):
603
+ return seeds
604
+
605
+ identity = self._identity_mapping()
606
+ transversals = {point: identity}
607
+ pending = [point]
608
+ while pending:
609
+ current = pending.pop()
610
+ current_transversal = transversals[current]
611
+ for generator in seeds:
612
+ image = generator[current]
613
+ if image in transversals:
614
+ continue
615
+ transversals[image] = self._compose_mappings(
616
+ current_transversal,
617
+ generator,
618
+ )
619
+ pending.append(image)
620
+
621
+ inverses = {
622
+ image: self._inverse_mapping(transport)
623
+ for image, transport in transversals.items()
624
+ }
625
+ stabilizer_generators = []
626
+ for current, current_transversal in transversals.items():
627
+ for generator in seeds:
628
+ image = generator[current]
629
+ schreier = self._compose_mappings(
630
+ self._compose_mappings(
631
+ current_transversal,
632
+ generator,
633
+ ),
634
+ inverses[image],
635
+ )
636
+ stabilizer_generators.append(schreier)
637
+ return self._deduplicate_mappings(stabilizer_generators)
638
+
639
+ def _stabilizer_generators(
640
+ self,
641
+ generators: Sequence[Mapping[Hashable, Hashable]],
642
+ fixed: tuple[Hashable, ...],
643
+ ) -> tuple[dict[Hashable, Hashable], ...]:
644
+ """Return generators for the subgroup fixing every point in ``fixed``."""
645
+ stabilizers = self._deduplicate_mappings(generators)
646
+ for point in fixed:
647
+ if all(generator[point] == point for generator in stabilizers):
648
+ continue
649
+ stabilizers = self._point_stabilizer_generators(stabilizers, point)
650
+ if not stabilizers:
651
+ break
652
+ return stabilizers
653
+
654
+ def _cached_stabilizer_generators(
655
+ self,
656
+ state: _SearchState,
657
+ fixed: tuple[Hashable, ...],
658
+ ) -> tuple[dict[Hashable, Hashable], ...]:
659
+ cache_key = (len(state.generators), fixed)
660
+ cached = state.stabilizer_cache.get(cache_key)
661
+ if cached is None:
662
+ cached = self._stabilizer_generators(state.generators, fixed)
663
+ state.stabilizer_cache[cache_key] = cached
664
+ return cached
665
+
666
+ def _pruning_generators(
667
+ self,
668
+ state: _SearchState,
669
+ fixed: tuple[Hashable, ...],
670
+ cell_index: Mapping[Hashable, int],
671
+ ) -> tuple[dict[Hashable, Hashable], ...]:
672
+ """Choose exact stabilizer generators without excessive group overhead."""
673
+ estimated_work = (
674
+ len(self._nodes) * max(1, len(state.generators)) * max(1, len(fixed))
675
+ )
676
+ # Schreier generators save search only while their construction remains
677
+ # cheaper than direct filtering of the verified generators.
678
+ if estimated_work > _STABILIZER_WORK_LIMIT:
679
+ # Root generators are immutable after registration. Checking only
680
+ # their moved points avoids rescanning fixed atoms at each node.
681
+ for mapping in state.generators[len(state.generator_supports):]:
682
+ state.generator_supports.append(tuple(
683
+ (node, image) for node, image in mapping.items()
684
+ if node != image
685
+ ))
686
+ return tuple(
687
+ mapping for mapping, support in zip(
688
+ state.generators, state.generator_supports
689
+ )
690
+ if all(cell_index[node] == cell_index[image] for node, image in support)
691
+ )
692
+ subgroup = self._cached_stabilizer_generators(state, fixed)
693
+ return tuple(
694
+ mapping
695
+ for mapping in subgroup
696
+ if self._stabilizes_partition(mapping, cell_index)
697
+ )
698
+
699
+ @staticmethod
700
+ def _stabilizes_partition(
701
+ mapping: Mapping[Hashable, Hashable],
702
+ cell_index: Mapping[Hashable, int],
703
+ ) -> bool:
704
+ return all(cell_index[node] == cell_index[mapping[node]] for node in cell_index)
705
+
706
+ @staticmethod
707
+ def _generator_orbit(
708
+ node: Hashable,
709
+ generators: Sequence[Mapping[Hashable, Hashable]],
710
+ ) -> frozenset[Hashable]:
711
+ orbit = {node}
712
+ pending = [node]
713
+ while pending:
714
+ current = pending.pop()
715
+ for mapping in generators:
716
+ image = mapping[current]
717
+ if image not in orbit:
718
+ orbit.add(image)
719
+ pending.append(image)
720
+ return frozenset(orbit)
721
+
722
+ def _visit_partition_pruned(
723
+ self,
724
+ partition: tuple[tuple[Hashable, ...], ...],
725
+ depth: int,
726
+ state: _SearchState,
727
+ *,
728
+ fixed: tuple[Hashable, ...] = (),
729
+ changed_members: frozenset[Hashable] | None = None,
730
+ active_component: frozenset[Hashable] | None = None,
731
+ already_refined: bool = False,
732
+ ) -> tuple[tuple[Any, ...], tuple[Hashable, ...]] | None:
733
+ """Visit one subtree, pruning only branches joined by a verified orbit."""
734
+ if state.termination_reason is not None:
735
+ return None
736
+ reason = self._stop_reason(state, depth)
737
+ if reason is not None:
738
+ state.termination_reason = reason
739
+ return None
740
+ state.visited_nodes += 1
741
+ refined = self._refine_search_partition(
742
+ partition,
743
+ changed_members,
744
+ already_refined,
745
+ )
746
+ ambiguous = tuple(
747
+ (index, cell) for index, cell in enumerate(refined) if len(cell) > 1
748
+ )
749
+ if self._certificate_prunes(refined, ambiguous, state.best_key):
750
+ return None
751
+ if not ambiguous:
752
+ return self._record_leaf(refined, state)
753
+
754
+ cell_index, target, active_component = self._select_component_target(
755
+ refined,
756
+ ambiguous,
757
+ active_component,
758
+ )
759
+ partition_cells = {
760
+ node: index for index, cell in enumerate(refined) for node in cell
761
+ }
762
+ explored: list[tuple[Hashable, tuple[Any, ...], tuple[Hashable, ...]]] = []
763
+ subtree_best: tuple[tuple[Any, ...], tuple[Hashable, ...]] | None = None
764
+ ordered_children = self._ordered_search_children(
765
+ refined,
766
+ cell_index,
767
+ target,
768
+ )
769
+ generation = -1
770
+ stabilizers = ()
771
+ for _trace, _position, chosen, child_refined in ordered_children:
772
+ if generation != len(state.generators):
773
+ stabilizers = self._pruning_generators(
774
+ state,
775
+ fixed,
776
+ partition_cells,
777
+ )
778
+ generation = len(state.generators)
779
+ if self._orbit_already_explored(
780
+ chosen,
781
+ explored,
782
+ stabilizers,
783
+ ):
784
+ continue
785
+
786
+ if child_refined is None:
787
+ child_refined = self._individualized_child(
788
+ refined,
789
+ cell_index,
790
+ target,
791
+ chosen,
792
+ )
793
+ child_best = self._visit_partition_pruned(
794
+ child_refined,
795
+ depth + 1,
796
+ state,
797
+ fixed=(*fixed, chosen),
798
+ active_component=active_component,
799
+ already_refined=True,
800
+ )
801
+ if state.termination_reason is not None:
802
+ return None
803
+ if child_best is None:
804
+ continue
805
+ key, order = child_best
806
+ self._register_equal_child_generators(
807
+ explored,
808
+ key,
809
+ order,
810
+ state,
811
+ )
812
+ explored.append((chosen, key, order))
813
+ if subtree_best is None or key < subtree_best[0]:
814
+ subtree_best = (key, order)
815
+ return subtree_best
816
+
817
+ def _verified_witnesses(
818
+ self,
819
+ best_order: tuple[Hashable, ...],
820
+ equal_orders: list[tuple[Hashable, ...]],
821
+ ) -> tuple[AutomorphismWitness, ...]:
822
+ witnesses = []
823
+ for order in equal_orders:
824
+ mapping = {
825
+ best_order[index]: order[index] for index in range(len(best_order))
826
+ }
827
+ if not self._is_automorphism(mapping):
828
+ raise RuntimeError(
829
+ "Equal canonical leaves did not induce an automorphism."
830
+ )
831
+ witnesses.append(AutomorphismWitness(tuple(mapping.items())))
832
+ return tuple(witnesses)
833
+
834
+ def _generated_witnesses(
835
+ self,
836
+ best_order: tuple[Hashable, ...],
837
+ equal_orders: list[tuple[Hashable, ...]],
838
+ generators: list[dict[Hashable, Hashable]],
839
+ ) -> tuple[AutomorphismWitness, ...]:
840
+ """Enumerate the exact group generated by pruned-search witnesses."""
841
+ seeds = list(generators)
842
+ for order in equal_orders:
843
+ seeds.append(
844
+ {best_order[index]: order[index] for index in range(len(best_order))}
845
+ )
846
+ identity = tuple(self._nodes)
847
+ generator_signatures = {
848
+ tuple(mapping[node] for node in self._nodes) for mapping in seeds
849
+ }
850
+ known = {identity}
851
+ pending = [identity]
852
+ while pending:
853
+ current = pending.pop()
854
+ current_mapping = dict(zip(self._nodes, current))
855
+ for generator_signature in generator_signatures:
856
+ generator = dict(zip(self._nodes, generator_signature))
857
+ composed = tuple(
858
+ generator[current_mapping[node]] for node in self._nodes
859
+ )
860
+ if composed not in known:
861
+ known.add(composed)
862
+ pending.append(composed)
863
+
864
+ witnesses = []
865
+ for signature in sorted(
866
+ known,
867
+ key=lambda item: tuple(repr(value) for value in item),
868
+ ):
869
+ mapping = dict(zip(self._nodes, signature))
870
+ if not self._is_automorphism(mapping):
871
+ raise RuntimeError(
872
+ "A generated symmetry witness was not an automorphism."
873
+ )
874
+ witnesses.append(AutomorphismWitness(tuple(mapping.items())))
875
+ return tuple(witnesses)
876
+
877
+ def _generator_witnesses(
878
+ self,
879
+ best_order: tuple[Hashable, ...],
880
+ equal_orders: list[tuple[Hashable, ...]],
881
+ generators: list[dict[Hashable, Hashable]],
882
+ ) -> tuple[AutomorphismWitness, ...]:
883
+ """Return verified generators without expanding the generated group."""
884
+ mappings = list(generators)
885
+ mappings.extend(
886
+ {best_order[index]: order[index] for index in range(len(best_order))}
887
+ for order in equal_orders
888
+ )
889
+ identity = {node: node for node in self._nodes}
890
+ mappings.append(identity)
891
+ unique = {
892
+ tuple(mapping[node] for node in self._nodes): mapping
893
+ for mapping in mappings
894
+ }
895
+ witnesses = []
896
+ for signature in sorted(
897
+ unique,
898
+ key=lambda item: tuple(repr(value) for value in item),
899
+ ):
900
+ mapping = unique[signature]
901
+ if not self._is_automorphism(mapping):
902
+ raise RuntimeError("A symmetry generator was not an automorphism.")
903
+ witnesses.append(AutomorphismWitness(tuple(mapping.items())))
904
+ return tuple(witnesses)
905
+
906
+ def search(
907
+ self,
908
+ *,
909
+ timeout_seconds: float | None = None,
910
+ max_search_nodes: int | None = None,
911
+ max_depth: int | None = None,
912
+ ) -> CanonicalResult:
913
+ """Run a complete or explicitly bounded canonical search."""
914
+ if timeout_seconds is not None and timeout_seconds < 0:
915
+ raise ValueError("Canonical timeout must be non-negative.")
916
+ if max_search_nodes is not None and max_search_nodes < 1:
917
+ raise ValueError("Canonical search-node budget must be positive.")
918
+ if max_depth is not None and max_depth < 0:
919
+ raise ValueError("Canonical search depth must be non-negative.")
920
+
921
+ started = perf_counter()
922
+ self._refinement_rounds = 0
923
+ state = _SearchState(
924
+ started,
925
+ timeout_seconds,
926
+ max_search_nodes,
927
+ max_depth,
928
+ )
929
+ if self._prune_automorphisms:
930
+ self._seed_twin_generators(state)
931
+ self._visit_partition_pruned(self._initial_partition(), 0, state)
932
+ else:
933
+ self._visit_partition(self._initial_partition(), 0, state)
934
+ statistics = CanonicalSearchStatistics(
935
+ visited_nodes=state.visited_nodes,
936
+ leaves=state.leaves,
937
+ refinement_rounds=self._refinement_rounds,
938
+ elapsed_seconds=perf_counter() - started,
939
+ )
940
+ if state.termination_reason is not None:
941
+ # Verified automorphisms remain valid even when canonicality
942
+ # was not proved. Preserve this subgroup without exposing a code.
943
+ witnesses = (
944
+ self._generator_witnesses(
945
+ state.best_order if state.best_order is not None else self._nodes,
946
+ state.equal_orders[:32] if state.best_order is not None else [],
947
+ state.generators[:256],
948
+ )
949
+ if self._prune_automorphisms else ()
950
+ )
951
+ return IncompleteCanonicalResult(
952
+ state.termination_reason,
953
+ statistics,
954
+ automorphisms=witnesses,
955
+ )
956
+ if state.best_key is None or state.best_order is None:
957
+ raise RuntimeError("Complete canonical search produced no leaf.")
958
+
959
+ if self._prune_automorphisms and self._enumerate_automorphism_group:
960
+ automorphisms = self._generated_witnesses(
961
+ state.best_order,
962
+ state.equal_orders,
963
+ state.generators,
964
+ )
965
+ elif self._prune_automorphisms:
966
+ automorphisms = self._generator_witnesses(
967
+ state.best_order,
968
+ state.equal_orders,
969
+ state.generators,
970
+ )
971
+ else:
972
+ automorphisms = self._verified_witnesses(
973
+ state.best_order,
974
+ state.equal_orders,
975
+ )
976
+ certificate_text = json.dumps(
977
+ state.best_key,
978
+ separators=(",", ":"),
979
+ )
980
+ result = ExactCanonicalResult(
981
+ canonical_order=state.best_order,
982
+ canonical_key=state.best_key,
983
+ certificate_text=certificate_text,
984
+ # The code is the injective serialization. The digest is only a
985
+ # compact index and must not be used as mathematical proof of
986
+ # graph identity.
987
+ canonical_code=certificate_text,
988
+ canonical_digest=hashlib.sha256(
989
+ certificate_text.encode("utf-8")
990
+ ).hexdigest(),
991
+ automorphisms=automorphisms,
992
+ orbits=self._orbits(automorphisms),
993
+ statistics=statistics,
994
+ automorphisms_complete=(
995
+ not self._prune_automorphisms or self._enumerate_automorphism_group
996
+ ),
997
+ )
998
+ if timeout_seconds is None and max_search_nodes is None and max_depth is None:
999
+ self._complete_cache = result
1000
+ return result
1001
+
1002
+ def canonicalize(self) -> ExactCanonicalResult:
1003
+ """Return exact identity, running an unbounded complete search once."""
1004
+ if self._complete_cache is None:
1005
+ result = self.search()
1006
+ if not isinstance(result, ExactCanonicalResult):
1007
+ raise RuntimeError("Unbounded canonical search was incomplete.")
1008
+ self._complete_cache = result
1009
+ return self._complete_cache
1010
+
1011
+
1012
+ __all__ = [
1013
+ "AutomorphismWitness",
1014
+ "CanonicalResult",
1015
+ "CanonicalSearchIncomplete",
1016
+ "CanonicalSearchStatistics",
1017
+ "ExactCanonicalResult",
1018
+ "ExactColoredGraphCanonicalizer",
1019
+ "IncompleteCanonicalResult",
1020
+ ]