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,1466 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections import Counter
|
|
5
|
+
from typing import Any, Literal, Optional
|
|
6
|
+
|
|
7
|
+
# ============================================================
|
|
8
|
+
# Optional SynKit imports
|
|
9
|
+
# ============================================================
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from synkit.Chem.Reaction.canon_rsmi import CanonRSMI
|
|
13
|
+
from synkit.Graph.ITS.its_expand import ITSExpand
|
|
14
|
+
from synkit.IO import rsmi_to_its
|
|
15
|
+
except ImportError:
|
|
16
|
+
CanonRSMI = None
|
|
17
|
+
rsmi_to_its = None
|
|
18
|
+
ITSExpand = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ============================================================
|
|
22
|
+
# Regex helpers
|
|
23
|
+
# ============================================================
|
|
24
|
+
|
|
25
|
+
BRACKET_ATOM_RE = re.compile(r"\[[^\[\]]+\]")
|
|
26
|
+
ATOM_MAP_RE = re.compile(r":(\d+)(?=\])")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ============================================================
|
|
30
|
+
# Arrow-code parsing
|
|
31
|
+
# ============================================================
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def parse_atom_list(text: str) -> list[int]:
|
|
35
|
+
"""Parse a comma-separated atom-map list.
|
|
36
|
+
|
|
37
|
+
:param text: Atom-map text such as ``"10"`` or ``"10,11"``.
|
|
38
|
+
:type text: str
|
|
39
|
+
:return: Parsed atom-map numbers.
|
|
40
|
+
:rtype: list[int]
|
|
41
|
+
"""
|
|
42
|
+
return [int(x.strip()) for x in text.split(",") if x.strip()]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def parse_arrow_step(step: str) -> tuple[list[int], list[int]]:
|
|
46
|
+
"""Convert one arrow-code step.
|
|
47
|
+
|
|
48
|
+
For example, ``"10=20"`` becomes ``([10], [20])`` and
|
|
49
|
+
``"12=11,12"`` becomes ``([12], [11, 12])``.
|
|
50
|
+
|
|
51
|
+
:param step: One arrow-code step containing a left and right side.
|
|
52
|
+
:type step: str
|
|
53
|
+
:return: Parsed left-side and right-side atom-map lists.
|
|
54
|
+
:rtype: tuple[list[int], list[int]]
|
|
55
|
+
:raises ValueError: If ``step`` does not contain ``"="``.
|
|
56
|
+
"""
|
|
57
|
+
if "=" not in step:
|
|
58
|
+
raise ValueError(f"Invalid arrow step without '=': {step}")
|
|
59
|
+
|
|
60
|
+
lhs, rhs = step.split("=", 1)
|
|
61
|
+
return parse_atom_list(lhs), parse_atom_list(rhs)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def split_arrow_code(arrow_code: str) -> list[str]:
|
|
65
|
+
"""Split an arrow code into non-empty steps.
|
|
66
|
+
|
|
67
|
+
:param arrow_code: Semicolon-separated arrow code.
|
|
68
|
+
:type arrow_code: str
|
|
69
|
+
:return: Individual stripped arrow-code steps.
|
|
70
|
+
:rtype: list[str]
|
|
71
|
+
"""
|
|
72
|
+
return [s.strip() for s in arrow_code.split(";") if s.strip()]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def ef_arrow_code_to_arrow_code(ef_arrow_code: str) -> str:
|
|
76
|
+
"""Normalize an EF-SMIRKS hyphen-form flow code for SynKit conversion.
|
|
77
|
+
|
|
78
|
+
EF-SMIRKS stores one electron-flow step as ``source-target``; SynKit's
|
|
79
|
+
internal arrow-code helpers use ``source=target``. For example,
|
|
80
|
+
``"10,20-20,21"`` becomes ``"10,20=20,21"``.
|
|
81
|
+
|
|
82
|
+
:param ef_arrow_code: Semicolon-separated hyphen-form electron-flow code.
|
|
83
|
+
:type ef_arrow_code: str
|
|
84
|
+
:return: Equivalent equals-form SynKit arrow code.
|
|
85
|
+
:rtype: str
|
|
86
|
+
:raises ValueError: If a step does not contain exactly one ``"-"``.
|
|
87
|
+
"""
|
|
88
|
+
normalized_steps = []
|
|
89
|
+
for step in split_arrow_code(ef_arrow_code):
|
|
90
|
+
if step.count("-") != 1:
|
|
91
|
+
raise ValueError(
|
|
92
|
+
"An EF-SMIRKS electron-flow step must contain exactly one '-': "
|
|
93
|
+
f"{step!r}"
|
|
94
|
+
)
|
|
95
|
+
lhs, rhs = step.split("-", 1)
|
|
96
|
+
normalized_steps.append(f"{lhs.strip()}={rhs.strip()}")
|
|
97
|
+
if not normalized_steps:
|
|
98
|
+
raise ValueError("EF-SMIRKS electron-flow code is empty.")
|
|
99
|
+
return ";".join(normalized_steps)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def arrow_code_to_ef_arrow_code(arrow_code: str) -> str:
|
|
103
|
+
"""Convert internal equals-form arrow code to EF-SMIRKS hyphen form.
|
|
104
|
+
|
|
105
|
+
:param arrow_code: Semicolon-separated SynKit arrow code.
|
|
106
|
+
:type arrow_code: str
|
|
107
|
+
:return: Equivalent hyphen-form electron-flow code.
|
|
108
|
+
:rtype: str
|
|
109
|
+
"""
|
|
110
|
+
return ";".join(step.replace("=", "-", 1) for step in split_arrow_code(arrow_code))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def split_ef_smirks(ef_smirks: str) -> tuple[str, str]:
|
|
114
|
+
"""Split an EF-SMIRKS string into reaction SMILES and flow code.
|
|
115
|
+
|
|
116
|
+
An EF-SMIRKS is an atom-mapped reaction SMILES followed by whitespace and
|
|
117
|
+
a hyphen-form electron-flow code, e.g. ``"[CH3:1][Cl:2]>>[CH3:1].[Cl-:2]
|
|
118
|
+
1-1,2"``.
|
|
119
|
+
|
|
120
|
+
:param ef_smirks: RSMI followed by whitespace and electron-flow code.
|
|
121
|
+
:type ef_smirks: str
|
|
122
|
+
:return: Reaction SMILES and hyphen-form electron-flow code.
|
|
123
|
+
:rtype: tuple[str, str]
|
|
124
|
+
:raises ValueError: If the input lacks an RSMI arrow or flow code.
|
|
125
|
+
"""
|
|
126
|
+
try:
|
|
127
|
+
reaction_smiles, ef_arrow_code = ef_smirks.strip().rsplit(None, 1)
|
|
128
|
+
except ValueError as exc:
|
|
129
|
+
raise ValueError(
|
|
130
|
+
"EF-SMIRKS must contain reaction SMILES followed by whitespace and "
|
|
131
|
+
"an electron-flow code."
|
|
132
|
+
) from exc
|
|
133
|
+
if ">>" not in reaction_smiles:
|
|
134
|
+
raise ValueError("EF-SMIRKS reaction component must contain '>>'.")
|
|
135
|
+
return reaction_smiles, ef_arrow_code
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def arrow_atom_maps(arrow_code: str) -> set[int]:
|
|
139
|
+
"""Return all atom maps used in an arrow code.
|
|
140
|
+
|
|
141
|
+
:param arrow_code: Semicolon-separated arrow code.
|
|
142
|
+
:type arrow_code: str
|
|
143
|
+
:return: Atom-map numbers referenced by the code.
|
|
144
|
+
:rtype: set[int]
|
|
145
|
+
"""
|
|
146
|
+
maps: set[int] = set()
|
|
147
|
+
|
|
148
|
+
for step in split_arrow_code(arrow_code):
|
|
149
|
+
lhs, rhs = parse_arrow_step(step)
|
|
150
|
+
maps.update(lhs)
|
|
151
|
+
maps.update(rhs)
|
|
152
|
+
|
|
153
|
+
return maps
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def classify_arrow_shape(step: str) -> str:
|
|
157
|
+
"""Classify one arrow-code step.
|
|
158
|
+
|
|
159
|
+
:param step: One arrow-code step.
|
|
160
|
+
:type step: str
|
|
161
|
+
:return: Shape label such as ``"a=b"`` or ``"a,b=c,d"``.
|
|
162
|
+
:rtype: str
|
|
163
|
+
"""
|
|
164
|
+
lhs, rhs = parse_arrow_step(step)
|
|
165
|
+
|
|
166
|
+
if len(lhs) == 1 and len(rhs) == 1:
|
|
167
|
+
return "a=b"
|
|
168
|
+
|
|
169
|
+
if len(lhs) == 1 and len(rhs) == 2:
|
|
170
|
+
return "a=b,c"
|
|
171
|
+
|
|
172
|
+
if len(lhs) == 2 and len(rhs) == 1:
|
|
173
|
+
return "a,b=c"
|
|
174
|
+
|
|
175
|
+
if len(lhs) == 2 and len(rhs) == 2:
|
|
176
|
+
return "a,b=c,d"
|
|
177
|
+
|
|
178
|
+
return f"unsupported:{len(lhs)},{len(rhs)}"
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def check_arrow_code_coverage(arrow_codes: list[str]) -> dict[str, Any]:
|
|
182
|
+
"""Check which arrow-code shapes appear in a dataset.
|
|
183
|
+
|
|
184
|
+
:param arrow_codes: Arrow codes to inspect.
|
|
185
|
+
:type arrow_codes: list[str]
|
|
186
|
+
:return: Shape counts, unsupported steps, and an all-supported flag.
|
|
187
|
+
:rtype: dict[str, Any]
|
|
188
|
+
"""
|
|
189
|
+
shape_counter = Counter()
|
|
190
|
+
unsupported = []
|
|
191
|
+
|
|
192
|
+
for row_idx, arrow_code in enumerate(arrow_codes, start=1):
|
|
193
|
+
for step in split_arrow_code(arrow_code):
|
|
194
|
+
shape = classify_arrow_shape(step)
|
|
195
|
+
shape_counter[shape] += 1
|
|
196
|
+
|
|
197
|
+
if shape.startswith("unsupported"):
|
|
198
|
+
unsupported.append(
|
|
199
|
+
{
|
|
200
|
+
"row_index": row_idx,
|
|
201
|
+
"arrow_code": arrow_code,
|
|
202
|
+
"step": step,
|
|
203
|
+
"shape": shape,
|
|
204
|
+
}
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
"shape_counts": dict(shape_counter),
|
|
209
|
+
"unsupported": unsupported,
|
|
210
|
+
"all_supported": len(unsupported) == 0,
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# ============================================================
|
|
215
|
+
# Atom-map preprocessing
|
|
216
|
+
# ============================================================
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def extract_atom_maps_from_smiles(smiles: str) -> list[int]:
|
|
220
|
+
"""Extract atom-map numbers from bracket atoms.
|
|
221
|
+
|
|
222
|
+
For example, ``"[CH:10]"`` yields ``10`` and ``"[N+:61]"`` yields ``61``.
|
|
223
|
+
|
|
224
|
+
:param smiles: SMILES or SMIRKS fragment.
|
|
225
|
+
:type smiles: str
|
|
226
|
+
:return: Atom-map numbers found in bracket atoms.
|
|
227
|
+
:rtype: list[int]
|
|
228
|
+
"""
|
|
229
|
+
return [int(x) for x in ATOM_MAP_RE.findall(smiles)]
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def duplicate_atom_maps_in_side(smiles: str) -> dict[int, int]:
|
|
233
|
+
"""Find duplicated atom maps in one side of a reaction.
|
|
234
|
+
|
|
235
|
+
:param smiles: Reactant-side or product-side SMILES text.
|
|
236
|
+
:type smiles: str
|
|
237
|
+
:return: Mapping of duplicated atom-map numbers to occurrence counts.
|
|
238
|
+
:rtype: dict[int, int]
|
|
239
|
+
"""
|
|
240
|
+
counts = Counter(extract_atom_maps_from_smiles(smiles))
|
|
241
|
+
return {atom_map: count for atom_map, count in counts.items() if count > 1}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def remove_duplicate_atom_maps(rsmi: str) -> tuple[str, dict[str, dict[int, int]]]:
|
|
245
|
+
"""Make each endpoint map-injective before partial-map expansion.
|
|
246
|
+
|
|
247
|
+
The first occurrence of a positive map on each endpoint is retained and
|
|
248
|
+
the label is removed from every later occurrence. The now-unmapped atoms
|
|
249
|
+
can then receive fresh labels during guarded AAM expansion.
|
|
250
|
+
|
|
251
|
+
:param rsmi: Reaction text in ``reactants>>products`` format.
|
|
252
|
+
:type rsmi: str
|
|
253
|
+
:return: Cleaned reaction and removed occurrence counts by endpoint.
|
|
254
|
+
:rtype: tuple[str, dict[str, dict[int, int]]]
|
|
255
|
+
:raises ValueError: If the reaction does not contain exactly one arrow.
|
|
256
|
+
"""
|
|
257
|
+
if rsmi.count(">>") != 1:
|
|
258
|
+
raise ValueError("RSMI must contain exactly one '>>'")
|
|
259
|
+
|
|
260
|
+
removed_by_side: dict[str, dict[int, int]] = {}
|
|
261
|
+
cleaned_sides: list[str] = []
|
|
262
|
+
for side_name, side in zip(("reactants", "products"), rsmi.split(">>", 1)):
|
|
263
|
+
seen: set[int] = set()
|
|
264
|
+
removed: Counter[int] = Counter()
|
|
265
|
+
|
|
266
|
+
def clean_bracket_atom(match: re.Match) -> str:
|
|
267
|
+
token = match.group(0)
|
|
268
|
+
map_match = ATOM_MAP_RE.search(token)
|
|
269
|
+
if map_match is None:
|
|
270
|
+
return token
|
|
271
|
+
atom_map = int(map_match.group(1))
|
|
272
|
+
if atom_map not in seen:
|
|
273
|
+
seen.add(atom_map)
|
|
274
|
+
return token
|
|
275
|
+
removed[atom_map] += 1
|
|
276
|
+
return ATOM_MAP_RE.sub("", token)
|
|
277
|
+
|
|
278
|
+
cleaned_sides.append(BRACKET_ATOM_RE.sub(clean_bracket_atom, side))
|
|
279
|
+
removed_by_side[side_name] = dict(sorted(removed.items()))
|
|
280
|
+
|
|
281
|
+
return ">>".join(cleaned_sides), removed_by_side
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def validate_arrow_maps(
|
|
285
|
+
rsmi: str,
|
|
286
|
+
arrow_code: str,
|
|
287
|
+
raise_on_arrow_duplicates: bool = True,
|
|
288
|
+
raise_on_missing_arrow_maps: bool = True,
|
|
289
|
+
) -> dict[str, Any]:
|
|
290
|
+
"""Validate atom maps before SynKit.
|
|
291
|
+
|
|
292
|
+
Rules
|
|
293
|
+
-----
|
|
294
|
+
1. Duplicated atom maps used by arrow_code are fatal.
|
|
295
|
+
2. Missing atom maps used by arrow_code are fatal.
|
|
296
|
+
3. Duplicated non-arrow atom maps are warnings only, because they
|
|
297
|
+
can be removed before SynKit expansion.
|
|
298
|
+
|
|
299
|
+
:param rsmi: Reaction SMILES in ``reactants>>products`` format.
|
|
300
|
+
:type rsmi: str
|
|
301
|
+
:param arrow_code: Arrow code whose atom maps must be validated.
|
|
302
|
+
:type arrow_code: str
|
|
303
|
+
:param raise_on_arrow_duplicates: Whether duplicated arrow atom maps are fatal.
|
|
304
|
+
:type raise_on_arrow_duplicates: bool
|
|
305
|
+
:param raise_on_missing_arrow_maps: Whether missing arrow atom maps are fatal.
|
|
306
|
+
:type raise_on_missing_arrow_maps: bool
|
|
307
|
+
:return: Validation diagnostics.
|
|
308
|
+
:rtype: dict[str, Any]
|
|
309
|
+
:raises ValueError: If the RSMI is malformed or enabled validation fails.
|
|
310
|
+
"""
|
|
311
|
+
if ">>" not in rsmi:
|
|
312
|
+
raise ValueError("RSMI must contain '>>'")
|
|
313
|
+
|
|
314
|
+
reactants, products = rsmi.split(">>", 1)
|
|
315
|
+
|
|
316
|
+
arrow_maps = arrow_atom_maps(arrow_code)
|
|
317
|
+
|
|
318
|
+
reactant_maps = extract_atom_maps_from_smiles(reactants)
|
|
319
|
+
product_maps = extract_atom_maps_from_smiles(products)
|
|
320
|
+
|
|
321
|
+
all_raw_maps = set(reactant_maps) | set(product_maps)
|
|
322
|
+
|
|
323
|
+
missing_arrow_maps = sorted(m for m in arrow_maps if m not in all_raw_maps)
|
|
324
|
+
|
|
325
|
+
r_dupes = duplicate_atom_maps_in_side(reactants)
|
|
326
|
+
p_dupes = duplicate_atom_maps_in_side(products)
|
|
327
|
+
|
|
328
|
+
arrow_dupes = {
|
|
329
|
+
"reactants": {m: c for m, c in r_dupes.items() if m in arrow_maps},
|
|
330
|
+
"products": {m: c for m, c in p_dupes.items() if m in arrow_maps},
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
non_arrow_dupes = {
|
|
334
|
+
"reactants": {m: c for m, c in r_dupes.items() if m not in arrow_maps},
|
|
335
|
+
"products": {m: c for m, c in p_dupes.items() if m not in arrow_maps},
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
diagnostics = {
|
|
339
|
+
"arrow_maps": sorted(arrow_maps),
|
|
340
|
+
"missing_arrow_maps": missing_arrow_maps,
|
|
341
|
+
"arrow_duplicate_maps": arrow_dupes,
|
|
342
|
+
"non_arrow_duplicate_maps": non_arrow_dupes,
|
|
343
|
+
"has_missing_arrow_maps": bool(missing_arrow_maps),
|
|
344
|
+
"has_arrow_duplicate_maps": bool(
|
|
345
|
+
arrow_dupes["reactants"] or arrow_dupes["products"]
|
|
346
|
+
),
|
|
347
|
+
"has_non_arrow_duplicate_maps": bool(
|
|
348
|
+
non_arrow_dupes["reactants"] or non_arrow_dupes["products"]
|
|
349
|
+
),
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if raise_on_missing_arrow_maps and diagnostics["has_missing_arrow_maps"]:
|
|
353
|
+
raise ValueError(
|
|
354
|
+
"Some atom maps used in arrow_code are missing from the reaction SMILES. "
|
|
355
|
+
f"Diagnostics: {diagnostics}"
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
if raise_on_arrow_duplicates and diagnostics["has_arrow_duplicate_maps"]:
|
|
359
|
+
raise ValueError(
|
|
360
|
+
"Some atom maps used in arrow_code are duplicated in the reaction SMILES. "
|
|
361
|
+
f"Diagnostics: {diagnostics}"
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
return diagnostics
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def remove_non_arrow_atom_maps(rsmi: str, arrow_code: str) -> str:
|
|
368
|
+
"""Keep only atom maps involved in arrow_code.
|
|
369
|
+
Remove every other atom map.
|
|
370
|
+
|
|
371
|
+
This is important because some source SMIRKS have duplicated
|
|
372
|
+
non-arrow atom maps, e.g.
|
|
373
|
+
|
|
374
|
+
[N+:61]2=[CH:61]
|
|
375
|
+
|
|
376
|
+
If 61 is not used by arrow_code, we remove it and let SynKit
|
|
377
|
+
CanonRSMI().expand_aam(...) generate clean full maps.
|
|
378
|
+
|
|
379
|
+
:param rsmi: Reaction SMILES to clean.
|
|
380
|
+
:type rsmi: str
|
|
381
|
+
:param arrow_code: Arrow code whose atom maps should be preserved.
|
|
382
|
+
:type arrow_code: str
|
|
383
|
+
:return: Reaction SMILES with non-arrow atom maps removed.
|
|
384
|
+
:rtype: str
|
|
385
|
+
"""
|
|
386
|
+
keep_maps = arrow_atom_maps(arrow_code)
|
|
387
|
+
|
|
388
|
+
def clean_bracket_atom(match: re.Match) -> str:
|
|
389
|
+
token = match.group(0)
|
|
390
|
+
|
|
391
|
+
map_match = ATOM_MAP_RE.search(token)
|
|
392
|
+
if map_match is None:
|
|
393
|
+
return token
|
|
394
|
+
|
|
395
|
+
atom_map = int(map_match.group(1))
|
|
396
|
+
|
|
397
|
+
if atom_map in keep_maps:
|
|
398
|
+
return token
|
|
399
|
+
|
|
400
|
+
return ATOM_MAP_RE.sub("", token)
|
|
401
|
+
|
|
402
|
+
return BRACKET_ATOM_RE.sub(clean_bracket_atom, rsmi)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
# ============================================================
|
|
406
|
+
# Generic LP/B conversion
|
|
407
|
+
# ============================================================
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def generic_convert_step(step: str) -> list[Any]:
|
|
411
|
+
"""Generic graph-independent conversion.
|
|
412
|
+
|
|
413
|
+
Supported grammar
|
|
414
|
+
-----------------
|
|
415
|
+
a=b
|
|
416
|
+
LP(a) forms bond a-b
|
|
417
|
+
-> ["LP-/B+", [a], [a, b]]
|
|
418
|
+
|
|
419
|
+
a=b,c
|
|
420
|
+
LP(a) forms/increases bond b-c
|
|
421
|
+
-> ["LP-/B+", [a], [b, c]]
|
|
422
|
+
|
|
423
|
+
a,b=c
|
|
424
|
+
bond a-b breaks; electrons end as LP on c
|
|
425
|
+
-> ["B-/LP+", [a, b], [c]]
|
|
426
|
+
|
|
427
|
+
a,b=c,d
|
|
428
|
+
bond a-b becomes bond c-d
|
|
429
|
+
-> ["B-/B+", [a, b], [c, d]]
|
|
430
|
+
|
|
431
|
+
:param step: One arrow-code step.
|
|
432
|
+
:type step: str
|
|
433
|
+
:return: Generic LP/B conversion record.
|
|
434
|
+
:rtype: list[Any]
|
|
435
|
+
:raises ValueError: If the step shape is unsupported.
|
|
436
|
+
"""
|
|
437
|
+
lhs, rhs = parse_arrow_step(step)
|
|
438
|
+
|
|
439
|
+
# a=b
|
|
440
|
+
if len(lhs) == 1 and len(rhs) == 1:
|
|
441
|
+
a = lhs[0]
|
|
442
|
+
b = rhs[0]
|
|
443
|
+
return ["LP-/B+", [a], [a, b]]
|
|
444
|
+
|
|
445
|
+
# a=b,c
|
|
446
|
+
if len(lhs) == 1 and len(rhs) == 2:
|
|
447
|
+
return ["LP-/B+", lhs, rhs]
|
|
448
|
+
|
|
449
|
+
# a,b=c
|
|
450
|
+
if len(lhs) == 2 and len(rhs) == 1:
|
|
451
|
+
return ["B-/LP+", lhs, rhs]
|
|
452
|
+
|
|
453
|
+
# a,b=c,d
|
|
454
|
+
if len(lhs) == 2 and len(rhs) == 2:
|
|
455
|
+
return ["B-/B+", lhs, rhs]
|
|
456
|
+
|
|
457
|
+
raise ValueError(f"Unsupported arrow step: {step}")
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def generic_convert_arrow_code(arrow_code: str) -> list[list[Any]]:
|
|
461
|
+
"""Convert every step in an arrow code to generic LP/B form.
|
|
462
|
+
|
|
463
|
+
:param arrow_code: Semicolon-separated arrow code.
|
|
464
|
+
:type arrow_code: str
|
|
465
|
+
:return: Generic conversion records for each step.
|
|
466
|
+
:rtype: list[list[Any]]
|
|
467
|
+
"""
|
|
468
|
+
return [generic_convert_step(step) for step in split_arrow_code(arrow_code)]
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
# ============================================================
|
|
472
|
+
# SynKit ITS construction
|
|
473
|
+
# ============================================================
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def build_its_from_rsmi(
|
|
477
|
+
rsmi: str,
|
|
478
|
+
arrow_code: str,
|
|
479
|
+
expand_aam: bool = True,
|
|
480
|
+
remove_non_arrow_maps: bool = True,
|
|
481
|
+
aam_fallback_to_other_side: bool = False,
|
|
482
|
+
require_constitution_preservation: bool = False,
|
|
483
|
+
fold_unmapped_explicit_hydrogens: bool = False,
|
|
484
|
+
ignore_stereochemistry: bool = False,
|
|
485
|
+
explicit_hydrogen: bool = False,
|
|
486
|
+
preserve_radical_state: bool = False,
|
|
487
|
+
):
|
|
488
|
+
"""Build SynKit ITS graph from reaction SMILES.
|
|
489
|
+
|
|
490
|
+
Pipeline
|
|
491
|
+
--------
|
|
492
|
+
raw SMIRKS
|
|
493
|
+
-> validate arrow atom maps
|
|
494
|
+
-> remove non-arrow atom maps
|
|
495
|
+
-> CanonRSMI().expand_aam(...)
|
|
496
|
+
-> rsmi_to_its(...)
|
|
497
|
+
|
|
498
|
+
:param rsmi: Reaction SMILES in ``reactants>>products`` format.
|
|
499
|
+
:type rsmi: str
|
|
500
|
+
:param arrow_code: Arrow code used to preserve relevant atom maps.
|
|
501
|
+
:type arrow_code: str
|
|
502
|
+
:param expand_aam: Whether to expand atom mapping before ITS construction.
|
|
503
|
+
:type expand_aam: bool
|
|
504
|
+
:param remove_non_arrow_maps: Whether to remove atom maps not used by the arrow code.
|
|
505
|
+
:type remove_non_arrow_maps: bool
|
|
506
|
+
:param aam_fallback_to_other_side: Retry partial-AAM reconstruction from
|
|
507
|
+
the opposite endpoint if the preferred side fails.
|
|
508
|
+
:type aam_fallback_to_other_side: bool
|
|
509
|
+
:param require_constitution_preservation: Reject AAM expansion if either
|
|
510
|
+
endpoint constitution changes.
|
|
511
|
+
:type require_constitution_preservation: bool
|
|
512
|
+
:param fold_unmapped_explicit_hydrogens: Fold removable unmapped explicit
|
|
513
|
+
H atoms before partial-AAM reconstruction while retaining mapped H.
|
|
514
|
+
:type fold_unmapped_explicit_hydrogens: bool
|
|
515
|
+
:param ignore_stereochemistry: Remove stereo from the working partial-AAM
|
|
516
|
+
RSMI before ITS construction while retaining the original endpoint
|
|
517
|
+
constitution as the guard reference.
|
|
518
|
+
:type ignore_stereochemistry: bool
|
|
519
|
+
:param explicit_hydrogen: Preserve explicit H graph nodes during expanded
|
|
520
|
+
RSMI serialization.
|
|
521
|
+
:type explicit_hydrogen: bool
|
|
522
|
+
:param preserve_radical_state: Preserve supplied endpoint radical counts
|
|
523
|
+
during partial-AAM reconstruction.
|
|
524
|
+
:type preserve_radical_state: bool
|
|
525
|
+
:return: ITS graph, expanded RSMI, cleaned RSMI, and validation diagnostics.
|
|
526
|
+
:rtype: tuple
|
|
527
|
+
:raises ImportError: If required SynKit conversion helpers are unavailable.
|
|
528
|
+
"""
|
|
529
|
+
if CanonRSMI is None or rsmi_to_its is None:
|
|
530
|
+
raise ImportError(
|
|
531
|
+
"SynKit conversion helpers are unavailable in this environment."
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
diagnostics = validate_arrow_maps(
|
|
535
|
+
rsmi=rsmi,
|
|
536
|
+
arrow_code=arrow_code,
|
|
537
|
+
raise_on_arrow_duplicates=True,
|
|
538
|
+
raise_on_missing_arrow_maps=True,
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
if remove_non_arrow_maps:
|
|
542
|
+
rsmi_for_its = remove_non_arrow_atom_maps(rsmi, arrow_code)
|
|
543
|
+
else:
|
|
544
|
+
rsmi_for_its = rsmi
|
|
545
|
+
|
|
546
|
+
if expand_aam:
|
|
547
|
+
expansion = ITSExpand().expand_aam_with_its_report(
|
|
548
|
+
rsmi_for_its,
|
|
549
|
+
relabel=False,
|
|
550
|
+
preserve_older_map=True,
|
|
551
|
+
fallback_to_other_side=aam_fallback_to_other_side,
|
|
552
|
+
require_constitution_preservation=require_constitution_preservation,
|
|
553
|
+
fold_unmapped_explicit_hydrogens=fold_unmapped_explicit_hydrogens,
|
|
554
|
+
ignore_stereochemistry=ignore_stereochemistry,
|
|
555
|
+
explicit_hydrogen=explicit_hydrogen,
|
|
556
|
+
preserve_radical_state=preserve_radical_state,
|
|
557
|
+
)
|
|
558
|
+
expanded_rsmi = expansion.rsmi
|
|
559
|
+
diagnostics["aam_expansion"] = expansion.to_dict()
|
|
560
|
+
else:
|
|
561
|
+
expanded_rsmi = rsmi_for_its
|
|
562
|
+
diagnostics["aam_expansion"] = {
|
|
563
|
+
"performed": False,
|
|
564
|
+
"constitution_checked": False,
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
its = rsmi_to_its(expanded_rsmi)
|
|
568
|
+
endpoint_states = _endpoint_electron_states(expanded_rsmi)
|
|
569
|
+
if endpoint_states is not None:
|
|
570
|
+
# ``typesGH`` retains endpoint charge/H presentation but not the
|
|
571
|
+
# endpoint-local lone-pair/radical partition. Keep that partition as
|
|
572
|
+
# conversion evidence on this transient ITS graph so compact arrow
|
|
573
|
+
# annotations can be typed from the actual pre/post resources.
|
|
574
|
+
its.graph["endpoint_electron_states"] = endpoint_states
|
|
575
|
+
diagnostics["endpoint_electron_states_available"] = True
|
|
576
|
+
else:
|
|
577
|
+
diagnostics["endpoint_electron_states_available"] = False
|
|
578
|
+
|
|
579
|
+
return its, expanded_rsmi, rsmi_for_its, diagnostics
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def _endpoint_electron_states(
|
|
583
|
+
mapped_reaction: str,
|
|
584
|
+
) -> dict[int, tuple[dict[str, Any], dict[str, Any]]] | None:
|
|
585
|
+
"""Return mapped pre/post nonbonding state for state-aware arrow typing."""
|
|
586
|
+
try:
|
|
587
|
+
from rdkit import Chem
|
|
588
|
+
|
|
589
|
+
from synkit.IO.mol_to_graph import MolToGraph
|
|
590
|
+
|
|
591
|
+
reactants, products = mapped_reaction.split(">>", 1)
|
|
592
|
+
parser = Chem.SmilesParserParams()
|
|
593
|
+
parser.removeHs = False
|
|
594
|
+
sides: list[dict[int, dict[str, Any]]] = []
|
|
595
|
+
for smiles in (reactants, products):
|
|
596
|
+
mol = Chem.MolFromSmiles(smiles, parser)
|
|
597
|
+
if mol is None:
|
|
598
|
+
return None
|
|
599
|
+
states: dict[int, dict[str, Any]] = {}
|
|
600
|
+
for atom in mol.GetAtoms():
|
|
601
|
+
atom_map = int(atom.GetAtomMapNum())
|
|
602
|
+
if atom_map <= 0:
|
|
603
|
+
continue
|
|
604
|
+
if atom_map in states:
|
|
605
|
+
return None
|
|
606
|
+
states[atom_map] = {
|
|
607
|
+
"element": atom.GetSymbol(),
|
|
608
|
+
"charge": int(atom.GetFormalCharge()),
|
|
609
|
+
"radical": int(atom.GetNumRadicalElectrons()),
|
|
610
|
+
"lone_pairs": int(MolToGraph.estimate_lone_pairs(atom)),
|
|
611
|
+
}
|
|
612
|
+
sides.append(states)
|
|
613
|
+
if set(sides[0]) != set(sides[1]):
|
|
614
|
+
return None
|
|
615
|
+
return {
|
|
616
|
+
atom_map: (sides[0][atom_map], sides[1][atom_map]) for atom_map in sides[0]
|
|
617
|
+
}
|
|
618
|
+
except (ImportError, TypeError, ValueError):
|
|
619
|
+
return None
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
# ============================================================
|
|
623
|
+
# ITS graph helpers
|
|
624
|
+
# ============================================================
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def atom_map_to_nodes(its) -> dict[int, list[Any]]:
|
|
628
|
+
"""Build atom-map-number -> list of ITS node ids.
|
|
629
|
+
|
|
630
|
+
This catches ambiguous duplicated atom maps after ITS construction.
|
|
631
|
+
|
|
632
|
+
:param its: ITS graph.
|
|
633
|
+
:type its: networkx.Graph
|
|
634
|
+
:return: Mapping from atom-map number to ITS node IDs.
|
|
635
|
+
:rtype: dict[int, list[Any]]
|
|
636
|
+
"""
|
|
637
|
+
mapping: dict[int, list[Any]] = {}
|
|
638
|
+
|
|
639
|
+
for node, data in its.nodes(data=True):
|
|
640
|
+
atom_map = int(data.get("atom_map", node))
|
|
641
|
+
mapping.setdefault(atom_map, []).append(node)
|
|
642
|
+
|
|
643
|
+
return mapping
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def get_unique_node_for_atom_map(
|
|
647
|
+
its,
|
|
648
|
+
atom_map: int,
|
|
649
|
+
strict: bool = True,
|
|
650
|
+
atom_map_nodes: Optional[dict[int, list[Any]]] = None,
|
|
651
|
+
) -> Optional[Any]:
|
|
652
|
+
"""Get the unique ITS node corresponding to an atom map.
|
|
653
|
+
|
|
654
|
+
:param its: ITS graph.
|
|
655
|
+
:type its: networkx.Graph
|
|
656
|
+
:param atom_map: Atom-map number to resolve.
|
|
657
|
+
:type atom_map: int
|
|
658
|
+
:param strict: Whether a missing atom map should raise.
|
|
659
|
+
:type strict: bool
|
|
660
|
+
:param atom_map_nodes: Optional precomputed atom-map to node index.
|
|
661
|
+
:type atom_map_nodes: Optional[dict[int, list[Any]]]
|
|
662
|
+
:return: Unique ITS node ID, or ``None`` when missing and ``strict`` is false.
|
|
663
|
+
:rtype: Optional[Any]
|
|
664
|
+
:raises ValueError: If the atom map is missing in strict mode or is ambiguous.
|
|
665
|
+
"""
|
|
666
|
+
mapping = atom_map_nodes if atom_map_nodes is not None else atom_map_to_nodes(its)
|
|
667
|
+
nodes = mapping.get(int(atom_map), [])
|
|
668
|
+
|
|
669
|
+
if len(nodes) == 0:
|
|
670
|
+
if strict:
|
|
671
|
+
raise ValueError(f"Atom map {atom_map} is missing from ITS graph.")
|
|
672
|
+
return None
|
|
673
|
+
|
|
674
|
+
if len(nodes) == 1:
|
|
675
|
+
return nodes[0]
|
|
676
|
+
|
|
677
|
+
raise ValueError(
|
|
678
|
+
f"Atom map {atom_map} maps to multiple ITS nodes: {nodes}. "
|
|
679
|
+
"This means atom mapping is ambiguous."
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def extract_order_from_edge_data(edge_data: Any) -> tuple[float, float]:
|
|
684
|
+
"""Extract SynKit ITS edge order.
|
|
685
|
+
|
|
686
|
+
Expected normal edge format:
|
|
687
|
+
{"order": (reactant_order, product_order)}
|
|
688
|
+
|
|
689
|
+
MultiGraph-like fallback:
|
|
690
|
+
{0: {"order": (reactant_order, product_order)}}
|
|
691
|
+
|
|
692
|
+
:param edge_data: ITS edge attributes.
|
|
693
|
+
:type edge_data: Any
|
|
694
|
+
:return: Reactant-side and product-side bond orders.
|
|
695
|
+
:rtype: tuple[float, float]
|
|
696
|
+
"""
|
|
697
|
+
if edge_data is None:
|
|
698
|
+
return 0.0, 0.0
|
|
699
|
+
|
|
700
|
+
if isinstance(edge_data, dict) and "order" in edge_data:
|
|
701
|
+
order = edge_data["order"]
|
|
702
|
+
return float(order[0]), float(order[1])
|
|
703
|
+
|
|
704
|
+
if isinstance(edge_data, dict):
|
|
705
|
+
for value in edge_data.values():
|
|
706
|
+
if isinstance(value, dict) and "order" in value:
|
|
707
|
+
order = value["order"]
|
|
708
|
+
return float(order[0]), float(order[1])
|
|
709
|
+
|
|
710
|
+
return 0.0, 0.0
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def get_its_bond_order(
|
|
714
|
+
its,
|
|
715
|
+
atom_a: int,
|
|
716
|
+
atom_b: int,
|
|
717
|
+
strict: bool = True,
|
|
718
|
+
context: str = "",
|
|
719
|
+
atom_map_nodes: Optional[dict[int, list[Any]]] = None,
|
|
720
|
+
) -> tuple[float, float]:
|
|
721
|
+
"""Return ITS bond order for atom-map pair.
|
|
722
|
+
|
|
723
|
+
For example, an edge with order ``(0.0, 1.0)`` represents new bond
|
|
724
|
+
formation from reactants to products.
|
|
725
|
+
|
|
726
|
+
:param its: ITS graph.
|
|
727
|
+
:type its: networkx.Graph
|
|
728
|
+
:param atom_a: First atom-map number.
|
|
729
|
+
:type atom_a: int
|
|
730
|
+
:param atom_b: Second atom-map number.
|
|
731
|
+
:type atom_b: int
|
|
732
|
+
:param strict: Whether missing nodes or edges should raise.
|
|
733
|
+
:type strict: bool
|
|
734
|
+
:param context: Optional context appended to strict-mode edge errors.
|
|
735
|
+
:type context: str
|
|
736
|
+
:param atom_map_nodes: Optional precomputed atom-map to node index.
|
|
737
|
+
:type atom_map_nodes: Optional[dict[int, list[Any]]]
|
|
738
|
+
:return: Reactant-side and product-side bond orders.
|
|
739
|
+
:rtype: tuple[float, float]
|
|
740
|
+
:raises ValueError: If strict lookup fails.
|
|
741
|
+
"""
|
|
742
|
+
node_a = get_unique_node_for_atom_map(
|
|
743
|
+
its,
|
|
744
|
+
atom_a,
|
|
745
|
+
strict=strict,
|
|
746
|
+
atom_map_nodes=atom_map_nodes,
|
|
747
|
+
)
|
|
748
|
+
node_b = get_unique_node_for_atom_map(
|
|
749
|
+
its,
|
|
750
|
+
atom_b,
|
|
751
|
+
strict=strict,
|
|
752
|
+
atom_map_nodes=atom_map_nodes,
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
if node_a is None or node_b is None:
|
|
756
|
+
return 0.0, 0.0
|
|
757
|
+
|
|
758
|
+
if not its.has_edge(node_a, node_b):
|
|
759
|
+
if strict:
|
|
760
|
+
extra = f" Context: {context}" if context else ""
|
|
761
|
+
raise ValueError(
|
|
762
|
+
f"ITS graph has no edge for atom maps {atom_a}-{atom_b}." f"{extra}"
|
|
763
|
+
)
|
|
764
|
+
return 0.0, 0.0
|
|
765
|
+
|
|
766
|
+
edge_data = its.get_edge_data(node_a, node_b)
|
|
767
|
+
return extract_order_from_edge_data(edge_data)
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
# ============================================================
|
|
771
|
+
# Sigma/Pi typing
|
|
772
|
+
# ============================================================
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
def is_zero(x: float, tol: float = 1e-6) -> bool:
|
|
776
|
+
"""Return whether a value is approximately zero.
|
|
777
|
+
|
|
778
|
+
:param x: Value to compare.
|
|
779
|
+
:type x: float
|
|
780
|
+
:param tol: Absolute tolerance.
|
|
781
|
+
:type tol: float
|
|
782
|
+
:return: Whether ``x`` is within tolerance of zero.
|
|
783
|
+
:rtype: bool
|
|
784
|
+
"""
|
|
785
|
+
return abs(x) < tol
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
def is_one(x: float, tol: float = 1e-6) -> bool:
|
|
789
|
+
"""Return whether a value is approximately one.
|
|
790
|
+
|
|
791
|
+
:param x: Value to compare.
|
|
792
|
+
:type x: float
|
|
793
|
+
:param tol: Absolute tolerance.
|
|
794
|
+
:type tol: float
|
|
795
|
+
:return: Whether ``x`` is within tolerance of one.
|
|
796
|
+
:rtype: bool
|
|
797
|
+
"""
|
|
798
|
+
return abs(x - 1.0) < tol
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def bond_minus_type(reactant_order: float) -> str:
|
|
802
|
+
"""Type consumed bond/electron-pair source.
|
|
803
|
+
|
|
804
|
+
A bond order of one maps to ``Sigma-``; larger orders, including aromatic
|
|
805
|
+
order 1.5, map to ``Pi-``. Unknown orders map to ``B-``.
|
|
806
|
+
|
|
807
|
+
:param reactant_order: Bond order on the reactant side.
|
|
808
|
+
:type reactant_order: float
|
|
809
|
+
:return: Typed consumed-bond label.
|
|
810
|
+
:rtype: str
|
|
811
|
+
"""
|
|
812
|
+
if is_one(reactant_order):
|
|
813
|
+
return "Sigma-"
|
|
814
|
+
|
|
815
|
+
if reactant_order > 1.0:
|
|
816
|
+
return "Pi-"
|
|
817
|
+
|
|
818
|
+
return "B-"
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
def bond_plus_type(
|
|
822
|
+
reactant_order: float,
|
|
823
|
+
product_order: float,
|
|
824
|
+
) -> str:
|
|
825
|
+
"""Type formed/increased bond destination.
|
|
826
|
+
|
|
827
|
+
Rules
|
|
828
|
+
-----
|
|
829
|
+
0 -> 1 : Sigma+
|
|
830
|
+
0 -> 1.5 : Sigma+, because new connectivity starts as sigma
|
|
831
|
+
0 -> 2 : Sigma+, because new connectivity starts as sigma
|
|
832
|
+
1 -> 2 : Pi+
|
|
833
|
+
1.5 -> 2 : Pi+
|
|
834
|
+
2 -> 3 : Pi+
|
|
835
|
+
|
|
836
|
+
:param reactant_order: Bond order on the reactant side.
|
|
837
|
+
:type reactant_order: float
|
|
838
|
+
:param product_order: Bond order on the product side.
|
|
839
|
+
:type product_order: float
|
|
840
|
+
:return: Typed formed-bond label.
|
|
841
|
+
:rtype: str
|
|
842
|
+
"""
|
|
843
|
+
if product_order <= 0:
|
|
844
|
+
return "B+"
|
|
845
|
+
|
|
846
|
+
# New bond formation. First new connectivity is sigma.
|
|
847
|
+
if is_zero(reactant_order) and product_order > 0:
|
|
848
|
+
return "Sigma+"
|
|
849
|
+
|
|
850
|
+
# Existing bond order increases. Added component is pi.
|
|
851
|
+
if product_order > reactant_order:
|
|
852
|
+
return "Pi+"
|
|
853
|
+
|
|
854
|
+
# Fallbacks.
|
|
855
|
+
if is_one(product_order):
|
|
856
|
+
return "Sigma+"
|
|
857
|
+
|
|
858
|
+
if product_order > 1.0:
|
|
859
|
+
return "Pi+"
|
|
860
|
+
|
|
861
|
+
return "B+"
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
def _infer_atom_locus_kind(
|
|
865
|
+
its,
|
|
866
|
+
atom_map: int,
|
|
867
|
+
*,
|
|
868
|
+
role: Literal["source", "target"],
|
|
869
|
+
electron_count: Literal[1, 2],
|
|
870
|
+
) -> str | None:
|
|
871
|
+
"""Infer an atom locus from its complete endpoint resource transition.
|
|
872
|
+
|
|
873
|
+
Compact EPD/EF-SMIRKS identifies atom-map endpoints but does not say
|
|
874
|
+
whether a one-atom fishhook touches a lone pair or a radical orbital. The
|
|
875
|
+
pre/post lone-pair and radical deltas disambiguate the supported cases.
|
|
876
|
+
"""
|
|
877
|
+
endpoint_states = its.graph.get("endpoint_electron_states")
|
|
878
|
+
if not isinstance(endpoint_states, dict) or atom_map not in endpoint_states:
|
|
879
|
+
return None
|
|
880
|
+
reactant, product = endpoint_states[atom_map]
|
|
881
|
+
lp_delta = int(product["lone_pairs"]) - int(reactant["lone_pairs"])
|
|
882
|
+
radical_delta = int(product["radical"]) - int(reactant["radical"])
|
|
883
|
+
electron_delta = 2 * lp_delta + radical_delta
|
|
884
|
+
expected_delta = -electron_count if role == "source" else electron_count
|
|
885
|
+
if electron_delta != expected_delta:
|
|
886
|
+
return None
|
|
887
|
+
|
|
888
|
+
if electron_count == 2:
|
|
889
|
+
if radical_delta == 0 and lp_delta == (-1 if role == "source" else 1):
|
|
890
|
+
return "LP"
|
|
891
|
+
return None
|
|
892
|
+
|
|
893
|
+
if role == "source":
|
|
894
|
+
if radical_delta == -1 and lp_delta == 0:
|
|
895
|
+
return "Rad"
|
|
896
|
+
if radical_delta == 1 and lp_delta == -1:
|
|
897
|
+
return "LP"
|
|
898
|
+
else:
|
|
899
|
+
if radical_delta == 1 and lp_delta == 0:
|
|
900
|
+
return "Rad"
|
|
901
|
+
if radical_delta == -1 and lp_delta == 1:
|
|
902
|
+
return "LP"
|
|
903
|
+
return None
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
def _infer_atom_to_atom_step(
|
|
907
|
+
its,
|
|
908
|
+
source_map: int,
|
|
909
|
+
target_map: int,
|
|
910
|
+
*,
|
|
911
|
+
electron_count: Literal[1, 2],
|
|
912
|
+
) -> list[Any] | None:
|
|
913
|
+
"""Resolve a compact ``a=b`` step as a nonbonding atom transfer."""
|
|
914
|
+
source_kind = _infer_atom_locus_kind(
|
|
915
|
+
its, source_map, role="source", electron_count=electron_count
|
|
916
|
+
)
|
|
917
|
+
target_kind = _infer_atom_locus_kind(
|
|
918
|
+
its, target_map, role="target", electron_count=electron_count
|
|
919
|
+
)
|
|
920
|
+
if source_kind is None or target_kind is None:
|
|
921
|
+
return None
|
|
922
|
+
return [f"{source_kind}-/{target_kind}+", [source_map], [target_map]]
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
# ============================================================
|
|
926
|
+
# Typed LP/Sigma/Pi conversion
|
|
927
|
+
# ============================================================
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
def typed_convert_step(
|
|
931
|
+
step: str,
|
|
932
|
+
its,
|
|
933
|
+
strict_bond_lookup: bool = True,
|
|
934
|
+
atom_map_nodes: Optional[dict[int, list[Any]]] = None,
|
|
935
|
+
electron_count: Literal[1, 2] = 2,
|
|
936
|
+
) -> list[Any]:
|
|
937
|
+
"""Convert one arrow-code step into typed LP/Sigma/Pi format.
|
|
938
|
+
|
|
939
|
+
Important
|
|
940
|
+
---------
|
|
941
|
+
This function does NOT globally force Sigma/Pi from orbital_class.
|
|
942
|
+
Each step is typed from local ITS bond-order changes.
|
|
943
|
+
|
|
944
|
+
:param step: One arrow-code step.
|
|
945
|
+
:type step: str
|
|
946
|
+
:param its: ITS graph used for local bond-order lookup.
|
|
947
|
+
:type its: networkx.Graph
|
|
948
|
+
:param strict_bond_lookup: Whether missing bond lookups should raise.
|
|
949
|
+
:type strict_bond_lookup: bool
|
|
950
|
+
:param atom_map_nodes: Optional precomputed atom-map to node index.
|
|
951
|
+
:type atom_map_nodes: Optional[dict[int, list[Any]]]
|
|
952
|
+
:param electron_count: Electrons carried by the annotated arrow.
|
|
953
|
+
:type electron_count: Literal[1, 2]
|
|
954
|
+
:return: Typed LP/Sigma/Pi conversion record.
|
|
955
|
+
:rtype: list[Any]
|
|
956
|
+
:raises ValueError: If the step shape is unsupported or strict lookup fails.
|
|
957
|
+
"""
|
|
958
|
+
lhs, rhs = parse_arrow_step(step)
|
|
959
|
+
|
|
960
|
+
# --------------------------------------------------------
|
|
961
|
+
# Case 1: a=b
|
|
962
|
+
# LP(a) forms bond a-b
|
|
963
|
+
# --------------------------------------------------------
|
|
964
|
+
if len(lhs) == 1 and len(rhs) == 1:
|
|
965
|
+
a = lhs[0]
|
|
966
|
+
b = rhs[0]
|
|
967
|
+
|
|
968
|
+
node_a = get_unique_node_for_atom_map(
|
|
969
|
+
its,
|
|
970
|
+
a,
|
|
971
|
+
strict=strict_bond_lookup,
|
|
972
|
+
atom_map_nodes=atom_map_nodes,
|
|
973
|
+
)
|
|
974
|
+
node_b = get_unique_node_for_atom_map(
|
|
975
|
+
its,
|
|
976
|
+
b,
|
|
977
|
+
strict=strict_bond_lookup,
|
|
978
|
+
atom_map_nodes=atom_map_nodes,
|
|
979
|
+
)
|
|
980
|
+
has_bond = (
|
|
981
|
+
node_a is not None and node_b is not None and its.has_edge(node_a, node_b)
|
|
982
|
+
)
|
|
983
|
+
|
|
984
|
+
r_order, p_order = get_its_bond_order(
|
|
985
|
+
its,
|
|
986
|
+
a,
|
|
987
|
+
b,
|
|
988
|
+
strict=False,
|
|
989
|
+
context=step,
|
|
990
|
+
atom_map_nodes=atom_map_nodes,
|
|
991
|
+
)
|
|
992
|
+
if is_zero(p_order - r_order):
|
|
993
|
+
atom_transfer = _infer_atom_to_atom_step(
|
|
994
|
+
its, a, b, electron_count=electron_count
|
|
995
|
+
)
|
|
996
|
+
if atom_transfer is not None:
|
|
997
|
+
return atom_transfer
|
|
998
|
+
if strict_bond_lookup and not has_bond:
|
|
999
|
+
raise ValueError(
|
|
1000
|
+
f"ITS graph has no edge for atom maps {a}-{b}. "
|
|
1001
|
+
"Endpoint states do not support an atom-to-atom transfer."
|
|
1002
|
+
)
|
|
1003
|
+
if strict_bond_lookup and "endpoint_electron_states" in its.graph:
|
|
1004
|
+
raise ValueError(
|
|
1005
|
+
"ATOM_TRANSFER_STATE_MISMATCH: one-atom endpoints neither change "
|
|
1006
|
+
f"bond {a}-{b} nor match a supported nonbonding resource "
|
|
1007
|
+
"transfer."
|
|
1008
|
+
)
|
|
1009
|
+
elif p_order < r_order and strict_bond_lookup:
|
|
1010
|
+
raise ValueError(
|
|
1011
|
+
f"ARROW_TARGET_BOND_NOT_FORMED: bond {a}-{b} decreases from "
|
|
1012
|
+
f"{r_order} to {p_order}."
|
|
1013
|
+
)
|
|
1014
|
+
plus = bond_plus_type(r_order, p_order)
|
|
1015
|
+
source_kind = (
|
|
1016
|
+
_infer_atom_locus_kind(its, a, role="source", electron_count=electron_count)
|
|
1017
|
+
or "LP"
|
|
1018
|
+
)
|
|
1019
|
+
|
|
1020
|
+
return [f"{source_kind}-/{plus}", [a], [a, b]]
|
|
1021
|
+
|
|
1022
|
+
# --------------------------------------------------------
|
|
1023
|
+
# Case 2: a=b,c
|
|
1024
|
+
# LP(a) forms/increases bond b-c
|
|
1025
|
+
#
|
|
1026
|
+
# Example:
|
|
1027
|
+
# 12=11,12
|
|
1028
|
+
# LP on 12 forms/increases 11-12 bond
|
|
1029
|
+
# --------------------------------------------------------
|
|
1030
|
+
if len(lhs) == 1 and len(rhs) == 2:
|
|
1031
|
+
a = lhs[0]
|
|
1032
|
+
b, c = rhs
|
|
1033
|
+
|
|
1034
|
+
r_order, p_order = get_its_bond_order(
|
|
1035
|
+
its,
|
|
1036
|
+
b,
|
|
1037
|
+
c,
|
|
1038
|
+
strict=strict_bond_lookup,
|
|
1039
|
+
context=step,
|
|
1040
|
+
atom_map_nodes=atom_map_nodes,
|
|
1041
|
+
)
|
|
1042
|
+
plus = bond_plus_type(r_order, p_order)
|
|
1043
|
+
source_kind = (
|
|
1044
|
+
_infer_atom_locus_kind(its, a, role="source", electron_count=electron_count)
|
|
1045
|
+
or "LP"
|
|
1046
|
+
)
|
|
1047
|
+
|
|
1048
|
+
return [f"{source_kind}-/{plus}", [a], [b, c]]
|
|
1049
|
+
|
|
1050
|
+
# --------------------------------------------------------
|
|
1051
|
+
# Case 3: a,b=c
|
|
1052
|
+
# bond a-b breaks; electrons become LP on c
|
|
1053
|
+
# --------------------------------------------------------
|
|
1054
|
+
if len(lhs) == 2 and len(rhs) == 1:
|
|
1055
|
+
a, b = lhs
|
|
1056
|
+
c = rhs[0]
|
|
1057
|
+
|
|
1058
|
+
r_order, _p_order = get_its_bond_order(
|
|
1059
|
+
its,
|
|
1060
|
+
a,
|
|
1061
|
+
b,
|
|
1062
|
+
strict=strict_bond_lookup,
|
|
1063
|
+
context=step,
|
|
1064
|
+
atom_map_nodes=atom_map_nodes,
|
|
1065
|
+
)
|
|
1066
|
+
minus = bond_minus_type(r_order)
|
|
1067
|
+
target_kind = (
|
|
1068
|
+
_infer_atom_locus_kind(its, c, role="target", electron_count=electron_count)
|
|
1069
|
+
or "LP"
|
|
1070
|
+
)
|
|
1071
|
+
|
|
1072
|
+
return [f"{minus}/{target_kind}+", [a, b], [c]]
|
|
1073
|
+
|
|
1074
|
+
# --------------------------------------------------------
|
|
1075
|
+
# Case 4: a,b=c,d
|
|
1076
|
+
# bond a-b becomes bond c-d
|
|
1077
|
+
# --------------------------------------------------------
|
|
1078
|
+
if len(lhs) == 2 and len(rhs) == 2:
|
|
1079
|
+
a, b = lhs
|
|
1080
|
+
c, d = rhs
|
|
1081
|
+
|
|
1082
|
+
src_r_order, _src_p_order = get_its_bond_order(
|
|
1083
|
+
its,
|
|
1084
|
+
a,
|
|
1085
|
+
b,
|
|
1086
|
+
strict=strict_bond_lookup,
|
|
1087
|
+
context=f"source of {step}",
|
|
1088
|
+
atom_map_nodes=atom_map_nodes,
|
|
1089
|
+
)
|
|
1090
|
+
|
|
1091
|
+
dst_r_order, dst_p_order = get_its_bond_order(
|
|
1092
|
+
its,
|
|
1093
|
+
c,
|
|
1094
|
+
d,
|
|
1095
|
+
strict=strict_bond_lookup,
|
|
1096
|
+
context=f"destination of {step}",
|
|
1097
|
+
atom_map_nodes=atom_map_nodes,
|
|
1098
|
+
)
|
|
1099
|
+
|
|
1100
|
+
minus = bond_minus_type(src_r_order)
|
|
1101
|
+
plus = bond_plus_type(dst_r_order, dst_p_order)
|
|
1102
|
+
|
|
1103
|
+
return [f"{minus}/{plus}", [a, b], [c, d]]
|
|
1104
|
+
|
|
1105
|
+
raise ValueError(f"Unsupported arrow step: {step}")
|
|
1106
|
+
|
|
1107
|
+
|
|
1108
|
+
def typed_convert_arrow_code(
|
|
1109
|
+
arrow_code: str,
|
|
1110
|
+
its,
|
|
1111
|
+
strict_bond_lookup: bool = True,
|
|
1112
|
+
electron_count: Literal[1, 2] = 2,
|
|
1113
|
+
) -> list[list[Any]]:
|
|
1114
|
+
"""Convert every step in an arrow code to typed LP/Sigma/Pi form.
|
|
1115
|
+
|
|
1116
|
+
:param arrow_code: Semicolon-separated arrow code.
|
|
1117
|
+
:type arrow_code: str
|
|
1118
|
+
:param its: ITS graph used for local bond-order lookup.
|
|
1119
|
+
:type its: networkx.Graph
|
|
1120
|
+
:param strict_bond_lookup: Whether missing bond lookups should raise.
|
|
1121
|
+
:type strict_bond_lookup: bool
|
|
1122
|
+
:param electron_count: Electrons carried by every supplied arrow.
|
|
1123
|
+
:type electron_count: Literal[1, 2]
|
|
1124
|
+
:return: Typed conversion records for each step.
|
|
1125
|
+
:rtype: list[list[Any]]
|
|
1126
|
+
"""
|
|
1127
|
+
atom_map_nodes = atom_map_to_nodes(its)
|
|
1128
|
+
|
|
1129
|
+
return [
|
|
1130
|
+
typed_convert_step(
|
|
1131
|
+
step=step,
|
|
1132
|
+
its=its,
|
|
1133
|
+
strict_bond_lookup=strict_bond_lookup,
|
|
1134
|
+
atom_map_nodes=atom_map_nodes,
|
|
1135
|
+
electron_count=electron_count,
|
|
1136
|
+
)
|
|
1137
|
+
for step in split_arrow_code(arrow_code)
|
|
1138
|
+
]
|
|
1139
|
+
|
|
1140
|
+
|
|
1141
|
+
# ============================================================
|
|
1142
|
+
# Main public conversion functions
|
|
1143
|
+
# ============================================================
|
|
1144
|
+
|
|
1145
|
+
|
|
1146
|
+
def convert_arrow_code(
|
|
1147
|
+
arrow_code: str,
|
|
1148
|
+
its=None,
|
|
1149
|
+
strict_bond_lookup: bool = True,
|
|
1150
|
+
) -> dict[str, Any]:
|
|
1151
|
+
"""Convert arrow code into generic and typed formats.
|
|
1152
|
+
|
|
1153
|
+
If ``its`` is ``None``, ``typed_converted`` is ``None``.
|
|
1154
|
+
|
|
1155
|
+
:param arrow_code: Semicolon-separated arrow code.
|
|
1156
|
+
:type arrow_code: str
|
|
1157
|
+
:param its: Optional ITS graph for typed conversion.
|
|
1158
|
+
:type its: Optional[networkx.Graph]
|
|
1159
|
+
:param strict_bond_lookup: Whether missing typed bond lookups should raise.
|
|
1160
|
+
:type strict_bond_lookup: bool
|
|
1161
|
+
:return: Arrow code with generic and optional typed conversions.
|
|
1162
|
+
:rtype: dict[str, Any]
|
|
1163
|
+
"""
|
|
1164
|
+
converted = generic_convert_arrow_code(arrow_code)
|
|
1165
|
+
|
|
1166
|
+
if its is None:
|
|
1167
|
+
typed_converted = None
|
|
1168
|
+
else:
|
|
1169
|
+
typed_converted = typed_convert_arrow_code(
|
|
1170
|
+
arrow_code=arrow_code,
|
|
1171
|
+
its=its,
|
|
1172
|
+
strict_bond_lookup=strict_bond_lookup,
|
|
1173
|
+
)
|
|
1174
|
+
|
|
1175
|
+
return {
|
|
1176
|
+
"arrow_code": arrow_code,
|
|
1177
|
+
"converted": converted,
|
|
1178
|
+
"typed_converted": typed_converted,
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
|
|
1182
|
+
def convert_reaction_arrow(
|
|
1183
|
+
reaction_smiles: str,
|
|
1184
|
+
arrow_code: str,
|
|
1185
|
+
orbital_class: Optional[str] = None,
|
|
1186
|
+
expand_aam: bool = True,
|
|
1187
|
+
remove_non_arrow_maps: bool = True,
|
|
1188
|
+
strict_bond_lookup: bool = True,
|
|
1189
|
+
) -> dict[str, Any]:
|
|
1190
|
+
"""Complete wrapper.
|
|
1191
|
+
|
|
1192
|
+
reaction SMILES + arrow code
|
|
1193
|
+
-> clean non-arrow maps
|
|
1194
|
+
-> expand AAM with SynKit
|
|
1195
|
+
-> ITS graph
|
|
1196
|
+
-> generic converted
|
|
1197
|
+
-> typed converted
|
|
1198
|
+
|
|
1199
|
+
orbital_class is stored as metadata only.
|
|
1200
|
+
It is not used to force Sigma/Pi typing.
|
|
1201
|
+
|
|
1202
|
+
:param reaction_smiles: Reaction SMILES in ``reactants>>products`` format.
|
|
1203
|
+
:type reaction_smiles: str
|
|
1204
|
+
:param arrow_code: Semicolon-separated arrow code.
|
|
1205
|
+
:type arrow_code: str
|
|
1206
|
+
:param orbital_class: Optional source-dataset orbital classification metadata.
|
|
1207
|
+
:type orbital_class: Optional[str]
|
|
1208
|
+
:param expand_aam: Whether to expand atom mapping before ITS construction.
|
|
1209
|
+
:type expand_aam: bool
|
|
1210
|
+
:param remove_non_arrow_maps: Whether to remove atom maps not used by the arrow code.
|
|
1211
|
+
:type remove_non_arrow_maps: bool
|
|
1212
|
+
:param strict_bond_lookup: Whether missing typed bond lookups should raise.
|
|
1213
|
+
:type strict_bond_lookup: bool
|
|
1214
|
+
:return: Conversion result and ITS preparation metadata.
|
|
1215
|
+
:rtype: dict[str, Any]
|
|
1216
|
+
"""
|
|
1217
|
+
its, expanded_rsmi, rsmi_for_its, diagnostics = build_its_from_rsmi(
|
|
1218
|
+
rsmi=reaction_smiles,
|
|
1219
|
+
arrow_code=arrow_code,
|
|
1220
|
+
expand_aam=expand_aam,
|
|
1221
|
+
remove_non_arrow_maps=remove_non_arrow_maps,
|
|
1222
|
+
)
|
|
1223
|
+
|
|
1224
|
+
result = convert_arrow_code(
|
|
1225
|
+
arrow_code=arrow_code,
|
|
1226
|
+
its=its,
|
|
1227
|
+
strict_bond_lookup=strict_bond_lookup,
|
|
1228
|
+
)
|
|
1229
|
+
|
|
1230
|
+
result["reaction_smiles"] = reaction_smiles
|
|
1231
|
+
result["rsmi_for_its"] = rsmi_for_its
|
|
1232
|
+
result["expanded_rsmi"] = expanded_rsmi
|
|
1233
|
+
result["orbital_class"] = orbital_class
|
|
1234
|
+
result["diagnostics"] = diagnostics
|
|
1235
|
+
|
|
1236
|
+
return result
|
|
1237
|
+
|
|
1238
|
+
|
|
1239
|
+
def ef_smirks_to_epd(
|
|
1240
|
+
ef_smirks: str,
|
|
1241
|
+
orbital_class: Optional[str] = None,
|
|
1242
|
+
strict_bond_lookup: bool = True,
|
|
1243
|
+
) -> dict[str, Any]:
|
|
1244
|
+
"""Complete AAM and convert an EF-SMIRKS string into EPD data.
|
|
1245
|
+
|
|
1246
|
+
The supplied atom maps used by the electron-flow code are preserved. All
|
|
1247
|
+
remaining atoms receive maps through :class:`ITSExpand`. The result
|
|
1248
|
+
contains both generic ``epd`` and Lewis-wavefunction-aware ``epd_lw``
|
|
1249
|
+
records; the latter uses local ITS bond orders to distinguish sigma and pi
|
|
1250
|
+
edits.
|
|
1251
|
+
|
|
1252
|
+
:param ef_smirks: RSMI followed by a hyphen-form electron-flow code.
|
|
1253
|
+
:type ef_smirks: str
|
|
1254
|
+
:param orbital_class: Optional source-dataset metadata.
|
|
1255
|
+
:type orbital_class: Optional[str]
|
|
1256
|
+
:param strict_bond_lookup: Whether missing ITS bond lookups should raise.
|
|
1257
|
+
:type strict_bond_lookup: bool
|
|
1258
|
+
:return: Completed AAM, generic EPD, typed ``epd_lw``, and diagnostics.
|
|
1259
|
+
:rtype: dict[str, Any]
|
|
1260
|
+
"""
|
|
1261
|
+
reaction_smiles, ef_arrow_code = split_ef_smirks(ef_smirks)
|
|
1262
|
+
arrow_code = ef_arrow_code_to_arrow_code(ef_arrow_code)
|
|
1263
|
+
result = convert_reaction_arrow(
|
|
1264
|
+
reaction_smiles=reaction_smiles,
|
|
1265
|
+
arrow_code=arrow_code,
|
|
1266
|
+
orbital_class=orbital_class,
|
|
1267
|
+
expand_aam=True,
|
|
1268
|
+
remove_non_arrow_maps=True,
|
|
1269
|
+
strict_bond_lookup=strict_bond_lookup,
|
|
1270
|
+
)
|
|
1271
|
+
result.update(
|
|
1272
|
+
{
|
|
1273
|
+
"ef_smirks": ef_smirks,
|
|
1274
|
+
"ef_arrow_code": ef_arrow_code,
|
|
1275
|
+
"complete_aam": result["expanded_rsmi"],
|
|
1276
|
+
"epd": result["converted"],
|
|
1277
|
+
"epd_lw": result["typed_converted"],
|
|
1278
|
+
}
|
|
1279
|
+
)
|
|
1280
|
+
return result
|
|
1281
|
+
|
|
1282
|
+
|
|
1283
|
+
def epd_to_ef_smirks(complete_aam: str, epd: list[list[Any]]) -> str:
|
|
1284
|
+
"""Reconstruct an EF-SMIRKS string from complete AAM and EPD records.
|
|
1285
|
+
|
|
1286
|
+
``epd`` may contain generic action names (``"B-/B+"``) or typed Lewis
|
|
1287
|
+
names (``"Sigma-/Pi+"``). Only action direction and source/target map
|
|
1288
|
+
lists are needed to reconstruct the electron-flow code.
|
|
1289
|
+
|
|
1290
|
+
:param complete_aam: Fully atom-mapped reaction SMILES.
|
|
1291
|
+
:type complete_aam: str
|
|
1292
|
+
:param epd: Generic or typed EPD records of ``[action, source, target]``.
|
|
1293
|
+
:type epd: list[list[Any]]
|
|
1294
|
+
:return: EF-SMIRKS text with a hyphen-form electron-flow code.
|
|
1295
|
+
:rtype: str
|
|
1296
|
+
:raises ValueError: If AAM or EPD records are malformed or unsupported.
|
|
1297
|
+
"""
|
|
1298
|
+
if complete_aam.count(">>") != 1:
|
|
1299
|
+
raise ValueError("complete_aam must contain exactly one '>>'.")
|
|
1300
|
+
if not epd:
|
|
1301
|
+
raise ValueError("epd must contain at least one electron-pushing step.")
|
|
1302
|
+
|
|
1303
|
+
def format_maps(atom_maps: list[int]) -> str:
|
|
1304
|
+
"""Format an atom-map sequence for one EF-SMIRKS endpoint."""
|
|
1305
|
+
return ",".join(str(atom_map) for atom_map in atom_maps)
|
|
1306
|
+
|
|
1307
|
+
steps = []
|
|
1308
|
+
for record in epd:
|
|
1309
|
+
if len(record) < 3:
|
|
1310
|
+
raise ValueError(f"Invalid EPD record: {record!r}")
|
|
1311
|
+
action, source, target = record[:3]
|
|
1312
|
+
action_parts = str(action).split("/", 1)
|
|
1313
|
+
if len(action_parts) != 2:
|
|
1314
|
+
raise ValueError(f"Invalid EPD action: {action!r}")
|
|
1315
|
+
consumed, produced = action_parts
|
|
1316
|
+
source = [int(atom_map) for atom_map in source]
|
|
1317
|
+
target = [int(atom_map) for atom_map in target]
|
|
1318
|
+
|
|
1319
|
+
if consumed == "LP-" and len(source) == 1 and len(target) in {1, 2}:
|
|
1320
|
+
lhs, rhs = source, target
|
|
1321
|
+
elif produced == "LP+" and len(source) == 2 and len(target) == 1:
|
|
1322
|
+
lhs, rhs = source, target
|
|
1323
|
+
elif (
|
|
1324
|
+
consumed.endswith("-")
|
|
1325
|
+
and produced.endswith("+")
|
|
1326
|
+
and len(source) == 2
|
|
1327
|
+
and len(target) == 2
|
|
1328
|
+
):
|
|
1329
|
+
lhs, rhs = source, target
|
|
1330
|
+
else:
|
|
1331
|
+
raise ValueError(f"Unsupported EPD record for EF-SMIRKS: {record!r}")
|
|
1332
|
+
|
|
1333
|
+
steps.append(f"{format_maps(lhs)}-{format_maps(rhs)}")
|
|
1334
|
+
|
|
1335
|
+
return f"{complete_aam} {';'.join(steps)}"
|
|
1336
|
+
|
|
1337
|
+
|
|
1338
|
+
def convert_record(
|
|
1339
|
+
record: dict[str, Any],
|
|
1340
|
+
reaction_key: str = "SMIRKS",
|
|
1341
|
+
arrow_key: str = "arrow_code",
|
|
1342
|
+
orbital_key: str = "orbital pair classification",
|
|
1343
|
+
expand_aam: bool = True,
|
|
1344
|
+
remove_non_arrow_maps: bool = True,
|
|
1345
|
+
strict_bond_lookup: bool = True,
|
|
1346
|
+
) -> dict[str, Any]:
|
|
1347
|
+
"""Convert one dictionary record.
|
|
1348
|
+
|
|
1349
|
+
The record is expected to contain reaction, arrow-code, and optional orbital
|
|
1350
|
+
classification fields under the configured keys.
|
|
1351
|
+
|
|
1352
|
+
:param record: Source record to convert.
|
|
1353
|
+
:type record: dict[str, Any]
|
|
1354
|
+
:param reaction_key: Key containing reaction SMILES.
|
|
1355
|
+
:type reaction_key: str
|
|
1356
|
+
:param arrow_key: Key containing arrow code.
|
|
1357
|
+
:type arrow_key: str
|
|
1358
|
+
:param orbital_key: Key containing optional orbital classification metadata.
|
|
1359
|
+
:type orbital_key: str
|
|
1360
|
+
:param expand_aam: Whether to expand atom mapping before ITS construction.
|
|
1361
|
+
:type expand_aam: bool
|
|
1362
|
+
:param remove_non_arrow_maps: Whether to remove atom maps not used by the arrow code.
|
|
1363
|
+
:type remove_non_arrow_maps: bool
|
|
1364
|
+
:param strict_bond_lookup: Whether missing typed bond lookups should raise.
|
|
1365
|
+
:type strict_bond_lookup: bool
|
|
1366
|
+
:return: Converted record with original metadata preserved.
|
|
1367
|
+
:rtype: dict[str, Any]
|
|
1368
|
+
"""
|
|
1369
|
+
reaction_smiles = record[reaction_key]
|
|
1370
|
+
arrow_code = record[arrow_key]
|
|
1371
|
+
orbital_class = record.get(orbital_key)
|
|
1372
|
+
|
|
1373
|
+
result = convert_reaction_arrow(
|
|
1374
|
+
reaction_smiles=reaction_smiles,
|
|
1375
|
+
arrow_code=arrow_code,
|
|
1376
|
+
orbital_class=orbital_class,
|
|
1377
|
+
expand_aam=expand_aam,
|
|
1378
|
+
remove_non_arrow_maps=remove_non_arrow_maps,
|
|
1379
|
+
strict_bond_lookup=strict_bond_lookup,
|
|
1380
|
+
)
|
|
1381
|
+
|
|
1382
|
+
# Preserve original metadata.
|
|
1383
|
+
for key, value in record.items():
|
|
1384
|
+
if key not in result:
|
|
1385
|
+
result[key] = value
|
|
1386
|
+
|
|
1387
|
+
return result
|
|
1388
|
+
|
|
1389
|
+
|
|
1390
|
+
def convert_records(
|
|
1391
|
+
records: list[dict[str, Any]],
|
|
1392
|
+
reaction_key: str = "SMIRKS",
|
|
1393
|
+
arrow_key: str = "arrow_code",
|
|
1394
|
+
orbital_key: str = "orbital pair classification",
|
|
1395
|
+
expand_aam: bool = True,
|
|
1396
|
+
remove_non_arrow_maps: bool = True,
|
|
1397
|
+
strict_bond_lookup: bool = True,
|
|
1398
|
+
keep_errors: bool = False,
|
|
1399
|
+
) -> list[dict[str, Any]]:
|
|
1400
|
+
"""Batch conversion.
|
|
1401
|
+
|
|
1402
|
+
keep_errors=False:
|
|
1403
|
+
raise immediately on first error.
|
|
1404
|
+
|
|
1405
|
+
keep_errors=True:
|
|
1406
|
+
collect errors into result dictionaries.
|
|
1407
|
+
|
|
1408
|
+
:param records: Source records to convert.
|
|
1409
|
+
:type records: list[dict[str, Any]]
|
|
1410
|
+
:param reaction_key: Key containing reaction SMILES.
|
|
1411
|
+
:type reaction_key: str
|
|
1412
|
+
:param arrow_key: Key containing arrow code.
|
|
1413
|
+
:type arrow_key: str
|
|
1414
|
+
:param orbital_key: Key containing optional orbital classification metadata.
|
|
1415
|
+
:type orbital_key: str
|
|
1416
|
+
:param expand_aam: Whether to expand atom mapping before ITS construction.
|
|
1417
|
+
:type expand_aam: bool
|
|
1418
|
+
:param remove_non_arrow_maps: Whether to remove atom maps not used by the arrow code.
|
|
1419
|
+
:type remove_non_arrow_maps: bool
|
|
1420
|
+
:param strict_bond_lookup: Whether missing typed bond lookups should raise.
|
|
1421
|
+
:type strict_bond_lookup: bool
|
|
1422
|
+
:param keep_errors: Whether to collect conversion failures instead of raising.
|
|
1423
|
+
:type keep_errors: bool
|
|
1424
|
+
:return: Converted records, including failures when ``keep_errors`` is true.
|
|
1425
|
+
:rtype: list[dict[str, Any]]
|
|
1426
|
+
"""
|
|
1427
|
+
results = []
|
|
1428
|
+
|
|
1429
|
+
for idx, record in enumerate(records, start=1):
|
|
1430
|
+
try:
|
|
1431
|
+
result = convert_record(
|
|
1432
|
+
record=record,
|
|
1433
|
+
reaction_key=reaction_key,
|
|
1434
|
+
arrow_key=arrow_key,
|
|
1435
|
+
orbital_key=orbital_key,
|
|
1436
|
+
expand_aam=expand_aam,
|
|
1437
|
+
remove_non_arrow_maps=remove_non_arrow_maps,
|
|
1438
|
+
strict_bond_lookup=strict_bond_lookup,
|
|
1439
|
+
)
|
|
1440
|
+
result["row_index"] = idx
|
|
1441
|
+
results.append(result)
|
|
1442
|
+
|
|
1443
|
+
except Exception as e:
|
|
1444
|
+
if not keep_errors:
|
|
1445
|
+
print("=" * 100)
|
|
1446
|
+
print(f"FAILED ROW {idx}")
|
|
1447
|
+
print("=" * 100)
|
|
1448
|
+
print("orbital_class:", record.get(orbital_key))
|
|
1449
|
+
print("arrow_code:", record.get(arrow_key))
|
|
1450
|
+
print("error:", repr(e))
|
|
1451
|
+
print("=" * 100)
|
|
1452
|
+
raise
|
|
1453
|
+
|
|
1454
|
+
failed = dict(record)
|
|
1455
|
+
failed["row_index"] = idx
|
|
1456
|
+
failed["error"] = repr(e)
|
|
1457
|
+
results.append(failed)
|
|
1458
|
+
|
|
1459
|
+
return results
|
|
1460
|
+
|
|
1461
|
+
|
|
1462
|
+
from .conversion_debug import ( # noqa: E402,F401
|
|
1463
|
+
check_typed_conversion_quality,
|
|
1464
|
+
debug_arrow_bond_orders,
|
|
1465
|
+
debug_record,
|
|
1466
|
+
)
|