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
synkit/Rule/syn_rule.py
ADDED
|
@@ -0,0 +1,1090 @@
|
|
|
1
|
+
"""syn_rule.py
|
|
2
|
+
================
|
|
3
|
+
Immutable description of a reaction template (SynRule) with canonical forms
|
|
4
|
+
and optional implicit‐hydrogen stripping.
|
|
5
|
+
|
|
6
|
+
Key features
|
|
7
|
+
------------
|
|
8
|
+
* **Fragment decomposition** – splits the ITS graph into rc, left, and right.
|
|
9
|
+
* **Implicit H‐handling** – converts explicit H nodes into hcount + h_pairs.
|
|
10
|
+
* **Canonicalisation** – wraps rc/left/right in SynGraph for stable signatures.
|
|
11
|
+
* **Value‑object semantics** – `__eq__`/`__hash__` use fragment signatures.
|
|
12
|
+
|
|
13
|
+
Quick start
|
|
14
|
+
-----------
|
|
15
|
+
>>> from synkit.Graph.syn_rule import SynRule
|
|
16
|
+
>>> rule = SynRule.from_smart("[CH3:1]C>>[CH2:1]C")
|
|
17
|
+
>>> rule.left.signature, rule.right.signature
|
|
18
|
+
('abc123...', 'def456...')
|
|
19
|
+
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
from numbers import Real
|
|
24
|
+
from typing import Any, Mapping, Optional, Tuple
|
|
25
|
+
|
|
26
|
+
import networkx as nx
|
|
27
|
+
|
|
28
|
+
from synkit.Graph.syn_graph import SynGraph
|
|
29
|
+
from synkit.Graph.canon_graph import GraphCanonicaliser
|
|
30
|
+
from synkit.Graph.ITS.its_decompose import its_decompose
|
|
31
|
+
from synkit.Graph.ITS.its_construction import ITSConstruction
|
|
32
|
+
from synkit.Graph.ITS.its_reverter import ITSReverter
|
|
33
|
+
from synkit.Graph.Hyrogen._misc import normalize_h_pair_graph
|
|
34
|
+
from synkit.Graph.Morphism.constraints import NodeStateKind, adapt_legacy_node_state
|
|
35
|
+
from synkit.Graph.Stereo import (
|
|
36
|
+
NonInvertibleStereoEffectError,
|
|
37
|
+
StereoCoupling,
|
|
38
|
+
StereoOutcome,
|
|
39
|
+
)
|
|
40
|
+
from synkit.Rule.stereo_identity import stereo_rule_isomorphic
|
|
41
|
+
from synkit.IO.chem_converter import (
|
|
42
|
+
ITSFormat,
|
|
43
|
+
detect_its_format,
|
|
44
|
+
rsmi_to_its,
|
|
45
|
+
gml_to_its,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
__all__ = ["NonInvertibleStereoEffectError", "SynRule"]
|
|
49
|
+
|
|
50
|
+
_WILDCARD_CONTRACT_KEYS = (
|
|
51
|
+
"wildcard_role",
|
|
52
|
+
"elements",
|
|
53
|
+
"charges",
|
|
54
|
+
"radicals",
|
|
55
|
+
"bond_orders",
|
|
56
|
+
"side",
|
|
57
|
+
"owner",
|
|
58
|
+
"capacity",
|
|
59
|
+
"resource_budget",
|
|
60
|
+
"stereo_slot",
|
|
61
|
+
"virtual_kind",
|
|
62
|
+
"mapped_identity",
|
|
63
|
+
"materialization",
|
|
64
|
+
)
|
|
65
|
+
_RELATIVE_NODE_RESOURCES = ("lone_pairs",)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class SynRule:
|
|
69
|
+
"""Immutable reaction template: rc, left, and right fragments as SynGraph Object.
|
|
70
|
+
|
|
71
|
+
:param rc_graph: Raw reaction-centre (RC) graph.
|
|
72
|
+
:type rc_graph: nx.Graph
|
|
73
|
+
:param name: Identifier for the rule.
|
|
74
|
+
:type name: str, default ``"rule"``
|
|
75
|
+
:param canonicaliser: Custom canonicaliser; if *None* a default is created.
|
|
76
|
+
:type canonicaliser: Optional[GraphCanonicaliser]
|
|
77
|
+
:param canon: If *True*, build canonical forms and SHA-256 signatures.
|
|
78
|
+
:type canon: bool, default ``True``
|
|
79
|
+
:param implicit_h: Convert explicit hydrogens in the **rc/left/right** fragments to an
|
|
80
|
+
integer ``hcount`` attribute and record cross-fragment hydrogen pairs
|
|
81
|
+
in a ``h_pairs`` attribute.
|
|
82
|
+
:type implicit_h: bool, default ``True``
|
|
83
|
+
:param stereo_outcomes: Explicit product-branch declarations keyed by descriptor target. A
|
|
84
|
+
``RACEMIC`` outcome uses the stored product descriptor as a reference
|
|
85
|
+
orientation and makes application emit it and its inverse.
|
|
86
|
+
:type stereo_outcomes: typing.Mapping or None
|
|
87
|
+
:param stereo_couplings: Coupled rule operations keyed by their central bond target. A vicinal
|
|
88
|
+
addition may be declared compactly as ``{"bond:2-3": "ANTI"}``;
|
|
89
|
+
centers and delivered ligands are inferred from the structural edit.
|
|
90
|
+
:type stereo_couplings: typing.Mapping or None
|
|
91
|
+
|
|
92
|
+
:ivar rc: Wrapped reaction‐centre graph.
|
|
93
|
+
:vartype rc: SynGraph
|
|
94
|
+
:ivar left: Wrapped left fragment.
|
|
95
|
+
:vartype left: SynGraph
|
|
96
|
+
:ivar right: Wrapped right fragment.
|
|
97
|
+
:vartype right: SynGraph
|
|
98
|
+
:ivar canonical_smiles: Pair of left/right fragment SHA‐256 signatures (or None if canon=False).
|
|
99
|
+
:vartype canonical_smiles: Optional[Tuple[str,str]]
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
# Alternate constructors
|
|
103
|
+
@classmethod
|
|
104
|
+
def from_smart(
|
|
105
|
+
cls,
|
|
106
|
+
smart: str,
|
|
107
|
+
name: str = "rule",
|
|
108
|
+
canonicaliser: Optional[GraphCanonicaliser] = None,
|
|
109
|
+
*,
|
|
110
|
+
canon: bool = True,
|
|
111
|
+
implicit_h: bool = True,
|
|
112
|
+
format: ITSFormat = "typesGH",
|
|
113
|
+
stereo_outcomes: Optional[
|
|
114
|
+
Mapping[str, StereoOutcome | str | Mapping[str, Any]]
|
|
115
|
+
] = None,
|
|
116
|
+
stereo_couplings: Optional[
|
|
117
|
+
Mapping[str, StereoCoupling | str | Mapping[str, Any]]
|
|
118
|
+
] = None,
|
|
119
|
+
) -> "SynRule":
|
|
120
|
+
"""Instantiate from a SMARTS string."""
|
|
121
|
+
return cls(
|
|
122
|
+
rsmi_to_its(smart, format=format),
|
|
123
|
+
name=name,
|
|
124
|
+
canonicaliser=canonicaliser,
|
|
125
|
+
canon=canon,
|
|
126
|
+
implicit_h=implicit_h,
|
|
127
|
+
format=format,
|
|
128
|
+
stereo_outcomes=stereo_outcomes,
|
|
129
|
+
stereo_couplings=stereo_couplings,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
@classmethod
|
|
133
|
+
def from_gml(
|
|
134
|
+
cls,
|
|
135
|
+
gml: str,
|
|
136
|
+
name: str = "rule",
|
|
137
|
+
canonicaliser: Optional[GraphCanonicaliser] = None,
|
|
138
|
+
*,
|
|
139
|
+
canon: bool = True,
|
|
140
|
+
implicit_h: bool = True,
|
|
141
|
+
stereo_couplings: Optional[
|
|
142
|
+
Mapping[str, StereoCoupling | str | Mapping[str, Any]]
|
|
143
|
+
] = None,
|
|
144
|
+
) -> "SynRule":
|
|
145
|
+
"""Instantiate from a GML string."""
|
|
146
|
+
return cls(
|
|
147
|
+
gml_to_its(gml),
|
|
148
|
+
name=name,
|
|
149
|
+
canonicaliser=canonicaliser,
|
|
150
|
+
canon=canon,
|
|
151
|
+
implicit_h=implicit_h,
|
|
152
|
+
stereo_couplings=stereo_couplings,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Initializer
|
|
156
|
+
def __init__(
|
|
157
|
+
self,
|
|
158
|
+
rc: nx.Graph,
|
|
159
|
+
name: str = "rule",
|
|
160
|
+
canonicaliser: Optional[GraphCanonicaliser] = None,
|
|
161
|
+
*,
|
|
162
|
+
canon: bool = True,
|
|
163
|
+
implicit_h: bool = True,
|
|
164
|
+
format: Optional[ITSFormat] = None,
|
|
165
|
+
stereo_outcomes: Optional[
|
|
166
|
+
Mapping[str, StereoOutcome | str | Mapping[str, Any]]
|
|
167
|
+
] = None,
|
|
168
|
+
stereo_couplings: Optional[
|
|
169
|
+
Mapping[str, StereoCoupling | str | Mapping[str, Any]]
|
|
170
|
+
] = None,
|
|
171
|
+
) -> None:
|
|
172
|
+
self._name = name
|
|
173
|
+
self._canon_enabled = canon
|
|
174
|
+
self._implicit_h = implicit_h
|
|
175
|
+
self._canonicaliser = canonicaliser or GraphCanonicaliser()
|
|
176
|
+
|
|
177
|
+
# Fragment decomposition
|
|
178
|
+
rc_graph = rc.copy()
|
|
179
|
+
self._format = format or detect_its_format(rc_graph)
|
|
180
|
+
if self._implicit_h:
|
|
181
|
+
rc_graph = normalize_h_pair_graph(rc_graph)
|
|
182
|
+
|
|
183
|
+
left_graph, right_graph = self._decompose(rc_graph, self._format)
|
|
184
|
+
|
|
185
|
+
# Optional H-stripping
|
|
186
|
+
if self._implicit_h and self._format == "typesGH":
|
|
187
|
+
self._strip_explicit_h(rc_graph, left_graph, right_graph)
|
|
188
|
+
# Update typesGH tuples with new hcount.
|
|
189
|
+
for node, att in rc_graph.nodes(data=True):
|
|
190
|
+
t0, t1 = att["typesGH"]
|
|
191
|
+
new_t0 = (
|
|
192
|
+
t0[0],
|
|
193
|
+
t0[1],
|
|
194
|
+
left_graph.nodes[node]["hcount"] + t0[2],
|
|
195
|
+
t0[3],
|
|
196
|
+
t0[4],
|
|
197
|
+
)
|
|
198
|
+
new_t1 = (
|
|
199
|
+
t1[0],
|
|
200
|
+
t1[1],
|
|
201
|
+
right_graph.nodes[node]["hcount"] + t1[2],
|
|
202
|
+
t1[3],
|
|
203
|
+
t1[4],
|
|
204
|
+
)
|
|
205
|
+
att["typesGH"] = (new_t0, new_t1)
|
|
206
|
+
left_graph, right_graph = self._decompose(rc_graph, self._format)
|
|
207
|
+
elif self._implicit_h and self._format == "tuple":
|
|
208
|
+
self._strip_explicit_h_tuple(rc_graph, left_graph, right_graph)
|
|
209
|
+
left_graph, right_graph = self._decompose(rc_graph, self._format)
|
|
210
|
+
if self._format == "tuple":
|
|
211
|
+
self._normalize_relative_node_resources(rc_graph)
|
|
212
|
+
left_graph, right_graph = self._decompose(rc_graph, self._format)
|
|
213
|
+
self._project_wildcard_contracts(rc_graph, left_graph, right_graph)
|
|
214
|
+
stereo_sides = rc_graph.graph.get("stereo_descriptors", {})
|
|
215
|
+
self.stereo_guards = dict(stereo_sides.get("reactant", {}))
|
|
216
|
+
self.stereo_effects = dict(rc_graph.graph.get("stereo_changes", {}))
|
|
217
|
+
raw_outcomes = (
|
|
218
|
+
stereo_outcomes
|
|
219
|
+
if stereo_outcomes is not None
|
|
220
|
+
else rc_graph.graph.get("stereo_outcomes", {})
|
|
221
|
+
)
|
|
222
|
+
self.stereo_outcomes = {
|
|
223
|
+
key: StereoOutcome.from_value(value) for key, value in raw_outcomes.items()
|
|
224
|
+
}
|
|
225
|
+
raw_couplings = (
|
|
226
|
+
stereo_couplings
|
|
227
|
+
if stereo_couplings is not None
|
|
228
|
+
else rc_graph.graph.get("stereo_couplings", {})
|
|
229
|
+
)
|
|
230
|
+
self.stereo_couplings = self._normalize_stereo_couplings(
|
|
231
|
+
raw_couplings,
|
|
232
|
+
left_graph,
|
|
233
|
+
right_graph,
|
|
234
|
+
)
|
|
235
|
+
self.stereo_query_policies = dict(
|
|
236
|
+
rc_graph.graph.get("stereo_query_policies", {})
|
|
237
|
+
)
|
|
238
|
+
invalid_query_policies = {
|
|
239
|
+
value
|
|
240
|
+
for value in self.stereo_query_policies.values()
|
|
241
|
+
if value not in {"exact", "wildcard", "either"}
|
|
242
|
+
}
|
|
243
|
+
if invalid_query_policies:
|
|
244
|
+
raise ValueError(
|
|
245
|
+
"Stereo query policies must be 'exact', 'wildcard', or "
|
|
246
|
+
f"'either'; received {sorted(invalid_query_policies)!r}."
|
|
247
|
+
)
|
|
248
|
+
self._reverse_stereo_outcomes = {
|
|
249
|
+
key: StereoOutcome.from_value(value)
|
|
250
|
+
for key, value in rc_graph.graph.get("stereo_reverse_outcomes", {}).items()
|
|
251
|
+
}
|
|
252
|
+
self._reverse_stereo_query_policies = dict(
|
|
253
|
+
rc_graph.graph.get("stereo_reverse_query_policies", {})
|
|
254
|
+
)
|
|
255
|
+
self._validate_stereo_outcomes()
|
|
256
|
+
self._validate_stereo_couplings(rc_graph)
|
|
257
|
+
rc_graph.graph["stereo_outcomes"] = {
|
|
258
|
+
key: outcome.to_dict() for key, outcome in self.stereo_outcomes.items()
|
|
259
|
+
}
|
|
260
|
+
rc_graph.graph["stereo_query_policies"] = dict(self.stereo_query_policies)
|
|
261
|
+
rc_graph.graph["stereo_couplings"] = {
|
|
262
|
+
key: coupling.relation for key, coupling in self.stereo_couplings.items()
|
|
263
|
+
}
|
|
264
|
+
if self._reverse_stereo_outcomes:
|
|
265
|
+
rc_graph.graph["stereo_reverse_outcomes"] = {
|
|
266
|
+
key: outcome.to_dict()
|
|
267
|
+
for key, outcome in self._reverse_stereo_outcomes.items()
|
|
268
|
+
}
|
|
269
|
+
rc_graph.graph["stereo_reverse_query_policies"] = dict(
|
|
270
|
+
self._reverse_stereo_query_policies
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
# Wrap graphs.
|
|
274
|
+
self.rc = SynGraph(rc_graph, self._canonicaliser, canon=canon)
|
|
275
|
+
self.left = SynGraph(left_graph, self._canonicaliser, canon=canon)
|
|
276
|
+
self.right = SynGraph(right_graph, self._canonicaliser, canon=canon)
|
|
277
|
+
|
|
278
|
+
self.stereo_mode = "ignore"
|
|
279
|
+
|
|
280
|
+
self.canonical_smiles: Optional[Tuple[str, str]] = (
|
|
281
|
+
(self.left.signature, self.right.signature) if canon else None
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
# Private utilities
|
|
285
|
+
def _validate_stereo_outcomes(self) -> None:
|
|
286
|
+
"""Require explicit branching declarations to match stored effects."""
|
|
287
|
+
for key, outcome in self.stereo_outcomes.items():
|
|
288
|
+
if key not in self.stereo_effects:
|
|
289
|
+
raise ValueError(f"Stereo outcome target {key!r} has no rule effect.")
|
|
290
|
+
change = self.stereo_effects[key]
|
|
291
|
+
if outcome.kind == "SINGLE":
|
|
292
|
+
continue
|
|
293
|
+
if (
|
|
294
|
+
change.change != "FORMED"
|
|
295
|
+
or change.after is None
|
|
296
|
+
or change.after.parity not in (-1, 1)
|
|
297
|
+
):
|
|
298
|
+
raise ValueError(
|
|
299
|
+
f"{outcome.kind} outcome is only valid for a specified, "
|
|
300
|
+
"newly formed chiral descriptor."
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
@staticmethod
|
|
304
|
+
def _mapped_neighbors(graph: nx.Graph, atom_map: int) -> set[int]:
|
|
305
|
+
"""Return mapped neighbors for one rule atom on one endpoint."""
|
|
306
|
+
by_map = {
|
|
307
|
+
attrs.get("atom_map", node): node
|
|
308
|
+
for node, attrs in graph.nodes(data=True)
|
|
309
|
+
if isinstance(attrs.get("atom_map", node), int)
|
|
310
|
+
}
|
|
311
|
+
node = by_map.get(atom_map)
|
|
312
|
+
if node is None:
|
|
313
|
+
return set()
|
|
314
|
+
result = set()
|
|
315
|
+
for neighbor in graph.neighbors(node):
|
|
316
|
+
reference = graph.nodes[neighbor].get("atom_map", neighbor)
|
|
317
|
+
if isinstance(reference, int) and reference > 0:
|
|
318
|
+
result.add(reference)
|
|
319
|
+
return result
|
|
320
|
+
|
|
321
|
+
@classmethod
|
|
322
|
+
def _infer_vicinal_coupling(
|
|
323
|
+
cls,
|
|
324
|
+
target: str,
|
|
325
|
+
relation: str,
|
|
326
|
+
left: nx.Graph,
|
|
327
|
+
right: nx.Graph,
|
|
328
|
+
) -> StereoCoupling:
|
|
329
|
+
"""Infer operation direction and ligand maps from a structural edit."""
|
|
330
|
+
try:
|
|
331
|
+
prefix, values = target.split(":", 1)
|
|
332
|
+
centers = tuple(int(value) for value in values.split("-"))
|
|
333
|
+
except (TypeError, ValueError) as error:
|
|
334
|
+
raise ValueError(
|
|
335
|
+
f"Compact stereo coupling target {target!r} must be 'bond:i-j'."
|
|
336
|
+
) from error
|
|
337
|
+
if prefix != "bond" or len(centers) != 2:
|
|
338
|
+
raise ValueError(
|
|
339
|
+
f"Compact stereo coupling target {target!r} must be 'bond:i-j'."
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
added_by_center = []
|
|
343
|
+
removed_by_center = []
|
|
344
|
+
for center in centers:
|
|
345
|
+
left_neighbors = cls._mapped_neighbors(left, center)
|
|
346
|
+
right_neighbors = cls._mapped_neighbors(right, center)
|
|
347
|
+
added = right_neighbors - left_neighbors
|
|
348
|
+
removed = left_neighbors - right_neighbors
|
|
349
|
+
added -= set(centers)
|
|
350
|
+
removed -= set(centers)
|
|
351
|
+
added_by_center.append(added)
|
|
352
|
+
removed_by_center.append(removed)
|
|
353
|
+
|
|
354
|
+
if all(len(values) == 1 for values in added_by_center) and all(
|
|
355
|
+
not values for values in removed_by_center
|
|
356
|
+
):
|
|
357
|
+
kind = "VICINAL_ADDITION"
|
|
358
|
+
ligand_sets = added_by_center
|
|
359
|
+
elif all(len(values) == 1 for values in removed_by_center) and all(
|
|
360
|
+
not values for values in added_by_center
|
|
361
|
+
):
|
|
362
|
+
kind = "VICINAL_ELIMINATION"
|
|
363
|
+
ligand_sets = removed_by_center
|
|
364
|
+
else:
|
|
365
|
+
raise ValueError(
|
|
366
|
+
f"Compact stereo coupling {target!r} requires exactly one "
|
|
367
|
+
"mapped ligand bond to be formed or broken at each center."
|
|
368
|
+
)
|
|
369
|
+
return StereoCoupling(
|
|
370
|
+
kind,
|
|
371
|
+
relation,
|
|
372
|
+
centers, # type: ignore[arg-type]
|
|
373
|
+
tuple(next(iter(values)) for values in ligand_sets), # type: ignore[arg-type]
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
@classmethod
|
|
377
|
+
def _normalize_stereo_couplings(
|
|
378
|
+
cls,
|
|
379
|
+
values: Mapping[str, StereoCoupling | str | Mapping[str, Any]],
|
|
380
|
+
left: nx.Graph,
|
|
381
|
+
right: nx.Graph,
|
|
382
|
+
) -> dict[str, StereoCoupling]:
|
|
383
|
+
"""Expand compact relation-only declarations into mapped values."""
|
|
384
|
+
result = {}
|
|
385
|
+
for target, value in values.items():
|
|
386
|
+
coupling = (
|
|
387
|
+
cls._infer_vicinal_coupling(target, value, left, right)
|
|
388
|
+
if isinstance(value, str)
|
|
389
|
+
else StereoCoupling.from_value(value)
|
|
390
|
+
)
|
|
391
|
+
result[target] = coupling
|
|
392
|
+
return result
|
|
393
|
+
|
|
394
|
+
def _validate_stereo_couplings(self, rc_graph: nx.Graph) -> None:
|
|
395
|
+
"""Require chemistry-level couplings without encoded endpoints."""
|
|
396
|
+
atom_maps = set()
|
|
397
|
+
for node, attrs in rc_graph.nodes(data=True):
|
|
398
|
+
atom_map = attrs.get("atom_map", node)
|
|
399
|
+
if isinstance(atom_map, tuple):
|
|
400
|
+
atom_maps.update(
|
|
401
|
+
value for value in atom_map if isinstance(value, int) and value > 0
|
|
402
|
+
)
|
|
403
|
+
elif isinstance(atom_map, int) and atom_map > 0:
|
|
404
|
+
atom_maps.add(atom_map)
|
|
405
|
+
|
|
406
|
+
for key, coupling in self.stereo_couplings.items():
|
|
407
|
+
if key != coupling.target:
|
|
408
|
+
raise ValueError(
|
|
409
|
+
f"Stereo coupling key {key!r} must equal its center target "
|
|
410
|
+
f"{coupling.target!r}."
|
|
411
|
+
)
|
|
412
|
+
missing = set(coupling.dependencies) - atom_maps
|
|
413
|
+
if missing:
|
|
414
|
+
raise ValueError(
|
|
415
|
+
f"Stereo coupling {key!r} references absent atom maps "
|
|
416
|
+
f"{sorted(missing)!r}."
|
|
417
|
+
)
|
|
418
|
+
coupled_targets = {
|
|
419
|
+
key,
|
|
420
|
+
*(f"atom:{center}" for center in coupling.centers),
|
|
421
|
+
}
|
|
422
|
+
redundant = coupled_targets & (
|
|
423
|
+
set(self.stereo_guards) | set(self.stereo_effects)
|
|
424
|
+
)
|
|
425
|
+
if redundant:
|
|
426
|
+
raise ValueError(
|
|
427
|
+
f"Stereo coupling {key!r} derives endpoint stereo; remove "
|
|
428
|
+
"stereo marks from the addition SMARTS instead of storing "
|
|
429
|
+
f"coupled endpoint descriptors at {sorted(redundant)!r}."
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
@staticmethod
|
|
433
|
+
def _decompose(rc: nx.Graph, format: ITSFormat) -> tuple[nx.Graph, nx.Graph]:
|
|
434
|
+
"""Return left/right fragments for either supported ITS representation."""
|
|
435
|
+
if format == "tuple":
|
|
436
|
+
reverter = ITSReverter(rc)
|
|
437
|
+
return reverter.to_reactant_graph(), reverter.to_product_graph()
|
|
438
|
+
return its_decompose(rc)
|
|
439
|
+
|
|
440
|
+
@staticmethod
|
|
441
|
+
def _normalize_relative_node_resources(rc: nx.Graph) -> None:
|
|
442
|
+
"""Store tuple resource edits as reactant demand and product supply."""
|
|
443
|
+
normalized_resources: list[str] = []
|
|
444
|
+
for resource in _RELATIVE_NODE_RESOURCES:
|
|
445
|
+
normalized = False
|
|
446
|
+
for _, attrs in rc.nodes(data=True):
|
|
447
|
+
value = attrs.get(resource)
|
|
448
|
+
if not (
|
|
449
|
+
isinstance(value, tuple)
|
|
450
|
+
and len(value) == 2
|
|
451
|
+
and all(
|
|
452
|
+
isinstance(endpoint, Real) and not isinstance(endpoint, bool)
|
|
453
|
+
for endpoint in value
|
|
454
|
+
)
|
|
455
|
+
):
|
|
456
|
+
continue
|
|
457
|
+
reactant, product = value
|
|
458
|
+
consumed = max(reactant - product, 0)
|
|
459
|
+
produced = max(product - reactant, 0)
|
|
460
|
+
attrs[resource] = (consumed, produced)
|
|
461
|
+
normalized = True
|
|
462
|
+
if normalized:
|
|
463
|
+
normalized_resources.append(resource)
|
|
464
|
+
if normalized_resources:
|
|
465
|
+
rc.graph["relative_node_resources"] = tuple(normalized_resources)
|
|
466
|
+
|
|
467
|
+
@staticmethod
|
|
468
|
+
def _project_wildcard_contracts(
|
|
469
|
+
rc: nx.Graph,
|
|
470
|
+
left: nx.Graph,
|
|
471
|
+
right: nx.Graph,
|
|
472
|
+
) -> None:
|
|
473
|
+
"""Preserve additive typed-wildcard metadata on rule endpoints."""
|
|
474
|
+
for node, attrs in rc.nodes(data=True):
|
|
475
|
+
contract = {
|
|
476
|
+
key: attrs[key] for key in _WILDCARD_CONTRACT_KEYS if key in attrs
|
|
477
|
+
}
|
|
478
|
+
if not contract:
|
|
479
|
+
continue
|
|
480
|
+
for endpoint in (left, right):
|
|
481
|
+
if node in endpoint:
|
|
482
|
+
endpoint.nodes[node].update(contract)
|
|
483
|
+
|
|
484
|
+
@staticmethod
|
|
485
|
+
def _reverse_wildcard_contracts(
|
|
486
|
+
original: nx.Graph,
|
|
487
|
+
reversed_graph: nx.Graph,
|
|
488
|
+
reactant_descriptors: Mapping[str, Any],
|
|
489
|
+
) -> None:
|
|
490
|
+
"""Transport typed wildcard contracts to the reversed source frame."""
|
|
491
|
+
atom_shapes = {
|
|
492
|
+
"tetrahedral",
|
|
493
|
+
"square_planar",
|
|
494
|
+
"trigonal_bipyramidal",
|
|
495
|
+
"octahedral",
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
def binding(descriptor: Any, reference: Any) -> tuple[Any, int] | None:
|
|
499
|
+
frame = descriptor.configuration.frame
|
|
500
|
+
positions = [
|
|
501
|
+
index for index, candidate in enumerate(frame) if candidate == reference
|
|
502
|
+
]
|
|
503
|
+
if len(positions) != 1:
|
|
504
|
+
return None
|
|
505
|
+
position = positions[0]
|
|
506
|
+
if descriptor.descriptor_class in atom_shapes:
|
|
507
|
+
return None if position == 0 else (frame[0], position - 1)
|
|
508
|
+
if position in {0, 1}:
|
|
509
|
+
return frame[2], position
|
|
510
|
+
if position in {4, 5}:
|
|
511
|
+
return frame[3], position - 4
|
|
512
|
+
return None
|
|
513
|
+
|
|
514
|
+
for node, attrs in original.nodes(data=True):
|
|
515
|
+
if "wildcard_role" not in attrs or node not in reversed_graph:
|
|
516
|
+
continue
|
|
517
|
+
contract = {
|
|
518
|
+
key: attrs[key] for key in _WILDCARD_CONTRACT_KEYS if key in attrs
|
|
519
|
+
}
|
|
520
|
+
side = contract.get("side")
|
|
521
|
+
if side == "reactant":
|
|
522
|
+
contract["side"] = "product"
|
|
523
|
+
elif side == "product":
|
|
524
|
+
contract["side"] = "reactant"
|
|
525
|
+
atom_map = attrs.get("atom_map", node)
|
|
526
|
+
if isinstance(atom_map, (tuple, list)) and len(atom_map) == 2:
|
|
527
|
+
atom_map = atom_map[1]
|
|
528
|
+
bindings = [
|
|
529
|
+
result
|
|
530
|
+
for descriptor in reactant_descriptors.values()
|
|
531
|
+
if (result := binding(descriptor, atom_map)) is not None
|
|
532
|
+
]
|
|
533
|
+
if contract.get("wildcard_role") == "stereo_ligand_port" and (
|
|
534
|
+
len(bindings) == 1
|
|
535
|
+
):
|
|
536
|
+
contract["owner"], contract["stereo_slot"] = bindings[0]
|
|
537
|
+
reversed_graph.nodes[node].update(contract)
|
|
538
|
+
|
|
539
|
+
@staticmethod
|
|
540
|
+
def _strip_explicit_h(
|
|
541
|
+
rc: nx.Graph,
|
|
542
|
+
left: nx.Graph,
|
|
543
|
+
right: nx.Graph,
|
|
544
|
+
) -> None:
|
|
545
|
+
"""Remove explicit hydrogens from rc, left, right—but only when *both*
|
|
546
|
+
left & right agree the H should be implicit.
|
|
547
|
+
|
|
548
|
+
Otherwise an H remains explicit in all three graphs.
|
|
549
|
+
"""
|
|
550
|
+
|
|
551
|
+
def _removable_on(graph: nx.Graph, h: str) -> bool:
|
|
552
|
+
# H+ (no neighbors) ⇒ not removable
|
|
553
|
+
nbrs = list(graph.neighbors(h))
|
|
554
|
+
if not nbrs:
|
|
555
|
+
return False
|
|
556
|
+
# H–H only ⇒ not removable
|
|
557
|
+
if all(graph.nodes[n].get("element") == "H" for n in nbrs):
|
|
558
|
+
return False
|
|
559
|
+
# otherwise bonded to ≥1 heavy ⇒ removable
|
|
560
|
+
return True
|
|
561
|
+
|
|
562
|
+
def _fully_removable(h: str) -> bool:
|
|
563
|
+
# only remove if BOTH left and right say removable
|
|
564
|
+
return _removable_on(left, h) and _removable_on(right, h)
|
|
565
|
+
|
|
566
|
+
# 1) initialize hcount & h_pairs
|
|
567
|
+
for g in (rc, left, right):
|
|
568
|
+
for n, data in g.nodes(data=True):
|
|
569
|
+
data["hcount"] = 0
|
|
570
|
+
if data.get("element") != "H":
|
|
571
|
+
data.setdefault("h_pairs", [])
|
|
572
|
+
|
|
573
|
+
# 2) shared H: only those removable on both sides
|
|
574
|
+
shared = sorted(
|
|
575
|
+
n
|
|
576
|
+
for n, d in left.nodes(data=True)
|
|
577
|
+
if d.get("element") == "H" and right.has_node(n) and _fully_removable(n)
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
pair_id = 1
|
|
581
|
+
for h in shared:
|
|
582
|
+
for g in (left, right, rc):
|
|
583
|
+
if not g.has_node(h):
|
|
584
|
+
continue
|
|
585
|
+
for nbr in list(g.neighbors(h)):
|
|
586
|
+
if g.nodes[nbr].get("element") != "H":
|
|
587
|
+
g.nodes[nbr]["hcount"] += 1
|
|
588
|
+
# only shared H get pair-IDs
|
|
589
|
+
g.nodes[nbr].setdefault("h_pairs", []).append(pair_id)
|
|
590
|
+
g.remove_node(h)
|
|
591
|
+
pair_id += 1
|
|
592
|
+
|
|
593
|
+
# 3) remaining explicit H in any graph: strip only if fully_removable
|
|
594
|
+
for g in (rc, left, right):
|
|
595
|
+
for h in [n for n, d in g.nodes(data=True) if d.get("element") == "H"]:
|
|
596
|
+
if not _fully_removable(h):
|
|
597
|
+
# at least one side wants to keep it explicit → skip
|
|
598
|
+
continue
|
|
599
|
+
# else both agree → convert to implicit
|
|
600
|
+
for nbr in list(g.neighbors(h)):
|
|
601
|
+
if g.nodes[nbr].get("element") != "H":
|
|
602
|
+
g.nodes[nbr]["hcount"] += 1
|
|
603
|
+
g.remove_node(h)
|
|
604
|
+
|
|
605
|
+
@staticmethod
|
|
606
|
+
def _strip_explicit_h_tuple(
|
|
607
|
+
rc: nx.Graph,
|
|
608
|
+
left: nx.Graph,
|
|
609
|
+
right: nx.Graph,
|
|
610
|
+
) -> None:
|
|
611
|
+
"""Tuple-style equivalent of legacy explicit-H stripping."""
|
|
612
|
+
|
|
613
|
+
def _removable_on(graph: nx.Graph, h: int) -> bool:
|
|
614
|
+
if not graph.has_node(h):
|
|
615
|
+
return False
|
|
616
|
+
nbrs = list(graph.neighbors(h))
|
|
617
|
+
if not nbrs:
|
|
618
|
+
return False
|
|
619
|
+
return not all(graph.nodes[n].get("element") == "H" for n in nbrs)
|
|
620
|
+
|
|
621
|
+
def _fully_removable(h: int) -> bool:
|
|
622
|
+
return _removable_on(left, h) and _removable_on(right, h)
|
|
623
|
+
|
|
624
|
+
for graph in (left, right):
|
|
625
|
+
for _, data in graph.nodes(data=True):
|
|
626
|
+
if data.get("element") != "H":
|
|
627
|
+
data.setdefault("h_pairs", [])
|
|
628
|
+
data.setdefault("h_pairs_left", [])
|
|
629
|
+
data.setdefault("h_pairs_right", [])
|
|
630
|
+
data.setdefault("h_pair_atom_maps", {})
|
|
631
|
+
|
|
632
|
+
for _, data in rc.nodes(data=True):
|
|
633
|
+
element = data.get("element")
|
|
634
|
+
is_h = (
|
|
635
|
+
isinstance(element, tuple)
|
|
636
|
+
and len(element) == 2
|
|
637
|
+
and all(value == "H" for value in element)
|
|
638
|
+
)
|
|
639
|
+
if not is_h:
|
|
640
|
+
data.setdefault("h_pairs", [])
|
|
641
|
+
data.setdefault("h_pairs_left", [])
|
|
642
|
+
data.setdefault("h_pairs_right", [])
|
|
643
|
+
data.setdefault("h_pair_atom_maps", {})
|
|
644
|
+
|
|
645
|
+
removable = sorted(
|
|
646
|
+
node
|
|
647
|
+
for node, attrs in left.nodes(data=True)
|
|
648
|
+
if attrs.get("element") == "H"
|
|
649
|
+
and right.has_node(node)
|
|
650
|
+
and _fully_removable(node)
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
for pair_id, h in enumerate(removable, start=1):
|
|
654
|
+
atom_map = left.nodes[h].get("atom_map", h)
|
|
655
|
+
for side, graph in (("left", left), ("right", right)):
|
|
656
|
+
for nbr in list(graph.neighbors(h)):
|
|
657
|
+
if graph.nodes[nbr].get("element") != "H":
|
|
658
|
+
graph.nodes[nbr]["hcount"] += 1
|
|
659
|
+
graph.nodes[nbr].setdefault("h_pairs", []).append(pair_id)
|
|
660
|
+
graph.nodes[nbr].setdefault(f"h_pairs_{side}", []).append(
|
|
661
|
+
pair_id
|
|
662
|
+
)
|
|
663
|
+
graph.nodes[nbr].setdefault("h_pair_atom_maps", {})[
|
|
664
|
+
pair_id
|
|
665
|
+
] = atom_map
|
|
666
|
+
graph.remove_node(h)
|
|
667
|
+
if rc.has_node(h):
|
|
668
|
+
rc.remove_node(h)
|
|
669
|
+
|
|
670
|
+
for node, attrs in rc.nodes(data=True):
|
|
671
|
+
if node not in left or node not in right:
|
|
672
|
+
continue
|
|
673
|
+
if attrs.get("element") == ("H", "H"):
|
|
674
|
+
continue
|
|
675
|
+
left_h = left.nodes[node].get("hcount", 0)
|
|
676
|
+
right_h = right.nodes[node].get("hcount", 0)
|
|
677
|
+
attrs["hcount"] = (left_h, right_h)
|
|
678
|
+
attrs["h_pairs"] = sorted(
|
|
679
|
+
set(left.nodes[node].get("h_pairs", []))
|
|
680
|
+
| set(right.nodes[node].get("h_pairs", []))
|
|
681
|
+
)
|
|
682
|
+
attrs["h_pairs_left"] = sorted(left.nodes[node].get("h_pairs_left", []))
|
|
683
|
+
attrs["h_pairs_right"] = sorted(right.nodes[node].get("h_pairs_right", []))
|
|
684
|
+
attrs["h_pair_atom_maps"] = {
|
|
685
|
+
**left.nodes[node].get("h_pair_atom_maps", {}),
|
|
686
|
+
**right.nodes[node].get("h_pair_atom_maps", {}),
|
|
687
|
+
}
|
|
688
|
+
typesgh = attrs.get("typesGH")
|
|
689
|
+
if typesgh and len(typesgh) == 2:
|
|
690
|
+
react_attr, prod_attr = typesgh
|
|
691
|
+
attrs["typesGH"] = (
|
|
692
|
+
tuple(list(react_attr[:2]) + [left_h] + list(react_attr[3:])),
|
|
693
|
+
tuple(list(prod_attr[:2]) + [right_h] + list(prod_attr[3:])),
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
# Dunder methods
|
|
697
|
+
def __eq__(self, other: object) -> bool:
|
|
698
|
+
return (
|
|
699
|
+
isinstance(other, SynRule)
|
|
700
|
+
and self.canonical_smiles == other.canonical_smiles
|
|
701
|
+
and self._stereo_signature() == other._stereo_signature()
|
|
702
|
+
and stereo_rule_isomorphic(self, other)
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
def __hash__(self) -> int:
|
|
706
|
+
return hash((self.canonical_smiles, self._stereo_signature()))
|
|
707
|
+
|
|
708
|
+
def _stereo_signature(self) -> tuple:
|
|
709
|
+
"""Return a coarse map-invariant prefilter for exact rule equality."""
|
|
710
|
+
guards = tuple(
|
|
711
|
+
sorted(
|
|
712
|
+
descriptor.descriptor_class
|
|
713
|
+
for descriptor in self.stereo_guards.values()
|
|
714
|
+
)
|
|
715
|
+
)
|
|
716
|
+
effects = tuple(
|
|
717
|
+
sorted(change.change for change in self.stereo_effects.values())
|
|
718
|
+
)
|
|
719
|
+
outcomes = tuple(
|
|
720
|
+
sorted(outcome.signature() for outcome in self.stereo_outcomes.values())
|
|
721
|
+
)
|
|
722
|
+
couplings = tuple(
|
|
723
|
+
sorted(
|
|
724
|
+
(coupling.kind, coupling.relation)
|
|
725
|
+
for coupling in self.stereo_couplings.values()
|
|
726
|
+
)
|
|
727
|
+
)
|
|
728
|
+
query_policies = tuple(sorted(self.stereo_query_policies.values()))
|
|
729
|
+
reverse_outcomes = tuple(
|
|
730
|
+
sorted(
|
|
731
|
+
outcome.signature()
|
|
732
|
+
for outcome in self._reverse_stereo_outcomes.values()
|
|
733
|
+
)
|
|
734
|
+
)
|
|
735
|
+
reverse_queries = tuple(sorted(self._reverse_stereo_query_policies.values()))
|
|
736
|
+
wildcard_contracts = []
|
|
737
|
+
for _, attrs in self.rc.raw.nodes(data=True):
|
|
738
|
+
if "wildcard_role" not in attrs:
|
|
739
|
+
continue
|
|
740
|
+
state = adapt_legacy_node_state(attrs)
|
|
741
|
+
if state.kind is not NodeStateKind.WILDCARD or state.constraint is None:
|
|
742
|
+
continue
|
|
743
|
+
values = list(state.constraint.normalized())
|
|
744
|
+
values[6] = state.constraint.owner is not None
|
|
745
|
+
wildcard_contracts.append(tuple(values))
|
|
746
|
+
return (
|
|
747
|
+
guards,
|
|
748
|
+
effects,
|
|
749
|
+
outcomes,
|
|
750
|
+
couplings,
|
|
751
|
+
query_policies,
|
|
752
|
+
reverse_outcomes,
|
|
753
|
+
reverse_queries,
|
|
754
|
+
tuple(sorted(wildcard_contracts, key=repr)),
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
def __str__(self) -> str:
|
|
758
|
+
if self._canon_enabled and self.canonical_smiles:
|
|
759
|
+
ls, rs = self.canonical_smiles
|
|
760
|
+
return f"<SynRule {self._name!r} left={ls[:8]}… right={rs[:8]}…>"
|
|
761
|
+
return f"<SynRule {self._name!r} (raw only)>"
|
|
762
|
+
|
|
763
|
+
@staticmethod
|
|
764
|
+
def _summary_descriptor(descriptor: Any) -> dict[str, Any]:
|
|
765
|
+
"""Return the chemically relevant part of one stored descriptor."""
|
|
766
|
+
return {
|
|
767
|
+
"atoms": list(descriptor.atoms),
|
|
768
|
+
"parity": descriptor.parity,
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
@staticmethod
|
|
772
|
+
def _reference_delta(before: Any, after: Any) -> dict[str, list[Any]]:
|
|
773
|
+
"""Report reference replacements while respecting multiplicity/order."""
|
|
774
|
+
remaining_after = list(after.atoms)
|
|
775
|
+
removed = []
|
|
776
|
+
for reference in before.atoms:
|
|
777
|
+
if reference in remaining_after:
|
|
778
|
+
remaining_after.remove(reference)
|
|
779
|
+
else:
|
|
780
|
+
removed.append(reference)
|
|
781
|
+
return {"removed": removed, "added": remaining_after}
|
|
782
|
+
|
|
783
|
+
def stereo_summary(self) -> dict[str, dict[str, Any]]:
|
|
784
|
+
"""Return a compact, per-target view of active stereo rule semantics.
|
|
785
|
+
|
|
786
|
+
The lossless graph metadata stores a reactant descriptor both as a
|
|
787
|
+
matching guard and as the ``before`` state of its change. This view
|
|
788
|
+
presents that descriptor once, alongside the product state, change,
|
|
789
|
+
reference replacement, matching policy, and optional branch outcome.
|
|
790
|
+
Parser provenance is intentionally omitted from this inspection view.
|
|
791
|
+
"""
|
|
792
|
+
targets = sorted(
|
|
793
|
+
set(self.stereo_guards)
|
|
794
|
+
| set(self.stereo_effects)
|
|
795
|
+
| set(self.stereo_outcomes)
|
|
796
|
+
| set(self.stereo_couplings)
|
|
797
|
+
| set(self.stereo_query_policies)
|
|
798
|
+
)
|
|
799
|
+
summary: dict[str, dict[str, Any]] = {}
|
|
800
|
+
for target in targets:
|
|
801
|
+
guard = self.stereo_guards.get(target)
|
|
802
|
+
effect = self.stereo_effects.get(target)
|
|
803
|
+
before = guard or (effect.before if effect else None)
|
|
804
|
+
after = effect.after if effect else None
|
|
805
|
+
transition = effect.transition if effect else None
|
|
806
|
+
descriptor = before or after or transition
|
|
807
|
+
|
|
808
|
+
item: dict[str, Any] = {}
|
|
809
|
+
if descriptor is not None:
|
|
810
|
+
item["descriptor"] = descriptor.descriptor_class
|
|
811
|
+
if target in self.stereo_query_policies:
|
|
812
|
+
item["query"] = self.stereo_query_policies[target]
|
|
813
|
+
if effect is not None:
|
|
814
|
+
item["change"] = effect.change
|
|
815
|
+
item["relation"] = effect.relation_evidence()
|
|
816
|
+
if before is not None:
|
|
817
|
+
item["reactant"] = self._summary_descriptor(before)
|
|
818
|
+
if after is not None:
|
|
819
|
+
item["product"] = self._summary_descriptor(after)
|
|
820
|
+
if before is not None and after is not None:
|
|
821
|
+
delta = self._reference_delta(before, after)
|
|
822
|
+
if delta["removed"] or delta["added"]:
|
|
823
|
+
item["reference_delta"] = delta
|
|
824
|
+
if transition is not None:
|
|
825
|
+
item["transition"] = self._summary_descriptor(transition)
|
|
826
|
+
outcome = self.stereo_outcomes.get(target)
|
|
827
|
+
if outcome is not None:
|
|
828
|
+
item["outcome"] = outcome.to_dict()
|
|
829
|
+
coupling = self.stereo_couplings.get(target)
|
|
830
|
+
if coupling is not None:
|
|
831
|
+
item["coupling"] = coupling.to_dict()
|
|
832
|
+
summary[target] = item
|
|
833
|
+
return summary
|
|
834
|
+
|
|
835
|
+
@staticmethod
|
|
836
|
+
def _repr_counts(values: tuple[str, ...]) -> str:
|
|
837
|
+
"""Return a compact, deterministic ``{value:count}`` summary."""
|
|
838
|
+
counts: dict[str, int] = {}
|
|
839
|
+
for value in values:
|
|
840
|
+
counts[value] = counts.get(value, 0) + 1
|
|
841
|
+
return (
|
|
842
|
+
"{" + ",".join(f"{value}:{counts[value]}" for value in sorted(counts)) + "}"
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
def _stereo_repr(self) -> str:
|
|
846
|
+
"""Summarize active rule-level stereochemical semantics for ``repr``."""
|
|
847
|
+
if not (
|
|
848
|
+
self.stereo_guards
|
|
849
|
+
or self.stereo_effects
|
|
850
|
+
or self.stereo_outcomes
|
|
851
|
+
or self.stereo_couplings
|
|
852
|
+
or self.stereo_query_policies
|
|
853
|
+
):
|
|
854
|
+
return "none"
|
|
855
|
+
|
|
856
|
+
guard_kinds = tuple(key.partition(":")[0] for key in self.stereo_guards)
|
|
857
|
+
effect_kinds = tuple(change.change for change in self.stereo_effects.values())
|
|
858
|
+
parts = [
|
|
859
|
+
f"guards={self._repr_counts(guard_kinds) if guard_kinds else '0'}",
|
|
860
|
+
f"effects={self._repr_counts(effect_kinds) if effect_kinds else '0'}",
|
|
861
|
+
]
|
|
862
|
+
if self.stereo_outcomes:
|
|
863
|
+
parts.append(
|
|
864
|
+
"outcomes="
|
|
865
|
+
+ self._repr_counts(
|
|
866
|
+
tuple(outcome.kind for outcome in self.stereo_outcomes.values())
|
|
867
|
+
)
|
|
868
|
+
)
|
|
869
|
+
if self.stereo_couplings:
|
|
870
|
+
parts.append(
|
|
871
|
+
"couplings="
|
|
872
|
+
+ self._repr_counts(
|
|
873
|
+
tuple(
|
|
874
|
+
coupling.relation for coupling in self.stereo_couplings.values()
|
|
875
|
+
)
|
|
876
|
+
)
|
|
877
|
+
)
|
|
878
|
+
if self.stereo_query_policies:
|
|
879
|
+
parts.append(
|
|
880
|
+
"queries="
|
|
881
|
+
+ self._repr_counts(tuple(self.stereo_query_policies.values()))
|
|
882
|
+
)
|
|
883
|
+
return "(" + ",".join(parts) + ")"
|
|
884
|
+
|
|
885
|
+
def __repr__(self) -> str:
|
|
886
|
+
try:
|
|
887
|
+
v_rc, e_rc = self.rc.raw.number_of_nodes(), self.rc.raw.number_of_edges()
|
|
888
|
+
v_l, e_l = self.left.raw.number_of_nodes(), self.left.raw.number_of_edges()
|
|
889
|
+
v_r, e_r = (
|
|
890
|
+
self.right.raw.number_of_nodes(),
|
|
891
|
+
self.right.raw.number_of_edges(),
|
|
892
|
+
)
|
|
893
|
+
except Exception:
|
|
894
|
+
v_rc = e_rc = v_l = e_l = v_r = e_r = 0
|
|
895
|
+
return (
|
|
896
|
+
f"SynRule(name={self._name!r}, "
|
|
897
|
+
f"rc=(|V|={v_rc},|E|={e_rc}), "
|
|
898
|
+
f"left=(|V|={v_l},|E|={e_l}), "
|
|
899
|
+
f"right=(|V|={v_r},|E|={e_r}), "
|
|
900
|
+
f"stereo={self._stereo_repr()})"
|
|
901
|
+
)
|
|
902
|
+
|
|
903
|
+
# Public API
|
|
904
|
+
def to_rule_span(
|
|
905
|
+
self,
|
|
906
|
+
*,
|
|
907
|
+
electron_complete: bool | None = None,
|
|
908
|
+
boundary: str = "abstract",
|
|
909
|
+
environment: Any = None,
|
|
910
|
+
) -> Any:
|
|
911
|
+
"""Return the native non-stereo LLG/DPO view of this rule."""
|
|
912
|
+
from synkit.Rule.Compose import synrule_to_span
|
|
913
|
+
|
|
914
|
+
return synrule_to_span(
|
|
915
|
+
self,
|
|
916
|
+
electron_complete=electron_complete,
|
|
917
|
+
boundary=boundary,
|
|
918
|
+
environment=environment,
|
|
919
|
+
)
|
|
920
|
+
|
|
921
|
+
def compose(
|
|
922
|
+
self,
|
|
923
|
+
other: "SynRule",
|
|
924
|
+
overlap: Mapping[Any, Any],
|
|
925
|
+
*,
|
|
926
|
+
overlap_edges: set[frozenset[Any]] | None = None,
|
|
927
|
+
electron_complete: bool | None = None,
|
|
928
|
+
) -> Any:
|
|
929
|
+
"""Natively compose with ``other`` along one explicit overlap map."""
|
|
930
|
+
from synkit.Rule.Compose import compose_synrules
|
|
931
|
+
|
|
932
|
+
options = {
|
|
933
|
+
"overlap_edges": overlap_edges,
|
|
934
|
+
"electron_complete": electron_complete,
|
|
935
|
+
}
|
|
936
|
+
return compose_synrules(self, other, overlap, **options)
|
|
937
|
+
|
|
938
|
+
def composition_candidates(
|
|
939
|
+
self,
|
|
940
|
+
other: "SynRule",
|
|
941
|
+
*,
|
|
942
|
+
limits: Any = None,
|
|
943
|
+
electron_complete: bool | None = None,
|
|
944
|
+
) -> Any:
|
|
945
|
+
"""Return every bounded native composition class and witness."""
|
|
946
|
+
from synkit.Rule.Compose import search_synrule_compositions
|
|
947
|
+
|
|
948
|
+
return search_synrule_compositions(
|
|
949
|
+
self,
|
|
950
|
+
other,
|
|
951
|
+
limits=limits,
|
|
952
|
+
electron_complete=electron_complete,
|
|
953
|
+
)
|
|
954
|
+
|
|
955
|
+
def non_invertible_stereo_targets(self) -> tuple[str, ...]:
|
|
956
|
+
"""Return targets whose stereo relation has no unique inverse."""
|
|
957
|
+
return tuple(
|
|
958
|
+
sorted(
|
|
959
|
+
target
|
|
960
|
+
for target, change in self.stereo_effects.items()
|
|
961
|
+
if change.non_invertible
|
|
962
|
+
)
|
|
963
|
+
)
|
|
964
|
+
|
|
965
|
+
@property
|
|
966
|
+
def is_stereo_reversible(self) -> bool:
|
|
967
|
+
"""Whether reverse construction can preserve stereo semantics."""
|
|
968
|
+
return not self.non_invertible_stereo_targets()
|
|
969
|
+
|
|
970
|
+
def reversed(
|
|
971
|
+
self,
|
|
972
|
+
*,
|
|
973
|
+
balance_its: bool = False,
|
|
974
|
+
semantics: str = "orbit",
|
|
975
|
+
diagnostics: list[Any] | None = None,
|
|
976
|
+
) -> "SynRule":
|
|
977
|
+
"""Return a rule with reactant/product stereo semantics reversed.
|
|
978
|
+
|
|
979
|
+
A forward two-enantiomer product outcome becomes an ``either``
|
|
980
|
+
reactant guard in reverse, because the reverse rule consumes either
|
|
981
|
+
enantiomer but does not create two achiral products. Reversing again
|
|
982
|
+
restores the original product outcome and query policies.
|
|
983
|
+
"""
|
|
984
|
+
non_invertible = self.non_invertible_stereo_targets()
|
|
985
|
+
if non_invertible:
|
|
986
|
+
raise NonInvertibleStereoEffectError(non_invertible)
|
|
987
|
+
|
|
988
|
+
# These are the authoritative endpoint graphs produced from the
|
|
989
|
+
# normalized reaction centre during construction. Reusing them avoids
|
|
990
|
+
# a redundant full ITS reversion on every backward replay.
|
|
991
|
+
left_graph, right_graph = self.left.raw, self.right.raw
|
|
992
|
+
if self._format == "tuple":
|
|
993
|
+
reversed_graph = ITSConstruction.construct(
|
|
994
|
+
right_graph,
|
|
995
|
+
left_graph,
|
|
996
|
+
balance_its=balance_its,
|
|
997
|
+
)
|
|
998
|
+
for node in reversed_graph:
|
|
999
|
+
consistency = self.rc.raw.nodes[node].get("charge_model_consistent")
|
|
1000
|
+
if isinstance(consistency, tuple) and len(consistency) == 2:
|
|
1001
|
+
reversed_graph.nodes[node]["charge_model_consistent"] = (
|
|
1002
|
+
consistency[1],
|
|
1003
|
+
consistency[0],
|
|
1004
|
+
)
|
|
1005
|
+
else:
|
|
1006
|
+
reversed_graph = ITSConstruction.ITSGraph(
|
|
1007
|
+
right_graph,
|
|
1008
|
+
left_graph,
|
|
1009
|
+
balance_its=balance_its,
|
|
1010
|
+
)
|
|
1011
|
+
|
|
1012
|
+
reversed_graph.graph["stereo_changes"] = {
|
|
1013
|
+
key: change.reverse(
|
|
1014
|
+
semantics=semantics,
|
|
1015
|
+
diagnostics=diagnostics,
|
|
1016
|
+
)
|
|
1017
|
+
for key, change in self.stereo_effects.items()
|
|
1018
|
+
}
|
|
1019
|
+
reversed_graph.graph["stereo_couplings"] = {
|
|
1020
|
+
key: coupling.relation for key, coupling in self.stereo_couplings.items()
|
|
1021
|
+
}
|
|
1022
|
+
sides = self.rc.raw.graph.get("stereo_descriptors", {})
|
|
1023
|
+
reversed_graph.graph["stereo_descriptors"] = {
|
|
1024
|
+
"reactant": dict(sides.get("product", {})),
|
|
1025
|
+
"product": dict(sides.get("reactant", {})),
|
|
1026
|
+
}
|
|
1027
|
+
if "transition" in sides:
|
|
1028
|
+
reversed_graph.graph["stereo_descriptors"]["transition"] = dict(
|
|
1029
|
+
sides["transition"]
|
|
1030
|
+
)
|
|
1031
|
+
self._reverse_wildcard_contracts(
|
|
1032
|
+
self.rc.raw,
|
|
1033
|
+
reversed_graph,
|
|
1034
|
+
reversed_graph.graph["stereo_descriptors"]["reactant"],
|
|
1035
|
+
)
|
|
1036
|
+
|
|
1037
|
+
if self._reverse_stereo_outcomes:
|
|
1038
|
+
# This rule is already the reverse view of a branching rule.
|
|
1039
|
+
reversed_graph.graph["stereo_outcomes"] = {
|
|
1040
|
+
key: outcome.to_dict()
|
|
1041
|
+
for key, outcome in self._reverse_stereo_outcomes.items()
|
|
1042
|
+
}
|
|
1043
|
+
reversed_graph.graph["stereo_query_policies"] = dict(
|
|
1044
|
+
self._reverse_stereo_query_policies
|
|
1045
|
+
)
|
|
1046
|
+
else:
|
|
1047
|
+
reversed_graph.graph["stereo_outcomes"] = {}
|
|
1048
|
+
reverse_policies = dict(self.stereo_query_policies)
|
|
1049
|
+
for key, outcome in self.stereo_outcomes.items():
|
|
1050
|
+
if outcome.kind != "SINGLE":
|
|
1051
|
+
reverse_policies[key] = "either"
|
|
1052
|
+
reversed_graph.graph["stereo_query_policies"] = reverse_policies
|
|
1053
|
+
if self.stereo_outcomes:
|
|
1054
|
+
reversed_graph.graph["stereo_reverse_outcomes"] = {
|
|
1055
|
+
key: outcome.to_dict()
|
|
1056
|
+
for key, outcome in self.stereo_outcomes.items()
|
|
1057
|
+
}
|
|
1058
|
+
reversed_graph.graph["stereo_reverse_query_policies"] = dict(
|
|
1059
|
+
self.stereo_query_policies
|
|
1060
|
+
)
|
|
1061
|
+
|
|
1062
|
+
return SynRule(
|
|
1063
|
+
reversed_graph,
|
|
1064
|
+
name=self._name,
|
|
1065
|
+
canonicaliser=self._canonicaliser,
|
|
1066
|
+
canon=self._canon_enabled,
|
|
1067
|
+
implicit_h=self._implicit_h,
|
|
1068
|
+
format=self._format,
|
|
1069
|
+
)
|
|
1070
|
+
|
|
1071
|
+
def help(self) -> None:
|
|
1072
|
+
"""Pretty-print raw / canonical contents for quick inspection."""
|
|
1073
|
+
print(f"SynRule name={self._name!r}")
|
|
1074
|
+
print("→ Full (raw) rc_graph edges:")
|
|
1075
|
+
for u, v, d in self.rc.raw.edges(data=True):
|
|
1076
|
+
print(f" ({u}, {v}): {d}")
|
|
1077
|
+
|
|
1078
|
+
if not self._canon_enabled:
|
|
1079
|
+
print("→ Canonicalisation disabled.")
|
|
1080
|
+
return
|
|
1081
|
+
|
|
1082
|
+
print("\n→ Full canonical_graph edges:")
|
|
1083
|
+
for u, v, d in self.rc.canonical.edges(data=True): # type: ignore[attr-defined]
|
|
1084
|
+
print(f" ({u}, {v}): {d}")
|
|
1085
|
+
|
|
1086
|
+
print("\n→ Left fragment:")
|
|
1087
|
+
self.left.help()
|
|
1088
|
+
print("\n→ Right fragment:")
|
|
1089
|
+
self.right.help()
|
|
1090
|
+
print("\n→ Fragment signatures:", self.canonical_smiles)
|