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,964 @@
|
|
|
1
|
+
"""Descriptor registry comparison for molecules, ITS graphs, and rules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Iterable, Mapping
|
|
7
|
+
|
|
8
|
+
import networkx as nx
|
|
9
|
+
|
|
10
|
+
from .descriptors import (
|
|
11
|
+
OctahedralStereo,
|
|
12
|
+
SUPPORTED_STEREO_DESCRIPTOR_CLASSES,
|
|
13
|
+
SquarePlanarStereo,
|
|
14
|
+
StereoValue,
|
|
15
|
+
TetrahedralStereo,
|
|
16
|
+
TrigonalBipyramidalStereo,
|
|
17
|
+
descriptor_id,
|
|
18
|
+
parse_virtual_reference,
|
|
19
|
+
virtual_reference,
|
|
20
|
+
)
|
|
21
|
+
from .orbits import Permutation, StereoRelation, StereoRelationKind
|
|
22
|
+
|
|
23
|
+
_ATOM_STEREO_TYPES = (
|
|
24
|
+
TetrahedralStereo,
|
|
25
|
+
SquarePlanarStereo,
|
|
26
|
+
TrigonalBipyramidalStereo,
|
|
27
|
+
OctahedralStereo,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class NonInvertibleStereoEffectError(ValueError):
|
|
32
|
+
"""Raised when reverse-rule construction would invent stereochemistry.
|
|
33
|
+
|
|
34
|
+
A known-to-unknown ``UNSPECIFIED`` endpoint relation is not one-to-one: a
|
|
35
|
+
specified reactant descriptor can map to an unknown product descriptor,
|
|
36
|
+
but that unknown state cannot identify which descriptor to recreate.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
reason = "non_reversible_unspecified_descriptor"
|
|
40
|
+
|
|
41
|
+
def __init__(self, targets: Iterable[str] = ()) -> None:
|
|
42
|
+
self.targets = tuple(sorted(set(targets)))
|
|
43
|
+
detail = f"; targets={list(self.targets)!r}" if self.targets else ""
|
|
44
|
+
super().__init__(f"{self.reason}{detail}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class StereoAlignmentError(ValueError):
|
|
48
|
+
"""Raised when a reaction effect has no unambiguous reference alignment."""
|
|
49
|
+
|
|
50
|
+
def __init__(self, issue_code: str, detail: str = "") -> None:
|
|
51
|
+
self.issue_code = issue_code
|
|
52
|
+
self.detail = detail
|
|
53
|
+
suffix = f": {detail}" if detail else ""
|
|
54
|
+
super().__init__(f"{issue_code}{suffix}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _peripheral_references(descriptor: StereoValue) -> tuple[Any, ...]:
|
|
58
|
+
if isinstance(descriptor, _ATOM_STEREO_TYPES):
|
|
59
|
+
return descriptor.atoms[1:]
|
|
60
|
+
from .extended_descriptors import HelicalStereo, PlanarChiralityStereo
|
|
61
|
+
from .global_stereo import FrameworkStereo
|
|
62
|
+
|
|
63
|
+
if isinstance(
|
|
64
|
+
descriptor,
|
|
65
|
+
(HelicalStereo, PlanarChiralityStereo, FrameworkStereo),
|
|
66
|
+
):
|
|
67
|
+
return ()
|
|
68
|
+
return (*descriptor.atoms[:2], *descriptor.atoms[4:])
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _mapping_sort_key(pair: tuple[Any, Any]) -> tuple[str, str, str, str]:
|
|
72
|
+
return type(pair[0]).__name__, repr(pair[0]), type(pair[1]).__name__, repr(pair[1])
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True)
|
|
76
|
+
class StereoReferenceAlignment:
|
|
77
|
+
"""Reference replacement evidence for one reaction-local stereo frame."""
|
|
78
|
+
|
|
79
|
+
mapping: tuple[tuple[Any, Any], ...] = ()
|
|
80
|
+
status: str = "identity"
|
|
81
|
+
removed: tuple[Any, ...] = ()
|
|
82
|
+
added: tuple[Any, ...] = ()
|
|
83
|
+
issue_code: str | None = None
|
|
84
|
+
detail: str | None = None
|
|
85
|
+
|
|
86
|
+
def __post_init__(self) -> None:
|
|
87
|
+
mapping = tuple(
|
|
88
|
+
sorted(
|
|
89
|
+
(tuple(pair) for pair in self.mapping),
|
|
90
|
+
key=_mapping_sort_key,
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
object.__setattr__(self, "mapping", mapping)
|
|
94
|
+
if self.status not in {"identity", "inferred", "explicit", "refused"}:
|
|
95
|
+
raise ValueError(f"Unsupported stereo alignment status: {self.status!r}.")
|
|
96
|
+
if self.status == "refused" and self.issue_code is None:
|
|
97
|
+
raise ValueError("A refused stereo alignment requires an issue code.")
|
|
98
|
+
if self.status != "refused" and self.issue_code is not None:
|
|
99
|
+
raise ValueError("Only a refused stereo alignment may carry an issue code.")
|
|
100
|
+
sources = [source for source, _ in mapping]
|
|
101
|
+
targets = [target for _, target in mapping]
|
|
102
|
+
if len(sources) != len(set(sources)) or len(targets) != len(set(targets)):
|
|
103
|
+
raise ValueError("A stereo reference mapping must be bijective.")
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def accepted(self) -> bool:
|
|
107
|
+
return self.status != "refused"
|
|
108
|
+
|
|
109
|
+
def reverse(self) -> "StereoReferenceAlignment":
|
|
110
|
+
if not self.accepted:
|
|
111
|
+
return StereoReferenceAlignment(
|
|
112
|
+
(),
|
|
113
|
+
"refused",
|
|
114
|
+
self.added,
|
|
115
|
+
self.removed,
|
|
116
|
+
self.issue_code,
|
|
117
|
+
self.detail,
|
|
118
|
+
)
|
|
119
|
+
status = "identity" if not self.mapping else self.status
|
|
120
|
+
return StereoReferenceAlignment(
|
|
121
|
+
tuple((target, source) for source, target in self.mapping),
|
|
122
|
+
status,
|
|
123
|
+
self.added,
|
|
124
|
+
self.removed,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def then(
|
|
128
|
+
self,
|
|
129
|
+
after: "StereoReferenceAlignment",
|
|
130
|
+
) -> "StereoReferenceAlignment":
|
|
131
|
+
"""Compose two endpoint-reference transports."""
|
|
132
|
+
if not self.accepted:
|
|
133
|
+
return self
|
|
134
|
+
if not after.accepted:
|
|
135
|
+
return after
|
|
136
|
+
first = dict(self.mapping)
|
|
137
|
+
second = dict(after.mapping)
|
|
138
|
+
composed = {
|
|
139
|
+
source: second.get(target, target) for source, target in first.items()
|
|
140
|
+
}
|
|
141
|
+
first_targets = set(first.values())
|
|
142
|
+
composed.update(
|
|
143
|
+
{
|
|
144
|
+
source: target
|
|
145
|
+
for source, target in second.items()
|
|
146
|
+
if source not in first_targets
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
composed = {
|
|
150
|
+
source: target for source, target in composed.items() if source != target
|
|
151
|
+
}
|
|
152
|
+
if not composed:
|
|
153
|
+
return StereoReferenceAlignment((), "identity")
|
|
154
|
+
status = "explicit" if "explicit" in {self.status, after.status} else "inferred"
|
|
155
|
+
return StereoReferenceAlignment(
|
|
156
|
+
tuple(composed.items()),
|
|
157
|
+
status,
|
|
158
|
+
tuple(composed),
|
|
159
|
+
tuple(composed.values()),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def relabel(
|
|
163
|
+
self,
|
|
164
|
+
mapping: Mapping[int, int],
|
|
165
|
+
) -> "StereoReferenceAlignment":
|
|
166
|
+
"""Relabel material and owner-qualified virtual references."""
|
|
167
|
+
|
|
168
|
+
def translate(reference: Any) -> Any:
|
|
169
|
+
if type(reference) is int:
|
|
170
|
+
return mapping.get(reference, reference)
|
|
171
|
+
virtual = parse_virtual_reference(reference)
|
|
172
|
+
if virtual is None:
|
|
173
|
+
return reference
|
|
174
|
+
return virtual_reference(
|
|
175
|
+
virtual.kind,
|
|
176
|
+
mapping.get(virtual.center, virtual.center),
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
return StereoReferenceAlignment(
|
|
180
|
+
tuple(
|
|
181
|
+
(translate(source), translate(target))
|
|
182
|
+
for source, target in self.mapping
|
|
183
|
+
),
|
|
184
|
+
self.status,
|
|
185
|
+
tuple(translate(value) for value in self.removed),
|
|
186
|
+
tuple(translate(value) for value in self.added),
|
|
187
|
+
self.issue_code,
|
|
188
|
+
self.detail,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def to_dict(self) -> dict[str, Any]:
|
|
192
|
+
result: dict[str, Any] = {
|
|
193
|
+
"status": self.status,
|
|
194
|
+
"mapping": [list(pair) for pair in self.mapping],
|
|
195
|
+
"removed": list(self.removed),
|
|
196
|
+
"added": list(self.added),
|
|
197
|
+
}
|
|
198
|
+
if self.issue_code is not None:
|
|
199
|
+
result["issue_code"] = self.issue_code
|
|
200
|
+
if self.detail is not None:
|
|
201
|
+
result["detail"] = self.detail
|
|
202
|
+
return result
|
|
203
|
+
|
|
204
|
+
@classmethod
|
|
205
|
+
def from_dict(
|
|
206
|
+
cls,
|
|
207
|
+
value: Mapping[str, Any],
|
|
208
|
+
) -> "StereoReferenceAlignment":
|
|
209
|
+
"""Read one serialized endpoint-reference alignment."""
|
|
210
|
+
return cls(
|
|
211
|
+
mapping=tuple(tuple(pair) for pair in value.get("mapping", ())),
|
|
212
|
+
status=str(value.get("status", "identity")),
|
|
213
|
+
removed=tuple(value.get("removed", ())),
|
|
214
|
+
added=tuple(value.get("added", ())),
|
|
215
|
+
issue_code=value.get("issue_code"),
|
|
216
|
+
detail=value.get("detail"),
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _reference_delta(
|
|
221
|
+
old: StereoValue,
|
|
222
|
+
new: StereoValue,
|
|
223
|
+
) -> tuple[tuple[Any, ...], tuple[Any, ...]]:
|
|
224
|
+
old_refs = _peripheral_references(old)
|
|
225
|
+
new_refs = _peripheral_references(new)
|
|
226
|
+
removed = tuple(reference for reference in old_refs if reference not in new_refs)
|
|
227
|
+
added = tuple(reference for reference in new_refs if reference not in old_refs)
|
|
228
|
+
return removed, added
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def align_stereo_references(
|
|
232
|
+
old: StereoValue,
|
|
233
|
+
new: StereoValue,
|
|
234
|
+
reference_mapping: Mapping[Any, Any] | Iterable[tuple[Any, Any]] | None = None,
|
|
235
|
+
) -> StereoReferenceAlignment:
|
|
236
|
+
"""Align endpoint frames, inferring only a unique one-for-one replacement."""
|
|
237
|
+
if type(old) is not type(new) or descriptor_id(old) != descriptor_id(new):
|
|
238
|
+
return StereoReferenceAlignment(
|
|
239
|
+
(),
|
|
240
|
+
"refused",
|
|
241
|
+
issue_code="STEREO_ALIGNMENT_INCOMPATIBLE_LOCUS",
|
|
242
|
+
detail="Endpoint descriptors do not share a shape and locus.",
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
removed, added = _reference_delta(old, new)
|
|
246
|
+
if reference_mapping is not None:
|
|
247
|
+
raw_items = (
|
|
248
|
+
reference_mapping.items()
|
|
249
|
+
if isinstance(reference_mapping, Mapping)
|
|
250
|
+
else reference_mapping
|
|
251
|
+
)
|
|
252
|
+
mapping = tuple(raw_items)
|
|
253
|
+
try:
|
|
254
|
+
candidate = StereoReferenceAlignment(
|
|
255
|
+
mapping,
|
|
256
|
+
"explicit" if mapping else "identity",
|
|
257
|
+
removed,
|
|
258
|
+
added,
|
|
259
|
+
)
|
|
260
|
+
except (TypeError, ValueError) as exc:
|
|
261
|
+
return StereoReferenceAlignment(
|
|
262
|
+
(),
|
|
263
|
+
"refused",
|
|
264
|
+
removed,
|
|
265
|
+
added,
|
|
266
|
+
"STEREO_ALIGNMENT_INVALID_EXPLICIT_MAP",
|
|
267
|
+
str(exc),
|
|
268
|
+
)
|
|
269
|
+
if set(source for source, _ in candidate.mapping) != set(removed) or set(
|
|
270
|
+
target for _, target in candidate.mapping
|
|
271
|
+
) != set(added):
|
|
272
|
+
return StereoReferenceAlignment(
|
|
273
|
+
(),
|
|
274
|
+
"refused",
|
|
275
|
+
removed,
|
|
276
|
+
added,
|
|
277
|
+
"STEREO_ALIGNMENT_INVALID_EXPLICIT_MAP",
|
|
278
|
+
"The explicit map must pair every removed reference with every added reference.",
|
|
279
|
+
)
|
|
280
|
+
return candidate
|
|
281
|
+
|
|
282
|
+
if not removed and not added:
|
|
283
|
+
return StereoReferenceAlignment((), "identity")
|
|
284
|
+
if len(removed) == len(added) == 1:
|
|
285
|
+
return StereoReferenceAlignment(
|
|
286
|
+
((removed[0], added[0]),),
|
|
287
|
+
"inferred",
|
|
288
|
+
removed,
|
|
289
|
+
added,
|
|
290
|
+
)
|
|
291
|
+
issue = (
|
|
292
|
+
"STEREO_ALIGNMENT_AMBIGUOUS"
|
|
293
|
+
if len(removed) == len(added) and len(removed) > 1
|
|
294
|
+
else "STEREO_ALIGNMENT_INCOMPATIBLE_REFERENCES"
|
|
295
|
+
)
|
|
296
|
+
return StereoReferenceAlignment(
|
|
297
|
+
(),
|
|
298
|
+
"refused",
|
|
299
|
+
removed,
|
|
300
|
+
added,
|
|
301
|
+
issue,
|
|
302
|
+
(
|
|
303
|
+
"Multiple replacements require an explicit bijection."
|
|
304
|
+
if issue == "STEREO_ALIGNMENT_AMBIGUOUS"
|
|
305
|
+
else "Removed and added reference populations differ."
|
|
306
|
+
),
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _relation_after_alignment(
|
|
311
|
+
old: StereoValue,
|
|
312
|
+
new: StereoValue,
|
|
313
|
+
alignment: StereoReferenceAlignment,
|
|
314
|
+
) -> StereoRelation | None:
|
|
315
|
+
if not alignment.accepted:
|
|
316
|
+
return None
|
|
317
|
+
aligned = (
|
|
318
|
+
old.replace_references(dict(alignment.mapping)) if alignment.mapping else old
|
|
319
|
+
)
|
|
320
|
+
return aligned.relation_to(new)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _change_from_relation(
|
|
324
|
+
old: StereoValue | None,
|
|
325
|
+
new: StereoValue | None,
|
|
326
|
+
transition: StereoValue | None,
|
|
327
|
+
relation: StereoRelation | None,
|
|
328
|
+
) -> str:
|
|
329
|
+
if old is None and new is None:
|
|
330
|
+
return "FLEETING" if transition is not None else "UNSPECIFIED"
|
|
331
|
+
if old is None:
|
|
332
|
+
return "FORMED"
|
|
333
|
+
if new is None:
|
|
334
|
+
return "BROKEN"
|
|
335
|
+
if relation is None:
|
|
336
|
+
return "UNSPECIFIED"
|
|
337
|
+
if relation.kind is StereoRelationKind.EQUIVALENT:
|
|
338
|
+
return "RETAINED"
|
|
339
|
+
if relation.kind is StereoRelationKind.OPPOSITE:
|
|
340
|
+
return "INVERTED"
|
|
341
|
+
if relation.kind is StereoRelationKind.RECONFIGURED:
|
|
342
|
+
# Beta-2 projected every descriptor-level TBP/octahedral inversion onto
|
|
343
|
+
# the binary INVERTED label. Keep that public string when applicable,
|
|
344
|
+
# while ``relation`` remains the authoritative non-binary class.
|
|
345
|
+
from .legacy import legacy_classify_stereo_change
|
|
346
|
+
|
|
347
|
+
legacy_projection = legacy_classify_stereo_change(old, new, transition)
|
|
348
|
+
if legacy_projection in {"RETAINED", "INVERTED"}:
|
|
349
|
+
return legacy_projection
|
|
350
|
+
return "UNSPECIFIED"
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _descriptor_from_configuration(
|
|
354
|
+
prototype: StereoValue,
|
|
355
|
+
frame: tuple[Any, ...],
|
|
356
|
+
*,
|
|
357
|
+
unspecified: bool,
|
|
358
|
+
) -> StereoValue:
|
|
359
|
+
parity = (
|
|
360
|
+
None
|
|
361
|
+
if unspecified
|
|
362
|
+
else (
|
|
363
|
+
0 if prototype.descriptor_class in {"square_planar", "planar_bond"} else 1
|
|
364
|
+
)
|
|
365
|
+
)
|
|
366
|
+
return type(prototype)(frame, parity, prototype.provenance)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _unspecified_descriptor(prototype: StereoValue) -> StereoValue:
|
|
370
|
+
value = prototype.to_dict()
|
|
371
|
+
if value["descriptor_class"] == "framework":
|
|
372
|
+
value["orientation"] = None
|
|
373
|
+
else:
|
|
374
|
+
value["parity"] = None
|
|
375
|
+
from .descriptors import stereo_from_dict
|
|
376
|
+
|
|
377
|
+
return stereo_from_dict(value)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
@dataclass(frozen=True)
|
|
381
|
+
class StereoChange:
|
|
382
|
+
"""One rule-local stereochemical state transition.
|
|
383
|
+
|
|
384
|
+
``before`` and ``after`` are the stable molecular states. ``transition``
|
|
385
|
+
is optional ITS/transition-state stereo and is deliberately separate from
|
|
386
|
+
either endpoint. The textual ``change`` remains a derived, readable
|
|
387
|
+
classification for compatibility with existing SynKit rule consumers.
|
|
388
|
+
"""
|
|
389
|
+
|
|
390
|
+
change: str
|
|
391
|
+
before: StereoValue | None
|
|
392
|
+
after: StereoValue | None
|
|
393
|
+
transition: StereoValue | None = None
|
|
394
|
+
relation: StereoRelation | None = None
|
|
395
|
+
reference_mapping: tuple[tuple[Any, Any], ...] | Mapping[Any, Any] | None = None
|
|
396
|
+
alignment: StereoReferenceAlignment | None = None
|
|
397
|
+
|
|
398
|
+
def __post_init__(self) -> None:
|
|
399
|
+
if self.before is None or self.after is None:
|
|
400
|
+
alignment = self.alignment or StereoReferenceAlignment((), "identity")
|
|
401
|
+
relation = self.relation
|
|
402
|
+
else:
|
|
403
|
+
alignment = self.alignment or align_stereo_references(
|
|
404
|
+
self.before,
|
|
405
|
+
self.after,
|
|
406
|
+
self.reference_mapping,
|
|
407
|
+
)
|
|
408
|
+
relation = self.relation or _relation_after_alignment(
|
|
409
|
+
self.before,
|
|
410
|
+
self.after,
|
|
411
|
+
alignment,
|
|
412
|
+
)
|
|
413
|
+
object.__setattr__(self, "alignment", alignment)
|
|
414
|
+
object.__setattr__(self, "reference_mapping", alignment.mapping)
|
|
415
|
+
object.__setattr__(self, "relation", relation)
|
|
416
|
+
|
|
417
|
+
@classmethod
|
|
418
|
+
def from_endpoints(
|
|
419
|
+
cls,
|
|
420
|
+
before: StereoValue | None,
|
|
421
|
+
after: StereoValue | None,
|
|
422
|
+
transition: StereoValue | None = None,
|
|
423
|
+
*,
|
|
424
|
+
reference_mapping: Mapping[Any, Any] | Iterable[tuple[Any, Any]] | None = None,
|
|
425
|
+
) -> "StereoChange":
|
|
426
|
+
alignment = (
|
|
427
|
+
align_stereo_references(before, after, reference_mapping)
|
|
428
|
+
if before is not None and after is not None
|
|
429
|
+
else StereoReferenceAlignment((), "identity")
|
|
430
|
+
)
|
|
431
|
+
relation = (
|
|
432
|
+
_relation_after_alignment(before, after, alignment)
|
|
433
|
+
if before is not None and after is not None
|
|
434
|
+
else None
|
|
435
|
+
)
|
|
436
|
+
return cls(
|
|
437
|
+
_change_from_relation(before, after, transition, relation),
|
|
438
|
+
before,
|
|
439
|
+
after,
|
|
440
|
+
transition,
|
|
441
|
+
relation,
|
|
442
|
+
alignment.mapping,
|
|
443
|
+
alignment,
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
@property
|
|
447
|
+
def dependencies(self) -> frozenset[int]:
|
|
448
|
+
values = set()
|
|
449
|
+
if self.before:
|
|
450
|
+
values.update(self.before.dependencies)
|
|
451
|
+
if self.after:
|
|
452
|
+
values.update(self.after.dependencies)
|
|
453
|
+
if self.transition:
|
|
454
|
+
values.update(self.transition.dependencies)
|
|
455
|
+
return frozenset(values)
|
|
456
|
+
|
|
457
|
+
@property
|
|
458
|
+
def fleeting(self) -> StereoValue | None:
|
|
459
|
+
"""Compatibility term for stereo present only at the transition state."""
|
|
460
|
+
return self.transition
|
|
461
|
+
|
|
462
|
+
@property
|
|
463
|
+
def evidence_kind(self) -> str:
|
|
464
|
+
"""Return the rich relation label behind the compatibility projection."""
|
|
465
|
+
if self.before is None and self.after is None:
|
|
466
|
+
return "fleeting" if self.transition is not None else "unspecified"
|
|
467
|
+
if self.before is None:
|
|
468
|
+
return "formed"
|
|
469
|
+
if self.after is None:
|
|
470
|
+
return "broken"
|
|
471
|
+
if self.relation is None:
|
|
472
|
+
return "alignment_refused"
|
|
473
|
+
if self.relation.kind is StereoRelationKind.RECONFIGURED:
|
|
474
|
+
encoded = "-".join(map(str, self.relation.class_id or ()))
|
|
475
|
+
return f"reconfigured:{encoded}"
|
|
476
|
+
return self.relation.kind.value
|
|
477
|
+
|
|
478
|
+
def relation_evidence(self) -> dict[str, Any]:
|
|
479
|
+
"""Return deterministic, machine-readable reaction relation evidence."""
|
|
480
|
+
result: dict[str, Any] = {
|
|
481
|
+
"kind": self.evidence_kind,
|
|
482
|
+
"alignment": self.alignment.to_dict(),
|
|
483
|
+
}
|
|
484
|
+
if self.relation is not None:
|
|
485
|
+
relation: dict[str, Any] = {
|
|
486
|
+
"kind": self.relation.kind.value,
|
|
487
|
+
"shape": self.relation.shape,
|
|
488
|
+
"class_id": (
|
|
489
|
+
list(self.relation.class_id)
|
|
490
|
+
if self.relation.class_id is not None
|
|
491
|
+
else None
|
|
492
|
+
),
|
|
493
|
+
}
|
|
494
|
+
if self.relation.witness is not None:
|
|
495
|
+
relation["witness"] = list(self.relation.witness.permutation.image)
|
|
496
|
+
result["relation"] = relation
|
|
497
|
+
return result
|
|
498
|
+
|
|
499
|
+
@property
|
|
500
|
+
def non_invertible(self) -> bool:
|
|
501
|
+
"""Whether reversing this change would fabricate endpoint parity."""
|
|
502
|
+
return (
|
|
503
|
+
self.change == "UNSPECIFIED"
|
|
504
|
+
and self.before is not None
|
|
505
|
+
and self.after is not None
|
|
506
|
+
and self.before.parity is not None
|
|
507
|
+
and self.after.parity is None
|
|
508
|
+
) or self.alignment.issue_code == "STEREO_COMPOSITION_INFORMATION_LOSS"
|
|
509
|
+
|
|
510
|
+
def reverse(
|
|
511
|
+
self,
|
|
512
|
+
*,
|
|
513
|
+
semantics: str = "orbit",
|
|
514
|
+
diagnostics: list[Any] | None = None,
|
|
515
|
+
) -> "StereoChange":
|
|
516
|
+
"""Return the same state transition in the opposite direction."""
|
|
517
|
+
from .legacy import (
|
|
518
|
+
StereoSemanticComparison,
|
|
519
|
+
StereoSemanticsMode,
|
|
520
|
+
legacy_classify_stereo_change,
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
if self.non_invertible:
|
|
524
|
+
raise NonInvertibleStereoEffectError()
|
|
525
|
+
mode = StereoSemanticsMode(semantics)
|
|
526
|
+
reverse_mapping = (
|
|
527
|
+
{target: source for source, target in self.reference_mapping}
|
|
528
|
+
if self.alignment.status == "explicit"
|
|
529
|
+
else None
|
|
530
|
+
)
|
|
531
|
+
orbit_change = StereoChange.from_endpoints(
|
|
532
|
+
self.after,
|
|
533
|
+
self.before,
|
|
534
|
+
self.transition,
|
|
535
|
+
reference_mapping=reverse_mapping,
|
|
536
|
+
)
|
|
537
|
+
if mode is StereoSemanticsMode.ORBIT:
|
|
538
|
+
return orbit_change
|
|
539
|
+
legacy_change = legacy_classify_stereo_change(
|
|
540
|
+
self.after,
|
|
541
|
+
self.before,
|
|
542
|
+
self.transition,
|
|
543
|
+
)
|
|
544
|
+
if mode is StereoSemanticsMode.COMPARE and diagnostics is not None:
|
|
545
|
+
diagnostics.append(
|
|
546
|
+
StereoSemanticComparison.create(
|
|
547
|
+
"reaction_stereo_reverse",
|
|
548
|
+
orbit_change.change,
|
|
549
|
+
legacy_change,
|
|
550
|
+
)
|
|
551
|
+
)
|
|
552
|
+
if mode is StereoSemanticsMode.LEGACY:
|
|
553
|
+
return StereoChange(
|
|
554
|
+
legacy_change,
|
|
555
|
+
orbit_change.before,
|
|
556
|
+
orbit_change.after,
|
|
557
|
+
orbit_change.transition,
|
|
558
|
+
orbit_change.relation,
|
|
559
|
+
orbit_change.reference_mapping,
|
|
560
|
+
orbit_change.alignment,
|
|
561
|
+
)
|
|
562
|
+
return orbit_change
|
|
563
|
+
|
|
564
|
+
def _permutation_for_substrate(
|
|
565
|
+
self, descriptor: StereoValue, permutation: Permutation
|
|
566
|
+
) -> Permutation:
|
|
567
|
+
"""Express a specified endpoint witness in the matched frame basis.
|
|
568
|
+
|
|
569
|
+
:param descriptor: Specified input descriptor at the validated guard locus.
|
|
570
|
+
:param permutation: Concrete endpoint witness stored by this change.
|
|
571
|
+
:return: Rebased witness, or the same intrinsically defined operation.
|
|
572
|
+
:raises StereoAlignmentError: Input references or a different guard orbit
|
|
573
|
+
do not determine the operation's basis.
|
|
574
|
+
"""
|
|
575
|
+
source_relation = self.before.relation_to(descriptor)
|
|
576
|
+
if source_relation.witness is None:
|
|
577
|
+
raise StereoAlignmentError(
|
|
578
|
+
"STEREO_ALIGNMENT_INCOMPATIBLE_REFERENCES",
|
|
579
|
+
"The substrate frame cannot be aligned to the rule guard frame.",
|
|
580
|
+
)
|
|
581
|
+
if source_relation.kind is StereoRelationKind.EQUIVALENT:
|
|
582
|
+
# Tuple actions are pullbacks. If u' = gamma.apply(u), the
|
|
583
|
+
# same operation in the new basis has slot image
|
|
584
|
+
# gamma^-1 o pi o gamma; its product differs only by gamma.
|
|
585
|
+
basis = source_relation.witness.permutation
|
|
586
|
+
return basis.inverse().then(permutation).then(basis)
|
|
587
|
+
group = self.before.configuration.definition.preserving_group
|
|
588
|
+
if any(
|
|
589
|
+
permutation.inverse().then(element).then(permutation) not in group
|
|
590
|
+
for element in group.elements
|
|
591
|
+
):
|
|
592
|
+
raise StereoAlignmentError(
|
|
593
|
+
"STEREO_RELATION_AMBIGUOUS_BASIS",
|
|
594
|
+
"A non-normalizing witness requires a substrate in the "
|
|
595
|
+
"rule guard's configuration orbit.",
|
|
596
|
+
)
|
|
597
|
+
return permutation
|
|
598
|
+
|
|
599
|
+
def _apply_orbit_to(self, descriptor: StereoValue) -> StereoValue | None:
|
|
600
|
+
if self.before is None:
|
|
601
|
+
return self.after
|
|
602
|
+
if self.after is None:
|
|
603
|
+
return None
|
|
604
|
+
if type(descriptor) is not type(self.before) or descriptor_id(descriptor) != (
|
|
605
|
+
descriptor_id(self.before)
|
|
606
|
+
):
|
|
607
|
+
raise StereoAlignmentError(
|
|
608
|
+
"STEREO_ALIGNMENT_INCOMPATIBLE_SUBSTRATE",
|
|
609
|
+
"The substrate descriptor does not share the rule effect locus.",
|
|
610
|
+
)
|
|
611
|
+
if not self.alignment.accepted:
|
|
612
|
+
raise StereoAlignmentError(
|
|
613
|
+
self.alignment.issue_code or "STEREO_ALIGNMENT_REFUSED",
|
|
614
|
+
self.alignment.detail or "",
|
|
615
|
+
)
|
|
616
|
+
if self.relation is None:
|
|
617
|
+
raise StereoAlignmentError(
|
|
618
|
+
"STEREO_RELATION_NOT_REPLAYABLE",
|
|
619
|
+
"The rule effect has no configuration relation.",
|
|
620
|
+
)
|
|
621
|
+
if (
|
|
622
|
+
self.relation.witness is None
|
|
623
|
+
or self.after.descriptor_class not in SUPPORTED_STEREO_DESCRIPTOR_CLASSES
|
|
624
|
+
):
|
|
625
|
+
source_relation = self.before.relation_to(descriptor)
|
|
626
|
+
if source_relation.kind is StereoRelationKind.EQUIVALENT:
|
|
627
|
+
return self.after
|
|
628
|
+
if source_relation.kind is StereoRelationKind.OPPOSITE:
|
|
629
|
+
return self.after.invert()
|
|
630
|
+
if source_relation.kind is StereoRelationKind.UNSPECIFIED:
|
|
631
|
+
return _unspecified_descriptor(self.after)
|
|
632
|
+
raise StereoAlignmentError(
|
|
633
|
+
"STEREO_RELATION_NOT_REPLAYABLE",
|
|
634
|
+
"The substrate has no transportable relation to the rule guard.",
|
|
635
|
+
)
|
|
636
|
+
unspecified = (
|
|
637
|
+
descriptor.parity is None
|
|
638
|
+
or self.after.parity is None
|
|
639
|
+
or self.relation.kind is StereoRelationKind.UNSPECIFIED
|
|
640
|
+
)
|
|
641
|
+
permutation = self.relation.witness.permutation
|
|
642
|
+
if not unspecified:
|
|
643
|
+
permutation = self._permutation_for_substrate(descriptor, permutation)
|
|
644
|
+
aligned = descriptor.replace_references(dict(self.reference_mapping))
|
|
645
|
+
frame = permutation.apply(aligned.configuration.frame)
|
|
646
|
+
return _descriptor_from_configuration(
|
|
647
|
+
self.after,
|
|
648
|
+
frame,
|
|
649
|
+
unspecified=unspecified,
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
def apply_to(
|
|
653
|
+
self,
|
|
654
|
+
descriptor: StereoValue,
|
|
655
|
+
*,
|
|
656
|
+
semantics: str = "orbit",
|
|
657
|
+
diagnostics: list[Any] | None = None,
|
|
658
|
+
) -> StereoValue | None:
|
|
659
|
+
"""Apply this relation, optionally auditing the frozen Beta-2 result."""
|
|
660
|
+
from .legacy import (
|
|
661
|
+
StereoSemanticComparison,
|
|
662
|
+
StereoSemanticsMode,
|
|
663
|
+
legacy_apply_stereo_change,
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
mode = StereoSemanticsMode(semantics)
|
|
667
|
+
if mode is StereoSemanticsMode.LEGACY:
|
|
668
|
+
return legacy_apply_stereo_change(self, descriptor)
|
|
669
|
+
orbit_result = self._apply_orbit_to(descriptor)
|
|
670
|
+
if mode is StereoSemanticsMode.COMPARE:
|
|
671
|
+
legacy_result = legacy_apply_stereo_change(self, descriptor)
|
|
672
|
+
expected_divergence = None
|
|
673
|
+
if (
|
|
674
|
+
self.relation is not None
|
|
675
|
+
and self.relation.kind is StereoRelationKind.RECONFIGURED
|
|
676
|
+
and self.relation.shape in {"trigonal_bipyramidal", "octahedral"}
|
|
677
|
+
):
|
|
678
|
+
expected_divergence = "nonbinary_orbit_reconfiguration"
|
|
679
|
+
if diagnostics is not None:
|
|
680
|
+
diagnostics.append(
|
|
681
|
+
StereoSemanticComparison.create(
|
|
682
|
+
"reaction_stereo_application",
|
|
683
|
+
orbit_result,
|
|
684
|
+
legacy_result,
|
|
685
|
+
expected_divergence=expected_divergence,
|
|
686
|
+
)
|
|
687
|
+
)
|
|
688
|
+
return orbit_result
|
|
689
|
+
|
|
690
|
+
def relabel(self, mapping: Mapping[int, int]) -> "StereoChange":
|
|
691
|
+
"""Relabel endpoints and the explicit reference alignment together."""
|
|
692
|
+
|
|
693
|
+
def relabel_reference(reference: Any) -> Any:
|
|
694
|
+
if type(reference) is int:
|
|
695
|
+
return mapping.get(reference, reference)
|
|
696
|
+
virtual = parse_virtual_reference(reference)
|
|
697
|
+
if virtual is None:
|
|
698
|
+
return reference
|
|
699
|
+
return virtual_reference(
|
|
700
|
+
virtual.kind,
|
|
701
|
+
mapping.get(virtual.center, virtual.center),
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
translated_reference_mapping = (
|
|
705
|
+
{
|
|
706
|
+
relabel_reference(source): relabel_reference(target)
|
|
707
|
+
for source, target in self.reference_mapping
|
|
708
|
+
}
|
|
709
|
+
if self.alignment.status == "explicit"
|
|
710
|
+
else None
|
|
711
|
+
)
|
|
712
|
+
before = self.before.relabel(mapping) if self.before is not None else None
|
|
713
|
+
after = self.after.relabel(mapping) if self.after is not None else None
|
|
714
|
+
transition = (
|
|
715
|
+
self.transition.relabel(mapping) if self.transition is not None else None
|
|
716
|
+
)
|
|
717
|
+
if not self.alignment.accepted:
|
|
718
|
+
return StereoChange(
|
|
719
|
+
self.change,
|
|
720
|
+
before,
|
|
721
|
+
after,
|
|
722
|
+
transition,
|
|
723
|
+
alignment=self.alignment.relabel(mapping),
|
|
724
|
+
)
|
|
725
|
+
return StereoChange.from_endpoints(
|
|
726
|
+
before,
|
|
727
|
+
after,
|
|
728
|
+
transition,
|
|
729
|
+
reference_mapping=translated_reference_mapping,
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
def then(self, after: "StereoChange") -> "StereoChange":
|
|
733
|
+
"""Compose two consecutive changes at the same stereo locus."""
|
|
734
|
+
if self.after != after.before:
|
|
735
|
+
raise StereoAlignmentError(
|
|
736
|
+
"STEREO_COMPOSITION_NONCOMPOSABLE",
|
|
737
|
+
"The first product descriptor must equal the second "
|
|
738
|
+
"reactant descriptor.",
|
|
739
|
+
)
|
|
740
|
+
if self.before is None and after.after is None:
|
|
741
|
+
transition = self.after or self.transition or after.transition
|
|
742
|
+
return StereoChange.from_endpoints(None, None, transition)
|
|
743
|
+
if self.after is None and after.before is None:
|
|
744
|
+
alignment = StereoReferenceAlignment(
|
|
745
|
+
(),
|
|
746
|
+
"refused",
|
|
747
|
+
issue_code="STEREO_COMPOSITION_INFORMATION_LOSS",
|
|
748
|
+
detail=(
|
|
749
|
+
"A broken descriptor was recreated without a continuous "
|
|
750
|
+
"configuration witness."
|
|
751
|
+
),
|
|
752
|
+
)
|
|
753
|
+
return StereoChange(
|
|
754
|
+
"UNSPECIFIED",
|
|
755
|
+
self.before,
|
|
756
|
+
after.after,
|
|
757
|
+
alignment=alignment,
|
|
758
|
+
)
|
|
759
|
+
alignment = self.alignment.then(after.alignment)
|
|
760
|
+
reference_mapping = (
|
|
761
|
+
dict(alignment.mapping)
|
|
762
|
+
if alignment.status == "explicit" or len(alignment.mapping) > 1
|
|
763
|
+
else None
|
|
764
|
+
)
|
|
765
|
+
return StereoChange.from_endpoints(
|
|
766
|
+
self.before,
|
|
767
|
+
after.after,
|
|
768
|
+
reference_mapping=reference_mapping,
|
|
769
|
+
)
|
|
770
|
+
|
|
771
|
+
def to_dict(self) -> dict[str, Any]:
|
|
772
|
+
"""Return the lossless version-independent value payload."""
|
|
773
|
+
|
|
774
|
+
def encode(value: StereoValue | None) -> dict[str, Any] | None:
|
|
775
|
+
return value.to_dict() if value is not None else None
|
|
776
|
+
|
|
777
|
+
return {
|
|
778
|
+
"change": self.change,
|
|
779
|
+
"before": encode(self.before),
|
|
780
|
+
"after": encode(self.after),
|
|
781
|
+
"transition": encode(self.transition),
|
|
782
|
+
"alignment": self.alignment.to_dict(),
|
|
783
|
+
"relation_evidence": self.relation_evidence(),
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
@classmethod
|
|
787
|
+
def from_dict(cls, value: Mapping[str, Any]) -> "StereoChange":
|
|
788
|
+
"""Read a serialized change and rederive its orbit relation."""
|
|
789
|
+
from .descriptors import stereo_from_dict
|
|
790
|
+
|
|
791
|
+
def decode(item: Any) -> StereoValue | None:
|
|
792
|
+
return stereo_from_dict(item) if item is not None else None
|
|
793
|
+
|
|
794
|
+
alignment = StereoReferenceAlignment.from_dict(value.get("alignment", {}))
|
|
795
|
+
return cls(
|
|
796
|
+
change=str(value["change"]),
|
|
797
|
+
before=decode(value.get("before")),
|
|
798
|
+
after=decode(value.get("after")),
|
|
799
|
+
transition=decode(value.get("transition")),
|
|
800
|
+
reference_mapping=alignment.mapping,
|
|
801
|
+
alignment=alignment,
|
|
802
|
+
)
|
|
803
|
+
|
|
804
|
+
def signature(self) -> tuple[Any, ...]:
|
|
805
|
+
"""Return a stable, hashable rule-identity signature."""
|
|
806
|
+
return (
|
|
807
|
+
self.change,
|
|
808
|
+
self.before,
|
|
809
|
+
self.after,
|
|
810
|
+
self.transition,
|
|
811
|
+
self.reference_mapping,
|
|
812
|
+
self.evidence_kind,
|
|
813
|
+
)
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def classify_stereo_change(
|
|
817
|
+
old: StereoValue | None,
|
|
818
|
+
new: StereoValue | None,
|
|
819
|
+
transition: StereoValue | None = None,
|
|
820
|
+
*,
|
|
821
|
+
reference_mapping: Mapping[Any, Any] | Iterable[tuple[Any, Any]] | None = None,
|
|
822
|
+
semantics: str = "orbit",
|
|
823
|
+
diagnostics: list[Any] | None = None,
|
|
824
|
+
) -> str:
|
|
825
|
+
"""Derive the compatibility label from an aligned orbit relation."""
|
|
826
|
+
from .legacy import (
|
|
827
|
+
StereoSemanticComparison,
|
|
828
|
+
StereoSemanticsMode,
|
|
829
|
+
legacy_classify_stereo_change,
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
mode = StereoSemanticsMode(semantics)
|
|
833
|
+
legacy_result = None
|
|
834
|
+
if mode is not StereoSemanticsMode.ORBIT:
|
|
835
|
+
legacy_result = legacy_classify_stereo_change(old, new, transition)
|
|
836
|
+
if mode is StereoSemanticsMode.LEGACY:
|
|
837
|
+
return legacy_result
|
|
838
|
+
change = StereoChange.from_endpoints(
|
|
839
|
+
old,
|
|
840
|
+
new,
|
|
841
|
+
transition,
|
|
842
|
+
reference_mapping=reference_mapping,
|
|
843
|
+
)
|
|
844
|
+
if mode is StereoSemanticsMode.COMPARE and diagnostics is not None:
|
|
845
|
+
diagnostics.append(
|
|
846
|
+
StereoSemanticComparison.create(
|
|
847
|
+
"reaction_stereo_change",
|
|
848
|
+
change.change,
|
|
849
|
+
legacy_result,
|
|
850
|
+
)
|
|
851
|
+
)
|
|
852
|
+
if (
|
|
853
|
+
change.relation is not None
|
|
854
|
+
and change.relation.kind is StereoRelationKind.RECONFIGURED
|
|
855
|
+
and change.relation.shape in {"trigonal_bipyramidal", "octahedral"}
|
|
856
|
+
):
|
|
857
|
+
diagnostics.append(
|
|
858
|
+
StereoSemanticComparison.create(
|
|
859
|
+
"reaction_stereo_relation",
|
|
860
|
+
change.evidence_kind,
|
|
861
|
+
str(legacy_result).lower(),
|
|
862
|
+
expected_divergence="nonbinary_orbit_reconfiguration",
|
|
863
|
+
)
|
|
864
|
+
)
|
|
865
|
+
return change.change
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
def stereo_registry(graph: nx.Graph) -> Mapping[str, StereoValue]:
|
|
869
|
+
return graph.graph.get("stereo_descriptors", {})
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
def compare_stereo_registries(
|
|
873
|
+
before: nx.Graph,
|
|
874
|
+
after: nx.Graph,
|
|
875
|
+
transition: nx.Graph | None = None,
|
|
876
|
+
*,
|
|
877
|
+
reference_mappings: (
|
|
878
|
+
Mapping[
|
|
879
|
+
str,
|
|
880
|
+
Mapping[Any, Any] | Iterable[tuple[Any, Any]],
|
|
881
|
+
]
|
|
882
|
+
| None
|
|
883
|
+
) = None,
|
|
884
|
+
semantics: str = "orbit",
|
|
885
|
+
diagnostics: list[Any] | None = None,
|
|
886
|
+
) -> dict[str, StereoChange]:
|
|
887
|
+
left, right = stereo_registry(before), stereo_registry(after)
|
|
888
|
+
middle = stereo_registry(transition) if transition is not None else {}
|
|
889
|
+
changes: dict[str, StereoChange] = {}
|
|
890
|
+
for key in sorted(set(left) | set(right) | set(middle)):
|
|
891
|
+
old, new, transient = left.get(key), right.get(key), middle.get(key)
|
|
892
|
+
reference_mapping = (
|
|
893
|
+
None if reference_mappings is None else reference_mappings.get(key)
|
|
894
|
+
)
|
|
895
|
+
change = StereoChange.from_endpoints(
|
|
896
|
+
old,
|
|
897
|
+
new,
|
|
898
|
+
transient,
|
|
899
|
+
reference_mapping=reference_mapping,
|
|
900
|
+
)
|
|
901
|
+
if semantics != "orbit":
|
|
902
|
+
classify_stereo_change(
|
|
903
|
+
old,
|
|
904
|
+
new,
|
|
905
|
+
transient,
|
|
906
|
+
reference_mapping=reference_mapping,
|
|
907
|
+
semantics=semantics,
|
|
908
|
+
diagnostics=diagnostics,
|
|
909
|
+
)
|
|
910
|
+
changes[key] = change
|
|
911
|
+
return changes
|
|
912
|
+
|
|
913
|
+
|
|
914
|
+
def annotate_its_stereo(
|
|
915
|
+
its: nx.Graph,
|
|
916
|
+
before: nx.Graph,
|
|
917
|
+
after: nx.Graph,
|
|
918
|
+
transition: nx.Graph | None = None,
|
|
919
|
+
*,
|
|
920
|
+
reference_mappings: (
|
|
921
|
+
Mapping[
|
|
922
|
+
str,
|
|
923
|
+
Mapping[Any, Any] | Iterable[tuple[Any, Any]],
|
|
924
|
+
]
|
|
925
|
+
| None
|
|
926
|
+
) = None,
|
|
927
|
+
) -> None:
|
|
928
|
+
changes = compare_stereo_registries(
|
|
929
|
+
before,
|
|
930
|
+
after,
|
|
931
|
+
transition,
|
|
932
|
+
reference_mappings=reference_mappings,
|
|
933
|
+
)
|
|
934
|
+
its.graph["stereo_changes"] = changes
|
|
935
|
+
its.graph["stereo_descriptors"] = {
|
|
936
|
+
"reactant": dict(stereo_registry(before)),
|
|
937
|
+
"product": dict(stereo_registry(after)),
|
|
938
|
+
}
|
|
939
|
+
if transition is not None:
|
|
940
|
+
its.graph["stereo_descriptors"]["transition"] = dict(
|
|
941
|
+
stereo_registry(transition)
|
|
942
|
+
)
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
def stereo_complete_reaction_center_nodes(its: nx.Graph) -> set[Any]:
|
|
946
|
+
changes = its.graph.get("stereo_changes", {})
|
|
947
|
+
if not changes:
|
|
948
|
+
return set()
|
|
949
|
+
atom_map_nodes = {}
|
|
950
|
+
for node, attrs in its.nodes(data=True):
|
|
951
|
+
atom_map = attrs.get("atom_map", node)
|
|
952
|
+
if isinstance(atom_map, int):
|
|
953
|
+
atom_map_nodes[atom_map] = node
|
|
954
|
+
elif isinstance(atom_map, tuple) and len(atom_map) == 2:
|
|
955
|
+
for side_map in atom_map:
|
|
956
|
+
if isinstance(side_map, int) and side_map > 0:
|
|
957
|
+
atom_map_nodes[side_map] = node
|
|
958
|
+
return {
|
|
959
|
+
atom_map_nodes[atom_map]
|
|
960
|
+
for change in changes.values()
|
|
961
|
+
if change.change != "RETAINED"
|
|
962
|
+
for atom_map in change.dependencies
|
|
963
|
+
if atom_map in atom_map_nodes
|
|
964
|
+
}
|