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,1251 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict, dataclass
|
|
4
|
+
|
|
5
|
+
import networkx as nx
|
|
6
|
+
from rdkit import Chem
|
|
7
|
+
|
|
8
|
+
from synkit.IO.chem_converter import rsmi_to_graph, graph_to_rsmi, smiles_to_graph
|
|
9
|
+
from synkit.Graph.ITS.its_decompose import get_rc, its_decompose
|
|
10
|
+
from synkit.Graph.ITS.its_construction import ITSConstruction
|
|
11
|
+
from synkit.Graph.ITS.its_builder import ITSBuilder
|
|
12
|
+
from synkit.Chem.Reaction.standardize import Standardize
|
|
13
|
+
from synkit.Graph.ITS.its_relabel import ITSRelabel
|
|
14
|
+
|
|
15
|
+
std = Standardize()
|
|
16
|
+
|
|
17
|
+
CONSTITUTIONAL_RC_NODE_ATTRS = (
|
|
18
|
+
"element",
|
|
19
|
+
"aromatic",
|
|
20
|
+
"hcount",
|
|
21
|
+
"charge",
|
|
22
|
+
"neighbors",
|
|
23
|
+
"atom_map",
|
|
24
|
+
)
|
|
25
|
+
CONSTITUTIONAL_SIDE_NODE_ATTRS = (
|
|
26
|
+
*CONSTITUTIONAL_RC_NODE_ATTRS,
|
|
27
|
+
"radical",
|
|
28
|
+
)
|
|
29
|
+
CONSTITUTIONAL_RC_EDGE_ATTRS = ("order",)
|
|
30
|
+
CONSTITUTIONAL_SIDE_EDGE_ATTRS = ("order",)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ITSExpansionError(ValueError):
|
|
34
|
+
"""Raised when partial-AAM ITS reconstruction cannot produce a safe RSMI."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class ITSExpansionResult:
|
|
39
|
+
"""Structured evidence for one successful partial-AAM expansion."""
|
|
40
|
+
|
|
41
|
+
rsmi: str
|
|
42
|
+
preferred_side: str
|
|
43
|
+
selected_side: str
|
|
44
|
+
fallback_used: bool
|
|
45
|
+
constitution_checked: bool
|
|
46
|
+
constitution_guard_passed: bool | None
|
|
47
|
+
unmapped_explicit_hydrogens_folded: bool = False
|
|
48
|
+
folded_unmapped_explicit_hydrogen_count: int = 0
|
|
49
|
+
stereochemistry_ignored_for_expansion: bool = False
|
|
50
|
+
explicit_hydrogen_serialization: bool = False
|
|
51
|
+
radical_state_preserved: bool = False
|
|
52
|
+
fallback_reason: str | None = None
|
|
53
|
+
|
|
54
|
+
def to_dict(self) -> dict:
|
|
55
|
+
"""Return JSON-serializable expansion evidence."""
|
|
56
|
+
return asdict(self)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ITSExpand:
|
|
60
|
+
"""Partially expand a reaction SMILES (RSMI) by reconstructing intermediate
|
|
61
|
+
transition states (ITS) and applying transformation rules based on the
|
|
62
|
+
reaction center graph.
|
|
63
|
+
|
|
64
|
+
This class identifies the reaction center from an RSMI, builds and
|
|
65
|
+
reconstructs the ITS graph, decomposes it back into reactants and products,
|
|
66
|
+
and standardizes atom mappings to produce a fully mapped AAM RSMI.
|
|
67
|
+
|
|
68
|
+
The optional ``preserve_older_map`` mode keeps existing atom-map numbers
|
|
69
|
+
from the input RSMI by reindexing the side graph before ITS reconstruction.
|
|
70
|
+
|
|
71
|
+
.. rubric:: Notes
|
|
72
|
+
|
|
73
|
+
``preserve_older_map=True`` is intended for the ITS expansion path only.
|
|
74
|
+
It should not be combined with ``relabel=True``, because ``ITSRelabel``
|
|
75
|
+
globally renumbers atom maps.
|
|
76
|
+
|
|
77
|
+
:cvar std: Standardize instance for reaction SMILES standardization.
|
|
78
|
+
:type std: Standardize
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(self) -> None:
|
|
82
|
+
"""Initialize ITSExpand.
|
|
83
|
+
|
|
84
|
+
No instance-specific attributes are required.
|
|
85
|
+
"""
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def _split_rsmi(rsmi: str) -> tuple[str, str]:
|
|
90
|
+
"""Split a reaction SMILES into reactant and product sides.
|
|
91
|
+
|
|
92
|
+
:param rsmi: Reaction SMILES string in ``reactant>>product`` format.
|
|
93
|
+
:type rsmi: str
|
|
94
|
+
:return: Reactant-side SMILES and product-side SMILES.
|
|
95
|
+
:rtype: tuple[str, str]
|
|
96
|
+
:raises ValueError: If the input is not a valid two-sided RSMI.
|
|
97
|
+
"""
|
|
98
|
+
try:
|
|
99
|
+
react_smi, prod_smi = rsmi.split(">>")
|
|
100
|
+
except ValueError as e:
|
|
101
|
+
raise ValueError("Input RSMI must be 'reactant>>product'") from e
|
|
102
|
+
|
|
103
|
+
if not react_smi or not prod_smi:
|
|
104
|
+
raise ValueError("Input RSMI must contain both reactant and product sides")
|
|
105
|
+
|
|
106
|
+
return react_smi, prod_smi
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
def _atom_map(data: dict) -> int:
|
|
110
|
+
"""Safely extract an atom-map number from node attributes.
|
|
111
|
+
|
|
112
|
+
:param data: Node attribute dictionary.
|
|
113
|
+
:type data: dict
|
|
114
|
+
:return: Atom-map number. Returns ``0`` if absent or falsy.
|
|
115
|
+
:rtype: int
|
|
116
|
+
"""
|
|
117
|
+
return int(data.get("atom_map", 0) or 0)
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def _nonzero_atom_maps(graph) -> list[int]:
|
|
121
|
+
"""Collect all nonzero atom-map numbers from a graph.
|
|
122
|
+
|
|
123
|
+
:param graph: Molecular graph.
|
|
124
|
+
:type graph: networkx.Graph
|
|
125
|
+
:return: List of nonzero atom-map numbers.
|
|
126
|
+
:rtype: list[int]
|
|
127
|
+
"""
|
|
128
|
+
return [
|
|
129
|
+
ITSExpand._atom_map(data)
|
|
130
|
+
for _, data in graph.nodes(data=True)
|
|
131
|
+
if ITSExpand._atom_map(data) != 0
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
@staticmethod
|
|
135
|
+
def _validate_unique_atom_maps(atom_maps: list[int]) -> None:
|
|
136
|
+
"""Validate that all nonzero atom-map numbers are unique.
|
|
137
|
+
|
|
138
|
+
:param atom_maps: Nonzero atom-map numbers.
|
|
139
|
+
:type atom_maps: list[int]
|
|
140
|
+
:raises ValueError: If duplicate nonzero atom-map numbers are found.
|
|
141
|
+
"""
|
|
142
|
+
if len(atom_maps) != len(set(atom_maps)):
|
|
143
|
+
raise ValueError(
|
|
144
|
+
"Duplicate nonzero atom_map values found in side graph. "
|
|
145
|
+
"Cannot safely reindex graph by atom_map."
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
@staticmethod
|
|
149
|
+
def _validate_atom_maps_within_range(
|
|
150
|
+
atom_maps: list[int],
|
|
151
|
+
n_nodes: int,
|
|
152
|
+
) -> None:
|
|
153
|
+
"""Validate that atom-map numbers can be used as contiguous node IDs.
|
|
154
|
+
|
|
155
|
+
In the side graph, we want final node IDs to remain exactly ``1..N``.
|
|
156
|
+
Therefore, a mapped atom can only be moved to its atom-map number if
|
|
157
|
+
that number is within ``1..N``.
|
|
158
|
+
|
|
159
|
+
:param atom_maps: Nonzero atom-map numbers.
|
|
160
|
+
:type atom_maps: list[int]
|
|
161
|
+
:param n_nodes: Number of nodes in the side graph.
|
|
162
|
+
:type n_nodes: int
|
|
163
|
+
:raises ValueError: If any atom-map number is outside ``1..N``.
|
|
164
|
+
"""
|
|
165
|
+
bad_targets = [
|
|
166
|
+
atom_map for atom_map in atom_maps if atom_map < 1 or atom_map > n_nodes
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
if bad_targets:
|
|
170
|
+
raise ValueError(
|
|
171
|
+
"Cannot keep side graph node ids contiguous from 1..N while "
|
|
172
|
+
f"also using atom_map as node id. The following atom maps are "
|
|
173
|
+
f"outside 1..{n_nodes}: {bad_targets}"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
@staticmethod
|
|
177
|
+
def _validate_positive_atom_maps(atom_maps: list[int]) -> None:
|
|
178
|
+
"""Validate that atom-map numbers are valid positive node IDs."""
|
|
179
|
+
bad_targets = [atom_map for atom_map in atom_maps if atom_map < 1]
|
|
180
|
+
|
|
181
|
+
if bad_targets:
|
|
182
|
+
raise ValueError(
|
|
183
|
+
"Cannot use non-positive atom_map values as node ids. "
|
|
184
|
+
f"Invalid atom maps: {bad_targets}"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
@staticmethod
|
|
188
|
+
def _assign_mapped_nodes(graph) -> tuple[dict, set[int]]:
|
|
189
|
+
"""Assign mapped atoms to node IDs equal to their atom-map numbers.
|
|
190
|
+
|
|
191
|
+
For example, if a node has ``atom_map=20``, the returned mapping assigns
|
|
192
|
+
that old node to new node ID ``20``.
|
|
193
|
+
|
|
194
|
+
:param graph: Molecular side graph.
|
|
195
|
+
:type graph: networkx.Graph
|
|
196
|
+
:return: A partial old-node to new-node mapping and the used node IDs.
|
|
197
|
+
:rtype: tuple[dict, set[int]]
|
|
198
|
+
"""
|
|
199
|
+
mapping = {}
|
|
200
|
+
used_ids = set()
|
|
201
|
+
|
|
202
|
+
for node, data in graph.nodes(data=True):
|
|
203
|
+
atom_map = ITSExpand._atom_map(data)
|
|
204
|
+
|
|
205
|
+
if atom_map == 0:
|
|
206
|
+
continue
|
|
207
|
+
|
|
208
|
+
mapping[node] = atom_map
|
|
209
|
+
used_ids.add(atom_map)
|
|
210
|
+
|
|
211
|
+
return mapping, used_ids
|
|
212
|
+
|
|
213
|
+
@staticmethod
|
|
214
|
+
def _assign_unmapped_nodes(
|
|
215
|
+
graph,
|
|
216
|
+
mapping: dict,
|
|
217
|
+
used_ids: set[int],
|
|
218
|
+
) -> dict:
|
|
219
|
+
"""Assign unmapped atoms while preserving contiguous node IDs.
|
|
220
|
+
|
|
221
|
+
Unmapped atoms keep their original node ID when possible. If an
|
|
222
|
+
unmapped atom's node ID conflicts with a mapped atom's target ID, it is
|
|
223
|
+
moved into one of the remaining free IDs inside ``1..N``.
|
|
224
|
+
|
|
225
|
+
:param graph: Molecular side graph.
|
|
226
|
+
:type graph: networkx.Graph
|
|
227
|
+
:param mapping: Existing mapping from mapped atoms.
|
|
228
|
+
:type mapping: dict
|
|
229
|
+
:param used_ids: Node IDs already occupied by mapped atoms.
|
|
230
|
+
:type used_ids: set[int]
|
|
231
|
+
:return: Complete old-node to new-node mapping.
|
|
232
|
+
:rtype: dict
|
|
233
|
+
"""
|
|
234
|
+
n_nodes = graph.number_of_nodes()
|
|
235
|
+
free_ids = set(range(1, n_nodes + 1)) - used_ids
|
|
236
|
+
pending_unmapped = []
|
|
237
|
+
|
|
238
|
+
for node, data in graph.nodes(data=True):
|
|
239
|
+
atom_map = ITSExpand._atom_map(data)
|
|
240
|
+
|
|
241
|
+
if atom_map != 0:
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
if isinstance(node, int) and node in free_ids:
|
|
245
|
+
mapping[node] = node
|
|
246
|
+
free_ids.remove(node)
|
|
247
|
+
else:
|
|
248
|
+
pending_unmapped.append(node)
|
|
249
|
+
|
|
250
|
+
for old_node, new_node in zip(pending_unmapped, sorted(free_ids)):
|
|
251
|
+
mapping[old_node] = new_node
|
|
252
|
+
|
|
253
|
+
return mapping
|
|
254
|
+
|
|
255
|
+
@staticmethod
|
|
256
|
+
def _assign_unmapped_nodes_sparse(
|
|
257
|
+
graph,
|
|
258
|
+
mapping: dict,
|
|
259
|
+
used_ids: set[int],
|
|
260
|
+
) -> dict:
|
|
261
|
+
"""Assign unmapped atoms without changing preserved sparse map IDs."""
|
|
262
|
+
next_candidate = 1
|
|
263
|
+
|
|
264
|
+
for node, data in graph.nodes(data=True):
|
|
265
|
+
if ITSExpand._atom_map(data) != 0:
|
|
266
|
+
continue
|
|
267
|
+
|
|
268
|
+
if isinstance(node, int) and node > 0 and node not in used_ids:
|
|
269
|
+
mapping[node] = node
|
|
270
|
+
used_ids.add(node)
|
|
271
|
+
next_candidate = max(next_candidate, node + 1)
|
|
272
|
+
continue
|
|
273
|
+
|
|
274
|
+
while next_candidate in used_ids:
|
|
275
|
+
next_candidate += 1
|
|
276
|
+
mapping[node] = next_candidate
|
|
277
|
+
used_ids.add(next_candidate)
|
|
278
|
+
next_candidate += 1
|
|
279
|
+
|
|
280
|
+
return mapping
|
|
281
|
+
|
|
282
|
+
@staticmethod
|
|
283
|
+
def _validate_contiguous_mapping(mapping: dict, n_nodes: int) -> None:
|
|
284
|
+
"""Validate that a mapping produces exactly node IDs ``1..N``.
|
|
285
|
+
|
|
286
|
+
:param mapping: Old-node to new-node mapping.
|
|
287
|
+
:type mapping: dict
|
|
288
|
+
:param n_nodes: Number of nodes in the graph.
|
|
289
|
+
:type n_nodes: int
|
|
290
|
+
:raises RuntimeError: If the mapped node IDs are not exactly ``1..N``.
|
|
291
|
+
"""
|
|
292
|
+
expected_ids = set(range(1, n_nodes + 1))
|
|
293
|
+
actual_ids = set(mapping.values())
|
|
294
|
+
|
|
295
|
+
if actual_ids != expected_ids:
|
|
296
|
+
missing = sorted(expected_ids - actual_ids)
|
|
297
|
+
extra = sorted(actual_ids - expected_ids)
|
|
298
|
+
raise RuntimeError(
|
|
299
|
+
f"Reindexing failed. Missing node ids: {missing}; "
|
|
300
|
+
f"extra node ids: {extra}"
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
@staticmethod
|
|
304
|
+
def _validate_complete_unique_mapping(mapping: dict, n_nodes: int) -> None:
|
|
305
|
+
"""Validate that every source node maps to one unique target node."""
|
|
306
|
+
if len(mapping) != n_nodes:
|
|
307
|
+
raise RuntimeError(
|
|
308
|
+
f"Reindexing failed. Expected {n_nodes} mapped nodes; "
|
|
309
|
+
f"got {len(mapping)}."
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
if len(set(mapping.values())) != n_nodes:
|
|
313
|
+
raise RuntimeError(
|
|
314
|
+
"Reindexing failed. Multiple source nodes map to the same "
|
|
315
|
+
"target node id."
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
@staticmethod
|
|
319
|
+
def _build_side_graph_reindex_mapping(
|
|
320
|
+
graph,
|
|
321
|
+
contiguous: bool = True,
|
|
322
|
+
) -> dict:
|
|
323
|
+
"""Build an old-node to new-node mapping for a side graph.
|
|
324
|
+
|
|
325
|
+
The mapping satisfies two conditions:
|
|
326
|
+
|
|
327
|
+
1. Every atom with ``atom_map != 0`` is assigned to node ID
|
|
328
|
+
``atom_map``.
|
|
329
|
+
2. The final node IDs are exactly contiguous from ``1..N`` when
|
|
330
|
+
``contiguous=True``. With ``contiguous=False``, existing positive,
|
|
331
|
+
sparse atom-map IDs are retained and unmapped atoms receive unused
|
|
332
|
+
positive IDs.
|
|
333
|
+
|
|
334
|
+
:param graph: Molecular side graph.
|
|
335
|
+
:type graph: networkx.Graph
|
|
336
|
+
:return: Old-node to new-node mapping.
|
|
337
|
+
:rtype: dict
|
|
338
|
+
:raises ValueError: If atom-map values are duplicated or incompatible
|
|
339
|
+
with contiguous node IDs.
|
|
340
|
+
"""
|
|
341
|
+
n_nodes = graph.number_of_nodes()
|
|
342
|
+
atom_maps = ITSExpand._nonzero_atom_maps(graph)
|
|
343
|
+
|
|
344
|
+
ITSExpand._validate_unique_atom_maps(atom_maps)
|
|
345
|
+
if contiguous:
|
|
346
|
+
ITSExpand._validate_atom_maps_within_range(atom_maps, n_nodes)
|
|
347
|
+
else:
|
|
348
|
+
ITSExpand._validate_positive_atom_maps(atom_maps)
|
|
349
|
+
|
|
350
|
+
mapping, used_ids = ITSExpand._assign_mapped_nodes(graph)
|
|
351
|
+
if contiguous:
|
|
352
|
+
mapping = ITSExpand._assign_unmapped_nodes(graph, mapping, used_ids)
|
|
353
|
+
ITSExpand._validate_contiguous_mapping(mapping, n_nodes)
|
|
354
|
+
else:
|
|
355
|
+
mapping = ITSExpand._assign_unmapped_nodes_sparse(
|
|
356
|
+
graph,
|
|
357
|
+
mapping,
|
|
358
|
+
used_ids,
|
|
359
|
+
)
|
|
360
|
+
ITSExpand._validate_complete_unique_mapping(mapping, n_nodes)
|
|
361
|
+
|
|
362
|
+
return mapping
|
|
363
|
+
|
|
364
|
+
@staticmethod
|
|
365
|
+
def _copy_nodes_with_mapping(graph, new_graph, mapping: dict) -> None:
|
|
366
|
+
"""Copy graph nodes into a new graph using a node mapping.
|
|
367
|
+
|
|
368
|
+
:param graph: Source graph.
|
|
369
|
+
:type graph: networkx.Graph
|
|
370
|
+
:param new_graph: Destination graph.
|
|
371
|
+
:type new_graph: networkx.Graph
|
|
372
|
+
:param mapping: Old-node to new-node mapping.
|
|
373
|
+
:type mapping: dict
|
|
374
|
+
"""
|
|
375
|
+
for old_node, new_node in mapping.items():
|
|
376
|
+
attrs = dict(graph.nodes[old_node])
|
|
377
|
+
new_graph.add_node(new_node, **attrs)
|
|
378
|
+
|
|
379
|
+
@staticmethod
|
|
380
|
+
def _copy_edges_with_mapping(graph, new_graph, mapping: dict) -> None:
|
|
381
|
+
"""Copy graph edges into a new graph using a node mapping.
|
|
382
|
+
|
|
383
|
+
Supports both simple graphs and multigraphs.
|
|
384
|
+
|
|
385
|
+
:param graph: Source graph.
|
|
386
|
+
:type graph: networkx.Graph
|
|
387
|
+
:param new_graph: Destination graph.
|
|
388
|
+
:type new_graph: networkx.Graph
|
|
389
|
+
:param mapping: Old-node to new-node mapping.
|
|
390
|
+
:type mapping: dict
|
|
391
|
+
"""
|
|
392
|
+
if graph.is_multigraph():
|
|
393
|
+
for u, v, key, attrs in graph.edges(keys=True, data=True):
|
|
394
|
+
new_graph.add_edge(
|
|
395
|
+
mapping[u],
|
|
396
|
+
mapping[v],
|
|
397
|
+
key=key,
|
|
398
|
+
**dict(attrs),
|
|
399
|
+
)
|
|
400
|
+
return
|
|
401
|
+
|
|
402
|
+
for u, v, attrs in graph.edges(data=True):
|
|
403
|
+
new_graph.add_edge(
|
|
404
|
+
mapping[u],
|
|
405
|
+
mapping[v],
|
|
406
|
+
**dict(attrs),
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
@staticmethod
|
|
410
|
+
def _rebuild_graph_with_mapping(graph, mapping: dict):
|
|
411
|
+
"""Rebuild a graph with remapped node IDs.
|
|
412
|
+
|
|
413
|
+
This avoids in-place relabeling collisions, for example when node ``27``
|
|
414
|
+
must become node ``20`` while old node ``20`` must move elsewhere.
|
|
415
|
+
|
|
416
|
+
:param graph: Source graph.
|
|
417
|
+
:type graph: networkx.Graph
|
|
418
|
+
:param mapping: Old-node to new-node mapping.
|
|
419
|
+
:type mapping: dict
|
|
420
|
+
:return: Rebuilt graph with remapped node IDs.
|
|
421
|
+
:rtype: networkx.Graph
|
|
422
|
+
"""
|
|
423
|
+
new_graph = graph.__class__()
|
|
424
|
+
new_graph.graph.update(graph.graph)
|
|
425
|
+
|
|
426
|
+
ITSExpand._copy_nodes_with_mapping(graph, new_graph, mapping)
|
|
427
|
+
ITSExpand._copy_edges_with_mapping(graph, new_graph, mapping)
|
|
428
|
+
|
|
429
|
+
return new_graph
|
|
430
|
+
|
|
431
|
+
@staticmethod
|
|
432
|
+
def reindex_side_graph_by_atom_map(graph, contiguous: bool = True):
|
|
433
|
+
"""Reindex a side graph so mapped atoms use ``atom_map`` as node ID.
|
|
434
|
+
|
|
435
|
+
By default, the returned graph keeps node IDs contiguous from ``1..N``.
|
|
436
|
+
Set ``contiguous=False`` to preserve valid sparse map IDs such as
|
|
437
|
+
``:10`` and ``:21``; this is needed by the EF-SMIRKS expansion path.
|
|
438
|
+
|
|
439
|
+
This is useful because the reaction-center graph produced by
|
|
440
|
+
``ITSConstruction().ITSGraph(...)`` uses atom-map numbers as node IDs,
|
|
441
|
+
whereas the side graph produced by ``smiles_to_graph(...)`` may use
|
|
442
|
+
RDKit-style atom indices as node IDs.
|
|
443
|
+
|
|
444
|
+
.. rubric:: Example
|
|
445
|
+
|
|
446
|
+
Before reindexing:
|
|
447
|
+
|
|
448
|
+
.. code-block:: text
|
|
449
|
+
|
|
450
|
+
Node 20: atom_map = 0
|
|
451
|
+
Node 27: atom_map = 20
|
|
452
|
+
|
|
453
|
+
After reindexing:
|
|
454
|
+
|
|
455
|
+
.. code-block:: text
|
|
456
|
+
|
|
457
|
+
Node 20: atom_map = 20
|
|
458
|
+
Node 27: atom_map = 0
|
|
459
|
+
|
|
460
|
+
or another unmapped atom may be moved into the freed node position.
|
|
461
|
+
|
|
462
|
+
:param graph: Molecular side graph.
|
|
463
|
+
:type graph: networkx.Graph
|
|
464
|
+
:param contiguous: Whether the returned node IDs must be ``1..N``.
|
|
465
|
+
:type contiguous: bool
|
|
466
|
+
:return: Reindexed side graph.
|
|
467
|
+
:rtype: networkx.Graph
|
|
468
|
+
:raises ValueError: If atom-map numbers cannot be safely used as node
|
|
469
|
+
IDs while preserving ``1..N`` indexing.
|
|
470
|
+
"""
|
|
471
|
+
mapping = ITSExpand._build_side_graph_reindex_mapping(graph, contiguous)
|
|
472
|
+
return ITSExpand._rebuild_graph_with_mapping(graph, mapping)
|
|
473
|
+
|
|
474
|
+
@staticmethod
|
|
475
|
+
def _canonical_unmapped_constitution(smiles: str) -> str:
|
|
476
|
+
"""Return an atom-map- and stereo-independent constitutional key.
|
|
477
|
+
|
|
478
|
+
Isotopes, formal charges, explicit hydrogens, bond orders, and molecular
|
|
479
|
+
fragments remain part of the key. Only atom-map labels and
|
|
480
|
+
stereochemical annotations are removed.
|
|
481
|
+
"""
|
|
482
|
+
mol = Chem.MolFromSmiles(smiles)
|
|
483
|
+
if mol is None:
|
|
484
|
+
raise ITSExpansionError(
|
|
485
|
+
f"Cannot parse endpoint while checking constitution: {smiles!r}"
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
for atom in mol.GetAtoms():
|
|
489
|
+
atom.SetAtomMapNum(0)
|
|
490
|
+
Chem.RemoveStereochemistry(mol)
|
|
491
|
+
return Chem.MolToSmiles(mol, canonical=True, isomericSmiles=True)
|
|
492
|
+
|
|
493
|
+
@classmethod
|
|
494
|
+
def endpoint_constitutions_match(
|
|
495
|
+
cls, source_rsmi: str, candidate_rsmi: str
|
|
496
|
+
) -> bool:
|
|
497
|
+
"""Check that reactant and product constitutions are independently equal.
|
|
498
|
+
|
|
499
|
+
This is a fail-closed guard for partial-AAM completion, not an atom-map
|
|
500
|
+
accuracy metric. It prevents a fallback reconstruction from silently
|
|
501
|
+
deleting fragments or changing either reaction endpoint.
|
|
502
|
+
"""
|
|
503
|
+
source_react, source_prod = cls._split_rsmi(source_rsmi)
|
|
504
|
+
candidate_react, candidate_prod = cls._split_rsmi(candidate_rsmi)
|
|
505
|
+
return cls._canonical_unmapped_constitution(
|
|
506
|
+
source_react
|
|
507
|
+
) == cls._canonical_unmapped_constitution(
|
|
508
|
+
candidate_react
|
|
509
|
+
) and cls._canonical_unmapped_constitution(
|
|
510
|
+
source_prod
|
|
511
|
+
) == cls._canonical_unmapped_constitution(
|
|
512
|
+
candidate_prod
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
@staticmethod
|
|
516
|
+
def _fold_unmapped_explicit_hydrogens_in_side_with_count(
|
|
517
|
+
smiles: str,
|
|
518
|
+
) -> tuple[str, int]:
|
|
519
|
+
"""Fold removable unmapped H atoms into their heavy-atom H count.
|
|
520
|
+
|
|
521
|
+
Mapped hydrogen atoms, isotopic hydrogens, and non-removable standalone
|
|
522
|
+
hydrogen fragments remain explicit. This prevents the partial-AAM
|
|
523
|
+
reaction-center path from deleting an unmapped explicit H without
|
|
524
|
+
transferring its hydrogen count to the attached atom.
|
|
525
|
+
"""
|
|
526
|
+
parser = Chem.SmilesParserParams()
|
|
527
|
+
parser.removeHs = False
|
|
528
|
+
mol = Chem.MolFromSmiles(smiles, parser)
|
|
529
|
+
if mol is None:
|
|
530
|
+
raise ITSExpansionError(
|
|
531
|
+
f"Cannot parse endpoint while folding unmapped H: {smiles!r}"
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
explicit_unmapped_before = sum(
|
|
535
|
+
atom.GetAtomicNum() == 1 and atom.GetAtomMapNum() == 0
|
|
536
|
+
for atom in mol.GetAtoms()
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
parameters = Chem.RemoveHsParameters()
|
|
540
|
+
parameters.removeMapped = False
|
|
541
|
+
parameters.updateExplicitCount = True
|
|
542
|
+
try:
|
|
543
|
+
folded = Chem.RemoveHs(mol, parameters, sanitize=True)
|
|
544
|
+
except Exception as exc:
|
|
545
|
+
raise ITSExpansionError(
|
|
546
|
+
f"Cannot safely fold unmapped explicit H: {smiles!r}; {exc}"
|
|
547
|
+
) from exc
|
|
548
|
+
explicit_unmapped_after = sum(
|
|
549
|
+
atom.GetAtomicNum() == 1 and atom.GetAtomMapNum() == 0
|
|
550
|
+
for atom in folded.GetAtoms()
|
|
551
|
+
)
|
|
552
|
+
return Chem.MolToSmiles(
|
|
553
|
+
folded,
|
|
554
|
+
canonical=False,
|
|
555
|
+
isomericSmiles=True,
|
|
556
|
+
), int(explicit_unmapped_before - explicit_unmapped_after)
|
|
557
|
+
|
|
558
|
+
@classmethod
|
|
559
|
+
def _fold_unmapped_explicit_hydrogens_in_side(cls, smiles: str) -> str:
|
|
560
|
+
"""Fold removable unmapped H atoms and return the normalized side."""
|
|
561
|
+
folded, _count = cls._fold_unmapped_explicit_hydrogens_in_side_with_count(
|
|
562
|
+
smiles
|
|
563
|
+
)
|
|
564
|
+
return folded
|
|
565
|
+
|
|
566
|
+
@classmethod
|
|
567
|
+
def fold_unmapped_explicit_hydrogens(cls, rsmi: str) -> str:
|
|
568
|
+
"""Normalize removable unmapped H on both reaction endpoints."""
|
|
569
|
+
reactant, product = cls._split_rsmi(rsmi)
|
|
570
|
+
return (
|
|
571
|
+
f"{cls._fold_unmapped_explicit_hydrogens_in_side(reactant)}>>"
|
|
572
|
+
f"{cls._fold_unmapped_explicit_hydrogens_in_side(product)}"
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
@classmethod
|
|
576
|
+
def _fold_unmapped_explicit_hydrogens_with_count(cls, rsmi: str) -> tuple[str, int]:
|
|
577
|
+
"""Normalize both endpoints and count the explicit H atoms folded."""
|
|
578
|
+
reactant, product = cls._split_rsmi(rsmi)
|
|
579
|
+
folded_reactant, reactant_count = (
|
|
580
|
+
cls._fold_unmapped_explicit_hydrogens_in_side_with_count(reactant)
|
|
581
|
+
)
|
|
582
|
+
folded_product, product_count = (
|
|
583
|
+
cls._fold_unmapped_explicit_hydrogens_in_side_with_count(product)
|
|
584
|
+
)
|
|
585
|
+
return (
|
|
586
|
+
f"{folded_reactant}>>{folded_product}",
|
|
587
|
+
reactant_count + product_count,
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
@staticmethod
|
|
591
|
+
def _side_has_stereochemistry(smiles: str) -> bool:
|
|
592
|
+
"""Return whether an endpoint contains atom or bond stereochemistry."""
|
|
593
|
+
parser = Chem.SmilesParserParams()
|
|
594
|
+
parser.removeHs = False
|
|
595
|
+
mol = Chem.MolFromSmiles(smiles, parser)
|
|
596
|
+
if mol is None:
|
|
597
|
+
raise ITSExpansionError(
|
|
598
|
+
f"Cannot parse endpoint while inspecting stereochemistry: {smiles!r}"
|
|
599
|
+
)
|
|
600
|
+
return any(
|
|
601
|
+
atom.GetChiralTag() != Chem.ChiralType.CHI_UNSPECIFIED
|
|
602
|
+
for atom in mol.GetAtoms()
|
|
603
|
+
) or any(
|
|
604
|
+
bond.GetStereo() != Chem.BondStereo.STEREONONE
|
|
605
|
+
or bond.GetBondDir() != Chem.BondDir.NONE
|
|
606
|
+
for bond in mol.GetBonds()
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
@staticmethod
|
|
610
|
+
def _remove_stereochemistry_in_side(smiles: str) -> str:
|
|
611
|
+
"""Remove atom/bond stereo while retaining explicit mapped H atoms."""
|
|
612
|
+
parser = Chem.SmilesParserParams()
|
|
613
|
+
parser.removeHs = False
|
|
614
|
+
mol = Chem.MolFromSmiles(smiles, parser)
|
|
615
|
+
if mol is None:
|
|
616
|
+
raise ITSExpansionError(
|
|
617
|
+
f"Cannot parse endpoint while removing stereochemistry: {smiles!r}"
|
|
618
|
+
)
|
|
619
|
+
Chem.RemoveStereochemistry(mol)
|
|
620
|
+
return Chem.MolToSmiles(mol, canonical=False, isomericSmiles=False)
|
|
621
|
+
|
|
622
|
+
@classmethod
|
|
623
|
+
def remove_stereochemistry(cls, rsmi: str) -> str:
|
|
624
|
+
"""Remove endpoint stereo without folding explicit mapped H atoms."""
|
|
625
|
+
reactant, product = cls._split_rsmi(rsmi)
|
|
626
|
+
return (
|
|
627
|
+
f"{cls._remove_stereochemistry_in_side(reactant)}>>"
|
|
628
|
+
f"{cls._remove_stereochemistry_in_side(product)}"
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
@classmethod
|
|
632
|
+
def _rsmi_has_stereochemistry(cls, rsmi: str) -> bool:
|
|
633
|
+
"""Return whether either reaction endpoint carries stereo metadata."""
|
|
634
|
+
if not any(marker in rsmi for marker in ("@", "/", "\\")):
|
|
635
|
+
return False
|
|
636
|
+
return any(
|
|
637
|
+
cls._side_has_stereochemistry(side) for side in cls._split_rsmi(rsmi)
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
@staticmethod
|
|
641
|
+
def _build_expansion_reaction_center(
|
|
642
|
+
rsmi: str,
|
|
643
|
+
*,
|
|
644
|
+
constitutional_only: bool,
|
|
645
|
+
) -> nx.Graph:
|
|
646
|
+
"""Convert partial endpoints and construct their ITS reaction centre."""
|
|
647
|
+
react_graph, prod_graph = rsmi_to_graph(
|
|
648
|
+
rsmi,
|
|
649
|
+
node_attrs=(CONSTITUTIONAL_RC_NODE_ATTRS if constitutional_only else None),
|
|
650
|
+
edge_attrs=(CONSTITUTIONAL_RC_EDGE_ATTRS if constitutional_only else None),
|
|
651
|
+
include_stereo_descriptors=not constitutional_only,
|
|
652
|
+
)
|
|
653
|
+
if react_graph is None or prod_graph is None:
|
|
654
|
+
raise ITSExpansionError("reaction graph conversion returned None")
|
|
655
|
+
rc_graph = ITSConstruction().ITSGraph(react_graph, prod_graph)
|
|
656
|
+
if rc_graph is None:
|
|
657
|
+
raise ITSExpansionError("ITS reaction-center construction returned None")
|
|
658
|
+
return rc_graph
|
|
659
|
+
|
|
660
|
+
@staticmethod
|
|
661
|
+
def _convert_expansion_side(
|
|
662
|
+
smiles: str,
|
|
663
|
+
*,
|
|
664
|
+
constitutional_only: bool,
|
|
665
|
+
preserve_radical_state: bool,
|
|
666
|
+
) -> nx.Graph:
|
|
667
|
+
"""Convert one complete endpoint with only expansion-required fields."""
|
|
668
|
+
graph = smiles_to_graph(
|
|
669
|
+
smiles,
|
|
670
|
+
sanitize=True,
|
|
671
|
+
drop_non_aam=False,
|
|
672
|
+
use_index_as_atom_map=False,
|
|
673
|
+
node_attrs=(
|
|
674
|
+
(
|
|
675
|
+
CONSTITUTIONAL_SIDE_NODE_ATTRS
|
|
676
|
+
if preserve_radical_state
|
|
677
|
+
else CONSTITUTIONAL_RC_NODE_ATTRS
|
|
678
|
+
)
|
|
679
|
+
if constitutional_only
|
|
680
|
+
else None
|
|
681
|
+
),
|
|
682
|
+
edge_attrs=(
|
|
683
|
+
CONSTITUTIONAL_SIDE_EDGE_ATTRS if constitutional_only else None
|
|
684
|
+
),
|
|
685
|
+
include_stereo_descriptors=not constitutional_only,
|
|
686
|
+
)
|
|
687
|
+
if graph is None:
|
|
688
|
+
raise ITSExpansionError("full endpoint conversion returned None")
|
|
689
|
+
return graph
|
|
690
|
+
|
|
691
|
+
@classmethod
|
|
692
|
+
def _expansion_side_graphs(
|
|
693
|
+
cls,
|
|
694
|
+
react_smi: str,
|
|
695
|
+
prod_smi: str,
|
|
696
|
+
*,
|
|
697
|
+
use_G: bool,
|
|
698
|
+
preserve_radical_state: bool,
|
|
699
|
+
constitutional_only: bool,
|
|
700
|
+
) -> tuple[nx.Graph, nx.Graph | None, nx.Graph | None]:
|
|
701
|
+
"""Return selected side and optional radical-reference endpoints."""
|
|
702
|
+
selected_smiles = react_smi if use_G else prod_smi
|
|
703
|
+
side_graph = cls._convert_expansion_side(
|
|
704
|
+
selected_smiles,
|
|
705
|
+
constitutional_only=constitutional_only,
|
|
706
|
+
preserve_radical_state=preserve_radical_state,
|
|
707
|
+
)
|
|
708
|
+
full_react_graph = side_graph if use_G else None
|
|
709
|
+
full_prod_graph = side_graph if not use_G else None
|
|
710
|
+
if preserve_radical_state:
|
|
711
|
+
other_graph = cls._convert_expansion_side(
|
|
712
|
+
prod_smi if use_G else react_smi,
|
|
713
|
+
constitutional_only=constitutional_only,
|
|
714
|
+
preserve_radical_state=True,
|
|
715
|
+
)
|
|
716
|
+
if use_G:
|
|
717
|
+
full_prod_graph = other_graph
|
|
718
|
+
else:
|
|
719
|
+
full_react_graph = other_graph
|
|
720
|
+
return side_graph, full_react_graph, full_prod_graph
|
|
721
|
+
|
|
722
|
+
@staticmethod
|
|
723
|
+
def _reacting_hydrogen_output_maps(
|
|
724
|
+
rc_graph: nx.Graph,
|
|
725
|
+
side_graph: nx.Graph,
|
|
726
|
+
*,
|
|
727
|
+
explicit_hydrogen: bool,
|
|
728
|
+
) -> list[int]:
|
|
729
|
+
"""Translate reacting H labels to post-builder atom-map identifiers."""
|
|
730
|
+
if explicit_hydrogen:
|
|
731
|
+
return []
|
|
732
|
+
reacting_hydrogen_maps = {
|
|
733
|
+
data["atom_map"]
|
|
734
|
+
for _, data in get_rc(rc_graph).nodes(data=True)
|
|
735
|
+
if data.get("element") == "H"
|
|
736
|
+
}
|
|
737
|
+
return [
|
|
738
|
+
node
|
|
739
|
+
for node, data in side_graph.nodes(data=True)
|
|
740
|
+
if data.get("atom_map") in reacting_hydrogen_maps
|
|
741
|
+
]
|
|
742
|
+
|
|
743
|
+
@staticmethod
|
|
744
|
+
def _serialize_expansion(
|
|
745
|
+
react_graph: nx.Graph,
|
|
746
|
+
prod_graph: nx.Graph,
|
|
747
|
+
its_graph: nx.Graph,
|
|
748
|
+
*,
|
|
749
|
+
explicit_hydrogen: bool,
|
|
750
|
+
preserve_hydrogen_maps: list[int],
|
|
751
|
+
standardize_output: bool,
|
|
752
|
+
) -> str:
|
|
753
|
+
"""Serialize a reconstructed ITS and optionally standardize its RSMI."""
|
|
754
|
+
expanded_rsmi = graph_to_rsmi(
|
|
755
|
+
react_graph,
|
|
756
|
+
prod_graph,
|
|
757
|
+
its_graph,
|
|
758
|
+
sanitize=True,
|
|
759
|
+
explicit_hydrogen=explicit_hydrogen,
|
|
760
|
+
preserve_hydrogen_maps=preserve_hydrogen_maps,
|
|
761
|
+
)
|
|
762
|
+
if not isinstance(expanded_rsmi, str) or not expanded_rsmi:
|
|
763
|
+
raise ITSExpansionError("graph-to-RSMI serialization returned no result")
|
|
764
|
+
if not standardize_output:
|
|
765
|
+
return expanded_rsmi
|
|
766
|
+
standardized = std.fit(
|
|
767
|
+
expanded_rsmi,
|
|
768
|
+
remove_aam=False,
|
|
769
|
+
remove_invalid=False,
|
|
770
|
+
)
|
|
771
|
+
if not standardized:
|
|
772
|
+
raise ITSExpansionError(
|
|
773
|
+
"expanded RSMI contains an invalid fragment; no fragments were dropped"
|
|
774
|
+
)
|
|
775
|
+
return standardized
|
|
776
|
+
|
|
777
|
+
@classmethod
|
|
778
|
+
def _expand_aam_once(
|
|
779
|
+
cls,
|
|
780
|
+
rsmi: str,
|
|
781
|
+
*,
|
|
782
|
+
use_G: bool,
|
|
783
|
+
preserve_older_map: bool,
|
|
784
|
+
explicit_hydrogen: bool,
|
|
785
|
+
preserve_radical_state: bool,
|
|
786
|
+
constitutional_only: bool,
|
|
787
|
+
standardize_output: bool,
|
|
788
|
+
) -> str:
|
|
789
|
+
"""Run one side-specific ITS expansion and fail on invalid output."""
|
|
790
|
+
side_name = "reactant" if use_G else "product"
|
|
791
|
+
try:
|
|
792
|
+
react_smi, prod_smi = cls._split_rsmi(rsmi)
|
|
793
|
+
rc_graph = cls._build_expansion_reaction_center(
|
|
794
|
+
rsmi,
|
|
795
|
+
constitutional_only=constitutional_only,
|
|
796
|
+
)
|
|
797
|
+
side_graph, full_react_graph, full_prod_graph = cls._expansion_side_graphs(
|
|
798
|
+
react_smi,
|
|
799
|
+
prod_smi,
|
|
800
|
+
use_G=use_G,
|
|
801
|
+
preserve_radical_state=preserve_radical_state,
|
|
802
|
+
constitutional_only=constitutional_only,
|
|
803
|
+
)
|
|
804
|
+
|
|
805
|
+
if preserve_older_map:
|
|
806
|
+
side_graph = cls.reindex_side_graph_by_atom_map(
|
|
807
|
+
side_graph,
|
|
808
|
+
contiguous=False,
|
|
809
|
+
)
|
|
810
|
+
|
|
811
|
+
preserve_hydrogen_maps = cls._reacting_hydrogen_output_maps(
|
|
812
|
+
rc_graph,
|
|
813
|
+
side_graph,
|
|
814
|
+
explicit_hydrogen=explicit_hydrogen,
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
its_graph = ITSBuilder().ITSGraph(side_graph, rc_graph)
|
|
818
|
+
if its_graph is None:
|
|
819
|
+
raise ITSExpansionError("ITS reconstruction returned None")
|
|
820
|
+
|
|
821
|
+
new_react, new_prod = its_decompose(its_graph)
|
|
822
|
+
if preserve_radical_state:
|
|
823
|
+
assert full_react_graph is not None
|
|
824
|
+
assert full_prod_graph is not None
|
|
825
|
+
cls._transfer_endpoint_radicals(
|
|
826
|
+
full_react_graph,
|
|
827
|
+
new_react,
|
|
828
|
+
preserve_atom_maps=preserve_older_map,
|
|
829
|
+
)
|
|
830
|
+
cls._transfer_endpoint_radicals(
|
|
831
|
+
full_prod_graph,
|
|
832
|
+
new_prod,
|
|
833
|
+
preserve_atom_maps=preserve_older_map,
|
|
834
|
+
)
|
|
835
|
+
return cls._serialize_expansion(
|
|
836
|
+
new_react,
|
|
837
|
+
new_prod,
|
|
838
|
+
its_graph,
|
|
839
|
+
explicit_hydrogen=explicit_hydrogen,
|
|
840
|
+
preserve_hydrogen_maps=preserve_hydrogen_maps,
|
|
841
|
+
standardize_output=standardize_output,
|
|
842
|
+
)
|
|
843
|
+
except ITSExpansionError as exc:
|
|
844
|
+
raise ITSExpansionError(
|
|
845
|
+
f"{side_name}-side ITS expansion failed: {exc}"
|
|
846
|
+
) from exc
|
|
847
|
+
except Exception as exc:
|
|
848
|
+
raise ITSExpansionError(
|
|
849
|
+
f"{side_name}-side ITS expansion failed: {exc}"
|
|
850
|
+
) from exc
|
|
851
|
+
|
|
852
|
+
@staticmethod
|
|
853
|
+
def _mapped_radical_assignments(source_graph, target_graph):
|
|
854
|
+
"""Return unique anchor-local radical assignments when compatible."""
|
|
855
|
+
radical_sources = [
|
|
856
|
+
(node, data)
|
|
857
|
+
for node, data in source_graph.nodes(data=True)
|
|
858
|
+
if int(data.get("radical", 0) or 0) > 0
|
|
859
|
+
]
|
|
860
|
+
target_by_map = {
|
|
861
|
+
int(data.get("atom_map", 0) or 0): node
|
|
862
|
+
for node, data in target_graph.nodes(data=True)
|
|
863
|
+
if int(data.get("atom_map", 0) or 0) > 0
|
|
864
|
+
}
|
|
865
|
+
assignments = []
|
|
866
|
+
assigned_targets = set()
|
|
867
|
+
|
|
868
|
+
def compatible(source_data, target_data) -> bool:
|
|
869
|
+
return all(
|
|
870
|
+
source_data.get(name) == target_data.get(name)
|
|
871
|
+
for name in ("element", "aromatic", "hcount", "charge")
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
def mapped_neighbour_signature(graph, node):
|
|
875
|
+
signature = []
|
|
876
|
+
for neighbour in graph.neighbors(node):
|
|
877
|
+
neighbour_data = graph.nodes[neighbour]
|
|
878
|
+
atom_map = int(neighbour_data.get("atom_map", 0) or 0)
|
|
879
|
+
if atom_map <= 0:
|
|
880
|
+
continue
|
|
881
|
+
signature.append(
|
|
882
|
+
(
|
|
883
|
+
atom_map,
|
|
884
|
+
neighbour_data.get("element"),
|
|
885
|
+
neighbour_data.get("charge"),
|
|
886
|
+
graph.edges[node, neighbour].get("order", 1.0),
|
|
887
|
+
)
|
|
888
|
+
)
|
|
889
|
+
return tuple(sorted(signature))
|
|
890
|
+
|
|
891
|
+
for source_node, source_data in radical_sources:
|
|
892
|
+
source_map = int(source_data.get("atom_map", 0) or 0)
|
|
893
|
+
if source_map > 0:
|
|
894
|
+
target_node = target_by_map.get(source_map)
|
|
895
|
+
if target_node is None or not compatible(
|
|
896
|
+
source_data, target_graph.nodes[target_node]
|
|
897
|
+
):
|
|
898
|
+
return None
|
|
899
|
+
else:
|
|
900
|
+
source_signature = mapped_neighbour_signature(source_graph, source_node)
|
|
901
|
+
if not source_signature:
|
|
902
|
+
return None
|
|
903
|
+
candidates = [
|
|
904
|
+
node
|
|
905
|
+
for node, target_data in target_graph.nodes(data=True)
|
|
906
|
+
if node not in assigned_targets
|
|
907
|
+
and compatible(source_data, target_data)
|
|
908
|
+
and source_graph.degree[source_node] == target_graph.degree[node]
|
|
909
|
+
and mapped_neighbour_signature(target_graph, node)
|
|
910
|
+
== source_signature
|
|
911
|
+
]
|
|
912
|
+
if len(candidates) != 1:
|
|
913
|
+
return None
|
|
914
|
+
target_node = candidates[0]
|
|
915
|
+
assigned_targets.add(target_node)
|
|
916
|
+
assignments.append((target_node, int(source_data.get("radical", 0) or 0)))
|
|
917
|
+
return assignments
|
|
918
|
+
|
|
919
|
+
@staticmethod
|
|
920
|
+
def _transfer_endpoint_radicals(
|
|
921
|
+
source_graph,
|
|
922
|
+
target_graph,
|
|
923
|
+
*,
|
|
924
|
+
preserve_atom_maps: bool = True,
|
|
925
|
+
) -> None:
|
|
926
|
+
"""Transfer radical counts through exact endpoint identity.
|
|
927
|
+
|
|
928
|
+
Existing atom maps constrain the correspondence only when the caller
|
|
929
|
+
requested anchor preservation. In normal, unanchored expansion the
|
|
930
|
+
reconstructed endpoint has deliberately received fresh map numbers,
|
|
931
|
+
so requiring equality to the source labels would make a valid graph
|
|
932
|
+
isomorphism impossible.
|
|
933
|
+
"""
|
|
934
|
+
|
|
935
|
+
radical_sources = [
|
|
936
|
+
(node, data)
|
|
937
|
+
for node, data in source_graph.nodes(data=True)
|
|
938
|
+
if int(data.get("radical", 0) or 0) > 0
|
|
939
|
+
]
|
|
940
|
+
if not radical_sources:
|
|
941
|
+
nx.set_node_attributes(target_graph, 0, "radical")
|
|
942
|
+
return
|
|
943
|
+
|
|
944
|
+
if preserve_atom_maps:
|
|
945
|
+
direct_assignments = ITSExpand._mapped_radical_assignments(
|
|
946
|
+
source_graph, target_graph
|
|
947
|
+
)
|
|
948
|
+
if direct_assignments is not None:
|
|
949
|
+
nx.set_node_attributes(target_graph, 0, "radical")
|
|
950
|
+
for target_node, radical_count in direct_assignments:
|
|
951
|
+
target_graph.nodes[target_node]["radical"] = radical_count
|
|
952
|
+
return
|
|
953
|
+
|
|
954
|
+
def node_match(source: dict, target: dict) -> bool:
|
|
955
|
+
if any(
|
|
956
|
+
source.get(name) != target.get(name)
|
|
957
|
+
for name in ("element", "aromatic", "hcount", "charge")
|
|
958
|
+
):
|
|
959
|
+
return False
|
|
960
|
+
if not preserve_atom_maps:
|
|
961
|
+
return True
|
|
962
|
+
required_map = int(source.get("atom_map", 0) or 0)
|
|
963
|
+
target_map = int(target.get("atom_map", 0) or 0)
|
|
964
|
+
return required_map == 0 or required_map == target_map
|
|
965
|
+
|
|
966
|
+
def edge_match(source: dict, target: dict) -> bool:
|
|
967
|
+
return source.get("order", 1.0) == target.get("order", 1.0)
|
|
968
|
+
|
|
969
|
+
matcher = nx.algorithms.isomorphism.GraphMatcher(
|
|
970
|
+
source_graph,
|
|
971
|
+
target_graph,
|
|
972
|
+
node_match=node_match,
|
|
973
|
+
edge_match=edge_match,
|
|
974
|
+
)
|
|
975
|
+
try:
|
|
976
|
+
mapping = next(matcher.isomorphisms_iter())
|
|
977
|
+
except StopIteration as exc:
|
|
978
|
+
raise ITSExpansionError(
|
|
979
|
+
"Cannot align a reconstructed endpoint with its supplied radical state"
|
|
980
|
+
) from exc
|
|
981
|
+
|
|
982
|
+
for source_node, target_node in mapping.items():
|
|
983
|
+
target_graph.nodes[target_node]["radical"] = int(
|
|
984
|
+
source_graph.nodes[source_node].get("radical", 0) or 0
|
|
985
|
+
)
|
|
986
|
+
|
|
987
|
+
@classmethod
|
|
988
|
+
def expand_aam_with_its_report(
|
|
989
|
+
cls,
|
|
990
|
+
rsmi: str,
|
|
991
|
+
relabel: bool = False,
|
|
992
|
+
use_G: bool = True,
|
|
993
|
+
preserve_older_map: bool = False,
|
|
994
|
+
fallback_to_other_side: bool = False,
|
|
995
|
+
require_constitution_preservation: bool = False,
|
|
996
|
+
fold_unmapped_explicit_hydrogens: bool = False,
|
|
997
|
+
ignore_stereochemistry: bool = False,
|
|
998
|
+
explicit_hydrogen: bool = False,
|
|
999
|
+
preserve_radical_state: bool = False,
|
|
1000
|
+
constitutional_only: bool = False,
|
|
1001
|
+
standardize_output: bool = True,
|
|
1002
|
+
) -> ITSExpansionResult:
|
|
1003
|
+
"""Expand a partial reaction SMILES to a full AAM RSMI using ITS
|
|
1004
|
+
reconstruction.
|
|
1005
|
+
|
|
1006
|
+
:param rsmi: Reaction SMILES string in the format
|
|
1007
|
+
``reactant>>product``.
|
|
1008
|
+
:type rsmi: str
|
|
1009
|
+
:param relabel: If True, directly apply ``ITSRelabel().fit(rsmi)``.
|
|
1010
|
+
This globally renumbers atom maps.
|
|
1011
|
+
:type relabel: bool
|
|
1012
|
+
:param use_G: If True, expand using the reactant side. If False,
|
|
1013
|
+
expand using the product side.
|
|
1014
|
+
:type use_G: bool
|
|
1015
|
+
:param preserve_older_map: If True, preserve existing nonzero atom-map
|
|
1016
|
+
numbers by reindexing the side graph before ITS reconstruction.
|
|
1017
|
+
This keeps old maps such as ``:20`` attached to the same atom.
|
|
1018
|
+
This option is incompatible with ``relabel=True``.
|
|
1019
|
+
:type preserve_older_map: bool
|
|
1020
|
+
:param fallback_to_other_side: Retry from the opposite reaction side if
|
|
1021
|
+
the preferred side cannot be expanded. Defaults to False.
|
|
1022
|
+
:type fallback_to_other_side: bool
|
|
1023
|
+
:param require_constitution_preservation: Require the expanded RSMI to
|
|
1024
|
+
preserve the reactant and product constitutions independently.
|
|
1025
|
+
This guard ignores atom maps and stereochemistry but retains
|
|
1026
|
+
isotopes, charges, hydrogens, fragments, and bond orders.
|
|
1027
|
+
:type require_constitution_preservation: bool
|
|
1028
|
+
:param fold_unmapped_explicit_hydrogens: Fold removable unmapped
|
|
1029
|
+
explicit H atoms into heavy-atom hydrogen counts before partial
|
|
1030
|
+
reaction-center construction, while retaining mapped H atoms.
|
|
1031
|
+
:type fold_unmapped_explicit_hydrogens: bool
|
|
1032
|
+
:param ignore_stereochemistry: Remove atom and bond stereo from the
|
|
1033
|
+
working RSMI before ITS construction. This is useful when stereo is
|
|
1034
|
+
explicitly outside the expansion task and malformed directional
|
|
1035
|
+
annotations prevent graph conversion. The constitution guard still
|
|
1036
|
+
compares both original endpoints independently.
|
|
1037
|
+
:type ignore_stereochemistry: bool
|
|
1038
|
+
:param explicit_hydrogen: Preserve explicit hydrogen graph nodes during
|
|
1039
|
+
graph-to-RSMI serialization. This does not materialize hydrogen
|
|
1040
|
+
atoms already represented only by ``hcount``.
|
|
1041
|
+
:type explicit_hydrogen: bool
|
|
1042
|
+
:param preserve_radical_state: Transfer supplied endpoint radical
|
|
1043
|
+
counts through exact endpoint identity instead of asking RDKit to
|
|
1044
|
+
infer them solely from valence during serialization. Existing maps
|
|
1045
|
+
constrain identity only when ``preserve_older_map=True``.
|
|
1046
|
+
:type preserve_radical_state: bool
|
|
1047
|
+
:param constitutional_only: Build only attributes required for
|
|
1048
|
+
constitutional partial-AAM completion and omit graph-level stereo
|
|
1049
|
+
descriptors. Defaults to False.
|
|
1050
|
+
:type constitutional_only: bool
|
|
1051
|
+
:param standardize_output: Reparse and standardize the already
|
|
1052
|
+
sanitized graph serialization. Disable only for the minimal RSMI
|
|
1053
|
+
adapter, whose validation is performed separately.
|
|
1054
|
+
:type standardize_output: bool
|
|
1055
|
+
:return: Fully mapped RSMI and structured expansion evidence.
|
|
1056
|
+
:rtype: ITSExpansionResult
|
|
1057
|
+
:raises ValueError: If input RSMI format is invalid, if incompatible
|
|
1058
|
+
options are used, or if side-graph reindexing is unsafe.
|
|
1059
|
+
|
|
1060
|
+
.. rubric:: Examples
|
|
1061
|
+
|
|
1062
|
+
>>> expander = ITSExpand()
|
|
1063
|
+
>>> expander.expand_aam_with_its(
|
|
1064
|
+
... "CC[CH2:3][Cl:1].[N:2]>>CC[CH2:3][N:2].[Cl:1]",
|
|
1065
|
+
... preserve_older_map=True,
|
|
1066
|
+
... )
|
|
1067
|
+
'[CH3:1][CH2:2][CH2:3][Cl:4].[N:5]>>[CH3:1][CH2:2][CH2:3][N:5].[Cl:4]'
|
|
1068
|
+
"""
|
|
1069
|
+
if relabel and preserve_older_map:
|
|
1070
|
+
raise ValueError(
|
|
1071
|
+
"preserve_older_map=True cannot be combined with relabel=True. "
|
|
1072
|
+
"ITSRelabel globally renumbers atom maps. Use relabel=False "
|
|
1073
|
+
"with preserve_older_map=True."
|
|
1074
|
+
)
|
|
1075
|
+
|
|
1076
|
+
if relabel:
|
|
1077
|
+
expanded = ITSRelabel().fit(rsmi)
|
|
1078
|
+
if (
|
|
1079
|
+
require_constitution_preservation
|
|
1080
|
+
and not cls.endpoint_constitutions_match(rsmi, expanded)
|
|
1081
|
+
):
|
|
1082
|
+
raise ITSExpansionError(
|
|
1083
|
+
"Relabelled RSMI does not preserve both endpoint constitutions."
|
|
1084
|
+
)
|
|
1085
|
+
return ITSExpansionResult(
|
|
1086
|
+
expanded,
|
|
1087
|
+
preferred_side="relabel",
|
|
1088
|
+
selected_side="relabel",
|
|
1089
|
+
fallback_used=False,
|
|
1090
|
+
constitution_checked=require_constitution_preservation,
|
|
1091
|
+
constitution_guard_passed=(
|
|
1092
|
+
True if require_constitution_preservation else None
|
|
1093
|
+
),
|
|
1094
|
+
explicit_hydrogen_serialization=explicit_hydrogen,
|
|
1095
|
+
radical_state_preserved=preserve_radical_state,
|
|
1096
|
+
)
|
|
1097
|
+
|
|
1098
|
+
cls._split_rsmi(rsmi)
|
|
1099
|
+
source_rsmi = rsmi
|
|
1100
|
+
if fold_unmapped_explicit_hydrogens:
|
|
1101
|
+
working_rsmi, folded_hydrogen_count = (
|
|
1102
|
+
cls._fold_unmapped_explicit_hydrogens_with_count(rsmi)
|
|
1103
|
+
)
|
|
1104
|
+
else:
|
|
1105
|
+
working_rsmi = rsmi
|
|
1106
|
+
folded_hydrogen_count = 0
|
|
1107
|
+
stereochemistry_ignored = bool(
|
|
1108
|
+
ignore_stereochemistry and cls._rsmi_has_stereochemistry(working_rsmi)
|
|
1109
|
+
)
|
|
1110
|
+
if stereochemistry_ignored:
|
|
1111
|
+
working_rsmi = cls.remove_stereochemistry(working_rsmi)
|
|
1112
|
+
preferred_name = "reactant" if use_G else "product"
|
|
1113
|
+
fallback_name = "product" if use_G else "reactant"
|
|
1114
|
+
|
|
1115
|
+
def expand_and_guard(side: bool) -> str:
|
|
1116
|
+
expanded = cls._expand_aam_once(
|
|
1117
|
+
working_rsmi,
|
|
1118
|
+
use_G=side,
|
|
1119
|
+
preserve_older_map=preserve_older_map,
|
|
1120
|
+
explicit_hydrogen=explicit_hydrogen,
|
|
1121
|
+
preserve_radical_state=preserve_radical_state,
|
|
1122
|
+
constitutional_only=constitutional_only,
|
|
1123
|
+
standardize_output=standardize_output,
|
|
1124
|
+
)
|
|
1125
|
+
if (
|
|
1126
|
+
require_constitution_preservation
|
|
1127
|
+
and not cls.endpoint_constitutions_match(source_rsmi, expanded)
|
|
1128
|
+
):
|
|
1129
|
+
side_name = "reactant" if side else "product"
|
|
1130
|
+
raise ITSExpansionError(
|
|
1131
|
+
f"{side_name}-side expansion changed an endpoint constitution"
|
|
1132
|
+
)
|
|
1133
|
+
return expanded
|
|
1134
|
+
|
|
1135
|
+
try:
|
|
1136
|
+
expanded = expand_and_guard(use_G)
|
|
1137
|
+
return ITSExpansionResult(
|
|
1138
|
+
expanded,
|
|
1139
|
+
preferred_side=preferred_name,
|
|
1140
|
+
selected_side=preferred_name,
|
|
1141
|
+
fallback_used=False,
|
|
1142
|
+
constitution_checked=require_constitution_preservation,
|
|
1143
|
+
constitution_guard_passed=(
|
|
1144
|
+
True if require_constitution_preservation else None
|
|
1145
|
+
),
|
|
1146
|
+
unmapped_explicit_hydrogens_folded=(folded_hydrogen_count > 0),
|
|
1147
|
+
folded_unmapped_explicit_hydrogen_count=folded_hydrogen_count,
|
|
1148
|
+
stereochemistry_ignored_for_expansion=stereochemistry_ignored,
|
|
1149
|
+
explicit_hydrogen_serialization=explicit_hydrogen,
|
|
1150
|
+
radical_state_preserved=preserve_radical_state,
|
|
1151
|
+
)
|
|
1152
|
+
except ITSExpansionError as preferred_error:
|
|
1153
|
+
if not fallback_to_other_side:
|
|
1154
|
+
raise
|
|
1155
|
+
|
|
1156
|
+
try:
|
|
1157
|
+
expanded = expand_and_guard(not use_G)
|
|
1158
|
+
return ITSExpansionResult(
|
|
1159
|
+
expanded,
|
|
1160
|
+
preferred_side=preferred_name,
|
|
1161
|
+
selected_side=fallback_name,
|
|
1162
|
+
fallback_used=True,
|
|
1163
|
+
constitution_checked=require_constitution_preservation,
|
|
1164
|
+
constitution_guard_passed=(
|
|
1165
|
+
True if require_constitution_preservation else None
|
|
1166
|
+
),
|
|
1167
|
+
unmapped_explicit_hydrogens_folded=(folded_hydrogen_count > 0),
|
|
1168
|
+
folded_unmapped_explicit_hydrogen_count=folded_hydrogen_count,
|
|
1169
|
+
stereochemistry_ignored_for_expansion=stereochemistry_ignored,
|
|
1170
|
+
explicit_hydrogen_serialization=explicit_hydrogen,
|
|
1171
|
+
radical_state_preserved=preserve_radical_state,
|
|
1172
|
+
fallback_reason=str(preferred_error),
|
|
1173
|
+
)
|
|
1174
|
+
except ITSExpansionError as fallback_error:
|
|
1175
|
+
raise ITSExpansionError(
|
|
1176
|
+
f"Both ITS expansion directions failed. {preferred_name}: "
|
|
1177
|
+
f"{preferred_error}; {fallback_name}: {fallback_error}"
|
|
1178
|
+
) from fallback_error
|
|
1179
|
+
|
|
1180
|
+
@classmethod
|
|
1181
|
+
def expand_rsmi(
|
|
1182
|
+
cls,
|
|
1183
|
+
rsmi: str,
|
|
1184
|
+
*,
|
|
1185
|
+
use_G: bool = True,
|
|
1186
|
+
preserve_radical_state: bool = False,
|
|
1187
|
+
) -> str:
|
|
1188
|
+
"""Complete partial AAM through the minimal RSMI reconstruction path.
|
|
1189
|
+
|
|
1190
|
+
This adapter parses the mapped reaction centre and one complete
|
|
1191
|
+
endpoint, performs ITS glue/decomposition, and serializes the result.
|
|
1192
|
+
It deliberately assigns fresh maps and excludes optional hydrogen,
|
|
1193
|
+
stereo, fallback, and post-expansion constitution guards. Radical
|
|
1194
|
+
attributes can be transported as part of reconstruction without
|
|
1195
|
+
enabling those guards.
|
|
1196
|
+
|
|
1197
|
+
:param rsmi: Partially mapped reaction SMILES.
|
|
1198
|
+
:type rsmi: str
|
|
1199
|
+
:param use_G: Reconstruct from reactants when True, products otherwise.
|
|
1200
|
+
:type use_G: bool
|
|
1201
|
+
:param preserve_radical_state: Transport endpoint radical counts as a
|
|
1202
|
+
required graph attribute. This does not enable validation guards.
|
|
1203
|
+
:type preserve_radical_state: bool
|
|
1204
|
+
:return: Completely mapped reaction SMILES.
|
|
1205
|
+
:rtype: str
|
|
1206
|
+
"""
|
|
1207
|
+
return cls.expand_aam_with_its(
|
|
1208
|
+
rsmi,
|
|
1209
|
+
use_G=use_G,
|
|
1210
|
+
preserve_older_map=False,
|
|
1211
|
+
preserve_radical_state=preserve_radical_state,
|
|
1212
|
+
constitutional_only=True,
|
|
1213
|
+
standardize_output=False,
|
|
1214
|
+
)
|
|
1215
|
+
|
|
1216
|
+
@classmethod
|
|
1217
|
+
def expand_aam_with_its(
|
|
1218
|
+
cls,
|
|
1219
|
+
rsmi: str,
|
|
1220
|
+
relabel: bool = False,
|
|
1221
|
+
use_G: bool = True,
|
|
1222
|
+
preserve_older_map: bool = False,
|
|
1223
|
+
fallback_to_other_side: bool = False,
|
|
1224
|
+
require_constitution_preservation: bool = False,
|
|
1225
|
+
fold_unmapped_explicit_hydrogens: bool = False,
|
|
1226
|
+
ignore_stereochemistry: bool = False,
|
|
1227
|
+
explicit_hydrogen: bool = False,
|
|
1228
|
+
preserve_radical_state: bool = False,
|
|
1229
|
+
constitutional_only: bool = False,
|
|
1230
|
+
standardize_output: bool = True,
|
|
1231
|
+
) -> str:
|
|
1232
|
+
"""Expand partial AAM and return the completed reaction SMILES.
|
|
1233
|
+
|
|
1234
|
+
Use :meth:`expand_aam_with_its_report` when selected-side and guard
|
|
1235
|
+
evidence are also required. This method preserves the historical
|
|
1236
|
+
string-returning API.
|
|
1237
|
+
"""
|
|
1238
|
+
return cls.expand_aam_with_its_report(
|
|
1239
|
+
rsmi,
|
|
1240
|
+
relabel=relabel,
|
|
1241
|
+
use_G=use_G,
|
|
1242
|
+
preserve_older_map=preserve_older_map,
|
|
1243
|
+
fallback_to_other_side=fallback_to_other_side,
|
|
1244
|
+
require_constitution_preservation=require_constitution_preservation,
|
|
1245
|
+
fold_unmapped_explicit_hydrogens=fold_unmapped_explicit_hydrogens,
|
|
1246
|
+
ignore_stereochemistry=ignore_stereochemistry,
|
|
1247
|
+
explicit_hydrogen=explicit_hydrogen,
|
|
1248
|
+
preserve_radical_state=preserve_radical_state,
|
|
1249
|
+
constitutional_only=constitutional_only,
|
|
1250
|
+
standardize_output=standardize_output,
|
|
1251
|
+
).rsmi
|