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,703 @@
|
|
|
1
|
+
"""Exact structural and stereochemical ITS deduplication helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import Counter, defaultdict
|
|
6
|
+
from copy import deepcopy
|
|
7
|
+
from itertools import combinations
|
|
8
|
+
from typing import Any, Dict, List, Tuple
|
|
9
|
+
|
|
10
|
+
import networkx as nx
|
|
11
|
+
from networkx.algorithms.isomorphism import (
|
|
12
|
+
GraphMatcher,
|
|
13
|
+
categorical_edge_match,
|
|
14
|
+
categorical_node_match,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from synkit.Graph.ITS.its_reverter import ITSReverter
|
|
18
|
+
from ..core import product as _product_state
|
|
19
|
+
|
|
20
|
+
NodeId = Any
|
|
21
|
+
_PRIMITIVE_IDENTITY_TYPES = frozenset((str, int, float, bool, type(None)))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _freeze_typed_identity(
|
|
25
|
+
value: Any,
|
|
26
|
+
cache: Dict[int, Tuple[Any, Any]] | None = None,
|
|
27
|
+
) -> Any:
|
|
28
|
+
"""Return a hashable identity without conflating container types.
|
|
29
|
+
|
|
30
|
+
This encoding is used by authoritative exact structural comparisons.
|
|
31
|
+
Unlike the coarser invariant helper above, equal encodings imply equal
|
|
32
|
+
typed attribute trees. Numeric/container coercions can therefore never
|
|
33
|
+
cause a false graph merge.
|
|
34
|
+
"""
|
|
35
|
+
value_type = type(value)
|
|
36
|
+
if value_type in _PRIMITIVE_IDENTITY_TYPES:
|
|
37
|
+
# Primitive values need neither recursive dispatch nor identity-cache
|
|
38
|
+
# bookkeeping. The concrete type keeps bool/int/float distinct.
|
|
39
|
+
return value_type, value
|
|
40
|
+
|
|
41
|
+
cache_key = id(value)
|
|
42
|
+
if cache is not None:
|
|
43
|
+
cached = cache.get(cache_key)
|
|
44
|
+
if cached is not None and cached[0] is value:
|
|
45
|
+
return cached[1]
|
|
46
|
+
|
|
47
|
+
if isinstance(value, dict):
|
|
48
|
+
identity = (
|
|
49
|
+
dict,
|
|
50
|
+
frozenset(
|
|
51
|
+
(
|
|
52
|
+
_freeze_typed_identity(key, cache),
|
|
53
|
+
_freeze_typed_identity(item, cache),
|
|
54
|
+
)
|
|
55
|
+
for key, item in value.items()
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
elif isinstance(value, list):
|
|
59
|
+
item_type = type(value[0]) if value else None
|
|
60
|
+
if item_type in _PRIMITIVE_IDENTITY_TYPES and all(
|
|
61
|
+
type(item) is item_type for item in value
|
|
62
|
+
):
|
|
63
|
+
identity = (list, item_type, tuple(value))
|
|
64
|
+
else:
|
|
65
|
+
identity = (
|
|
66
|
+
list,
|
|
67
|
+
tuple(_freeze_typed_identity(item, cache) for item in value),
|
|
68
|
+
)
|
|
69
|
+
elif isinstance(value, tuple):
|
|
70
|
+
item_type = type(value[0]) if value else None
|
|
71
|
+
if item_type in _PRIMITIVE_IDENTITY_TYPES and all(
|
|
72
|
+
type(item) is item_type for item in value
|
|
73
|
+
):
|
|
74
|
+
# This is exactly the same typed equality as recursively encoding
|
|
75
|
+
# each primitive item, but avoids two Python calls for the common
|
|
76
|
+
# paired endpoint representation.
|
|
77
|
+
identity = (tuple, item_type, value)
|
|
78
|
+
else:
|
|
79
|
+
identity = (
|
|
80
|
+
tuple,
|
|
81
|
+
tuple(_freeze_typed_identity(item, cache) for item in value),
|
|
82
|
+
)
|
|
83
|
+
elif isinstance(value, set):
|
|
84
|
+
identity = (
|
|
85
|
+
set,
|
|
86
|
+
frozenset(_freeze_typed_identity(item, cache) for item in value),
|
|
87
|
+
)
|
|
88
|
+
else:
|
|
89
|
+
try:
|
|
90
|
+
hash(value)
|
|
91
|
+
except TypeError:
|
|
92
|
+
# Unknown mutable objects fail closed: only the same live object
|
|
93
|
+
# may compare equal here. This can retain an extra representative
|
|
94
|
+
# but cannot authorize a false merge from a lossy representation.
|
|
95
|
+
identity = (type(value), cache_key)
|
|
96
|
+
else:
|
|
97
|
+
# The concrete type is part of the identity, so Python's numeric
|
|
98
|
+
# coercions (for example True == 1 == 1.0) cannot merge labels.
|
|
99
|
+
identity = (type(value), value)
|
|
100
|
+
|
|
101
|
+
if cache is not None:
|
|
102
|
+
# Keep the object itself beside the result: an id can be reused only
|
|
103
|
+
# after its previous object dies, which cannot happen while retained
|
|
104
|
+
# by this quotient-local cache.
|
|
105
|
+
cache[cache_key] = (value, identity)
|
|
106
|
+
return identity
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _merge_application_orbits(
|
|
110
|
+
representative: nx.Graph,
|
|
111
|
+
members: List[nx.Graph],
|
|
112
|
+
) -> None:
|
|
113
|
+
"""Retain every application contributing to one exact product orbit."""
|
|
114
|
+
contributions = []
|
|
115
|
+
aggregate_weight = 0.0
|
|
116
|
+
aggregate_multiplicity = 0
|
|
117
|
+
for member in members:
|
|
118
|
+
aggregate_weight += float(
|
|
119
|
+
member.graph.get(
|
|
120
|
+
"stereo_aggregate_weight",
|
|
121
|
+
member.graph.get("stereo_branch_weight", 1.0),
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
aggregate_multiplicity += int(member.graph.get("stereo_branch_multiplicity", 1))
|
|
125
|
+
existing = member.graph.get("stereo_branch_contributions")
|
|
126
|
+
if existing is not None:
|
|
127
|
+
contributions.extend(deepcopy(existing))
|
|
128
|
+
continue
|
|
129
|
+
contributions.append(
|
|
130
|
+
{
|
|
131
|
+
"weight": float(member.graph.get("stereo_branch_weight", 1.0)),
|
|
132
|
+
"branch_path": deepcopy(member.graph.get("stereo_branch_path", ())),
|
|
133
|
+
"branch": deepcopy(member.graph.get("stereo_branch", {})),
|
|
134
|
+
"coupling_branch": deepcopy(
|
|
135
|
+
member.graph.get("stereo_coupling_branch", {})
|
|
136
|
+
),
|
|
137
|
+
"application": deepcopy(member.graph.get("application_provenance")),
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
representative.graph["stereo_aggregate_weight"] = aggregate_weight
|
|
141
|
+
representative.graph["stereo_branch_multiplicity"] = aggregate_multiplicity
|
|
142
|
+
representative.graph["stereo_branch_contributions"] = contributions
|
|
143
|
+
|
|
144
|
+
applications = []
|
|
145
|
+
for member in members:
|
|
146
|
+
orbit = member.graph.get("application_orbit")
|
|
147
|
+
if orbit is not None:
|
|
148
|
+
applications.extend(orbit.get("applications", ()))
|
|
149
|
+
continue
|
|
150
|
+
provenance = member.graph.get("application_provenance")
|
|
151
|
+
if provenance is not None:
|
|
152
|
+
applications.append(provenance)
|
|
153
|
+
if applications:
|
|
154
|
+
applications.sort(key=lambda value: value["application_index"])
|
|
155
|
+
representative.graph["application_orbit"] = {
|
|
156
|
+
"multiplicity": len(applications),
|
|
157
|
+
"applications": applications,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _chemical_rewrite_role(role: Any) -> Any:
|
|
162
|
+
"""Drop provenance-only atom-map identity from chemical rewrite roles."""
|
|
163
|
+
if isinstance(role, tuple) and len(role) >= 9:
|
|
164
|
+
chemical_role = role[:-1]
|
|
165
|
+
if chemical_role[0] == "H":
|
|
166
|
+
return chemical_role[:-1] + ((),)
|
|
167
|
+
return chemical_role
|
|
168
|
+
return role
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _rewrite_locus_nodes(reaction_center: nx.Graph) -> frozenset[Any]:
|
|
172
|
+
"""Return rule nodes whose placement can alter a non-stereo product."""
|
|
173
|
+
active = set()
|
|
174
|
+
node_state_keys = (
|
|
175
|
+
"element",
|
|
176
|
+
"aromatic",
|
|
177
|
+
"hcount",
|
|
178
|
+
"charge",
|
|
179
|
+
"radical",
|
|
180
|
+
"lone_pairs",
|
|
181
|
+
"valence_electrons",
|
|
182
|
+
"present",
|
|
183
|
+
)
|
|
184
|
+
for node, attrs in reaction_center.nodes(data=True):
|
|
185
|
+
types = attrs.get("typesGH")
|
|
186
|
+
if (
|
|
187
|
+
isinstance(types, tuple)
|
|
188
|
+
and len(types) == 2
|
|
189
|
+
and _chemical_rewrite_role(types[0]) != _chemical_rewrite_role(types[1])
|
|
190
|
+
):
|
|
191
|
+
active.add(node)
|
|
192
|
+
continue
|
|
193
|
+
if any(
|
|
194
|
+
isinstance(attrs.get(key), tuple)
|
|
195
|
+
and len(attrs[key]) == 2
|
|
196
|
+
and attrs[key][0] != attrs[key][1]
|
|
197
|
+
for key in node_state_keys
|
|
198
|
+
):
|
|
199
|
+
active.add(node)
|
|
200
|
+
|
|
201
|
+
edge_state_keys = (
|
|
202
|
+
"order",
|
|
203
|
+
"kekule_order",
|
|
204
|
+
"sigma_order",
|
|
205
|
+
"pi_order",
|
|
206
|
+
"standard_order",
|
|
207
|
+
)
|
|
208
|
+
for left, right, attrs in reaction_center.edges(data=True):
|
|
209
|
+
if any(
|
|
210
|
+
isinstance(attrs.get(key), tuple)
|
|
211
|
+
and len(attrs[key]) == 2
|
|
212
|
+
and attrs[key][0] != attrs[key][1]
|
|
213
|
+
for key in edge_state_keys
|
|
214
|
+
):
|
|
215
|
+
active.update((left, right))
|
|
216
|
+
return frozenset(active)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _mapping_quotient_input_supported(
|
|
220
|
+
mappings: List[Dict[Any, Any]],
|
|
221
|
+
host: nx.Graph,
|
|
222
|
+
active: frozenset[Any],
|
|
223
|
+
) -> bool:
|
|
224
|
+
"""Return whether the exact component quotient supports this input."""
|
|
225
|
+
return (
|
|
226
|
+
len(mappings) >= 2
|
|
227
|
+
and not host.is_multigraph()
|
|
228
|
+
and not host.graph.get("stereo_descriptors")
|
|
229
|
+
and bool(active)
|
|
230
|
+
and all(active.issubset(mapping) for mapping in mappings)
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _project_mapped_rewrite_locus(
|
|
235
|
+
mappings: List[Dict[Any, Any]],
|
|
236
|
+
reaction_center: nx.Graph,
|
|
237
|
+
active: frozenset[Any],
|
|
238
|
+
) -> frozenset[Any] | None:
|
|
239
|
+
"""Project explicit-H roles onto their anchored implicit heavy roles.
|
|
240
|
+
|
|
241
|
+
Explicit hydrogen nodes are deliberately absent from the first matching
|
|
242
|
+
pass. Their extensions remain equivariant under an attributed host
|
|
243
|
+
isomorphism when each omitted H is source-bound to an already mapped
|
|
244
|
+
rewrite-locus node; the anchored explicit-H search then supplies exactly
|
|
245
|
+
the corresponding fibers on both applications.
|
|
246
|
+
"""
|
|
247
|
+
if not mappings:
|
|
248
|
+
return None
|
|
249
|
+
common_domain = set.intersection(*(set(mapping) for mapping in mappings))
|
|
250
|
+
projected = frozenset(active & common_domain)
|
|
251
|
+
for node in active - projected:
|
|
252
|
+
element = reaction_center.nodes[node].get("element")
|
|
253
|
+
source_element = element[0] if isinstance(element, tuple) else element
|
|
254
|
+
if source_element != "H":
|
|
255
|
+
return None
|
|
256
|
+
anchored = False
|
|
257
|
+
for neighbor in reaction_center.neighbors(node):
|
|
258
|
+
if neighbor not in projected:
|
|
259
|
+
continue
|
|
260
|
+
order = reaction_center.edges[node, neighbor].get("order", 0.0)
|
|
261
|
+
source_order = order[0] if isinstance(order, tuple) else order
|
|
262
|
+
if source_order and float(source_order) > 0.0:
|
|
263
|
+
anchored = True
|
|
264
|
+
break
|
|
265
|
+
if not anchored:
|
|
266
|
+
return None
|
|
267
|
+
return projected
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _deduplicate_rewrite_equivalent_mappings( # noqa: C901
|
|
271
|
+
mappings: List[Dict[Any, Any]],
|
|
272
|
+
host: nx.Graph,
|
|
273
|
+
reaction_center: nx.Graph,
|
|
274
|
+
) -> List[Dict[Any, Any]]:
|
|
275
|
+
"""Quotient mappings that provably produce isomorphic products.
|
|
276
|
+
|
|
277
|
+
Each affected host component is coloured by the exact reaction-center
|
|
278
|
+
roles placed on it. Two mapping applications are merged only when exact
|
|
279
|
+
attributed-graph isomorphisms identify the resulting multiset of coloured
|
|
280
|
+
source components. Because the same fixed rule is then glued to the same
|
|
281
|
+
preserved roles, those applications have isomorphic non-stereo products.
|
|
282
|
+
|
|
283
|
+
Unchanged context nodes are deliberately absent from the colours: their
|
|
284
|
+
embeddings constrain matching but cannot affect the rewrite endpoint.
|
|
285
|
+
"""
|
|
286
|
+
from .structural import (
|
|
287
|
+
_canonical_attributed_graph_certificate,
|
|
288
|
+
_refine_structural_colours_in_place,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
active = _rewrite_locus_nodes(reaction_center)
|
|
292
|
+
active = _project_mapped_rewrite_locus(mappings, reaction_center, active)
|
|
293
|
+
if active is None:
|
|
294
|
+
return mappings
|
|
295
|
+
if not _mapping_quotient_input_supported(mappings, host, active):
|
|
296
|
+
return mappings
|
|
297
|
+
|
|
298
|
+
components = [
|
|
299
|
+
frozenset(component)
|
|
300
|
+
for component in (
|
|
301
|
+
nx.weakly_connected_components(host)
|
|
302
|
+
if host.is_directed()
|
|
303
|
+
else nx.connected_components(host)
|
|
304
|
+
)
|
|
305
|
+
]
|
|
306
|
+
component_index = {
|
|
307
|
+
node: index for index, component in enumerate(components) for node in component
|
|
308
|
+
}
|
|
309
|
+
node_attrs = (
|
|
310
|
+
"element",
|
|
311
|
+
"aromatic",
|
|
312
|
+
"hcount",
|
|
313
|
+
"charge",
|
|
314
|
+
"radical",
|
|
315
|
+
"lone_pairs",
|
|
316
|
+
"valence_electrons",
|
|
317
|
+
"present",
|
|
318
|
+
"typesGH",
|
|
319
|
+
)
|
|
320
|
+
edge_attrs = (
|
|
321
|
+
"order",
|
|
322
|
+
"kekule_order",
|
|
323
|
+
"sigma_order",
|
|
324
|
+
"pi_order",
|
|
325
|
+
"standard_order",
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# Build every affected source component once. A role-free WL colour is
|
|
329
|
+
# an inexpensive necessary invariant for a role-preserving component
|
|
330
|
+
# isomorphism. Mappings with distinct invariant signatures therefore
|
|
331
|
+
# cannot be rewrite-equivalent and bypass exact whole-component VF2.
|
|
332
|
+
mapping_placements: List[Dict[int, List[Tuple[Any, Any]]]] = []
|
|
333
|
+
affected_components = set()
|
|
334
|
+
for mapping in mappings:
|
|
335
|
+
by_component: Dict[int, List[Tuple[Any, Any]]] = defaultdict(list)
|
|
336
|
+
for pattern_node in active:
|
|
337
|
+
host_node = mapping[pattern_node]
|
|
338
|
+
index = component_index[host_node]
|
|
339
|
+
affected_components.add(index)
|
|
340
|
+
by_component[index].append((pattern_node, host_node))
|
|
341
|
+
mapping_placements.append(by_component)
|
|
342
|
+
|
|
343
|
+
base_components: Dict[int, nx.Graph] = {}
|
|
344
|
+
base_node_values: Dict[int, Dict[Any, Tuple[Any, ...]]] = {}
|
|
345
|
+
component_keys = {}
|
|
346
|
+
node_colours = {}
|
|
347
|
+
identity_cache: Dict[int, Tuple[Any, Any]] = {}
|
|
348
|
+
for index in affected_components:
|
|
349
|
+
base = host.subgraph(components[index]).copy()
|
|
350
|
+
values = {}
|
|
351
|
+
for node, attrs in base.nodes(data=True):
|
|
352
|
+
value = tuple(attrs.get(key) for key in node_attrs)
|
|
353
|
+
values[node] = value
|
|
354
|
+
attrs["_rewrite_mapping_node_sig"] = _freeze_typed_identity(
|
|
355
|
+
value + (None,),
|
|
356
|
+
identity_cache,
|
|
357
|
+
)
|
|
358
|
+
for _, _, attrs in base.edges(data=True):
|
|
359
|
+
attrs["_rewrite_mapping_edge_sig"] = _freeze_typed_identity(
|
|
360
|
+
tuple(attrs.get(key) for key in edge_attrs),
|
|
361
|
+
identity_cache,
|
|
362
|
+
)
|
|
363
|
+
base_components[index] = base
|
|
364
|
+
base_node_values[index] = values
|
|
365
|
+
|
|
366
|
+
_refine_structural_colours_in_place(
|
|
367
|
+
list(base_components.values()),
|
|
368
|
+
iterations=3,
|
|
369
|
+
node_signature_attr="_rewrite_mapping_node_sig",
|
|
370
|
+
edge_signature_attr="_rewrite_mapping_edge_sig",
|
|
371
|
+
node_colour_attr="_rewrite_mapping_node_colour",
|
|
372
|
+
edge_colour_attr="_rewrite_mapping_edge_colour",
|
|
373
|
+
)
|
|
374
|
+
for index, base in base_components.items():
|
|
375
|
+
colours = {
|
|
376
|
+
node: attrs["_rewrite_mapping_node_colour"]
|
|
377
|
+
for node, attrs in base.nodes(data=True)
|
|
378
|
+
}
|
|
379
|
+
node_colours[index] = colours
|
|
380
|
+
component_keys[index] = (
|
|
381
|
+
base.is_directed(),
|
|
382
|
+
len(base),
|
|
383
|
+
base.number_of_edges(),
|
|
384
|
+
tuple(sorted(colours.values())),
|
|
385
|
+
tuple(
|
|
386
|
+
sorted(
|
|
387
|
+
(
|
|
388
|
+
attrs["_rewrite_mapping_edge_colour"]
|
|
389
|
+
for _, _, attrs in base.edges(data=True)
|
|
390
|
+
),
|
|
391
|
+
)
|
|
392
|
+
),
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
fast_keys = []
|
|
396
|
+
for by_component in mapping_placements:
|
|
397
|
+
component_placements = []
|
|
398
|
+
for index, placements in by_component.items():
|
|
399
|
+
ordered = sorted(placements, key=lambda item: repr(item[0]))
|
|
400
|
+
roles = tuple(
|
|
401
|
+
(
|
|
402
|
+
_freeze_typed_identity(pattern_node, identity_cache),
|
|
403
|
+
node_colours[index][host_node],
|
|
404
|
+
)
|
|
405
|
+
for pattern_node, host_node in ordered
|
|
406
|
+
)
|
|
407
|
+
pair_edges = []
|
|
408
|
+
base = base_components[index]
|
|
409
|
+
for (left_role, left_host), (right_role, right_host) in combinations(
|
|
410
|
+
ordered,
|
|
411
|
+
2,
|
|
412
|
+
):
|
|
413
|
+
edge = (
|
|
414
|
+
base.edges[left_host, right_host].get("_rewrite_mapping_edge_sig")
|
|
415
|
+
if base.has_edge(left_host, right_host)
|
|
416
|
+
else None
|
|
417
|
+
)
|
|
418
|
+
pair_edges.append(
|
|
419
|
+
(
|
|
420
|
+
_freeze_typed_identity(left_role, identity_cache),
|
|
421
|
+
_freeze_typed_identity(right_role, identity_cache),
|
|
422
|
+
edge,
|
|
423
|
+
)
|
|
424
|
+
)
|
|
425
|
+
component_placements.append(
|
|
426
|
+
(component_keys[index], roles, tuple(pair_edges))
|
|
427
|
+
)
|
|
428
|
+
fast_keys.append(tuple(sorted(component_placements, key=repr)))
|
|
429
|
+
|
|
430
|
+
fast_counts = Counter(fast_keys)
|
|
431
|
+
cache: Dict[Any, int] = {}
|
|
432
|
+
representatives: Dict[Any, List[Tuple[int, nx.Graph]]] = defaultdict(list)
|
|
433
|
+
certificate_classes: Dict[str, int] = {}
|
|
434
|
+
certificate_node_palette: Dict[Any, int] = {}
|
|
435
|
+
certificate_edge_palette: Dict[Any, int] = {}
|
|
436
|
+
certificate_topology_cache: Dict[Any, Any] = {}
|
|
437
|
+
next_class = 0
|
|
438
|
+
node_match = categorical_node_match("_rewrite_mapping_node_sig", ())
|
|
439
|
+
edge_match = categorical_edge_match("_rewrite_mapping_edge_sig", ())
|
|
440
|
+
|
|
441
|
+
def classify_component(
|
|
442
|
+
index: int,
|
|
443
|
+
placements: Tuple[Tuple[Any, Any], ...],
|
|
444
|
+
) -> int:
|
|
445
|
+
nonlocal next_class
|
|
446
|
+
cache_key = (index, placements)
|
|
447
|
+
cached = cache.get(cache_key)
|
|
448
|
+
if cached is not None:
|
|
449
|
+
return cached
|
|
450
|
+
|
|
451
|
+
decorated = base_components[index].copy()
|
|
452
|
+
for pattern_node, host_node in placements:
|
|
453
|
+
decorated.nodes[host_node]["_rewrite_mapping_node_sig"] = (
|
|
454
|
+
_freeze_typed_identity(
|
|
455
|
+
base_node_values[index][host_node] + (pattern_node,),
|
|
456
|
+
identity_cache,
|
|
457
|
+
)
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
certificate = _canonical_attributed_graph_certificate(
|
|
461
|
+
decorated,
|
|
462
|
+
node_attribute="_rewrite_mapping_node_sig",
|
|
463
|
+
edge_attribute="_rewrite_mapping_edge_sig",
|
|
464
|
+
node_palette=certificate_node_palette,
|
|
465
|
+
edge_palette=certificate_edge_palette,
|
|
466
|
+
topology_cache=certificate_topology_cache,
|
|
467
|
+
)
|
|
468
|
+
if certificate is not None:
|
|
469
|
+
component_class = certificate_classes.get(certificate)
|
|
470
|
+
if component_class is None:
|
|
471
|
+
component_class = next_class
|
|
472
|
+
next_class += 1
|
|
473
|
+
certificate_classes[certificate] = component_class
|
|
474
|
+
cache[cache_key] = component_class
|
|
475
|
+
return component_class
|
|
476
|
+
|
|
477
|
+
node_labels = [
|
|
478
|
+
attrs["_rewrite_mapping_node_sig"]
|
|
479
|
+
for _, attrs in decorated.nodes(data=True)
|
|
480
|
+
]
|
|
481
|
+
edge_labels = [
|
|
482
|
+
attrs["_rewrite_mapping_edge_sig"]
|
|
483
|
+
for _, _, attrs in decorated.edges(data=True)
|
|
484
|
+
]
|
|
485
|
+
environments = []
|
|
486
|
+
for node, attrs in decorated.nodes(data=True):
|
|
487
|
+
incident = Counter(
|
|
488
|
+
(
|
|
489
|
+
decorated.edges[node, neighbor]["_rewrite_mapping_edge_sig"],
|
|
490
|
+
decorated.nodes[neighbor]["_rewrite_mapping_node_sig"],
|
|
491
|
+
)
|
|
492
|
+
for neighbor in decorated.neighbors(node)
|
|
493
|
+
)
|
|
494
|
+
environments.append(
|
|
495
|
+
(
|
|
496
|
+
attrs["_rewrite_mapping_node_sig"],
|
|
497
|
+
frozenset(incident.items()),
|
|
498
|
+
)
|
|
499
|
+
)
|
|
500
|
+
fingerprint = (
|
|
501
|
+
decorated.number_of_nodes(),
|
|
502
|
+
decorated.number_of_edges(),
|
|
503
|
+
frozenset(Counter(node_labels).items()),
|
|
504
|
+
frozenset(Counter(edge_labels).items()),
|
|
505
|
+
frozenset(Counter(environments).items()),
|
|
506
|
+
)
|
|
507
|
+
for class_id, representative in representatives[fingerprint]:
|
|
508
|
+
if nx.is_isomorphic(
|
|
509
|
+
decorated,
|
|
510
|
+
representative,
|
|
511
|
+
node_match=node_match,
|
|
512
|
+
edge_match=edge_match,
|
|
513
|
+
):
|
|
514
|
+
cache[cache_key] = class_id
|
|
515
|
+
return class_id
|
|
516
|
+
|
|
517
|
+
class_id = next_class
|
|
518
|
+
next_class += 1
|
|
519
|
+
representatives[fingerprint].append((class_id, decorated))
|
|
520
|
+
cache[cache_key] = class_id
|
|
521
|
+
return class_id
|
|
522
|
+
|
|
523
|
+
seen_by_fast_key = defaultdict(set)
|
|
524
|
+
unique = []
|
|
525
|
+
for mapping, by_component, fast_key in zip(
|
|
526
|
+
mappings,
|
|
527
|
+
mapping_placements,
|
|
528
|
+
fast_keys,
|
|
529
|
+
):
|
|
530
|
+
if fast_counts[fast_key] == 1:
|
|
531
|
+
unique.append(mapping)
|
|
532
|
+
continue
|
|
533
|
+
signature = tuple(
|
|
534
|
+
sorted(
|
|
535
|
+
classify_component(
|
|
536
|
+
index,
|
|
537
|
+
tuple(sorted(placements, key=repr)),
|
|
538
|
+
)
|
|
539
|
+
for index, placements in by_component.items()
|
|
540
|
+
)
|
|
541
|
+
)
|
|
542
|
+
if signature in seen_by_fast_key[fast_key]:
|
|
543
|
+
continue
|
|
544
|
+
seen_by_fast_key[fast_key].add(signature)
|
|
545
|
+
unique.append(mapping)
|
|
546
|
+
return unique
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def _finalize_product_electron_fields(
|
|
550
|
+
its_graphs: List[nx.Graph],
|
|
551
|
+
) -> List[nx.Graph]:
|
|
552
|
+
"""Finalize deferred tuple products and reject unperceivable candidates."""
|
|
553
|
+
finalized: List[nx.Graph] = []
|
|
554
|
+
for its in its_graphs:
|
|
555
|
+
try:
|
|
556
|
+
if its.graph.get("electron_aware_rewrite", False) and not its.graph.get(
|
|
557
|
+
"_product_electron_fields_current", False
|
|
558
|
+
):
|
|
559
|
+
_product_state._refresh_product_electron_fields(its)
|
|
560
|
+
except _product_state.ProductStatePerceptionError:
|
|
561
|
+
# Reject only this application; other embeddings may remain valid.
|
|
562
|
+
continue
|
|
563
|
+
finalized.append(its)
|
|
564
|
+
return finalized
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def _deduplicate_structural_its(its_graphs: List[nx.Graph]) -> List[nx.Graph]:
|
|
568
|
+
"""Keep one representative per exact structural/stereo ITS identity.
|
|
569
|
+
|
|
570
|
+
Electron state is finalized before quotienting. This guarantees that the
|
|
571
|
+
deduplication relation is a congruence of endpoint serialization: two
|
|
572
|
+
provisional candidates may not be merged before every state field that
|
|
573
|
+
affects their serialized endpoints has been materialized.
|
|
574
|
+
"""
|
|
575
|
+
from .structural import _cluster_structural_its
|
|
576
|
+
|
|
577
|
+
if not its_graphs:
|
|
578
|
+
return its_graphs
|
|
579
|
+
|
|
580
|
+
has_deferred_electrons = any(
|
|
581
|
+
its.graph.get("electron_aware_rewrite", False)
|
|
582
|
+
and not its.graph.get("_product_electron_fields_current", False)
|
|
583
|
+
for its in its_graphs
|
|
584
|
+
)
|
|
585
|
+
if not has_deferred_electrons:
|
|
586
|
+
return _cluster_structural_its(
|
|
587
|
+
its_graphs,
|
|
588
|
+
refresh_electrons=False,
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
its_graphs = _finalize_product_electron_fields(its_graphs)
|
|
592
|
+
if not its_graphs:
|
|
593
|
+
return []
|
|
594
|
+
return _cluster_structural_its(
|
|
595
|
+
its_graphs,
|
|
596
|
+
refresh_electrons=False,
|
|
597
|
+
hash_iterations=3,
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def _deduplicate_coupling_face_products(
|
|
602
|
+
its_graphs: List[nx.Graph],
|
|
603
|
+
) -> List[nx.Graph]:
|
|
604
|
+
"""Collapse symmetry-identical coupled faces but keep enantiomers.
|
|
605
|
+
|
|
606
|
+
A meso product can be reached through both correlated face branches.
|
|
607
|
+
Atom-map labels make those ITS registries look different even though
|
|
608
|
+
a stereo-preserving molecular automorphism relates them. This pass is
|
|
609
|
+
limited to coupling branches without explicit population outcomes;
|
|
610
|
+
true enantiomers remain non-isomorphic and are retained.
|
|
611
|
+
"""
|
|
612
|
+
from .structural import (
|
|
613
|
+
_EXACT_EDGE_SIG,
|
|
614
|
+
_EXACT_NODE_SIG,
|
|
615
|
+
_REFINED_NODE_COLOUR,
|
|
616
|
+
_prepare_its_for_structural_cluster,
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
if len(its_graphs) < 2:
|
|
620
|
+
return its_graphs
|
|
621
|
+
|
|
622
|
+
from synkit.Graph.Stereo import stereo_isomorphic
|
|
623
|
+
|
|
624
|
+
representatives: List[Tuple[nx.Graph, nx.Graph, nx.Graph, nx.Graph]] = []
|
|
625
|
+
unique = []
|
|
626
|
+
for its in its_graphs:
|
|
627
|
+
if not its.graph.get("stereo_coupling_branch") or its.graph.get(
|
|
628
|
+
"stereo_outcomes"
|
|
629
|
+
):
|
|
630
|
+
unique.append(its)
|
|
631
|
+
continue
|
|
632
|
+
reverter = ITSReverter(its)
|
|
633
|
+
reactant = reverter.to_reactant_graph()
|
|
634
|
+
product = reverter.to_product_graph()
|
|
635
|
+
prepared = _prepare_its_for_structural_cluster(its)
|
|
636
|
+
duplicate = False
|
|
637
|
+
for (
|
|
638
|
+
other_its,
|
|
639
|
+
other_reactant,
|
|
640
|
+
other_product,
|
|
641
|
+
other_prepared,
|
|
642
|
+
) in representatives:
|
|
643
|
+
if not nx.is_isomorphic(
|
|
644
|
+
prepared,
|
|
645
|
+
other_prepared,
|
|
646
|
+
node_match=categorical_node_match(
|
|
647
|
+
[_EXACT_NODE_SIG, _REFINED_NODE_COLOUR],
|
|
648
|
+
[(), ""],
|
|
649
|
+
),
|
|
650
|
+
edge_match=categorical_edge_match(_EXACT_EDGE_SIG, ()),
|
|
651
|
+
):
|
|
652
|
+
continue
|
|
653
|
+
if stereo_isomorphic(reactant, other_reactant) and stereo_isomorphic(
|
|
654
|
+
product, other_product
|
|
655
|
+
):
|
|
656
|
+
retained = other_its.graph.get("stereo_coupling_branch", {})
|
|
657
|
+
duplicate_metadata = its.graph.get("stereo_coupling_branch", {})
|
|
658
|
+
for target, metadata in duplicate_metadata.items():
|
|
659
|
+
retained_metadata = retained.get(target)
|
|
660
|
+
if retained_metadata is None:
|
|
661
|
+
continue
|
|
662
|
+
branches = set(
|
|
663
|
+
retained_metadata.get(
|
|
664
|
+
"equivalent_face_branches",
|
|
665
|
+
[retained_metadata.get("face_branch")],
|
|
666
|
+
)
|
|
667
|
+
)
|
|
668
|
+
branches.add(metadata.get("face_branch"))
|
|
669
|
+
branches.discard(None)
|
|
670
|
+
retained_metadata["equivalent_face_branches"] = sorted(branches)
|
|
671
|
+
retained_metadata["symmetry_multiplicity"] = len(branches)
|
|
672
|
+
_merge_application_orbits(
|
|
673
|
+
other_its,
|
|
674
|
+
[other_its, its],
|
|
675
|
+
)
|
|
676
|
+
duplicate = True
|
|
677
|
+
break
|
|
678
|
+
if duplicate:
|
|
679
|
+
continue
|
|
680
|
+
representatives.append((its, reactant, product, prepared))
|
|
681
|
+
unique.append(its)
|
|
682
|
+
return unique
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def _components_are_equivalent(
|
|
686
|
+
pattern: nx.Graph,
|
|
687
|
+
left: frozenset[NodeId],
|
|
688
|
+
right: frozenset[NodeId],
|
|
689
|
+
node_attrs: List[str],
|
|
690
|
+
edge_attrs: List[str],
|
|
691
|
+
) -> bool:
|
|
692
|
+
"""Return whether two disconnected pattern components have one role shape."""
|
|
693
|
+
left_graph = pattern.subgraph(left)
|
|
694
|
+
right_graph = pattern.subgraph(right)
|
|
695
|
+
node_defaults = [0 if attr == "charge" else "*" for attr in node_attrs]
|
|
696
|
+
edge_defaults = [1.0 for _ in edge_attrs]
|
|
697
|
+
matcher = GraphMatcher(
|
|
698
|
+
left_graph,
|
|
699
|
+
right_graph,
|
|
700
|
+
node_match=categorical_node_match(node_attrs, node_defaults),
|
|
701
|
+
edge_match=categorical_edge_match(edge_attrs, edge_defaults),
|
|
702
|
+
)
|
|
703
|
+
return matcher.is_isomorphic()
|