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,920 @@
|
|
|
1
|
+
"""Matching, automorphism, and mapping-deduplication behavior for SynReactor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from typing import Any, Dict, List, Mapping, Optional, Tuple
|
|
7
|
+
|
|
8
|
+
import networkx as nx
|
|
9
|
+
|
|
10
|
+
from synkit.Graph import has_wildcard_node, remove_wildcard_nodes
|
|
11
|
+
from synkit.Graph.Hyrogen._misc import h_to_implicit, has_XH
|
|
12
|
+
from synkit.Graph.Matcher.partial_matcher import PartialMatcher
|
|
13
|
+
from synkit.Graph.Matcher.subgraph_matcher import (
|
|
14
|
+
SubgraphSearchEngine,
|
|
15
|
+
resolve_template_match_attrs,
|
|
16
|
+
)
|
|
17
|
+
from synkit.IO import setup_logging
|
|
18
|
+
from ..stereo.assignment import StereoWildcardAssignmentLimitError
|
|
19
|
+
from .policy import (
|
|
20
|
+
contextual_electron_pattern_graph,
|
|
21
|
+
deduplicate_joint_rule_mappings,
|
|
22
|
+
has_heavy_cross_component_correlation,
|
|
23
|
+
)
|
|
24
|
+
from .symmetry import (
|
|
25
|
+
deduplicate_exact_pattern_mappings,
|
|
26
|
+
deduplicate_free_host_component_mappings,
|
|
27
|
+
deduplicate_port_witness_mappings,
|
|
28
|
+
deduplicate_pure_coupling_mappings,
|
|
29
|
+
)
|
|
30
|
+
from ..core.strategy import Strategy
|
|
31
|
+
|
|
32
|
+
NodeId = Any
|
|
33
|
+
MappingDict = Dict[NodeId, NodeId]
|
|
34
|
+
log = setup_logging(task_type="synreactor")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ReactorMatchingMixin:
|
|
38
|
+
"""Internal matching behavior shared by the SynReactor facade."""
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def _filter_radical_lower_bound(
|
|
42
|
+
mappings: List[MappingDict], pattern: nx.Graph, host: nx.Graph
|
|
43
|
+
) -> List[MappingDict]:
|
|
44
|
+
"""Keep mappings whose host provides at least pattern radical count.
|
|
45
|
+
|
|
46
|
+
Exact radical comparison remains the concrete-mechanism default. This
|
|
47
|
+
opt-in policy is for intentionally generalized rule templates only.
|
|
48
|
+
"""
|
|
49
|
+
return [
|
|
50
|
+
mapping
|
|
51
|
+
for mapping in mappings
|
|
52
|
+
if all(
|
|
53
|
+
int(host.nodes[host_node].get("radical", 0))
|
|
54
|
+
>= int(pattern.nodes[pattern_node].get("radical", 0))
|
|
55
|
+
for pattern_node, host_node in mapping.items()
|
|
56
|
+
)
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
# ------------------------------------------------------------------
|
|
60
|
+
# Mapping / ITS / SMARTS (computed once, cached) --------------------
|
|
61
|
+
# ------------------------------------------------------------------
|
|
62
|
+
@property
|
|
63
|
+
def mappings(self) -> List[MappingDict]:
|
|
64
|
+
"""Return unique sub‑graph mappings, optionally pruned via automorphisms."""
|
|
65
|
+
if self._mappings is None:
|
|
66
|
+
log.debug("Finding sub‑graph mappings (strategy=%s)", self.strategy)
|
|
67
|
+
full_pattern_graph = self._relative_pi_pattern_graph(self.rule.left.raw)
|
|
68
|
+
full_pattern_graph = contextual_electron_pattern_graph(
|
|
69
|
+
full_pattern_graph,
|
|
70
|
+
reaction_center=self.rule.rc.raw,
|
|
71
|
+
template_format=getattr(self.rule, "_format", None),
|
|
72
|
+
)
|
|
73
|
+
# Handle explicit‑H constraints
|
|
74
|
+
if has_XH(full_pattern_graph):
|
|
75
|
+
self._flag_pattern_has_explicit_H = True
|
|
76
|
+
full_pattern_graph = h_to_implicit(full_pattern_graph)
|
|
77
|
+
pattern_graph = full_pattern_graph
|
|
78
|
+
# Handle wildcard‑node patterns
|
|
79
|
+
if has_wildcard_node(pattern_graph):
|
|
80
|
+
pattern_graph = remove_wildcard_nodes(pattern_graph, inplace=False)
|
|
81
|
+
|
|
82
|
+
matching_host = self._matching_host_graph()
|
|
83
|
+
node_attrs, edge_attrs = resolve_template_match_attrs(pattern_graph)
|
|
84
|
+
if self.radical_policy in {"lower_bound", "ignore"}:
|
|
85
|
+
node_attrs = [attr for attr in node_attrs if attr != "radical"]
|
|
86
|
+
|
|
87
|
+
# --- Choose matcher ------------------------------------------------
|
|
88
|
+
if self.partial:
|
|
89
|
+
max_results = (
|
|
90
|
+
self.embed_threshold / 100 if self.embed_threshold else None
|
|
91
|
+
)
|
|
92
|
+
matcher = PartialMatcher(
|
|
93
|
+
host=matching_host,
|
|
94
|
+
pattern=pattern_graph,
|
|
95
|
+
node_attrs=node_attrs,
|
|
96
|
+
edge_attrs=edge_attrs,
|
|
97
|
+
strategy=Strategy.from_string(self.strategy),
|
|
98
|
+
threshold=self.embed_threshold,
|
|
99
|
+
pre_filter=self.embed_pre_filter,
|
|
100
|
+
max_results=max_results,
|
|
101
|
+
# WL colour classes are not automorphism orbits in
|
|
102
|
+
# general. Keep the exhaustive partial population here;
|
|
103
|
+
# exact rule/product quotients below remain authoritative.
|
|
104
|
+
prune_auto=False,
|
|
105
|
+
)
|
|
106
|
+
raw_maps = matcher.get_mappings()
|
|
107
|
+
else:
|
|
108
|
+
raw_maps = SubgraphSearchEngine.find_subgraph_mappings(
|
|
109
|
+
host=matching_host,
|
|
110
|
+
pattern=pattern_graph,
|
|
111
|
+
node_attrs=node_attrs,
|
|
112
|
+
edge_attrs=edge_attrs,
|
|
113
|
+
strategy=Strategy.from_string(self.strategy),
|
|
114
|
+
threshold=self.embed_threshold,
|
|
115
|
+
pre_filter=self.embed_pre_filter,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
raw_maps.sort(
|
|
119
|
+
key=lambda candidate: self._mapping_atom_map_alignment(
|
|
120
|
+
pattern_graph,
|
|
121
|
+
matching_host,
|
|
122
|
+
candidate,
|
|
123
|
+
),
|
|
124
|
+
reverse=True,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
stereo_substitutions = self._typed_stereo_wildcard_substitutions(
|
|
128
|
+
full_pattern_graph
|
|
129
|
+
)
|
|
130
|
+
certified_generic = bool(
|
|
131
|
+
self.rule.rc.raw.graph.get("generic_stereo_extraction")
|
|
132
|
+
)
|
|
133
|
+
raw_maps = self._expand_stereo_wildcard_mappings(
|
|
134
|
+
raw_maps,
|
|
135
|
+
full_pattern_graph,
|
|
136
|
+
matching_host,
|
|
137
|
+
substitutions=stereo_substitutions,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if self.radical_policy == "lower_bound":
|
|
141
|
+
raw_maps = self._filter_radical_lower_bound(
|
|
142
|
+
raw_maps, pattern_graph, matching_host
|
|
143
|
+
)
|
|
144
|
+
if self.stereo_mode in {"require", "strict"}:
|
|
145
|
+
from synkit.Graph.Stereo import candidate_mapping_stereo_matches
|
|
146
|
+
|
|
147
|
+
presence_mode = (
|
|
148
|
+
"require"
|
|
149
|
+
if self.stereo_mode == "strict" and self.rule.stereo_couplings
|
|
150
|
+
else self.stereo_mode
|
|
151
|
+
)
|
|
152
|
+
raw_maps = [
|
|
153
|
+
mapping
|
|
154
|
+
for mapping in raw_maps
|
|
155
|
+
if candidate_mapping_stereo_matches(
|
|
156
|
+
full_pattern_graph,
|
|
157
|
+
matching_host,
|
|
158
|
+
mapping,
|
|
159
|
+
mode=presence_mode,
|
|
160
|
+
unknown_policy=self.stereo_query_mode,
|
|
161
|
+
query_policies=self.rule.stereo_query_policies,
|
|
162
|
+
substitutions=stereo_substitutions,
|
|
163
|
+
semantics=self.stereo_semantics,
|
|
164
|
+
diagnostics=self._stereo_semantic_diagnostics,
|
|
165
|
+
morphism_issues=self._stereo_morphism_issues,
|
|
166
|
+
)
|
|
167
|
+
]
|
|
168
|
+
elif self.stereo_mode == "propagate":
|
|
169
|
+
if certified_generic and stereo_substitutions:
|
|
170
|
+
from synkit.Graph.Stereo import candidate_mapping_stereo_matches
|
|
171
|
+
|
|
172
|
+
raw_maps = [
|
|
173
|
+
mapping
|
|
174
|
+
for mapping in raw_maps
|
|
175
|
+
if candidate_mapping_stereo_matches(
|
|
176
|
+
full_pattern_graph,
|
|
177
|
+
matching_host,
|
|
178
|
+
mapping,
|
|
179
|
+
mode="propagate",
|
|
180
|
+
unknown_policy=self.stereo_query_mode,
|
|
181
|
+
query_policies=self.rule.stereo_query_policies,
|
|
182
|
+
substitutions=stereo_substitutions,
|
|
183
|
+
semantics=self.stereo_semantics,
|
|
184
|
+
diagnostics=self._stereo_semantic_diagnostics,
|
|
185
|
+
morphism_issues=self._stereo_morphism_issues,
|
|
186
|
+
)
|
|
187
|
+
]
|
|
188
|
+
exact_targets = self._noncovariant_propagation_targets()
|
|
189
|
+
if exact_targets:
|
|
190
|
+
from synkit.Graph.Stereo import candidate_mapping_stereo_matches
|
|
191
|
+
|
|
192
|
+
selective_pattern = full_pattern_graph.copy()
|
|
193
|
+
selective_pattern.graph["stereo_descriptors"] = {
|
|
194
|
+
key: self.rule.stereo_guards[key] for key in exact_targets
|
|
195
|
+
}
|
|
196
|
+
raw_maps = [
|
|
197
|
+
mapping
|
|
198
|
+
for mapping in raw_maps
|
|
199
|
+
if candidate_mapping_stereo_matches(
|
|
200
|
+
selective_pattern,
|
|
201
|
+
matching_host,
|
|
202
|
+
mapping,
|
|
203
|
+
mode="require",
|
|
204
|
+
unknown_policy="exact",
|
|
205
|
+
query_policies={key: "exact" for key in exact_targets},
|
|
206
|
+
semantics=self.stereo_semantics,
|
|
207
|
+
diagnostics=self._stereo_semantic_diagnostics,
|
|
208
|
+
)
|
|
209
|
+
]
|
|
210
|
+
|
|
211
|
+
if self.rule.stereo_couplings and self.stereo_mode != "ignore":
|
|
212
|
+
raw_maps = [
|
|
213
|
+
mapping
|
|
214
|
+
for mapping in raw_maps
|
|
215
|
+
if self._coupling_mapping_inputs_match(
|
|
216
|
+
matching_host,
|
|
217
|
+
mapping,
|
|
218
|
+
strict=self.stereo_mode == "strict",
|
|
219
|
+
)
|
|
220
|
+
]
|
|
221
|
+
|
|
222
|
+
stereo_sensitive = bool(
|
|
223
|
+
self.rule.stereo_guards
|
|
224
|
+
or self.rule.stereo_effects
|
|
225
|
+
or self.rule.stereo_outcomes
|
|
226
|
+
or self.rule.stereo_couplings
|
|
227
|
+
)
|
|
228
|
+
if len(raw_maps) < 2:
|
|
229
|
+
self._mappings = raw_maps
|
|
230
|
+
elif certified_generic:
|
|
231
|
+
# Certified generic rules retain the complete injective port
|
|
232
|
+
# assignment population. Exact product quotienting happens
|
|
233
|
+
# only after rewrite, where multiplicity and provenance can
|
|
234
|
+
# be preserved. A future stabilizer optimization must prove
|
|
235
|
+
# population equivalence before entering this branch.
|
|
236
|
+
self._mappings = raw_maps
|
|
237
|
+
log.debug(
|
|
238
|
+
"Certified generic stereo rule retained %d exhaustive mapping(s)",
|
|
239
|
+
len(raw_maps),
|
|
240
|
+
)
|
|
241
|
+
elif self.automorphism and not stereo_sensitive:
|
|
242
|
+
# Build transition roles only when a nontrivial mapping
|
|
243
|
+
# population can actually benefit from the exact quotient.
|
|
244
|
+
automorphism_pattern = self._automorphism_pattern_graph(pattern_graph)
|
|
245
|
+
components = [
|
|
246
|
+
frozenset(component)
|
|
247
|
+
for component in nx.connected_components(automorphism_pattern)
|
|
248
|
+
]
|
|
249
|
+
correlated = has_heavy_cross_component_correlation(
|
|
250
|
+
automorphism_pattern,
|
|
251
|
+
self.rule.rc.raw,
|
|
252
|
+
)
|
|
253
|
+
fixed_nodes = (
|
|
254
|
+
frozenset()
|
|
255
|
+
if correlated or len(components) < 2
|
|
256
|
+
else max(components, key=len)
|
|
257
|
+
)
|
|
258
|
+
self._mappings = deduplicate_joint_rule_mappings(
|
|
259
|
+
raw_maps,
|
|
260
|
+
automorphism_pattern,
|
|
261
|
+
self.rule.rc.raw,
|
|
262
|
+
node_attrs=self._automorphism_node_attrs(
|
|
263
|
+
automorphism_pattern,
|
|
264
|
+
node_attrs,
|
|
265
|
+
),
|
|
266
|
+
edge_attrs=edge_attrs,
|
|
267
|
+
fixed_nodes=fixed_nodes,
|
|
268
|
+
)
|
|
269
|
+
log.debug(
|
|
270
|
+
"Exact rule symmetry: %d → %d mapping(s)",
|
|
271
|
+
len(raw_maps),
|
|
272
|
+
len(self._mappings),
|
|
273
|
+
)
|
|
274
|
+
elif stereo_sensitive:
|
|
275
|
+
# Wildcard ports are existential stereo witnesses rather than
|
|
276
|
+
# rewrite resources. Once a witness has passed the typed
|
|
277
|
+
# stereo predicate, retain one per identical structural map.
|
|
278
|
+
# Exact host-component and coupling group actions then remove
|
|
279
|
+
# only jointly realizable symmetry variants.
|
|
280
|
+
stereo_pattern = self._stereo_automorphism_pattern_graph(pattern_graph)
|
|
281
|
+
stereo_attrs = self._automorphism_node_attrs(
|
|
282
|
+
stereo_pattern,
|
|
283
|
+
node_attrs,
|
|
284
|
+
)
|
|
285
|
+
if "_stereo_role" not in stereo_attrs:
|
|
286
|
+
stereo_attrs.append("_stereo_role")
|
|
287
|
+
port_unique = deduplicate_port_witness_mappings(
|
|
288
|
+
raw_maps,
|
|
289
|
+
stereo_pattern,
|
|
290
|
+
)
|
|
291
|
+
# Preserve the established application-provenance population
|
|
292
|
+
# for reactant-side stereo guards/destruction. Product-side
|
|
293
|
+
# formation is covariant under the decorated pattern action,
|
|
294
|
+
# so it can use the exact quotient before rewrite.
|
|
295
|
+
product_side_stereo = (
|
|
296
|
+
bool(self.rule.stereo_effects)
|
|
297
|
+
and not self.rule.stereo_guards
|
|
298
|
+
and not self.rule.stereo_couplings
|
|
299
|
+
and all(
|
|
300
|
+
change.before is None
|
|
301
|
+
for change in self.rule.stereo_effects.values()
|
|
302
|
+
)
|
|
303
|
+
)
|
|
304
|
+
pattern_unique = (
|
|
305
|
+
deduplicate_exact_pattern_mappings(
|
|
306
|
+
port_unique,
|
|
307
|
+
stereo_pattern,
|
|
308
|
+
node_attrs=stereo_attrs,
|
|
309
|
+
edge_attrs=edge_attrs,
|
|
310
|
+
)
|
|
311
|
+
if product_side_stereo
|
|
312
|
+
else port_unique
|
|
313
|
+
)
|
|
314
|
+
host_unique = deduplicate_free_host_component_mappings(
|
|
315
|
+
pattern_unique,
|
|
316
|
+
matching_host,
|
|
317
|
+
node_attrs=stereo_attrs,
|
|
318
|
+
edge_attrs=edge_attrs,
|
|
319
|
+
)
|
|
320
|
+
self._mappings = deduplicate_pure_coupling_mappings(
|
|
321
|
+
host_unique,
|
|
322
|
+
stereo_pattern,
|
|
323
|
+
self.rule,
|
|
324
|
+
node_attrs=stereo_attrs,
|
|
325
|
+
edge_attrs=edge_attrs,
|
|
326
|
+
)
|
|
327
|
+
log.debug(
|
|
328
|
+
"Stereo witness/exact symmetry: %d → %d → %d → %d "
|
|
329
|
+
"→ %d mapping(s)",
|
|
330
|
+
len(raw_maps),
|
|
331
|
+
len(port_unique),
|
|
332
|
+
len(pattern_unique),
|
|
333
|
+
len(host_unique),
|
|
334
|
+
len(self._mappings),
|
|
335
|
+
)
|
|
336
|
+
else:
|
|
337
|
+
self._mappings = raw_maps
|
|
338
|
+
|
|
339
|
+
log.info("%d mapping(s) discovered", len(self._mappings))
|
|
340
|
+
return self._mappings
|
|
341
|
+
|
|
342
|
+
def _expand_stereo_wildcard_mappings(
|
|
343
|
+
self,
|
|
344
|
+
mappings: List[MappingDict],
|
|
345
|
+
pattern: nx.Graph,
|
|
346
|
+
host: nx.Graph,
|
|
347
|
+
*,
|
|
348
|
+
substitutions: Mapping[NodeId, Any] | None = None,
|
|
349
|
+
) -> List[MappingDict]:
|
|
350
|
+
"""Bind wildcard stereo references to actual host neighbors.
|
|
351
|
+
|
|
352
|
+
Wildcard ligand nodes are omitted from structural matching, but a
|
|
353
|
+
tetrahedral descriptor still needs their concrete identities for
|
|
354
|
+
parity propagation. Only wildcards referenced by stored stereo state
|
|
355
|
+
are expanded; ordinary wildcard regions retain their legacy behavior.
|
|
356
|
+
"""
|
|
357
|
+
wildcard_nodes = {
|
|
358
|
+
node
|
|
359
|
+
for node, attrs in pattern.nodes(data=True)
|
|
360
|
+
if attrs.get("element") == "*"
|
|
361
|
+
}
|
|
362
|
+
if not mappings or not wildcard_nodes:
|
|
363
|
+
return mappings
|
|
364
|
+
|
|
365
|
+
referenced_maps = {
|
|
366
|
+
reference
|
|
367
|
+
for descriptor in self.rule.stereo_guards.values()
|
|
368
|
+
for reference in descriptor.atoms
|
|
369
|
+
if isinstance(reference, int)
|
|
370
|
+
}
|
|
371
|
+
referenced_maps.update(
|
|
372
|
+
reference
|
|
373
|
+
for change in self.rule.stereo_effects.values()
|
|
374
|
+
for descriptor in (change.before, change.after, change.transition)
|
|
375
|
+
if descriptor is not None
|
|
376
|
+
for reference in descriptor.atoms
|
|
377
|
+
if isinstance(reference, int)
|
|
378
|
+
)
|
|
379
|
+
by_map = self._nodes_by_atom_map(pattern)
|
|
380
|
+
stereo_wildcards = sorted(
|
|
381
|
+
(
|
|
382
|
+
by_map[atom_map]
|
|
383
|
+
for atom_map in referenced_maps
|
|
384
|
+
if atom_map in by_map and by_map[atom_map] in wildcard_nodes
|
|
385
|
+
),
|
|
386
|
+
key=repr,
|
|
387
|
+
)
|
|
388
|
+
if not stereo_wildcards:
|
|
389
|
+
return mappings
|
|
390
|
+
|
|
391
|
+
expanded: List[MappingDict] = []
|
|
392
|
+
assignment_count = 0
|
|
393
|
+
substitutions = substitutions or {}
|
|
394
|
+
from synkit.Graph.Morphism import (
|
|
395
|
+
StereoMorphismIssue,
|
|
396
|
+
StereoMorphismIssueCode,
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
def reject(
|
|
400
|
+
code: Any,
|
|
401
|
+
message: str,
|
|
402
|
+
wildcard: NodeId,
|
|
403
|
+
candidate: NodeId | None = None,
|
|
404
|
+
) -> None:
|
|
405
|
+
self._stereo_morphism_issues.append(
|
|
406
|
+
StereoMorphismIssue(
|
|
407
|
+
code,
|
|
408
|
+
message,
|
|
409
|
+
{
|
|
410
|
+
"source_node": repr(wildcard),
|
|
411
|
+
"target_node": repr(candidate),
|
|
412
|
+
},
|
|
413
|
+
)
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
for mapping in mappings:
|
|
417
|
+
partials = [dict(mapping)]
|
|
418
|
+
for wildcard in stereo_wildcards:
|
|
419
|
+
next_partials: List[MappingDict] = []
|
|
420
|
+
anchors = [
|
|
421
|
+
neighbor
|
|
422
|
+
for neighbor in pattern.neighbors(wildcard)
|
|
423
|
+
if neighbor not in wildcard_nodes
|
|
424
|
+
]
|
|
425
|
+
for partial in partials:
|
|
426
|
+
mapped_anchors = [
|
|
427
|
+
partial[anchor] for anchor in anchors if anchor in partial
|
|
428
|
+
]
|
|
429
|
+
if len(mapped_anchors) != len(anchors) or not mapped_anchors:
|
|
430
|
+
continue
|
|
431
|
+
candidates = set(host.neighbors(mapped_anchors[0]))
|
|
432
|
+
for anchor in mapped_anchors[1:]:
|
|
433
|
+
candidates &= set(host.neighbors(anchor))
|
|
434
|
+
candidates -= set(partial.values())
|
|
435
|
+
for candidate in sorted(candidates, key=repr):
|
|
436
|
+
constraint = substitutions.get(wildcard)
|
|
437
|
+
if constraint is not None:
|
|
438
|
+
owner = constraint.owner
|
|
439
|
+
owner_node = (
|
|
440
|
+
owner if owner in pattern else by_map.get(owner)
|
|
441
|
+
)
|
|
442
|
+
target_owner = (
|
|
443
|
+
partial.get(owner_node)
|
|
444
|
+
if owner_node is not None
|
|
445
|
+
else None
|
|
446
|
+
)
|
|
447
|
+
if target_owner is None or not host.has_edge(
|
|
448
|
+
target_owner, candidate
|
|
449
|
+
):
|
|
450
|
+
reject(
|
|
451
|
+
StereoMorphismIssueCode.PORT_OWNER_MISMATCH,
|
|
452
|
+
"A typed stereo port owner is absent from the candidate mapping.",
|
|
453
|
+
wildcard,
|
|
454
|
+
candidate,
|
|
455
|
+
)
|
|
456
|
+
continue
|
|
457
|
+
attrs = host.nodes[candidate]
|
|
458
|
+
element = attrs.get("element")
|
|
459
|
+
virtual_kind = (
|
|
460
|
+
"H"
|
|
461
|
+
if element == "H"
|
|
462
|
+
else ("LP" if element in {"LP", "Lp", "lp"} else None)
|
|
463
|
+
)
|
|
464
|
+
concrete = {
|
|
465
|
+
"element": element,
|
|
466
|
+
"charge": attrs.get("charge", 0),
|
|
467
|
+
"radical": attrs.get("radical", 0),
|
|
468
|
+
"bond_order": host.edges[target_owner, candidate].get(
|
|
469
|
+
"order"
|
|
470
|
+
),
|
|
471
|
+
"owner": constraint.owner,
|
|
472
|
+
"side": "reactant",
|
|
473
|
+
"stereo_slot": constraint.stereo_slot,
|
|
474
|
+
"virtual_kind": virtual_kind,
|
|
475
|
+
"mapped_identity": attrs.get("atom_map", candidate),
|
|
476
|
+
"materialization": attrs.get(
|
|
477
|
+
"materialization",
|
|
478
|
+
(
|
|
479
|
+
"virtual"
|
|
480
|
+
if virtual_kind is not None
|
|
481
|
+
else "concrete"
|
|
482
|
+
),
|
|
483
|
+
),
|
|
484
|
+
"capacity": 1,
|
|
485
|
+
"resource_usage": 1,
|
|
486
|
+
}
|
|
487
|
+
if constraint.virtual_kind is not None and (
|
|
488
|
+
virtual_kind != constraint.virtual_kind
|
|
489
|
+
):
|
|
490
|
+
reject(
|
|
491
|
+
StereoMorphismIssueCode.PORT_VIRTUAL_KIND_MISMATCH,
|
|
492
|
+
"A stereo port candidate has the wrong virtual kind.",
|
|
493
|
+
wildcard,
|
|
494
|
+
candidate,
|
|
495
|
+
)
|
|
496
|
+
continue
|
|
497
|
+
if constraint.capacity < 1 or (
|
|
498
|
+
constraint.resource_budget is not None
|
|
499
|
+
and constraint.resource_budget < 1
|
|
500
|
+
):
|
|
501
|
+
reject(
|
|
502
|
+
StereoMorphismIssueCode.PORT_CAPACITY_MISMATCH,
|
|
503
|
+
"A stereo port has no materialization capacity.",
|
|
504
|
+
wildcard,
|
|
505
|
+
candidate,
|
|
506
|
+
)
|
|
507
|
+
continue
|
|
508
|
+
if not constraint.satisfies(concrete):
|
|
509
|
+
reject(
|
|
510
|
+
StereoMorphismIssueCode.PORT_DOMAIN_MISMATCH,
|
|
511
|
+
"A stereo port candidate violates its typed domain.",
|
|
512
|
+
wildcard,
|
|
513
|
+
candidate,
|
|
514
|
+
)
|
|
515
|
+
continue
|
|
516
|
+
branch = dict(partial)
|
|
517
|
+
branch[wildcard] = candidate
|
|
518
|
+
next_partials.append(branch)
|
|
519
|
+
partials = next_partials
|
|
520
|
+
assignment_count += len(partials)
|
|
521
|
+
if (
|
|
522
|
+
self.stereo_assignment_limit is not None
|
|
523
|
+
and assignment_count > self.stereo_assignment_limit
|
|
524
|
+
):
|
|
525
|
+
raise StereoWildcardAssignmentLimitError(
|
|
526
|
+
self.stereo_assignment_limit,
|
|
527
|
+
assignment_count,
|
|
528
|
+
)
|
|
529
|
+
expanded.extend(partials)
|
|
530
|
+
return expanded
|
|
531
|
+
|
|
532
|
+
@staticmethod
|
|
533
|
+
def _typed_stereo_wildcard_substitutions(
|
|
534
|
+
pattern: nx.Graph,
|
|
535
|
+
) -> Dict[NodeId, Any]:
|
|
536
|
+
"""Adapt explicitly typed stereo wildcard nodes at the Reactor boundary."""
|
|
537
|
+
from synkit.Graph.Morphism import (
|
|
538
|
+
NodeStateKind,
|
|
539
|
+
WildcardRole,
|
|
540
|
+
adapt_legacy_node_state,
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
substitutions: Dict[NodeId, Any] = {}
|
|
544
|
+
for node, attrs in pattern.nodes(data=True):
|
|
545
|
+
if attrs.get("element") not in {"*", ("*", "*")} or (
|
|
546
|
+
"wildcard_role" not in attrs
|
|
547
|
+
):
|
|
548
|
+
continue
|
|
549
|
+
state = adapt_legacy_node_state(attrs)
|
|
550
|
+
if (
|
|
551
|
+
state.kind is NodeStateKind.WILDCARD
|
|
552
|
+
and state.constraint is not None
|
|
553
|
+
and state.constraint.role is WildcardRole.STEREO_LIGAND_PORT
|
|
554
|
+
):
|
|
555
|
+
substitutions[node] = state.constraint
|
|
556
|
+
return substitutions
|
|
557
|
+
|
|
558
|
+
@staticmethod
|
|
559
|
+
def _nodes_by_atom_map(graph: nx.Graph) -> Dict[int, Any]:
|
|
560
|
+
"""Return graph node identifiers keyed by positive atom-map value."""
|
|
561
|
+
result: Dict[int, Any] = {}
|
|
562
|
+
for node, attrs in graph.nodes(data=True):
|
|
563
|
+
atom_map = attrs.get("atom_map", node)
|
|
564
|
+
if isinstance(atom_map, tuple) and atom_map:
|
|
565
|
+
atom_map = atom_map[0]
|
|
566
|
+
if isinstance(atom_map, int) and atom_map > 0:
|
|
567
|
+
if atom_map in result and result[atom_map] != node:
|
|
568
|
+
raise ValueError(f"Duplicate atom map {atom_map} in graph.")
|
|
569
|
+
result[atom_map] = node
|
|
570
|
+
return result
|
|
571
|
+
|
|
572
|
+
def _relative_pi_rewrite_edges(self, rc: nx.Graph) -> set[frozenset[Any]]:
|
|
573
|
+
"""Infer coupled bonds whose edit is one relative pi reduction.
|
|
574
|
+
|
|
575
|
+
This is intentionally restricted to declared vicinal additions. A
|
|
576
|
+
normal structural rule continues to match and write absolute bond
|
|
577
|
+
orders exactly as before.
|
|
578
|
+
"""
|
|
579
|
+
by_map = self._nodes_by_atom_map(rc)
|
|
580
|
+
result: set[frozenset[Any]] = set()
|
|
581
|
+
for coupling in self.rule.stereo_couplings.values():
|
|
582
|
+
if coupling.kind != "VICINAL_ADDITION":
|
|
583
|
+
continue
|
|
584
|
+
try:
|
|
585
|
+
left, right = (by_map[value] for value in coupling.centers)
|
|
586
|
+
except KeyError:
|
|
587
|
+
continue
|
|
588
|
+
if not rc.has_edge(left, right):
|
|
589
|
+
continue
|
|
590
|
+
attrs = rc.edges[left, right]
|
|
591
|
+
pi_order = attrs.get("pi_order")
|
|
592
|
+
sigma_order = attrs.get("sigma_order")
|
|
593
|
+
if not (
|
|
594
|
+
isinstance(pi_order, tuple)
|
|
595
|
+
and len(pi_order) == 2
|
|
596
|
+
and float(pi_order[0]) - float(pi_order[1]) == 1.0
|
|
597
|
+
and isinstance(sigma_order, tuple)
|
|
598
|
+
and len(sigma_order) == 2
|
|
599
|
+
and float(sigma_order[0]) == float(sigma_order[1]) == 1.0
|
|
600
|
+
):
|
|
601
|
+
continue
|
|
602
|
+
result.add(frozenset((left, right)))
|
|
603
|
+
return result
|
|
604
|
+
|
|
605
|
+
def _relative_pi_pattern_graph(self, pattern: nx.Graph) -> nx.Graph:
|
|
606
|
+
"""Decorate only inferred relative pi-addition query positions."""
|
|
607
|
+
relative_edges = self._relative_pi_rewrite_edges(self.rule.rc.raw)
|
|
608
|
+
if not relative_edges:
|
|
609
|
+
return pattern
|
|
610
|
+
decorated = pattern.copy()
|
|
611
|
+
for edge in relative_edges:
|
|
612
|
+
left, right = tuple(edge)
|
|
613
|
+
if not decorated.has_edge(left, right):
|
|
614
|
+
continue
|
|
615
|
+
attrs = decorated.edges[left, right]
|
|
616
|
+
minimum_pi = float(attrs.get("pi_order", 0.0))
|
|
617
|
+
if minimum_pi < 1.0:
|
|
618
|
+
continue
|
|
619
|
+
attrs["_minimum_pi_order"] = minimum_pi
|
|
620
|
+
for node in (left, right):
|
|
621
|
+
policies = dict(
|
|
622
|
+
decorated.nodes[node].get(
|
|
623
|
+
"_query_attribute_policies",
|
|
624
|
+
{},
|
|
625
|
+
)
|
|
626
|
+
)
|
|
627
|
+
policies["radical"] = "unknown"
|
|
628
|
+
decorated.nodes[node]["_query_attribute_policies"] = policies
|
|
629
|
+
return decorated
|
|
630
|
+
|
|
631
|
+
@staticmethod
|
|
632
|
+
def _automorphism_node_attrs(
|
|
633
|
+
pattern: nx.Graph,
|
|
634
|
+
node_attrs: List[str],
|
|
635
|
+
) -> List[str]:
|
|
636
|
+
"""Keep pruning at least as role-aware as the stored template data."""
|
|
637
|
+
attrs = list(node_attrs)
|
|
638
|
+
for attr in (
|
|
639
|
+
"aromatic",
|
|
640
|
+
"neighbors",
|
|
641
|
+
"_query_attribute_policies",
|
|
642
|
+
"_rewrite_role",
|
|
643
|
+
):
|
|
644
|
+
if attr not in attrs and any(
|
|
645
|
+
attr in data for _, data in pattern.nodes(data=True)
|
|
646
|
+
):
|
|
647
|
+
attrs.append(attr)
|
|
648
|
+
return attrs
|
|
649
|
+
|
|
650
|
+
def _automorphism_pattern_graph(self, pattern: nx.Graph) -> nx.Graph:
|
|
651
|
+
"""Decorate patterns with complete local rewrite roles for pruning.
|
|
652
|
+
|
|
653
|
+
Pattern connectivity contains only the source endpoint. Two source
|
|
654
|
+
atoms can therefore be symmetric even when the rule reconnects them
|
|
655
|
+
to different leaving groups on the other endpoint. Automorphism
|
|
656
|
+
pruning must include that transition incidence or it can discard the
|
|
657
|
+
only application that reconstructs the reference precursors.
|
|
658
|
+
"""
|
|
659
|
+
|
|
660
|
+
def freeze(value: Any) -> Any:
|
|
661
|
+
if isinstance(value, dict):
|
|
662
|
+
return tuple(sorted((key, freeze(item)) for key, item in value.items()))
|
|
663
|
+
if isinstance(value, (list, tuple)):
|
|
664
|
+
shallow = tuple(value)
|
|
665
|
+
try:
|
|
666
|
+
hash(shallow)
|
|
667
|
+
except TypeError:
|
|
668
|
+
return tuple(freeze(item) for item in value)
|
|
669
|
+
return shallow
|
|
670
|
+
if isinstance(value, set):
|
|
671
|
+
return tuple(sorted((freeze(item) for item in value), key=repr))
|
|
672
|
+
return value
|
|
673
|
+
|
|
674
|
+
def node_transition(attrs: Mapping[str, Any]) -> Any:
|
|
675
|
+
types = attrs.get("typesGH")
|
|
676
|
+
if isinstance(types, tuple) and len(types) == 2:
|
|
677
|
+
return tuple(
|
|
678
|
+
freeze(self._chemical_rewrite_role(side)) for side in types
|
|
679
|
+
)
|
|
680
|
+
return freeze(
|
|
681
|
+
tuple(
|
|
682
|
+
(key, attrs.get(key))
|
|
683
|
+
for key in (
|
|
684
|
+
"element",
|
|
685
|
+
"aromatic",
|
|
686
|
+
"hcount",
|
|
687
|
+
"charge",
|
|
688
|
+
"lone_pairs",
|
|
689
|
+
"radical",
|
|
690
|
+
"valence_electrons",
|
|
691
|
+
)
|
|
692
|
+
)
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
decorated = pattern.copy()
|
|
696
|
+
rc = self.rule.rc.raw
|
|
697
|
+
for node, attrs in decorated.nodes(data=True):
|
|
698
|
+
rc_attrs = rc.nodes.get(node)
|
|
699
|
+
if not rc_attrs:
|
|
700
|
+
continue
|
|
701
|
+
incidence = []
|
|
702
|
+
for neighbor in rc.neighbors(node):
|
|
703
|
+
edge = rc.edges[node, neighbor]
|
|
704
|
+
incidence.append(
|
|
705
|
+
(
|
|
706
|
+
node_transition(rc.nodes[neighbor]),
|
|
707
|
+
freeze(edge.get("order")),
|
|
708
|
+
freeze(edge.get("standard_order")),
|
|
709
|
+
)
|
|
710
|
+
)
|
|
711
|
+
attrs["_rewrite_role"] = (
|
|
712
|
+
node_transition(rc_attrs),
|
|
713
|
+
tuple(sorted(incidence, key=repr)),
|
|
714
|
+
)
|
|
715
|
+
return decorated
|
|
716
|
+
|
|
717
|
+
def _stereo_automorphism_pattern_graph(self, pattern: nx.Graph) -> nx.Graph:
|
|
718
|
+
"""Decorate pattern atoms by their ordered rule-descriptor roles.
|
|
719
|
+
|
|
720
|
+
Exact positions are deliberately conservative: an automorphism may
|
|
721
|
+
still exchange atoms absent from all stereo states (for example H2),
|
|
722
|
+
but cannot exchange two references whose permutation could invert a
|
|
723
|
+
stored descriptor.
|
|
724
|
+
"""
|
|
725
|
+
decorated = self._automorphism_pattern_graph(pattern)
|
|
726
|
+
by_map: Dict[int, Any] = {}
|
|
727
|
+
for node, attrs in decorated.nodes(data=True):
|
|
728
|
+
atom_map = attrs.get("atom_map", node)
|
|
729
|
+
if isinstance(atom_map, int):
|
|
730
|
+
by_map[atom_map] = node
|
|
731
|
+
|
|
732
|
+
roles: Dict[Any, List[Tuple[str, str, int]]] = defaultdict(list)
|
|
733
|
+
|
|
734
|
+
def role_position(descriptor: Any, position: int) -> int:
|
|
735
|
+
# Planar-bond identity permits reversal of the complete bond and
|
|
736
|
+
# simultaneous end swaps. Molecular connectivity/atom labels
|
|
737
|
+
# couple those moves, while a coarse center/reference role lets
|
|
738
|
+
# the legitimate whole-bond reversal survive automorphism.
|
|
739
|
+
if getattr(descriptor, "descriptor_class", None) == "planar_bond":
|
|
740
|
+
return -2 if position in {2, 3} else -1
|
|
741
|
+
# Tetrahedral odd/even permutation parity requires exact slots.
|
|
742
|
+
return position
|
|
743
|
+
|
|
744
|
+
for key, change in self.rule.stereo_effects.items():
|
|
745
|
+
for state_name, descriptor in (
|
|
746
|
+
("before", change.before),
|
|
747
|
+
("after", change.after),
|
|
748
|
+
("transition", change.transition),
|
|
749
|
+
):
|
|
750
|
+
if descriptor is None:
|
|
751
|
+
continue
|
|
752
|
+
for position, atom_map in enumerate(descriptor.atoms):
|
|
753
|
+
if isinstance(atom_map, int) and atom_map in by_map:
|
|
754
|
+
roles[by_map[atom_map]].append(
|
|
755
|
+
(key, state_name, role_position(descriptor, position))
|
|
756
|
+
)
|
|
757
|
+
for key, descriptor in self.rule.stereo_guards.items():
|
|
758
|
+
for position, atom_map in enumerate(descriptor.atoms):
|
|
759
|
+
if isinstance(atom_map, int) and atom_map in by_map:
|
|
760
|
+
roles[by_map[atom_map]].append(
|
|
761
|
+
(key, "guard", role_position(descriptor, position))
|
|
762
|
+
)
|
|
763
|
+
for node in decorated.nodes:
|
|
764
|
+
decorated.nodes[node]["_stereo_role"] = tuple(
|
|
765
|
+
sorted(set(roles.get(node, [])))
|
|
766
|
+
)
|
|
767
|
+
return decorated
|
|
768
|
+
|
|
769
|
+
@staticmethod
|
|
770
|
+
def _mapping_provenance(
|
|
771
|
+
mapping: MappingDict,
|
|
772
|
+
pattern: nx.Graph,
|
|
773
|
+
host: nx.Graph,
|
|
774
|
+
) -> Tuple[Tuple[Any, Any], ...]:
|
|
775
|
+
"""Return a deterministic template-map to substrate-map application trace."""
|
|
776
|
+
pairs = []
|
|
777
|
+
for pattern_node, host_node in mapping.items():
|
|
778
|
+
pattern_ref = pattern.nodes[pattern_node].get("atom_map", pattern_node)
|
|
779
|
+
host_ref = host.nodes[host_node].get("atom_map", host_node)
|
|
780
|
+
if not isinstance(pattern_ref, int) or pattern_ref <= 0:
|
|
781
|
+
pattern_ref = pattern_node
|
|
782
|
+
if not isinstance(host_ref, int) or host_ref <= 0:
|
|
783
|
+
host_ref = host_node
|
|
784
|
+
pairs.append((pattern_ref, host_ref))
|
|
785
|
+
return tuple(sorted(pairs, key=lambda pair: (repr(pair[0]), repr(pair[1]))))
|
|
786
|
+
|
|
787
|
+
@staticmethod
|
|
788
|
+
def _configuration_provenance(configuration: Any | None) -> Any:
|
|
789
|
+
"""Return deterministic JSON-compatible local-frame evidence."""
|
|
790
|
+
if configuration is None:
|
|
791
|
+
return None
|
|
792
|
+
shape, specification, frame = configuration.canonical_form()
|
|
793
|
+
return {
|
|
794
|
+
"shape": shape,
|
|
795
|
+
"specification": specification,
|
|
796
|
+
"canonical_frame": [repr(reference) for reference in frame],
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
def _stereo_application_provenance(
|
|
800
|
+
self,
|
|
801
|
+
pattern: nx.Graph,
|
|
802
|
+
host: nx.Graph,
|
|
803
|
+
mapping: MappingDict,
|
|
804
|
+
substitutions: Mapping[NodeId, Any],
|
|
805
|
+
) -> Dict[str, Any]:
|
|
806
|
+
"""Build replayable local stereo evidence for one accepted mapping."""
|
|
807
|
+
from synkit.Graph.Stereo import candidate_mapping_stereo_morphism
|
|
808
|
+
|
|
809
|
+
proof = candidate_mapping_stereo_morphism(
|
|
810
|
+
pattern,
|
|
811
|
+
host,
|
|
812
|
+
mapping,
|
|
813
|
+
mode=self.stereo_mode,
|
|
814
|
+
unknown_policy=self.stereo_query_mode,
|
|
815
|
+
query_policies=self.rule.stereo_query_policies,
|
|
816
|
+
substitutions=substitutions,
|
|
817
|
+
)
|
|
818
|
+
certificates = []
|
|
819
|
+
for certificate in proof.certificates:
|
|
820
|
+
relation = certificate.relation
|
|
821
|
+
witness = certificate.witness
|
|
822
|
+
certificates.append(
|
|
823
|
+
{
|
|
824
|
+
"layer": certificate.layer,
|
|
825
|
+
"status": certificate.status.value,
|
|
826
|
+
"information_policy": certificate.information_policy.value,
|
|
827
|
+
"source": self._configuration_provenance(
|
|
828
|
+
certificate.source_configuration
|
|
829
|
+
),
|
|
830
|
+
"target": self._configuration_provenance(
|
|
831
|
+
certificate.target_configuration
|
|
832
|
+
),
|
|
833
|
+
"relation": (
|
|
834
|
+
None
|
|
835
|
+
if relation is None
|
|
836
|
+
else {
|
|
837
|
+
"kind": relation.kind.value,
|
|
838
|
+
"shape": relation.shape,
|
|
839
|
+
"class_id": relation.class_id,
|
|
840
|
+
}
|
|
841
|
+
),
|
|
842
|
+
"witness": (
|
|
843
|
+
None if witness is None else tuple(witness.permutation.image)
|
|
844
|
+
),
|
|
845
|
+
}
|
|
846
|
+
)
|
|
847
|
+
return {
|
|
848
|
+
"presence_mode": proof.presence_mode.value,
|
|
849
|
+
"information_policy": proof.information_policy.value,
|
|
850
|
+
"certificates": certificates,
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
def _deduplicate_equivalent_free_components(
|
|
854
|
+
self,
|
|
855
|
+
mappings: List[MappingDict],
|
|
856
|
+
pattern: nx.Graph,
|
|
857
|
+
anchor: Optional[frozenset[NodeId]],
|
|
858
|
+
node_attrs: List[str],
|
|
859
|
+
edge_attrs: List[str],
|
|
860
|
+
) -> List[MappingDict]:
|
|
861
|
+
"""Collapse swaps of equivalent disconnected non-anchor tuple components."""
|
|
862
|
+
if getattr(self.rule, "_format", None) != "tuple" or len(mappings) < 2:
|
|
863
|
+
return mappings
|
|
864
|
+
|
|
865
|
+
anchor = anchor or frozenset()
|
|
866
|
+
free_components = [
|
|
867
|
+
frozenset(component)
|
|
868
|
+
for component in nx.connected_components(pattern)
|
|
869
|
+
if not set(component) & set(anchor)
|
|
870
|
+
]
|
|
871
|
+
if len(free_components) < 2:
|
|
872
|
+
return mappings
|
|
873
|
+
|
|
874
|
+
component_groups: List[List[frozenset[NodeId]]] = []
|
|
875
|
+
for component in free_components:
|
|
876
|
+
for group in component_groups:
|
|
877
|
+
if self._components_are_equivalent(
|
|
878
|
+
pattern,
|
|
879
|
+
component,
|
|
880
|
+
group[0],
|
|
881
|
+
node_attrs,
|
|
882
|
+
edge_attrs,
|
|
883
|
+
):
|
|
884
|
+
group.append(component)
|
|
885
|
+
break
|
|
886
|
+
else:
|
|
887
|
+
component_groups.append([component])
|
|
888
|
+
|
|
889
|
+
swappable_groups = [group for group in component_groups if len(group) > 1]
|
|
890
|
+
if not swappable_groups:
|
|
891
|
+
return mappings
|
|
892
|
+
|
|
893
|
+
swappable_nodes = set().union(
|
|
894
|
+
*[set().union(*group) for group in swappable_groups]
|
|
895
|
+
)
|
|
896
|
+
seen = set()
|
|
897
|
+
unique: List[MappingDict] = []
|
|
898
|
+
for mapping in mappings:
|
|
899
|
+
fixed = tuple(
|
|
900
|
+
sorted(
|
|
901
|
+
(node, host)
|
|
902
|
+
for node, host in mapping.items()
|
|
903
|
+
if node not in swappable_nodes
|
|
904
|
+
)
|
|
905
|
+
)
|
|
906
|
+
component_bags = tuple(
|
|
907
|
+
tuple(
|
|
908
|
+
sorted(
|
|
909
|
+
tuple(sorted(mapping[node] for node in component))
|
|
910
|
+
for component in group
|
|
911
|
+
)
|
|
912
|
+
)
|
|
913
|
+
for group in swappable_groups
|
|
914
|
+
)
|
|
915
|
+
signature = (fixed, component_bags)
|
|
916
|
+
if signature in seen:
|
|
917
|
+
continue
|
|
918
|
+
seen.add(signature)
|
|
919
|
+
unique.append(mapping)
|
|
920
|
+
return unique
|