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.
- synkit/CRN/Benchmark/__init__.py +53 -0
- synkit/CRN/Benchmark/crosschecks.py +13 -0
- synkit/CRN/Benchmark/kegg.py +12 -0
- synkit/CRN/Benchmark/networks.py +13 -0
- synkit/CRN/Benchmark/scaling.py +13 -0
- synkit/CRN/Benchmark/validate.py +13 -0
- synkit/CRN/Construct/__init__.py +16 -0
- synkit/CRN/Construct/abstract.py +668 -0
- synkit/CRN/Construct/arity.py +36 -0
- synkit/CRN/Construct/builder.py +619 -0
- synkit/CRN/Construct/derivation.py +63 -0
- synkit/CRN/Construct/flattener.py +100 -0
- synkit/CRN/Construct/keys.py +14 -0
- synkit/CRN/Construct/mixtures.py +117 -0
- synkit/CRN/Construct/rdkit_worker.py +69 -0
- synkit/CRN/Construct/smiles.py +99 -0
- synkit/CRN/Construct/state.py +31 -0
- synkit/CRN/Construct/strategy.py +75 -0
- synkit/CRN/Construct/worker.py +42 -0
- synkit/CRN/IO/__init__.py +36 -0
- synkit/CRN/IO/sbml.py +925 -0
- synkit/CRN/Pathway/__init__.py +72 -0
- synkit/CRN/Pathway/_adapter.py +333 -0
- synkit/CRN/Pathway/pathfinder.py +500 -0
- synkit/CRN/Pathway/reachability.py +796 -0
- synkit/CRN/Pathway/realizability.py +1322 -0
- synkit/CRN/Petrinet/__init__.py +93 -0
- synkit/CRN/Petrinet/analyzer.py +486 -0
- synkit/CRN/Petrinet/minimal_semiflows.py +436 -0
- synkit/CRN/Petrinet/net.py +1041 -0
- synkit/CRN/Petrinet/persistence.py +97 -0
- synkit/CRN/Petrinet/semiflows.py +483 -0
- synkit/CRN/Petrinet/siphon_search.py +402 -0
- synkit/CRN/Petrinet/structure.py +286 -0
- synkit/CRN/Props/__init__.py +130 -0
- synkit/CRN/Props/_rank.py +29 -0
- synkit/CRN/Props/deficiency.py +953 -0
- synkit/CRN/Props/dynamics.py +832 -0
- synkit/CRN/Props/helper.py +151 -0
- synkit/CRN/Props/stoich.py +751 -0
- synkit/CRN/Props/thermo.py +584 -0
- synkit/CRN/Query/__init__.py +66 -0
- synkit/CRN/Query/cache.py +41 -0
- synkit/CRN/Query/kegg_api.py +99 -0
- synkit/CRN/Query/kegg_extract.py +714 -0
- synkit/CRN/Query/kegg_impute.py +707 -0
- synkit/CRN/Query/kegg_parse.py +480 -0
- synkit/CRN/Query/to_syncrn.py +290 -0
- synkit/CRN/README.md +234 -0
- synkit/CRN/Structure/__init__.py +12 -0
- synkit/CRN/Structure/_graph_io.py +713 -0
- synkit/CRN/Structure/_matrices.py +88 -0
- synkit/CRN/Structure/_parse.py +544 -0
- synkit/CRN/Structure/reaction.py +237 -0
- synkit/CRN/Structure/rule.py +55 -0
- synkit/CRN/Structure/species.py +46 -0
- synkit/CRN/Structure/syncrn.py +929 -0
- synkit/CRN/Symmetry/__init__.py +20 -0
- synkit/CRN/Symmetry/_common.py +1059 -0
- synkit/CRN/Symmetry/_ir.py +940 -0
- synkit/CRN/Symmetry/automorphism.py +472 -0
- synkit/CRN/Symmetry/canon.py +441 -0
- synkit/CRN/Symmetry/isomorphism.py +412 -0
- synkit/CRN/Symmetry/symmetry.py +61 -0
- synkit/CRN/Symmetry/wl_canon.py +967 -0
- synkit/CRN/Visualize/__init__.py +11 -0
- synkit/CRN/Visualize/labels.py +113 -0
- synkit/CRN/Visualize/layout.py +708 -0
- synkit/CRN/Visualize/palette.py +292 -0
- synkit/CRN/Visualize/validation.py +122 -0
- synkit/CRN/Visualize/vis.py +1152 -0
- synkit/CRN/__init__.py +233 -0
- synkit/CRN/_build.py +50 -0
- synkit/CRN/_native_build.py +175 -0
- synkit/CRN/analysis.py +629 -0
- synkit/CRN/backend.py +74 -0
- synkit/CRN/cpp/CMakeLists.txt +58 -0
- synkit/CRN/cpp/algebra.cpp +46 -0
- synkit/CRN/cpp/closure.cpp +29 -0
- synkit/CRN/cpp/exact.h +55 -0
- synkit/CRN/cpp/pathway.cpp +46 -0
- synkit/CRN/cpp/semiflows.cpp +209 -0
- synkit/CRN/cpp/structural.cpp +240 -0
- synkit/CRN/cpp/symmetry.cpp +45 -0
- synkit/CRN/cpp/write_manifest.cmake +28 -0
- synkit/CRN/kinds.py +137 -0
- synkit/CRN/lib/synkit_crn.dll +0 -0
- synkit/CRN/lib/synkit_crn.json +33 -0
- synkit/CRN/native.py +388 -0
- synkit/CRN/native_analysis.py +459 -0
- synkit/Chem/Cluster/__init__.py +0 -0
- synkit/Chem/Cluster/butina.py +140 -0
- synkit/Chem/Fingerprint/__init__.py +0 -0
- synkit/Chem/Fingerprint/fp_calculator.py +155 -0
- synkit/Chem/Fingerprint/smiles_featurizer.py +258 -0
- synkit/Chem/Fingerprint/transformation_fp.py +139 -0
- synkit/Chem/Mapper/README.rst +43 -0
- synkit/Chem/Mapper/__init__.py +255 -0
- synkit/Chem/Mapper/_compat.py +130 -0
- synkit/Chem/Mapper/adapters/__init__.py +1 -0
- synkit/Chem/Mapper/adapters/prediction.py +126 -0
- synkit/Chem/Mapper/adapters/template.py +412 -0
- synkit/Chem/Mapper/chem/__init__.py +52 -0
- synkit/Chem/Mapper/chem/aam.py +673 -0
- synkit/Chem/Mapper/chem/blind.py +160 -0
- synkit/Chem/Mapper/chem/its.py +309 -0
- synkit/Chem/Mapper/chem/smiles.py +564 -0
- synkit/Chem/Mapper/graph/__init__.py +66 -0
- synkit/Chem/Mapper/graph/automorphism.py +395 -0
- synkit/Chem/Mapper/graph/blockcut.py +98 -0
- synkit/Chem/Mapper/graph/labeled_graph.py +104 -0
- synkit/Chem/Mapper/graph/refine.py +176 -0
- synkit/Chem/Mapper/graph/synkit_adapter.py +110 -0
- synkit/Chem/Mapper/io/__init__.py +13 -0
- synkit/Chem/Mapper/io/idxmapstr.py +82 -0
- synkit/Chem/Mapper/mapper.py +685 -0
- synkit/Chem/Mapper/reaction_search.py +554 -0
- synkit/Chem/Mapper/reporting/__init__.py +1 -0
- synkit/Chem/Mapper/reporting/alternatives.py +451 -0
- synkit/Chem/Mapper/reporting/analysis.py +681 -0
- synkit/Chem/Mapper/reporting/native_analysis.py +485 -0
- synkit/Chem/Mapper/reporting/spectrum.py +642 -0
- synkit/Chem/Mapper/solvers/__init__.py +1 -0
- synkit/Chem/Mapper/solvers/exact/__init__.py +141 -0
- synkit/Chem/Mapper/solvers/exact/assignment/__init__.py +1 -0
- synkit/Chem/Mapper/solvers/exact/assignment/assignment_certificate.py +113 -0
- synkit/Chem/Mapper/solvers/exact/assignment/assignment_domains.py +162 -0
- synkit/Chem/Mapper/solvers/exact/assignment/assignment_verification.py +58 -0
- synkit/Chem/Mapper/solvers/exact/assignment/incremental_assignment.py +247 -0
- synkit/Chem/Mapper/solvers/exact/certificate.py +128 -0
- synkit/Chem/Mapper/solvers/exact/core.py +183 -0
- synkit/Chem/Mapper/solvers/exact/distance_search/__init__.py +1 -0
- synkit/Chem/Mapper/solvers/exact/distance_search/bounds.py +319 -0
- synkit/Chem/Mapper/solvers/exact/distance_search/exhaustive.py +179 -0
- synkit/Chem/Mapper/solvers/exact/distance_search/inputs.py +283 -0
- synkit/Chem/Mapper/solvers/exact/distance_search/records.py +242 -0
- synkit/Chem/Mapper/solvers/exact/distance_search/search.py +1025 -0
- synkit/Chem/Mapper/solvers/exact/distance_search/verify.py +378 -0
- synkit/Chem/Mapper/solvers/exact/edit_support.py +375 -0
- synkit/Chem/Mapper/solvers/exact/hybrid.py +224 -0
- synkit/Chem/Mapper/solvers/exact/hydrogen.py +264 -0
- synkit/Chem/Mapper/solvers/exact/kernel_solvers/__init__.py +1 -0
- synkit/Chem/Mapper/solvers/exact/kernel_solvers/branching.py +499 -0
- synkit/Chem/Mapper/solvers/exact/kernel_solvers/enumerate.py +1050 -0
- synkit/Chem/Mapper/solvers/exact/kernel_solvers/enumeration_result.py +30 -0
- synkit/Chem/Mapper/solvers/exact/kernel_solvers/kernel.py +256 -0
- synkit/Chem/Mapper/solvers/exact/kernel_solvers/milp.py +194 -0
- synkit/Chem/Mapper/solvers/exact/native/__init__.py +1 -0
- synkit/Chem/Mapper/solvers/exact/native/build.py +154 -0
- synkit/Chem/Mapper/solvers/exact/native/candidates.py +593 -0
- synkit/Chem/Mapper/solvers/exact/native/canonical.py +396 -0
- synkit/Chem/Mapper/solvers/exact/native/disk_frontier.py +415 -0
- synkit/Chem/Mapper/solvers/exact/native/frontier.py +390 -0
- synkit/Chem/Mapper/solvers/exact/native/hydrogen_quotient.py +213 -0
- synkit/Chem/Mapper/solvers/exact/native/its.py +528 -0
- synkit/Chem/Mapper/solvers/exact/native/native_distance.cpp +2803 -0
- synkit/Chem/Mapper/solvers/exact/native/orbit_aggregation.py +328 -0
- synkit/Chem/Mapper/solvers/exact/native/parallel.py +205 -0
- synkit/Chem/Mapper/solvers/exact/native/pmcd.py +203 -0
- synkit/Chem/Mapper/solvers/exact/native/runtime.py +40 -0
- synkit/Chem/Mapper/solvers/exact/native/search.py +294 -0
- synkit/Chem/Mapper/solvers/exact/native/separator.py +224 -0
- synkit/Chem/Mapper/solvers/exact/native_build.py +12 -0
- synkit/Chem/Mapper/solvers/exact/pabs/__init__.py +1 -0
- synkit/Chem/Mapper/solvers/exact/pabs/cost_lattice.py +43 -0
- synkit/Chem/Mapper/solvers/exact/pabs/factor_spectrum.py +110 -0
- synkit/Chem/Mapper/solvers/exact/pabs/multi_shell.py +368 -0
- synkit/Chem/Mapper/solvers/exact/pabs/propagation.py +679 -0
- synkit/Chem/Mapper/solvers/exact/pabs/propagation_bounds.py +436 -0
- synkit/Chem/Mapper/solvers/exact/pabs/propagation_limits.py +13 -0
- synkit/Chem/Mapper/solvers/exact/pabs/propagation_search.py +856 -0
- synkit/Chem/Mapper/solvers/exact/pabs/propagation_symmetry.py +66 -0
- synkit/Chem/Mapper/solvers/exact/pabs/residual_typed_bonds.py +136 -0
- synkit/Chem/Mapper/solvers/exact/pabs/reward_frontier.py +201 -0
- synkit/Chem/Mapper/solvers/exact/pabs/separator_bound.py +258 -0
- synkit/Chem/Mapper/solvers/exact/pabs/separator_spectrum.py +308 -0
- synkit/Chem/Mapper/solvers/exact/pabs/sparse_reward_bound.py +207 -0
- synkit/Chem/Mapper/solvers/exact/pabs/suffix_spectrum.py +299 -0
- synkit/Chem/Mapper/solvers/exact/search.py +55 -0
- synkit/Chem/Mapper/solvers/exact/seeds/__init__.py +1 -0
- synkit/Chem/Mapper/solvers/exact/seeds/seed.py +104 -0
- synkit/Chem/Mapper/solvers/exact/seeds/seed_fragments.py +148 -0
- synkit/Chem/Mapper/solvers/exact/seeds/seed_relaxation.py +181 -0
- synkit/Chem/Mapper/solvers/exact/symmetry.py +223 -0
- synkit/Chem/Mapper/solvers/native/__init__.py +4 -0
- synkit/Chem/Mapper/solvers/native/assignment.py +99 -0
- synkit/Chem/Mapper/solvers/native/astar.py +22 -0
- synkit/Chem/Mapper/solvers/native/build.py +385 -0
- synkit/Chem/Mapper/solvers/native/classification.py +641 -0
- synkit/Chem/Mapper/solvers/native/core.py +378 -0
- synkit/Chem/Mapper/solvers/native/cpp/CMakeLists.txt +120 -0
- synkit/Chem/Mapper/solvers/native/cpp/examples/map_reaction.cpp +29 -0
- synkit/Chem/Mapper/solvers/native/cpp/examples/slap_graph.cpp +63 -0
- synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/assignment/assignment.h +18 -0
- synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/astar/astar.h +18 -0
- synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/astar/bound.h +33 -0
- synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/core/pmcd_witness.h +16 -0
- synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/core/runtime.h +130 -0
- synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/hydrogen/hydrogen.h +73 -0
- synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/mapper.h +175 -0
- synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/mces/mces.h +9 -0
- synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/pabs/seed.h +34 -0
- synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/reaction/reaction.h +30 -0
- synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/sepdp/sepdp.h +21 -0
- synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/slap.h +72 -0
- synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/solvers/solvers.h +16 -0
- synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/symmetry/classification.h +140 -0
- synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/symmetry/cpabs.h +33 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/assignment/abb.cpp +302 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/astar/CMakeLists.txt +12 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/astar/README.md +47 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/astar/astar.cpp +228 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/astar/astar_graph.cpp +37 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/astar/bound.cpp +212 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/core/dispatch.cpp +272 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/core/pmcd_witness.cpp +36 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/hydrogen/hydrogen.cpp +380 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/mces/README.md +57 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/mces/mces.cpp +287 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/pabs/bridge.cpp +334 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/pabs/chemical_floor.inc +181 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/pabs/endpoint_symmetry.inc +104 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/pabs/exact_helpers.h +20 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/pabs/lp_assignment_certificate.h +165 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/pabs/lp_certificate.h +125 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/pabs/lp_model.h +365 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/pabs/lp_provider.inc +319 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/pabs/lp_rounding.inc +170 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/pabs/lp_test_hooks.h +342 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/pabs/objective_reduction.inc +137 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/pabs/overlap_dual.inc +351 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/pabs/profile_seed.inc +214 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/pabs/seed_heuristics.inc +640 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/pabs/sparse_embedding.cpp +430 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/pabs/sparse_root_lp.h +176 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/pabs/test_hooks.inc +262 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/reaction/reaction.cpp +611 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/sepdp/README.md +61 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/sepdp/sepdp.cpp +253 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/slap.cpp +810 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/solvers/formulation.h +138 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/solvers/maxsat.cpp +526 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/solvers/ortools.cpp +475 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/solvers/seed.h +80 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/solvers/weighted_cnf.h +256 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/supporting/README.md +43 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/supporting/dfs.cpp +123 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/supporting/edit_support.cpp +224 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/classification.cpp +553 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/classification_pipeline.cpp +124 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/classification_stream.h +13 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/cpabs.cpp +455 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/edge_deck.cpp +743 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/COPYRIGHT +44 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/LICENSE-2.0.txt +202 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/naugraph.c +732 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/naurng.c +94 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/naurng.h +49 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/nausparse.c +1771 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/nausparse.h +133 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/nautil.c +750 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/naututil.h +355 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/nauty.c +1256 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/nauty.h +1544 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/schreier.c +1131 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/schreier.h +70 -0
- synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/sorttemplates.c +580 -0
- synkit/Chem/Mapper/solvers/native/hydrogen.py +711 -0
- synkit/Chem/Mapper/solvers/native/implicit.py +506 -0
- synkit/Chem/Mapper/solvers/native/implicit_certificate.py +187 -0
- synkit/Chem/Mapper/solvers/native/implicit_preparation.py +309 -0
- synkit/Chem/Mapper/solvers/native/lib/synkit_mapper.dll +0 -0
- synkit/Chem/Mapper/solvers/native/mces.py +16 -0
- synkit/Chem/Mapper/solvers/native/reaction.py +179 -0
- synkit/Chem/Mapper/solvers/native/reaction_build.py +277 -0
- synkit/Chem/Mapper/solvers/native/reaction_text.py +26 -0
- synkit/Chem/Mapper/solvers/native/runtime.py +242 -0
- synkit/Chem/Mapper/solvers/native/sepdp.py +47 -0
- synkit/Chem/Mapper/solvers/native/slap.py +266 -0
- synkit/Chem/Mapper/solvers/native/solvers.py +67 -0
- synkit/Chem/Mapper/solvers/native/stereo.py +86 -0
- synkit/Chem/Mapper/solvers/native/supporting.py +34 -0
- synkit/Chem/Mapper/solvers/native/symmetry.py +91 -0
- synkit/Chem/Mapper/solvers/slap/__init__.py +29 -0
- synkit/Chem/Mapper/solvers/slap/lap.py +174 -0
- synkit/Chem/Mapper/solvers/slap/sequential.py +696 -0
- synkit/Chem/Mapper/validation/__init__.py +1 -0
- synkit/Chem/Mapper/validation/aam_validator.py +442 -0
- synkit/Chem/Mapper/validation/annotations.py +274 -0
- synkit/Chem/Mapper/validation/bonds.py +124 -0
- synkit/Chem/Mapper/validation/cohorts.py +72 -0
- synkit/Chem/Mapper/validation/identifiability.py +150 -0
- synkit/Chem/Mapper/validation/numerical_scope.py +36 -0
- synkit/Chem/Mapper/validation/orbits.py +137 -0
- synkit/Chem/Molecule/__init__.py +0 -0
- synkit/Chem/Molecule/_chirality_loci.py +165 -0
- synkit/Chem/Molecule/_chirality_types.py +231 -0
- synkit/Chem/Molecule/_helical_loci.py +167 -0
- synkit/Chem/Molecule/_stereo_axis_evidence.py +202 -0
- synkit/Chem/Molecule/_stereo_element_detection.py +193 -0
- synkit/Chem/Molecule/_stereo_orientation_constraints.py +511 -0
- synkit/Chem/Molecule/_tetrahedral_carriers.py +63 -0
- synkit/Chem/Molecule/atom_features.py +379 -0
- synkit/Chem/Molecule/chirality.py +1041 -0
- synkit/Chem/Molecule/cip_assignment.py +569 -0
- synkit/Chem/Molecule/cip_ranking.py +843 -0
- synkit/Chem/Molecule/coordinate_stereo.py +503 -0
- synkit/Chem/Molecule/derived_stereo_naming.py +165 -0
- synkit/Chem/Molecule/descriptors.py +452 -0
- synkit/Chem/Molecule/formula.py +249 -0
- synkit/Chem/Molecule/global_stereo.py +175 -0
- synkit/Chem/Molecule/graph_annotator.py +300 -0
- synkit/Chem/Molecule/standardize.py +817 -0
- synkit/Chem/Molecule/stereo_evidence.py +179 -0
- synkit/Chem/Molecule/stereo_perception.py +1032 -0
- synkit/Chem/Molecule/valence.py +123 -0
- synkit/Chem/Reaction/Mapper/__init__.py +44 -0
- synkit/Chem/Reaction/__init__.py +20 -0
- synkit/Chem/Reaction/aam_validator.py +5 -0
- synkit/Chem/Reaction/balance_check.py +138 -0
- synkit/Chem/Reaction/canon_rsmi.py +249 -0
- synkit/Chem/Reaction/cleaning.py +58 -0
- synkit/Chem/Reaction/deionize.py +199 -0
- synkit/Chem/Reaction/explicit_h_audit.py +232 -0
- synkit/Chem/Reaction/fix_aam.py +64 -0
- synkit/Chem/Reaction/neutralize.py +207 -0
- synkit/Chem/Reaction/radical_wildcard.py +224 -0
- synkit/Chem/Reaction/standardize.py +203 -0
- synkit/Chem/Reaction/tautomerize.py +205 -0
- synkit/Chem/__init__.py +1 -0
- synkit/Chem/utils.py +315 -0
- synkit/Data/__init__.py +0 -0
- synkit/Data/aldol.json.gz +1 -0
- synkit/Data/gen_partial_aam.py +147 -0
- synkit/Data/paracetamol.json.gz +1 -0
- synkit/Graph/Canon/__init__.py +16 -0
- synkit/Graph/Canon/_native/__init__.py +13 -0
- synkit/Graph/Canon/_native/build.py +93 -0
- synkit/Graph/Canon/_native/lib/synkit_canon.dll +0 -0
- synkit/Graph/Canon/_native/prepare.py +49 -0
- synkit/Graph/Canon/_native/runtime.py +140 -0
- synkit/Graph/Canon/_native/search.py +137 -0
- synkit/Graph/Canon/canon_algs.py +228 -0
- synkit/Graph/Canon/canon_graph.py +531 -0
- synkit/Graph/Canon/cpp/CMakeLists.txt +42 -0
- synkit/Graph/Canon/cpp/README.md +123 -0
- synkit/Graph/Canon/cpp/examples/canonical_graph.cpp +16 -0
- synkit/Graph/Canon/cpp/include/synkit_canon/canon.h +85 -0
- synkit/Graph/Canon/cpp/src/canonical.cpp +391 -0
- synkit/Graph/Canon/cpp/src/symmetry.h +83 -0
- synkit/Graph/Canon/cpp/tests/symmetry.cpp +93 -0
- synkit/Graph/Canon/exact.py +1020 -0
- synkit/Graph/Canon/exact_refinement.py +368 -0
- synkit/Graph/Canon/nauty.py +143 -0
- synkit/Graph/Context/__init__.py +0 -0
- synkit/Graph/Context/hier_context.py +244 -0
- synkit/Graph/Context/radius_expand.py +252 -0
- synkit/Graph/FG/__init__.py +19 -0
- synkit/Graph/FG/api.py +39 -0
- synkit/Graph/FG/audit.py +155 -0
- synkit/Graph/FG/catalog.py +919 -0
- synkit/Graph/FG/detector.py +288 -0
- synkit/Graph/FG/model.py +97 -0
- synkit/Graph/FG/recognizers.py +274 -0
- synkit/Graph/FG/ring_system.py +154 -0
- synkit/Graph/Feature/Descriptors/topology.py +857 -0
- synkit/Graph/Feature/Fingerprint/__init__.py +0 -0
- synkit/Graph/Feature/Fingerprint/wl_rxn_fps.py +222 -0
- synkit/Graph/Feature/__init__.py +5 -0
- synkit/Graph/Feature/graph_descriptors.py +323 -0
- synkit/Graph/Feature/graph_fps.py +96 -0
- synkit/Graph/Feature/graph_signature.py +239 -0
- synkit/Graph/Feature/hash_fps.py +133 -0
- synkit/Graph/Feature/morgan_fps.py +88 -0
- synkit/Graph/Feature/path_fps.py +81 -0
- synkit/Graph/Feature/wl_hash.py +134 -0
- synkit/Graph/Fusion/__init__.py +85 -0
- synkit/Graph/Fusion/candidate.py +559 -0
- synkit/Graph/Fusion/constructor.py +877 -0
- synkit/Graph/Fusion/identity.py +184 -0
- synkit/Graph/Fusion/interface.py +700 -0
- synkit/Graph/Fusion/search.py +241 -0
- synkit/Graph/Hyrogen/__init__.py +6 -0
- synkit/Graph/Hyrogen/_misc.py +591 -0
- synkit/Graph/Hyrogen/hcomplete.py +995 -0
- synkit/Graph/Hyrogen/hcomplete_legacy.py +368 -0
- synkit/Graph/Hyrogen/hcompletion_algorithms.py +713 -0
- synkit/Graph/Hyrogen/hextend.py +570 -0
- synkit/Graph/Hyrogen/hextend_legacy.py +215 -0
- synkit/Graph/ITS/__init__.py +4 -0
- synkit/Graph/ITS/its_builder.py +120 -0
- synkit/Graph/ITS/its_construction.py +719 -0
- synkit/Graph/ITS/its_decompose.py +572 -0
- synkit/Graph/ITS/its_destruction.py +331 -0
- synkit/Graph/ITS/its_expand.py +1251 -0
- synkit/Graph/ITS/its_relabel.py +187 -0
- synkit/Graph/ITS/its_reverter.py +356 -0
- synkit/Graph/ITS/normalize_aam.py +145 -0
- synkit/Graph/ITS/partial_its.py +238 -0
- synkit/Graph/ITS/rc_extractor.py +656 -0
- synkit/Graph/ITS/stereo.py +74 -0
- synkit/Graph/MTG/__init__.py +107 -0
- synkit/Graph/MTG/gluing.py +451 -0
- synkit/Graph/MTG/history.py +928 -0
- synkit/Graph/MTG/inventory.py +118 -0
- synkit/Graph/MTG/local.py +447 -0
- synkit/Graph/MTG/mcs_matcher.py +246 -0
- synkit/Graph/MTG/mtg.py +611 -0
- synkit/Graph/MTG/mtg_explore.py +74 -0
- synkit/Graph/MTG/observation.py +209 -0
- synkit/Graph/MTG/process.py +577 -0
- synkit/Graph/MTG/queries.py +66 -0
- synkit/Graph/MTG/realization.py +95 -0
- synkit/Graph/MTG/series_parallel.py +107 -0
- synkit/Graph/MTG/utils.py +424 -0
- synkit/Graph/Matcher/__init__.py +10 -0
- synkit/Graph/Matcher/approx_mcs.py +1264 -0
- synkit/Graph/Matcher/auto_est.py +736 -0
- synkit/Graph/Matcher/automorphism.py +314 -0
- synkit/Graph/Matcher/batch_cluster.py +224 -0
- synkit/Graph/Matcher/dedup_matches.py +247 -0
- synkit/Graph/Matcher/graph_cluster.py +191 -0
- synkit/Graph/Matcher/graph_matcher.py +312 -0
- synkit/Graph/Matcher/graph_morphism.py +421 -0
- synkit/Graph/Matcher/mcs_matcher.py +804 -0
- synkit/Graph/Matcher/multi_turbo_iso.py +186 -0
- synkit/Graph/Matcher/orbit.py +284 -0
- synkit/Graph/Matcher/partial_matcher.py +694 -0
- synkit/Graph/Matcher/sing.py +544 -0
- synkit/Graph/Matcher/subgraph_matcher.py +812 -0
- synkit/Graph/Matcher/turbo_iso.py +272 -0
- synkit/Graph/Matcher/wl_sel.py +537 -0
- synkit/Graph/Mech/__init__.py +26 -0
- synkit/Graph/Mech/conversion.py +1466 -0
- synkit/Graph/Mech/conversion_debug.py +237 -0
- synkit/Graph/Mech/electron_accounting.py +200 -0
- synkit/Graph/Mech/lwg_editor.py +406 -0
- synkit/Graph/Mech/lwg_ops.py +313 -0
- synkit/Graph/Morphism/__init__.py +99 -0
- synkit/Graph/Morphism/constraints.py +460 -0
- synkit/Graph/Morphism/llg.py +999 -0
- synkit/Graph/Morphism/morphism.py +349 -0
- synkit/Graph/Morphism/stereo.py +258 -0
- synkit/Graph/Morphism/stereo_morphism.py +383 -0
- synkit/Graph/Morphism/stereo_morphism_helpers.py +957 -0
- synkit/Graph/Morphism/stereo_morphism_types.py +117 -0
- synkit/Graph/Stereo/__init__.py +376 -0
- synkit/Graph/Stereo/_descriptor_core.py +207 -0
- synkit/Graph/Stereo/_descriptor_serialization.py +138 -0
- synkit/Graph/Stereo/canonical.py +648 -0
- synkit/Graph/Stereo/changes.py +964 -0
- synkit/Graph/Stereo/composition.py +391 -0
- synkit/Graph/Stereo/configured.py +946 -0
- synkit/Graph/Stereo/couplings.py +245 -0
- synkit/Graph/Stereo/descriptors.py +1030 -0
- synkit/Graph/Stereo/enumeration.py +597 -0
- synkit/Graph/Stereo/extended_descriptors.py +406 -0
- synkit/Graph/Stereo/global_configured.py +152 -0
- synkit/Graph/Stereo/global_stereo.py +304 -0
- synkit/Graph/Stereo/identity.py +648 -0
- synkit/Graph/Stereo/interchange.py +496 -0
- synkit/Graph/Stereo/legacy.py +460 -0
- synkit/Graph/Stereo/matching.py +899 -0
- synkit/Graph/Stereo/native.py +154 -0
- synkit/Graph/Stereo/orbits.py +668 -0
- synkit/Graph/Stereo/outcomes.py +99 -0
- synkit/Graph/Stereo/pairwise.py +350 -0
- synkit/Graph/Stereo/rdkit_adapter.py +662 -0
- synkit/Graph/Stereo/rdkit_identity.py +118 -0
- synkit/Graph/Stereo/rdkit_resonance.py +140 -0
- synkit/Graph/Stereo/reaction_interchange.py +266 -0
- synkit/Graph/Stereo/semantics.py +296 -0
- synkit/Graph/Stereo/supports.py +257 -0
- synkit/Graph/Stereo/wire.py +339 -0
- synkit/Graph/Wildcard/__init__.py +0 -0
- synkit/Graph/Wildcard/fuse_graph.py +156 -0
- synkit/Graph/Wildcard/graph_wc.py +303 -0
- synkit/Graph/Wildcard/its_merge.py +857 -0
- synkit/Graph/Wildcard/radwc.py +114 -0
- synkit/Graph/Wildcard/wc_matcher.py +576 -0
- synkit/Graph/Wildcard/wildcard.py +225 -0
- synkit/Graph/__init__.py +17 -0
- synkit/Graph/canon_graph.py +14 -0
- synkit/Graph/syn_graph.py +199 -0
- synkit/Graph/utils.py +324 -0
- synkit/IO/__init__.py +4 -0
- synkit/IO/chem_converter.py +1059 -0
- synkit/IO/combinatorial/__init__.py +8 -0
- synkit/IO/combinatorial/gml_to_graph.py +223 -0
- synkit/IO/combinatorial/graph_to_gml.py +280 -0
- synkit/IO/combinatorial/graph_to_smarts.py +186 -0
- synkit/IO/combinatorial/smarts_expander.py +147 -0
- synkit/IO/combinatorial/smarts_generalizer.py +133 -0
- synkit/IO/combinatorial/smarts_to_graph.py +179 -0
- synkit/IO/conversion.py +42 -0
- synkit/IO/data_io.py +269 -0
- synkit/IO/data_process.py +143 -0
- synkit/IO/debug.py +73 -0
- synkit/IO/gml_to_nx.py +150 -0
- synkit/IO/graph_to_mol.py +199 -0
- synkit/IO/mol_to_graph.py +1105 -0
- synkit/IO/mol_to_graph_chemistry.py +545 -0
- synkit/IO/nx_to_gml.py +209 -0
- synkit/IO/smiles_to_id.py +123 -0
- synkit/Mechanism/__init__.py +151 -0
- synkit/Mechanism/adapters.py +136 -0
- synkit/Mechanism/audit.py +323 -0
- synkit/Mechanism/benchmark.py +638 -0
- synkit/Mechanism/compatibility.py +13 -0
- synkit/Mechanism/drawing.py +85 -0
- synkit/Mechanism/electron_models.py +725 -0
- synkit/Mechanism/equivalence.py +521 -0
- synkit/Mechanism/evidence.py +262 -0
- synkit/Mechanism/interchange.py +141 -0
- synkit/Mechanism/model.py +918 -0
- synkit/Mechanism/neighbor_stereo.py +312 -0
- synkit/Mechanism/radical_data.py +740 -0
- synkit/Mechanism/replay.py +986 -0
- synkit/Mechanism/schema.py +264 -0
- synkit/Mechanism/stereo_state.py +416 -0
- synkit/Mechanism/symbols.py +91 -0
- synkit/Rule/Apply/__init__.py +35 -0
- synkit/Rule/Apply/conditions.py +45 -0
- synkit/Rule/Apply/dpo.py +998 -0
- synkit/Rule/Apply/rule_matcher.py +218 -0
- synkit/Rule/Apply/validation.py +354 -0
- synkit/Rule/Compose/__init__.py +106 -0
- synkit/Rule/Compose/_identity.py +43 -0
- synkit/Rule/Compose/composition.py +976 -0
- synkit/Rule/Compose/laws.py +439 -0
- synkit/Rule/Compose/search.py +771 -0
- synkit/Rule/Compose/synrule.py +170 -0
- synkit/Rule/Modify/__init__.py +0 -0
- synkit/Rule/Modify/implict_rule.py +56 -0
- synkit/Rule/Modify/longest_path.py +92 -0
- synkit/Rule/Modify/molecule_rule.py +115 -0
- synkit/Rule/Modify/prune_templates.py +78 -0
- synkit/Rule/Modify/rule_utils.py +194 -0
- synkit/Rule/Modify/strip_rule.py +97 -0
- synkit/Rule/__init__.py +28 -0
- synkit/Rule/generic_stereo.py +884 -0
- synkit/Rule/stereo_identity.py +271 -0
- synkit/Rule/syn_rule.py +1090 -0
- synkit/Synthesis/MSR/__init__.py +0 -0
- synkit/Synthesis/MSR/path_finder.py +239 -0
- synkit/Synthesis/Metrics/__init__.py +0 -0
- synkit/Synthesis/Metrics/_base.py +52 -0
- synkit/Synthesis/Metrics/_plot.py +129 -0
- synkit/Synthesis/Metrics/_ranking.py +179 -0
- synkit/Synthesis/RBL/__init__.py +80 -0
- synkit/Synthesis/RBL/engine.py +1249 -0
- synkit/Synthesis/RBL/matching.py +652 -0
- synkit/Synthesis/RBL/overlap.py +336 -0
- synkit/Synthesis/RBL/pipeline.py +376 -0
- synkit/Synthesis/RBL/policy.py +139 -0
- synkit/Synthesis/RBL/proof.py +370 -0
- synkit/Synthesis/RBL/reaction.py +506 -0
- synkit/Synthesis/RBL/state.py +169 -0
- synkit/Synthesis/RBL/validation.py +1060 -0
- synkit/Synthesis/Reactor/__init__.py +60 -0
- synkit/Synthesis/Reactor/core/__init__.py +1 -0
- synkit/Synthesis/Reactor/core/engine.py +967 -0
- synkit/Synthesis/Reactor/core/product.py +621 -0
- synkit/Synthesis/Reactor/core/rewrite.py +561 -0
- synkit/Synthesis/Reactor/core/strategy.py +44 -0
- synkit/Synthesis/Reactor/matching/__init__.py +1 -0
- synkit/Synthesis/Reactor/matching/mixin.py +920 -0
- synkit/Synthesis/Reactor/matching/policy.py +345 -0
- synkit/Synthesis/Reactor/matching/symmetry.py +309 -0
- synkit/Synthesis/Reactor/output/__init__.py +1 -0
- synkit/Synthesis/Reactor/output/deduplication.py +703 -0
- synkit/Synthesis/Reactor/output/policy.py +16 -0
- synkit/Synthesis/Reactor/output/serialization.py +485 -0
- synkit/Synthesis/Reactor/output/structural.py +893 -0
- synkit/Synthesis/Reactor/stereo/__init__.py +1 -0
- synkit/Synthesis/Reactor/stereo/assignment.py +35 -0
- synkit/Synthesis/Reactor/stereo/mixin.py +780 -0
- synkit/Synthesis/Reactor/variants/__init__.py +1 -0
- synkit/Synthesis/Reactor/variants/imbalanced.py +180 -0
- synkit/Synthesis/Reactor/variants/partial.py +84 -0
- synkit/Synthesis/Reactor/workflow/__init__.py +1 -0
- synkit/Synthesis/Reactor/workflow/batch.py +550 -0
- synkit/Synthesis/Reactor/workflow/benchmark.py +143 -0
- synkit/Synthesis/Reactor/workflow/postprocess.py +267 -0
- synkit/Synthesis/Reactor/workflow/rule_filter.py +210 -0
- synkit/Synthesis/__init__.py +0 -0
- synkit/Synthesis/reactor_utils.py +316 -0
- synkit/Utils/__init__.py +0 -0
- synkit/Utils/utils.py +172 -0
- synkit/Vis/__init__.py +91 -0
- synkit/Vis/crn/__init__.py +40 -0
- synkit/Vis/epd/__init__.py +20 -0
- synkit/Vis/epd/arrows.py +425 -0
- synkit/Vis/epd/chem.py +197 -0
- synkit/Vis/epd/constants.py +83 -0
- synkit/Vis/epd/layout.py +283 -0
- synkit/Vis/epd/mapping.py +51 -0
- synkit/Vis/epd/models.py +136 -0
- synkit/Vis/epd/render.py +543 -0
- synkit/Vis/epd/utils.py +108 -0
- synkit/Vis/epd/visualizer.py +811 -0
- synkit/Vis/graph_visualizer.py +375 -0
- synkit/Vis/its/__init__.py +19 -0
- synkit/Vis/its/drawer.py +615 -0
- synkit/Vis/its_drawer.py +35 -0
- synkit/Vis/molecule/__init__.py +31 -0
- synkit/Vis/molecule/drawer.py +563 -0
- synkit/Vis/molecule/reaction.py +285 -0
- synkit/Vis/molecule_drawer.py +33 -0
- synkit/Vis/mtg/__init__.py +17 -0
- synkit/Vis/mtg/drawer.py +769 -0
- synkit/Vis/mtg_drawer.py +34 -0
- synkit/Vis/pdf_writer.py +146 -0
- synkit/Vis/reaction/__init__.py +19 -0
- synkit/Vis/reaction/rule.py +141 -0
- synkit/Vis/reaction/rxn.py +532 -0
- synkit/Vis/reaction_drawer.py +41 -0
- synkit/Vis/space/__init__.py +17 -0
- synkit/Vis/space/chemical.py +80 -0
- synkit/Vis/space/embedding.py +87 -0
- synkit/Vis/vis_synedu/Vis/__init__.py +7 -0
- synkit/Vis/vis_synedu/Vis/dpo.py +862 -0
- synkit/Vis/vis_synedu/rxn_vis.py +377 -0
- synkit/Vis/vis_synedu/vis.py +501 -0
- synkit/Vis/visual_drawer.py +215 -0
- synkit/Vis/visual_model.py +520 -0
- synkit/__init__.py +3 -0
- synkit/_native.py +91 -0
- synkit/examples.py +50 -0
- synkit/version.py +23 -0
- synkit-1.6.5.dist-info/METADATA +156 -0
- synkit-1.6.5.dist-info/RECORD +636 -0
- synkit-1.6.5.dist-info/WHEEL +4 -0
- synkit-1.6.5.dist-info/licenses/LICENSE +21 -0
- synkit-1.6.5.dist-info/licenses/synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/COPYRIGHT +44 -0
- 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
|
+
]
|