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,999 @@
|
|
|
1
|
+
"""Finite Lewis-labelled graphs and their strict embedding category.
|
|
2
|
+
|
|
3
|
+
This module defines the generic, non-stereo graph layer used by formal rule
|
|
4
|
+
composition. Objects are finite simple undirected attributed graphs. Arrows
|
|
5
|
+
are total injective node maps whose induced edge maps preserve incidence and
|
|
6
|
+
all declared identity and state labels. Derived labels are checked when an
|
|
7
|
+
object is built but are not independent matching data; annotations are never
|
|
8
|
+
matching data. Partial patterns use the same schema with omitted coordinates;
|
|
9
|
+
DPO embeddings and certificate maps preserve their defined coordinates.
|
|
10
|
+
|
|
11
|
+
The category deliberately makes no blanket adhesivity claim. Pullbacks of
|
|
12
|
+
strict embeddings are available as finite fibre products. A pushout along
|
|
13
|
+
strict embeddings is admitted only when quotienting creates no identity/state
|
|
14
|
+
label conflict. Pushout complements are partial and belong to the DPO layer,
|
|
15
|
+
where dangling and identification premises can be reported explicitly.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from dataclasses import dataclass, field, replace
|
|
21
|
+
from enum import Enum
|
|
22
|
+
from numbers import Integral, Real
|
|
23
|
+
from typing import Any, Hashable, Iterable, Mapping
|
|
24
|
+
|
|
25
|
+
import networkx as nx
|
|
26
|
+
|
|
27
|
+
FrozenValue = Hashable
|
|
28
|
+
LabelBundle = tuple[tuple[str, FrozenValue], ...]
|
|
29
|
+
EdgeKey = frozenset[Hashable]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class LLGIssueCode(str, Enum):
|
|
33
|
+
"""Stable failures for LLG objects, adapters, and morphisms."""
|
|
34
|
+
|
|
35
|
+
SCHEMA_OVERLAP = "LLG_SCHEMA_OVERLAP"
|
|
36
|
+
DIRECTED_GRAPH = "LLG_DIRECTED_GRAPH"
|
|
37
|
+
MULTIGRAPH = "LLG_MULTIGRAPH"
|
|
38
|
+
SELF_LOOP = "LLG_SELF_LOOP"
|
|
39
|
+
DUPLICATE_NODE = "LLG_DUPLICATE_NODE"
|
|
40
|
+
DUPLICATE_EDGE = "LLG_DUPLICATE_EDGE"
|
|
41
|
+
UNKNOWN_ENDPOINT = "LLG_UNKNOWN_ENDPOINT"
|
|
42
|
+
MISSING_NODE_LABEL = "LLG_MISSING_NODE_LABEL"
|
|
43
|
+
MISSING_EDGE_LABEL = "LLG_MISSING_EDGE_LABEL"
|
|
44
|
+
DERIVED_LABEL_MISMATCH = "LLG_DERIVED_LABEL_MISMATCH"
|
|
45
|
+
OUTSIDE_SOURCE = "LLG_MORPHISM_OUTSIDE_SOURCE"
|
|
46
|
+
OUTSIDE_TARGET = "LLG_MORPHISM_OUTSIDE_TARGET"
|
|
47
|
+
PARTIAL_MAPPING = "LLG_MORPHISM_PARTIAL_MAPPING"
|
|
48
|
+
NON_INJECTIVE = "LLG_MORPHISM_NON_INJECTIVE"
|
|
49
|
+
MISSING_EDGE = "LLG_MORPHISM_MISSING_EDGE"
|
|
50
|
+
NODE_LABEL_MISMATCH = "LLG_MORPHISM_NODE_LABEL_MISMATCH"
|
|
51
|
+
EDGE_LABEL_MISMATCH = "LLG_MORPHISM_EDGE_LABEL_MISMATCH"
|
|
52
|
+
ENDPOINT_MISMATCH = "LLG_MORPHISM_ENDPOINT_MISMATCH"
|
|
53
|
+
INVALID_RELABELING = "LLG_INVALID_RELABELING"
|
|
54
|
+
UNSUPPORTED_ITS_FORMAT = "LLG_UNSUPPORTED_ITS_FORMAT"
|
|
55
|
+
LOSSY_ELECTRON_ADAPTER = "LLG_LOSSY_ELECTRON_ADAPTER"
|
|
56
|
+
INVALID_ISOTOPE = "LLG_INVALID_ISOTOPE"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class LLGIssue:
|
|
61
|
+
"""One typed LLG refusal."""
|
|
62
|
+
|
|
63
|
+
code: LLGIssueCode
|
|
64
|
+
message: str
|
|
65
|
+
context: Mapping[str, Any] = field(default_factory=dict)
|
|
66
|
+
|
|
67
|
+
def to_dict(self) -> dict[str, Any]:
|
|
68
|
+
return {
|
|
69
|
+
"code": self.code.value,
|
|
70
|
+
"message": self.message,
|
|
71
|
+
"context": dict(self.context),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class LLGError(ValueError):
|
|
76
|
+
"""Raised when an invalid object or arrow would enter the category."""
|
|
77
|
+
|
|
78
|
+
def __init__(self, *issues: LLGIssue):
|
|
79
|
+
self.issues = tuple(issues)
|
|
80
|
+
super().__init__("; ".join(issue.message for issue in issues))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _freeze(value: Any) -> FrozenValue:
|
|
84
|
+
"""Recursively freeze common mutable attribute values."""
|
|
85
|
+
if isinstance(value, Mapping):
|
|
86
|
+
return tuple(
|
|
87
|
+
sorted(((str(key), _freeze(item)) for key, item in value.items()), key=repr)
|
|
88
|
+
)
|
|
89
|
+
if isinstance(value, (list, tuple)):
|
|
90
|
+
return tuple(_freeze(item) for item in value)
|
|
91
|
+
if isinstance(value, (set, frozenset)):
|
|
92
|
+
return frozenset(_freeze(item) for item in value)
|
|
93
|
+
try:
|
|
94
|
+
hash(value)
|
|
95
|
+
except TypeError as error:
|
|
96
|
+
raise TypeError(
|
|
97
|
+
f"LLG attribute values must be recursively hashable: {value!r}"
|
|
98
|
+
) from error
|
|
99
|
+
return value
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _bundle(values: Mapping[str, Any]) -> LabelBundle:
|
|
103
|
+
return tuple(sorted(((str(key), _freeze(value)) for key, value in values.items())))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _bundle_dict(values: LabelBundle) -> dict[str, FrozenValue]:
|
|
107
|
+
return dict(values)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _carrier_key(value: Hashable) -> tuple[str, str, str]:
|
|
111
|
+
kind = type(value)
|
|
112
|
+
return (kind.__module__, kind.__qualname__, repr(value))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _edge_sort_key(edge: EdgeKey) -> tuple[tuple[str, str, str], ...]:
|
|
116
|
+
return tuple(sorted((_carrier_key(node) for node in edge)))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass(frozen=True)
|
|
120
|
+
class LabelSchema:
|
|
121
|
+
"""Classify labels by mathematical role.
|
|
122
|
+
|
|
123
|
+
Identity and state labels are preserved by every morphism. Derived labels
|
|
124
|
+
must be coherent with authoritative state but are not matched separately.
|
|
125
|
+
Annotation labels, including atom maps, names, and display fields, are
|
|
126
|
+
retained without affecting admissibility.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
node_identity: tuple[str, ...] = ()
|
|
130
|
+
node_state: tuple[str, ...] = ()
|
|
131
|
+
edge_identity: tuple[str, ...] = ()
|
|
132
|
+
edge_state: tuple[str, ...] = ()
|
|
133
|
+
node_derived: tuple[str, ...] = ()
|
|
134
|
+
edge_derived: tuple[str, ...] = ()
|
|
135
|
+
node_annotations: tuple[str, ...] = ()
|
|
136
|
+
edge_annotations: tuple[str, ...] = ()
|
|
137
|
+
name: str = "llg"
|
|
138
|
+
|
|
139
|
+
def __post_init__(self) -> None:
|
|
140
|
+
groups = {
|
|
141
|
+
"node_identity": self.node_identity,
|
|
142
|
+
"node_state": self.node_state,
|
|
143
|
+
"node_derived": self.node_derived,
|
|
144
|
+
"node_annotations": self.node_annotations,
|
|
145
|
+
"edge_identity": self.edge_identity,
|
|
146
|
+
"edge_state": self.edge_state,
|
|
147
|
+
"edge_derived": self.edge_derived,
|
|
148
|
+
"edge_annotations": self.edge_annotations,
|
|
149
|
+
}
|
|
150
|
+
for key, values in groups.items():
|
|
151
|
+
normalized = tuple(dict.fromkeys(str(value) for value in values))
|
|
152
|
+
object.__setattr__(self, key, normalized)
|
|
153
|
+
issues: list[LLGIssue] = []
|
|
154
|
+
for prefix in ("node", "edge"):
|
|
155
|
+
owners: dict[str, list[str]] = {}
|
|
156
|
+
for key, values in groups.items():
|
|
157
|
+
if not key.startswith(prefix):
|
|
158
|
+
continue
|
|
159
|
+
for value in values:
|
|
160
|
+
owners.setdefault(str(value), []).append(key)
|
|
161
|
+
overlap = {key: value for key, value in owners.items() if len(value) > 1}
|
|
162
|
+
if overlap:
|
|
163
|
+
issues.append(
|
|
164
|
+
LLGIssue(
|
|
165
|
+
LLGIssueCode.SCHEMA_OVERLAP,
|
|
166
|
+
f"{prefix.title()} labels must have exactly one class.",
|
|
167
|
+
{"labels": overlap},
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
if issues:
|
|
171
|
+
raise LLGError(*issues)
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def semantic_node_keys(self) -> tuple[str, ...]:
|
|
175
|
+
return self.node_identity + self.node_state
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def semantic_edge_keys(self) -> tuple[str, ...]:
|
|
179
|
+
return self.edge_identity + self.edge_state
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def declared_node_keys(self) -> tuple[str, ...]:
|
|
183
|
+
return self.semantic_node_keys + self.node_derived + self.node_annotations
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def declared_edge_keys(self) -> tuple[str, ...]:
|
|
187
|
+
return self.semantic_edge_keys + self.edge_derived + self.edge_annotations
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
COMMON_CHEMICAL_SCHEMA = LabelSchema(
|
|
191
|
+
node_identity=("element",),
|
|
192
|
+
node_state=("aromatic", "hcount", "charge"),
|
|
193
|
+
edge_state=("order",),
|
|
194
|
+
node_annotations=("atom_map", "source_format"),
|
|
195
|
+
name="chemical-common/1",
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
ELECTRON_LLG_SCHEMA = LabelSchema(
|
|
199
|
+
node_identity=("element",),
|
|
200
|
+
node_state=(
|
|
201
|
+
"aromatic",
|
|
202
|
+
"hcount",
|
|
203
|
+
"radical",
|
|
204
|
+
"lone_pairs",
|
|
205
|
+
"valence_electrons",
|
|
206
|
+
),
|
|
207
|
+
edge_state=("sigma_order", "pi_order"),
|
|
208
|
+
node_derived=("charge", "bond_order_sum"),
|
|
209
|
+
edge_derived=("order", "kekule_order"),
|
|
210
|
+
node_annotations=("atom_map", "source_format"),
|
|
211
|
+
name="lewis-electron/1",
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
ISOTOPE_ELECTRON_LLG_SCHEMA = replace(
|
|
215
|
+
ELECTRON_LLG_SCHEMA,
|
|
216
|
+
node_identity=("element", "isotope"),
|
|
217
|
+
name="lewis-electron-isotope/1",
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def is_electron_schema(schema: LabelSchema) -> bool:
|
|
222
|
+
"""Return whether the schema has the supported authoritative Lewis state."""
|
|
223
|
+
return schema in (ELECTRON_LLG_SCHEMA, ISOTOPE_ELECTRON_LLG_SCHEMA)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _validate_isotopes(nodes, schema):
|
|
227
|
+
if schema != ISOTOPE_ELECTRON_LLG_SCHEMA:
|
|
228
|
+
return
|
|
229
|
+
for node, labels in nodes:
|
|
230
|
+
values = dict(labels)
|
|
231
|
+
if "isotope" not in values:
|
|
232
|
+
continue
|
|
233
|
+
mass = values["isotope"]
|
|
234
|
+
if isinstance(mass, bool) or not isinstance(mass, Integral) or mass < 0:
|
|
235
|
+
raise LLGError(
|
|
236
|
+
LLGIssue(
|
|
237
|
+
LLGIssueCode.INVALID_ISOTOPE,
|
|
238
|
+
"An isotope mass must be a nonnegative integer; zero is unspecified.",
|
|
239
|
+
{"node": repr(node), "isotope": repr(mass)},
|
|
240
|
+
)
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _select_labels(
|
|
245
|
+
values: Mapping[str, Any],
|
|
246
|
+
declared: Iterable[str],
|
|
247
|
+
*,
|
|
248
|
+
required: Iterable[str],
|
|
249
|
+
issue_code: LLGIssueCode,
|
|
250
|
+
owner: Any,
|
|
251
|
+
) -> dict[str, Any]:
|
|
252
|
+
required_set = set(required)
|
|
253
|
+
missing = required_set - set(values)
|
|
254
|
+
if missing:
|
|
255
|
+
raise LLGError(
|
|
256
|
+
LLGIssue(
|
|
257
|
+
issue_code,
|
|
258
|
+
"A declared semantic label is missing.",
|
|
259
|
+
{"owner": repr(owner), "labels": tuple(sorted(missing))},
|
|
260
|
+
)
|
|
261
|
+
)
|
|
262
|
+
selected = {key: values[key] for key in declared if key in values}
|
|
263
|
+
declared_set = set(declared)
|
|
264
|
+
for key, value in values.items():
|
|
265
|
+
if key not in declared_set:
|
|
266
|
+
selected[key if key.startswith("annotation:") else f"annotation:{key}"] = (
|
|
267
|
+
value
|
|
268
|
+
)
|
|
269
|
+
return selected
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@dataclass(frozen=True)
|
|
273
|
+
class LewisLabelledGraph:
|
|
274
|
+
"""Immutable object of the finite LLG category."""
|
|
275
|
+
|
|
276
|
+
schema: LabelSchema
|
|
277
|
+
nodes: tuple[tuple[Hashable, LabelBundle], ...]
|
|
278
|
+
edges: tuple[tuple[EdgeKey, LabelBundle], ...]
|
|
279
|
+
name: Hashable | None = field(default=None, compare=False, hash=False)
|
|
280
|
+
partial: bool = False
|
|
281
|
+
|
|
282
|
+
def __post_init__(self) -> None:
|
|
283
|
+
normalized_nodes = tuple(
|
|
284
|
+
sorted(
|
|
285
|
+
((node, tuple(labels)) for node, labels in self.nodes),
|
|
286
|
+
key=lambda item: _carrier_key(item[0]),
|
|
287
|
+
)
|
|
288
|
+
)
|
|
289
|
+
normalized_edges = tuple(
|
|
290
|
+
sorted(
|
|
291
|
+
((frozenset(edge), tuple(labels)) for edge, labels in self.edges),
|
|
292
|
+
key=lambda item: _edge_sort_key(item[0]),
|
|
293
|
+
)
|
|
294
|
+
)
|
|
295
|
+
object.__setattr__(self, "nodes", normalized_nodes)
|
|
296
|
+
object.__setattr__(self, "edges", normalized_edges)
|
|
297
|
+
_validate_isotopes(normalized_nodes, self.schema)
|
|
298
|
+
|
|
299
|
+
issues: list[LLGIssue] = []
|
|
300
|
+
node_ids = tuple(node for node, _ in normalized_nodes)
|
|
301
|
+
if len(set(node_ids)) != len(node_ids):
|
|
302
|
+
issues.append(
|
|
303
|
+
LLGIssue(LLGIssueCode.DUPLICATE_NODE, "Duplicate carrier node.")
|
|
304
|
+
)
|
|
305
|
+
edge_keys = tuple(edge for edge, _ in normalized_edges)
|
|
306
|
+
if len(set(edge_keys)) != len(edge_keys):
|
|
307
|
+
issues.append(
|
|
308
|
+
LLGIssue(LLGIssueCode.DUPLICATE_EDGE, "Duplicate simple edge.")
|
|
309
|
+
)
|
|
310
|
+
for edge in edge_keys:
|
|
311
|
+
if len(edge) != 2:
|
|
312
|
+
issues.append(
|
|
313
|
+
LLGIssue(
|
|
314
|
+
LLGIssueCode.SELF_LOOP,
|
|
315
|
+
"LLG objects are loop-free simple graphs.",
|
|
316
|
+
{"edge": tuple(map(repr, edge))},
|
|
317
|
+
)
|
|
318
|
+
)
|
|
319
|
+
unknown = set(edge) - set(node_ids)
|
|
320
|
+
if unknown:
|
|
321
|
+
issues.append(
|
|
322
|
+
LLGIssue(
|
|
323
|
+
LLGIssueCode.UNKNOWN_ENDPOINT,
|
|
324
|
+
"An edge endpoint is outside the node carrier.",
|
|
325
|
+
{"nodes": tuple(sorted(map(repr, unknown)))},
|
|
326
|
+
)
|
|
327
|
+
)
|
|
328
|
+
if issues:
|
|
329
|
+
raise LLGError(*issues)
|
|
330
|
+
|
|
331
|
+
# Missing keys represent undefined coordinates only in partial patterns.
|
|
332
|
+
for node, labels in normalized_nodes:
|
|
333
|
+
if not self.partial:
|
|
334
|
+
_select_labels(
|
|
335
|
+
dict(labels),
|
|
336
|
+
self.schema.declared_node_keys,
|
|
337
|
+
required=self.schema.semantic_node_keys,
|
|
338
|
+
issue_code=LLGIssueCode.MISSING_NODE_LABEL,
|
|
339
|
+
owner=node,
|
|
340
|
+
)
|
|
341
|
+
for edge, labels in normalized_edges:
|
|
342
|
+
if not self.partial:
|
|
343
|
+
_select_labels(
|
|
344
|
+
dict(labels),
|
|
345
|
+
self.schema.declared_edge_keys,
|
|
346
|
+
required=self.schema.semantic_edge_keys,
|
|
347
|
+
issue_code=LLGIssueCode.MISSING_EDGE_LABEL,
|
|
348
|
+
owner=edge,
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
@classmethod
|
|
352
|
+
def from_networkx(
|
|
353
|
+
cls,
|
|
354
|
+
graph: nx.Graph,
|
|
355
|
+
schema: LabelSchema,
|
|
356
|
+
*,
|
|
357
|
+
name: Hashable | None = None,
|
|
358
|
+
partial: bool = False,
|
|
359
|
+
) -> "LewisLabelledGraph":
|
|
360
|
+
"""Freeze a NetworkX graph under an explicit label schema."""
|
|
361
|
+
if graph.is_directed():
|
|
362
|
+
raise LLGError(
|
|
363
|
+
LLGIssue(LLGIssueCode.DIRECTED_GRAPH, "LLG objects are undirected.")
|
|
364
|
+
)
|
|
365
|
+
if graph.is_multigraph():
|
|
366
|
+
raise LLGError(
|
|
367
|
+
LLGIssue(LLGIssueCode.MULTIGRAPH, "LLG objects are simple graphs.")
|
|
368
|
+
)
|
|
369
|
+
if nx.number_of_selfloops(graph):
|
|
370
|
+
raise LLGError(
|
|
371
|
+
LLGIssue(LLGIssueCode.SELF_LOOP, "LLG objects are loop-free.")
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
nodes = []
|
|
375
|
+
for node, attrs in graph.nodes(data=True):
|
|
376
|
+
labels = _select_labels(
|
|
377
|
+
attrs,
|
|
378
|
+
schema.declared_node_keys,
|
|
379
|
+
required=() if partial else schema.semantic_node_keys,
|
|
380
|
+
issue_code=LLGIssueCode.MISSING_NODE_LABEL,
|
|
381
|
+
owner=node,
|
|
382
|
+
)
|
|
383
|
+
nodes.append((node, _bundle(labels)))
|
|
384
|
+
edges = []
|
|
385
|
+
for left, right, attrs in graph.edges(data=True):
|
|
386
|
+
labels = _select_labels(
|
|
387
|
+
attrs,
|
|
388
|
+
schema.declared_edge_keys,
|
|
389
|
+
required=() if partial else schema.semantic_edge_keys,
|
|
390
|
+
issue_code=LLGIssueCode.MISSING_EDGE_LABEL,
|
|
391
|
+
owner=(left, right),
|
|
392
|
+
)
|
|
393
|
+
edges.append((frozenset((left, right)), _bundle(labels)))
|
|
394
|
+
return cls(schema, tuple(nodes), tuple(edges), name, partial)
|
|
395
|
+
|
|
396
|
+
@property
|
|
397
|
+
def node_ids(self) -> frozenset[Hashable]:
|
|
398
|
+
return self._index(self.nodes, "_node_index")[1]
|
|
399
|
+
|
|
400
|
+
@property
|
|
401
|
+
def edge_keys(self) -> frozenset[EdgeKey]:
|
|
402
|
+
return self._index(self.edges, "_edge_index")[1]
|
|
403
|
+
|
|
404
|
+
def _index(self, values, cache_name):
|
|
405
|
+
# Cache geometry/bundle references, never label values or validation results.
|
|
406
|
+
# Identity of the authoritative tuple also detects bypassed frozen-field edits.
|
|
407
|
+
cached = self.__dict__.get(cache_name)
|
|
408
|
+
if cached is None or cached[0] is not values:
|
|
409
|
+
lookup = dict(values)
|
|
410
|
+
cached = (values, lookup, frozenset(lookup))
|
|
411
|
+
object.__setattr__(self, cache_name, cached)
|
|
412
|
+
return cached[1:]
|
|
413
|
+
|
|
414
|
+
def node_labels(self, node: Hashable, *, semantic: bool = False) -> dict[str, Any]:
|
|
415
|
+
values = _bundle_dict(self._index(self.nodes, "_node_index")[0][node])
|
|
416
|
+
if semantic:
|
|
417
|
+
return {
|
|
418
|
+
key: values[key]
|
|
419
|
+
for key in self.schema.semantic_node_keys
|
|
420
|
+
if key in values
|
|
421
|
+
}
|
|
422
|
+
return values
|
|
423
|
+
|
|
424
|
+
def edge_labels(self, edge: EdgeKey, *, semantic: bool = False) -> dict[str, Any]:
|
|
425
|
+
values = _bundle_dict(self._index(self.edges, "_edge_index")[0][frozenset(edge)])
|
|
426
|
+
if semantic:
|
|
427
|
+
return {
|
|
428
|
+
key: values[key]
|
|
429
|
+
for key in self.schema.semantic_edge_keys
|
|
430
|
+
if key in values
|
|
431
|
+
}
|
|
432
|
+
return values
|
|
433
|
+
|
|
434
|
+
def _matching_node_labels(self, node: Hashable) -> dict[str, Any]:
|
|
435
|
+
"""Return semantic node labels with aromatic-system invariants.
|
|
436
|
+
|
|
437
|
+
:param node: Carrier node.
|
|
438
|
+
:type node: Hashable
|
|
439
|
+
:return: Labels used by isomorphisms and morphisms.
|
|
440
|
+
:rtype: dict[str, Any]
|
|
441
|
+
"""
|
|
442
|
+
values = self.node_labels(node, semantic=True)
|
|
443
|
+
if (
|
|
444
|
+
self.partial
|
|
445
|
+
or not is_electron_schema(self.schema)
|
|
446
|
+
or not values.get("aromatic", False)
|
|
447
|
+
):
|
|
448
|
+
return values
|
|
449
|
+
pi_valence = 0.0
|
|
450
|
+
for edge in self.edge_keys:
|
|
451
|
+
if node not in edge or not self._is_kekule_aromatic_edge(edge):
|
|
452
|
+
continue
|
|
453
|
+
pi_valence += float(self.edge_labels(edge, semantic=True)["pi_order"])
|
|
454
|
+
values["aromatic_pi_valence"] = pi_valence
|
|
455
|
+
return values
|
|
456
|
+
|
|
457
|
+
def _matching_edge_labels(self, edge: EdgeKey) -> dict[str, Any]:
|
|
458
|
+
"""Return semantic edge labels with Kekulé phase normalized.
|
|
459
|
+
|
|
460
|
+
:param edge: Simple undirected edge key.
|
|
461
|
+
:type edge: EdgeKey
|
|
462
|
+
:return: Labels used by isomorphisms and morphisms.
|
|
463
|
+
:rtype: dict[str, Any]
|
|
464
|
+
"""
|
|
465
|
+
values = self.edge_labels(edge, semantic=True)
|
|
466
|
+
if self._is_kekule_aromatic_edge(edge):
|
|
467
|
+
values["sigma_order"] = "aromatic_delocalized"
|
|
468
|
+
values["pi_order"] = "aromatic_delocalized"
|
|
469
|
+
return values
|
|
470
|
+
|
|
471
|
+
def _is_kekule_aromatic_edge(self, edge: EdgeKey) -> bool:
|
|
472
|
+
"""Return whether an edge belongs to a normal Kekulé aromatic system.
|
|
473
|
+
|
|
474
|
+
:param edge: Simple undirected edge key.
|
|
475
|
+
:type edge: EdgeKey
|
|
476
|
+
:return: Whether Kekulé phase may be normalized for matching.
|
|
477
|
+
:rtype: bool
|
|
478
|
+
"""
|
|
479
|
+
if self.partial or not is_electron_schema(self.schema):
|
|
480
|
+
return False
|
|
481
|
+
endpoints = tuple(edge)
|
|
482
|
+
if len(endpoints) != 2 or not all(
|
|
483
|
+
self.node_labels(node, semantic=True).get("aromatic", False)
|
|
484
|
+
for node in endpoints
|
|
485
|
+
):
|
|
486
|
+
return False
|
|
487
|
+
values = self.edge_labels(edge, semantic=True)
|
|
488
|
+
return _numbers_equal(values.get("sigma_order"), 1) and any(
|
|
489
|
+
_numbers_equal(values.get("pi_order"), phase) for phase in (0, 1)
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
def _aromatic_component(self, node: Hashable) -> frozenset[Hashable]:
|
|
493
|
+
"""Return the normal Kekulé aromatic component containing a node.
|
|
494
|
+
|
|
495
|
+
:param node: Carrier node.
|
|
496
|
+
:type node: Hashable
|
|
497
|
+
:return: Aromatic component, or an empty set for a non-aromatic node.
|
|
498
|
+
:rtype: frozenset[Hashable]
|
|
499
|
+
"""
|
|
500
|
+
if not self.node_labels(node, semantic=True).get("aromatic", False):
|
|
501
|
+
return frozenset()
|
|
502
|
+
graph = nx.Graph()
|
|
503
|
+
graph.add_node(node)
|
|
504
|
+
graph.add_edges_from(
|
|
505
|
+
tuple(edge)
|
|
506
|
+
for edge in self.edge_keys
|
|
507
|
+
if self._is_kekule_aromatic_edge(edge)
|
|
508
|
+
)
|
|
509
|
+
return frozenset(nx.node_connected_component(graph, node))
|
|
510
|
+
|
|
511
|
+
def _matching_graph(self) -> nx.Graph:
|
|
512
|
+
"""Return a graph containing only normalized matching labels.
|
|
513
|
+
|
|
514
|
+
:return: Mutable semantic comparison graph.
|
|
515
|
+
:rtype: nx.Graph
|
|
516
|
+
"""
|
|
517
|
+
graph = nx.Graph()
|
|
518
|
+
graph.add_nodes_from(
|
|
519
|
+
(node, {"labels": self._matching_node_labels(node)})
|
|
520
|
+
for node in self.node_ids
|
|
521
|
+
)
|
|
522
|
+
for edge in self.edge_keys:
|
|
523
|
+
left, right = tuple(edge)
|
|
524
|
+
graph.add_edge(
|
|
525
|
+
left,
|
|
526
|
+
right,
|
|
527
|
+
labels=self._matching_edge_labels(edge),
|
|
528
|
+
)
|
|
529
|
+
return graph
|
|
530
|
+
|
|
531
|
+
def to_networkx(self) -> nx.Graph:
|
|
532
|
+
"""Return a mutable copy. Frozen container values remain immutable."""
|
|
533
|
+
graph = nx.Graph()
|
|
534
|
+
graph.add_nodes_from(
|
|
535
|
+
(node, _bundle_dict(labels)) for node, labels in self.nodes
|
|
536
|
+
)
|
|
537
|
+
for edge, labels in self.edges:
|
|
538
|
+
left, right = tuple(edge)
|
|
539
|
+
graph.add_edge(left, right, **_bundle_dict(labels))
|
|
540
|
+
return graph
|
|
541
|
+
|
|
542
|
+
def relabel(self, mapping: Mapping[Hashable, Hashable]) -> "LewisLabelledGraph":
|
|
543
|
+
"""Transport an object along a total carrier bijection."""
|
|
544
|
+
if set(mapping) != set(self.node_ids) or len(set(mapping.values())) != len(
|
|
545
|
+
mapping
|
|
546
|
+
):
|
|
547
|
+
raise LLGError(
|
|
548
|
+
LLGIssue(
|
|
549
|
+
LLGIssueCode.INVALID_RELABELING,
|
|
550
|
+
"An LLG relabeling must be a total carrier bijection.",
|
|
551
|
+
)
|
|
552
|
+
)
|
|
553
|
+
return LewisLabelledGraph(
|
|
554
|
+
self.schema,
|
|
555
|
+
tuple((mapping[node], labels) for node, labels in self.nodes),
|
|
556
|
+
tuple(
|
|
557
|
+
(frozenset(mapping[node] for node in edge), labels)
|
|
558
|
+
for edge, labels in self.edges
|
|
559
|
+
),
|
|
560
|
+
self.name,
|
|
561
|
+
self.partial,
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
def is_isomorphic(self, other: "LewisLabelledGraph") -> bool:
|
|
565
|
+
"""Decide labeled-graph isomorphism; no digest is a proof.
|
|
566
|
+
|
|
567
|
+
Kekulé single/double placement is phase-invariant inside electron LLG
|
|
568
|
+
aromatic systems. Aromatic node state and local pi valence remain part
|
|
569
|
+
of the match, so phase normalization does not erase electron counts.
|
|
570
|
+
"""
|
|
571
|
+
if self.schema != other.schema:
|
|
572
|
+
return False
|
|
573
|
+
matcher = nx.algorithms.isomorphism.GraphMatcher(
|
|
574
|
+
self._matching_graph(),
|
|
575
|
+
other._matching_graph(),
|
|
576
|
+
node_match=lambda left, right: left["labels"] == right["labels"],
|
|
577
|
+
edge_match=lambda left, right: left["labels"] == right["labels"],
|
|
578
|
+
)
|
|
579
|
+
return matcher.is_isomorphic()
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def _complete_aromatic_morphism_nodes(
|
|
583
|
+
source: LewisLabelledGraph,
|
|
584
|
+
target: LewisLabelledGraph,
|
|
585
|
+
mapping: Mapping[Hashable, Hashable],
|
|
586
|
+
) -> frozenset[Hashable]:
|
|
587
|
+
"""Return source nodes whose complete aromatic systems are mapped.
|
|
588
|
+
|
|
589
|
+
:param source: Morphism source.
|
|
590
|
+
:type source: LewisLabelledGraph
|
|
591
|
+
:param target: Morphism target.
|
|
592
|
+
:type target: LewisLabelledGraph
|
|
593
|
+
:param mapping: Total source-to-target node mapping.
|
|
594
|
+
:type mapping: Mapping[Hashable, Hashable]
|
|
595
|
+
:return: Nodes eligible for Kekulé-phase-invariant comparison.
|
|
596
|
+
:rtype: frozenset[Hashable]
|
|
597
|
+
"""
|
|
598
|
+
complete: set[Hashable] = set()
|
|
599
|
+
for node in source.node_ids:
|
|
600
|
+
source_component = source._aromatic_component(node)
|
|
601
|
+
if not source_component or source_component & complete:
|
|
602
|
+
continue
|
|
603
|
+
target_component = target._aromatic_component(mapping[node])
|
|
604
|
+
mapped_component = frozenset(mapping[item] for item in source_component)
|
|
605
|
+
if mapped_component == target_component:
|
|
606
|
+
complete.update(source_component)
|
|
607
|
+
return frozenset(complete)
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def _morphism_node_labels_preserved(
|
|
611
|
+
source: LewisLabelledGraph,
|
|
612
|
+
target: LewisLabelledGraph,
|
|
613
|
+
node: Hashable,
|
|
614
|
+
image: Hashable,
|
|
615
|
+
phase_invariant_nodes: frozenset[Hashable],
|
|
616
|
+
) -> bool:
|
|
617
|
+
"""Return whether one mapped node preserves matching semantics.
|
|
618
|
+
|
|
619
|
+
:param source: Morphism source.
|
|
620
|
+
:type source: LewisLabelledGraph
|
|
621
|
+
:param target: Morphism target.
|
|
622
|
+
:type target: LewisLabelledGraph
|
|
623
|
+
:param node: Source node.
|
|
624
|
+
:type node: Hashable
|
|
625
|
+
:param image: Target image node.
|
|
626
|
+
:type image: Hashable
|
|
627
|
+
:param phase_invariant_nodes: Complete mapped aromatic-system nodes.
|
|
628
|
+
:type phase_invariant_nodes: frozenset[Hashable]
|
|
629
|
+
:return: Whether node labels are preserved.
|
|
630
|
+
:rtype: bool
|
|
631
|
+
"""
|
|
632
|
+
if node in phase_invariant_nodes:
|
|
633
|
+
return source._matching_node_labels(node) == target._matching_node_labels(image)
|
|
634
|
+
return source.node_labels(node, semantic=True) == target.node_labels(
|
|
635
|
+
image, semantic=True
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def _morphism_edge_labels_preserved(
|
|
640
|
+
source: LewisLabelledGraph,
|
|
641
|
+
target: LewisLabelledGraph,
|
|
642
|
+
edge: EdgeKey,
|
|
643
|
+
image_edge: EdgeKey,
|
|
644
|
+
phase_invariant_nodes: frozenset[Hashable],
|
|
645
|
+
) -> bool:
|
|
646
|
+
"""Return whether one mapped edge preserves matching semantics.
|
|
647
|
+
|
|
648
|
+
:param source: Morphism source.
|
|
649
|
+
:type source: LewisLabelledGraph
|
|
650
|
+
:param target: Morphism target.
|
|
651
|
+
:type target: LewisLabelledGraph
|
|
652
|
+
:param edge: Source edge.
|
|
653
|
+
:type edge: EdgeKey
|
|
654
|
+
:param image_edge: Target image edge.
|
|
655
|
+
:type image_edge: EdgeKey
|
|
656
|
+
:param phase_invariant_nodes: Complete mapped aromatic-system nodes.
|
|
657
|
+
:type phase_invariant_nodes: frozenset[Hashable]
|
|
658
|
+
:return: Whether edge labels are preserved.
|
|
659
|
+
:rtype: bool
|
|
660
|
+
"""
|
|
661
|
+
if edge <= phase_invariant_nodes:
|
|
662
|
+
return source._matching_edge_labels(edge) == target._matching_edge_labels(
|
|
663
|
+
image_edge
|
|
664
|
+
)
|
|
665
|
+
return source.edge_labels(edge, semantic=True) == target.edge_labels(
|
|
666
|
+
image_edge, semantic=True
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
@dataclass(frozen=True)
|
|
671
|
+
class LLGMorphism:
|
|
672
|
+
"""A strict injective incidence- and label-preserving LLG morphism."""
|
|
673
|
+
|
|
674
|
+
source: LewisLabelledGraph
|
|
675
|
+
target: LewisLabelledGraph
|
|
676
|
+
f: tuple[tuple[Hashable, Hashable], ...]
|
|
677
|
+
|
|
678
|
+
def __post_init__(self) -> None:
|
|
679
|
+
pairs = tuple(self.f.items()) if isinstance(self.f, Mapping) else tuple(self.f)
|
|
680
|
+
object.__setattr__(
|
|
681
|
+
self,
|
|
682
|
+
"f",
|
|
683
|
+
tuple(
|
|
684
|
+
sorted(
|
|
685
|
+
pairs,
|
|
686
|
+
key=lambda item: (_carrier_key(item[0]), _carrier_key(item[1])),
|
|
687
|
+
)
|
|
688
|
+
),
|
|
689
|
+
)
|
|
690
|
+
issues: list[LLGIssue] = []
|
|
691
|
+
if self.source.schema != self.target.schema:
|
|
692
|
+
issues.append(
|
|
693
|
+
LLGIssue(
|
|
694
|
+
LLGIssueCode.NODE_LABEL_MISMATCH,
|
|
695
|
+
"Morphism endpoints must use the same label schema.",
|
|
696
|
+
)
|
|
697
|
+
)
|
|
698
|
+
source_keys = tuple(left for left, _ in pairs)
|
|
699
|
+
target_values = tuple(right for _, right in pairs)
|
|
700
|
+
outside_source = set(source_keys) - set(self.source.node_ids)
|
|
701
|
+
outside_target = set(target_values) - set(self.target.node_ids)
|
|
702
|
+
missing = set(self.source.node_ids) - set(source_keys)
|
|
703
|
+
if outside_source:
|
|
704
|
+
issues.append(
|
|
705
|
+
LLGIssue(
|
|
706
|
+
LLGIssueCode.OUTSIDE_SOURCE,
|
|
707
|
+
"The map contains a node outside its source.",
|
|
708
|
+
{"nodes": tuple(sorted(map(repr, outside_source)))},
|
|
709
|
+
)
|
|
710
|
+
)
|
|
711
|
+
if outside_target:
|
|
712
|
+
issues.append(
|
|
713
|
+
LLGIssue(
|
|
714
|
+
LLGIssueCode.OUTSIDE_TARGET,
|
|
715
|
+
"The map contains an image outside its target.",
|
|
716
|
+
{"nodes": tuple(sorted(map(repr, outside_target)))},
|
|
717
|
+
)
|
|
718
|
+
)
|
|
719
|
+
if len(set(source_keys)) != len(source_keys) or missing:
|
|
720
|
+
issues.append(
|
|
721
|
+
LLGIssue(
|
|
722
|
+
LLGIssueCode.PARTIAL_MAPPING,
|
|
723
|
+
"An LLG morphism maps each source node exactly once.",
|
|
724
|
+
{"missing": tuple(sorted(map(repr, missing)))},
|
|
725
|
+
)
|
|
726
|
+
)
|
|
727
|
+
if len(set(target_values)) != len(target_values):
|
|
728
|
+
issues.append(
|
|
729
|
+
LLGIssue(
|
|
730
|
+
LLGIssueCode.NON_INJECTIVE,
|
|
731
|
+
"An LLG morphism is injective on material nodes.",
|
|
732
|
+
)
|
|
733
|
+
)
|
|
734
|
+
if issues:
|
|
735
|
+
raise LLGError(*issues)
|
|
736
|
+
|
|
737
|
+
mapping = dict(pairs)
|
|
738
|
+
phase_invariant_nodes = _complete_aromatic_morphism_nodes(
|
|
739
|
+
self.source, self.target, mapping
|
|
740
|
+
)
|
|
741
|
+
for node, image in pairs:
|
|
742
|
+
if not _morphism_node_labels_preserved(
|
|
743
|
+
self.source,
|
|
744
|
+
self.target,
|
|
745
|
+
node,
|
|
746
|
+
image,
|
|
747
|
+
phase_invariant_nodes,
|
|
748
|
+
):
|
|
749
|
+
issues.append(
|
|
750
|
+
LLGIssue(
|
|
751
|
+
LLGIssueCode.NODE_LABEL_MISMATCH,
|
|
752
|
+
"A node identity or state label is not preserved.",
|
|
753
|
+
{"source": repr(node), "target": repr(image)},
|
|
754
|
+
)
|
|
755
|
+
)
|
|
756
|
+
target_edges = self.target.edge_keys
|
|
757
|
+
for edge in self.source.edge_keys:
|
|
758
|
+
image_edge = frozenset(mapping[node] for node in edge)
|
|
759
|
+
if image_edge not in target_edges:
|
|
760
|
+
issues.append(
|
|
761
|
+
LLGIssue(
|
|
762
|
+
LLGIssueCode.MISSING_EDGE,
|
|
763
|
+
"The induced edge image is absent from the target.",
|
|
764
|
+
{"edge": tuple(sorted(map(repr, edge)))},
|
|
765
|
+
)
|
|
766
|
+
)
|
|
767
|
+
continue
|
|
768
|
+
if not _morphism_edge_labels_preserved(
|
|
769
|
+
self.source,
|
|
770
|
+
self.target,
|
|
771
|
+
edge,
|
|
772
|
+
image_edge,
|
|
773
|
+
phase_invariant_nodes,
|
|
774
|
+
):
|
|
775
|
+
issues.append(
|
|
776
|
+
LLGIssue(
|
|
777
|
+
LLGIssueCode.EDGE_LABEL_MISMATCH,
|
|
778
|
+
"An edge identity or state label is not preserved.",
|
|
779
|
+
{"edge": tuple(sorted(map(repr, edge)))},
|
|
780
|
+
)
|
|
781
|
+
)
|
|
782
|
+
if issues:
|
|
783
|
+
raise LLGError(*issues)
|
|
784
|
+
|
|
785
|
+
@property
|
|
786
|
+
def mapping(self) -> dict[Hashable, Hashable]:
|
|
787
|
+
return dict(self.f)
|
|
788
|
+
|
|
789
|
+
@property
|
|
790
|
+
def edge_mapping(self) -> dict[EdgeKey, EdgeKey]:
|
|
791
|
+
mapping = self.mapping
|
|
792
|
+
return {
|
|
793
|
+
edge: frozenset(mapping[node] for node in edge)
|
|
794
|
+
for edge in self.source.edge_keys
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
@property
|
|
798
|
+
def is_isomorphism(self) -> bool:
|
|
799
|
+
return len(self.source.node_ids) == len(self.target.node_ids) and len(
|
|
800
|
+
self.source.edge_keys
|
|
801
|
+
) == len(self.target.edge_keys)
|
|
802
|
+
|
|
803
|
+
@classmethod
|
|
804
|
+
def identity(cls, graph: LewisLabelledGraph) -> "LLGMorphism":
|
|
805
|
+
return cls(graph, graph, tuple((node, node) for node in graph.node_ids))
|
|
806
|
+
|
|
807
|
+
def then(self, after: "LLGMorphism") -> "LLGMorphism":
|
|
808
|
+
"""Return ``after ∘ self``."""
|
|
809
|
+
if self.target != after.source:
|
|
810
|
+
raise LLGError(
|
|
811
|
+
LLGIssue(
|
|
812
|
+
LLGIssueCode.ENDPOINT_MISMATCH,
|
|
813
|
+
"Composable LLG morphisms must share the same object.",
|
|
814
|
+
)
|
|
815
|
+
)
|
|
816
|
+
right = after.mapping
|
|
817
|
+
return LLGMorphism(
|
|
818
|
+
self.source,
|
|
819
|
+
after.target,
|
|
820
|
+
tuple((node, right[image]) for node, image in self.f),
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
def compose(self, after: "LLGMorphism") -> "LLGMorphism":
|
|
824
|
+
return self.then(after)
|
|
825
|
+
|
|
826
|
+
def relabel(
|
|
827
|
+
self,
|
|
828
|
+
source_labels: Mapping[Hashable, Hashable],
|
|
829
|
+
target_labels: Mapping[Hashable, Hashable],
|
|
830
|
+
) -> "LLGMorphism":
|
|
831
|
+
source = self.source.relabel(source_labels)
|
|
832
|
+
target = self.target.relabel(target_labels)
|
|
833
|
+
return LLGMorphism(
|
|
834
|
+
source,
|
|
835
|
+
target,
|
|
836
|
+
tuple(
|
|
837
|
+
(source_labels[left], target_labels[right]) for left, right in self.f
|
|
838
|
+
),
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
def derive_electron_labeled_graph(graph: nx.Graph) -> nx.Graph:
|
|
843
|
+
"""Return a copy with deterministic Lewis-derived charge and bond orders.
|
|
844
|
+
|
|
845
|
+
Authoritative fields are ``valence_electrons``, ``hcount``, ``lone_pairs``,
|
|
846
|
+
``radical``, ``sigma_order``, and ``pi_order``. A stored derived value may
|
|
847
|
+
be absent; if present and inconsistent, construction fails rather than
|
|
848
|
+
silently rewriting it.
|
|
849
|
+
"""
|
|
850
|
+
result = graph.copy()
|
|
851
|
+
for left, right, attrs in result.edges(data=True):
|
|
852
|
+
try:
|
|
853
|
+
total = float(attrs["sigma_order"]) + float(attrs["pi_order"])
|
|
854
|
+
except KeyError as error:
|
|
855
|
+
raise LLGError(
|
|
856
|
+
LLGIssue(
|
|
857
|
+
LLGIssueCode.MISSING_EDGE_LABEL,
|
|
858
|
+
"Electron LLG edges require sigma_order and pi_order.",
|
|
859
|
+
{"edge": (repr(left), repr(right)), "label": str(error)},
|
|
860
|
+
)
|
|
861
|
+
) from error
|
|
862
|
+
for key in ("order", "kekule_order"):
|
|
863
|
+
if key in attrs and not _numbers_equal(attrs[key], total):
|
|
864
|
+
raise LLGError(
|
|
865
|
+
LLGIssue(
|
|
866
|
+
LLGIssueCode.DERIVED_LABEL_MISMATCH,
|
|
867
|
+
f"Stored {key} disagrees with sigma+pi order.",
|
|
868
|
+
{
|
|
869
|
+
"edge": (repr(left), repr(right)),
|
|
870
|
+
"stored": attrs[key],
|
|
871
|
+
"derived": total,
|
|
872
|
+
},
|
|
873
|
+
)
|
|
874
|
+
)
|
|
875
|
+
attrs[key] = total
|
|
876
|
+
|
|
877
|
+
for node, attrs in result.nodes(data=True):
|
|
878
|
+
required = {"valence_electrons", "hcount", "lone_pairs", "radical"}
|
|
879
|
+
missing = required - set(attrs)
|
|
880
|
+
if missing:
|
|
881
|
+
raise LLGError(
|
|
882
|
+
LLGIssue(
|
|
883
|
+
LLGIssueCode.MISSING_NODE_LABEL,
|
|
884
|
+
"Electron LLG nodes lack authoritative electron state.",
|
|
885
|
+
{"node": repr(node), "labels": tuple(sorted(missing))},
|
|
886
|
+
)
|
|
887
|
+
)
|
|
888
|
+
bond_sum = sum(
|
|
889
|
+
float(edge_attrs["sigma_order"]) + float(edge_attrs["pi_order"])
|
|
890
|
+
for _, _, edge_attrs in result.edges(node, data=True)
|
|
891
|
+
)
|
|
892
|
+
charge = (
|
|
893
|
+
float(attrs["valence_electrons"])
|
|
894
|
+
- 2.0 * float(attrs["lone_pairs"])
|
|
895
|
+
- float(attrs["radical"])
|
|
896
|
+
- float(attrs["hcount"])
|
|
897
|
+
- bond_sum
|
|
898
|
+
)
|
|
899
|
+
charge = int(charge) if charge.is_integer() else charge
|
|
900
|
+
for key, derived in (("bond_order_sum", bond_sum), ("charge", charge)):
|
|
901
|
+
if key in attrs and not _numbers_equal(attrs[key], derived):
|
|
902
|
+
raise LLGError(
|
|
903
|
+
LLGIssue(
|
|
904
|
+
LLGIssueCode.DERIVED_LABEL_MISMATCH,
|
|
905
|
+
f"Stored {key} disagrees with authoritative electron state.",
|
|
906
|
+
{"node": repr(node), "stored": attrs[key], "derived": derived},
|
|
907
|
+
)
|
|
908
|
+
)
|
|
909
|
+
attrs[key] = derived
|
|
910
|
+
return result
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
def _numbers_equal(left: Any, right: Any) -> bool:
|
|
914
|
+
if isinstance(left, Real) and isinstance(right, Real):
|
|
915
|
+
return float(left) == float(right)
|
|
916
|
+
return left == right
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
def llg_from_its(
|
|
920
|
+
graph: nx.Graph,
|
|
921
|
+
side: str | int,
|
|
922
|
+
*,
|
|
923
|
+
electron_complete: bool = False,
|
|
924
|
+
) -> LewisLabelledGraph:
|
|
925
|
+
"""Adapt tuple or legacy ``typesGH`` ITS data to one LLG value contract.
|
|
926
|
+
|
|
927
|
+
The common schema is lossless for element, aromaticity, H count, charge,
|
|
928
|
+
and total bond order in both formats. Electron-complete adaptation is
|
|
929
|
+
available only for tuple ITS because legacy ``typesGH`` does not retain
|
|
930
|
+
side-specific lone pairs, radicals, or sigma/pi decomposition.
|
|
931
|
+
"""
|
|
932
|
+
from synkit.Graph.ITS.its_decompose import its_decompose
|
|
933
|
+
from synkit.Graph.ITS.its_reverter import ITSReverter
|
|
934
|
+
from synkit.IO.chem_converter import detect_its_format
|
|
935
|
+
|
|
936
|
+
side_index = {
|
|
937
|
+
"reactant": 0,
|
|
938
|
+
"r": 0,
|
|
939
|
+
"left": 0,
|
|
940
|
+
0: 0,
|
|
941
|
+
"product": 1,
|
|
942
|
+
"p": 1,
|
|
943
|
+
"right": 1,
|
|
944
|
+
1: 1,
|
|
945
|
+
}.get(side)
|
|
946
|
+
if side_index is None:
|
|
947
|
+
raise ValueError(f"Unsupported ITS side: {side!r}")
|
|
948
|
+
representation = detect_its_format(graph)
|
|
949
|
+
if representation == "tuple":
|
|
950
|
+
molecular = ITSReverter(graph).to_graph(side_index)
|
|
951
|
+
elif representation == "typesGH":
|
|
952
|
+
if electron_complete:
|
|
953
|
+
raise LLGError(
|
|
954
|
+
LLGIssue(
|
|
955
|
+
LLGIssueCode.LOSSY_ELECTRON_ADAPTER,
|
|
956
|
+
"Legacy typesGH lacks side-specific electron fields.",
|
|
957
|
+
)
|
|
958
|
+
)
|
|
959
|
+
molecular = its_decompose(graph)[side_index]
|
|
960
|
+
else: # pragma: no cover - detect_its_format is a closed public contract
|
|
961
|
+
raise LLGError(
|
|
962
|
+
LLGIssue(
|
|
963
|
+
LLGIssueCode.UNSUPPORTED_ITS_FORMAT,
|
|
964
|
+
"Unsupported ITS representation.",
|
|
965
|
+
{"format": repr(representation)},
|
|
966
|
+
)
|
|
967
|
+
)
|
|
968
|
+
|
|
969
|
+
for node in molecular:
|
|
970
|
+
molecular.nodes[node]["source_format"] = representation
|
|
971
|
+
molecular.nodes[node].setdefault("atom_map", node)
|
|
972
|
+
if electron_complete:
|
|
973
|
+
molecular = derive_electron_labeled_graph(molecular)
|
|
974
|
+
return LewisLabelledGraph.from_networkx(
|
|
975
|
+
molecular, ELECTRON_LLG_SCHEMA, name=(representation, side_index)
|
|
976
|
+
)
|
|
977
|
+
|
|
978
|
+
for _, _, attrs in molecular.edges(data=True):
|
|
979
|
+
if "order" not in attrs and "kekule_order" in attrs:
|
|
980
|
+
attrs["order"] = attrs["kekule_order"]
|
|
981
|
+
return LewisLabelledGraph.from_networkx(
|
|
982
|
+
molecular, COMMON_CHEMICAL_SCHEMA, name=(representation, side_index)
|
|
983
|
+
)
|
|
984
|
+
|
|
985
|
+
|
|
986
|
+
__all__ = [
|
|
987
|
+
"COMMON_CHEMICAL_SCHEMA",
|
|
988
|
+
"ELECTRON_LLG_SCHEMA",
|
|
989
|
+
"ISOTOPE_ELECTRON_LLG_SCHEMA",
|
|
990
|
+
"LLGError",
|
|
991
|
+
"LLGIssue",
|
|
992
|
+
"LLGIssueCode",
|
|
993
|
+
"LLGMorphism",
|
|
994
|
+
"LabelSchema",
|
|
995
|
+
"LewisLabelledGraph",
|
|
996
|
+
"derive_electron_labeled_graph",
|
|
997
|
+
"is_electron_schema",
|
|
998
|
+
"llg_from_its",
|
|
999
|
+
]
|