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,1041 @@
|
|
|
1
|
+
"""Whole-molecule chirality classification by mirror automorphism.
|
|
2
|
+
|
|
3
|
+
This module is intentionally independent of reaction rules and reaction
|
|
4
|
+
stereo transport. It asks one molecular question: is a molecule isomorphic
|
|
5
|
+
to its mirror image?
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from functools import lru_cache
|
|
12
|
+
from typing import Any, Mapping
|
|
13
|
+
|
|
14
|
+
import networkx as nx
|
|
15
|
+
from rdkit import Chem
|
|
16
|
+
from rdkit.Chem.EnumerateStereoisomers import (
|
|
17
|
+
EnumerateStereoisomers,
|
|
18
|
+
GetStereoisomerCount,
|
|
19
|
+
StereoEnumerationOptions,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from synkit.Graph.Stereo.descriptors import (
|
|
23
|
+
AtropBondStereo,
|
|
24
|
+
CumuleneAxisStereo,
|
|
25
|
+
ExtendedCisTransStereo,
|
|
26
|
+
Reference,
|
|
27
|
+
TetrahedralStereo,
|
|
28
|
+
descriptor_id,
|
|
29
|
+
parse_virtual_reference,
|
|
30
|
+
virtual_reference,
|
|
31
|
+
)
|
|
32
|
+
from synkit.Graph.Stereo.extended_descriptors import HelicalStereo
|
|
33
|
+
from synkit.Graph.Stereo.identity import descriptor_relative_form
|
|
34
|
+
from synkit.IO.mol_to_graph import MolToGraph
|
|
35
|
+
|
|
36
|
+
from ._chirality_loci import detect_potential_axis_supports
|
|
37
|
+
from ._chirality_types import (
|
|
38
|
+
MolecularChirality,
|
|
39
|
+
MolecularChiralityAssessment,
|
|
40
|
+
MolecularChiralityOutcome,
|
|
41
|
+
MolecularChiralityResult,
|
|
42
|
+
PotentialStereoLocus,
|
|
43
|
+
PotentialStereoLocusType,
|
|
44
|
+
StereoOrientationState,
|
|
45
|
+
StereoStabilityStatus,
|
|
46
|
+
UnspecifiedMolecularStereoError,
|
|
47
|
+
connectivity_edge_match as _connectivity_edge_match,
|
|
48
|
+
molecular_node_colours as _molecular_node_colours,
|
|
49
|
+
molecular_node_match as _molecular_node_match,
|
|
50
|
+
)
|
|
51
|
+
from .stereo_evidence import (
|
|
52
|
+
ExtendedStereoStability,
|
|
53
|
+
MolecularStereoConfiguration,
|
|
54
|
+
MolecularStereoConfigurationSet,
|
|
55
|
+
StereoEvidenceSource,
|
|
56
|
+
StereoPopulationStatus,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
__all__ = [
|
|
60
|
+
"MolecularChirality",
|
|
61
|
+
"MolecularChiralityAssessment",
|
|
62
|
+
"MolecularChiralityOutcome",
|
|
63
|
+
"MolecularChiralityResult",
|
|
64
|
+
"MolecularStereoConfiguration",
|
|
65
|
+
"MolecularStereoConfigurationSet",
|
|
66
|
+
"ExtendedStereoStability",
|
|
67
|
+
"PotentialStereoLocus",
|
|
68
|
+
"PotentialStereoLocusType",
|
|
69
|
+
"StereoOrientationState",
|
|
70
|
+
"StereoEvidenceSource",
|
|
71
|
+
"StereoPopulationStatus",
|
|
72
|
+
"StereoStabilityStatus",
|
|
73
|
+
"UnspecifiedMolecularStereoError",
|
|
74
|
+
"assess_molecular_chirality",
|
|
75
|
+
"classify_molecular_chirality",
|
|
76
|
+
"clear_molecular_chirality_cache",
|
|
77
|
+
"detect_potential_stereo_loci",
|
|
78
|
+
"is_molecular_chiral",
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _resolved_cumulene_form(
|
|
83
|
+
descriptor: CumuleneAxisStereo,
|
|
84
|
+
resolve: Any,
|
|
85
|
+
) -> tuple[Any, ...]:
|
|
86
|
+
"""Resolve a complete cumulene path without entering rule semantics."""
|
|
87
|
+
path = tuple(resolve(atom) for atom in descriptor.axis_path)
|
|
88
|
+
frames = tuple(
|
|
89
|
+
tuple(resolve(reference) for reference in frame)
|
|
90
|
+
for frame in descriptor.terminal_frames
|
|
91
|
+
)
|
|
92
|
+
if descriptor.parity is None:
|
|
93
|
+
left, right = (tuple(sorted(frame, key=repr)) for frame in frames)
|
|
94
|
+
candidates = ((path, left, right), (tuple(reversed(path)), right, left))
|
|
95
|
+
return descriptor.descriptor_class, None, min(candidates, key=repr)
|
|
96
|
+
atoms = (*frames[0], path[0], path[-1], *frames[1])
|
|
97
|
+
if descriptor.parity == -1:
|
|
98
|
+
atoms = tuple(atoms[index] for index in descriptor._INVERSION)
|
|
99
|
+
candidates = []
|
|
100
|
+
for permutation in descriptor._PERMUTATIONS:
|
|
101
|
+
frame = tuple(atoms[index] for index in permutation)
|
|
102
|
+
oriented_path = tuple(reversed(path)) if permutation[2] == 3 else path
|
|
103
|
+
candidates.append((oriented_path, frame))
|
|
104
|
+
return descriptor.descriptor_class, 1, min(candidates, key=repr)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _resolved_extended_cis_trans_form(
|
|
108
|
+
descriptor: ExtendedCisTransStereo,
|
|
109
|
+
resolve: Any,
|
|
110
|
+
) -> tuple[Any, ...]:
|
|
111
|
+
"""Resolve a complete odd-bond cumulene path and terminal frames."""
|
|
112
|
+
path = tuple(resolve(atom) for atom in descriptor.path)
|
|
113
|
+
frames = tuple(
|
|
114
|
+
tuple(resolve(reference) for reference in frame)
|
|
115
|
+
for frame in descriptor.terminal_frames
|
|
116
|
+
)
|
|
117
|
+
if descriptor.parity is None:
|
|
118
|
+
left, right = (tuple(sorted(frame, key=repr)) for frame in frames)
|
|
119
|
+
candidates = ((path, left, right), (tuple(reversed(path)), right, left))
|
|
120
|
+
return descriptor.descriptor_class, None, min(candidates, key=repr)
|
|
121
|
+
atoms = (*frames[0], path[0], path[-1], *frames[1])
|
|
122
|
+
candidates = []
|
|
123
|
+
for permutation in descriptor._PERMUTATIONS:
|
|
124
|
+
frame = tuple(atoms[index] for index in permutation)
|
|
125
|
+
oriented_path = tuple(reversed(path)) if permutation[2] == 3 else path
|
|
126
|
+
candidates.append((oriented_path, frame))
|
|
127
|
+
return descriptor.descriptor_class, 0, min(candidates, key=repr)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _resolved_helical_form(
|
|
131
|
+
descriptor: HelicalStereo,
|
|
132
|
+
resolve: Any,
|
|
133
|
+
) -> tuple[Any, ...]:
|
|
134
|
+
"""Resolve open/cyclic path identity for molecular mirror matching."""
|
|
135
|
+
path = tuple(resolve(atom) for atom in descriptor.path)
|
|
136
|
+
if descriptor.cyclic:
|
|
137
|
+
reverse = tuple(reversed(path))
|
|
138
|
+
variants = tuple(
|
|
139
|
+
sequence[offset:] + sequence[:offset]
|
|
140
|
+
for sequence in (path, reverse)
|
|
141
|
+
for offset in range(len(path))
|
|
142
|
+
)
|
|
143
|
+
else:
|
|
144
|
+
variants = path, tuple(reversed(path))
|
|
145
|
+
return (
|
|
146
|
+
descriptor.descriptor_class,
|
|
147
|
+
min(variants, key=repr),
|
|
148
|
+
descriptor.cyclic,
|
|
149
|
+
descriptor.parity,
|
|
150
|
+
descriptor.coupling_id,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _molecular_stereo_form(graph: Any) -> tuple[Any, ...]:
|
|
155
|
+
"""Return a safe colour-refinement prefilter for mirror isomorphism."""
|
|
156
|
+
registry = graph.graph.get("stereo_descriptors", {})
|
|
157
|
+
if not registry:
|
|
158
|
+
return ()
|
|
159
|
+
colours = _molecular_node_colours(graph)
|
|
160
|
+
|
|
161
|
+
def resolve(reference: int | str) -> tuple[str, Any]:
|
|
162
|
+
if type(reference) is int:
|
|
163
|
+
if reference not in colours:
|
|
164
|
+
raise ValueError(f"Stereo reference {reference} is absent.")
|
|
165
|
+
return "atom", colours[reference]
|
|
166
|
+
virtual = parse_virtual_reference(reference)
|
|
167
|
+
if virtual is None or virtual.center not in colours:
|
|
168
|
+
raise ValueError(f"Invalid virtual stereo reference: {reference!r}.")
|
|
169
|
+
return "virtual", (virtual.kind, colours[virtual.center])
|
|
170
|
+
|
|
171
|
+
def descriptor_form(descriptor: Any) -> tuple[Any, ...]:
|
|
172
|
+
if isinstance(descriptor, CumuleneAxisStereo):
|
|
173
|
+
return _resolved_cumulene_form(descriptor, resolve)
|
|
174
|
+
if isinstance(descriptor, ExtendedCisTransStereo):
|
|
175
|
+
return _resolved_extended_cis_trans_form(descriptor, resolve)
|
|
176
|
+
if isinstance(descriptor, HelicalStereo):
|
|
177
|
+
return _resolved_helical_form(descriptor, resolve)
|
|
178
|
+
return descriptor_relative_form(descriptor, resolve)
|
|
179
|
+
|
|
180
|
+
return tuple(
|
|
181
|
+
sorted(
|
|
182
|
+
(descriptor_form(descriptor) for descriptor in registry.values()),
|
|
183
|
+
key=repr,
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _resolved_registry_matches(
|
|
189
|
+
source: tuple[Any, ...],
|
|
190
|
+
target: frozenset[Any],
|
|
191
|
+
mapping: Mapping[int, int],
|
|
192
|
+
) -> bool:
|
|
193
|
+
"""Reject any fully transported descriptor absent from the target."""
|
|
194
|
+
mapped_nodes = mapping.keys()
|
|
195
|
+
for descriptor in source:
|
|
196
|
+
if not descriptor.dependencies.issubset(mapped_nodes):
|
|
197
|
+
continue
|
|
198
|
+
if descriptor.relabel(mapping) not in target:
|
|
199
|
+
return False
|
|
200
|
+
return True
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class _MolecularStereoGraphMatcher(nx.isomorphism.GraphMatcher):
|
|
204
|
+
"""VF2 matcher that checks complete local frames during expansion."""
|
|
205
|
+
|
|
206
|
+
def __init__(self, left: Any, right: Any) -> None:
|
|
207
|
+
nx.set_node_attributes(
|
|
208
|
+
left,
|
|
209
|
+
_molecular_node_colours(left),
|
|
210
|
+
"_molecular_colour",
|
|
211
|
+
)
|
|
212
|
+
nx.set_node_attributes(
|
|
213
|
+
right,
|
|
214
|
+
_molecular_node_colours(right),
|
|
215
|
+
"_molecular_colour",
|
|
216
|
+
)
|
|
217
|
+
super().__init__(
|
|
218
|
+
left,
|
|
219
|
+
right,
|
|
220
|
+
node_match=_molecular_node_match,
|
|
221
|
+
edge_match=_connectivity_edge_match,
|
|
222
|
+
)
|
|
223
|
+
self._left_registry = tuple(left.graph.get("stereo_descriptors", {}).values())
|
|
224
|
+
self._right_registry = tuple(right.graph.get("stereo_descriptors", {}).values())
|
|
225
|
+
self._left_configurations = frozenset(self._left_registry)
|
|
226
|
+
self._right_configurations = frozenset(self._right_registry)
|
|
227
|
+
|
|
228
|
+
def semantic_feasibility(self, left_node: int, right_node: int) -> bool:
|
|
229
|
+
if not super().semantic_feasibility(left_node, right_node):
|
|
230
|
+
return False
|
|
231
|
+
forward = dict(self.core_1)
|
|
232
|
+
reverse = dict(self.core_2)
|
|
233
|
+
forward[left_node] = right_node
|
|
234
|
+
reverse[right_node] = left_node
|
|
235
|
+
return _resolved_registry_matches(
|
|
236
|
+
self._left_registry,
|
|
237
|
+
self._right_configurations,
|
|
238
|
+
forward,
|
|
239
|
+
) and _resolved_registry_matches(
|
|
240
|
+
self._right_registry,
|
|
241
|
+
self._left_configurations,
|
|
242
|
+
reverse,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _indexed_copy(molecule: Chem.Mol) -> Chem.Mol:
|
|
247
|
+
if molecule is None:
|
|
248
|
+
raise ValueError("Molecular chirality classification requires a molecule.")
|
|
249
|
+
working = Chem.Mol(molecule)
|
|
250
|
+
for atom in working.GetAtoms():
|
|
251
|
+
atom.SetAtomMapNum(atom.GetIdx() + 1)
|
|
252
|
+
Chem.AssignStereochemistry(working, cleanIt=False, force=True)
|
|
253
|
+
return working
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _configuration_covers_rdkit_locus(
|
|
257
|
+
molecule: Chem.Mol,
|
|
258
|
+
info: Any,
|
|
259
|
+
configuration: MolecularStereoConfiguration,
|
|
260
|
+
) -> bool:
|
|
261
|
+
if info.type != Chem.StereoType.Bond_Double:
|
|
262
|
+
return False
|
|
263
|
+
bond = molecule.GetBondWithIdx(int(info.centeredOn))
|
|
264
|
+
locus = frozenset((bond.GetBeginAtomIdx(), bond.GetEndAtomIdx()))
|
|
265
|
+
return any(
|
|
266
|
+
isinstance(
|
|
267
|
+
descriptor,
|
|
268
|
+
(CumuleneAxisStereo, ExtendedCisTransStereo),
|
|
269
|
+
)
|
|
270
|
+
and locus
|
|
271
|
+
in {
|
|
272
|
+
frozenset(pair)
|
|
273
|
+
for pair in zip(
|
|
274
|
+
(
|
|
275
|
+
descriptor.axis_path
|
|
276
|
+
if isinstance(descriptor, CumuleneAxisStereo)
|
|
277
|
+
else descriptor.path
|
|
278
|
+
),
|
|
279
|
+
(
|
|
280
|
+
descriptor.axis_path[1:]
|
|
281
|
+
if isinstance(descriptor, CumuleneAxisStereo)
|
|
282
|
+
else descriptor.path[1:]
|
|
283
|
+
),
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
for descriptor in configuration.descriptors
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _unspecified_stereo_loci(
|
|
291
|
+
molecule: Chem.Mol,
|
|
292
|
+
configurations: tuple[MolecularStereoConfiguration, ...] = (),
|
|
293
|
+
) -> tuple[str, ...]:
|
|
294
|
+
def unresolved(info: Any) -> bool:
|
|
295
|
+
if info.specified == Chem.StereoSpecified.Specified:
|
|
296
|
+
return False
|
|
297
|
+
return not configurations or not all(
|
|
298
|
+
_configuration_covers_rdkit_locus(molecule, info, configuration)
|
|
299
|
+
for configuration in configurations
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
return tuple(
|
|
303
|
+
sorted(
|
|
304
|
+
f"{info.type}:{int(info.centeredOn)}"
|
|
305
|
+
for info in Chem.FindPotentialStereo(molecule)
|
|
306
|
+
if unresolved(info)
|
|
307
|
+
)
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _unsupported_stereo_loci(molecule: Chem.Mol) -> tuple[str, ...]:
|
|
312
|
+
supported = {
|
|
313
|
+
Chem.StereoType.Atom_Tetrahedral,
|
|
314
|
+
Chem.StereoType.Bond_Double,
|
|
315
|
+
}
|
|
316
|
+
return tuple(
|
|
317
|
+
sorted(
|
|
318
|
+
f"{info.type}:{int(info.centeredOn)}"
|
|
319
|
+
for info in Chem.FindPotentialStereo(molecule)
|
|
320
|
+
if info.specified != Chem.StereoSpecified.Specified
|
|
321
|
+
and info.type not in supported
|
|
322
|
+
)
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _complete_tetrahedral_topology(
|
|
327
|
+
molecule: Chem.Mol,
|
|
328
|
+
registry: dict[str, Any],
|
|
329
|
+
) -> tuple[int, ...]:
|
|
330
|
+
"""Add fixed probes at eligible unrepresented sp3 topologies.
|
|
331
|
+
|
|
332
|
+
These probes are not claims that each atom is a stereocentre. Their
|
|
333
|
+
arbitrary common orientation lets the whole-molecule automorphism test
|
|
334
|
+
decide which local probes cancel by symmetry and which collectively make
|
|
335
|
+
the molecular topology chiral. This is the role of ``stereo_complete`` in
|
|
336
|
+
the published StereoMolGraph validation protocol.
|
|
337
|
+
"""
|
|
338
|
+
represented = {
|
|
339
|
+
descriptor.atoms[0]
|
|
340
|
+
for descriptor in registry.values()
|
|
341
|
+
if descriptor.descriptor_class == "tetrahedral"
|
|
342
|
+
}
|
|
343
|
+
completed = []
|
|
344
|
+
for atom in molecule.GetAtoms():
|
|
345
|
+
center = atom.GetIdx() + 1
|
|
346
|
+
if center in represented:
|
|
347
|
+
continue
|
|
348
|
+
if atom.GetHybridization() != Chem.HybridizationType.SP3:
|
|
349
|
+
continue
|
|
350
|
+
references: list[int | str] = [
|
|
351
|
+
neighbor.GetIdx() + 1 for neighbor in atom.GetNeighbors()
|
|
352
|
+
]
|
|
353
|
+
hidden_hydrogens = int(atom.GetNumExplicitHs()) + int(atom.GetNumImplicitHs())
|
|
354
|
+
if len(references) == 3 and hidden_hydrogens == 1:
|
|
355
|
+
references.append(virtual_reference("H", center))
|
|
356
|
+
elif len(references) != 4:
|
|
357
|
+
continue
|
|
358
|
+
descriptor = TetrahedralStereo(
|
|
359
|
+
(center, *references),
|
|
360
|
+
1,
|
|
361
|
+
"molecular_chirality:stereo_complete",
|
|
362
|
+
)
|
|
363
|
+
registry[descriptor_id(descriptor)] = descriptor
|
|
364
|
+
completed.append(center)
|
|
365
|
+
return tuple(completed)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def detect_potential_stereo_loci(
|
|
369
|
+
molecule: Chem.Mol,
|
|
370
|
+
*,
|
|
371
|
+
include_extended_ring_axes: bool = False,
|
|
372
|
+
) -> tuple[PotentialStereoLocus, ...]:
|
|
373
|
+
"""Return topology-supported, orientation-unspecified axial loci.
|
|
374
|
+
|
|
375
|
+
The detector intentionally over-approximates. A returned locus is not a
|
|
376
|
+
configured descriptor, a rotational-stability claim, or proof that the
|
|
377
|
+
complete molecule is chiral.
|
|
378
|
+
"""
|
|
379
|
+
loci = tuple(
|
|
380
|
+
PotentialStereoLocus(
|
|
381
|
+
locus_type=PotentialStereoLocusType(locus_type),
|
|
382
|
+
support=support,
|
|
383
|
+
)
|
|
384
|
+
for locus_type, support in detect_potential_axis_supports(
|
|
385
|
+
molecule,
|
|
386
|
+
include_extended_ring_axes=include_extended_ring_axes,
|
|
387
|
+
)
|
|
388
|
+
)
|
|
389
|
+
return tuple(sorted(loci, key=lambda locus: locus.identifier))
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _validate_material_frame(
|
|
393
|
+
molecule: Chem.Mol,
|
|
394
|
+
owner: int,
|
|
395
|
+
references: tuple[Reference, ...],
|
|
396
|
+
) -> None:
|
|
397
|
+
"""Validate one zero-based molecular sidecar frame."""
|
|
398
|
+
owner_atom = molecule.GetAtomWithIdx(owner)
|
|
399
|
+
neighbors = {neighbor.GetIdx() for neighbor in owner_atom.GetNeighbors()}
|
|
400
|
+
virtual_counts = {"H": 0, "LP": 0}
|
|
401
|
+
for reference in references:
|
|
402
|
+
if type(reference) is int:
|
|
403
|
+
if reference < 0 or reference >= molecule.GetNumAtoms():
|
|
404
|
+
raise ValueError(f"Extended stereo reference {reference} is absent.")
|
|
405
|
+
if reference not in neighbors:
|
|
406
|
+
raise ValueError(
|
|
407
|
+
f"Extended stereo reference {reference} is not adjacent to {owner}."
|
|
408
|
+
)
|
|
409
|
+
continue
|
|
410
|
+
virtual = parse_virtual_reference(reference)
|
|
411
|
+
if virtual is None or virtual.center != owner:
|
|
412
|
+
raise ValueError(f"Invalid extended virtual reference: {reference!r}.")
|
|
413
|
+
virtual_counts[virtual.kind] += 1
|
|
414
|
+
available_h = int(owner_atom.GetNumExplicitHs()) + int(
|
|
415
|
+
owner_atom.GetNumImplicitHs()
|
|
416
|
+
)
|
|
417
|
+
if virtual_counts["H"] > available_h:
|
|
418
|
+
raise ValueError(f"Extended stereo requires unavailable hydrogen at {owner}.")
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _validate_molecular_configuration(
|
|
422
|
+
molecule: Chem.Mol,
|
|
423
|
+
configuration: MolecularStereoConfiguration,
|
|
424
|
+
) -> None:
|
|
425
|
+
"""Prove that declared zero-based descriptor supports exist in the molecule."""
|
|
426
|
+
atom_count = molecule.GetNumAtoms()
|
|
427
|
+
for descriptor in configuration.descriptors:
|
|
428
|
+
material = descriptor.dependencies
|
|
429
|
+
if any(atom < 0 or atom >= atom_count for atom in material):
|
|
430
|
+
raise ValueError("Extended stereo support contains an absent atom index.")
|
|
431
|
+
if isinstance(
|
|
432
|
+
descriptor,
|
|
433
|
+
(CumuleneAxisStereo, ExtendedCisTransStereo),
|
|
434
|
+
):
|
|
435
|
+
path = (
|
|
436
|
+
descriptor.axis_path
|
|
437
|
+
if isinstance(descriptor, CumuleneAxisStereo)
|
|
438
|
+
else descriptor.path
|
|
439
|
+
)
|
|
440
|
+
for left, right in zip(path, path[1:]):
|
|
441
|
+
bond = molecule.GetBondBetweenAtoms(left, right)
|
|
442
|
+
if bond is None or bond.GetBondType() != Chem.BondType.DOUBLE:
|
|
443
|
+
raise ValueError(
|
|
444
|
+
"Cumulene evidence requires a continuous double-bond path."
|
|
445
|
+
)
|
|
446
|
+
for owner, frame in zip(
|
|
447
|
+
(path[0], path[-1]),
|
|
448
|
+
descriptor.terminal_frames,
|
|
449
|
+
):
|
|
450
|
+
_validate_material_frame(molecule, owner, frame)
|
|
451
|
+
elif isinstance(descriptor, AtropBondStereo):
|
|
452
|
+
left, right = descriptor.atoms[2:4]
|
|
453
|
+
if molecule.GetBondBetweenAtoms(left, right) is None:
|
|
454
|
+
raise ValueError("Atrop evidence requires its central bond.")
|
|
455
|
+
_validate_material_frame(molecule, left, descriptor.atoms[:2])
|
|
456
|
+
_validate_material_frame(molecule, right, descriptor.atoms[4:])
|
|
457
|
+
elif isinstance(descriptor, HelicalStereo):
|
|
458
|
+
pairs = list(zip(descriptor.path, descriptor.path[1:]))
|
|
459
|
+
if descriptor.cyclic:
|
|
460
|
+
pairs.append((descriptor.path[-1], descriptor.path[0]))
|
|
461
|
+
if any(
|
|
462
|
+
molecule.GetBondBetweenAtoms(left, right) is None
|
|
463
|
+
for left, right in pairs
|
|
464
|
+
):
|
|
465
|
+
raise ValueError(
|
|
466
|
+
"Helical evidence requires a continuous molecular path."
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _configuration_covers_locus(
|
|
471
|
+
configuration: MolecularStereoConfiguration,
|
|
472
|
+
locus: PotentialStereoLocus,
|
|
473
|
+
) -> bool:
|
|
474
|
+
target = min(locus.support.path, tuple(reversed(locus.support.path)))
|
|
475
|
+
for descriptor in configuration.descriptors:
|
|
476
|
+
if isinstance(descriptor, (CumuleneAxisStereo, AtropBondStereo)):
|
|
477
|
+
path = descriptor.support.path
|
|
478
|
+
if min(path, tuple(reversed(path))) == target:
|
|
479
|
+
return True
|
|
480
|
+
return False
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _inject_molecular_configuration(
|
|
484
|
+
registry: dict[str, Any],
|
|
485
|
+
configuration: MolecularStereoConfiguration,
|
|
486
|
+
) -> None:
|
|
487
|
+
for descriptor in configuration.graph_descriptors():
|
|
488
|
+
key = descriptor_id(descriptor)
|
|
489
|
+
previous = registry.get(key)
|
|
490
|
+
if previous is not None and previous != descriptor:
|
|
491
|
+
raise ValueError(f"Conflicting configured stereo evidence at {key}.")
|
|
492
|
+
registry[key] = descriptor
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _configuration_result_metadata(
|
|
496
|
+
configuration: MolecularStereoConfiguration | None,
|
|
497
|
+
) -> dict[str, Any]:
|
|
498
|
+
if configuration is None:
|
|
499
|
+
return {}
|
|
500
|
+
return {
|
|
501
|
+
"configured_extended_descriptor_count": len(configuration.descriptors),
|
|
502
|
+
"stereo_evidence_source": configuration.evidence_source.value,
|
|
503
|
+
"extended_stability_status": configuration.stability.value,
|
|
504
|
+
"population_fraction": configuration.population_fraction,
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def classify_molecular_chirality(
|
|
509
|
+
molecule: Chem.Mol,
|
|
510
|
+
*,
|
|
511
|
+
stereo_complete: bool = True,
|
|
512
|
+
require_specified: bool = False,
|
|
513
|
+
stereo_configuration: MolecularStereoConfiguration | None = None,
|
|
514
|
+
) -> MolecularChiralityResult:
|
|
515
|
+
"""Classify a molecule as globally chiral or achiral.
|
|
516
|
+
|
|
517
|
+
The molecular identity profile uses element, isotope, total hydrogen
|
|
518
|
+
count, and connectivity. It otherwise follows the published
|
|
519
|
+
StereoMolGraph validation protocol and deliberately avoids raw charge and
|
|
520
|
+
bond-order fields from a single Lewis/resonance form. Assigned local
|
|
521
|
+
descriptors are retained. When ``stereo_complete`` is true, eligible
|
|
522
|
+
unrepresented sp3 topologies receive the published protocol's completion
|
|
523
|
+
probes before all parity-bearing configurations are reflected. Cumulene
|
|
524
|
+
and biaryl topology is reported separately as orientation-unspecified
|
|
525
|
+
potential loci and never injected into the descriptor registry.
|
|
526
|
+
|
|
527
|
+
This is a molecule classifier. It does not extract, apply, or compare
|
|
528
|
+
reaction rules. The identity profile is a graph-topology convention, not a
|
|
529
|
+
quantum/geometric chirality proof: bond-order distinctions that do not
|
|
530
|
+
change hydrogen topology can be collapsed, as in validation case VS170.
|
|
531
|
+
``stereo_complete`` cannot recover a stereochemical configuration erased
|
|
532
|
+
from an input SMILES. In particular, a topological biaryl candidate proves
|
|
533
|
+
neither orientation nor a high rotational barrier, and ordinary SMILES
|
|
534
|
+
cannot distinguish helicene handedness. Set ``require_specified`` to reject
|
|
535
|
+
RDKit-recognized unresolved input and detected unconfigured axial loci.
|
|
536
|
+
``stereo_configuration`` may add fixed extended descriptors only through
|
|
537
|
+
the authorized evidence model; its references are zero-based RDKit indices.
|
|
538
|
+
"""
|
|
539
|
+
if stereo_configuration is not None:
|
|
540
|
+
_validate_molecular_configuration(molecule, stereo_configuration)
|
|
541
|
+
working = _indexed_copy(molecule)
|
|
542
|
+
unspecified = _unspecified_stereo_loci(
|
|
543
|
+
working,
|
|
544
|
+
() if stereo_configuration is None else (stereo_configuration,),
|
|
545
|
+
)
|
|
546
|
+
potential_loci = detect_potential_stereo_loci(working)
|
|
547
|
+
potential_identifiers = tuple(
|
|
548
|
+
locus.identifier
|
|
549
|
+
for locus in potential_loci
|
|
550
|
+
if stereo_configuration is None
|
|
551
|
+
or not _configuration_covers_locus(stereo_configuration, locus)
|
|
552
|
+
)
|
|
553
|
+
unresolved = tuple(sorted((*unspecified, *potential_identifiers)))
|
|
554
|
+
if require_specified and unresolved:
|
|
555
|
+
raise UnspecifiedMolecularStereoError(unresolved)
|
|
556
|
+
graph = MolToGraph(attr_profile="minimal").transform(
|
|
557
|
+
working,
|
|
558
|
+
use_index_as_atom_map=True,
|
|
559
|
+
)
|
|
560
|
+
registry = dict(graph.graph.get("stereo_descriptors", {}))
|
|
561
|
+
completed = (
|
|
562
|
+
_complete_tetrahedral_topology(working, registry) if stereo_complete else ()
|
|
563
|
+
)
|
|
564
|
+
if stereo_configuration is not None:
|
|
565
|
+
_inject_molecular_configuration(registry, stereo_configuration)
|
|
566
|
+
graph.graph["stereo_descriptors"] = registry
|
|
567
|
+
|
|
568
|
+
mirror = graph.copy()
|
|
569
|
+
mirror.graph["stereo_descriptors"] = {
|
|
570
|
+
key: descriptor.invert() if descriptor.parity in {-1, 1} else descriptor
|
|
571
|
+
for key, descriptor in registry.items()
|
|
572
|
+
}
|
|
573
|
+
if _molecular_stereo_form(graph) != _molecular_stereo_form(mirror):
|
|
574
|
+
return MolecularChiralityResult(
|
|
575
|
+
classification=MolecularChirality.CHIRAL,
|
|
576
|
+
mirror_isomorphism=None,
|
|
577
|
+
descriptor_count=len(registry),
|
|
578
|
+
completed_tetrahedral_centers=completed,
|
|
579
|
+
potential_stereo_loci=potential_loci,
|
|
580
|
+
decision_method="stereo_colour_prefilter",
|
|
581
|
+
input_stereo_status=("underspecified" if unresolved else "specified"),
|
|
582
|
+
unspecified_stereo_loci=unresolved,
|
|
583
|
+
**_configuration_result_metadata(stereo_configuration),
|
|
584
|
+
)
|
|
585
|
+
matcher = _MolecularStereoGraphMatcher(graph, mirror)
|
|
586
|
+
mapping = dict(matcher.mapping) if matcher.is_isomorphic() else None
|
|
587
|
+
classification = (
|
|
588
|
+
MolecularChirality.ACHIRAL if mapping is not None else MolecularChirality.CHIRAL
|
|
589
|
+
)
|
|
590
|
+
return MolecularChiralityResult(
|
|
591
|
+
classification=classification,
|
|
592
|
+
mirror_isomorphism=(
|
|
593
|
+
None if mapping is None else tuple(sorted(mapping.items()))
|
|
594
|
+
),
|
|
595
|
+
descriptor_count=len(registry),
|
|
596
|
+
completed_tetrahedral_centers=completed,
|
|
597
|
+
potential_stereo_loci=potential_loci,
|
|
598
|
+
input_stereo_status=("underspecified" if unresolved else "specified"),
|
|
599
|
+
unspecified_stereo_loci=unresolved,
|
|
600
|
+
**_configuration_result_metadata(stereo_configuration),
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def is_molecular_chiral(
|
|
605
|
+
molecule: Chem.Mol,
|
|
606
|
+
*,
|
|
607
|
+
stereo_complete: bool = True,
|
|
608
|
+
require_specified: bool = False,
|
|
609
|
+
stereo_configuration: MolecularStereoConfiguration | None = None,
|
|
610
|
+
) -> bool:
|
|
611
|
+
"""Return the boolean whole-molecule chirality classification."""
|
|
612
|
+
return classify_molecular_chirality(
|
|
613
|
+
molecule,
|
|
614
|
+
stereo_complete=stereo_complete,
|
|
615
|
+
require_specified=require_specified,
|
|
616
|
+
stereo_configuration=stereo_configuration,
|
|
617
|
+
).is_chiral
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def _canonical_isomeric_smiles(molecule: Chem.Mol) -> str:
|
|
621
|
+
working = Chem.Mol(molecule)
|
|
622
|
+
for atom in working.GetAtoms():
|
|
623
|
+
atom.SetAtomMapNum(0)
|
|
624
|
+
return Chem.MolToSmiles(working, canonical=True, isomericSmiles=True)
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
@lru_cache(maxsize=4096)
|
|
628
|
+
def _cached_isomer_classification(
|
|
629
|
+
isomeric_smiles: str,
|
|
630
|
+
stereo_complete: bool,
|
|
631
|
+
) -> MolecularChirality:
|
|
632
|
+
molecule = Chem.MolFromSmiles(isomeric_smiles)
|
|
633
|
+
if molecule is None:
|
|
634
|
+
raise ValueError(f"RDKit rejected cached isomeric SMILES: {isomeric_smiles}")
|
|
635
|
+
return classify_molecular_chirality(
|
|
636
|
+
molecule,
|
|
637
|
+
stereo_complete=stereo_complete,
|
|
638
|
+
require_specified=False,
|
|
639
|
+
).classification
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def clear_molecular_chirality_cache() -> None:
|
|
643
|
+
"""Clear the bounded cache used by configuration-aware enumeration."""
|
|
644
|
+
_cached_isomer_classification.cache_clear()
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
@dataclass(frozen=True)
|
|
648
|
+
class _TetrahedralCompletionAnalysis:
|
|
649
|
+
"""Exact achiral-existence result over unresolved tetrahedral assignments."""
|
|
650
|
+
|
|
651
|
+
achiral_witness: str | None
|
|
652
|
+
baseline_smiles: str
|
|
653
|
+
baseline_classification: MolecularChirality
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def _parity_constraint_assignment(
|
|
657
|
+
descriptors: Mapping[int, TetrahedralStereo],
|
|
658
|
+
variable_centers: frozenset[int],
|
|
659
|
+
mapping: Mapping[int, int],
|
|
660
|
+
) -> dict[int, int] | None:
|
|
661
|
+
"""Solve mirror-compatibility XOR constraints for one automorphism."""
|
|
662
|
+
constraints: dict[int, list[tuple[int, int]]] = {
|
|
663
|
+
center: [] for center in descriptors
|
|
664
|
+
}
|
|
665
|
+
for center, source in descriptors.items():
|
|
666
|
+
target = descriptors.get(mapping[center])
|
|
667
|
+
if target is None:
|
|
668
|
+
return None
|
|
669
|
+
transported = source.relabel(mapping)
|
|
670
|
+
if transported == target.invert():
|
|
671
|
+
relation = 0
|
|
672
|
+
elif transported == target:
|
|
673
|
+
relation = 1
|
|
674
|
+
else:
|
|
675
|
+
return None
|
|
676
|
+
constraints[center].append((target.center, relation))
|
|
677
|
+
constraints[target.center].append((center, relation))
|
|
678
|
+
|
|
679
|
+
values = {center: 0 for center in descriptors if center not in variable_centers}
|
|
680
|
+
for root in descriptors:
|
|
681
|
+
if root not in values:
|
|
682
|
+
values[root] = 0
|
|
683
|
+
pending = [root]
|
|
684
|
+
while pending:
|
|
685
|
+
left = pending.pop()
|
|
686
|
+
for right, relation in constraints[left]:
|
|
687
|
+
expected = values[left] ^ relation
|
|
688
|
+
if right in values:
|
|
689
|
+
if values[right] != expected:
|
|
690
|
+
return None
|
|
691
|
+
continue
|
|
692
|
+
values[right] = expected
|
|
693
|
+
pending.append(right)
|
|
694
|
+
return values
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def _flip_tetrahedral_assignments(
|
|
698
|
+
baseline: Chem.Mol,
|
|
699
|
+
variable_centers: frozenset[int],
|
|
700
|
+
values: Mapping[int, int],
|
|
701
|
+
) -> Chem.Mol | None:
|
|
702
|
+
"""Apply a solved descriptor-parity assignment to an RDKit baseline."""
|
|
703
|
+
witness = Chem.Mol(baseline)
|
|
704
|
+
for center in variable_centers:
|
|
705
|
+
if not values[center]:
|
|
706
|
+
continue
|
|
707
|
+
atom = witness.GetAtomWithIdx(center - 1)
|
|
708
|
+
tag = atom.GetChiralTag()
|
|
709
|
+
if tag == Chem.ChiralType.CHI_TETRAHEDRAL_CW:
|
|
710
|
+
atom.SetChiralTag(Chem.ChiralType.CHI_TETRAHEDRAL_CCW)
|
|
711
|
+
elif tag == Chem.ChiralType.CHI_TETRAHEDRAL_CCW:
|
|
712
|
+
atom.SetChiralTag(Chem.ChiralType.CHI_TETRAHEDRAL_CW)
|
|
713
|
+
else:
|
|
714
|
+
return None
|
|
715
|
+
return witness
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
def _analyze_unresolved_tetrahedral_completions(
|
|
719
|
+
molecule: Chem.Mol,
|
|
720
|
+
*,
|
|
721
|
+
stereo_complete: bool,
|
|
722
|
+
automorphism_limit: int = 4096,
|
|
723
|
+
) -> _TetrahedralCompletionAnalysis | None:
|
|
724
|
+
"""Prove whether any unresolved tetrahedral completion is achiral.
|
|
725
|
+
|
|
726
|
+
The raw assignment space grows as ``2**n``. Achirality, however, only
|
|
727
|
+
requires one constitutional automorphism whose transported local
|
|
728
|
+
configurations equal the mirror configurations. For each automorphism,
|
|
729
|
+
those requirements are binary XOR constraints. Exhausting the much
|
|
730
|
+
smaller automorphism set therefore proves nonexistence without enumerating
|
|
731
|
+
every stereoisomer.
|
|
732
|
+
|
|
733
|
+
``None`` means this exact shortcut is outside its supported boundary or
|
|
734
|
+
exceeded its automorphism guard; callers must retain fail-closed capped
|
|
735
|
+
enumeration semantics.
|
|
736
|
+
"""
|
|
737
|
+
unresolved = tuple(
|
|
738
|
+
info
|
|
739
|
+
for info in Chem.FindPotentialStereo(molecule)
|
|
740
|
+
if info.specified != Chem.StereoSpecified.Specified
|
|
741
|
+
)
|
|
742
|
+
if not unresolved or any(
|
|
743
|
+
info.type != Chem.StereoType.Atom_Tetrahedral for info in unresolved
|
|
744
|
+
):
|
|
745
|
+
return None
|
|
746
|
+
variable_centers = frozenset(int(info.centeredOn) + 1 for info in unresolved)
|
|
747
|
+
options = StereoEnumerationOptions(
|
|
748
|
+
tryEmbedding=False,
|
|
749
|
+
onlyUnassigned=True,
|
|
750
|
+
maxIsomers=1,
|
|
751
|
+
rand=0x5A17,
|
|
752
|
+
unique=True,
|
|
753
|
+
)
|
|
754
|
+
baseline = next(iter(EnumerateStereoisomers(molecule, options=options)), None)
|
|
755
|
+
if baseline is None:
|
|
756
|
+
return None
|
|
757
|
+
baseline_smiles = _canonical_isomeric_smiles(baseline)
|
|
758
|
+
baseline_classification = classify_molecular_chirality(
|
|
759
|
+
baseline,
|
|
760
|
+
stereo_complete=stereo_complete,
|
|
761
|
+
require_specified=False,
|
|
762
|
+
).classification
|
|
763
|
+
|
|
764
|
+
indexed = _indexed_copy(baseline)
|
|
765
|
+
graph = MolToGraph(attr_profile="minimal").transform(
|
|
766
|
+
indexed,
|
|
767
|
+
use_index_as_atom_map=True,
|
|
768
|
+
)
|
|
769
|
+
registry = dict(graph.graph.get("stereo_descriptors", {}))
|
|
770
|
+
if stereo_complete:
|
|
771
|
+
_complete_tetrahedral_topology(indexed, registry)
|
|
772
|
+
if any(not isinstance(value, TetrahedralStereo) for value in registry.values()):
|
|
773
|
+
return None
|
|
774
|
+
descriptors = {value.center: value for value in registry.values()}
|
|
775
|
+
if not variable_centers.issubset(descriptors):
|
|
776
|
+
return None
|
|
777
|
+
|
|
778
|
+
graph.graph["stereo_descriptors"] = {}
|
|
779
|
+
verification_failed = False
|
|
780
|
+
matcher = _MolecularStereoGraphMatcher(graph, graph)
|
|
781
|
+
for position, mapping in enumerate(matcher.isomorphisms_iter()):
|
|
782
|
+
if position >= automorphism_limit:
|
|
783
|
+
return None
|
|
784
|
+
values = _parity_constraint_assignment(
|
|
785
|
+
descriptors,
|
|
786
|
+
variable_centers,
|
|
787
|
+
mapping,
|
|
788
|
+
)
|
|
789
|
+
if values is None:
|
|
790
|
+
continue
|
|
791
|
+
witness = _flip_tetrahedral_assignments(
|
|
792
|
+
baseline,
|
|
793
|
+
variable_centers,
|
|
794
|
+
values,
|
|
795
|
+
)
|
|
796
|
+
if witness is None:
|
|
797
|
+
verification_failed = True
|
|
798
|
+
continue
|
|
799
|
+
witness_result = classify_molecular_chirality(
|
|
800
|
+
witness,
|
|
801
|
+
stereo_complete=stereo_complete,
|
|
802
|
+
require_specified=False,
|
|
803
|
+
)
|
|
804
|
+
if witness_result.classification is MolecularChirality.ACHIRAL:
|
|
805
|
+
return _TetrahedralCompletionAnalysis(
|
|
806
|
+
achiral_witness=_canonical_isomeric_smiles(witness),
|
|
807
|
+
baseline_smiles=baseline_smiles,
|
|
808
|
+
baseline_classification=baseline_classification,
|
|
809
|
+
)
|
|
810
|
+
verification_failed = True
|
|
811
|
+
|
|
812
|
+
if verification_failed or baseline_classification is MolecularChirality.ACHIRAL:
|
|
813
|
+
return None
|
|
814
|
+
return _TetrahedralCompletionAnalysis(
|
|
815
|
+
achiral_witness=None,
|
|
816
|
+
baseline_smiles=baseline_smiles,
|
|
817
|
+
baseline_classification=baseline_classification,
|
|
818
|
+
)
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
def _assessment_configuration_alternatives(
|
|
822
|
+
molecule: Chem.Mol,
|
|
823
|
+
configuration_set: MolecularStereoConfigurationSet | None,
|
|
824
|
+
) -> tuple[MolecularStereoConfiguration | None, ...]:
|
|
825
|
+
if configuration_set is None:
|
|
826
|
+
return (None,)
|
|
827
|
+
for configuration in configuration_set.configurations:
|
|
828
|
+
_validate_molecular_configuration(molecule, configuration)
|
|
829
|
+
return configuration_set.configurations
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
def _covered_rdkit_binary_loci(
|
|
833
|
+
molecule: Chem.Mol,
|
|
834
|
+
configurations: tuple[MolecularStereoConfiguration | None, ...],
|
|
835
|
+
) -> int:
|
|
836
|
+
configured = tuple(item for item in configurations if item is not None)
|
|
837
|
+
if len(configured) != len(configurations):
|
|
838
|
+
return 0
|
|
839
|
+
return sum(
|
|
840
|
+
info.specified != Chem.StereoSpecified.Specified
|
|
841
|
+
and all(
|
|
842
|
+
_configuration_covers_rdkit_locus(molecule, info, configuration)
|
|
843
|
+
for configuration in configured
|
|
844
|
+
)
|
|
845
|
+
for info in Chem.FindPotentialStereo(molecule)
|
|
846
|
+
)
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
def assess_molecular_chirality( # noqa: C901
|
|
850
|
+
molecule: Chem.Mol,
|
|
851
|
+
*,
|
|
852
|
+
max_isomers: int = 256,
|
|
853
|
+
stereo_complete: bool = True,
|
|
854
|
+
try_embedding: bool = False,
|
|
855
|
+
use_cache: bool = True,
|
|
856
|
+
stereo_configurations: MolecularStereoConfigurationSet | None = None,
|
|
857
|
+
) -> MolecularChiralityAssessment:
|
|
858
|
+
"""Assess chirality across supported completions of unresolved stereo.
|
|
859
|
+
|
|
860
|
+
RDKit-supported unassigned tetrahedral atoms and double bonds are
|
|
861
|
+
enumerated. A mixed chiral/achiral population proves
|
|
862
|
+
``configuration_dependent`` even when the search cap truncates the full
|
|
863
|
+
population. For an oversized all-tetrahedral product, exact parity
|
|
864
|
+
constraints over the constitutional automorphisms can independently prove
|
|
865
|
+
that no achiral completion exists or construct an achiral witness. A
|
|
866
|
+
one-sided truncated population without such a proof is never promoted to
|
|
867
|
+
a necessary conclusion. Unsupported unresolved stereo types also fail
|
|
868
|
+
closed as ``unsupported_or_incomplete``.
|
|
869
|
+
"""
|
|
870
|
+
if molecule is None:
|
|
871
|
+
raise ValueError("Molecular chirality assessment requires a molecule.")
|
|
872
|
+
if type(max_isomers) is not int or max_isomers < 1:
|
|
873
|
+
raise ValueError("max_isomers must be a positive integer.")
|
|
874
|
+
|
|
875
|
+
working = Chem.Mol(molecule)
|
|
876
|
+
configurations = _assessment_configuration_alternatives(
|
|
877
|
+
working, stereo_configurations
|
|
878
|
+
)
|
|
879
|
+
for atom in working.GetAtoms():
|
|
880
|
+
atom.SetAtomMapNum(0)
|
|
881
|
+
Chem.AssignStereochemistry(working, cleanIt=False, force=True)
|
|
882
|
+
configured = tuple(
|
|
883
|
+
configuration for configuration in configurations if configuration is not None
|
|
884
|
+
)
|
|
885
|
+
unspecified = _unspecified_stereo_loci(working, configured)
|
|
886
|
+
potential_loci = detect_potential_stereo_loci(working)
|
|
887
|
+
unsupported_potential = tuple(
|
|
888
|
+
locus.identifier
|
|
889
|
+
for locus in potential_loci
|
|
890
|
+
if any(
|
|
891
|
+
configuration is None
|
|
892
|
+
or not _configuration_covers_locus(configuration, locus)
|
|
893
|
+
for configuration in configurations
|
|
894
|
+
)
|
|
895
|
+
)
|
|
896
|
+
unsupported = tuple(
|
|
897
|
+
sorted((*_unsupported_stereo_loci(working),) + unsupported_potential)
|
|
898
|
+
)
|
|
899
|
+
if unsupported:
|
|
900
|
+
return MolecularChiralityAssessment(
|
|
901
|
+
outcome=MolecularChiralityOutcome.UNSUPPORTED_OR_INCOMPLETE,
|
|
902
|
+
observed_classifications=(),
|
|
903
|
+
input_stereo_status="underspecified",
|
|
904
|
+
unspecified_stereo_loci=unspecified,
|
|
905
|
+
unsupported_stereo_loci=unsupported,
|
|
906
|
+
theoretical_isomer_upper_bound=0,
|
|
907
|
+
evaluated_isomer_count=0,
|
|
908
|
+
enumeration_complete=False,
|
|
909
|
+
max_isomers=max_isomers,
|
|
910
|
+
configured_alternative_count=(
|
|
911
|
+
0 if stereo_configurations is None else len(configurations)
|
|
912
|
+
),
|
|
913
|
+
configured_population_status=(
|
|
914
|
+
None
|
|
915
|
+
if stereo_configurations is None
|
|
916
|
+
else stereo_configurations.population_status.value
|
|
917
|
+
),
|
|
918
|
+
decision_method="unsupported_locus_detection",
|
|
919
|
+
)
|
|
920
|
+
|
|
921
|
+
options = StereoEnumerationOptions(
|
|
922
|
+
tryEmbedding=try_embedding,
|
|
923
|
+
onlyUnassigned=True,
|
|
924
|
+
maxIsomers=max_isomers,
|
|
925
|
+
rand=0x5A17,
|
|
926
|
+
unique=True,
|
|
927
|
+
)
|
|
928
|
+
raw_rdkit_theoretical = int(GetStereoisomerCount(working, options=options))
|
|
929
|
+
covered_binary = _covered_rdkit_binary_loci(working, configurations)
|
|
930
|
+
rdkit_theoretical = max(1, raw_rdkit_theoretical // (2**covered_binary))
|
|
931
|
+
theoretical = rdkit_theoretical * len(configurations)
|
|
932
|
+
classifications: set[MolecularChirality] = set()
|
|
933
|
+
representatives: dict[MolecularChirality, str] = {}
|
|
934
|
+
evaluated = 0
|
|
935
|
+
stopped_after_decisive_mixture = False
|
|
936
|
+
stopped_at_cap = False
|
|
937
|
+
decision_method = "stereo_completion_enumeration"
|
|
938
|
+
|
|
939
|
+
tetrahedral_analysis = None
|
|
940
|
+
if theoretical > max_isomers and configurations == (None,):
|
|
941
|
+
tetrahedral_analysis = _analyze_unresolved_tetrahedral_completions(
|
|
942
|
+
working,
|
|
943
|
+
stereo_complete=stereo_complete,
|
|
944
|
+
)
|
|
945
|
+
if tetrahedral_analysis is not None:
|
|
946
|
+
baseline_classification = tetrahedral_analysis.baseline_classification
|
|
947
|
+
classifications.add(baseline_classification)
|
|
948
|
+
representatives[baseline_classification] = tetrahedral_analysis.baseline_smiles
|
|
949
|
+
evaluated = 1
|
|
950
|
+
if tetrahedral_analysis.achiral_witness is None:
|
|
951
|
+
decision_method = "automorphism_parity_nonexistence_proof"
|
|
952
|
+
elif baseline_classification is MolecularChirality.CHIRAL:
|
|
953
|
+
classifications.add(MolecularChirality.ACHIRAL)
|
|
954
|
+
representatives[MolecularChirality.ACHIRAL] = (
|
|
955
|
+
tetrahedral_analysis.achiral_witness
|
|
956
|
+
)
|
|
957
|
+
evaluated += 1
|
|
958
|
+
stopped_after_decisive_mixture = True
|
|
959
|
+
decision_method = "automorphism_parity_achiral_witness"
|
|
960
|
+
|
|
961
|
+
analysis_is_decisive = tetrahedral_analysis is not None and (
|
|
962
|
+
tetrahedral_analysis.achiral_witness is None or len(classifications) > 1
|
|
963
|
+
)
|
|
964
|
+
if not analysis_is_decisive:
|
|
965
|
+
classifications.clear()
|
|
966
|
+
representatives.clear()
|
|
967
|
+
evaluated = 0
|
|
968
|
+
for isomer in EnumerateStereoisomers(working, options=options):
|
|
969
|
+
isomeric_smiles = _canonical_isomeric_smiles(isomer)
|
|
970
|
+
for configuration in configurations:
|
|
971
|
+
if evaluated >= max_isomers:
|
|
972
|
+
stopped_at_cap = True
|
|
973
|
+
break
|
|
974
|
+
classification = (
|
|
975
|
+
_cached_isomer_classification(isomeric_smiles, stereo_complete)
|
|
976
|
+
if use_cache and configuration is None
|
|
977
|
+
else classify_molecular_chirality(
|
|
978
|
+
isomer,
|
|
979
|
+
stereo_complete=stereo_complete,
|
|
980
|
+
stereo_configuration=configuration,
|
|
981
|
+
).classification
|
|
982
|
+
)
|
|
983
|
+
evaluated += 1
|
|
984
|
+
classifications.add(classification)
|
|
985
|
+
representatives.setdefault(classification, isomeric_smiles)
|
|
986
|
+
if len(classifications) > 1:
|
|
987
|
+
stopped_after_decisive_mixture = True
|
|
988
|
+
break
|
|
989
|
+
if stopped_after_decisive_mixture or stopped_at_cap:
|
|
990
|
+
break
|
|
991
|
+
|
|
992
|
+
observed = tuple(sorted(classifications, key=lambda value: value.value))
|
|
993
|
+
evidence = tuple(
|
|
994
|
+
(representatives[classification], classification) for classification in observed
|
|
995
|
+
)
|
|
996
|
+
enumeration_exhausted = (
|
|
997
|
+
not analysis_is_decisive
|
|
998
|
+
and not stopped_after_decisive_mixture
|
|
999
|
+
and not stopped_at_cap
|
|
1000
|
+
and (theoretical <= max_isomers or evaluated < max_isomers)
|
|
1001
|
+
)
|
|
1002
|
+
if len(classifications) > 1:
|
|
1003
|
+
outcome = MolecularChiralityOutcome.CONFIGURATION_DEPENDENT
|
|
1004
|
+
if decision_method == "stereo_completion_enumeration":
|
|
1005
|
+
decision_method = "enumerated_mixed_population"
|
|
1006
|
+
elif (
|
|
1007
|
+
decision_method == "automorphism_parity_nonexistence_proof"
|
|
1008
|
+
and classifications == {MolecularChirality.CHIRAL}
|
|
1009
|
+
):
|
|
1010
|
+
outcome = MolecularChiralityOutcome.NECESSARILY_CHIRAL
|
|
1011
|
+
elif not enumeration_exhausted or not classifications:
|
|
1012
|
+
outcome = MolecularChiralityOutcome.UNSUPPORTED_OR_INCOMPLETE
|
|
1013
|
+
decision_method = "capped_stereo_completion_enumeration"
|
|
1014
|
+
elif MolecularChirality.CHIRAL in classifications:
|
|
1015
|
+
outcome = MolecularChiralityOutcome.NECESSARILY_CHIRAL
|
|
1016
|
+
decision_method = "complete_stereo_completion_enumeration"
|
|
1017
|
+
else:
|
|
1018
|
+
outcome = MolecularChiralityOutcome.NECESSARILY_ACHIRAL
|
|
1019
|
+
decision_method = "complete_stereo_completion_enumeration"
|
|
1020
|
+
|
|
1021
|
+
return MolecularChiralityAssessment(
|
|
1022
|
+
outcome=outcome,
|
|
1023
|
+
observed_classifications=observed,
|
|
1024
|
+
input_stereo_status=("underspecified" if unspecified else "specified"),
|
|
1025
|
+
unspecified_stereo_loci=unspecified,
|
|
1026
|
+
unsupported_stereo_loci=(),
|
|
1027
|
+
theoretical_isomer_upper_bound=theoretical,
|
|
1028
|
+
evaluated_isomer_count=evaluated,
|
|
1029
|
+
enumeration_complete=enumeration_exhausted,
|
|
1030
|
+
max_isomers=max_isomers,
|
|
1031
|
+
representative_isomers=evidence,
|
|
1032
|
+
configured_alternative_count=(
|
|
1033
|
+
0 if stereo_configurations is None else len(configurations)
|
|
1034
|
+
),
|
|
1035
|
+
configured_population_status=(
|
|
1036
|
+
None
|
|
1037
|
+
if stereo_configurations is None
|
|
1038
|
+
else stereo_configurations.population_status.value
|
|
1039
|
+
),
|
|
1040
|
+
decision_method=decision_method,
|
|
1041
|
+
)
|