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,1060 @@
|
|
|
1
|
+
"""Typed validation contracts for wildcard-based reaction fusion.
|
|
2
|
+
|
|
3
|
+
The validator in this module is deliberately independent of a particular
|
|
4
|
+
fusion search strategy. It checks invariants that every RBL exit path must
|
|
5
|
+
obey and returns structured issues instead of relying on logs or exceptions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections import Counter
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Any, Mapping, Sequence
|
|
14
|
+
|
|
15
|
+
import networkx as nx
|
|
16
|
+
from rdkit import Chem
|
|
17
|
+
|
|
18
|
+
from synkit.Graph.Morphism.constraints import (
|
|
19
|
+
NodeStateKind,
|
|
20
|
+
WildcardRole,
|
|
21
|
+
adapt_legacy_node_state,
|
|
22
|
+
)
|
|
23
|
+
from synkit.Graph.Fusion.identity import (
|
|
24
|
+
graph_identity_digest,
|
|
25
|
+
graphs_exactly_equivalent,
|
|
26
|
+
)
|
|
27
|
+
from synkit.Graph.Stereo import (
|
|
28
|
+
mapped_stereo_subgraph_registries_match,
|
|
29
|
+
stereo_registry_layers,
|
|
30
|
+
)
|
|
31
|
+
from synkit.IO.mol_to_graph import MolToGraph
|
|
32
|
+
from synkit.Synthesis.Reactor import Strategy
|
|
33
|
+
|
|
34
|
+
_ENDPOINT_NODE_ATTRS = (
|
|
35
|
+
"element",
|
|
36
|
+
"isotope",
|
|
37
|
+
"charge",
|
|
38
|
+
"aromatic",
|
|
39
|
+
"hcount",
|
|
40
|
+
"lone_pairs",
|
|
41
|
+
"radical",
|
|
42
|
+
)
|
|
43
|
+
_ENDPOINT_EDGE_ATTRS = ("order", "sigma_order", "pi_order", "aromatic")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _mapped_smiles_mol(smiles: str) -> Chem.Mol | None:
|
|
47
|
+
"""Parse mapped SMILES without discarding heavy-atom-bound H identities."""
|
|
48
|
+
parameters = Chem.SmilesParserParams()
|
|
49
|
+
parameters.removeHs = False
|
|
50
|
+
return Chem.MolFromSmiles(smiles, parameters)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class FusionIssueCode(str, Enum):
|
|
54
|
+
"""Stable machine-readable issue codes emitted by fusion validation."""
|
|
55
|
+
|
|
56
|
+
INVALID_REACTION = "FUSION_INVALID_REACTION"
|
|
57
|
+
PARSE_FAILURE = "FUSION_PARSE_FAILURE"
|
|
58
|
+
DANGLING_WILDCARD = "FUSION_DANGLING_WILDCARD"
|
|
59
|
+
SIDE_ONLY_STANDALONE_HYDROGEN = "FUSION_SIDE_ONLY_STANDALONE_HYDROGEN"
|
|
60
|
+
DUPLICATE_ATOM_MAP = "FUSION_DUPLICATE_ATOM_MAP"
|
|
61
|
+
ATOM_MAP_IMBALANCE = "FUSION_ATOM_MAP_IMBALANCE"
|
|
62
|
+
ELEMENT_MAP_CONFLICT = "FUSION_ELEMENT_MAP_CONFLICT"
|
|
63
|
+
ISOTOPE_MAP_CONFLICT = "FUSION_ISOTOPE_MAP_CONFLICT"
|
|
64
|
+
HYDROGEN_MAP_IMBALANCE = "FUSION_HYDROGEN_MAP_IMBALANCE"
|
|
65
|
+
WILDCARD_ROLE_CONFLICT = "FUSION_WILDCARD_ROLE_CONFLICT"
|
|
66
|
+
INTERFACE_INVALID = "FUSION_INTERFACE_INVALID"
|
|
67
|
+
CONSTRUCTION_INVALID = "FUSION_CONSTRUCTION_INVALID"
|
|
68
|
+
PROOF_FAILED = "FUSION_PROOF_FAILED"
|
|
69
|
+
OPERATION_FAILED = "FUSION_OPERATION_FAILED"
|
|
70
|
+
SERIALIZATION_FAILED = "FUSION_SERIALIZATION_FAILED"
|
|
71
|
+
POSTPROCESS_FAILED = "FUSION_POSTPROCESS_FAILED"
|
|
72
|
+
REACTANT_ENDPOINT_NOT_PRESERVED = "FUSION_REACTANT_ENDPOINT_NOT_PRESERVED"
|
|
73
|
+
PRODUCT_ENDPOINT_NOT_PRESERVED = "FUSION_PRODUCT_ENDPOINT_NOT_PRESERVED"
|
|
74
|
+
REACTANT_COMPONENT_NOT_PRESERVED = (
|
|
75
|
+
"FUSION_REACTANT_COMPONENT_NOT_PRESERVED"
|
|
76
|
+
)
|
|
77
|
+
PRODUCT_COMPONENT_NOT_PRESERVED = "FUSION_PRODUCT_COMPONENT_NOT_PRESERVED"
|
|
78
|
+
ELEMENT_ISOTOPE_IMBALANCE = "FUSION_ELEMENT_ISOTOPE_IMBALANCE"
|
|
79
|
+
CHARGE_IMBALANCE = "FUSION_CHARGE_IMBALANCE"
|
|
80
|
+
UNMAPPED_MATERIAL_ATOM = "FUSION_UNMAPPED_MATERIAL_ATOM"
|
|
81
|
+
ENVIRONMENT_DELTA_MISMATCH = "FUSION_ENVIRONMENT_DELTA_MISMATCH"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True)
|
|
85
|
+
class FusionIssue:
|
|
86
|
+
"""One failed fusion invariant."""
|
|
87
|
+
|
|
88
|
+
code: FusionIssueCode
|
|
89
|
+
stage: str
|
|
90
|
+
message: str
|
|
91
|
+
context: Mapping[str, Any] = field(default_factory=dict)
|
|
92
|
+
|
|
93
|
+
def to_dict(self) -> dict[str, Any]:
|
|
94
|
+
"""Return a JSON-serialisable representation."""
|
|
95
|
+
return {
|
|
96
|
+
"code": self.code.value,
|
|
97
|
+
"stage": self.stage,
|
|
98
|
+
"message": self.message,
|
|
99
|
+
"context": dict(self.context),
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass(frozen=True)
|
|
104
|
+
class FusionValidation:
|
|
105
|
+
"""Validation outcome shared by every RBL execution mode."""
|
|
106
|
+
|
|
107
|
+
valid: bool
|
|
108
|
+
stage: str = "fusion"
|
|
109
|
+
issues: tuple[FusionIssue, ...] = ()
|
|
110
|
+
evidence: Mapping[str, Any] = field(default_factory=dict)
|
|
111
|
+
|
|
112
|
+
def to_dict(self) -> dict[str, Any]:
|
|
113
|
+
"""Return a JSON-serialisable representation."""
|
|
114
|
+
return {
|
|
115
|
+
"valid": self.valid,
|
|
116
|
+
"stage": self.stage,
|
|
117
|
+
"issues": [issue.to_dict() for issue in self.issues],
|
|
118
|
+
"evidence": dict(self.evidence),
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _issue(
|
|
123
|
+
code: FusionIssueCode,
|
|
124
|
+
message: str,
|
|
125
|
+
**context: Any,
|
|
126
|
+
) -> FusionIssue:
|
|
127
|
+
return FusionIssue(
|
|
128
|
+
code=code,
|
|
129
|
+
stage="fusion",
|
|
130
|
+
message=message,
|
|
131
|
+
context=context,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _mapped_elements(mol: Chem.Mol) -> tuple[dict[int, str], list[int]]:
|
|
136
|
+
elements: dict[int, str] = {}
|
|
137
|
+
maps: list[int] = []
|
|
138
|
+
for atom in mol.GetAtoms():
|
|
139
|
+
atom_map = int(atom.GetAtomMapNum())
|
|
140
|
+
if atom_map <= 0:
|
|
141
|
+
continue
|
|
142
|
+
maps.append(atom_map)
|
|
143
|
+
elements[atom_map] = atom.GetSymbol()
|
|
144
|
+
return elements, maps
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _mapped_isotopes(mol: Chem.Mol) -> dict[int, int]:
|
|
148
|
+
return {
|
|
149
|
+
int(atom.GetAtomMapNum()): int(atom.GetIsotope())
|
|
150
|
+
for atom in mol.GetAtoms()
|
|
151
|
+
if atom.GetAtomMapNum() > 0
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _unmapped_standalone_hydrogens(mol: Chem.Mol) -> Counter[str]:
|
|
156
|
+
"""Count untraceable isolated H atoms by isotope/charge/radical state.
|
|
157
|
+
|
|
158
|
+
A mapped H may legitimately move from a standalone proton or hydride into
|
|
159
|
+
a bond; its conserved atom-map identity proves that it was transferred.
|
|
160
|
+
An unmapped isolated H has no such identity and must remain endpoint
|
|
161
|
+
symmetric to cross the verified representation boundary.
|
|
162
|
+
"""
|
|
163
|
+
return Counter(
|
|
164
|
+
f"isotope={atom.GetIsotope()},charge={atom.GetFormalCharge()},"
|
|
165
|
+
f"radical={atom.GetNumRadicalElectrons()}"
|
|
166
|
+
for atom in mol.GetAtoms()
|
|
167
|
+
if (
|
|
168
|
+
atom.GetAtomicNum() == 1
|
|
169
|
+
and atom.GetDegree() == 0
|
|
170
|
+
and atom.GetAtomMapNum() <= 0
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _mapped_identity_issues(
|
|
176
|
+
endpoints: Sequence[tuple[str, Chem.Mol]],
|
|
177
|
+
) -> list[FusionIssue]:
|
|
178
|
+
"""Validate uniqueness, side symmetry, and element-stable mapped atoms."""
|
|
179
|
+
issues: list[FusionIssue] = []
|
|
180
|
+
endpoint_elements: list[dict[int, str]] = []
|
|
181
|
+
for side, mol in endpoints:
|
|
182
|
+
elements, maps = _mapped_elements(mol)
|
|
183
|
+
endpoint_elements.append(elements)
|
|
184
|
+
duplicates = sorted(
|
|
185
|
+
atom_map for atom_map, count in Counter(maps).items() if count > 1
|
|
186
|
+
)
|
|
187
|
+
if duplicates:
|
|
188
|
+
issues.append(
|
|
189
|
+
_issue(
|
|
190
|
+
FusionIssueCode.DUPLICATE_ATOM_MAP,
|
|
191
|
+
"An atom-map identity occurs more than once on one endpoint.",
|
|
192
|
+
side=side,
|
|
193
|
+
atom_maps=duplicates,
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
reactant_elements, product_elements = endpoint_elements
|
|
198
|
+
reactant_maps = set(reactant_elements)
|
|
199
|
+
product_maps = set(product_elements)
|
|
200
|
+
if reactant_maps != product_maps:
|
|
201
|
+
issues.append(
|
|
202
|
+
_issue(
|
|
203
|
+
FusionIssueCode.ATOM_MAP_IMBALANCE,
|
|
204
|
+
"Mapped atom identities are not side-symmetric.",
|
|
205
|
+
reactants_only=sorted(reactant_maps - product_maps),
|
|
206
|
+
products_only=sorted(product_maps - reactant_maps),
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
conflicts = {
|
|
210
|
+
atom_map: (reactant_elements[atom_map], product_elements[atom_map])
|
|
211
|
+
for atom_map in sorted(reactant_maps & product_maps)
|
|
212
|
+
if reactant_elements[atom_map] != product_elements[atom_map]
|
|
213
|
+
}
|
|
214
|
+
if conflicts:
|
|
215
|
+
issues.append(
|
|
216
|
+
_issue(
|
|
217
|
+
FusionIssueCode.ELEMENT_MAP_CONFLICT,
|
|
218
|
+
"Mapped atom identities change element across the reaction.",
|
|
219
|
+
conflicts=conflicts,
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
endpoint_isotopes = [_mapped_isotopes(mol) for _, mol in endpoints]
|
|
224
|
+
isotope_conflicts = {
|
|
225
|
+
atom_map: (endpoint_isotopes[0][atom_map], endpoint_isotopes[1][atom_map])
|
|
226
|
+
for atom_map in sorted(reactant_maps & product_maps)
|
|
227
|
+
if endpoint_isotopes[0][atom_map] != endpoint_isotopes[1][atom_map]
|
|
228
|
+
}
|
|
229
|
+
if isotope_conflicts:
|
|
230
|
+
issues.append(
|
|
231
|
+
_issue(
|
|
232
|
+
FusionIssueCode.ISOTOPE_MAP_CONFLICT,
|
|
233
|
+
"Mapped atom identities change isotope across the reaction.",
|
|
234
|
+
conflicts=isotope_conflicts,
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
hydrogen_maps = [
|
|
239
|
+
{atom_map for atom_map, element in elements.items() if element == "H"}
|
|
240
|
+
for elements in endpoint_elements
|
|
241
|
+
]
|
|
242
|
+
if hydrogen_maps[0] != hydrogen_maps[1]:
|
|
243
|
+
issues.append(
|
|
244
|
+
_issue(
|
|
245
|
+
FusionIssueCode.HYDROGEN_MAP_IMBALANCE,
|
|
246
|
+
"Mapped explicit hydrogen identities are not side-symmetric.",
|
|
247
|
+
reactants=sorted(hydrogen_maps[0]),
|
|
248
|
+
products=sorted(hydrogen_maps[1]),
|
|
249
|
+
)
|
|
250
|
+
)
|
|
251
|
+
return issues
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def validate_fusion_rsmi(
|
|
255
|
+
rsmi: str,
|
|
256
|
+
*,
|
|
257
|
+
allow_wildcards: bool = False,
|
|
258
|
+
) -> FusionValidation:
|
|
259
|
+
"""Validate endpoint invariants of a fused reaction SMILES.
|
|
260
|
+
|
|
261
|
+
This is not a reaction-balancing oracle. It guards the representation
|
|
262
|
+
boundary most vulnerable during RBL fusion: parseability, unique and
|
|
263
|
+
side-symmetric atom-map identities, element preservation, no side-only
|
|
264
|
+
*unmapped* isolated H, and no unresolved wildcard unless explicitly
|
|
265
|
+
requested by the caller. A mapped isolated H may become bound because its
|
|
266
|
+
symmetric map identity already certifies atom conservation.
|
|
267
|
+
"""
|
|
268
|
+
if not isinstance(rsmi, str) or rsmi.count(">>") != 1:
|
|
269
|
+
issue = _issue(
|
|
270
|
+
FusionIssueCode.INVALID_REACTION,
|
|
271
|
+
"Expected exactly one '>>' reaction separator.",
|
|
272
|
+
)
|
|
273
|
+
return FusionValidation(valid=False, issues=(issue,))
|
|
274
|
+
|
|
275
|
+
reactants, products = rsmi.split(">>", 1)
|
|
276
|
+
mols: list[Chem.Mol] = []
|
|
277
|
+
issues: list[FusionIssue] = []
|
|
278
|
+
for side, smiles in (("reactants", reactants), ("products", products)):
|
|
279
|
+
mol = _mapped_smiles_mol(smiles)
|
|
280
|
+
if mol is None:
|
|
281
|
+
issues.append(
|
|
282
|
+
_issue(
|
|
283
|
+
FusionIssueCode.PARSE_FAILURE,
|
|
284
|
+
f"RDKit could not parse the {side} endpoint.",
|
|
285
|
+
side=side,
|
|
286
|
+
)
|
|
287
|
+
)
|
|
288
|
+
else:
|
|
289
|
+
mols.append(mol)
|
|
290
|
+
|
|
291
|
+
if issues:
|
|
292
|
+
return FusionValidation(valid=False, issues=tuple(issues))
|
|
293
|
+
|
|
294
|
+
reactant_mol, product_mol = mols
|
|
295
|
+
if not allow_wildcards:
|
|
296
|
+
for side, mol in (
|
|
297
|
+
("reactants", reactant_mol),
|
|
298
|
+
("products", product_mol),
|
|
299
|
+
):
|
|
300
|
+
wildcard_maps = [
|
|
301
|
+
int(atom.GetAtomMapNum())
|
|
302
|
+
for atom in mol.GetAtoms()
|
|
303
|
+
if atom.GetAtomicNum() == 0
|
|
304
|
+
]
|
|
305
|
+
if wildcard_maps:
|
|
306
|
+
issues.append(
|
|
307
|
+
_issue(
|
|
308
|
+
FusionIssueCode.DANGLING_WILDCARD,
|
|
309
|
+
"An unresolved wildcard crossed the final fusion boundary.",
|
|
310
|
+
side=side,
|
|
311
|
+
atom_maps=wildcard_maps,
|
|
312
|
+
)
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
issues.extend(
|
|
316
|
+
_mapped_identity_issues(
|
|
317
|
+
(("reactants", reactant_mol), ("products", product_mol))
|
|
318
|
+
)
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
reactant_standalone = _unmapped_standalone_hydrogens(reactant_mol)
|
|
322
|
+
product_standalone = _unmapped_standalone_hydrogens(product_mol)
|
|
323
|
+
if reactant_standalone != product_standalone:
|
|
324
|
+
issues.append(
|
|
325
|
+
_issue(
|
|
326
|
+
FusionIssueCode.SIDE_ONLY_STANDALONE_HYDROGEN,
|
|
327
|
+
"Standalone explicit hydrogen fragments differ between endpoints.",
|
|
328
|
+
reactants=dict(reactant_standalone),
|
|
329
|
+
products=dict(product_standalone),
|
|
330
|
+
)
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
return FusionValidation(valid=not issues, issues=tuple(issues))
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _parse_unmapped_endpoint_graph(side: str) -> nx.Graph | None:
|
|
337
|
+
"""Parse one endpoint into SynKit's map-independent molecular graph."""
|
|
338
|
+
if not side:
|
|
339
|
+
graph = nx.Graph()
|
|
340
|
+
graph.graph["stereo_descriptors"] = {}
|
|
341
|
+
return graph
|
|
342
|
+
mol = Chem.MolFromSmiles(side)
|
|
343
|
+
if mol is None:
|
|
344
|
+
return None
|
|
345
|
+
mol = Chem.Mol(mol)
|
|
346
|
+
for atom in mol.GetAtoms():
|
|
347
|
+
atom.SetAtomMapNum(0)
|
|
348
|
+
# Endpoint preservation is a molecular-graph claim, independent of
|
|
349
|
+
# whether a bound hydrogen was serialized explicitly to retain its AAM
|
|
350
|
+
# identity. Clear AAM first, then fold only RDKit-removable bound H; H2
|
|
351
|
+
# and standalone hydrogen components remain explicit.
|
|
352
|
+
mol = Chem.RemoveHs(mol)
|
|
353
|
+
return MolToGraph(
|
|
354
|
+
node_attrs=list(_ENDPOINT_NODE_ATTRS),
|
|
355
|
+
edge_attrs=list(_ENDPOINT_EDGE_ATTRS),
|
|
356
|
+
attr_profile="minimal",
|
|
357
|
+
).transform(mol)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _endpoint_embedding_proof(
|
|
361
|
+
original_side: str,
|
|
362
|
+
candidate_side: str,
|
|
363
|
+
) -> list[dict[str, Any]] | None:
|
|
364
|
+
"""Find a component-injective exact subgraph embedding.
|
|
365
|
+
|
|
366
|
+
Every original molecule must embed into a distinct candidate molecule.
|
|
367
|
+
This prevents two observed species from being collapsed into one newly
|
|
368
|
+
connected component while still allowing RBL to add missing components.
|
|
369
|
+
Atom and bond queries are exact RDKit graph queries with specified
|
|
370
|
+
chirality enabled; atom-map numbers are representation labels and are
|
|
371
|
+
therefore excluded from the match relation.
|
|
372
|
+
"""
|
|
373
|
+
# Keep this import local so validation that never reaches the endpoint
|
|
374
|
+
# proof does not initialize the general subgraph-matching stack.
|
|
375
|
+
from synkit.Graph.Matcher.subgraph_matcher import SubgraphSearchEngine
|
|
376
|
+
|
|
377
|
+
original = _parse_unmapped_endpoint_graph(original_side)
|
|
378
|
+
candidate = _parse_unmapped_endpoint_graph(candidate_side)
|
|
379
|
+
if original is None or candidate is None:
|
|
380
|
+
return None
|
|
381
|
+
|
|
382
|
+
query_has_stereo = bool(stereo_registry_layers(original))
|
|
383
|
+
mappings = SubgraphSearchEngine.find_subgraph_mappings(
|
|
384
|
+
candidate,
|
|
385
|
+
original,
|
|
386
|
+
node_attrs=list(_ENDPOINT_NODE_ATTRS),
|
|
387
|
+
edge_attrs=list(_ENDPOINT_EDGE_ATTRS),
|
|
388
|
+
strategy=Strategy.COMPONENT,
|
|
389
|
+
max_results=None if query_has_stereo else 1,
|
|
390
|
+
strict_cc_count=False,
|
|
391
|
+
threshold=50_000,
|
|
392
|
+
)
|
|
393
|
+
mapping = next(
|
|
394
|
+
(
|
|
395
|
+
structural_mapping
|
|
396
|
+
for structural_mapping in mappings
|
|
397
|
+
if mapped_stereo_subgraph_registries_match(
|
|
398
|
+
original,
|
|
399
|
+
candidate,
|
|
400
|
+
structural_mapping,
|
|
401
|
+
)
|
|
402
|
+
),
|
|
403
|
+
None,
|
|
404
|
+
)
|
|
405
|
+
if mapping is None:
|
|
406
|
+
return None
|
|
407
|
+
|
|
408
|
+
original_components = [
|
|
409
|
+
set(component) for component in nx.connected_components(original)
|
|
410
|
+
]
|
|
411
|
+
candidate_components = [
|
|
412
|
+
set(component) for component in nx.connected_components(candidate)
|
|
413
|
+
]
|
|
414
|
+
candidate_component_by_node = {
|
|
415
|
+
node: component_index
|
|
416
|
+
for component_index, component in enumerate(candidate_components)
|
|
417
|
+
for node in component
|
|
418
|
+
}
|
|
419
|
+
proof: list[dict[str, Any]] = []
|
|
420
|
+
for original_index, component in enumerate(original_components):
|
|
421
|
+
ordered_nodes = sorted(component, key=repr)
|
|
422
|
+
atom_image = [mapping[node] for node in ordered_nodes]
|
|
423
|
+
candidate_component = candidate_component_by_node[atom_image[0]]
|
|
424
|
+
proof.append(
|
|
425
|
+
{
|
|
426
|
+
"original_component": original_index,
|
|
427
|
+
"candidate_component": candidate_component,
|
|
428
|
+
"atom_mapping": [
|
|
429
|
+
[query_node, host_node]
|
|
430
|
+
for query_node, host_node in zip(ordered_nodes, atom_image)
|
|
431
|
+
],
|
|
432
|
+
}
|
|
433
|
+
)
|
|
434
|
+
return proof
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def validate_endpoint_preservation(
|
|
438
|
+
original_rsmi: str,
|
|
439
|
+
candidate_rsmi: str,
|
|
440
|
+
*,
|
|
441
|
+
required_sides: Sequence[str] = ("reactants", "products"),
|
|
442
|
+
) -> FusionValidation:
|
|
443
|
+
"""Prove component-injective endpoint embeddings on selected sides."""
|
|
444
|
+
if original_rsmi.count(">>") != 1 or candidate_rsmi.count(">>") != 1:
|
|
445
|
+
issue = _issue(
|
|
446
|
+
FusionIssueCode.INVALID_REACTION,
|
|
447
|
+
"Endpoint preservation requires two valid reaction separators.",
|
|
448
|
+
)
|
|
449
|
+
return FusionValidation(valid=False, issues=(issue,))
|
|
450
|
+
|
|
451
|
+
original_reactants, original_products = original_rsmi.split(">>", 1)
|
|
452
|
+
candidate_reactants, candidate_products = candidate_rsmi.split(">>", 1)
|
|
453
|
+
required = set(required_sides)
|
|
454
|
+
unknown = required - {"reactants", "products"}
|
|
455
|
+
if unknown:
|
|
456
|
+
raise ValueError(f"Unknown endpoint side(s): {sorted(unknown)!r}")
|
|
457
|
+
|
|
458
|
+
reactant_proof = (
|
|
459
|
+
_endpoint_embedding_proof(original_reactants, candidate_reactants)
|
|
460
|
+
if "reactants" in required
|
|
461
|
+
else []
|
|
462
|
+
)
|
|
463
|
+
product_proof = (
|
|
464
|
+
_endpoint_embedding_proof(original_products, candidate_products)
|
|
465
|
+
if "products" in required
|
|
466
|
+
else []
|
|
467
|
+
)
|
|
468
|
+
issues: list[FusionIssue] = []
|
|
469
|
+
if "reactants" in required and reactant_proof is None:
|
|
470
|
+
issues.append(
|
|
471
|
+
_issue(
|
|
472
|
+
FusionIssueCode.REACTANT_ENDPOINT_NOT_PRESERVED,
|
|
473
|
+
"The original reactant graph does not embed component-wise "
|
|
474
|
+
"in the candidate reactant graph.",
|
|
475
|
+
)
|
|
476
|
+
)
|
|
477
|
+
if "products" in required and product_proof is None:
|
|
478
|
+
issues.append(
|
|
479
|
+
_issue(
|
|
480
|
+
FusionIssueCode.PRODUCT_ENDPOINT_NOT_PRESERVED,
|
|
481
|
+
"The original product graph does not embed component-wise "
|
|
482
|
+
"in the candidate product graph.",
|
|
483
|
+
)
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
evidence: dict[str, Any] = {}
|
|
487
|
+
evidence["matcher"] = "synkit.SubgraphSearchEngine"
|
|
488
|
+
evidence["stereo_policy"] = "synkit.relative_stereo_subgraph"
|
|
489
|
+
if "reactants" in required and reactant_proof is not None:
|
|
490
|
+
evidence["reactant_embeddings"] = reactant_proof
|
|
491
|
+
if "products" in required and product_proof is not None:
|
|
492
|
+
evidence["product_embeddings"] = product_proof
|
|
493
|
+
return FusionValidation(
|
|
494
|
+
valid=not issues,
|
|
495
|
+
issues=tuple(issues),
|
|
496
|
+
evidence=evidence,
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def validate_rbl_candidate(
|
|
501
|
+
original_rsmi: str,
|
|
502
|
+
candidate_rsmi: str,
|
|
503
|
+
*,
|
|
504
|
+
allow_wildcards: bool = False,
|
|
505
|
+
preserve_sides: Sequence[str] = ("products",),
|
|
506
|
+
) -> FusionValidation:
|
|
507
|
+
"""Apply the complete, mode-independent RBL acceptance predicate.
|
|
508
|
+
|
|
509
|
+
RBL reconstructs missing reactants and coproducts, so its default target
|
|
510
|
+
invariant preserves the observed product endpoint. Callers that require
|
|
511
|
+
a conservative balancing-only transformation may additionally request the
|
|
512
|
+
reactant side through ``preserve_sides``.
|
|
513
|
+
"""
|
|
514
|
+
fusion = validate_fusion_rsmi(
|
|
515
|
+
candidate_rsmi,
|
|
516
|
+
allow_wildcards=allow_wildcards,
|
|
517
|
+
)
|
|
518
|
+
preservation = validate_endpoint_preservation(
|
|
519
|
+
original_rsmi,
|
|
520
|
+
candidate_rsmi,
|
|
521
|
+
required_sides=preserve_sides,
|
|
522
|
+
)
|
|
523
|
+
issues = fusion.issues + preservation.issues
|
|
524
|
+
return FusionValidation(
|
|
525
|
+
valid=not issues,
|
|
526
|
+
issues=issues,
|
|
527
|
+
evidence={
|
|
528
|
+
"endpoint_preservation": dict(preservation.evidence),
|
|
529
|
+
},
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _component_inventory(side: str) -> Counter[str] | None:
|
|
534
|
+
"""Return an atom-map-independent exact molecular-component multiset."""
|
|
535
|
+
molecule = _mapped_smiles_mol(side)
|
|
536
|
+
if molecule is None:
|
|
537
|
+
return None
|
|
538
|
+
inventory: Counter[str] = Counter()
|
|
539
|
+
for fragment in Chem.GetMolFrags(molecule, asMols=True, sanitizeFrags=True):
|
|
540
|
+
fragment = Chem.Mol(fragment)
|
|
541
|
+
for atom in fragment.GetAtoms():
|
|
542
|
+
atom.SetAtomMapNum(0)
|
|
543
|
+
try:
|
|
544
|
+
normalized = Chem.RemoveHs(fragment)
|
|
545
|
+
signature = Chem.MolToSmiles(
|
|
546
|
+
normalized,
|
|
547
|
+
canonical=True,
|
|
548
|
+
isomericSmiles=True,
|
|
549
|
+
)
|
|
550
|
+
except (RuntimeError, ValueError):
|
|
551
|
+
return None
|
|
552
|
+
inventory[signature] += 1
|
|
553
|
+
return inventory
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def _material_balance(
|
|
557
|
+
rsmi: str,
|
|
558
|
+
) -> tuple[Counter[tuple[str, int]], Counter[tuple[str, int]], int, int] | None:
|
|
559
|
+
if rsmi.count(">>") != 1:
|
|
560
|
+
return None
|
|
561
|
+
inventories: list[Counter[tuple[str, int]]] = []
|
|
562
|
+
charges: list[int] = []
|
|
563
|
+
for side in rsmi.split(">>"):
|
|
564
|
+
molecule = _mapped_smiles_mol(side)
|
|
565
|
+
if molecule is None:
|
|
566
|
+
return None
|
|
567
|
+
try:
|
|
568
|
+
expanded = Chem.AddHs(molecule)
|
|
569
|
+
except RuntimeError:
|
|
570
|
+
return None
|
|
571
|
+
inventories.append(
|
|
572
|
+
Counter(
|
|
573
|
+
(atom.GetSymbol(), int(atom.GetIsotope()))
|
|
574
|
+
for atom in expanded.GetAtoms()
|
|
575
|
+
)
|
|
576
|
+
)
|
|
577
|
+
charges.append(int(Chem.GetFormalCharge(molecule)))
|
|
578
|
+
return inventories[0], inventories[1], charges[0], charges[1]
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def _resource_delta(
|
|
582
|
+
reactants: Counter[tuple[str, int]],
|
|
583
|
+
products: Counter[tuple[str, int]],
|
|
584
|
+
reactant_charge: int,
|
|
585
|
+
product_charge: int,
|
|
586
|
+
) -> dict[str, int]:
|
|
587
|
+
delta = {
|
|
588
|
+
f"element:{element}:{isotope}": count
|
|
589
|
+
for (element, isotope), count in products.items()
|
|
590
|
+
}
|
|
591
|
+
for (element, isotope), count in reactants.items():
|
|
592
|
+
key = f"element:{element}:{isotope}"
|
|
593
|
+
delta[key] = delta.get(key, 0) - count
|
|
594
|
+
delta = {key: value for key, value in delta.items() if value}
|
|
595
|
+
charge_delta = product_charge - reactant_charge
|
|
596
|
+
if charge_delta:
|
|
597
|
+
delta["formal_charge"] = charge_delta
|
|
598
|
+
return dict(sorted(delta.items()))
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def validate_strict_rbl_candidate( # noqa: C901
|
|
602
|
+
original_rsmi: str,
|
|
603
|
+
candidate_rsmi: str,
|
|
604
|
+
*,
|
|
605
|
+
allow_wildcards: bool = False,
|
|
606
|
+
boundary: str = "closed",
|
|
607
|
+
environment_delta: Mapping[str, int] | None = None,
|
|
608
|
+
require_mapped_material: bool = True,
|
|
609
|
+
) -> FusionValidation:
|
|
610
|
+
"""Validate strict component-completion and conservation semantics.
|
|
611
|
+
|
|
612
|
+
Both observed endpoints must occur as exact molecular-component multisets;
|
|
613
|
+
fragment embeddings are deliberately insufficient. A closed boundary
|
|
614
|
+
requires isotope/element and net-formal-charge conservation. An open
|
|
615
|
+
boundary must declare the exact material/charge delta supplied by its
|
|
616
|
+
environment.
|
|
617
|
+
"""
|
|
618
|
+
if boundary not in {"closed", "open"}:
|
|
619
|
+
raise ValueError("boundary must be 'closed' or 'open'.")
|
|
620
|
+
base = validate_fusion_rsmi(candidate_rsmi, allow_wildcards=allow_wildcards)
|
|
621
|
+
issues = list(base.issues)
|
|
622
|
+
evidence: dict[str, Any] = {
|
|
623
|
+
"observation_relation": "exact_component_multiset_inclusion",
|
|
624
|
+
"boundary": boundary,
|
|
625
|
+
}
|
|
626
|
+
if original_rsmi.count(">>") != 1 or candidate_rsmi.count(">>") != 1:
|
|
627
|
+
return FusionValidation(valid=False, issues=tuple(issues), evidence=evidence)
|
|
628
|
+
|
|
629
|
+
original_sides = original_rsmi.split(">>")
|
|
630
|
+
candidate_sides = candidate_rsmi.split(">>")
|
|
631
|
+
component_evidence: dict[str, Any] = {}
|
|
632
|
+
for index, side_name in enumerate(("reactants", "products")):
|
|
633
|
+
observed = _component_inventory(original_sides[index])
|
|
634
|
+
completed = _component_inventory(candidate_sides[index])
|
|
635
|
+
if observed is None or completed is None:
|
|
636
|
+
issues.append(
|
|
637
|
+
_issue(
|
|
638
|
+
FusionIssueCode.PARSE_FAILURE,
|
|
639
|
+
f"Could not normalize {side_name} component inventory.",
|
|
640
|
+
side=side_name,
|
|
641
|
+
)
|
|
642
|
+
)
|
|
643
|
+
continue
|
|
644
|
+
missing = observed - completed
|
|
645
|
+
component_evidence[side_name] = {
|
|
646
|
+
"observed": dict(sorted(observed.items())),
|
|
647
|
+
"candidate": dict(sorted(completed.items())),
|
|
648
|
+
"missing": dict(sorted(missing.items())),
|
|
649
|
+
}
|
|
650
|
+
if missing:
|
|
651
|
+
code = (
|
|
652
|
+
FusionIssueCode.REACTANT_COMPONENT_NOT_PRESERVED
|
|
653
|
+
if side_name == "reactants"
|
|
654
|
+
else FusionIssueCode.PRODUCT_COMPONENT_NOT_PRESERVED
|
|
655
|
+
)
|
|
656
|
+
issues.append(
|
|
657
|
+
_issue(
|
|
658
|
+
code,
|
|
659
|
+
f"Observed {side_name} components are not an exact multiset "
|
|
660
|
+
"subset of the candidate endpoint.",
|
|
661
|
+
missing=dict(sorted(missing.items())),
|
|
662
|
+
)
|
|
663
|
+
)
|
|
664
|
+
evidence["component_inventory"] = component_evidence
|
|
665
|
+
|
|
666
|
+
balance = _material_balance(candidate_rsmi)
|
|
667
|
+
if balance is None:
|
|
668
|
+
issues.append(
|
|
669
|
+
_issue(
|
|
670
|
+
FusionIssueCode.PARSE_FAILURE,
|
|
671
|
+
"Could not compute candidate material balance.",
|
|
672
|
+
)
|
|
673
|
+
)
|
|
674
|
+
else:
|
|
675
|
+
reactants, products, reactant_charge, product_charge = balance
|
|
676
|
+
delta = _resource_delta(
|
|
677
|
+
reactants,
|
|
678
|
+
products,
|
|
679
|
+
reactant_charge,
|
|
680
|
+
product_charge,
|
|
681
|
+
)
|
|
682
|
+
evidence["resource_delta"] = delta
|
|
683
|
+
if boundary == "closed":
|
|
684
|
+
element_delta = {
|
|
685
|
+
key: value for key, value in delta.items() if key != "formal_charge"
|
|
686
|
+
}
|
|
687
|
+
if element_delta:
|
|
688
|
+
issues.append(
|
|
689
|
+
_issue(
|
|
690
|
+
FusionIssueCode.ELEMENT_ISOTOPE_IMBALANCE,
|
|
691
|
+
"Closed reconstruction does not conserve element/isotope inventory.",
|
|
692
|
+
delta=element_delta,
|
|
693
|
+
)
|
|
694
|
+
)
|
|
695
|
+
if delta.get("formal_charge", 0):
|
|
696
|
+
issues.append(
|
|
697
|
+
_issue(
|
|
698
|
+
FusionIssueCode.CHARGE_IMBALANCE,
|
|
699
|
+
"Closed reconstruction does not conserve net formal charge.",
|
|
700
|
+
delta=delta["formal_charge"],
|
|
701
|
+
)
|
|
702
|
+
)
|
|
703
|
+
else:
|
|
704
|
+
declared = dict(sorted((environment_delta or {}).items()))
|
|
705
|
+
evidence["environment_delta"] = declared
|
|
706
|
+
if delta != declared:
|
|
707
|
+
issues.append(
|
|
708
|
+
_issue(
|
|
709
|
+
FusionIssueCode.ENVIRONMENT_DELTA_MISMATCH,
|
|
710
|
+
"Open reconstruction delta differs from its environment token.",
|
|
711
|
+
observed=delta,
|
|
712
|
+
declared=declared,
|
|
713
|
+
)
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
if require_mapped_material:
|
|
717
|
+
unmapped: dict[str, list[int]] = {}
|
|
718
|
+
for side_name, side in zip(
|
|
719
|
+
("reactants", "products"), candidate_sides, strict=True
|
|
720
|
+
):
|
|
721
|
+
molecule = _mapped_smiles_mol(side)
|
|
722
|
+
if molecule is None:
|
|
723
|
+
continue
|
|
724
|
+
missing_maps = [
|
|
725
|
+
atom.GetIdx()
|
|
726
|
+
for atom in molecule.GetAtoms()
|
|
727
|
+
if atom.GetAtomicNum() > 1 and atom.GetAtomMapNum() <= 0
|
|
728
|
+
]
|
|
729
|
+
if missing_maps:
|
|
730
|
+
unmapped[side_name] = missing_maps
|
|
731
|
+
if unmapped:
|
|
732
|
+
issues.append(
|
|
733
|
+
_issue(
|
|
734
|
+
FusionIssueCode.UNMAPPED_MATERIAL_ATOM,
|
|
735
|
+
"Every material atom in strict reconstruction requires provenance.",
|
|
736
|
+
atoms=unmapped,
|
|
737
|
+
)
|
|
738
|
+
)
|
|
739
|
+
evidence["unmapped_material_atoms"] = unmapped
|
|
740
|
+
|
|
741
|
+
return FusionValidation(
|
|
742
|
+
valid=not issues,
|
|
743
|
+
issues=tuple(issues),
|
|
744
|
+
evidence=evidence,
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def validate_wildcard_mapping_roles(
|
|
749
|
+
graph1: nx.Graph,
|
|
750
|
+
graph2: nx.Graph,
|
|
751
|
+
mapping: Mapping[Any, Any],
|
|
752
|
+
*,
|
|
753
|
+
element_key: str = "element",
|
|
754
|
+
wildcard_element: Any = ("*", "*"),
|
|
755
|
+
role_key: str = "wildcard_role",
|
|
756
|
+
) -> FusionValidation:
|
|
757
|
+
"""Reject an explicit mapping that conflates wildcard semantics.
|
|
758
|
+
|
|
759
|
+
Wildcards pruned before matching never enter this contract. A
|
|
760
|
+
wildcard-to-wildcard mapping requires compatible declared roles. A typed
|
|
761
|
+
wildcard may instead map to a concrete node admitted by its declared
|
|
762
|
+
domain; the verified interface subsequently proves owner incidence and
|
|
763
|
+
substitution. Missing roles fail closed.
|
|
764
|
+
"""
|
|
765
|
+
scalar_wildcard = (
|
|
766
|
+
wildcard_element[0] if isinstance(wildcard_element, tuple) else wildcard_element
|
|
767
|
+
)
|
|
768
|
+
wildcard_values = (wildcard_element, scalar_wildcard)
|
|
769
|
+
issues: list[FusionIssue] = []
|
|
770
|
+
|
|
771
|
+
for node1, node2 in mapping.items():
|
|
772
|
+
if node1 not in graph1 or node2 not in graph2:
|
|
773
|
+
continue
|
|
774
|
+
attrs1 = graph1.nodes[node1]
|
|
775
|
+
attrs2 = graph2.nodes[node2]
|
|
776
|
+
wildcard1 = attrs1.get(element_key) in wildcard_values
|
|
777
|
+
wildcard2 = attrs2.get(element_key) in wildcard_values
|
|
778
|
+
if not wildcard1 and not wildcard2:
|
|
779
|
+
continue
|
|
780
|
+
|
|
781
|
+
raw_role1 = attrs1.get(role_key)
|
|
782
|
+
raw_role2 = attrs2.get(role_key)
|
|
783
|
+
try:
|
|
784
|
+
state1 = adapt_legacy_node_state(
|
|
785
|
+
attrs1,
|
|
786
|
+
element_key=element_key,
|
|
787
|
+
role_key=role_key,
|
|
788
|
+
wildcard_values=wildcard_values,
|
|
789
|
+
)
|
|
790
|
+
constraint1 = (
|
|
791
|
+
state1.constraint if state1.kind is NodeStateKind.WILDCARD else None
|
|
792
|
+
)
|
|
793
|
+
except (TypeError, ValueError):
|
|
794
|
+
constraint1 = None
|
|
795
|
+
try:
|
|
796
|
+
state2 = adapt_legacy_node_state(
|
|
797
|
+
attrs2,
|
|
798
|
+
element_key=element_key,
|
|
799
|
+
role_key=role_key,
|
|
800
|
+
wildcard_values=wildcard_values,
|
|
801
|
+
)
|
|
802
|
+
constraint2 = (
|
|
803
|
+
state2.constraint if state2.kind is NodeStateKind.WILDCARD else None
|
|
804
|
+
)
|
|
805
|
+
except (TypeError, ValueError):
|
|
806
|
+
constraint2 = None
|
|
807
|
+
|
|
808
|
+
compatibility = None
|
|
809
|
+
if constraint1 is not None and constraint2 is not None:
|
|
810
|
+
compatibility = constraint1.relabel_owner(mapping).intersect(constraint2)
|
|
811
|
+
|
|
812
|
+
concrete_constraint = constraint1 if wildcard1 else constraint2
|
|
813
|
+
concrete_attrs = attrs2 if wildcard1 else attrs1
|
|
814
|
+
concrete_element = concrete_attrs.get(element_key)
|
|
815
|
+
if isinstance(concrete_element, (tuple, list)) and len(concrete_element) == 2:
|
|
816
|
+
concrete_element = (
|
|
817
|
+
concrete_element[0]
|
|
818
|
+
if concrete_element[0] == concrete_element[1]
|
|
819
|
+
else None
|
|
820
|
+
)
|
|
821
|
+
concrete_charge = concrete_attrs.get("charge", 0)
|
|
822
|
+
concrete_radical = concrete_attrs.get("radical", 0)
|
|
823
|
+
if isinstance(concrete_charge, (tuple, list)) and len(concrete_charge) == 2:
|
|
824
|
+
concrete_charge = (
|
|
825
|
+
concrete_charge[0] if concrete_charge[0] == concrete_charge[1] else None
|
|
826
|
+
)
|
|
827
|
+
if isinstance(concrete_radical, (tuple, list)) and len(concrete_radical) == 2:
|
|
828
|
+
concrete_radical = (
|
|
829
|
+
concrete_radical[0]
|
|
830
|
+
if concrete_radical[0] == concrete_radical[1]
|
|
831
|
+
else None
|
|
832
|
+
)
|
|
833
|
+
concrete_ok = (
|
|
834
|
+
concrete_constraint is not None
|
|
835
|
+
and concrete_constraint.virtual_kind is None
|
|
836
|
+
and (
|
|
837
|
+
concrete_constraint.elements is None
|
|
838
|
+
or concrete_element in concrete_constraint.elements
|
|
839
|
+
)
|
|
840
|
+
and (
|
|
841
|
+
concrete_constraint.charges is None
|
|
842
|
+
or concrete_charge in concrete_constraint.charges
|
|
843
|
+
)
|
|
844
|
+
and (
|
|
845
|
+
concrete_constraint.radicals is None
|
|
846
|
+
or concrete_radical in concrete_constraint.radicals
|
|
847
|
+
)
|
|
848
|
+
)
|
|
849
|
+
valid = (
|
|
850
|
+
compatibility is not None and compatibility.valid
|
|
851
|
+
if wildcard1 and wildcard2
|
|
852
|
+
else concrete_ok
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
if not valid:
|
|
856
|
+
issues.append(
|
|
857
|
+
_issue(
|
|
858
|
+
FusionIssueCode.WILDCARD_ROLE_CONFLICT,
|
|
859
|
+
"Mapped wildcards require compatible typed constraints.",
|
|
860
|
+
graph1_node=node1,
|
|
861
|
+
graph2_node=node2,
|
|
862
|
+
graph1_role=(
|
|
863
|
+
constraint1.role.value if constraint1 is not None else raw_role1
|
|
864
|
+
),
|
|
865
|
+
graph2_role=(
|
|
866
|
+
constraint2.role.value if constraint2 is not None else raw_role2
|
|
867
|
+
),
|
|
868
|
+
constraint_issues=(
|
|
869
|
+
[item.to_dict() for item in compatibility.issues]
|
|
870
|
+
if compatibility is not None
|
|
871
|
+
else []
|
|
872
|
+
),
|
|
873
|
+
)
|
|
874
|
+
)
|
|
875
|
+
|
|
876
|
+
return FusionValidation(valid=not issues, issues=tuple(issues))
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
def _normalize_completion_materialization(
|
|
880
|
+
source: nx.Graph,
|
|
881
|
+
*,
|
|
882
|
+
element_key: str,
|
|
883
|
+
wildcard_element: Any,
|
|
884
|
+
role_key: str,
|
|
885
|
+
allowed_roles: set[str],
|
|
886
|
+
) -> tuple[nx.Graph, list[dict[str, str]], list[FusionIssue]]:
|
|
887
|
+
"""Materialize typed completion ports on a copy of the source graph."""
|
|
888
|
+
normalized = source.copy()
|
|
889
|
+
scalar_wildcard = (
|
|
890
|
+
wildcard_element[0] if isinstance(wildcard_element, tuple) else wildcard_element
|
|
891
|
+
)
|
|
892
|
+
wildcard_values = (wildcard_element, scalar_wildcard)
|
|
893
|
+
materialized: list[dict[str, str]] = []
|
|
894
|
+
issues: list[FusionIssue] = []
|
|
895
|
+
for node, attributes in normalized.nodes(data=True):
|
|
896
|
+
if attributes.get(element_key) not in wildcard_values:
|
|
897
|
+
continue
|
|
898
|
+
raw_role = attributes.get(role_key)
|
|
899
|
+
role = raw_role.value if isinstance(raw_role, WildcardRole) else raw_role
|
|
900
|
+
if role not in allowed_roles:
|
|
901
|
+
issues.append(
|
|
902
|
+
_issue(
|
|
903
|
+
FusionIssueCode.PROOF_FAILED,
|
|
904
|
+
"Only typed completion wildcards may materialize as hydrogen.",
|
|
905
|
+
node=repr(node),
|
|
906
|
+
wildcard_role=role,
|
|
907
|
+
)
|
|
908
|
+
)
|
|
909
|
+
continue
|
|
910
|
+
attributes[element_key] = (
|
|
911
|
+
("H", "H") if isinstance(attributes.get(element_key), tuple) else "H"
|
|
912
|
+
)
|
|
913
|
+
types_gh = attributes.get("typesGH")
|
|
914
|
+
if isinstance(types_gh, tuple) and len(types_gh) == 2:
|
|
915
|
+
attributes["typesGH"] = tuple(
|
|
916
|
+
(
|
|
917
|
+
(("H",) + tuple(endpoint[1:]))
|
|
918
|
+
if isinstance(endpoint, tuple) and endpoint
|
|
919
|
+
else endpoint
|
|
920
|
+
)
|
|
921
|
+
for endpoint in types_gh
|
|
922
|
+
)
|
|
923
|
+
attributes.pop(role_key, None)
|
|
924
|
+
materialized.append({"node": repr(node), "role": str(role)})
|
|
925
|
+
|
|
926
|
+
for _, attributes in normalized.nodes(data=True):
|
|
927
|
+
neighbors = attributes.get("neighbors")
|
|
928
|
+
if isinstance(neighbors, tuple) and len(neighbors) == 2:
|
|
929
|
+
attributes["neighbors"] = tuple(
|
|
930
|
+
["H" if item == scalar_wildcard else item for item in endpoint or ()]
|
|
931
|
+
for endpoint in neighbors
|
|
932
|
+
)
|
|
933
|
+
elif isinstance(neighbors, list):
|
|
934
|
+
attributes["neighbors"] = [
|
|
935
|
+
"H" if item == scalar_wildcard else item for item in neighbors
|
|
936
|
+
]
|
|
937
|
+
return normalized, materialized, issues
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
def certify_fusion_postprocessing(
|
|
941
|
+
source: nx.Graph,
|
|
942
|
+
target: nx.Graph,
|
|
943
|
+
*,
|
|
944
|
+
materialize_hydrogen: bool,
|
|
945
|
+
element_key: str = "element",
|
|
946
|
+
wildcard_element: Any = ("*", "*"),
|
|
947
|
+
role_key: str = "wildcard_role",
|
|
948
|
+
) -> FusionValidation:
|
|
949
|
+
"""Certify the narrow representation change allowed after a pushout.
|
|
950
|
+
|
|
951
|
+
An exact graph identity is always accepted. When hydrogen
|
|
952
|
+
materialisation is requested, a wildcard may additionally become H only
|
|
953
|
+
when it has an explicit completion role. The consumed role annotation
|
|
954
|
+
and wildcard tokens in cached neighbour/type fields are updated before an
|
|
955
|
+
exact map-invariant graph comparison. No nodes or edges may be added,
|
|
956
|
+
removed, or otherwise changed.
|
|
957
|
+
|
|
958
|
+
This is deliberately narrower than general reaction standardisation: a
|
|
959
|
+
query atom, attachment port, side-presence placeholder, or untyped legacy
|
|
960
|
+
wildcard is never evidence for a hydrogen atom.
|
|
961
|
+
"""
|
|
962
|
+
source_digest = graph_identity_digest(source)
|
|
963
|
+
target_digest = graph_identity_digest(target)
|
|
964
|
+
if graphs_exactly_equivalent(source, target):
|
|
965
|
+
return FusionValidation(
|
|
966
|
+
valid=True,
|
|
967
|
+
evidence={
|
|
968
|
+
"postprocess_proof": {
|
|
969
|
+
"kind": "identity",
|
|
970
|
+
"source_digest": source_digest,
|
|
971
|
+
"normalized_digest": source_digest,
|
|
972
|
+
"target_digest": target_digest,
|
|
973
|
+
"materialized_nodes": [],
|
|
974
|
+
}
|
|
975
|
+
},
|
|
976
|
+
)
|
|
977
|
+
|
|
978
|
+
allowed_roles = {
|
|
979
|
+
WildcardRole.RADICAL_COMPLETION.value,
|
|
980
|
+
WildcardRole.HYDROGEN_COMPLETION.value,
|
|
981
|
+
}
|
|
982
|
+
if not materialize_hydrogen:
|
|
983
|
+
return FusionValidation(
|
|
984
|
+
valid=False,
|
|
985
|
+
issues=(
|
|
986
|
+
_issue(
|
|
987
|
+
FusionIssueCode.PROOF_FAILED,
|
|
988
|
+
"Post-processing changed the certified pushout graph.",
|
|
989
|
+
source_digest=source_digest,
|
|
990
|
+
target_digest=target_digest,
|
|
991
|
+
),
|
|
992
|
+
),
|
|
993
|
+
)
|
|
994
|
+
|
|
995
|
+
normalized, materialized, issues = _normalize_completion_materialization(
|
|
996
|
+
source,
|
|
997
|
+
element_key=element_key,
|
|
998
|
+
wildcard_element=wildcard_element,
|
|
999
|
+
role_key=role_key,
|
|
1000
|
+
allowed_roles=allowed_roles,
|
|
1001
|
+
)
|
|
1002
|
+
|
|
1003
|
+
if issues:
|
|
1004
|
+
return FusionValidation(valid=False, issues=tuple(issues))
|
|
1005
|
+
if not materialized:
|
|
1006
|
+
return FusionValidation(
|
|
1007
|
+
valid=False,
|
|
1008
|
+
issues=(
|
|
1009
|
+
_issue(
|
|
1010
|
+
FusionIssueCode.PROOF_FAILED,
|
|
1011
|
+
"The graph changed without a typed wildcard materialisation.",
|
|
1012
|
+
source_digest=source_digest,
|
|
1013
|
+
target_digest=target_digest,
|
|
1014
|
+
),
|
|
1015
|
+
),
|
|
1016
|
+
)
|
|
1017
|
+
|
|
1018
|
+
normalized_digest = graph_identity_digest(normalized)
|
|
1019
|
+
if not graphs_exactly_equivalent(normalized, target):
|
|
1020
|
+
return FusionValidation(
|
|
1021
|
+
valid=False,
|
|
1022
|
+
issues=(
|
|
1023
|
+
_issue(
|
|
1024
|
+
FusionIssueCode.PROOF_FAILED,
|
|
1025
|
+
"Post-processing exceeds typed wildcard-to-hydrogen materialisation.",
|
|
1026
|
+
source_digest=source_digest,
|
|
1027
|
+
normalized_digest=normalized_digest,
|
|
1028
|
+
target_digest=target_digest,
|
|
1029
|
+
materialized_nodes=materialized,
|
|
1030
|
+
),
|
|
1031
|
+
),
|
|
1032
|
+
)
|
|
1033
|
+
|
|
1034
|
+
return FusionValidation(
|
|
1035
|
+
valid=True,
|
|
1036
|
+
evidence={
|
|
1037
|
+
"postprocess_proof": {
|
|
1038
|
+
"kind": "typed_wildcard_hydrogen_materialization",
|
|
1039
|
+
"source_digest": source_digest,
|
|
1040
|
+
"normalized_digest": normalized_digest,
|
|
1041
|
+
"target_digest": target_digest,
|
|
1042
|
+
"materialized_nodes": materialized,
|
|
1043
|
+
"allowed_roles": sorted(allowed_roles),
|
|
1044
|
+
}
|
|
1045
|
+
},
|
|
1046
|
+
)
|
|
1047
|
+
|
|
1048
|
+
|
|
1049
|
+
__all__: Sequence[str] = (
|
|
1050
|
+
"FusionIssue",
|
|
1051
|
+
"FusionIssueCode",
|
|
1052
|
+
"FusionValidation",
|
|
1053
|
+
"WildcardRole",
|
|
1054
|
+
"certify_fusion_postprocessing",
|
|
1055
|
+
"validate_fusion_rsmi",
|
|
1056
|
+
"validate_endpoint_preservation",
|
|
1057
|
+
"validate_rbl_candidate",
|
|
1058
|
+
"validate_strict_rbl_candidate",
|
|
1059
|
+
"validate_wildcard_mapping_roles",
|
|
1060
|
+
)
|