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,621 @@
|
|
|
1
|
+
"""Electron-aware product-state helpers for the reactor facade.
|
|
2
|
+
|
|
3
|
+
This module is intentionally independent of SynReactor so the reactor remains
|
|
4
|
+
an orchestration facade rather than a dependency of its leaf helpers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections import defaultdict
|
|
10
|
+
from typing import Any, Dict, Iterable, Mapping, Tuple
|
|
11
|
+
|
|
12
|
+
import networkx as nx
|
|
13
|
+
from rdkit import Chem
|
|
14
|
+
|
|
15
|
+
from synkit.Graph.Hyrogen._misc import implicit_hydrogen
|
|
16
|
+
from synkit.Graph.ITS.its_decompose import its_decompose
|
|
17
|
+
from synkit.Graph.ITS.its_reverter import ITSReverter
|
|
18
|
+
from synkit.Graph.Mech.electron_accounting import refresh_electron_fields
|
|
19
|
+
from synkit.IO.chem_converter import _get_preserved_hydrogen_maps
|
|
20
|
+
from synkit.IO.graph_to_mol import GraphToMol
|
|
21
|
+
from synkit.IO.mol_to_graph import MolToGraph
|
|
22
|
+
|
|
23
|
+
ITS_STRUCTURAL_EDGE_ATTRS = ["order", "kekule_order", "sigma_order", "pi_order"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ProductStatePerceptionError(RuntimeError):
|
|
27
|
+
"""Raised when a dirty aromatic product cannot be safely reperceived."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _product_value(value: Any) -> Any:
|
|
31
|
+
"""Return the product member of a scalar or endpoint pair."""
|
|
32
|
+
if isinstance(value, tuple) and len(value) == 2:
|
|
33
|
+
return value[1]
|
|
34
|
+
return value
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _electron_refresh_support( # noqa: C901
|
|
38
|
+
its: nx.Graph,
|
|
39
|
+
rewrite_nodes: Iterable[Any],
|
|
40
|
+
) -> frozenset[Any]:
|
|
41
|
+
"""Return the exact one-local support of product electron recomputation.
|
|
42
|
+
|
|
43
|
+
The derived state at a vertex depends only on its intrinsic node fields
|
|
44
|
+
and on the electron orders of incident edges. The rewrite implementation
|
|
45
|
+
writes intrinsic fields only at ``rewrite_nodes`` and writes edges only
|
|
46
|
+
between such nodes. A presence change can additionally remove every
|
|
47
|
+
external incident edge, so neighbors across that boundary are included
|
|
48
|
+
whenever the actual endpoint edge state differs. This is the complete
|
|
49
|
+
support of the local formal-charge functional, not a corpus assumption.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def side_values(value: Any) -> Tuple[Any, Any]:
|
|
53
|
+
if isinstance(value, tuple) and len(value) == 2:
|
|
54
|
+
return value
|
|
55
|
+
return value, value
|
|
56
|
+
|
|
57
|
+
def node_exists(node: Any, side: int) -> bool:
|
|
58
|
+
attrs = its.nodes[node]
|
|
59
|
+
present = attrs.get("present")
|
|
60
|
+
if isinstance(present, tuple) and len(present) == 2:
|
|
61
|
+
return bool(present[side])
|
|
62
|
+
return side_values(attrs.get("element"))[side] not in (None, "")
|
|
63
|
+
|
|
64
|
+
def edge_state(left: Any, right: Any, attrs: Mapping[str, Any], side: int) -> Any:
|
|
65
|
+
if not node_exists(left, side) or not node_exists(right, side):
|
|
66
|
+
return None
|
|
67
|
+
values = tuple(
|
|
68
|
+
side_values(attrs.get(name))[side]
|
|
69
|
+
for name in (
|
|
70
|
+
"order",
|
|
71
|
+
"kekule_order",
|
|
72
|
+
"sigma_order",
|
|
73
|
+
"pi_order",
|
|
74
|
+
"bond_type",
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
if all(value in (None, "", 0, 0.0) for value in values):
|
|
78
|
+
return None
|
|
79
|
+
return values
|
|
80
|
+
|
|
81
|
+
support = {node for node in rewrite_nodes if node in its}
|
|
82
|
+
|
|
83
|
+
# Every edge written by the rewrite has both endpoints in ``support``.
|
|
84
|
+
# Those endpoints are already scheduled, regardless of the edge delta.
|
|
85
|
+
# An edge crossing out of the mapped locus can therefore change endpoint
|
|
86
|
+
# state only when its mapped endpoint changes presence. Test only those
|
|
87
|
+
# boundary edges instead of rescanning every incident edge of every
|
|
88
|
+
# application. This is the same one-local support, derived directly from
|
|
89
|
+
# the rewrite locality invariant above.
|
|
90
|
+
presence_changes = []
|
|
91
|
+
for node in support:
|
|
92
|
+
if node_exists(node, 0) != node_exists(node, 1):
|
|
93
|
+
presence_changes.append(node)
|
|
94
|
+
for node in presence_changes:
|
|
95
|
+
for left, right, attrs in its.edges(node, data=True):
|
|
96
|
+
if left in support and right in support:
|
|
97
|
+
continue
|
|
98
|
+
if edge_state(left, right, attrs, 0) != edge_state(left, right, attrs, 1):
|
|
99
|
+
support.update((left, right))
|
|
100
|
+
return frozenset(support)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _template_charge_is_authoritative(
|
|
104
|
+
its: nx.Graph,
|
|
105
|
+
node: Any,
|
|
106
|
+
product_attrs: Mapping[str, Any],
|
|
107
|
+
) -> bool:
|
|
108
|
+
"""Return whether the source state lies outside the local Lewis model.
|
|
109
|
+
|
|
110
|
+
Product charge is derived only when both parsed template endpoints satisfy
|
|
111
|
+
the same ``VE - NBE - B`` identity. An inconsistent endpoint is evidence
|
|
112
|
+
that the ordinary graph omits electronic context, so its exact charge is
|
|
113
|
+
retained in either replay direction. This is a representation invariant
|
|
114
|
+
and does not classify atoms by element.
|
|
115
|
+
"""
|
|
116
|
+
if node not in its:
|
|
117
|
+
return True
|
|
118
|
+
attrs = its.nodes[node]
|
|
119
|
+
model_consistent = attrs.get("charge_model_consistent")
|
|
120
|
+
if (
|
|
121
|
+
isinstance(model_consistent, tuple)
|
|
122
|
+
and len(model_consistent) == 2
|
|
123
|
+
and all(isinstance(value, bool) for value in model_consistent)
|
|
124
|
+
):
|
|
125
|
+
return not all(model_consistent)
|
|
126
|
+
present = attrs.get("present")
|
|
127
|
+
if isinstance(present, tuple) and len(present) == 2 and not present[0]:
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
def reactant_value(value: Any) -> Any:
|
|
131
|
+
if isinstance(value, tuple) and len(value) == 2:
|
|
132
|
+
return value[0]
|
|
133
|
+
return value
|
|
134
|
+
|
|
135
|
+
required = ("valence_electrons", "lone_pairs", "radical", "hcount", "charge")
|
|
136
|
+
values = {name: reactant_value(attrs.get(name)) for name in required}
|
|
137
|
+
if any(value is None for value in values.values()):
|
|
138
|
+
return True
|
|
139
|
+
|
|
140
|
+
bond_sum = 0.0
|
|
141
|
+
for _, _, edge_attrs in its.edges(node, data=True):
|
|
142
|
+
sigma = reactant_value(edge_attrs.get("sigma_order"))
|
|
143
|
+
pi = reactant_value(edge_attrs.get("pi_order"))
|
|
144
|
+
if sigma is None or pi is None:
|
|
145
|
+
order = reactant_value(edge_attrs.get("kekule_order"))
|
|
146
|
+
if order is None:
|
|
147
|
+
order = reactant_value(edge_attrs.get("order"))
|
|
148
|
+
if order is None:
|
|
149
|
+
return True
|
|
150
|
+
bond_sum += float(order)
|
|
151
|
+
else:
|
|
152
|
+
bond_sum += float(sigma) + float(pi)
|
|
153
|
+
|
|
154
|
+
modeled_charge = (
|
|
155
|
+
float(values["valence_electrons"])
|
|
156
|
+
- 2.0 * float(values["lone_pairs"])
|
|
157
|
+
- float(values["radical"])
|
|
158
|
+
- float(values["hcount"])
|
|
159
|
+
- bond_sum
|
|
160
|
+
)
|
|
161
|
+
return modeled_charge != float(values["charge"])
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _pair_electron_aware_node_attrs(
|
|
165
|
+
host_n: Dict[str, Any],
|
|
166
|
+
rc_n: Dict[str, Any],
|
|
167
|
+
*,
|
|
168
|
+
preserve_unchanged_state: bool = False,
|
|
169
|
+
relative_resources: frozenset[str] = frozenset(),
|
|
170
|
+
) -> None:
|
|
171
|
+
"""Store paired attrs, preserving generic relative-query state locally."""
|
|
172
|
+
_, product_types = host_n["typesGH"]
|
|
173
|
+
rc_present = rc_n.get("present")
|
|
174
|
+
reactant_is_absent = (
|
|
175
|
+
isinstance(rc_present, tuple) and len(rc_present) == 2 and not rc_present[0]
|
|
176
|
+
)
|
|
177
|
+
legacy_product_values = {
|
|
178
|
+
"element": product_types[0],
|
|
179
|
+
"aromatic": product_types[1],
|
|
180
|
+
"hcount": product_types[2],
|
|
181
|
+
"neighbors": product_types[4],
|
|
182
|
+
}
|
|
183
|
+
model_consistent = rc_n.get("charge_model_consistent")
|
|
184
|
+
if isinstance(model_consistent, tuple) and len(model_consistent) == 2:
|
|
185
|
+
host_n["charge_model_consistent"] = (
|
|
186
|
+
bool(model_consistent[0]),
|
|
187
|
+
bool(model_consistent[1]),
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
for key, product_value in legacy_product_values.items():
|
|
191
|
+
left_value = host_n.get(key)
|
|
192
|
+
product_is_absent = (
|
|
193
|
+
isinstance(rc_present, tuple) and len(rc_present) == 2 and not rc_present[1]
|
|
194
|
+
)
|
|
195
|
+
rc_value = rc_n.get(key)
|
|
196
|
+
if reactant_is_absent and isinstance(rc_value, tuple) and len(rc_value) == 2:
|
|
197
|
+
product_value = rc_value[1]
|
|
198
|
+
if key == "element" and product_value == "*" and not product_is_absent:
|
|
199
|
+
product_value = left_value
|
|
200
|
+
host_n[key] = (left_value, product_value)
|
|
201
|
+
|
|
202
|
+
for key in ("radical", "lone_pairs", "valence_electrons"):
|
|
203
|
+
rc_value = rc_n.get(key)
|
|
204
|
+
if isinstance(rc_value, tuple) and len(rc_value) == 2:
|
|
205
|
+
left_value = host_n.get(key)
|
|
206
|
+
if left_value is None:
|
|
207
|
+
left_value = rc_value[0]
|
|
208
|
+
if key in relative_resources:
|
|
209
|
+
product_value = left_value - rc_value[0] + rc_value[1]
|
|
210
|
+
else:
|
|
211
|
+
product_value = (
|
|
212
|
+
left_value
|
|
213
|
+
if preserve_unchanged_state and rc_value[0] == rc_value[1]
|
|
214
|
+
else rc_value[1]
|
|
215
|
+
)
|
|
216
|
+
host_n[key] = (left_value, product_value)
|
|
217
|
+
|
|
218
|
+
host_n["template_charge"] = (host_n.get("charge"), product_types[3])
|
|
219
|
+
|
|
220
|
+
# Electron-authoritative RCs derive charge at the product boundary.
|
|
221
|
+
# Keep the reactant-side value temporarily so mutation does not copy
|
|
222
|
+
# the RC's product charge label.
|
|
223
|
+
host_n["charge"] = (host_n.get("charge"), host_n.get("charge"))
|
|
224
|
+
|
|
225
|
+
if "atom_map" in host_n:
|
|
226
|
+
host_n["atom_map"] = (host_n["atom_map"], host_n["atom_map"])
|
|
227
|
+
if isinstance(rc_present, tuple) and len(rc_present) == 2:
|
|
228
|
+
host_n["present"] = (bool(host_n.get("present", True)), rc_present[1])
|
|
229
|
+
else:
|
|
230
|
+
host_n["present"] = (True, True)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _ensure_host_atom_maps(host: nx.Graph) -> None:
|
|
234
|
+
"""Assign stable fresh atom maps to unmapped host atoms."""
|
|
235
|
+
used: set[int] = set()
|
|
236
|
+
for node, attrs in host.nodes(data=True):
|
|
237
|
+
atom_map = attrs.get("atom_map")
|
|
238
|
+
if not isinstance(atom_map, int) or atom_map <= 0:
|
|
239
|
+
continue
|
|
240
|
+
if atom_map in used:
|
|
241
|
+
raise ValueError(f"Duplicate atom map {atom_map} on host graph.")
|
|
242
|
+
used.add(atom_map)
|
|
243
|
+
fresh = max(used, default=0) + 1
|
|
244
|
+
for node, attrs in host.nodes(data=True):
|
|
245
|
+
if not isinstance(attrs.get("atom_map"), int) or attrs["atom_map"] <= 0:
|
|
246
|
+
while fresh in used:
|
|
247
|
+
fresh += 1
|
|
248
|
+
attrs["atom_map"] = fresh
|
|
249
|
+
used.add(fresh)
|
|
250
|
+
fresh += 1
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _refresh_product_electron_fields(
|
|
254
|
+
its: nx.Graph,
|
|
255
|
+
) -> None:
|
|
256
|
+
"""Refresh product-side electron fields from the scalar product graph."""
|
|
257
|
+
# Direct refresh is valid while the product Kekule phase is unchanged.
|
|
258
|
+
if not its.graph.get("_product_kekule_phase_dirty", True):
|
|
259
|
+
_refresh_product_electron_fields_direct(its)
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
product = _prepared_electron_product_graph(its)
|
|
263
|
+
refreshed = refresh_electron_fields(product)
|
|
264
|
+
structural_dirty_nodes = its.graph.get("_structural_exact_dirty_nodes")
|
|
265
|
+
structural_dirty_edges = its.graph.get("_structural_exact_dirty_edges")
|
|
266
|
+
for node, attrs in refreshed.nodes(data=True):
|
|
267
|
+
its.nodes[node].pop("_structural_exact_node_sig", None)
|
|
268
|
+
if isinstance(structural_dirty_nodes, set):
|
|
269
|
+
structural_dirty_nodes.add(node)
|
|
270
|
+
current_charge = its.nodes[node].get("charge")
|
|
271
|
+
left_charge = (
|
|
272
|
+
current_charge[0]
|
|
273
|
+
if isinstance(current_charge, tuple) and len(current_charge) == 2
|
|
274
|
+
else current_charge
|
|
275
|
+
)
|
|
276
|
+
product_charge = _electron_product_charge(its, node, attrs)
|
|
277
|
+
if product_charge is not None:
|
|
278
|
+
its.nodes[node]["charge"] = (left_charge, product_charge)
|
|
279
|
+
|
|
280
|
+
template_charge = its.nodes[node].get("template_charge")
|
|
281
|
+
if isinstance(template_charge, tuple) and len(template_charge) == 2:
|
|
282
|
+
attrs["charge_mismatch"] = template_charge[1] != attrs.get(
|
|
283
|
+
"recomputed_charge"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
for key in ("bond_order_sum", "recomputed_charge", "charge_mismatch"):
|
|
287
|
+
if key in attrs:
|
|
288
|
+
current = its.nodes[node].get(key)
|
|
289
|
+
left_value = (
|
|
290
|
+
current[0]
|
|
291
|
+
if isinstance(current, tuple) and len(current) == 2
|
|
292
|
+
else current
|
|
293
|
+
)
|
|
294
|
+
its.nodes[node][key] = (left_value, attrs[key])
|
|
295
|
+
for u, v, attrs in refreshed.edges(data=True):
|
|
296
|
+
its.edges[u, v].pop("_structural_exact_edge_sig", None)
|
|
297
|
+
if isinstance(structural_dirty_edges, set):
|
|
298
|
+
structural_dirty_edges.add((u, v))
|
|
299
|
+
for key in ("kekule_order", "sigma_order", "pi_order"):
|
|
300
|
+
if key not in attrs:
|
|
301
|
+
continue
|
|
302
|
+
current = its.edges[u, v].get(key)
|
|
303
|
+
left_value = (
|
|
304
|
+
current[0]
|
|
305
|
+
if isinstance(current, tuple) and len(current) == 2
|
|
306
|
+
else current
|
|
307
|
+
)
|
|
308
|
+
its.edges[u, v][key] = (left_value, attrs[key])
|
|
309
|
+
its.graph["_product_electron_fields_current"] = True
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _refresh_product_electron_fields_direct(its: nx.Graph) -> None: # noqa: C901
|
|
313
|
+
"""Refresh derived product fields without materialising a side graph.
|
|
314
|
+
|
|
315
|
+
This path is exact while the aromatic Kekule phase is unchanged.
|
|
316
|
+
Explicit hydrogen collapse preserves the heavy atom's electron count:
|
|
317
|
+
a removed H--X sigma bond becomes one unit of ``hcount``.
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
def product_node_exists(attrs: Mapping[str, Any]) -> bool:
|
|
321
|
+
present = attrs.get("present")
|
|
322
|
+
if isinstance(present, tuple) and len(present) == 2:
|
|
323
|
+
return bool(present[1])
|
|
324
|
+
return _product_value(attrs.get("element")) not in (None, "")
|
|
325
|
+
|
|
326
|
+
def product_edge_exists(attrs: Mapping[str, Any]) -> bool:
|
|
327
|
+
for name in ("order", "kekule_order", "bond_type"):
|
|
328
|
+
value = _product_value(attrs.get(name))
|
|
329
|
+
if value not in (None, "", 0, 0.0):
|
|
330
|
+
return True
|
|
331
|
+
return False
|
|
332
|
+
|
|
333
|
+
refresh_hint = its.graph.get("_product_refresh_nodes")
|
|
334
|
+
candidate_nodes = (
|
|
335
|
+
tuple(its)
|
|
336
|
+
if refresh_hint is None
|
|
337
|
+
else tuple(node for node in refresh_hint if node in its)
|
|
338
|
+
)
|
|
339
|
+
product_nodes = {
|
|
340
|
+
node for node in candidate_nodes if product_node_exists(its.nodes[node])
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
# Every changed edge is incident to a mapped rule node. Build the exact
|
|
344
|
+
# local edge population once; unchanged edges elsewhere already carry
|
|
345
|
+
# valid scalar host fields and need no tuple-side refresh.
|
|
346
|
+
product_edges = []
|
|
347
|
+
structural_dirty_nodes = its.graph.get("_structural_exact_dirty_nodes")
|
|
348
|
+
structural_dirty_edges = its.graph.get("_structural_exact_dirty_edges")
|
|
349
|
+
seen_edges: set[frozenset[Any]] = set()
|
|
350
|
+
for node in product_nodes:
|
|
351
|
+
for left, right, attrs in its.edges(node, data=True):
|
|
352
|
+
edge_key = frozenset((left, right))
|
|
353
|
+
if edge_key in seen_edges:
|
|
354
|
+
continue
|
|
355
|
+
seen_edges.add(edge_key)
|
|
356
|
+
if (
|
|
357
|
+
left in its
|
|
358
|
+
and right in its
|
|
359
|
+
and product_node_exists(its.nodes[left])
|
|
360
|
+
and product_node_exists(its.nodes[right])
|
|
361
|
+
and product_edge_exists(attrs)
|
|
362
|
+
):
|
|
363
|
+
product_edges.append((left, right, attrs))
|
|
364
|
+
|
|
365
|
+
bond_sums: Dict[Any, float] = defaultdict(float)
|
|
366
|
+
for left, right, attrs in product_edges:
|
|
367
|
+
attrs.pop("_structural_exact_edge_sig", None)
|
|
368
|
+
if isinstance(structural_dirty_edges, set):
|
|
369
|
+
structural_dirty_edges.add((left, right))
|
|
370
|
+
sigma = float(_product_value(attrs.get("sigma_order", 0.0)) or 0.0)
|
|
371
|
+
pi = float(_product_value(attrs.get("pi_order", 0.0)) or 0.0)
|
|
372
|
+
bond_order = sigma + pi
|
|
373
|
+
bond_sums[left] += bond_order
|
|
374
|
+
bond_sums[right] += bond_order
|
|
375
|
+
|
|
376
|
+
current = attrs.get("kekule_order")
|
|
377
|
+
left_value = (
|
|
378
|
+
current[0] if isinstance(current, tuple) and len(current) == 2 else current
|
|
379
|
+
)
|
|
380
|
+
attrs["kekule_order"] = (left_value, bond_order)
|
|
381
|
+
|
|
382
|
+
for node in product_nodes:
|
|
383
|
+
attrs = its.nodes[node]
|
|
384
|
+
attrs.pop("_structural_exact_node_sig", None)
|
|
385
|
+
if isinstance(structural_dirty_nodes, set):
|
|
386
|
+
structural_dirty_nodes.add(node)
|
|
387
|
+
bond_sum = bond_sums[node]
|
|
388
|
+
current_bond_sum = attrs.get("bond_order_sum")
|
|
389
|
+
left_bond_sum = (
|
|
390
|
+
current_bond_sum[0]
|
|
391
|
+
if isinstance(current_bond_sum, tuple) and len(current_bond_sum) == 2
|
|
392
|
+
else current_bond_sum
|
|
393
|
+
)
|
|
394
|
+
attrs["bond_order_sum"] = (left_bond_sum, bond_sum)
|
|
395
|
+
|
|
396
|
+
valence_electrons = _product_value(attrs.get("valence_electrons"))
|
|
397
|
+
if valence_electrons is None:
|
|
398
|
+
continue
|
|
399
|
+
lone_pairs = float(_product_value(attrs.get("lone_pairs", 0)) or 0)
|
|
400
|
+
radical = float(_product_value(attrs.get("radical", 0)) or 0)
|
|
401
|
+
hcount = float(_product_value(attrs.get("hcount", 0)) or 0)
|
|
402
|
+
recomputed_charge = (
|
|
403
|
+
float(valence_electrons) - 2.0 * lone_pairs - radical - hcount - bond_sum
|
|
404
|
+
)
|
|
405
|
+
if recomputed_charge.is_integer():
|
|
406
|
+
recomputed_charge = int(recomputed_charge)
|
|
407
|
+
|
|
408
|
+
current_recomputed = attrs.get("recomputed_charge")
|
|
409
|
+
left_recomputed = (
|
|
410
|
+
current_recomputed[0]
|
|
411
|
+
if isinstance(current_recomputed, tuple) and len(current_recomputed) == 2
|
|
412
|
+
else current_recomputed
|
|
413
|
+
)
|
|
414
|
+
attrs["recomputed_charge"] = (left_recomputed, recomputed_charge)
|
|
415
|
+
|
|
416
|
+
template_charge = attrs.get("template_charge")
|
|
417
|
+
represented_charge = _product_value(attrs.get("charge", 0))
|
|
418
|
+
mismatch = float(represented_charge or 0) != recomputed_charge
|
|
419
|
+
if isinstance(template_charge, tuple) and len(template_charge) == 2:
|
|
420
|
+
mismatch = template_charge[1] != recomputed_charge
|
|
421
|
+
current_mismatch = attrs.get("charge_mismatch")
|
|
422
|
+
left_mismatch = (
|
|
423
|
+
current_mismatch[0]
|
|
424
|
+
if isinstance(current_mismatch, tuple) and len(current_mismatch) == 2
|
|
425
|
+
else current_mismatch
|
|
426
|
+
)
|
|
427
|
+
attrs["charge_mismatch"] = (left_mismatch, mismatch)
|
|
428
|
+
|
|
429
|
+
current_charge = attrs.get("charge")
|
|
430
|
+
left_charge = (
|
|
431
|
+
current_charge[0]
|
|
432
|
+
if isinstance(current_charge, tuple) and len(current_charge) == 2
|
|
433
|
+
else current_charge
|
|
434
|
+
)
|
|
435
|
+
product_charge = _electron_product_charge(
|
|
436
|
+
its,
|
|
437
|
+
node,
|
|
438
|
+
{
|
|
439
|
+
"aromatic": bool(_product_value(attrs.get("aromatic", False))),
|
|
440
|
+
"element": _product_value(attrs.get("element")),
|
|
441
|
+
"recomputed_charge": recomputed_charge,
|
|
442
|
+
},
|
|
443
|
+
)
|
|
444
|
+
attrs["charge"] = (left_charge, product_charge)
|
|
445
|
+
|
|
446
|
+
its.graph["_product_electron_fields_current"] = True
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _product_kekule_phase_is_dirty(its: nx.Graph) -> bool:
|
|
450
|
+
"""Return whether a rewrite can invalidate an aromatic Kekule phase.
|
|
451
|
+
|
|
452
|
+
Substituent and hydrogen-count edits do not alter the alternating phase
|
|
453
|
+
inside an aromatic system. Electronic changes on aromatic atoms and
|
|
454
|
+
edits to bonds within that system do, and therefore still require full
|
|
455
|
+
RDKit re-perception.
|
|
456
|
+
"""
|
|
457
|
+
|
|
458
|
+
def side_values(value: Any) -> Tuple[Any, Any]:
|
|
459
|
+
if isinstance(value, tuple) and len(value) == 2:
|
|
460
|
+
return value
|
|
461
|
+
return value, value
|
|
462
|
+
|
|
463
|
+
refresh_hint = its.graph.get("_product_refresh_nodes")
|
|
464
|
+
candidate_nodes = (
|
|
465
|
+
tuple(its)
|
|
466
|
+
if refresh_hint is None
|
|
467
|
+
else tuple(node for node in refresh_hint if node in its)
|
|
468
|
+
)
|
|
469
|
+
aromatic_nodes = {
|
|
470
|
+
node
|
|
471
|
+
for node in candidate_nodes
|
|
472
|
+
if any(
|
|
473
|
+
bool(value) for value in side_values(its.nodes[node].get("aromatic", False))
|
|
474
|
+
)
|
|
475
|
+
}
|
|
476
|
+
if not aromatic_nodes:
|
|
477
|
+
return False
|
|
478
|
+
|
|
479
|
+
for node in aromatic_nodes:
|
|
480
|
+
attrs = its.nodes[node]
|
|
481
|
+
for name in (
|
|
482
|
+
"element",
|
|
483
|
+
"aromatic",
|
|
484
|
+
"radical",
|
|
485
|
+
"lone_pairs",
|
|
486
|
+
"valence_electrons",
|
|
487
|
+
"present",
|
|
488
|
+
"template_charge",
|
|
489
|
+
):
|
|
490
|
+
left, right = side_values(attrs.get(name))
|
|
491
|
+
if left != right:
|
|
492
|
+
return True
|
|
493
|
+
|
|
494
|
+
seen_edges: set[frozenset[Any]] = set()
|
|
495
|
+
for node in aromatic_nodes:
|
|
496
|
+
for left, right, attrs in its.edges(node, data=True):
|
|
497
|
+
edge_key = frozenset((left, right))
|
|
498
|
+
if edge_key in seen_edges:
|
|
499
|
+
continue
|
|
500
|
+
seen_edges.add(edge_key)
|
|
501
|
+
if left not in aromatic_nodes or right not in aromatic_nodes:
|
|
502
|
+
continue
|
|
503
|
+
for name in ITS_STRUCTURAL_EDGE_ATTRS:
|
|
504
|
+
before, after = side_values(attrs.get(name))
|
|
505
|
+
if before != after:
|
|
506
|
+
return True
|
|
507
|
+
return False
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _prepared_electron_product_graph(its: nx.Graph) -> nx.Graph:
|
|
511
|
+
"""Build the scalar product graph used for electron recomputation."""
|
|
512
|
+
product = ITSReverter(its).to_product_graph()
|
|
513
|
+
preserved_hydrogens = _get_preserved_hydrogen_maps(its, "tuple")
|
|
514
|
+
product = implicit_hydrogen(product, set(preserved_hydrogens))
|
|
515
|
+
return _reperceive_product_kekule_phase(product, its)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _electron_product_charge(
|
|
519
|
+
its: nx.Graph,
|
|
520
|
+
node: Any,
|
|
521
|
+
product_attrs: Mapping[str, Any],
|
|
522
|
+
) -> Any:
|
|
523
|
+
"""Choose the product charge used for electron-aware serialization.
|
|
524
|
+
|
|
525
|
+
Recompute charge when the template endpoints satisfy the local Lewis
|
|
526
|
+
identity. Preserve explicit template charge for aromatic or
|
|
527
|
+
model-inconsistent endpoints.
|
|
528
|
+
"""
|
|
529
|
+
if node in its:
|
|
530
|
+
template_charge = its.nodes[node].get("template_charge")
|
|
531
|
+
aromatic = product_attrs.get("aromatic", its.nodes[node].get("aromatic"))
|
|
532
|
+
if isinstance(template_charge, tuple) and len(template_charge) == 2:
|
|
533
|
+
if aromatic is True or _template_charge_is_authoritative(
|
|
534
|
+
its,
|
|
535
|
+
node,
|
|
536
|
+
product_attrs,
|
|
537
|
+
):
|
|
538
|
+
return template_charge[1]
|
|
539
|
+
else:
|
|
540
|
+
return _product_value(its.nodes[node].get("charge"))
|
|
541
|
+
return product_attrs.get("recomputed_charge")
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _reperceive_product_kekule_phase( # noqa: C901
|
|
545
|
+
product: nx.Graph,
|
|
546
|
+
its: nx.Graph,
|
|
547
|
+
) -> nx.Graph:
|
|
548
|
+
"""Refresh aromatic sigma/pi phase from full product presentation bonds."""
|
|
549
|
+
if not any(data.get("order") == 1.5 for _, _, data in product.edges(data=True)):
|
|
550
|
+
return product
|
|
551
|
+
if not its.graph.get("_product_kekule_phase_dirty", True):
|
|
552
|
+
return product
|
|
553
|
+
|
|
554
|
+
probe = product.copy()
|
|
555
|
+
for node, attrs in probe.nodes(data=True):
|
|
556
|
+
template_charge = its.nodes[node].get("template_charge")
|
|
557
|
+
if isinstance(template_charge, tuple) and len(template_charge) == 2:
|
|
558
|
+
attrs["charge"] = template_charge[1]
|
|
559
|
+
|
|
560
|
+
try:
|
|
561
|
+
mol = GraphToMol(edge_attributes={"order": "order"}).graph_to_mol(
|
|
562
|
+
probe,
|
|
563
|
+
sanitize=True,
|
|
564
|
+
use_h_count=True,
|
|
565
|
+
prefer_kekule_order=False,
|
|
566
|
+
)
|
|
567
|
+
except Exception as exc:
|
|
568
|
+
raise ProductStatePerceptionError(
|
|
569
|
+
"Could not reperceive the dirty aromatic product state."
|
|
570
|
+
) from exc
|
|
571
|
+
|
|
572
|
+
refreshed = product.copy()
|
|
573
|
+
try:
|
|
574
|
+
# Only three edge fields are needed here. Building a complete
|
|
575
|
+
# MolToGraph representation also derives every atom descriptor and
|
|
576
|
+
# stereo registry, which is substantially more work. Read the
|
|
577
|
+
# Kekule bond phase directly from a sanitized RDKit copy instead.
|
|
578
|
+
kekule = Chem.Mol(mol)
|
|
579
|
+
Chem.Kekulize(kekule, clearAromaticFlags=True)
|
|
580
|
+
node_by_index = tuple(
|
|
581
|
+
atom.GetAtomMapNum() or atom.GetIdx() + 1 for atom in kekule.GetAtoms()
|
|
582
|
+
)
|
|
583
|
+
if len(node_by_index) != len(set(node_by_index)) or set(node_by_index) != set(
|
|
584
|
+
refreshed
|
|
585
|
+
):
|
|
586
|
+
raise ValueError("Sanitized product atom identities are not bijective.")
|
|
587
|
+
for bond in kekule.GetBonds():
|
|
588
|
+
left = node_by_index[bond.GetBeginAtomIdx()]
|
|
589
|
+
right = node_by_index[bond.GetEndAtomIdx()]
|
|
590
|
+
if not refreshed.has_edge(left, right):
|
|
591
|
+
raise ValueError("Sanitized product bond is absent from the graph.")
|
|
592
|
+
order = float(bond.GetBondTypeAsDouble())
|
|
593
|
+
refreshed[left][right]["kekule_order"] = order
|
|
594
|
+
refreshed[left][right]["sigma_order"] = 1.0 if order > 0 else 0.0
|
|
595
|
+
refreshed[left][right]["pi_order"] = max(0.0, order - 1.0)
|
|
596
|
+
except Exception:
|
|
597
|
+
# Preserve the established conservative path for unusual third-party
|
|
598
|
+
# graphs whose atom maps cannot identify the sanitized RDKit bonds.
|
|
599
|
+
try:
|
|
600
|
+
reperceived = MolToGraph(attr_profile="minimal").transform(
|
|
601
|
+
mol,
|
|
602
|
+
use_index_as_atom_map=True,
|
|
603
|
+
)
|
|
604
|
+
except Exception as exc:
|
|
605
|
+
raise ProductStatePerceptionError(
|
|
606
|
+
"Could not reperceive the dirty aromatic product state."
|
|
607
|
+
) from exc
|
|
608
|
+
for u, v in refreshed.edges():
|
|
609
|
+
if not reperceived.has_edge(u, v):
|
|
610
|
+
continue
|
|
611
|
+
for key in ("kekule_order", "sigma_order", "pi_order"):
|
|
612
|
+
if key in reperceived[u][v]:
|
|
613
|
+
refreshed[u][v][key] = reperceived[u][v][key]
|
|
614
|
+
return refreshed
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def _product_graph_for_diagnostics(its: nx.Graph) -> nx.Graph:
|
|
618
|
+
"""Return the product graph matching the rewrite representation."""
|
|
619
|
+
if its.graph.get("electron_aware_rewrite", False):
|
|
620
|
+
return ITSReverter(its).to_product_graph()
|
|
621
|
+
return its_decompose(its)[1]
|