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