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,1032 @@
|
|
|
1
|
+
"""Typed evidence for molecular stereo-element perception.
|
|
2
|
+
|
|
3
|
+
Perception answers whether the molecular input can support a stereo element.
|
|
4
|
+
It does not assign a configuration, a CIP label, configurational stability, or
|
|
5
|
+
whole-molecule chirality. Keeping this boundary explicit prevents potential
|
|
6
|
+
topology from being promoted to an invented stereochemical state.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from enum import Enum
|
|
13
|
+
import hashlib
|
|
14
|
+
from typing import Iterable, Mapping
|
|
15
|
+
|
|
16
|
+
import networkx as nx
|
|
17
|
+
from networkx.algorithms.isomorphism import (
|
|
18
|
+
GraphMatcher,
|
|
19
|
+
categorical_edge_match,
|
|
20
|
+
categorical_node_match,
|
|
21
|
+
)
|
|
22
|
+
from rdkit import Chem
|
|
23
|
+
|
|
24
|
+
from synkit.Graph.Stereo.descriptors import (
|
|
25
|
+
Reference,
|
|
26
|
+
TetrahedralStereo,
|
|
27
|
+
virtual_reference,
|
|
28
|
+
)
|
|
29
|
+
from synkit.Graph.Stereo.supports import (
|
|
30
|
+
AtomStereoSupport,
|
|
31
|
+
AxisStereoSupport,
|
|
32
|
+
BondStereoSupport,
|
|
33
|
+
PathStereoSupport,
|
|
34
|
+
StereoSupport,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
from ._stereo_axis_evidence import (
|
|
38
|
+
StereoCarrierStatus,
|
|
39
|
+
validated_carrier_status,
|
|
40
|
+
)
|
|
41
|
+
from ._stereo_orientation_constraints import (
|
|
42
|
+
LocalOrientationConstraints,
|
|
43
|
+
extract_local_orientation_constraints,
|
|
44
|
+
tetrahedral_stabilizer_result,
|
|
45
|
+
)
|
|
46
|
+
from ._tetrahedral_carriers import (
|
|
47
|
+
detect_tetrahedral_carriers,
|
|
48
|
+
hidden_hydrogen_count as _hidden_hydrogen_count,
|
|
49
|
+
is_tetrahedral_carrier as _is_tetrahedral_carrier,
|
|
50
|
+
lone_pair_count as _lone_pair_count,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class StereoElementType(str, Enum):
|
|
55
|
+
"""Stereo-element classes currently recognized from molecular input."""
|
|
56
|
+
|
|
57
|
+
TETRAHEDRAL = "tetrahedral"
|
|
58
|
+
DOUBLE_BOND = "double_bond"
|
|
59
|
+
CUMULENE_AXIS = "cumulene_axis"
|
|
60
|
+
EXTENDED_CIS_TRANS = "extended_cis_trans"
|
|
61
|
+
ATROP_AXIS = "atrop_axis"
|
|
62
|
+
HELICAL = "helical"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class StereoConfigurationState(str, Enum):
|
|
66
|
+
"""Whether the input supplies a configuration for a perceived element."""
|
|
67
|
+
|
|
68
|
+
SPECIFIED = "specified"
|
|
69
|
+
UNSPECIFIED = "unspecified"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class TetrahedralConstitutionStatus(str, Enum):
|
|
73
|
+
"""Canonicalization conclusion for one tetrahedral carrier."""
|
|
74
|
+
|
|
75
|
+
CONSTITUTIONALLY_DISTINCT = "constitutionally_distinct"
|
|
76
|
+
CONSTITUTIONALLY_ORIENTATION_DISTINCT = "constitutionally_orientation_distinct"
|
|
77
|
+
STEREO_DEPENDENT_DISTINCT = "stereo_dependent_distinct"
|
|
78
|
+
SYMMETRY_RELATED = "symmetry_related"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class TetrahedralFrameStatus(str, Enum):
|
|
82
|
+
"""Outcome of configuration-neutral tetrahedral frame construction."""
|
|
83
|
+
|
|
84
|
+
CANONICAL = "canonical"
|
|
85
|
+
ORIENTATION_ORBIT = "orientation_orbit"
|
|
86
|
+
SYMMETRY_RELATED = "symmetry_related"
|
|
87
|
+
NEIGHBORHOOD_KEY_COLLISION = "neighborhood_key_collision"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True, order=True)
|
|
91
|
+
class LocalNeighborKey:
|
|
92
|
+
"""Recursive, configuration-neutral key for one ligand environment.
|
|
93
|
+
|
|
94
|
+
``digest`` is derived only from constitutional atom/bond attributes and
|
|
95
|
+
recursively refined neighboring keys. It never includes atom indices,
|
|
96
|
+
supplied stereo, CIP properties, or coordinates. Exact carrier-fixed
|
|
97
|
+
automorphisms remain authoritative when two references share a key.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
digest: str
|
|
101
|
+
depth: int
|
|
102
|
+
algorithm: str = "recursive_local_neighbor_sha256_v2"
|
|
103
|
+
|
|
104
|
+
def __post_init__(self) -> None:
|
|
105
|
+
if len(self.digest) != 64 or any(
|
|
106
|
+
character not in "0123456789abcdef" for character in self.digest
|
|
107
|
+
):
|
|
108
|
+
raise ValueError("Local-neighbor keys require a SHA-256 hex digest.")
|
|
109
|
+
if type(self.depth) is not int or self.depth < 0:
|
|
110
|
+
raise ValueError("Local-neighbor key depth must be non-negative.")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass(frozen=True)
|
|
114
|
+
class LigandSymmetryClass:
|
|
115
|
+
"""Ligand slots equivalent under automorphisms fixing their owner."""
|
|
116
|
+
|
|
117
|
+
references: tuple[Reference, ...]
|
|
118
|
+
multiplicity: int
|
|
119
|
+
neighborhood_key: LocalNeighborKey | None = None
|
|
120
|
+
|
|
121
|
+
def __post_init__(self) -> None:
|
|
122
|
+
object.__setattr__(self, "references", tuple(self.references))
|
|
123
|
+
if not self.references:
|
|
124
|
+
raise ValueError("Ligand symmetry classes cannot be empty.")
|
|
125
|
+
if type(self.multiplicity) is not int or self.multiplicity < 1:
|
|
126
|
+
raise ValueError("Ligand symmetry multiplicity must be positive.")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass(frozen=True)
|
|
130
|
+
class TetrahedralConstitution:
|
|
131
|
+
"""Exact local-symmetry evidence for a tetrahedral carrier.
|
|
132
|
+
|
|
133
|
+
The authoritative criterion is the parity action of the exact center
|
|
134
|
+
stabilizer on the four coordination slots: the carrier is stereogenic
|
|
135
|
+
exactly when no allowed automorphism induces an odd slot permutation.
|
|
136
|
+
``ligand_classes`` records the coarser slot orbits for diagnostics. Local
|
|
137
|
+
orientation constraints may restrict the stabilizer, but configuration at
|
|
138
|
+
the focal center, CIP labels, ranks, and atom indices never participate.
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
support: AtomStereoSupport
|
|
142
|
+
ligand_classes: tuple[LigandSymmetryClass, ...]
|
|
143
|
+
status: TetrahedralConstitutionStatus
|
|
144
|
+
automorphism_witness_checks: int
|
|
145
|
+
canonical_frame: tuple[Reference, ...] | None = None
|
|
146
|
+
frame_status: TetrahedralFrameStatus = TetrahedralFrameStatus.SYMMETRY_RELATED
|
|
147
|
+
refinement_depth: int = 0
|
|
148
|
+
dependency_depth: int = 0
|
|
149
|
+
stereo_marker_count: int = 0
|
|
150
|
+
method: str = "exact_center_stabilizer_slot_parity_v1"
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def ligand_count(self) -> int:
|
|
154
|
+
return sum(item.multiplicity for item in self.ligand_classes)
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def confirms_stereogenic_center(self) -> bool:
|
|
158
|
+
return self.status in {
|
|
159
|
+
TetrahedralConstitutionStatus.CONSTITUTIONALLY_DISTINCT,
|
|
160
|
+
TetrahedralConstitutionStatus.CONSTITUTIONALLY_ORIENTATION_DISTINCT,
|
|
161
|
+
TetrahedralConstitutionStatus.STEREO_DEPENDENT_DISTINCT,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def is_stereo_dependent(self) -> bool:
|
|
166
|
+
return self.status is TetrahedralConstitutionStatus.STEREO_DEPENDENT_DISTINCT
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def has_canonical_frame(self) -> bool:
|
|
170
|
+
return self.frame_status is TetrahedralFrameStatus.CANONICAL
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
_EXPECTED_SUPPORT = {
|
|
174
|
+
StereoElementType.TETRAHEDRAL: AtomStereoSupport,
|
|
175
|
+
StereoElementType.DOUBLE_BOND: BondStereoSupport,
|
|
176
|
+
StereoElementType.CUMULENE_AXIS: AxisStereoSupport,
|
|
177
|
+
StereoElementType.EXTENDED_CIS_TRANS: AxisStereoSupport,
|
|
178
|
+
StereoElementType.ATROP_AXIS: AxisStereoSupport,
|
|
179
|
+
StereoElementType.HELICAL: PathStereoSupport,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@dataclass(frozen=True)
|
|
184
|
+
class PotentialStereoElement:
|
|
185
|
+
"""One perceived stereo carrier, separate from configured stereo state.
|
|
186
|
+
|
|
187
|
+
Atom references use zero-based RDKit indices. ``source_identifier`` is a
|
|
188
|
+
diagnostic pointer into the perception source, not part of chemical
|
|
189
|
+
identity. In particular, a returned axis carries no barrier or handedness
|
|
190
|
+
claim.
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
element_type: StereoElementType
|
|
194
|
+
support: StereoSupport
|
|
195
|
+
configuration_state: StereoConfigurationState
|
|
196
|
+
evidence_provenance: str
|
|
197
|
+
source_identifier: str
|
|
198
|
+
constitutional_evidence: TetrahedralConstitution | None = None
|
|
199
|
+
configuration: TetrahedralStereo | None = None
|
|
200
|
+
carrier_status: StereoCarrierStatus = StereoCarrierStatus.CONFIRMED
|
|
201
|
+
carrier_reason: str | None = None
|
|
202
|
+
|
|
203
|
+
def __post_init__(self) -> None:
|
|
204
|
+
object.__setattr__(self, "element_type", StereoElementType(self.element_type))
|
|
205
|
+
object.__setattr__(
|
|
206
|
+
self,
|
|
207
|
+
"configuration_state",
|
|
208
|
+
StereoConfigurationState(self.configuration_state),
|
|
209
|
+
)
|
|
210
|
+
object.__setattr__(
|
|
211
|
+
self,
|
|
212
|
+
"carrier_status",
|
|
213
|
+
validated_carrier_status(self.carrier_status, self.carrier_reason),
|
|
214
|
+
)
|
|
215
|
+
expected = _EXPECTED_SUPPORT[self.element_type]
|
|
216
|
+
if not isinstance(self.support, expected):
|
|
217
|
+
raise TypeError(
|
|
218
|
+
f"{self.element_type.value} perception requires "
|
|
219
|
+
f"{expected.__name__}, not {type(self.support).__name__}."
|
|
220
|
+
)
|
|
221
|
+
if not self.evidence_provenance:
|
|
222
|
+
raise ValueError("Stereo perception provenance cannot be empty.")
|
|
223
|
+
if not self.source_identifier:
|
|
224
|
+
raise ValueError("Stereo perception source identifier cannot be empty.")
|
|
225
|
+
if self.constitutional_evidence is not None:
|
|
226
|
+
if self.element_type is not StereoElementType.TETRAHEDRAL:
|
|
227
|
+
raise TypeError(
|
|
228
|
+
"Constitutional evidence is currently supported only for "
|
|
229
|
+
"tetrahedral elements."
|
|
230
|
+
)
|
|
231
|
+
if self.constitutional_evidence.support != self.support:
|
|
232
|
+
raise ValueError(
|
|
233
|
+
"Constitutional evidence and perceived support disagree."
|
|
234
|
+
)
|
|
235
|
+
if self.configuration is not None:
|
|
236
|
+
if self.element_type is not StereoElementType.TETRAHEDRAL:
|
|
237
|
+
raise TypeError(
|
|
238
|
+
"Attached configuration is currently supported only for "
|
|
239
|
+
"tetrahedral elements."
|
|
240
|
+
)
|
|
241
|
+
if self.configuration.support != self.support:
|
|
242
|
+
raise ValueError("Configuration and perceived support disagree.")
|
|
243
|
+
if self.configuration_state is not StereoConfigurationState.SPECIFIED:
|
|
244
|
+
raise ValueError(
|
|
245
|
+
"An attached configuration requires specified input evidence."
|
|
246
|
+
)
|
|
247
|
+
if self.constitutional_evidence is None:
|
|
248
|
+
raise ValueError(
|
|
249
|
+
"Attached configuration requires constitutional evidence."
|
|
250
|
+
)
|
|
251
|
+
if self.constitutional_evidence.canonical_frame != self.configuration.atoms:
|
|
252
|
+
raise ValueError(
|
|
253
|
+
"Attached configuration must use the constitutional frame."
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
@property
|
|
257
|
+
def identifier(self) -> str:
|
|
258
|
+
"""Return a deterministic support identifier for diagnostics."""
|
|
259
|
+
if isinstance(self.support, AtomStereoSupport):
|
|
260
|
+
carrier = str(self.support.center)
|
|
261
|
+
elif isinstance(self.support, BondStereoSupport):
|
|
262
|
+
carrier = "-".join(str(value) for value in self.support.endpoints)
|
|
263
|
+
else:
|
|
264
|
+
carrier = "-".join(str(value) for value in self.support.path)
|
|
265
|
+
return f"{self.element_type.value}:{carrier}"
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@dataclass(frozen=True)
|
|
269
|
+
class TetrahedralPerceptionResult:
|
|
270
|
+
"""Fixed-point result for primary and stereo-dependent tetrahedral centers."""
|
|
271
|
+
|
|
272
|
+
carrier_evidence: tuple[TetrahedralConstitution, ...]
|
|
273
|
+
elements: tuple[PotentialStereoElement, ...]
|
|
274
|
+
dependency_iterations: int
|
|
275
|
+
stereo_markers: tuple[tuple[int, tuple[str, int]], ...]
|
|
276
|
+
|
|
277
|
+
def __post_init__(self) -> None:
|
|
278
|
+
object.__setattr__(self, "carrier_evidence", tuple(self.carrier_evidence))
|
|
279
|
+
object.__setattr__(self, "elements", tuple(self.elements))
|
|
280
|
+
object.__setattr__(self, "stereo_markers", tuple(self.stereo_markers))
|
|
281
|
+
if (
|
|
282
|
+
type(self.dependency_iterations) is not int
|
|
283
|
+
or self.dependency_iterations < 0
|
|
284
|
+
):
|
|
285
|
+
raise ValueError("Dependency iteration count must be non-negative.")
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _configuration_state(info: object) -> StereoConfigurationState:
|
|
289
|
+
specified = getattr(info, "specified", None)
|
|
290
|
+
if specified == Chem.StereoSpecified.Specified:
|
|
291
|
+
return StereoConfigurationState.SPECIFIED
|
|
292
|
+
return StereoConfigurationState.UNSPECIFIED
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _cumulated_double_bonds(molecule: Chem.Mol) -> frozenset[int]:
|
|
296
|
+
"""Return double bonds that belong to a run of at least two double bonds."""
|
|
297
|
+
double_bonds = [
|
|
298
|
+
bond
|
|
299
|
+
for bond in molecule.GetBonds()
|
|
300
|
+
if bond.GetBondType() == Chem.BondType.DOUBLE
|
|
301
|
+
]
|
|
302
|
+
incidence: dict[int, list[int]] = {}
|
|
303
|
+
for bond in double_bonds:
|
|
304
|
+
for atom in (bond.GetBeginAtomIdx(), bond.GetEndAtomIdx()):
|
|
305
|
+
incidence.setdefault(atom, []).append(bond.GetIdx())
|
|
306
|
+
cumulated = {
|
|
307
|
+
bond_index
|
|
308
|
+
for bond_indices in incidence.values()
|
|
309
|
+
if len(bond_indices) > 1
|
|
310
|
+
for bond_index in bond_indices
|
|
311
|
+
}
|
|
312
|
+
return frozenset(cumulated)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _constitutional_graph(
|
|
316
|
+
molecule: Chem.Mol,
|
|
317
|
+
center: int,
|
|
318
|
+
stereo_markers: Mapping[int, tuple[str, int]] | None = None,
|
|
319
|
+
) -> nx.Graph:
|
|
320
|
+
"""Build the attributed component used for exact center stabilizers."""
|
|
321
|
+
markers = stereo_markers or {}
|
|
322
|
+
graph = nx.Graph()
|
|
323
|
+
for atom in molecule.GetAtoms():
|
|
324
|
+
index = atom.GetIdx()
|
|
325
|
+
graph.add_node(
|
|
326
|
+
index,
|
|
327
|
+
atomic_number=atom.GetAtomicNum(),
|
|
328
|
+
isotope=atom.GetIsotope(),
|
|
329
|
+
formal_charge=atom.GetFormalCharge(),
|
|
330
|
+
radical_electrons=atom.GetNumRadicalElectrons(),
|
|
331
|
+
hcount=_hidden_hydrogen_count(atom),
|
|
332
|
+
aromatic=atom.GetIsAromatic(),
|
|
333
|
+
stereo_anchor=index == center,
|
|
334
|
+
stereo_marker=None if index == center else markers.get(index),
|
|
335
|
+
)
|
|
336
|
+
for bond in molecule.GetBonds():
|
|
337
|
+
graph.add_edge(
|
|
338
|
+
bond.GetBeginAtomIdx(),
|
|
339
|
+
bond.GetEndAtomIdx(),
|
|
340
|
+
bond_type=str(bond.GetBondType()),
|
|
341
|
+
aromatic=bond.GetIsAromatic(),
|
|
342
|
+
)
|
|
343
|
+
component = nx.node_connected_component(graph, center)
|
|
344
|
+
return graph.subgraph(component).copy()
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _local_key_digest(value: object) -> str:
|
|
348
|
+
return hashlib.sha256(repr(value).encode("utf-8")).hexdigest()
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _local_key_node_seed(attributes: Mapping[str, object]) -> tuple[object, ...]:
|
|
352
|
+
"""Return the constitution-only seed; map IDs and stereo are absent."""
|
|
353
|
+
return (
|
|
354
|
+
int(attributes["atomic_number"]),
|
|
355
|
+
int(attributes["isotope"]),
|
|
356
|
+
int(attributes["formal_charge"]),
|
|
357
|
+
int(attributes["radical_electrons"]),
|
|
358
|
+
int(attributes["hcount"]),
|
|
359
|
+
bool(attributes["aromatic"]),
|
|
360
|
+
bool(attributes["stereo_anchor"]),
|
|
361
|
+
attributes["stereo_marker"],
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _local_key_edge_seed(attributes: Mapping[str, object]) -> tuple[object, ...]:
|
|
366
|
+
return (
|
|
367
|
+
str(attributes["bond_type"]),
|
|
368
|
+
bool(attributes["aromatic"]),
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _local_key_graph(
|
|
373
|
+
molecule: Chem.Mol,
|
|
374
|
+
center: int,
|
|
375
|
+
stereo_markers: Mapping[int, tuple[str, int]] | None = None,
|
|
376
|
+
) -> tuple[nx.Graph, tuple[object, ...]]:
|
|
377
|
+
"""Materialize center-owned hydrogens for local recursive refinement."""
|
|
378
|
+
graph = _constitutional_graph(molecule, center, stereo_markers)
|
|
379
|
+
hidden_hydrogens = _hidden_hydrogen_count(molecule.GetAtomWithIdx(center))
|
|
380
|
+
virtual_nodes: list[object] = []
|
|
381
|
+
if hidden_hydrogens:
|
|
382
|
+
# The count is represented by explicit virtual leaves below, so it must
|
|
383
|
+
# not also remain folded into the center seed.
|
|
384
|
+
graph.nodes[center]["hcount"] = 0
|
|
385
|
+
for occurrence in range(hidden_hydrogens):
|
|
386
|
+
node = ("virtual_H", center, occurrence)
|
|
387
|
+
virtual_nodes.append(node)
|
|
388
|
+
graph.add_node(
|
|
389
|
+
node,
|
|
390
|
+
atomic_number=1,
|
|
391
|
+
isotope=0,
|
|
392
|
+
formal_charge=0,
|
|
393
|
+
radical_electrons=0,
|
|
394
|
+
hcount=0,
|
|
395
|
+
aromatic=False,
|
|
396
|
+
stereo_anchor=False,
|
|
397
|
+
stereo_marker=None,
|
|
398
|
+
)
|
|
399
|
+
graph.add_edge(
|
|
400
|
+
center,
|
|
401
|
+
node,
|
|
402
|
+
bond_type=str(Chem.BondType.SINGLE),
|
|
403
|
+
aromatic=False,
|
|
404
|
+
)
|
|
405
|
+
return graph, tuple(virtual_nodes)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def tetrahedral_local_neighbor_keys(
|
|
409
|
+
molecule: Chem.Mol,
|
|
410
|
+
center: int,
|
|
411
|
+
*,
|
|
412
|
+
stereo_markers: Mapping[int, tuple[str, int]] | None = None,
|
|
413
|
+
) -> Mapping[Reference, LocalNeighborKey]:
|
|
414
|
+
"""Return recursive constitutional keys for a tetrahedral carrier's ligands.
|
|
415
|
+
|
|
416
|
+
Refinement runs for the size of the complete connected component, including
|
|
417
|
+
center-owned virtual hydrogens, so information can propagate across rings
|
|
418
|
+
and through the full ligand environment. SHA-256 values are ordering keys,
|
|
419
|
+
not equality proof: exact center-stabilizer orbits audit every tie.
|
|
420
|
+
"""
|
|
421
|
+
if molecule is None:
|
|
422
|
+
raise ValueError("Tetrahedral local-key construction requires a molecule.")
|
|
423
|
+
if type(center) is not int or center < 0 or center >= molecule.GetNumAtoms():
|
|
424
|
+
raise ValueError(f"Tetrahedral center {center!r} is absent.")
|
|
425
|
+
working = Chem.Mol(molecule)
|
|
426
|
+
atom = working.GetAtomWithIdx(center)
|
|
427
|
+
if not _is_tetrahedral_carrier(atom):
|
|
428
|
+
raise ValueError(f"Atom {center} is not a supported tetrahedral carrier.")
|
|
429
|
+
|
|
430
|
+
graph, virtual_nodes = _local_key_graph(
|
|
431
|
+
working,
|
|
432
|
+
center,
|
|
433
|
+
stereo_markers,
|
|
434
|
+
)
|
|
435
|
+
colors = {
|
|
436
|
+
node: _local_key_digest(
|
|
437
|
+
("local_neighbor_seed_v2", _local_key_node_seed(attributes))
|
|
438
|
+
)
|
|
439
|
+
for node, attributes in graph.nodes(data=True)
|
|
440
|
+
}
|
|
441
|
+
depth = len(graph)
|
|
442
|
+
for _round in range(depth):
|
|
443
|
+
colors = {
|
|
444
|
+
node: _local_key_digest(
|
|
445
|
+
(
|
|
446
|
+
"local_neighbor_refine_v2",
|
|
447
|
+
colors[node],
|
|
448
|
+
tuple(
|
|
449
|
+
sorted(
|
|
450
|
+
(
|
|
451
|
+
_local_key_edge_seed(graph.edges[node, neighbor]),
|
|
452
|
+
colors[neighbor],
|
|
453
|
+
)
|
|
454
|
+
for neighbor in graph.neighbors(node)
|
|
455
|
+
)
|
|
456
|
+
),
|
|
457
|
+
)
|
|
458
|
+
)
|
|
459
|
+
for node in graph
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
keys: dict[Reference, LocalNeighborKey] = {
|
|
463
|
+
neighbor.GetIdx(): LocalNeighborKey(colors[neighbor.GetIdx()], depth)
|
|
464
|
+
for neighbor in atom.GetNeighbors()
|
|
465
|
+
}
|
|
466
|
+
if virtual_nodes:
|
|
467
|
+
virtual_digests = {colors[node] for node in virtual_nodes}
|
|
468
|
+
if len(virtual_digests) != 1:
|
|
469
|
+
raise RuntimeError(
|
|
470
|
+
"Equivalent center-owned hydrogens received unequal keys."
|
|
471
|
+
)
|
|
472
|
+
keys[virtual_reference("H", center)] = LocalNeighborKey(
|
|
473
|
+
virtual_digests.pop(),
|
|
474
|
+
depth,
|
|
475
|
+
)
|
|
476
|
+
if _lone_pair_count(atom):
|
|
477
|
+
keys[virtual_reference("LP", center)] = LocalNeighborKey(
|
|
478
|
+
_local_key_digest(("virtual_lone_pair_v2",)),
|
|
479
|
+
depth,
|
|
480
|
+
)
|
|
481
|
+
return keys
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
_CONSTITUTIONAL_NODE_ATTRIBUTES = (
|
|
485
|
+
"atomic_number",
|
|
486
|
+
"isotope",
|
|
487
|
+
"formal_charge",
|
|
488
|
+
"radical_electrons",
|
|
489
|
+
"hcount",
|
|
490
|
+
"aromatic",
|
|
491
|
+
"stereo_anchor",
|
|
492
|
+
"stereo_marker",
|
|
493
|
+
"ligand_probe",
|
|
494
|
+
)
|
|
495
|
+
_CONSTITUTIONAL_EDGE_ATTRIBUTES = ("bond_type", "aromatic")
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _center_fixed_ligands_are_equivalent(
|
|
499
|
+
graph: nx.Graph,
|
|
500
|
+
left: int,
|
|
501
|
+
right: int,
|
|
502
|
+
) -> bool:
|
|
503
|
+
"""Test one exact ligand-orbit relation without enumerating the group."""
|
|
504
|
+
left_rooted = graph.copy()
|
|
505
|
+
right_rooted = graph.copy()
|
|
506
|
+
nx.set_node_attributes(left_rooted, False, "ligand_probe")
|
|
507
|
+
nx.set_node_attributes(right_rooted, False, "ligand_probe")
|
|
508
|
+
left_rooted.nodes[left]["ligand_probe"] = True
|
|
509
|
+
right_rooted.nodes[right]["ligand_probe"] = True
|
|
510
|
+
matcher = GraphMatcher(
|
|
511
|
+
left_rooted,
|
|
512
|
+
right_rooted,
|
|
513
|
+
node_match=categorical_node_match(
|
|
514
|
+
_CONSTITUTIONAL_NODE_ATTRIBUTES,
|
|
515
|
+
(0, 0, 0, 0, 0, False, False, None, False),
|
|
516
|
+
),
|
|
517
|
+
edge_match=categorical_edge_match(
|
|
518
|
+
_CONSTITUTIONAL_EDGE_ATTRIBUTES,
|
|
519
|
+
("", False),
|
|
520
|
+
),
|
|
521
|
+
)
|
|
522
|
+
return matcher.is_isomorphic()
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _exact_material_ligand_classes(
|
|
526
|
+
graph: nx.Graph,
|
|
527
|
+
neighbors: set[int],
|
|
528
|
+
local_keys: Mapping[Reference, LocalNeighborKey],
|
|
529
|
+
) -> tuple[list[LigandSymmetryClass], int]:
|
|
530
|
+
"""Partition only tied local-key groups by exact rooted isomorphism."""
|
|
531
|
+
by_key: dict[LocalNeighborKey, list[int]] = {}
|
|
532
|
+
for reference in neighbors:
|
|
533
|
+
by_key.setdefault(local_keys[reference], []).append(reference)
|
|
534
|
+
|
|
535
|
+
classes = []
|
|
536
|
+
witness_checks = 0
|
|
537
|
+
for key, references in by_key.items():
|
|
538
|
+
parent = {reference: reference for reference in references}
|
|
539
|
+
|
|
540
|
+
def find(reference: int) -> int:
|
|
541
|
+
while parent[reference] != reference:
|
|
542
|
+
parent[reference] = parent[parent[reference]]
|
|
543
|
+
reference = parent[reference]
|
|
544
|
+
return reference
|
|
545
|
+
|
|
546
|
+
def union(left: int, right: int) -> None:
|
|
547
|
+
left_root = find(left)
|
|
548
|
+
right_root = find(right)
|
|
549
|
+
if left_root != right_root:
|
|
550
|
+
parent[right_root] = left_root
|
|
551
|
+
|
|
552
|
+
for left_position, left in enumerate(references):
|
|
553
|
+
for right in references[left_position + 1 :]:
|
|
554
|
+
witness_checks += 1
|
|
555
|
+
if _center_fixed_ligands_are_equivalent(graph, left, right):
|
|
556
|
+
union(left, right)
|
|
557
|
+
|
|
558
|
+
members: dict[int, list[int]] = {}
|
|
559
|
+
for reference in references:
|
|
560
|
+
members.setdefault(find(reference), []).append(reference)
|
|
561
|
+
classes.extend(
|
|
562
|
+
LigandSymmetryClass(
|
|
563
|
+
tuple(sorted(values)),
|
|
564
|
+
len(values),
|
|
565
|
+
key,
|
|
566
|
+
)
|
|
567
|
+
for values in members.values()
|
|
568
|
+
)
|
|
569
|
+
return classes, witness_checks
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _validated_tetrahedral_atom(
|
|
573
|
+
molecule: Chem.Mol,
|
|
574
|
+
center: int,
|
|
575
|
+
dependency_depth: int,
|
|
576
|
+
) -> tuple[Chem.Mol, Chem.Atom]:
|
|
577
|
+
"""Validate one constitutional request and return an isolated atom view."""
|
|
578
|
+
if molecule is None:
|
|
579
|
+
raise ValueError("Tetrahedral constitutional analysis requires a molecule.")
|
|
580
|
+
if type(center) is not int or center < 0 or center >= molecule.GetNumAtoms():
|
|
581
|
+
raise ValueError(f"Tetrahedral center {center!r} is absent.")
|
|
582
|
+
if type(dependency_depth) is not int or dependency_depth < 0:
|
|
583
|
+
raise ValueError("Tetrahedral dependency depth must be non-negative.")
|
|
584
|
+
working = Chem.Mol(molecule)
|
|
585
|
+
atom = working.GetAtomWithIdx(center)
|
|
586
|
+
if not _is_tetrahedral_carrier(atom):
|
|
587
|
+
raise ValueError(f"Atom {center} is not a supported tetrahedral carrier.")
|
|
588
|
+
return working, atom
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def canonicalize_tetrahedral_constitution(
|
|
592
|
+
molecule: Chem.Mol,
|
|
593
|
+
center: int,
|
|
594
|
+
*,
|
|
595
|
+
stereo_markers: Mapping[int, tuple[str, int]] | None = None,
|
|
596
|
+
neighbor_orientation_constraints: LocalOrientationConstraints | None = None,
|
|
597
|
+
dependency_depth: int = 0,
|
|
598
|
+
) -> TetrahedralConstitution:
|
|
599
|
+
"""Classify a tetrahedral carrier by exact center-stabilizer parity.
|
|
600
|
+
|
|
601
|
+
The depth-zero calculation uses constitution alone. A constrained pass
|
|
602
|
+
may additionally require graph maps to preserve supplied orientations at
|
|
603
|
+
other local elements, but never reads the focal center's configuration or
|
|
604
|
+
any CIP descriptor. A center is confirmed exactly when its allowed
|
|
605
|
+
stabilizer contains no odd permutation of the four coordination slots.
|
|
606
|
+
"""
|
|
607
|
+
working, atom = _validated_tetrahedral_atom(
|
|
608
|
+
molecule,
|
|
609
|
+
center,
|
|
610
|
+
dependency_depth,
|
|
611
|
+
)
|
|
612
|
+
markers = {
|
|
613
|
+
index: marker
|
|
614
|
+
for index, marker in (stereo_markers or {}).items()
|
|
615
|
+
if index != center
|
|
616
|
+
}
|
|
617
|
+
graph = _constitutional_graph(working, center, markers)
|
|
618
|
+
local_keys = tetrahedral_local_neighbor_keys(
|
|
619
|
+
working,
|
|
620
|
+
center,
|
|
621
|
+
stereo_markers=markers,
|
|
622
|
+
)
|
|
623
|
+
neighbors = {neighbor.GetIdx() for neighbor in atom.GetNeighbors()}
|
|
624
|
+
classes, witness_checks = _exact_material_ligand_classes(
|
|
625
|
+
graph,
|
|
626
|
+
neighbors,
|
|
627
|
+
local_keys,
|
|
628
|
+
)
|
|
629
|
+
hidden_hydrogens = _hidden_hydrogen_count(atom)
|
|
630
|
+
if hidden_hydrogens:
|
|
631
|
+
classes.append(
|
|
632
|
+
LigandSymmetryClass(
|
|
633
|
+
(virtual_reference("H", center),),
|
|
634
|
+
hidden_hydrogens,
|
|
635
|
+
local_keys[virtual_reference("H", center)],
|
|
636
|
+
)
|
|
637
|
+
)
|
|
638
|
+
lone_pairs = _lone_pair_count(atom)
|
|
639
|
+
if lone_pairs:
|
|
640
|
+
classes.append(
|
|
641
|
+
LigandSymmetryClass(
|
|
642
|
+
(virtual_reference("LP", center),),
|
|
643
|
+
lone_pairs,
|
|
644
|
+
local_keys[virtual_reference("LP", center)],
|
|
645
|
+
)
|
|
646
|
+
)
|
|
647
|
+
classes.sort(
|
|
648
|
+
key=lambda item: (
|
|
649
|
+
item.neighborhood_key,
|
|
650
|
+
item.multiplicity,
|
|
651
|
+
repr(item.references),
|
|
652
|
+
)
|
|
653
|
+
)
|
|
654
|
+
references = tuple(
|
|
655
|
+
[
|
|
656
|
+
*(neighbor.GetIdx() for neighbor in atom.GetNeighbors()),
|
|
657
|
+
*(virtual_reference("H", center) for _ in range(hidden_hydrogens)),
|
|
658
|
+
*(virtual_reference("LP", center) for _ in range(lone_pairs)),
|
|
659
|
+
]
|
|
660
|
+
)
|
|
661
|
+
if neighbor_orientation_constraints is not None and not isinstance(
|
|
662
|
+
neighbor_orientation_constraints,
|
|
663
|
+
LocalOrientationConstraints,
|
|
664
|
+
):
|
|
665
|
+
raise TypeError("Neighbor orientation constraints have an unsupported type.")
|
|
666
|
+
singleton_ligands = len(classes) == 4 and all(
|
|
667
|
+
item.multiplicity == 1 for item in classes
|
|
668
|
+
)
|
|
669
|
+
stabilizer = (
|
|
670
|
+
None
|
|
671
|
+
if singleton_ligands
|
|
672
|
+
else tetrahedral_stabilizer_result(
|
|
673
|
+
graph,
|
|
674
|
+
references, # type: ignore[arg-type]
|
|
675
|
+
constraints=neighbor_orientation_constraints,
|
|
676
|
+
)
|
|
677
|
+
)
|
|
678
|
+
if stabilizer is not None and not stabilizer.is_stereogenic:
|
|
679
|
+
status = TetrahedralConstitutionStatus.SYMMETRY_RELATED
|
|
680
|
+
elif dependency_depth or neighbor_orientation_constraints is not None:
|
|
681
|
+
status = TetrahedralConstitutionStatus.STEREO_DEPENDENT_DISTINCT
|
|
682
|
+
elif singleton_ligands:
|
|
683
|
+
status = TetrahedralConstitutionStatus.CONSTITUTIONALLY_DISTINCT
|
|
684
|
+
else:
|
|
685
|
+
status = TetrahedralConstitutionStatus.CONSTITUTIONALLY_ORIENTATION_DISTINCT
|
|
686
|
+
class_keys = tuple(item.neighborhood_key for item in classes)
|
|
687
|
+
if status is TetrahedralConstitutionStatus.SYMMETRY_RELATED:
|
|
688
|
+
canonical_frame = None
|
|
689
|
+
frame_status = TetrahedralFrameStatus.SYMMETRY_RELATED
|
|
690
|
+
elif not singleton_ligands:
|
|
691
|
+
canonical_frame = None
|
|
692
|
+
frame_status = TetrahedralFrameStatus.ORIENTATION_ORBIT
|
|
693
|
+
elif len(set(class_keys)) != len(class_keys):
|
|
694
|
+
canonical_frame = None
|
|
695
|
+
frame_status = TetrahedralFrameStatus.NEIGHBORHOOD_KEY_COLLISION
|
|
696
|
+
else:
|
|
697
|
+
canonical_frame = (
|
|
698
|
+
center,
|
|
699
|
+
*(item.references[0] for item in classes),
|
|
700
|
+
)
|
|
701
|
+
frame_status = TetrahedralFrameStatus.CANONICAL
|
|
702
|
+
result = TetrahedralConstitution(
|
|
703
|
+
support=AtomStereoSupport(center),
|
|
704
|
+
ligand_classes=tuple(classes),
|
|
705
|
+
status=status,
|
|
706
|
+
automorphism_witness_checks=(
|
|
707
|
+
witness_checks
|
|
708
|
+
+ (0 if stabilizer is None else stabilizer.automorphism_checks)
|
|
709
|
+
),
|
|
710
|
+
canonical_frame=canonical_frame,
|
|
711
|
+
frame_status=frame_status,
|
|
712
|
+
refinement_depth=next(iter(local_keys.values())).depth,
|
|
713
|
+
dependency_depth=dependency_depth,
|
|
714
|
+
stereo_marker_count=sum(
|
|
715
|
+
index in graph and index != center for index in markers
|
|
716
|
+
),
|
|
717
|
+
)
|
|
718
|
+
if result.ligand_count != 4:
|
|
719
|
+
raise RuntimeError("Tetrahedral constitutional evidence lost a ligand slot.")
|
|
720
|
+
return result
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def _reference_permutation_sign(
|
|
724
|
+
values: tuple[Reference, ...],
|
|
725
|
+
ordered: tuple[Reference, ...],
|
|
726
|
+
) -> int:
|
|
727
|
+
positions = {value: index for index, value in enumerate(ordered)}
|
|
728
|
+
permutation = tuple(positions[value] for value in values)
|
|
729
|
+
inversions = sum(
|
|
730
|
+
permutation[left] > permutation[right]
|
|
731
|
+
for left in range(len(permutation))
|
|
732
|
+
for right in range(left + 1, len(permutation))
|
|
733
|
+
)
|
|
734
|
+
return -1 if inversions % 2 else 1
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def canonicalize_tetrahedral_configuration(
|
|
738
|
+
molecule: Chem.Mol,
|
|
739
|
+
descriptor: TetrahedralStereo,
|
|
740
|
+
*,
|
|
741
|
+
constitutional_evidence: TetrahedralConstitution | None = None,
|
|
742
|
+
) -> TetrahedralStereo:
|
|
743
|
+
"""Attach tetrahedral configuration to its constitution-derived frame.
|
|
744
|
+
|
|
745
|
+
Frame construction is completed before this function reads parity.
|
|
746
|
+
The returned parity is transported through the ligand permutation, so
|
|
747
|
+
relative configuration is preserved without allowing it to influence the
|
|
748
|
+
canonical ligand order.
|
|
749
|
+
"""
|
|
750
|
+
if not isinstance(descriptor, TetrahedralStereo):
|
|
751
|
+
raise TypeError(
|
|
752
|
+
"Tetrahedral configuration attachment requires TetrahedralStereo."
|
|
753
|
+
)
|
|
754
|
+
if type(descriptor.center) is not int:
|
|
755
|
+
raise ValueError("Tetrahedral configuration centers must be atom indices.")
|
|
756
|
+
evidence = constitutional_evidence or canonicalize_tetrahedral_constitution(
|
|
757
|
+
molecule,
|
|
758
|
+
descriptor.center,
|
|
759
|
+
)
|
|
760
|
+
if evidence.support != descriptor.support:
|
|
761
|
+
raise ValueError(
|
|
762
|
+
"Tetrahedral constitutional evidence belongs to another center."
|
|
763
|
+
)
|
|
764
|
+
if evidence.canonical_frame is None:
|
|
765
|
+
raise ValueError(
|
|
766
|
+
"Tetrahedral configuration has no canonical constitutional frame: "
|
|
767
|
+
f"{evidence.frame_status.value}."
|
|
768
|
+
)
|
|
769
|
+
canonical_references = tuple(evidence.canonical_frame[1:])
|
|
770
|
+
supplied_references = tuple(descriptor.atoms[1:])
|
|
771
|
+
if set(supplied_references) != set(canonical_references):
|
|
772
|
+
raise ValueError(
|
|
773
|
+
"Tetrahedral descriptor references do not match the constitutional frame."
|
|
774
|
+
)
|
|
775
|
+
parity = (
|
|
776
|
+
None
|
|
777
|
+
if descriptor.parity is None
|
|
778
|
+
else descriptor.parity
|
|
779
|
+
* _reference_permutation_sign(supplied_references, canonical_references)
|
|
780
|
+
)
|
|
781
|
+
return TetrahedralStereo(
|
|
782
|
+
evidence.canonical_frame, # type: ignore[arg-type]
|
|
783
|
+
parity,
|
|
784
|
+
descriptor.provenance,
|
|
785
|
+
)
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
_TETRAHEDRAL_TAG_PARITY = {
|
|
789
|
+
Chem.ChiralType.CHI_TETRAHEDRAL_CW: 1,
|
|
790
|
+
Chem.ChiralType.CHI_TETRAHEDRAL_CCW: -1,
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
def _raw_tetrahedral_configuration(
|
|
795
|
+
atom: Chem.Atom,
|
|
796
|
+
) -> TetrahedralStereo | None:
|
|
797
|
+
"""Read supplied RDKit orientation in zero-based local-reference order."""
|
|
798
|
+
parity = _TETRAHEDRAL_TAG_PARITY.get(atom.GetChiralTag())
|
|
799
|
+
if parity is None:
|
|
800
|
+
return None
|
|
801
|
+
center = atom.GetIdx()
|
|
802
|
+
references: list[Reference] = [
|
|
803
|
+
neighbor.GetIdx() for neighbor in atom.GetNeighbors()
|
|
804
|
+
]
|
|
805
|
+
hidden_hydrogens = _hidden_hydrogen_count(atom)
|
|
806
|
+
references.extend(virtual_reference("H", center) for _ in range(hidden_hydrogens))
|
|
807
|
+
references.extend(
|
|
808
|
+
virtual_reference("LP", center) for _ in range(_lone_pair_count(atom))
|
|
809
|
+
)
|
|
810
|
+
if len(references) != 4:
|
|
811
|
+
raise ValueError(
|
|
812
|
+
f"Configured tetrahedral center {center} does not expose four ligands."
|
|
813
|
+
)
|
|
814
|
+
return TetrahedralStereo(
|
|
815
|
+
(center, *references), # type: ignore[arg-type]
|
|
816
|
+
parity,
|
|
817
|
+
"rdkit",
|
|
818
|
+
)
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
def analyze_tetrahedral_carriers(
|
|
822
|
+
molecule: Chem.Mol,
|
|
823
|
+
) -> tuple[TetrahedralConstitution, ...]:
|
|
824
|
+
"""Return configuration-neutral evidence for every broad carrier."""
|
|
825
|
+
return tuple(
|
|
826
|
+
canonicalize_tetrahedral_constitution(molecule, support.center)
|
|
827
|
+
for support in detect_tetrahedral_carriers(molecule)
|
|
828
|
+
)
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
def _element_from_tetrahedral_evidence(
|
|
832
|
+
molecule: Chem.Mol,
|
|
833
|
+
evidence: TetrahedralConstitution,
|
|
834
|
+
) -> PotentialStereoElement:
|
|
835
|
+
support = evidence.support
|
|
836
|
+
if not evidence.confirms_stereogenic_center:
|
|
837
|
+
raise ValueError("Tetrahedral evidence does not define a stereo element.")
|
|
838
|
+
raw_configuration = _raw_tetrahedral_configuration(
|
|
839
|
+
molecule.GetAtomWithIdx(support.center)
|
|
840
|
+
)
|
|
841
|
+
configuration = (
|
|
842
|
+
canonicalize_tetrahedral_configuration(
|
|
843
|
+
molecule,
|
|
844
|
+
raw_configuration,
|
|
845
|
+
constitutional_evidence=evidence,
|
|
846
|
+
)
|
|
847
|
+
if raw_configuration is not None and evidence.has_canonical_frame
|
|
848
|
+
else None
|
|
849
|
+
)
|
|
850
|
+
configuration_state = (
|
|
851
|
+
StereoConfigurationState.SPECIFIED
|
|
852
|
+
if raw_configuration is not None
|
|
853
|
+
else StereoConfigurationState.UNSPECIFIED
|
|
854
|
+
)
|
|
855
|
+
if evidence.is_stereo_dependent:
|
|
856
|
+
provenance = "synkit_orientation_constrained_center_stabilizer"
|
|
857
|
+
elif (
|
|
858
|
+
evidence.status
|
|
859
|
+
is TetrahedralConstitutionStatus.CONSTITUTIONALLY_ORIENTATION_DISTINCT
|
|
860
|
+
):
|
|
861
|
+
provenance = "synkit_even_center_stabilizer"
|
|
862
|
+
else:
|
|
863
|
+
provenance = "synkit_recursive_neighborhood_and_exact_symmetry"
|
|
864
|
+
return PotentialStereoElement(
|
|
865
|
+
StereoElementType.TETRAHEDRAL,
|
|
866
|
+
support,
|
|
867
|
+
configuration_state,
|
|
868
|
+
provenance,
|
|
869
|
+
f"Atom_Tetrahedral:{support.center}",
|
|
870
|
+
evidence,
|
|
871
|
+
configuration,
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
def perceive_tetrahedral_stereo(
|
|
876
|
+
molecule: Chem.Mol,
|
|
877
|
+
*,
|
|
878
|
+
excluded_centers: Iterable[int] = (),
|
|
879
|
+
) -> TetrahedralPerceptionResult:
|
|
880
|
+
"""Resolve tetrahedral centers from constitution and local neighbor frames.
|
|
881
|
+
|
|
882
|
+
A constrained pass reads oriented neighbor frames directly from molecular
|
|
883
|
+
input. It never converts them to CIP or canonical local descriptors, and
|
|
884
|
+
the focal center's own frame is excluded.
|
|
885
|
+
"""
|
|
886
|
+
if molecule is None:
|
|
887
|
+
raise ValueError("Tetrahedral perception requires a molecule.")
|
|
888
|
+
working = Chem.Mol(molecule)
|
|
889
|
+
excluded = frozenset(int(center) for center in excluded_centers)
|
|
890
|
+
if any(center < 0 or center >= working.GetNumAtoms() for center in excluded):
|
|
891
|
+
raise ValueError("Excluded tetrahedral center is absent.")
|
|
892
|
+
initial = tuple(
|
|
893
|
+
canonicalize_tetrahedral_constitution(working, support.center)
|
|
894
|
+
for support in detect_tetrahedral_carriers(working)
|
|
895
|
+
if support.center not in excluded
|
|
896
|
+
)
|
|
897
|
+
evidence_by_center = {evidence.support.center: evidence for evidence in initial}
|
|
898
|
+
elements: dict[int, PotentialStereoElement] = {}
|
|
899
|
+
pending = set(evidence_by_center)
|
|
900
|
+
|
|
901
|
+
for center, evidence in evidence_by_center.items():
|
|
902
|
+
if not evidence.confirms_stereogenic_center:
|
|
903
|
+
continue
|
|
904
|
+
element = _element_from_tetrahedral_evidence(working, evidence)
|
|
905
|
+
elements[center] = element
|
|
906
|
+
pending.discard(center)
|
|
907
|
+
|
|
908
|
+
dependency_iterations = 0
|
|
909
|
+
if pending:
|
|
910
|
+
constraints = extract_local_orientation_constraints(working)
|
|
911
|
+
if constraints.tetrahedral or constraints.planar or constraints.cumulene:
|
|
912
|
+
dependency_iterations = 1
|
|
913
|
+
for center in sorted(tuple(pending)):
|
|
914
|
+
focal_constraints = LocalOrientationConstraints(
|
|
915
|
+
tuple(
|
|
916
|
+
constraint
|
|
917
|
+
for constraint in constraints.tetrahedral
|
|
918
|
+
if constraint.center != center
|
|
919
|
+
),
|
|
920
|
+
constraints.planar,
|
|
921
|
+
constraints.cumulene,
|
|
922
|
+
)
|
|
923
|
+
evidence = canonicalize_tetrahedral_constitution(
|
|
924
|
+
working,
|
|
925
|
+
center,
|
|
926
|
+
neighbor_orientation_constraints=focal_constraints,
|
|
927
|
+
dependency_depth=1,
|
|
928
|
+
)
|
|
929
|
+
evidence_by_center[center] = evidence
|
|
930
|
+
if not evidence.confirms_stereogenic_center:
|
|
931
|
+
continue
|
|
932
|
+
elements[center] = _element_from_tetrahedral_evidence(
|
|
933
|
+
working,
|
|
934
|
+
evidence,
|
|
935
|
+
)
|
|
936
|
+
pending.discard(center)
|
|
937
|
+
|
|
938
|
+
return TetrahedralPerceptionResult(
|
|
939
|
+
carrier_evidence=tuple(
|
|
940
|
+
evidence_by_center[center] for center in sorted(evidence_by_center)
|
|
941
|
+
),
|
|
942
|
+
elements=tuple(elements[center] for center in sorted(elements)),
|
|
943
|
+
dependency_iterations=dependency_iterations,
|
|
944
|
+
stereo_markers=(),
|
|
945
|
+
)
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
def detect_tetrahedral_elements(
|
|
949
|
+
molecule: Chem.Mol,
|
|
950
|
+
*,
|
|
951
|
+
excluded_centers: Iterable[int] = (),
|
|
952
|
+
) -> tuple[PotentialStereoElement, ...]:
|
|
953
|
+
"""Return primary and fixed-point stereo-dependent tetrahedral elements."""
|
|
954
|
+
return perceive_tetrahedral_stereo(
|
|
955
|
+
molecule,
|
|
956
|
+
excluded_centers=excluded_centers,
|
|
957
|
+
).elements
|
|
958
|
+
|
|
959
|
+
|
|
960
|
+
def detect_constitutionally_distinct_tetrahedral_centers(
|
|
961
|
+
molecule: Chem.Mol,
|
|
962
|
+
) -> tuple[TetrahedralConstitution, ...]:
|
|
963
|
+
"""Return tetrahedral centers confirmed by constitution alone."""
|
|
964
|
+
return tuple(
|
|
965
|
+
evidence
|
|
966
|
+
for evidence in analyze_tetrahedral_carriers(molecule)
|
|
967
|
+
if evidence.confirms_stereogenic_center
|
|
968
|
+
)
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
def detect_potential_stereo_elements(
|
|
972
|
+
molecule: Chem.Mol,
|
|
973
|
+
*,
|
|
974
|
+
include_extended_ring_axes: bool = False,
|
|
975
|
+
excluded_tetrahedral_centers: Iterable[int] = (),
|
|
976
|
+
) -> tuple[PotentialStereoElement, ...]:
|
|
977
|
+
"""Detect supported atom, bond, and axis stereo carriers.
|
|
978
|
+
|
|
979
|
+
The result is an intentionally non-promoting perception inventory. It
|
|
980
|
+
includes configured atom/bond elements but does not reconstruct their
|
|
981
|
+
descriptor frames. Cumulated double bonds are not emitted as independent
|
|
982
|
+
E/Z elements. Even-bond cumulenes are emitted as axes and odd-bond
|
|
983
|
+
extended cumulenes are emitted once as path-level cis/trans supports.
|
|
984
|
+
"""
|
|
985
|
+
if molecule is None:
|
|
986
|
+
raise ValueError("Stereo-element perception requires a molecule.")
|
|
987
|
+
from ._stereo_element_detection import (
|
|
988
|
+
axis_elements,
|
|
989
|
+
extended_cis_trans_elements,
|
|
990
|
+
helical_elements,
|
|
991
|
+
rdkit_double_bond_elements,
|
|
992
|
+
)
|
|
993
|
+
|
|
994
|
+
working = Chem.Mol(molecule)
|
|
995
|
+
Chem.AssignStereochemistry(working, cleanIt=False, force=True)
|
|
996
|
+
elements = (
|
|
997
|
+
*detect_tetrahedral_elements(
|
|
998
|
+
working,
|
|
999
|
+
excluded_centers=excluded_tetrahedral_centers,
|
|
1000
|
+
),
|
|
1001
|
+
*rdkit_double_bond_elements(working),
|
|
1002
|
+
*axis_elements(
|
|
1003
|
+
working,
|
|
1004
|
+
include_extended_ring_axes=include_extended_ring_axes,
|
|
1005
|
+
),
|
|
1006
|
+
*extended_cis_trans_elements(working),
|
|
1007
|
+
*helical_elements(working),
|
|
1008
|
+
)
|
|
1009
|
+
return tuple(sorted(elements, key=lambda element: element.identifier))
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
__all__ = [
|
|
1013
|
+
"LigandSymmetryClass",
|
|
1014
|
+
"LocalNeighborKey",
|
|
1015
|
+
"PotentialStereoElement",
|
|
1016
|
+
"StereoCarrierStatus",
|
|
1017
|
+
"StereoConfigurationState",
|
|
1018
|
+
"StereoElementType",
|
|
1019
|
+
"TetrahedralConstitution",
|
|
1020
|
+
"TetrahedralConstitutionStatus",
|
|
1021
|
+
"TetrahedralFrameStatus",
|
|
1022
|
+
"TetrahedralPerceptionResult",
|
|
1023
|
+
"analyze_tetrahedral_carriers",
|
|
1024
|
+
"canonicalize_tetrahedral_configuration",
|
|
1025
|
+
"canonicalize_tetrahedral_constitution",
|
|
1026
|
+
"detect_constitutionally_distinct_tetrahedral_centers",
|
|
1027
|
+
"detect_potential_stereo_elements",
|
|
1028
|
+
"detect_tetrahedral_carriers",
|
|
1029
|
+
"detect_tetrahedral_elements",
|
|
1030
|
+
"perceive_tetrahedral_stereo",
|
|
1031
|
+
"tetrahedral_local_neighbor_keys",
|
|
1032
|
+
]
|