chython 3.0.2__tar.gz → 3.1__tar.gz
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.
- {chython-3.0.2/chython.egg-info → chython-3.1}/PKG-INFO +1 -1
- {chython-3.0.2 → chython-3.1}/chython/chemistry/_canonicalize.py +7 -3
- chython-3.1/chython/chemistry/_organometallics.py +133 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/_standardize.py +12 -1
- {chython-3.0.2 → chython-3.1}/chython/chemistry/tables/salts.tsv +1 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/tables/standardize_metals.tsv +7 -0
- chython-3.1/chython/chemistry/test/test_organometallics.py +219 -0
- {chython-3.0.2 → chython-3.1}/chython/core/_smiles_read.pxi +20 -0
- {chython-3.0.2 → chython-3.1}/chython/core/_smiles_write.pxi +12 -6
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_smiles_read.py +49 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_smiles_write.py +18 -0
- {chython-3.0.2 → chython-3.1}/chython/reactions/tables/functional.tsv +11 -0
- {chython-3.0.2 → chython-3.1}/chython/reactions/test/_frozen_ids.py +5 -1
- {chython-3.0.2 → chython-3.1}/chython/test/test_facade_names.py +1 -1
- {chython-3.0.2 → chython-3.1/chython.egg-info}/PKG-INFO +1 -1
- {chython-3.0.2 → chython-3.1}/chython.egg-info/SOURCES.txt +2 -0
- {chython-3.0.2 → chython-3.1}/pyproject.toml +1 -1
- {chython-3.0.2 → chython-3.1}/LICENSE +0 -0
- {chython-3.0.2 → chython-3.1}/MANIFEST.in +0 -0
- {chython-3.0.2 → chython-3.1}/README.md +0 -0
- {chython-3.0.2 → chython-3.1}/build_inchi.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/__init__.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/_functions.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/__init__.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/_abbreviations.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/_counts.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/_crippen.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/_hydrogens.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/_implicit.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/_isomers.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/_maccs.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/_perceive.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/_pharmacophore.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/_protomers.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/_qed.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/_residues.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/_resonance.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/_salts.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/_saturate.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/_smarts.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/_tables.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/_tpsa.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/tables/abbreviations.tsv +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/tables/acids.tsv +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/tables/covalent_radii.tsv +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/tables/crippen.tsv +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/tables/hbond.tsv +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/tables/maccs.tsv +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/tables/maccs_corpus.tsv +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/tables/pharmacophore.tsv +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/tables/qed_alerts.tsv +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/tables/residues.tsv +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/tables/resonance.tsv +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/tables/rotatable.tsv +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/tables/standardize_groups.tsv +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/tables/sybyl_types.tsv +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/tables/tpsa.tsv +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/__init__.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/_corpus.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/_oracle.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/gen_standardize_rules.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_abbreviations.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_acids_tsv.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_canonicalize.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_counts.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_covalent_radii_tsv.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_crippen.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_crippen_tsv.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_dependency_direction.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_featurizer_injection.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_featurizer_tables_lazy.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_isomers.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_maccs.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_maccs_corpus.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_maccs_tsv.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_perceive.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_pharmacophore.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_protomers.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_qed.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_qed_alerts_tsv.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_reaction_hydrogen_repair.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_reaction_passes.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_residues.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_resonance.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_resonance_tsv.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_salts.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_saturate.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_smarts.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_standardize_differential.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_standardize_groups_port.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_standardize_overvalent_nitrogen.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_standardize_rules_examples.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_standardize_rules_merges.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_standardize_rules_tsv.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_thiele_is_single_purpose.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_tpsa.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_tpsa_tsv.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_valence_report.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_z_translation.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/RULES.md +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/__init__.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/_canonical.pxi +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/_core.pyx +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/_descriptors.pxi +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/_elements.pxi +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/_facade.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/_features.pxi +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/_fingerprints.pxi +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/_hydrogens.pxi +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/_inchi.pxi +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/_isomorphism.pxi +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/_kekule.pxi +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/_log.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/_ml.pxi +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/_molecule_arena.pxi +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/_molecule_container.pxi +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/_molecule_topology.pxi +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/_molecule_views.pxi +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/_morgan.pxi +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/_pach.pxi +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/_pach3.pxi +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/_query_arena.pxi +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/_query_boxes.pxi +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/_query_container.pxi +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/_query_seal.pxi +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/_reaction_passes.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/_rings.pxi +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/_smarts_read.pxi +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/_smirks_patch.pxi +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/_smirks_read.pxi +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/_sssr.pxi +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/_stereo.pxi +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/_thiele.pxi +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/_valence.pxi +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/elements.tsv +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/isotopes.tsv +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/reaction.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/__init__.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/arena_v4_corpus.bin.gz +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/bench_ml.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/chytorch_oracle.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/gen_element_tables.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/gen_modeling_view_corpus.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/gen_pach3_corpus.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/gen_reaction_pach_corpus.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/gen_v3_fixtures.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/gen_v4_fixtures.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/gen_valence_rules.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/modeling_view_corpus.json.gz +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/modeling_view_corpus.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/oracle.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/pach3_corpus.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/pach_corpus.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/pach_v0_corpus.bin.gz +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/pach_v0_native_corpus.bin.gz +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/pach_v2_corpus.bin.gz +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/pach_v3_corpus.bin.gz +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/pach_v4_corpus.bin.gz +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/reaction_pach_corpus.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/reaction_pach_v2_corpus.bin.gz +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_aggregates.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_alternative_spellings.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_apply_scratch_probe.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_arena_f60.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_arena_identity.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_arena_v3_compat.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_arena_v4_compat.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_aromatic_storage.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_canonical.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_canonical_mirror.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_cip_storage.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_clean_isotopes_and_coordinate_bonds.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_clean_stereo.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_conformers.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_container_log.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_copy_caches.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_derive.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_descriptors.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_element_tables.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_facade.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_features.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_featurizer_injection.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_fingerprints.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_geometry.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_h_unknown.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_hydrogens.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_inchi.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_interop_injection.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_isomorphism.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_kekule.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_log.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_magic.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_meta.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_ml_encoding.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_ml_reaction_transition.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_ml_state_view.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_ml_transition_view.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_ml_unpack_differential.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_modeling_view_frozen.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_molecule.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_morgan.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_no_chython_two_imports.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_oracle.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_pach.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_pach3.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_pack.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_query.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_r_edit.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_r_query.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_r_semantics.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_r_serialisation.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_r_smirks.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_r_storage.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_reaction_container.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_reaction_identity.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_reaction_pach.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_reaction_passes.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_reaction_patch_stereo.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_reaction_smiles.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_rings.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_rings_c60.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_set_element.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_sgroups.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_smarts_read.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_smiles_r.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_smiles_roundtrip.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_smiles_write_aromatic.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_smiles_write_cis_trans.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_smiles_write_detached.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_smiles_write_differential.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_smiles_write_h_unknown.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_smiles_write_stereo.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_smiles_write_sticky.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_smirks_filter.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_smirks_patch.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_smirks_read.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_smirks_report.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_smirks_stereo.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_stereo_acceptance.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_stereo_parity.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_stereo_perception.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_stereo_query.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_stereo_units.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_stereo_v2_differential.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_structure.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_thiele.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_title.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_topology.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_union_stereo.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_valence.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/test_view_surface.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/v3_fixtures.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/test/v4_fixtures.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/valence_rules.tsv +0 -0
- {chython-3.0.2 → chython-3.1}/chython/core/wedge.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/__init__.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/_config.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/_hooks.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/bonds.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/colorbar.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/colormap.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/field.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/figure.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/label.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/layout/__init__.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/layout/clean2d.js +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/layout/molecule.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/layout/reaction.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/metrics/__init__.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/metrics/helvetica.tsv +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/metrics/times.tsv +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/overlay.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/render/__init__.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/render/svg.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/scene.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/style.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/test/__init__.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/test/test_bonds.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/test/test_clean2d.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/test/test_colorbar.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/test/test_colormap.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/test/test_field.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/test/test_figure.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/test/test_label.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/test/test_metrics.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/test/test_overlay.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/test/test_package.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/test/test_r_atom.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/test/test_scene.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/test/test_style.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/test/test_svg.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/test/test_wedge_draw.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/test/test_x3dom.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/wedge.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/depict/x3dom.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/exceptions.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/__init__.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/_text.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/CTFILE.md +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/__init__.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/_ctab.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/_errors.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/_facade.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/_hydrogens.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/_rdf.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/_rxn.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/_sdf.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/_sgroup.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/_stream.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/_tokens.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/_v2000.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/_v3000.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/test/__init__.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/test/conftest.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/test/test_aromatic.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/test/test_container_log.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/test/test_data_labels.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/test/test_facade.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/test/test_fidelity.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/test/test_hydrogens.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/test/test_r_atom.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/test/test_rdf.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/test/test_rxn.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/test/test_sdf.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/test/test_sgroup.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/test/test_stream.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/test/test_tokens.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/test/test_v2000.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/test/test_v3000.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/test/test_wedge.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/mol2.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/pdb/__init__.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/pdb/_builder.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/pdb/_legacy.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/pdb/_mmcif.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/pdb/_records.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/pdb/_star.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/test/__init__.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/test/conftest.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/test/oracles.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/test/test_conformance.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/test/test_done_when.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/test/test_isolation.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/test/test_log_prefix.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/test/test_mmcif.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/test/test_mol2.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/test/test_no_review_bookkeeping.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/test/test_oracles.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/test/test_pdb.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/test/test_pdb_builder.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/test/test_perceive_agreement.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/test/test_read_only_facades.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/test/test_xyz.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/test/test_xyz_builder.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/xml/__init__.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/xml/_cml.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/xml/_dialect.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/xml/_errors.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/xml/_facade.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/xml/_mrv.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/xml/_tree.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/xml/test/__init__.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/xml/test/conftest.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/xml/test/test_cml.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/xml/test/test_container_log.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/xml/test/test_dialect.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/xml/test/test_equivalence.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/xml/test/test_facade.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/xml/test/test_mrv.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/xml/test/test_mrv_census.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/xml/test/test_tree.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/formats/xyz.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/interop/__init__.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/interop/_cdk.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/interop/_cdpkit.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/interop/_indigo.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/interop/_iupac.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/interop/_java.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/interop/_openbabel.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/interop/_pandas.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/interop/_rdkit.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/interop/_records.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/interop/_stereo.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/interop/config.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/interop/conformers.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/interop/test/__init__.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/interop/test/conftest.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/interop/test/test_cdk.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/interop/test/test_cdpkit.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/interop/test/test_config.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/interop/test/test_conformers.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/interop/test/test_coordinate_honesty.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/interop/test/test_dispatch.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/interop/test/test_indigo.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/interop/test/test_iupac.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/interop/test/test_log_delivery.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/interop/test/test_openbabel.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/interop/test/test_pandas.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/interop/test/test_rdkit.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/interop/test/test_stereo.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/interop/test/test_v2_oracle.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/reactions/__init__.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/reactions/_enumerate.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/reactions/_numbering.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/reactions/_reconstruct.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/reactions/_stickers.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/reactions/_tables.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/reactions/attention/__init__.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/reactions/attention/_assign.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/reactions/attention/_encode.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/reactions/attention/_session.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/reactions/tables/protective.tsv +0 -0
- {chython-3.0.2 → chython-3.1}/chython/reactions/tables/reactions.tsv +0 -0
- {chython-3.0.2 → chython-3.1}/chython/reactions/tables/roles.tsv +0 -0
- {chython-3.0.2 → chython-3.1}/chython/reactions/test/__init__.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/reactions/test/gen_corpus_glossary.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/reactions/test/golden_subset.smi +0 -0
- {chython-3.0.2 → chython-3.1}/chython/reactions/test/test_attention.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/reactions/test/test_attention_assign.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/reactions/test/test_attention_encode.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/reactions/test/test_attention_isolation.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/reactions/test/test_corpus_glossary.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/reactions/test/test_dependency_direction.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/reactions/test/test_enumerate.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/reactions/test/test_functional.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/reactions/test/test_id_stability.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/reactions/test/test_numbering.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/reactions/test/test_probes.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/reactions/test/test_protective.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/reactions/test/test_reconstruct.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/reactions/test/test_roles.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/reactions/test/test_stickers.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/reactions/test/test_tables.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/test/__init__.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/test/test_code_hygiene.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/test/test_container_methods.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/test/test_doc_figures.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/test/test_doc_references.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/test/test_doc_samples.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/test/test_hydrogen_parity.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/test/test_libinchi_staging.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/test/test_log_records.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/test/test_optional_numpy.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/test/test_packaging.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/test/test_performance.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/test/test_r_atom_integration.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/test/test_release_build.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/test/test_stereo_bluebook.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython/test/test_v2_boundary.py +0 -0
- {chython-3.0.2 → chython-3.1}/chython.egg-info/dependency_links.txt +0 -0
- {chython-3.0.2 → chython-3.1}/chython.egg-info/requires.txt +0 -0
- {chython-3.0.2 → chython-3.1}/chython.egg-info/top_level.txt +0 -0
- {chython-3.0.2 → chython-3.1}/setup.cfg +0 -0
- {chython-3.0.2 → chython-3.1}/setup.py +0 -0
|
@@ -62,9 +62,13 @@ def canonicalize(molecule: MoleculeContainer, *, fix_tautomers: bool = True,
|
|
|
62
62
|
needing the aromatic form.
|
|
63
63
|
|
|
64
64
|
CHARGES ARE PAIRED OFF, not preserved atom by atom: glycine's zwitterion and its neutral drawing
|
|
65
|
-
share a key, because step 4 runs `neutralize()`.
|
|
66
|
-
stays sodium acetate -- there is no proton in it to move -- while ammonium acetate
|
|
67
|
-
acid and ammonia, both drawings of one salt.
|
|
65
|
+
share a key, because step 4 runs `neutralize()`. `neutralize()` leaves the NET charge untouched, so
|
|
66
|
+
sodium acetate stays sodium acetate -- there is no proton in it to move -- while ammonium acetate
|
|
67
|
+
becomes acetic acid and ammonia, both drawings of one salt.
|
|
68
|
+
|
|
69
|
+
One stage does move the net charge, `standardize()`'s organometallic completion: a zinc or magnesium
|
|
70
|
+
holding one carbon and no halide is charged rather than left as a neutral one-coordinate metal. It
|
|
71
|
+
is the only place in the pipeline where the total changes, and it says so in the log.
|
|
68
72
|
"""
|
|
69
73
|
# A refusal at the answer boundary, which is the only place one belongs. `smiles()` returns
|
|
70
74
|
# whichever container its string describes, so a `>>` in a structure column arrives here as a
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2026 Ramil Nugmanov <nougmanoff@protonmail.com>
|
|
4
|
+
# This file is part of chython.
|
|
5
|
+
#
|
|
6
|
+
# chython is free software; you can redistribute it and/or modify
|
|
7
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
|
8
|
+
# the Free Software Foundation; either version 3 of the License, or
|
|
9
|
+
# (at your option) any later version.
|
|
10
|
+
#
|
|
11
|
+
# This program is distributed in the hope that it will be useful,
|
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
# GNU Lesser General Public License for more details.
|
|
15
|
+
#
|
|
16
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
17
|
+
# along with this program; if not, see <https://www.gnu.org/licenses/>.
|
|
18
|
+
#
|
|
19
|
+
"""An organozinc or Grignard drawn apart from its halide: `CC[Zn+].[Cl-]` is one `CC[Zn]Cl`.
|
|
20
|
+
|
|
21
|
+
A ONE-COORDINATE ZINC OR MAGNESIUM HOLDING A CARBON IS AN INCOMPLETE DRAWING, and this stage completes
|
|
22
|
+
it two ways. A free halide in the record is the halide that belongs on the metal, so it is bonded there.
|
|
23
|
+
A metal left without one is charged `+`: the halide is missing from the drawing rather than from the
|
|
24
|
+
compound, and a neutral one-coordinate metal is not a species anybody meant. Both readings apply to
|
|
25
|
+
zinc and magnesium alike.
|
|
26
|
+
|
|
27
|
+
A `standardize()` stage rather than a `standardize_metals.tsv` row, and for one reason: a rule table
|
|
28
|
+
applies whichever match the isomorphism search returned first, so a drawing offering more than one
|
|
29
|
+
candidate would be settled by its atom order. Pairing is a question about all the candidates at once.
|
|
30
|
+
|
|
31
|
+
Two orderings answer it, and neither can see the input's spelling:
|
|
32
|
+
|
|
33
|
+
* **halides** by `Cl > Br > I > F`, a fact about the reagents rather than about the graph;
|
|
34
|
+
* **metals** by canonical rank, which is `_isomers._ranks` without the stripping step -- the bond being
|
|
35
|
+
placed is the one that is absent, so the molecule as it arrived already is the placement-free frame.
|
|
36
|
+
|
|
37
|
+
Ties are left tied. Two candidates of equal rank are automorphic, so which one is taken is not
|
|
38
|
+
observable in the result, and the sort being stable makes the arbitrary half of the choice cheap.
|
|
39
|
+
Charging needs no order at all, being one atom's charge and nothing else's.
|
|
40
|
+
|
|
41
|
+
NET CHARGE MOVES HERE, which no other `standardize()` stage does, and every spelling that moves it is
|
|
42
|
+
accepted: a lone `[Zn+]` beside a neutral halogen sits at `+1`, a neutral metal beside `[Cl-]` at `-1`,
|
|
43
|
+
and a charged metal with no halide leaves `0` for `+1`. A dropped sign is the premise of the stage, so
|
|
44
|
+
the record states which way the total went rather than the pass declining to move it.
|
|
45
|
+
"""
|
|
46
|
+
from collections.abc import MutableSequence
|
|
47
|
+
from ..core import LogRecord, MoleculeContainer
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
__all__ = ['unite_organometallics']
|
|
51
|
+
|
|
52
|
+
#: Table-qualified, as every rule id must be -- never a bare index. Two and not one: bonding a halide
|
|
53
|
+
#: the record HAS and charging a metal whose halide the record LACKS are different claims about the
|
|
54
|
+
#: drawing, and a consumer filtering the log can decline the second while keeping the first.
|
|
55
|
+
_RULE = 'organometallics:unite'
|
|
56
|
+
_RULE_CHARGE = 'organometallics:charge'
|
|
57
|
+
|
|
58
|
+
#: `Cl > Br > I > F`, low wins. Chloride and bromide are the reagent halides; fluoride is last because
|
|
59
|
+
#: a free `[F-]` beside a magnesium is more often a separate salt than a bond somebody forgot to draw.
|
|
60
|
+
_PREFERENCE = {17: 0, 35: 1, 53: 2, 9: 3}
|
|
61
|
+
|
|
62
|
+
#: Zinc and magnesium only. Widening this is a chemical claim per element, not a constant to grow.
|
|
63
|
+
_METALS = frozenset({12, 30})
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _candidates(molecule: MoleculeContainer) -> tuple[list[int], list[int]]:
|
|
67
|
+
"""The one-bonded metals holding a carbon, and the free halides, in arena order.
|
|
68
|
+
|
|
69
|
+
A metal is a candidate at charge `0` or `+1` and at exactly one single bond to carbon: two bonds
|
|
70
|
+
means it is already satisfied, and an alkoxide or amide is a different compound rather than a
|
|
71
|
+
reagent drawn apart. A halide is a candidate at charge `0` or `-1` with no bonds at all -- bonded,
|
|
72
|
+
it belongs to whatever it is bonded to. Radicals are nobody's dropped sign.
|
|
73
|
+
"""
|
|
74
|
+
metals: list[int] = []
|
|
75
|
+
halides: list[int] = []
|
|
76
|
+
for atom in molecule.atoms():
|
|
77
|
+
n = atom.n
|
|
78
|
+
if atom.is_radical:
|
|
79
|
+
continue
|
|
80
|
+
if atom.element in _METALS:
|
|
81
|
+
if atom.degree == 1 and atom.charge in (0, 1):
|
|
82
|
+
m = next(iter(molecule.neighbors_of(n)))
|
|
83
|
+
if molecule.atom(m).element == 6 and molecule.order_of(n, m) == 1:
|
|
84
|
+
metals.append(n)
|
|
85
|
+
elif atom.element in _PREFERENCE and not atom.degree and atom.charge in (0, -1):
|
|
86
|
+
halides.append(n)
|
|
87
|
+
return metals, halides
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def unite_organometallics(molecule: MoleculeContainer, log: MutableSequence) -> set[int]:
|
|
91
|
+
"""Bond each candidate metal to one candidate halide, and charge whichever went without. Written ids.
|
|
92
|
+
|
|
93
|
+
Spare halides are left as drawn -- a second chloride beside a satisfied metal is a counterion -- so
|
|
94
|
+
only the metal side is completed either way.
|
|
95
|
+
"""
|
|
96
|
+
metals, halides = _candidates(molecule)
|
|
97
|
+
if not metals:
|
|
98
|
+
return set()
|
|
99
|
+
|
|
100
|
+
pairs: list[tuple[int, int, int]] = []
|
|
101
|
+
if halides:
|
|
102
|
+
# before the edit scope, which answers reads from the pre-scope arena. `atoms_order` is the
|
|
103
|
+
# expensive word here, so it is asked for only when there is a pairing to decide.
|
|
104
|
+
ranks = molecule.atoms_order
|
|
105
|
+
metals.sort(key=lambda n: ranks[n])
|
|
106
|
+
halides.sort(key=lambda n: (_PREFERENCE[molecule.atom(n).element], molecule.atom(n).isotope,
|
|
107
|
+
ranks[n]))
|
|
108
|
+
pairs = [(n, m, molecule.charge_of(n) + molecule.charge_of(m)) for n, m in zip(metals, halides)]
|
|
109
|
+
|
|
110
|
+
# a metal that took a halide is neutral by the join; one that did not is charged, unless the drawing
|
|
111
|
+
# already said `+`. Ranking cannot matter to this half -- every leftover is treated alike.
|
|
112
|
+
bonded = {n for n, _, _ in pairs}
|
|
113
|
+
stranded = [n for n in metals if n not in bonded and not molecule.charge_of(n)]
|
|
114
|
+
if not pairs and not stranded:
|
|
115
|
+
return set()
|
|
116
|
+
|
|
117
|
+
with molecule.edit():
|
|
118
|
+
for n, m, _ in pairs:
|
|
119
|
+
molecule.add_bond(n, m, 1)
|
|
120
|
+
molecule.set_charge(n, 0)
|
|
121
|
+
molecule.set_charge(m, 0)
|
|
122
|
+
for n in stranded:
|
|
123
|
+
molecule.set_charge(n, 1)
|
|
124
|
+
|
|
125
|
+
for n, m, before in pairs:
|
|
126
|
+
moved = f', and the net charge of the pair moved {before:+d} -> 0' if before else ''
|
|
127
|
+
log.append(LogRecord(_RULE, (n, m), f'atoms {n} and {m} are one organometallic reagent drawn '
|
|
128
|
+
f'apart; joined by a single bond{moved}'))
|
|
129
|
+
for n in stranded:
|
|
130
|
+
log.append(LogRecord(_RULE_CHARGE, (n,), f'atom {n} holds one carbon and no halide; charged +1, '
|
|
131
|
+
f'the halide being absent from the drawing rather than '
|
|
132
|
+
f'from the compound, so the net charge moves 0 -> +1'))
|
|
133
|
+
return bonded | {m for _, m, _ in pairs} | set(stranded)
|
|
@@ -22,9 +22,15 @@ A repair pass the caller asks for -- no reader or writer runs it. The patch lan
|
|
|
22
22
|
charge delta and an optional absolute radical flag per matched atom, plus a new order in {1,2,3,8}
|
|
23
23
|
per matched bond; no rule touches the atom or bond set, an isotope, an aromatic order or a hydrogen
|
|
24
24
|
count. A patch is validated in full before any of it is written.
|
|
25
|
+
|
|
26
|
+
`standardize()` closes with one stage that is not a table row, `_organometallics`: completing a
|
|
27
|
+
one-coordinate zinc or magnesium adds a bond, and which halide joins which metal is a question about
|
|
28
|
+
every candidate at once, neither of which a rule table can state. It is also the one stage that moves
|
|
29
|
+
net charge, a metal nobody drew a halide for being charged instead.
|
|
25
30
|
"""
|
|
26
31
|
from collections.abc import MutableSequence
|
|
27
32
|
from ._implicit import calc_implicit
|
|
33
|
+
from ._organometallics import unite_organometallics
|
|
28
34
|
from ._tables import Rule, groups_rules, metals_rules
|
|
29
35
|
from ..core import LogRecord, MoleculeContainer, recording
|
|
30
36
|
|
|
@@ -128,7 +134,9 @@ def standardize(molecule: MoleculeContainer, *, fix_hydrogens: bool = True,
|
|
|
128
134
|
fix_tautomers: bool = True) -> bool:
|
|
129
135
|
"""Repair mis-drawn functional groups and metal-organic bonding in place. Did anything change?
|
|
130
136
|
|
|
131
|
-
Runs the functional-group rules
|
|
137
|
+
Runs the functional-group rules, then the metal-organic ones, then completes any organozinc or
|
|
138
|
+
Grignard that arrived one-coordinate -- bonding a free halide to it, or charging it when the drawing
|
|
139
|
+
offers none -- then recomputes the implicit
|
|
132
140
|
hydrogen count of every atom a patch wrote -- charge, radical state and bond order all change what
|
|
133
141
|
the valence collection gives an atom. `fix_hydrogens=False` skips that recompute, for a caller about
|
|
134
142
|
to kekulise anyway. `molecule.log` takes a record per patch applied and per patch refused.
|
|
@@ -141,6 +149,9 @@ def standardize(molecule: MoleculeContainer, *, fix_hydrogens: bool = True,
|
|
|
141
149
|
with recording(molecule, stage='standardize') as lg:
|
|
142
150
|
written = _pass(molecule, groups_rules(), lg, fix_tautomers)
|
|
143
151
|
written |= _pass(molecule, metals_rules(), lg, fix_tautomers)
|
|
152
|
+
# last, and the order is not observable: no `metals:` row matches a sigma metal-carbon bond, so
|
|
153
|
+
# none of them can see either the ion pair this reads or the covalent form it writes.
|
|
154
|
+
written |= unite_organometallics(molecule, lg)
|
|
144
155
|
if not written:
|
|
145
156
|
return False
|
|
146
157
|
if fix_hydrogens:
|
|
@@ -87,6 +87,7 @@ salts:benzoic counterion OC(=O)c1ccccc1 - benzoic
|
|
|
87
87
|
salts:salicylic counterion OC(=O)c1ccccc1O - salicylic
|
|
88
88
|
salts:gentisic counterion OC(=O)c1cc(O)ccc1O - gentisic
|
|
89
89
|
salts:xinafoic counterion OC(=O)c1c(O)ccc2ccccc12 - 1-hydroxy-2-naphthoic (xinafoic)
|
|
90
|
+
salts:oxynaphthoic counterion OC(=O)c1cc2ccccc2cc1O - 3-hydroxy-2-naphthoic (beta-oxynaphthoic, BON). Same formula as `salts:xinafoic` and a different constitution, so the key mechanism keeps them apart and both are needed
|
|
90
91
|
salts:pamoic counterion OC(=O)c1c(O)c(Cc2c(O)c(C(O)=O)c3ccccc3c2)cc4ccccc14 - pamoic (embonic)
|
|
91
92
|
salts:hippuric counterion OC(=O)CNC(=O)c1ccccc1 - hippuric
|
|
92
93
|
salts:nicotinic counterion OC(=O)c1cccnc1 - nicotinic
|
|
@@ -18,6 +18,13 @@
|
|
|
18
18
|
#
|
|
19
19
|
# The examples are one metal with one ligand each. Nine of the 19 rules are also exercised by real drawings,
|
|
20
20
|
# in `test/_corpus.py`.
|
|
21
|
+
#
|
|
22
|
+
# NO ROW HERE COMPLETES A ONE-COORDINATE ORGANOZINC OR GRIGNARD, and none can: `bonds_fix` changes the order
|
|
23
|
+
# of a bond that exists, and which of several free halides joins which metal is a question about all the
|
|
24
|
+
# candidates at once rather than about one match. `_organometallics.py` is that stage, run by `standardize()`
|
|
25
|
+
# after this table -- it bonds a free halide to the metal, or charges the metal when the drawing offers none.
|
|
26
|
+
# Its direction is the opposite of these 19 rows and does not compete with them: they break a DATIVE contact
|
|
27
|
+
# from a lone pair, it makes a polar covalent sigma metal-carbon bond.
|
|
21
28
|
|
|
22
29
|
id smarts atom_fix bonds_fix tautomer after examples why
|
|
23
30
|
metals:00 [M;*;^,!^:1]=[C:2]-1-[N;D3;x0;z1:3]-[C;z2:5]-,=[C;z2:6]-[N;D3;x0;z1:4]-1 2:-1:-;3:1:- 1:2:8;2:3:2 0 - C=1N(C)C(=[Cu]I)N(C)C=1>>[C-]=1(N(C)C=C[N+]=1C)~[Cu]I An N-heterocyclic carbene drawn with a covalent M=C double bond; the carbene carbon becomes the [C-] that donates to the metal and the imidazolium charge is restored on a ring nitrogen
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2026 Ramil Nugmanov <nougmanoff@protonmail.com>
|
|
4
|
+
# This file is part of chython.
|
|
5
|
+
#
|
|
6
|
+
# chython is free software; you can redistribute it and/or modify
|
|
7
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
|
8
|
+
# the Free Software Foundation; either version 3 of the License, or
|
|
9
|
+
# (at your option) any later version.
|
|
10
|
+
#
|
|
11
|
+
# This program is distributed in the hope that it will be useful,
|
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
# GNU Lesser General Public License for more details.
|
|
15
|
+
#
|
|
16
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
17
|
+
# along with this program; if not, see <https://www.gnu.org/licenses/>.
|
|
18
|
+
#
|
|
19
|
+
"""Completing a one-coordinate organozinc or Grignard: `[Zn+].[Cl-]` joins, and a halide-less one charges.
|
|
20
|
+
|
|
21
|
+
The invariant under test is that THE RESULT DOES NOT DEPEND ON INPUT ATOM ORDER. Two orderings settle
|
|
22
|
+
it -- halides by `Cl > Br > I > F`, metals by canonical rank -- and every test that could see an order
|
|
23
|
+
effect permutes its input and demands one answer. Charging needs no order, so what is tested there is
|
|
24
|
+
that net charge moves, that it moves once, and that it is a record of its own.
|
|
25
|
+
"""
|
|
26
|
+
# `__all__` and not the package object: `from ... import chemistry` would execute the facade, which
|
|
27
|
+
# `test_dependency_direction.py` ratchets against.
|
|
28
|
+
from itertools import permutations
|
|
29
|
+
from .. import canonicalize, standardize
|
|
30
|
+
from ...core import read_smiles as smiles
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _std(s):
|
|
34
|
+
"""`standardize()` then a canonical string, so two spellings of one answer compare equal."""
|
|
35
|
+
m = smiles(s)
|
|
36
|
+
standardize(m)
|
|
37
|
+
m.canonicalize()
|
|
38
|
+
return format(m, '')
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _one(spellings):
|
|
42
|
+
"""The single canonical answer every spelling gives, or an assertion naming the ones that differ."""
|
|
43
|
+
got = {s: _std(s) for s in spellings}
|
|
44
|
+
assert len(set(got.values())) == 1, got
|
|
45
|
+
return next(iter(got.values()))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# what gets joined
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_every_charge_spelling_of_a_drawn_apart_reagent_reaches_the_covalent_form():
|
|
52
|
+
"""All four spellings are the same reagent: ethylzinc chloride, and phenylmagnesium bromide.
|
|
53
|
+
|
|
54
|
+
Two of the four do not conserve net charge -- a lone `[Zn+]` beside a neutral halogen is at `+1`, a
|
|
55
|
+
neutral metal beside `[Cl-]` at `-1` -- and both are joined anyway, the premise being that the
|
|
56
|
+
drawing dropped a sign rather than that a cation and a radical were meant.
|
|
57
|
+
"""
|
|
58
|
+
assert _one(['CC[Zn+].[Cl-]', 'CC[Zn+].[Cl]', 'CC[Zn].[Cl-]', 'CC[Zn].[Cl]']) == _std('CC[Zn]Cl')
|
|
59
|
+
assert _one(['c1ccccc1[Mg+].[Br-]', 'c1ccccc1[Mg].[Br-]']) == _std('c1ccccc1[Mg]Br')
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_all_four_halogens_are_joined():
|
|
63
|
+
for x in ('F', 'Cl', 'Br', 'I'):
|
|
64
|
+
assert _std(f'CC[Zn+].[{x}-]') == _std(f'CC[Zn]{x}'), x
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_the_joined_form_is_a_fixed_point_and_a_plain_molecule_is_untouched():
|
|
68
|
+
assert _std('CC[Zn]Cl') == _std(_std('CC[Zn]Cl'))
|
|
69
|
+
m = smiles('c1ccccc1O')
|
|
70
|
+
assert not standardize(m)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# what is left alone
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_only_a_one_bonded_metal_holding_a_carbon_takes_a_halide():
|
|
77
|
+
for s in ('C[Zn](C)C.[Cl-]', # already two-coordinate: no room
|
|
78
|
+
'CO[Zn].[Cl-]', # bonded to oxygen, not to carbon: an alkoxide, not a reagent
|
|
79
|
+
'CC[Zn]Cl.[Cl-]', # the metal is satisfied; the second chloride is a counterion
|
|
80
|
+
'CC[Zn+].C[Cl]', # the halogen is bonded, so it is not a free halide
|
|
81
|
+
'CC[Cu+].[Cl-]'): # not Zn or Mg
|
|
82
|
+
m = smiles(s)
|
|
83
|
+
before = format(m, '')
|
|
84
|
+
standardize(m)
|
|
85
|
+
assert format(m, '') == before, s
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_a_metal_with_no_halide_anywhere_is_charged_instead():
|
|
89
|
+
"""A neutral one-coordinate metal is not a species: the halide is missing from the DRAWING.
|
|
90
|
+
|
|
91
|
+
Zinc and magnesium alike, and it is the one thing `standardize()` does that moves net charge.
|
|
92
|
+
"""
|
|
93
|
+
for s, expect in [('CC[Zn]', 'CC[Zn+]'), ('CC[Mg]', 'CC[Mg+]'),
|
|
94
|
+
('c1ccccc1[Mg]', 'c1ccccc1[Mg+]')]:
|
|
95
|
+
m = smiles(s)
|
|
96
|
+
assert standardize(m), s
|
|
97
|
+
assert _std(s) == _std(expect), s
|
|
98
|
+
assert sum(m.charge_of(n) for n in m) == 1, s
|
|
99
|
+
assert not m.check_valence(), s
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_a_metal_the_drawing_already_charged_is_not_charged_twice():
|
|
103
|
+
m = smiles('CC[Zn+]')
|
|
104
|
+
assert not standardize(m)
|
|
105
|
+
assert sum(m.charge_of(n) for n in m) == 1
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_a_metal_left_over_when_the_halides_run_short_is_charged():
|
|
109
|
+
"""One chloride between two reagents: rank decides who takes it, and the other is charged."""
|
|
110
|
+
assert _one(['C[Zn+].CC[Zn].[Cl-]', '[Cl-].CC[Zn].C[Zn+]', 'CC[Zn].[Cl-].C[Zn+]'])
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# the two orderings, each proved by permuting the input
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_a_mixed_halide_set_is_decided_by_element_and_not_by_input_order():
|
|
117
|
+
"""`Cl > Br > I > F`: the metal takes the preferred halogen and the rest stay as counterions."""
|
|
118
|
+
assert _one(['CC[Zn+].[Cl-].[Br-]', 'CC[Zn+].[Br-].[Cl-]',
|
|
119
|
+
'[Br-].[Cl-].CC[Zn+]']) == _std('CC[Zn]Cl.[Br-]')
|
|
120
|
+
assert _one(['CC[Zn+].[Br-].[I-]', '[I-].CC[Zn+].[Br-]']) == _std('CC[Zn]Br.[I-]')
|
|
121
|
+
assert _one(['CC[Zn+].[I-].[F-]', '[F-].[I-].CC[Zn+]']) == _std('CC[Zn]I.[F-]')
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_interchangeable_halides_give_one_answer_whichever_is_picked():
|
|
125
|
+
"""Two identical chlorides are the same choice twice, so the tie needs no breaking."""
|
|
126
|
+
assert _one(['CC[Zn+].[Cl-].[Cl-]', '[Cl-].CC[Zn+].[Cl-]',
|
|
127
|
+
'[Cl-].[Cl-].CC[Zn+]']) == _std('CC[Zn]Cl.[Cl-]')
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_which_metal_takes_the_single_halide_is_decided_by_canonical_rank():
|
|
131
|
+
"""Two different reagents drawn with one chloride between them: rank picks, and picks the same way.
|
|
132
|
+
|
|
133
|
+
This is the case that cannot be a rule-table row -- the table applies whichever match the
|
|
134
|
+
isomorphism search returned first, which is what the input order decides.
|
|
135
|
+
"""
|
|
136
|
+
assert _one(['C[Zn+].CC[Zn+].[Cl-]', 'CC[Zn+].C[Zn+].[Cl-]', '[Cl-].CC[Zn+].C[Zn+]',
|
|
137
|
+
'C[Zn+].[Cl-].CC[Zn+]'])
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_equivalent_metals_are_automorphic_so_the_tie_is_not_observable():
|
|
141
|
+
assert _one(['C[Zn+].C[Zn+].[Cl-]', '[Cl-].C[Zn+].C[Zn+]', 'C[Zn+].[Cl-].C[Zn+]'])
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_several_metals_and_several_halides_are_paired_one_each():
|
|
145
|
+
"""Two reagents, two chlorides: each metal takes one, and no permutation changes the pairing."""
|
|
146
|
+
assert _one([f'{a}.{b}.{c}.{d}' for a, b, c, d in
|
|
147
|
+
permutations(['C[Zn+]', 'CC[Zn+]', '[Cl-]', '[Cl-]'])]) == _std('C[Zn]Cl.CC[Zn]Cl')
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# what the join is worth downstream
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_the_joined_form_is_the_one_the_reaction_corpus_names():
|
|
154
|
+
"""The ion pair answers with the generic carbanion; joined, each reagent names itself."""
|
|
155
|
+
for s, group in [('CC[Zn+].[Cl-]', 'alkyl_zinc'), ('c1ccccc1[Mg+].[Br-]', 'aryl_grignard')]:
|
|
156
|
+
m = smiles(s)
|
|
157
|
+
assert 'metalate_carbanion' in m.functional_groups()
|
|
158
|
+
standardize(m)
|
|
159
|
+
assert group in m.functional_groups(), s
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_the_join_leaves_a_clean_valence_and_recomputes_the_hydrogen_count():
|
|
163
|
+
for s in ('CC[Zn+].[Cl-]', 'CC[Zn].[Cl]', 'c1ccccc1[Mg+].[Br-]'):
|
|
164
|
+
m = smiles(s)
|
|
165
|
+
standardize(m)
|
|
166
|
+
assert not m.check_valence(), s
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# the record
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def test_the_join_is_recorded_against_a_table_qualified_id():
|
|
173
|
+
m = smiles('CC[Zn+].[Cl-]')
|
|
174
|
+
standardize(m)
|
|
175
|
+
records = [r for r in m.log if r.rule.startswith('organometallics:')]
|
|
176
|
+
assert len(records) == 1, [r.rule for r in m.log]
|
|
177
|
+
assert records[0].rule == 'organometallics:unite'
|
|
178
|
+
assert m.log.by_stage('standardize')
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_charging_a_stranded_metal_is_a_separate_record_from_joining_one():
|
|
182
|
+
"""A consumer can decline the weaker claim -- the halide nobody drew -- and keep the join."""
|
|
183
|
+
m = smiles('CC[Zn]')
|
|
184
|
+
standardize(m)
|
|
185
|
+
records = [r for r in m.log if r.rule.startswith('organometallics:')]
|
|
186
|
+
assert [r.rule for r in records] == ['organometallics:charge']
|
|
187
|
+
assert 'no halide' in records[0].message
|
|
188
|
+
|
|
189
|
+
both = smiles('C[Zn].CC[Zn].[Cl-]')
|
|
190
|
+
standardize(both)
|
|
191
|
+
assert {r.rule for r in both.log if r.rule.startswith('organometallics:')} == \
|
|
192
|
+
{'organometallics:unite', 'organometallics:charge'}
|
|
193
|
+
|
|
194
|
+
# and the two halves compose to one answer: whether the leftover metal arrived neutral or already
|
|
195
|
+
# `+`, the halide goes to the same reagent and the other ends up charged either way.
|
|
196
|
+
assert _std('C[Zn].CC[Zn].[Cl-]') == _std('C[Zn+].CC[Zn].[Cl-]') == _std('CC[Zn]Cl.C[Zn+]')
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def test_a_join_that_moves_net_charge_says_so_and_one_that_does_not_stays_quiet_about_it():
|
|
200
|
+
def charge(m):
|
|
201
|
+
return sum(m.charge_of(n) for n in m)
|
|
202
|
+
|
|
203
|
+
conserving, moving = smiles('CC[Zn+].[Cl-]'), smiles('CC[Zn+].[Cl]')
|
|
204
|
+
assert charge(conserving) == 0 and charge(moving) == 1
|
|
205
|
+
for m in (conserving, moving):
|
|
206
|
+
standardize(m)
|
|
207
|
+
assert charge(conserving) == 0 and charge(moving) == 0
|
|
208
|
+
said = [r for r in moving.log if r.rule == 'organometallics:unite'][0].message
|
|
209
|
+
assert 'charge' in said
|
|
210
|
+
assert 'charge' not in [r for r in conserving.log if r.rule == 'organometallics:unite'][0].message
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# the pipeline
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def test_canonicalize_joins_it_too():
|
|
217
|
+
m = smiles('CC[Zn+].[Cl-]')
|
|
218
|
+
canonicalize(m)
|
|
219
|
+
assert format(m, '') == _std('CC[Zn]Cl')
|
|
@@ -1936,6 +1936,26 @@ cdef int smi_marks(smi_parse_t *p, MoleculeContainer mol, object log) except -1:
|
|
|
1936
1936
|
refused.append(mc_record('smiles:cip-refused', (),
|
|
1937
1937
|
'atom %d: the CIP descriptor the `{...}` block names '
|
|
1938
1938
|
'cannot be stored (%s)' % (i + 1, e), mc_lost()))
|
|
1939
|
+
else:
|
|
1940
|
+
# A STORED DESCRIPTOR PUTS ITS CENTRE IN THE ABSOLUTE COLLECTION. The field states
|
|
1941
|
+
# two things -- somebody's completed determination, and that the centre is one such a
|
|
1942
|
+
# determination could be made about -- and the dialect has no `a:` to spell the second
|
|
1943
|
+
# with. The descriptor alone is the weaker half: beside a drawing it is one letter,
|
|
1944
|
+
# while the collection is what says the configuration is THIS one and not one of a set.
|
|
1945
|
+
#
|
|
1946
|
+
# Two guards, both because this reading is the READER'S and not the input's words.
|
|
1947
|
+
# `sg_kind` must be unnamed, a group field being the input saying so directly and the
|
|
1948
|
+
# stronger statement wherever the block spells it -- `{A19=r;A22=r;o1:19,22}` keeps its
|
|
1949
|
+
# OR collection, and would in either field order, the loop above having settled every
|
|
1950
|
+
# group before this line runs for any atom. And the configuration must be STATED: `@?`
|
|
1951
|
+
# and a bare `C` have nothing to be absolute about, and marking them would draw an `a`
|
|
1952
|
+
# beside a methyl in every depiction.
|
|
1953
|
+
#
|
|
1954
|
+
# Identity is untouched -- a configured atom in no collection already means absolute,
|
|
1955
|
+
# so the canonical form does not separate the two -- and `set_stereo_group` cannot
|
|
1956
|
+
# refuse ABS: it forces the group to 0 and validates only the stable id.
|
|
1957
|
+
if not a.sg_kind and (a.chiral == SMI_CHIRAL_AT or a.chiral == SMI_CHIRAL_ATAT):
|
|
1958
|
+
mol.set_stereo_group(a.sid, <int> SMI_SG_ABS, 0)
|
|
1939
1959
|
log.extend(refused)
|
|
1940
1960
|
return 0
|
|
1941
1961
|
|
|
@@ -2318,12 +2318,18 @@ cdef str smw_tail_text(tuple t, list fgroups=None):
|
|
|
2318
2318
|
parts.append('$%s$' % ';'.join(group))
|
|
2319
2319
|
if radicals:
|
|
2320
2320
|
parts.append('^1:' + ','.join(map(str, sorted(radicals))))
|
|
2321
|
-
if abs_atoms
|
|
2322
|
-
#
|
|
2323
|
-
#
|
|
2324
|
-
#
|
|
2325
|
-
#
|
|
2326
|
-
#
|
|
2321
|
+
if abs_atoms:
|
|
2322
|
+
# WHENEVER AN ATOM CARRIES THE ABS KIND, alone or beside an AND or OR collection, so the field
|
|
2323
|
+
# round-trips: an explicit ABS collection is what the arena was told, and a writer that dropped
|
|
2324
|
+
# it would answer "unspecified" to a reader who asked what the input said. Only an EXPLICIT
|
|
2325
|
+
# kind reaches `abs_atoms` -- `smw_tail_parts` collects `kind == 1` and a configured atom in no
|
|
2326
|
+
# collection is kind 0 -- so a plain `F[C@H](Cl)Br` still writes bare.
|
|
2327
|
+
#
|
|
2328
|
+
# The cost, and it is the reason this field was once suppressed alone: a configured atom in no
|
|
2329
|
+
# collection ALREADY means absolute, so `a:` states nothing new about the structure and two
|
|
2330
|
+
# spellings of one compound now write two strings. They still compare and hash EQUAL, the
|
|
2331
|
+
# collection not being part of the canonical form, so the split is in the text only -- and a
|
|
2332
|
+
# cache keyed on the string rather than on the container will store both.
|
|
2327
2333
|
parts.append('a:' + ','.join(map(str, sorted(abs_atoms))))
|
|
2328
2334
|
for key in sorted(and_groups):
|
|
2329
2335
|
parts.append('&%d:%s' % (key, ','.join(map(str, sorted(and_groups[key])))))
|
|
@@ -1132,6 +1132,55 @@ def test_the_q_descriptor_states_no_determination_and_is_not_a_loss():
|
|
|
1132
1132
|
assert len(log) == 1 and log[0].rule == 'smiles:cip-undetermined'
|
|
1133
1133
|
|
|
1134
1134
|
|
|
1135
|
+
def test_a_stated_descriptor_puts_its_centre_in_the_absolute_collection():
|
|
1136
|
+
# `A<i>=<letter>` is TWO statements about the centre: somebody's completed determination, and that
|
|
1137
|
+
# the centre is absolutely configured -- nothing determines a descriptor for a member of an OR or
|
|
1138
|
+
# AND collection. The dialect has no `a:` field beside a descriptor, so the collection is read off
|
|
1139
|
+
# the descriptor, which is the statement that survives a reader who does not want the letter.
|
|
1140
|
+
log = []
|
|
1141
|
+
mol = read_smiles('CC(=O)c1cc2CN(C(=O)OC(C)(C)C)[C@H](C)Cn2n1 {A15=R}', log)
|
|
1142
|
+
assert not log, log
|
|
1143
|
+
assert mol.atom_cips() == {16: 'R'}
|
|
1144
|
+
assert mol.stereo_groups() == {(STEREO_ABS, 0): [16]}
|
|
1145
|
+
# and it is the same collection the pipe dialect spells, so nothing downstream sees two kinds of ABS
|
|
1146
|
+
assert mol.stereo_groups() == read_smiles(
|
|
1147
|
+
'CC(=O)c1cc2CN(C(=O)OC(C)(C)C)[C@H](C)Cn2n1 |a:15|').stereo_groups()
|
|
1148
|
+
|
|
1149
|
+
|
|
1150
|
+
def test_a_descriptor_never_moves_a_centre_out_of_the_collection_a_field_named():
|
|
1151
|
+
# the group fields are the stronger statement and field ORDER must not decide it, so the reading
|
|
1152
|
+
# above applies only where no field of the block named a group for that atom at all.
|
|
1153
|
+
assert read_smiles(BRACE_OR).stereo_groups() == {(STEREO_OR, 1): [20, 23]}
|
|
1154
|
+
for s in ('F[C@H](Cl)Br {o1:1;A1=R}', 'F[C@H](Cl)Br {A1=R;o1:1}'):
|
|
1155
|
+
assert read_smiles(s).stereo_groups() == {(STEREO_OR, 1): [2]}, s
|
|
1156
|
+
|
|
1157
|
+
|
|
1158
|
+
def test_only_a_configured_centre_is_made_absolute_by_its_descriptor():
|
|
1159
|
+
# the collection is the READER's inference and not the input's words, so it is drawn only where
|
|
1160
|
+
# there is a configuration to be absolute about. Otherwise a block that states a descriptor for a
|
|
1161
|
+
# methyl carbon puts an `a` beside a methyl in every depiction of it.
|
|
1162
|
+
assert read_smiles('C[C@H](N)C {A0=R}').stereo_groups() == {} # `A0` is the methyl
|
|
1163
|
+
assert read_smiles('C[C@?H](N)O {A1=R}').stereo_groups() == {} # `@?`: no configuration stated
|
|
1164
|
+
assert read_smiles('C[C@H](N)O {A1=q}').stereo_groups() == {} # `q`: no determination stated
|
|
1165
|
+
assert read_smiles('C[C@H](N)C {A1=E}').stereo_groups() == {} # refused, so nothing to infer from
|
|
1166
|
+
|
|
1167
|
+
|
|
1168
|
+
def test_the_absolute_collection_a_descriptor_implies_is_written_but_is_not_identity():
|
|
1169
|
+
# a configured atom in no collection already means absolute, so the two molecules below are one
|
|
1170
|
+
# compound: they compare and hash EQUAL, the collection not being part of the canonical form. This
|
|
1171
|
+
# is the test that fails if ABS ever enters the canonical bytes.
|
|
1172
|
+
a, b = read_smiles('F[C@H](Cl)Br'), read_smiles('F[C@H](Cl)Br {A1=R}')
|
|
1173
|
+
a.canonicalize()
|
|
1174
|
+
b.canonicalize()
|
|
1175
|
+
assert a == b and hash(a) == hash(b)
|
|
1176
|
+
# and the writer states the collection anyway, so what the input said survives a round trip. The
|
|
1177
|
+
# cost is here and only here: one compound, two strings, so a cache keyed on the TEXT stores both
|
|
1178
|
+
# while one keyed on the container does not.
|
|
1179
|
+
assert format(a, '') == '[C@@H](F)(Cl)Br'
|
|
1180
|
+
assert format(b, '') == '[C@@H](F)(Cl)Br |a:0|'
|
|
1181
|
+
assert format(read_smiles(format(b, '')), '') == format(b, '')
|
|
1182
|
+
|
|
1183
|
+
|
|
1135
1184
|
def test_the_cx_relative_flag_is_a_loss_only_where_it_stands_alone():
|
|
1136
1185
|
# `r` carries no atom list. Beside an `&`/`o` group it restates the group -- the output is
|
|
1137
1186
|
# byte-identical to the same string without it -- and alone it is the only statement that the
|
|
@@ -470,6 +470,24 @@ def test_an_alias_is_written_as_the_tail_s_label_field():
|
|
|
470
470
|
assert write_smiles(m, '!x') == 'C(C)C'
|
|
471
471
|
|
|
472
472
|
|
|
473
|
+
def test_the_abs_collection_is_written_alone_and_beside_another_one():
|
|
474
|
+
# `a:` is written wherever an atom carries the ABS kind, so what the input stated survives the round
|
|
475
|
+
# trip. Alone it says nothing new about the STRUCTURE -- a configured atom in no collection already
|
|
476
|
+
# means absolute -- which is why only an EXPLICIT kind reaches the field: the last line is the
|
|
477
|
+
# negative control that a plain configured centre still writes bare.
|
|
478
|
+
assert write_smiles(read_smiles('F[C@H](Cl)Br |a:1|')) == '[C@@H](F)(Cl)Br |a:0|'
|
|
479
|
+
assert write_smiles(read_smiles('F[C@H](Cl)Br {a:1}')) == '[C@@H](F)(Cl)Br |a:0|'
|
|
480
|
+
# beside an AND or OR collection it keeps its older job as well, naming the centres NOT in one
|
|
481
|
+
text = write_smiles(read_smiles('C[C@H](O)[C@H](N)[C@H](F)C |a:1,&1:3,o1:5|'))
|
|
482
|
+
assert text == 'C[C@@H](F)[C@H]([C@@H](O)C)N |a:4,&1:3,o1:1|'
|
|
483
|
+
# a fixed point, which is the round trip stated without naming a stable id: reading the string back
|
|
484
|
+
# and writing it again reproduces it, so every collection landed on the atom it named
|
|
485
|
+
assert write_smiles(read_smiles(text)) == text
|
|
486
|
+
# and `!x` suppresses this field with the rest of the block
|
|
487
|
+
assert write_smiles(read_smiles('F[C@H](Cl)Br |a:1|'), '!x') == '[C@@H](F)(Cl)Br'
|
|
488
|
+
assert write_smiles(read_smiles('F[C@H](Cl)Br')) == '[C@@H](F)(Cl)Br'
|
|
489
|
+
|
|
490
|
+
|
|
473
491
|
def test_an_alias_survives_a_round_trip_through_the_string():
|
|
474
492
|
# the atoms are re-ordered canonically, so what a round trip preserves is the PAIRING of a label
|
|
475
493
|
# to its atom, which is what comparing the two molecules asserts
|
|
@@ -44,6 +44,12 @@
|
|
|
44
44
|
# it). A sulfone/sulfonyl/nitro centre is `z5`, but a row already writing both `=[O]` out has stated
|
|
45
45
|
# that, so the rows below do not repeat it. `x` counts heteroatom neighbours, and B, S, Si, Sn and Mg
|
|
46
46
|
# all count -- a CF3 carbon bonded to sulfur is `x4`, not `x3`.
|
|
47
|
+
#
|
|
48
|
+
# THE RADICAL STATE IS THE CXSMARTS `|^1:0|` TAIL, there being no in-bracket radical primitive, so a
|
|
49
|
+
# radical row carries one and its `example` carries the matching CXSMILES tail. EACH RADICAL ROW COVERS
|
|
50
|
+
# EXACTLY ONE RADICAL ATOM, which is what lets a caller ask whether every radical in a record is named: the
|
|
51
|
+
# radical rows' counts sum to the radical atom count when they are, and fall short when one is a drawing
|
|
52
|
+
# error. A `decoys` cell splits on commas, so a decoy's tail may name one atom only.
|
|
47
53
|
id name smarts example decoys description
|
|
48
54
|
1 terminal_alkene [C;z2;x0;D1:1]=[C;z2;x0;D2,D3:2] C=CCC CC=CC,C=CN monosubstituted C=C
|
|
49
55
|
2 alkene [C;z2;x0;D2,D3:1]=[C;z2;x0;D2,D3:2] CC=CC C=CCC,CC=CO internal C=C, no heteroatom on either carbon
|
|
@@ -308,3 +314,8 @@ id name smarts example decoys description
|
|
|
308
314
|
247 lactam_4_halide [F,Cl,Br,I;D1]-[C;z2;r6:1]=1-[C;D3;z2;M]-[N;D3;M]-[C,N;z2,z4;M]=,:[C,N;z2,z4;M]-[C,N;z2;M]=1 O=C1N(C)C=CC=C1Cl O=C1N(C)C=CC(Cl)=C1 Activated C(sp2)-X on an N-substituted six-membered lactam, halide at C3 of a 2-pyridinone, adjacent to the carbonyl
|
|
309
315
|
248 lactam_5_halide [F,Cl,Br,I;D1]-[C;z2;r6:1]1=[C,N;z2;M]-[N;D3;M]-[C,N;z2,z4;M]=,:[C,N;z2,z4;M]-[C;D3;z2;M]1 O=C1C(Cl)=CN(C)C=C1 O=C1C=C(Cl)N(C)C=C1 Activated C(sp2)-X on an N-substituted six-membered lactam, halide at C3 of a 4-pyridinone, adjacent to the carbonyl
|
|
310
316
|
249 lactam_6_halide [F,Cl,Br,I;D1]-[C;z2;r6:1]1=[C,N;z2;M]-[N;D3;M]-[C;D3;z2;M]-[C,N;z2,z4;M]=,:[C,N;z2,z4;M]1 O=C1N(C)C=C(Cl)C=C1 O=C1N(C)C(Cl)=CC=C1 Activated C(sp2)-X on an N-substituted six-membered lactam, halide at C5 of a 2-pyridinone
|
|
317
|
+
# --- the persistent radicals, where the substituents are what make the radical isolable ---
|
|
318
|
+
250 nitroxide [O;D1;h0;z1:1]-[N;D3;z1:2](-[C;z1,z4;D3,D4:3])-[C;z1,z4;D3,D4:4] |^1:0| CC1(C)CCCC(C)(C)N1[O] |^1:10| CC1(C)CCCC(C)(C)N1O,CC1(C)CCCC(C)(C)[N+]1=O,C[N]([O])C |^1:2| R2N-O•, the radical on oxygen -- TEMPO and its kin. `D3,D4` on both alpha carbons is the isolability: a nitroxide with an alpha hydrogen disproportionates, so dimethylnitroxide is a decoy and not a match. The hydroxylamine and the oxoammonium cation are the same skeleton at the other two oxidation states
|
|
319
|
+
251 hindered_aryloxyl [O;D1;h0;z1:1]-[C;a:2](:[C;a:3]-[C;D4;z1:4]):[C;a:5]-[C;D4;z1:6] |^1:0| [O]c1c(C(C)(C)C)cc(C(C)(C)C)cc1C(C)(C)C |^1:0| [O]c1ccccc1 |^1:0|,Oc1c(C(C)(C)C)cc(C)cc1C(C)(C)C Ar-O• with a quaternary carbon at BOTH ortho positions, which is what makes it isolable; the unhindered phenoxyl radical is a decoy
|
|
320
|
+
252 hydrazyl [N;D2;h0;z1:1](-[C;a:2])-[N;D3;z1:3](-[C;a:4])-[C;a:5] |^1:0| [N](N(c1ccccc1)c1ccccc1)c1c([N+](=O)[O-])cc([N+](=O)[O-])cc1[N+](=O)[O-] |^1:0| c1ccccc1N(c1ccccc1)Nc1ccccc1,C[N]N(C)C |^1:1| Ar2N-N•-Ar, the DPPH class: three aryl rings to delocalise over, and the trialkyl hydrazyl with none of them is a decoy
|
|
321
|
+
253 triarylmethyl_radical [C;D3;h0;z1:1](-[C;a:2])(-[C;a:3])-[C;a:4] |^1:0| [C](c1ccccc1)(c1ccccc1)c1ccccc1 |^1:0| C(c1ccccc1)(c1ccccc1)c1ccccc1,[C+](c1ccccc1)(c1ccccc1)c1ccccc1,C[C](C)c1ccccc1 |^1:1| Ar3C•, Gomberg's radical and the trityl spin labels. `_radical` is in the name because `protective.tsv`'s trityl rows are this skeleton without one
|
|
@@ -26,7 +26,7 @@ and read the diff. A new line is a new row; a CHANGED line is a renumber, which
|
|
|
26
26
|
means. Never hand-edit a line to make the ratchet pass.
|
|
27
27
|
"""
|
|
28
28
|
|
|
29
|
-
#: `functional.tsv`:
|
|
29
|
+
#: `functional.tsv`: 253 rows, id -> the row it names.
|
|
30
30
|
FUNCTIONAL = {
|
|
31
31
|
'functional:1': 'terminal_alkene',
|
|
32
32
|
'functional:2': 'alkene',
|
|
@@ -277,6 +277,10 @@ FUNCTIONAL = {
|
|
|
277
277
|
'functional:247': 'lactam_4_halide',
|
|
278
278
|
'functional:248': 'lactam_5_halide',
|
|
279
279
|
'functional:249': 'lactam_6_halide',
|
|
280
|
+
'functional:250': 'nitroxide',
|
|
281
|
+
'functional:251': 'hindered_aryloxyl',
|
|
282
|
+
'functional:252': 'hydrazyl',
|
|
283
|
+
'functional:253': 'triarylmethyl_radical',
|
|
280
284
|
}
|
|
281
285
|
|
|
282
286
|
#: `protective.tsv`: 103 rows, id -> the row it names.
|
|
@@ -177,7 +177,7 @@ def test_the_whole_corpus_is_reachable_without_a_molecule():
|
|
|
177
177
|
import chython
|
|
178
178
|
|
|
179
179
|
functional, protective = chython.functional_rules(), chython.protective_rules()
|
|
180
|
-
assert len(functional) ==
|
|
180
|
+
assert len(functional) == 253 and len(protective) == 103
|
|
181
181
|
assert all(isinstance(k, str) and v.name == k for k, v in functional.items())
|
|
182
182
|
assert all(isinstance(k, str) and v.name == k for k, v in protective.items())
|
|
183
183
|
assert functional['carboxylic_acid'].smarts and protective['amine_boc'].protects == ('amine',)
|