chython 3.0__cp313-cp313-macosx_10_13_universal2.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.
- chython/__init__.py +68 -0
- chython/_functions.py +113 -0
- chython/chemistry/__init__.py +86 -0
- chython/chemistry/_abbreviations.py +127 -0
- chython/chemistry/_canonicalize.py +152 -0
- chython/chemistry/_counts.py +81 -0
- chython/chemistry/_crippen.py +93 -0
- chython/chemistry/_hydrogens.py +174 -0
- chython/chemistry/_implicit.py +87 -0
- chython/chemistry/_isomers.py +564 -0
- chython/chemistry/_maccs.py +156 -0
- chython/chemistry/_perceive.py +164 -0
- chython/chemistry/_pharmacophore.py +83 -0
- chython/chemistry/_protomers.py +215 -0
- chython/chemistry/_qed.py +137 -0
- chython/chemistry/_residues.py +192 -0
- chython/chemistry/_resonance.py +393 -0
- chython/chemistry/_salts.py +308 -0
- chython/chemistry/_saturate.py +472 -0
- chython/chemistry/_smarts.py +75 -0
- chython/chemistry/_standardize.py +149 -0
- chython/chemistry/_tables.py +1139 -0
- chython/chemistry/_tpsa.py +69 -0
- chython/chemistry/tables/abbreviations.tsv +93 -0
- chython/chemistry/tables/acids.tsv +37 -0
- chython/chemistry/tables/covalent_radii.tsv +109 -0
- chython/chemistry/tables/crippen.tsv +146 -0
- chython/chemistry/tables/hbond.tsv +29 -0
- chython/chemistry/tables/maccs.tsv +218 -0
- chython/chemistry/tables/maccs_corpus.tsv +342 -0
- chython/chemistry/tables/pharmacophore.tsv +21 -0
- chython/chemistry/tables/qed_alerts.tsv +86 -0
- chython/chemistry/tables/residues.tsv +119 -0
- chython/chemistry/tables/resonance.tsv +58 -0
- chython/chemistry/tables/rotatable.tsv +11 -0
- chython/chemistry/tables/salts.tsv +162 -0
- chython/chemistry/tables/standardize_groups.tsv +165 -0
- chython/chemistry/tables/standardize_metals.tsv +41 -0
- chython/chemistry/tables/sybyl_types.tsv +75 -0
- chython/chemistry/tables/tpsa.tsv +56 -0
- chython/chemistry/test/__init__.py +18 -0
- chython/chemistry/test/_corpus.py +110 -0
- chython/chemistry/test/_oracle.py +49 -0
- chython/chemistry/test/gen_standardize_rules.py +538 -0
- chython/chemistry/test/test_abbreviations.py +231 -0
- chython/chemistry/test/test_acids_tsv.py +121 -0
- chython/chemistry/test/test_canonicalize.py +606 -0
- chython/chemistry/test/test_counts.py +221 -0
- chython/chemistry/test/test_covalent_radii_tsv.py +179 -0
- chython/chemistry/test/test_crippen.py +159 -0
- chython/chemistry/test/test_crippen_tsv.py +168 -0
- chython/chemistry/test/test_dependency_direction.py +164 -0
- chython/chemistry/test/test_featurizer_injection.py +100 -0
- chython/chemistry/test/test_featurizer_tables_lazy.py +80 -0
- chython/chemistry/test/test_isomers.py +390 -0
- chython/chemistry/test/test_maccs.py +135 -0
- chython/chemistry/test/test_maccs_corpus.py +74 -0
- chython/chemistry/test/test_maccs_tsv.py +123 -0
- chython/chemistry/test/test_perceive.py +226 -0
- chython/chemistry/test/test_pharmacophore.py +236 -0
- chython/chemistry/test/test_protomers.py +229 -0
- chython/chemistry/test/test_qed.py +128 -0
- chython/chemistry/test/test_qed_alerts_tsv.py +102 -0
- chython/chemistry/test/test_reaction_hydrogen_repair.py +78 -0
- chython/chemistry/test/test_reaction_passes.py +158 -0
- chython/chemistry/test/test_residues.py +532 -0
- chython/chemistry/test/test_resonance.py +288 -0
- chython/chemistry/test/test_resonance_tsv.py +95 -0
- chython/chemistry/test/test_salts.py +565 -0
- chython/chemistry/test/test_saturate.py +737 -0
- chython/chemistry/test/test_smarts.py +309 -0
- chython/chemistry/test/test_standardize_differential.py +165 -0
- chython/chemistry/test/test_standardize_groups_port.py +371 -0
- chython/chemistry/test/test_standardize_overvalent_nitrogen.py +157 -0
- chython/chemistry/test/test_standardize_rules_examples.py +170 -0
- chython/chemistry/test/test_standardize_rules_merges.py +187 -0
- chython/chemistry/test/test_standardize_rules_tsv.py +266 -0
- chython/chemistry/test/test_thiele_is_single_purpose.py +133 -0
- chython/chemistry/test/test_tpsa.py +136 -0
- chython/chemistry/test/test_tpsa_tsv.py +134 -0
- chython/chemistry/test/test_valence_report.py +131 -0
- chython/chemistry/test/test_z_translation.py +178 -0
- chython/core/RULES.md +1046 -0
- chython/core/__init__.py +151 -0
- chython/core/_core.cpython-313-darwin.so +0 -0
- chython/core/_facade.py +183 -0
- chython/core/_log.py +268 -0
- chython/core/_reaction_passes.py +606 -0
- chython/core/elements.tsv +167 -0
- chython/core/isotopes.tsv +475 -0
- chython/core/libinchi.dylib +0 -0
- chython/core/reaction.py +1213 -0
- chython/core/test/__init__.py +18 -0
- chython/core/test/arena_v4_corpus.bin.gz +0 -0
- chython/core/test/bench_ml.py +123 -0
- chython/core/test/chytorch_oracle.py +162 -0
- chython/core/test/gen_element_tables.py +339 -0
- chython/core/test/gen_modeling_view_corpus.py +55 -0
- chython/core/test/gen_pach3_corpus.py +49 -0
- chython/core/test/gen_reaction_pach_corpus.py +153 -0
- chython/core/test/gen_v3_fixtures.py +250 -0
- chython/core/test/gen_v4_fixtures.py +116 -0
- chython/core/test/gen_valence_rules.py +761 -0
- chython/core/test/modeling_view_corpus.json.gz +0 -0
- chython/core/test/modeling_view_corpus.py +108 -0
- chython/core/test/oracle.py +707 -0
- chython/core/test/pach3_corpus.py +169 -0
- chython/core/test/pach_corpus.py +108 -0
- chython/core/test/pach_v0_corpus.bin.gz +0 -0
- chython/core/test/pach_v0_native_corpus.bin.gz +0 -0
- chython/core/test/pach_v2_corpus.bin.gz +0 -0
- chython/core/test/pach_v3_corpus.bin.gz +0 -0
- chython/core/test/pach_v4_corpus.bin.gz +0 -0
- chython/core/test/reaction_pach_corpus.py +97 -0
- chython/core/test/reaction_pach_v2_corpus.bin.gz +0 -0
- chython/core/test/test_aggregates.py +199 -0
- chython/core/test/test_alternative_spellings.py +191 -0
- chython/core/test/test_apply_scratch_probe.py +235 -0
- chython/core/test/test_arena_f60.py +143 -0
- chython/core/test/test_arena_identity.py +367 -0
- chython/core/test/test_arena_v3_compat.py +392 -0
- chython/core/test/test_arena_v4_compat.py +100 -0
- chython/core/test/test_aromatic_storage.py +643 -0
- chython/core/test/test_canonical.py +634 -0
- chython/core/test/test_canonical_mirror.py +503 -0
- chython/core/test/test_cip_storage.py +720 -0
- chython/core/test/test_clean_isotopes_and_coordinate_bonds.py +207 -0
- chython/core/test/test_clean_stereo.py +253 -0
- chython/core/test/test_conformers.py +704 -0
- chython/core/test/test_container_log.py +96 -0
- chython/core/test/test_copy_caches.py +139 -0
- chython/core/test/test_derive.py +177 -0
- chython/core/test/test_descriptors.py +1457 -0
- chython/core/test/test_element_tables.py +470 -0
- chython/core/test/test_facade.py +288 -0
- chython/core/test/test_features.py +675 -0
- chython/core/test/test_featurizer_injection.py +52 -0
- chython/core/test/test_fingerprints.py +620 -0
- chython/core/test/test_geometry.py +216 -0
- chython/core/test/test_h_unknown.py +458 -0
- chython/core/test/test_hydrogens.py +320 -0
- chython/core/test/test_inchi.py +797 -0
- chython/core/test/test_interop_injection.py +125 -0
- chython/core/test/test_isomorphism.py +1073 -0
- chython/core/test/test_kekule.py +1201 -0
- chython/core/test/test_log.py +300 -0
- chython/core/test/test_magic.py +792 -0
- chython/core/test/test_meta.py +76 -0
- chython/core/test/test_ml_encoding.py +151 -0
- chython/core/test/test_ml_reaction_transition.py +288 -0
- chython/core/test/test_ml_state_view.py +263 -0
- chython/core/test/test_ml_transition_view.py +180 -0
- chython/core/test/test_ml_unpack_differential.py +125 -0
- chython/core/test/test_modeling_view_frozen.py +86 -0
- chython/core/test/test_molecule.py +1138 -0
- chython/core/test/test_morgan.py +387 -0
- chython/core/test/test_no_chython_two_imports.py +223 -0
- chython/core/test/test_oracle.py +238 -0
- chython/core/test/test_pach.py +994 -0
- chython/core/test/test_pach3.py +1395 -0
- chython/core/test/test_pack.py +821 -0
- chython/core/test/test_query.py +1456 -0
- chython/core/test/test_r_edit.py +92 -0
- chython/core/test/test_r_query.py +72 -0
- chython/core/test/test_r_semantics.py +308 -0
- chython/core/test/test_r_serialisation.py +73 -0
- chython/core/test/test_r_smirks.py +118 -0
- chython/core/test/test_r_storage.py +205 -0
- chython/core/test/test_reaction_container.py +489 -0
- chython/core/test/test_reaction_identity.py +217 -0
- chython/core/test/test_reaction_pach.py +780 -0
- chython/core/test/test_reaction_passes.py +346 -0
- chython/core/test/test_reaction_patch_stereo.py +162 -0
- chython/core/test/test_reaction_smiles.py +333 -0
- chython/core/test/test_rings.py +812 -0
- chython/core/test/test_rings_c60.py +60 -0
- chython/core/test/test_set_element.py +156 -0
- chython/core/test/test_sgroups.py +607 -0
- chython/core/test/test_smarts_read.py +1135 -0
- chython/core/test/test_smiles_r.py +161 -0
- chython/core/test/test_smiles_read.py +1193 -0
- chython/core/test/test_smiles_roundtrip.py +345 -0
- chython/core/test/test_smiles_write.py +721 -0
- chython/core/test/test_smiles_write_aromatic.py +361 -0
- chython/core/test/test_smiles_write_cis_trans.py +960 -0
- chython/core/test/test_smiles_write_detached.py +674 -0
- chython/core/test/test_smiles_write_differential.py +814 -0
- chython/core/test/test_smiles_write_h_unknown.py +373 -0
- chython/core/test/test_smiles_write_stereo.py +559 -0
- chython/core/test/test_smiles_write_sticky.py +441 -0
- chython/core/test/test_smirks_filter.py +215 -0
- chython/core/test/test_smirks_patch.py +547 -0
- chython/core/test/test_smirks_read.py +491 -0
- chython/core/test/test_smirks_report.py +73 -0
- chython/core/test/test_smirks_stereo.py +986 -0
- chython/core/test/test_stereo_acceptance.py +583 -0
- chython/core/test/test_stereo_parity.py +2135 -0
- chython/core/test/test_stereo_perception.py +1453 -0
- chython/core/test/test_stereo_query.py +1694 -0
- chython/core/test/test_stereo_units.py +1192 -0
- chython/core/test/test_stereo_v2_differential.py +226 -0
- chython/core/test/test_structure.py +318 -0
- chython/core/test/test_thiele.py +698 -0
- chython/core/test/test_title.py +110 -0
- chython/core/test/test_topology.py +405 -0
- chython/core/test/test_union_stereo.py +167 -0
- chython/core/test/test_valence.py +756 -0
- chython/core/test/test_view_surface.py +320 -0
- chython/core/test/v3_fixtures.py +221 -0
- chython/core/test/v4_fixtures.py +33 -0
- chython/core/valence_rules.tsv +1089 -0
- chython/core/wedge.py +1510 -0
- chython/depict/__init__.py +52 -0
- chython/depict/_config.py +78 -0
- chython/depict/_hooks.py +41 -0
- chython/depict/bonds.py +671 -0
- chython/depict/colorbar.py +145 -0
- chython/depict/colormap.py +302 -0
- chython/depict/field.py +686 -0
- chython/depict/figure.py +300 -0
- chython/depict/label.py +500 -0
- chython/depict/layout/__init__.py +28 -0
- chython/depict/layout/clean2d.js +3 -0
- chython/depict/layout/molecule.py +466 -0
- chython/depict/layout/reaction.py +118 -0
- chython/depict/metrics/__init__.py +161 -0
- chython/depict/metrics/helvetica.tsv +332 -0
- chython/depict/metrics/times.tsv +332 -0
- chython/depict/overlay.py +752 -0
- chython/depict/render/__init__.py +28 -0
- chython/depict/render/svg.py +216 -0
- chython/depict/scene.py +526 -0
- chython/depict/style.py +457 -0
- chython/depict/test/__init__.py +18 -0
- chython/depict/test/test_bonds.py +1223 -0
- chython/depict/test/test_clean2d.py +713 -0
- chython/depict/test/test_colorbar.py +276 -0
- chython/depict/test/test_colormap.py +201 -0
- chython/depict/test/test_field.py +519 -0
- chython/depict/test/test_figure.py +957 -0
- chython/depict/test/test_label.py +803 -0
- chython/depict/test/test_metrics.py +178 -0
- chython/depict/test/test_overlay.py +469 -0
- chython/depict/test/test_package.py +276 -0
- chython/depict/test/test_r_atom.py +80 -0
- chython/depict/test/test_scene.py +341 -0
- chython/depict/test/test_style.py +228 -0
- chython/depict/test/test_svg.py +358 -0
- chython/depict/test/test_wedge_draw.py +1049 -0
- chython/depict/test/test_x3dom.py +179 -0
- chython/depict/wedge.py +383 -0
- chython/depict/x3dom.py +372 -0
- chython/exceptions.py +154 -0
- chython/formats/__init__.py +68 -0
- chython/formats/_text.py +33 -0
- chython/formats/ctfile/CTFILE.md +646 -0
- chython/formats/ctfile/__init__.py +53 -0
- chython/formats/ctfile/_ctab.py +481 -0
- chython/formats/ctfile/_errors.py +40 -0
- chython/formats/ctfile/_facade.py +164 -0
- chython/formats/ctfile/_hydrogens.py +504 -0
- chython/formats/ctfile/_rdf.py +688 -0
- chython/formats/ctfile/_rxn.py +389 -0
- chython/formats/ctfile/_sdf.py +245 -0
- chython/formats/ctfile/_sgroup.py +546 -0
- chython/formats/ctfile/_stream.py +307 -0
- chython/formats/ctfile/_tokens.py +321 -0
- chython/formats/ctfile/_v2000.py +958 -0
- chython/formats/ctfile/_v3000.py +848 -0
- chython/formats/ctfile/test/__init__.py +0 -0
- chython/formats/ctfile/test/conftest.py +112 -0
- chython/formats/ctfile/test/test_aromatic.py +345 -0
- chython/formats/ctfile/test/test_container_log.py +229 -0
- chython/formats/ctfile/test/test_data_labels.py +180 -0
- chython/formats/ctfile/test/test_facade.py +341 -0
- chython/formats/ctfile/test/test_fidelity.py +529 -0
- chython/formats/ctfile/test/test_hydrogens.py +887 -0
- chython/formats/ctfile/test/test_r_atom.py +330 -0
- chython/formats/ctfile/test/test_rdf.py +1605 -0
- chython/formats/ctfile/test/test_rxn.py +404 -0
- chython/formats/ctfile/test/test_sdf.py +300 -0
- chython/formats/ctfile/test/test_sgroup.py +436 -0
- chython/formats/ctfile/test/test_stream.py +379 -0
- chython/formats/ctfile/test/test_tokens.py +272 -0
- chython/formats/ctfile/test/test_v2000.py +608 -0
- chython/formats/ctfile/test/test_v3000.py +746 -0
- chython/formats/ctfile/test/test_wedge.py +2274 -0
- chython/formats/mol2.py +812 -0
- chython/formats/pdb/__init__.py +36 -0
- chython/formats/pdb/_builder.py +632 -0
- chython/formats/pdb/_legacy.py +519 -0
- chython/formats/pdb/_mmcif.py +666 -0
- chython/formats/pdb/_records.py +242 -0
- chython/formats/pdb/_star.py +453 -0
- chython/formats/test/__init__.py +18 -0
- chython/formats/test/conftest.py +76 -0
- chython/formats/test/oracles.py +1995 -0
- chython/formats/test/test_conformance.py +638 -0
- chython/formats/test/test_done_when.py +84 -0
- chython/formats/test/test_isolation.py +100 -0
- chython/formats/test/test_log_prefix.py +199 -0
- chython/formats/test/test_mmcif.py +1084 -0
- chython/formats/test/test_mol2.py +1360 -0
- chython/formats/test/test_no_review_bookkeeping.py +96 -0
- chython/formats/test/test_oracles.py +133 -0
- chython/formats/test/test_pdb.py +726 -0
- chython/formats/test/test_pdb_builder.py +924 -0
- chython/formats/test/test_perceive_agreement.py +116 -0
- chython/formats/test/test_read_only_facades.py +96 -0
- chython/formats/test/test_xyz.py +869 -0
- chython/formats/test/test_xyz_builder.py +149 -0
- chython/formats/xml/__init__.py +50 -0
- chython/formats/xml/_cml.py +972 -0
- chython/formats/xml/_dialect.py +958 -0
- chython/formats/xml/_errors.py +46 -0
- chython/formats/xml/_facade.py +77 -0
- chython/formats/xml/_mrv.py +1243 -0
- chython/formats/xml/_tree.py +315 -0
- chython/formats/xml/test/__init__.py +0 -0
- chython/formats/xml/test/conftest.py +79 -0
- chython/formats/xml/test/test_cml.py +1282 -0
- chython/formats/xml/test/test_container_log.py +112 -0
- chython/formats/xml/test/test_dialect.py +729 -0
- chython/formats/xml/test/test_equivalence.py +346 -0
- chython/formats/xml/test/test_facade.py +96 -0
- chython/formats/xml/test/test_mrv.py +1250 -0
- chython/formats/xml/test/test_mrv_census.py +459 -0
- chython/formats/xml/test/test_tree.py +355 -0
- chython/formats/xyz.py +520 -0
- chython/interop/__init__.py +179 -0
- chython/interop/_cdk.py +527 -0
- chython/interop/_cdpkit.py +191 -0
- chython/interop/_indigo.py +322 -0
- chython/interop/_iupac.py +94 -0
- chython/interop/_java.py +65 -0
- chython/interop/_openbabel.py +334 -0
- chython/interop/_pandas.py +54 -0
- chython/interop/_rdkit.py +638 -0
- chython/interop/_records.py +62 -0
- chython/interop/_stereo.py +140 -0
- chython/interop/config.py +93 -0
- chython/interop/conformers.py +143 -0
- chython/interop/test/__init__.py +0 -0
- chython/interop/test/conftest.py +79 -0
- chython/interop/test/test_cdk.py +358 -0
- chython/interop/test/test_cdpkit.py +458 -0
- chython/interop/test/test_config.py +181 -0
- chython/interop/test/test_conformers.py +248 -0
- chython/interop/test/test_coordinate_honesty.py +55 -0
- chython/interop/test/test_dispatch.py +189 -0
- chython/interop/test/test_indigo.py +553 -0
- chython/interop/test/test_iupac.py +147 -0
- chython/interop/test/test_log_delivery.py +189 -0
- chython/interop/test/test_openbabel.py +429 -0
- chython/interop/test/test_pandas.py +105 -0
- chython/interop/test/test_rdkit.py +860 -0
- chython/interop/test/test_stereo.py +143 -0
- chython/interop/test/test_v2_oracle.py +480 -0
- chython/reactions/__init__.py +62 -0
- chython/reactions/_enumerate.py +447 -0
- chython/reactions/_numbering.py +102 -0
- chython/reactions/_reconstruct.py +415 -0
- chython/reactions/_stickers.py +169 -0
- chython/reactions/_tables.py +513 -0
- chython/reactions/attention/__init__.py +137 -0
- chython/reactions/attention/_assign.py +102 -0
- chython/reactions/attention/_encode.py +176 -0
- chython/reactions/attention/_session.py +91 -0
- chython/reactions/tables/functional.tsv +310 -0
- chython/reactions/tables/protective.tsv +138 -0
- chython/reactions/tables/reactions.tsv +427 -0
- chython/reactions/tables/roles.tsv +93 -0
- chython/reactions/test/__init__.py +18 -0
- chython/reactions/test/_frozen_ids.py +776 -0
- chython/reactions/test/gen_corpus_glossary.py +146 -0
- chython/reactions/test/golden_subset.smi +32 -0
- chython/reactions/test/test_attention.py +380 -0
- chython/reactions/test/test_attention_assign.py +174 -0
- chython/reactions/test/test_attention_encode.py +223 -0
- chython/reactions/test/test_attention_isolation.py +212 -0
- chython/reactions/test/test_corpus_glossary.py +91 -0
- chython/reactions/test/test_dependency_direction.py +166 -0
- chython/reactions/test/test_enumerate.py +451 -0
- chython/reactions/test/test_functional.py +55 -0
- chython/reactions/test/test_id_stability.py +147 -0
- chython/reactions/test/test_numbering.py +107 -0
- chython/reactions/test/test_probes.py +90 -0
- chython/reactions/test/test_protective.py +560 -0
- chython/reactions/test/test_reconstruct.py +456 -0
- chython/reactions/test/test_roles.py +140 -0
- chython/reactions/test/test_stickers.py +221 -0
- chython/reactions/test/test_tables.py +421 -0
- chython/test/__init__.py +24 -0
- chython/test/test_code_hygiene.py +184 -0
- chython/test/test_container_methods.py +200 -0
- chython/test/test_doc_figures.py +196 -0
- chython/test/test_doc_references.py +163 -0
- chython/test/test_doc_samples.py +256 -0
- chython/test/test_facade_names.py +198 -0
- chython/test/test_hydrogen_parity.py +315 -0
- chython/test/test_libinchi_staging.py +253 -0
- chython/test/test_log_records.py +84 -0
- chython/test/test_optional_numpy.py +239 -0
- chython/test/test_packaging.py +271 -0
- chython/test/test_performance.py +515 -0
- chython/test/test_r_atom_integration.py +88 -0
- chython/test/test_release_build.py +210 -0
- chython/test/test_stereo_bluebook.py +713 -0
- chython/test/test_v2_boundary.py +358 -0
- chython-3.0.dist-info/METADATA +203 -0
- chython-3.0.dist-info/RECORD +414 -0
- chython-3.0.dist-info/WHEEL +5 -0
- chython-3.0.dist-info/licenses/LICENSE +165 -0
- chython-3.0.dist-info/top_level.txt +1 -0
chython/__init__.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2014-2026 Ramil Nugmanov <nougmanoff@protonmail.com>
|
|
4
|
+
# Copyright 2014-2019 Timur Madzhidov tmadzhidov@gmail.com features and API discussion
|
|
5
|
+
# Copyright 2014-2019 Alexandre Varnek <varnek@unistra.fr> base idea of CGR approach
|
|
6
|
+
# This file is part of chython.
|
|
7
|
+
#
|
|
8
|
+
# chython is free software; you can redistribute it and/or modify
|
|
9
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
|
10
|
+
# the Free Software Foundation; either version 3 of the License, or
|
|
11
|
+
# (at your option) any later version.
|
|
12
|
+
#
|
|
13
|
+
# This program is distributed in the hope that it will be useful,
|
|
14
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
15
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
16
|
+
# GNU Lesser General Public License for more details.
|
|
17
|
+
#
|
|
18
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
19
|
+
# along with this program; if not, see <https://www.gnu.org/licenses/>.
|
|
20
|
+
#
|
|
21
|
+
"""chython's public surface: a facade re-exporting the packages below it.
|
|
22
|
+
|
|
23
|
+
`__all__` is empty by design. `smarts` is the short spelling of `read_smarts` and the same function
|
|
24
|
+
object. `smiles` is bidirectional: a string in reads -- a `>` that is not a dative `->` makes it a
|
|
25
|
+
reaction SMILES and the result a `ReactionContainer` -- and a container in writes one. `pach` is the
|
|
26
|
+
same door for the wire format, and `unpach`/`unpack` its import half under chython 2's two names.
|
|
27
|
+
"""
|
|
28
|
+
from sys import modules as _modules
|
|
29
|
+
from types import ModuleType as _ModuleType
|
|
30
|
+
from .core import *
|
|
31
|
+
from .core import read_smarts as smarts
|
|
32
|
+
from .depict import (Clean2DEngine, DepictStyle, get_clean2d_engine, get_depict_style,
|
|
33
|
+
set_clean2d_engine, set_depict_style)
|
|
34
|
+
from .formats import *
|
|
35
|
+
# By full path, not through `formats`' star: `pdb` is that subpackage's name too and the function
|
|
36
|
+
# would shadow it.
|
|
37
|
+
from .formats.pdb import PDBAtom, PDBBond, PDBRecord, build_molecule, mmcif, pdb, read_mmcif, read_pdb
|
|
38
|
+
# Imported for its registration side effect as much as for its names: it calls `_set_standardize_fn`
|
|
39
|
+
# at import time, which is what makes `mol.standardize()` exist. Not an unused import.
|
|
40
|
+
from .chemistry import *
|
|
41
|
+
# Likewise: `_set_reactions_fns` at import time is what makes `mol.react()` and `mol @ other` exist.
|
|
42
|
+
from .reactions import *
|
|
43
|
+
from .interop import iupac, patch_pandas
|
|
44
|
+
from .interop.config import _facade_alias as _interop_facade_alias
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class _Facade(_ModuleType):
|
|
48
|
+
"""Gives `chython` itself a property, so `chython.clean2d_engine` forwards both the read and the
|
|
49
|
+
write to its one home in `depict/_config.py` and the setter validates the name on the spot.
|
|
50
|
+
"""
|
|
51
|
+
@property
|
|
52
|
+
def clean2d_engine(self) -> Clean2DEngine:
|
|
53
|
+
return get_clean2d_engine()
|
|
54
|
+
|
|
55
|
+
@clean2d_engine.setter
|
|
56
|
+
def clean2d_engine(self, engine: Clean2DEngine):
|
|
57
|
+
set_clean2d_engine(engine)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
_modules[__name__].__class__ = _Facade
|
|
61
|
+
|
|
62
|
+
# `conformer_engine` and `class_paths` live in `chython.interop.config`; aliased here rather than
|
|
63
|
+
# copied, or `chython.conformer_engine = 'cdpkit'` would be a silent no-op. Must run AFTER the
|
|
64
|
+
# `__class__` assignment above: `_facade_alias` subclasses whatever class the module currently has, so
|
|
65
|
+
# the reverse order would replace the aliasing subclass and turn both names into AttributeErrors.
|
|
66
|
+
_interop_facade_alias(__name__, 'conformer_engine', 'class_paths')
|
|
67
|
+
|
|
68
|
+
__all__ = []
|
chython/_functions.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2020-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
|
+
from functools import wraps
|
|
20
|
+
from itertools import product
|
|
21
|
+
from warnings import warn
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_SENTINEL = object()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def renamed_name(old, new):
|
|
28
|
+
"""Announce a superseded attribute spelling, naming what replaced it.
|
|
29
|
+
|
|
30
|
+
ONE FUNCTION SO THE MESSAGE HAS ONE WORDING. Every alias routes through here, so the text a
|
|
31
|
+
consumer greps for while porting is the same text in all of them, and the removal is one edit.
|
|
32
|
+
|
|
33
|
+
`stacklevel=3` charges the warning to the CALLER, which is the only person who can act on it: the
|
|
34
|
+
three frames are `warn` -> this function -> the property's fget or fset -> the consumer's line.
|
|
35
|
+
THE NUMBER IS MEASURED, NOT REASONED. Its counterpart in the compiled core is 1 for the same
|
|
36
|
+
intent, because neither a `cdef` helper nor a compiled `def` pushes a Python frame; so the right
|
|
37
|
+
number is a property of the call chain rather than of the source, and both are asserted by a test
|
|
38
|
+
on the blamed line rather than trusted.
|
|
39
|
+
"""
|
|
40
|
+
warn(f'`{old}` was renamed to `{new}` and will be removed in a later release; use `{new}`',
|
|
41
|
+
DeprecationWarning, stacklevel=3)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def cached_method(func):
|
|
45
|
+
"""Cache no-argument method result in instance __dict__. Cleared by flush_cache().
|
|
46
|
+
|
|
47
|
+
Thread-safe for concurrent reads without locking:
|
|
48
|
+
- dict.get/setitem are atomic in CPython 3.14 free-threaded mode
|
|
49
|
+
- Wrapped functions are pure (deterministic, read-only on self)
|
|
50
|
+
- Duplicate computation on cold cache is benign (same result)
|
|
51
|
+
- Mutations must be sequential (caller's responsibility)
|
|
52
|
+
"""
|
|
53
|
+
key = f'__cached_method_{func.__name__}'
|
|
54
|
+
|
|
55
|
+
@wraps(func)
|
|
56
|
+
def wrapper(self):
|
|
57
|
+
val = self.__dict__.get(key, _SENTINEL)
|
|
58
|
+
if val is not _SENTINEL:
|
|
59
|
+
return val
|
|
60
|
+
val = func(self)
|
|
61
|
+
self.__dict__[key] = val
|
|
62
|
+
return val
|
|
63
|
+
return wrapper
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# lazy itertools.product with diagonal combination precedence
|
|
67
|
+
def lazy_product(*args):
|
|
68
|
+
if len(args) == 1:
|
|
69
|
+
for x in args[0]:
|
|
70
|
+
yield x,
|
|
71
|
+
elif not args:
|
|
72
|
+
yield ()
|
|
73
|
+
else:
|
|
74
|
+
gens = [iter(x) for x in args]
|
|
75
|
+
empty = [False] * len(args)
|
|
76
|
+
pools = [[] for _ in range(len(args))]
|
|
77
|
+
indices = set()
|
|
78
|
+
|
|
79
|
+
reached = 0
|
|
80
|
+
while True:
|
|
81
|
+
out = []
|
|
82
|
+
ind = []
|
|
83
|
+
for n, (p, g, e) in enumerate(zip(pools, gens, empty)):
|
|
84
|
+
if e:
|
|
85
|
+
out.append(p[-1])
|
|
86
|
+
else:
|
|
87
|
+
try:
|
|
88
|
+
x = next(g)
|
|
89
|
+
except StopIteration:
|
|
90
|
+
if not p: # one of gens empty
|
|
91
|
+
return
|
|
92
|
+
reached += 1
|
|
93
|
+
if reached == len(args):
|
|
94
|
+
break
|
|
95
|
+
out.append(p[-1])
|
|
96
|
+
empty[n] = True
|
|
97
|
+
else:
|
|
98
|
+
p.append(x)
|
|
99
|
+
out.append(x)
|
|
100
|
+
ind.append(len(p) - 1)
|
|
101
|
+
else:
|
|
102
|
+
yield tuple(out)
|
|
103
|
+
indices.add(tuple(ind))
|
|
104
|
+
continue
|
|
105
|
+
break
|
|
106
|
+
|
|
107
|
+
for ind in product(*(range(len(p)) for p in pools)):
|
|
108
|
+
if ind in indices:
|
|
109
|
+
continue
|
|
110
|
+
yield tuple(p[x] for x, p in zip(ind, pools))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
__all__ = ['cached_method', 'lazy_product', 'renamed_name']
|
|
@@ -0,0 +1,86 @@
|
|
|
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
|
+
"""Chemical knowledge as TSV in `tables/`, and the passes that apply it.
|
|
20
|
+
|
|
21
|
+
Imports `chython.core` and the standard library only, never the `chython` facade. Importing it registers
|
|
22
|
+
`standardize()`, `canonicalize()` and friends on the core container by injection, `MoleculeContainer`
|
|
23
|
+
being a `cdef class`. No pass runs on parse.
|
|
24
|
+
|
|
25
|
+
`perceive_bonds`, `saturate` and `expand_abbreviations` are the passes with no method. The first two
|
|
26
|
+
are the two halves of building a molecule out of a coordinate file -- which pairs are bonded, then at
|
|
27
|
+
what order -- so their caller is whoever read that file, and neither runs on read. The third reads what
|
|
28
|
+
a drawing wrote on an atom, which is a fact about a FILE and not about a structure, so it belongs beside
|
|
29
|
+
the reader that stored the alias rather than on every molecule.
|
|
30
|
+
"""
|
|
31
|
+
from ._abbreviations import expand_abbreviations
|
|
32
|
+
from ._canonicalize import canonicalize
|
|
33
|
+
from ._counts import (hydrogen_bond_acceptors_count, hydrogen_bond_donors_count,
|
|
34
|
+
rotatable_bonds_count)
|
|
35
|
+
from ._crippen import crippen_logp, crippen_mr
|
|
36
|
+
from ._hydrogens import explicify_hydrogens, implicify_hydrogens
|
|
37
|
+
from ._implicit import calc_implicit, check_valence
|
|
38
|
+
from ._isomers import standardize_isomers
|
|
39
|
+
from ._maccs import maccs_bit_set, maccs_keys
|
|
40
|
+
from ._perceive import perceive_bonds
|
|
41
|
+
from ._pharmacophore import pharmacophore_invariants
|
|
42
|
+
from ._protomers import neutralize
|
|
43
|
+
from ._qed import alert_count, qed, qed_properties
|
|
44
|
+
from ._residues import (RESIDUE_KINDS, ResidueTemplate, normalize_atom_name, residue_template,
|
|
45
|
+
residue_templates)
|
|
46
|
+
from ._resonance import fix_resonance
|
|
47
|
+
from ._salts import SaltComposition, decompose_salts, split_salts
|
|
48
|
+
from ._saturate import saturate
|
|
49
|
+
from ._smarts import SmartsSyntaxError, compile_smarts
|
|
50
|
+
from ._standardize import LogRecord, standardize
|
|
51
|
+
from ._tables import (ACID_ROLES, AbbreviationRow, AcidRow, Endpoint, RESONANCE_ROLES, Rule, SALT_ROLES,
|
|
52
|
+
SaltRow, abbreviation_row, abbreviations_rows, acids_rules, acids_rules_by_role,
|
|
53
|
+
acids_table_text, groups_rules,
|
|
54
|
+
metals_rules, read_table, resonance_rules, resonance_rules_by_role,
|
|
55
|
+
resonance_table_text, salts_rows, salts_rows_by_role, salts_species_keys,
|
|
56
|
+
salts_table_text, standardize_rules)
|
|
57
|
+
from ._tpsa import tpsa
|
|
58
|
+
from ..core._core import (_set_canonicalize_fn, _set_featurizer_fns, _set_hydrogens_fns,
|
|
59
|
+
_set_isomers_fn, _set_protomers_fn, _set_resonance_fn, _set_salts_fns,
|
|
60
|
+
_set_standardize_fn, _set_valence_fn)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
__all__ = ['ACID_ROLES', 'AbbreviationRow', 'LogRecord', 'SALT_ROLES', 'SaltComposition',
|
|
64
|
+
'abbreviation_row', 'abbreviations_rows', 'alert_count',
|
|
65
|
+
'calc_implicit', 'canonicalize', 'check_valence', 'crippen_logp', 'crippen_mr',
|
|
66
|
+
'decompose_salts', 'expand_abbreviations', 'explicify_hydrogens', 'fix_resonance',
|
|
67
|
+
'hydrogen_bond_acceptors_count',
|
|
68
|
+
'hydrogen_bond_donors_count', 'implicify_hydrogens', 'maccs_bit_set', 'maccs_keys',
|
|
69
|
+
'neutralize', 'perceive_bonds', 'pharmacophore_invariants', 'qed', 'qed_properties',
|
|
70
|
+
'rotatable_bonds_count',
|
|
71
|
+
'saturate', 'split_salts', 'standardize', 'standardize_isomers', 'tpsa']
|
|
72
|
+
|
|
73
|
+
_set_standardize_fn(standardize)
|
|
74
|
+
_set_canonicalize_fn(canonicalize)
|
|
75
|
+
_set_hydrogens_fns(implicify_hydrogens, explicify_hydrogens)
|
|
76
|
+
_set_isomers_fn(standardize_isomers)
|
|
77
|
+
_set_valence_fn(check_valence)
|
|
78
|
+
_set_salts_fns(split_salts=split_salts, decompose_salts=decompose_salts)
|
|
79
|
+
_set_protomers_fn(neutralize)
|
|
80
|
+
_set_resonance_fn(fix_resonance)
|
|
81
|
+
_set_featurizer_fns(rotatable_bonds_count=rotatable_bonds_count,
|
|
82
|
+
hydrogen_bond_donors_count=hydrogen_bond_donors_count,
|
|
83
|
+
hydrogen_bond_acceptors_count=hydrogen_bond_acceptors_count,
|
|
84
|
+
tpsa=tpsa, crippen_logp=crippen_logp, crippen_mr=crippen_mr, qed=qed,
|
|
85
|
+
maccs_keys=maccs_keys, maccs_bit_set=maccs_bit_set,
|
|
86
|
+
pharmacophore_invariants=pharmacophore_invariants)
|
|
@@ -0,0 +1,127 @@
|
|
|
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
|
+
"""Expansion of a contracted group drawn as one labelled atom.
|
|
20
|
+
|
|
21
|
+
A file that draws one atom and writes `OMe` on it has stated a methoxy group. The reader stores the
|
|
22
|
+
label as the atom's alias and the atom as an R -- neither invents a structure -- and this pass turns the
|
|
23
|
+
ones `tables/abbreviations.tsv` knows into atoms. A label the table does not know is left alone with its
|
|
24
|
+
alias, which is what makes the table safe to grow.
|
|
25
|
+
|
|
26
|
+
THE LABELLED ATOM IS TRANSMUTED, NOT REPLACED. `set_element` keeps its stable id, its bonds and its
|
|
27
|
+
neighbours' parities; deleting it and adding the fragment's attachment atom instead would reorder a
|
|
28
|
+
neighbouring stereocentre's references and invalidate a parity nothing here restated.
|
|
29
|
+
"""
|
|
30
|
+
from ._tables import abbreviation_row
|
|
31
|
+
from ..core import H_UNKNOWN, LogRecord, MoleculeContainer, REFUSED, REPAIRED, recording
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
__all__ = ['expand_abbreviations']
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
#: Neither exactly one neighbour nor a single bond to it: the marker in the table states the one single
|
|
38
|
+
#: bond a contracted group hangs by, and a site that is not that is not this group.
|
|
39
|
+
_RULE_ATTACHMENT = 'abbreviations:attachment'
|
|
40
|
+
|
|
41
|
+
#: The file stated a charge, a radical or an isotope on the labelled atom, and the label states one too.
|
|
42
|
+
#: Nothing here ranks the two, so the site keeps the label.
|
|
43
|
+
_RULE_STATED = 'abbreviations:stated-atom'
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def expand_abbreviations(molecule: MoleculeContainer) -> bool:
|
|
47
|
+
"""Replace every atom whose alias names a row of `tables/abbreviations.tsv` with that fragment.
|
|
48
|
+
|
|
49
|
+
All-or-nothing per site. A site that survives every check is expanded and its alias dropped; a site
|
|
50
|
+
that fails one is left exactly as the file drew it, alias included, and the reason is a REFUSED
|
|
51
|
+
record. Returns True when at least one site was expanded.
|
|
52
|
+
|
|
53
|
+
| Outcome | Rule | Severity |
|
|
54
|
+
| --- | --- | --- |
|
|
55
|
+
| expanded | the row's own id, `abbreviations:OMe` | REPAIRED |
|
|
56
|
+
| not one single bond to one neighbour | `abbreviations:attachment` | REFUSED |
|
|
57
|
+
| charge, radical or isotope stated on the labelled atom | `abbreviations:stated-atom` | REFUSED |
|
|
58
|
+
|
|
59
|
+
The grafted atoms take the labelled atom's coordinates, so a record with a depiction needs
|
|
60
|
+
`clean2d()` afterwards; the message says so where there was one to disturb.
|
|
61
|
+
"""
|
|
62
|
+
with recording(molecule, stage='abbreviations') as log:
|
|
63
|
+
aliases = molecule.aliases
|
|
64
|
+
if not aliases:
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
sites = []
|
|
68
|
+
for n, text in aliases.items():
|
|
69
|
+
try:
|
|
70
|
+
label = text.decode('utf-8')
|
|
71
|
+
except UnicodeDecodeError: # not a spelling any table holds
|
|
72
|
+
continue
|
|
73
|
+
row = abbreviation_row(label)
|
|
74
|
+
if row is None:
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
neighbors = list(molecule.neighbors_of(n))
|
|
78
|
+
if len(neighbors) != 1 or molecule.order_of(n, neighbors[0]) != 1:
|
|
79
|
+
log.append(LogRecord(_RULE_ATTACHMENT, (n,),
|
|
80
|
+
f'atom {n}: {label} hangs by one single bond and this atom has '
|
|
81
|
+
f'{len(neighbors)} neighbours; the label is kept', REFUSED))
|
|
82
|
+
continue
|
|
83
|
+
atom = molecule.atom(n)
|
|
84
|
+
if atom.charge or atom.is_radical or atom.isotope:
|
|
85
|
+
log.append(LogRecord(_RULE_STATED, (n,),
|
|
86
|
+
f'atom {n}: the record states charge {atom.charge}, radical '
|
|
87
|
+
f'{atom.is_radical} and isotope {atom.isotope} here, and {label} '
|
|
88
|
+
f'states its own; the label is kept', REFUSED))
|
|
89
|
+
continue
|
|
90
|
+
sites.append((n, label, row, atom.r_index, molecule.xy_of(n)))
|
|
91
|
+
|
|
92
|
+
if not sites:
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
with molecule.edit():
|
|
96
|
+
for n, _, row, r_index, xy in sites:
|
|
97
|
+
fragment = row.fragment
|
|
98
|
+
anchor = fragment.atom(row.attachment)
|
|
99
|
+
if r_index: # an R index is only settable on element 0
|
|
100
|
+
molecule.set_r_index(n, 0)
|
|
101
|
+
molecule.set_element(n, anchor.element)
|
|
102
|
+
molecule.set_charge(n, anchor.charge)
|
|
103
|
+
molecule.set_radical(n, anchor.is_radical)
|
|
104
|
+
molecule.set_isotope(n, anchor.isotope)
|
|
105
|
+
molecule.set_hydrogens(n, H_UNKNOWN if anchor.implicit_h is None else anchor.implicit_h)
|
|
106
|
+
|
|
107
|
+
grafted = {row.attachment: n}
|
|
108
|
+
for a in fragment.atoms():
|
|
109
|
+
if a.n == row.marker or a.n == row.attachment:
|
|
110
|
+
continue
|
|
111
|
+
grafted[a.n] = molecule.add_atom(a.element, charge=a.charge, isotope=a.isotope,
|
|
112
|
+
radical=a.is_radical, implicit_h=a.implicit_h)
|
|
113
|
+
if xy is not None:
|
|
114
|
+
molecule.set_xy(grafted[a.n], xy[0], xy[1])
|
|
115
|
+
for bond in fragment.bonds():
|
|
116
|
+
if bond.n == row.marker or bond.m == row.marker:
|
|
117
|
+
continue
|
|
118
|
+
molecule.add_bond(grafted[bond.n], grafted[bond.m], bond.order)
|
|
119
|
+
|
|
120
|
+
molecule.set_aliases({n: text for n, text in aliases.items()
|
|
121
|
+
if n not in {n for n, _, _, _, _ in sites}})
|
|
122
|
+
for n, label, row, _, xy in sites:
|
|
123
|
+
drawn = ', and the grafted atoms share its coordinates, so the record needs a 2D clean' \
|
|
124
|
+
if xy is not None else ''
|
|
125
|
+
log.append(LogRecord(row.id, (n,), f'atom {n}: the label {label} named a contracted group '
|
|
126
|
+
f'and was expanded to {row.smiles}{drawn}', REPAIRED))
|
|
127
|
+
return True
|
|
@@ -0,0 +1,152 @@
|
|
|
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
|
+
"""`canonicalize()` -- the pre-pass that makes `canonical_bytes` a compound identity rather than a
|
|
20
|
+
drawing identity, so equal compounds hash equal and a corpus deduplicates by hash. The stage order
|
|
21
|
+
is a correctness constraint, not taste: `kekule()`, `standardize()`, `implicify_hydrogens()`,
|
|
22
|
+
`neutralize()`, `thiele()`, `standardize_isomers()`, and `kekule()` again only under
|
|
23
|
+
`keep_kekule=True`. Each ordering pair is justified at its numbered step below.
|
|
24
|
+
|
|
25
|
+
The order alone is not enough, because the last stage can unblock the second one: a `tautomer` row
|
|
26
|
+
wanting a free ring nitrogen cannot fire while the mobile hydrogen sits on it, and the placement is
|
|
27
|
+
what moves that hydrogen off. So steps 2 to 6 run to a fixed point rather than once -- step 7.
|
|
28
|
+
"""
|
|
29
|
+
from ._hydrogens import implicify_hydrogens
|
|
30
|
+
from ._isomers import standardize_isomers
|
|
31
|
+
from ._protomers import neutralize
|
|
32
|
+
from ._standardize import standardize
|
|
33
|
+
from ..core import LOST, LogRecord, MoleculeContainer, recording
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
__all__ = ['canonicalize']
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
_RULE_ROUNDS = 'canonicalize:rounds'
|
|
40
|
+
|
|
41
|
+
#: How many times steps 2 to 6 may be re-run before the pipeline gives up and says so. Every shape
|
|
42
|
+
#: measured converges in two, and the loop cannot cycle in principle: the `tautomer` rows move a
|
|
43
|
+
#: hydrogen from oxygen or sulfur to nitrogen and never back, and the placement stage never makes an
|
|
44
|
+
#: oxygen or a sulfur a site. The cap is here so a rule table that breaks either half is reported as
|
|
45
|
+
#: a loss rather than hanging.
|
|
46
|
+
_ROUNDS_MAX = 5
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def canonicalize(molecule: MoleculeContainer, *, fix_tautomers: bool = True,
|
|
50
|
+
keep_kekule: bool = False) -> bool:
|
|
51
|
+
"""Bring `molecule` to the representation two drawings of one compound share. Did it change?
|
|
52
|
+
|
|
53
|
+
Run before deduplicating by `canonical_bytes`, `__hash__` or `__eq__`, which otherwise answer "same
|
|
54
|
+
drawing" rather than "same compound".
|
|
55
|
+
|
|
56
|
+
`molecule.log` gets one record per thing done or declined, tagged with the stage that wrote it, so
|
|
57
|
+
`mol.log.by_stage('standardize')`, `mol.log.repaired()` and `mol.log.lost()` all answer afterwards.
|
|
58
|
+
`fix_tautomers` is forwarded to `standardize()`, and switching it off gives up part of the guarantee
|
|
59
|
+
(`Oc1ccccn1` and `O=c1cccc[nH]1` stop hashing equal); it deliberately does not reach
|
|
60
|
+
`standardize_isomers()`, which picks between two valid annular forms rather than repairing one.
|
|
61
|
+
`keep_kekule=True` costs a second kekulisation rather than a skipped `thiele()`, the placement stage
|
|
62
|
+
needing the aromatic form.
|
|
63
|
+
|
|
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()`. The NET charge is untouched, so sodium acetate
|
|
66
|
+
stays sodium acetate -- there is no proton in it to move -- while ammonium acetate becomes acetic
|
|
67
|
+
acid and ammonia, both drawings of one salt.
|
|
68
|
+
"""
|
|
69
|
+
# A refusal at the answer boundary, which is the only place one belongs. `smiles()` returns
|
|
70
|
+
# whichever container its string describes, so a `>>` in a structure column arrives here as a
|
|
71
|
+
# reaction; without this the first read of `canonical_bytes` fails with an `AttributeError` naming
|
|
72
|
+
# a private attribute, which tells a caller nothing about what the pass takes.
|
|
73
|
+
if not isinstance(molecule, MoleculeContainer):
|
|
74
|
+
raise TypeError(f'canonicalize() takes a MoleculeContainer, got '
|
|
75
|
+
f'{type(molecule).__name__}; a ReactionContainer has its own canonicalize(), '
|
|
76
|
+
f'which runs this pass on each of its molecules')
|
|
77
|
+
|
|
78
|
+
# the bool is measured, not accumulated: steps 1 and 5 are a round trip, so summing the stages'
|
|
79
|
+
# own flags would report a change through both ends of a no-op on an already-canonical molecule.
|
|
80
|
+
before = molecule.canonical_bytes
|
|
81
|
+
|
|
82
|
+
# 1. Kekule first, so the group rules see definite bond orders. An aromatic system with no Kekule
|
|
83
|
+
# form does not stop the pipeline: it is reported and the rest still runs. `kekule()` also
|
|
84
|
+
# heals the hydrogen counts its own orders made derivable, so nothing here does that -- the
|
|
85
|
+
# pipeline is literally the manual steps.
|
|
86
|
+
# Nothing is recorded here about `result.unresolved`: the kekuliser writes a `LOST` record per
|
|
87
|
+
# system to `molecule.log` itself, naming it in `atoms`, so a second one here would summarise an
|
|
88
|
+
# event already reported. `check_valence()` is still how those atoms are found.
|
|
89
|
+
molecule.kekule()
|
|
90
|
+
|
|
91
|
+
# 2. Repair the drawing.
|
|
92
|
+
standardize(molecule, fix_tautomers=fix_tautomers)
|
|
93
|
+
|
|
94
|
+
# 3. Explicit hydrogens are a `canonical_bytes` difference, so they have to go.
|
|
95
|
+
implicify_hydrogens(molecule)
|
|
96
|
+
|
|
97
|
+
# 4. Pair off the charges an acid/base row can pair off, so a zwitterion and its neutral drawing
|
|
98
|
+
# hash equal. AFTER step 3, because `acids.tsv` reads implicit hydrogens: a cation drawn with
|
|
99
|
+
# hydrogen ATOMS is invisible to it until they have been folded in. `keep_charge` stays at its
|
|
100
|
+
# default -- the net charge is part of the compound, so a canonical form may move a proton but
|
|
101
|
+
# never create or destroy one. A quaternary ammonium keeps its counterion: it has no proton to
|
|
102
|
+
# give, so the pass finds no donor and declines.
|
|
103
|
+
neutralize(molecule)
|
|
104
|
+
|
|
105
|
+
# 5. Back to the aromatic form, which is the representation callers compare. Ahead of step 6,
|
|
106
|
+
# because a mobile hydrogen is a property of the aromatic form: step 1's definite orders already
|
|
107
|
+
# say where the hydrogen is, leaving the placement stage nothing to choose.
|
|
108
|
+
# `result.refused` is not recorded on top of the pass's own records either, and for the same
|
|
109
|
+
# reason -- with the severity the aromatiser itself states, which is `REFUSED` and not a loss.
|
|
110
|
+
molecule.thiele()
|
|
111
|
+
|
|
112
|
+
# 6. Canonical placement of mobile hydrogens and charges -- what makes the two N-H forms of
|
|
113
|
+
# 4-methylimidazole hash equal. Not gated by `fix_tautomers`: that flag withholds local repair
|
|
114
|
+
# rules, and this picks which of two valid drawings to keep rather than repairing one.
|
|
115
|
+
moved = standardize_isomers(molecule)
|
|
116
|
+
|
|
117
|
+
# 7. Steps 2 to 6 again, while the placement keeps unblocking a repair. `Oc1[nH]cnc2nncc1-2` is
|
|
118
|
+
# the shape: its mobile hydrogen sits on the one ring nitrogen the hydroxy-azine rows need free,
|
|
119
|
+
# so step 2 declines, and by the time step 6 has moved it the repair is behind us -- the drawing
|
|
120
|
+
# kept its hydroxy form and the same compound drawn the other way got the oxo form and a
|
|
121
|
+
# different key. Only the placement is re-entered from, since it is the one stage that can put
|
|
122
|
+
# the molecule back into a shape an earlier stage would have acted on.
|
|
123
|
+
#
|
|
124
|
+
# `kekule()` leads, and not for the reason step 1 does: the `tautomer` rows are written against
|
|
125
|
+
# definite bond orders, so on the aromatic form step 5 left behind they match nothing at all and
|
|
126
|
+
# re-running step 2 would be a guaranteed no-op. Step 4 is re-entered only behind a repair,
|
|
127
|
+
# which is the only thing that can hand it a charged site it has not already seen.
|
|
128
|
+
for _ in range(_ROUNDS_MAX):
|
|
129
|
+
if not moved:
|
|
130
|
+
break
|
|
131
|
+
molecule.kekule()
|
|
132
|
+
changed = standardize(molecule, fix_tautomers=fix_tautomers)
|
|
133
|
+
if changed:
|
|
134
|
+
implicify_hydrogens(molecule)
|
|
135
|
+
neutralize(molecule)
|
|
136
|
+
molecule.thiele() # unconditional: step 5's form is what a caller compares, and
|
|
137
|
+
if not changed: # the kekulisation above has to be undone either way
|
|
138
|
+
break
|
|
139
|
+
moved = standardize_isomers(molecule)
|
|
140
|
+
else:
|
|
141
|
+
with recording(molecule, stage='canonicalize') as log:
|
|
142
|
+
log.append(LogRecord(_RULE_ROUNDS, (), f'repair and placement were still changing the '
|
|
143
|
+
f'molecule after {_ROUNDS_MAX} rounds; the pipeline stopped there, so '
|
|
144
|
+
f'this molecule is not a fixed point and two drawings of it may not '
|
|
145
|
+
f'share a key', LOST))
|
|
146
|
+
|
|
147
|
+
# 8. `keep_kekule` undoes step 5 rather than skipping it: skipping 5 would skip 6 with it, and the
|
|
148
|
+
# flag would then decide which tautomer the caller gets.
|
|
149
|
+
if keep_kekule:
|
|
150
|
+
molecule.kekule()
|
|
151
|
+
|
|
152
|
+
return molecule.canonical_bytes != before
|
|
@@ -0,0 +1,81 @@
|
|
|
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
|
+
"""Rotatable bond and H-bond donor/acceptor counts, over `tables/rotatable.tsv` and `tables/hbond.tsv`.
|
|
20
|
+
"""
|
|
21
|
+
from ._standardize import LogRecord
|
|
22
|
+
from ..core import recording
|
|
23
|
+
from ._tables import hbond_rules_by_role, rotatable_rules_by_role
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _mapped_bond(row, mapping):
|
|
27
|
+
"""The (low, high) stable-id pair the row's :1 and :2 matched, order-normalised."""
|
|
28
|
+
a = mapping[row.numbers[1]]
|
|
29
|
+
b = mapping[row.numbers[2]]
|
|
30
|
+
return (a, b) if a < b else (b, a)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def rotatable_bonds_count(molecule) -> int:
|
|
34
|
+
"""Number of rotatable bonds, by the definition in `tables/rotatable.tsv`.
|
|
35
|
+
|
|
36
|
+
A bond is counted once however many ways a pattern maps onto it: row 1 is symmetric in its two
|
|
37
|
+
atoms and the matcher offers each bond in both directions, so the set is what makes this a count of
|
|
38
|
+
bonds rather than of matches. Charged and radical atoms count.
|
|
39
|
+
"""
|
|
40
|
+
by_role = rotatable_rules_by_role()
|
|
41
|
+
found = set()
|
|
42
|
+
excluded = []
|
|
43
|
+
for row in by_role['rotatable']:
|
|
44
|
+
for mapping in row.query.get_mapping(molecule):
|
|
45
|
+
found.add(_mapped_bond(row, mapping))
|
|
46
|
+
for row in by_role['exclude']:
|
|
47
|
+
for mapping in row.query.get_mapping(molecule):
|
|
48
|
+
bond = _mapped_bond(row, mapping)
|
|
49
|
+
if bond in found:
|
|
50
|
+
found.discard(bond)
|
|
51
|
+
excluded.append(LogRecord(row.id, bond, row.description))
|
|
52
|
+
with recording(molecule, stage='rotatable') as log:
|
|
53
|
+
log.extend(excluded)
|
|
54
|
+
return len(found)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def hbond_atoms(molecule, role: str) -> frozenset:
|
|
58
|
+
"""Stable ids of the atoms `tables/hbond.tsv` types with `role`.
|
|
59
|
+
|
|
60
|
+
Shared by the two counts and by `pharmacophore_invariants`, which is why it returns ids rather
|
|
61
|
+
than a number.
|
|
62
|
+
"""
|
|
63
|
+
rules = hbond_rules_by_role()
|
|
64
|
+
if role not in rules:
|
|
65
|
+
raise ValueError(f'role {role!r} is not one of {tuple(rules)}')
|
|
66
|
+
out = set()
|
|
67
|
+
for row in rules[role]:
|
|
68
|
+
subject = row.numbers[1] # the stable id of :1, from compile_smarts at load time
|
|
69
|
+
for mapping in row.query.get_mapping(molecule):
|
|
70
|
+
out.add(mapping[subject])
|
|
71
|
+
return frozenset(out)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def hydrogen_bond_donors_count(molecule) -> int:
|
|
75
|
+
"""Count of hydrogen bond donor ATOMS, over `tables/hbond.tsv`."""
|
|
76
|
+
return len(hbond_atoms(molecule, 'donor'))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def hydrogen_bond_acceptors_count(molecule) -> int:
|
|
80
|
+
"""Count of hydrogen bond acceptor ATOMS, over `tables/hbond.tsv`."""
|
|
81
|
+
return len(hbond_atoms(molecule, 'acceptor'))
|