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.
Files changed (454) hide show
  1. {chython-3.0.2/chython.egg-info → chython-3.1}/PKG-INFO +1 -1
  2. {chython-3.0.2 → chython-3.1}/chython/chemistry/_canonicalize.py +7 -3
  3. chython-3.1/chython/chemistry/_organometallics.py +133 -0
  4. {chython-3.0.2 → chython-3.1}/chython/chemistry/_standardize.py +12 -1
  5. {chython-3.0.2 → chython-3.1}/chython/chemistry/tables/salts.tsv +1 -0
  6. {chython-3.0.2 → chython-3.1}/chython/chemistry/tables/standardize_metals.tsv +7 -0
  7. chython-3.1/chython/chemistry/test/test_organometallics.py +219 -0
  8. {chython-3.0.2 → chython-3.1}/chython/core/_smiles_read.pxi +20 -0
  9. {chython-3.0.2 → chython-3.1}/chython/core/_smiles_write.pxi +12 -6
  10. {chython-3.0.2 → chython-3.1}/chython/core/test/test_smiles_read.py +49 -0
  11. {chython-3.0.2 → chython-3.1}/chython/core/test/test_smiles_write.py +18 -0
  12. {chython-3.0.2 → chython-3.1}/chython/reactions/tables/functional.tsv +11 -0
  13. {chython-3.0.2 → chython-3.1}/chython/reactions/test/_frozen_ids.py +5 -1
  14. {chython-3.0.2 → chython-3.1}/chython/test/test_facade_names.py +1 -1
  15. {chython-3.0.2 → chython-3.1/chython.egg-info}/PKG-INFO +1 -1
  16. {chython-3.0.2 → chython-3.1}/chython.egg-info/SOURCES.txt +2 -0
  17. {chython-3.0.2 → chython-3.1}/pyproject.toml +1 -1
  18. {chython-3.0.2 → chython-3.1}/LICENSE +0 -0
  19. {chython-3.0.2 → chython-3.1}/MANIFEST.in +0 -0
  20. {chython-3.0.2 → chython-3.1}/README.md +0 -0
  21. {chython-3.0.2 → chython-3.1}/build_inchi.py +0 -0
  22. {chython-3.0.2 → chython-3.1}/chython/__init__.py +0 -0
  23. {chython-3.0.2 → chython-3.1}/chython/_functions.py +0 -0
  24. {chython-3.0.2 → chython-3.1}/chython/chemistry/__init__.py +0 -0
  25. {chython-3.0.2 → chython-3.1}/chython/chemistry/_abbreviations.py +0 -0
  26. {chython-3.0.2 → chython-3.1}/chython/chemistry/_counts.py +0 -0
  27. {chython-3.0.2 → chython-3.1}/chython/chemistry/_crippen.py +0 -0
  28. {chython-3.0.2 → chython-3.1}/chython/chemistry/_hydrogens.py +0 -0
  29. {chython-3.0.2 → chython-3.1}/chython/chemistry/_implicit.py +0 -0
  30. {chython-3.0.2 → chython-3.1}/chython/chemistry/_isomers.py +0 -0
  31. {chython-3.0.2 → chython-3.1}/chython/chemistry/_maccs.py +0 -0
  32. {chython-3.0.2 → chython-3.1}/chython/chemistry/_perceive.py +0 -0
  33. {chython-3.0.2 → chython-3.1}/chython/chemistry/_pharmacophore.py +0 -0
  34. {chython-3.0.2 → chython-3.1}/chython/chemistry/_protomers.py +0 -0
  35. {chython-3.0.2 → chython-3.1}/chython/chemistry/_qed.py +0 -0
  36. {chython-3.0.2 → chython-3.1}/chython/chemistry/_residues.py +0 -0
  37. {chython-3.0.2 → chython-3.1}/chython/chemistry/_resonance.py +0 -0
  38. {chython-3.0.2 → chython-3.1}/chython/chemistry/_salts.py +0 -0
  39. {chython-3.0.2 → chython-3.1}/chython/chemistry/_saturate.py +0 -0
  40. {chython-3.0.2 → chython-3.1}/chython/chemistry/_smarts.py +0 -0
  41. {chython-3.0.2 → chython-3.1}/chython/chemistry/_tables.py +0 -0
  42. {chython-3.0.2 → chython-3.1}/chython/chemistry/_tpsa.py +0 -0
  43. {chython-3.0.2 → chython-3.1}/chython/chemistry/tables/abbreviations.tsv +0 -0
  44. {chython-3.0.2 → chython-3.1}/chython/chemistry/tables/acids.tsv +0 -0
  45. {chython-3.0.2 → chython-3.1}/chython/chemistry/tables/covalent_radii.tsv +0 -0
  46. {chython-3.0.2 → chython-3.1}/chython/chemistry/tables/crippen.tsv +0 -0
  47. {chython-3.0.2 → chython-3.1}/chython/chemistry/tables/hbond.tsv +0 -0
  48. {chython-3.0.2 → chython-3.1}/chython/chemistry/tables/maccs.tsv +0 -0
  49. {chython-3.0.2 → chython-3.1}/chython/chemistry/tables/maccs_corpus.tsv +0 -0
  50. {chython-3.0.2 → chython-3.1}/chython/chemistry/tables/pharmacophore.tsv +0 -0
  51. {chython-3.0.2 → chython-3.1}/chython/chemistry/tables/qed_alerts.tsv +0 -0
  52. {chython-3.0.2 → chython-3.1}/chython/chemistry/tables/residues.tsv +0 -0
  53. {chython-3.0.2 → chython-3.1}/chython/chemistry/tables/resonance.tsv +0 -0
  54. {chython-3.0.2 → chython-3.1}/chython/chemistry/tables/rotatable.tsv +0 -0
  55. {chython-3.0.2 → chython-3.1}/chython/chemistry/tables/standardize_groups.tsv +0 -0
  56. {chython-3.0.2 → chython-3.1}/chython/chemistry/tables/sybyl_types.tsv +0 -0
  57. {chython-3.0.2 → chython-3.1}/chython/chemistry/tables/tpsa.tsv +0 -0
  58. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/__init__.py +0 -0
  59. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/_corpus.py +0 -0
  60. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/_oracle.py +0 -0
  61. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/gen_standardize_rules.py +0 -0
  62. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_abbreviations.py +0 -0
  63. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_acids_tsv.py +0 -0
  64. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_canonicalize.py +0 -0
  65. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_counts.py +0 -0
  66. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_covalent_radii_tsv.py +0 -0
  67. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_crippen.py +0 -0
  68. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_crippen_tsv.py +0 -0
  69. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_dependency_direction.py +0 -0
  70. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_featurizer_injection.py +0 -0
  71. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_featurizer_tables_lazy.py +0 -0
  72. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_isomers.py +0 -0
  73. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_maccs.py +0 -0
  74. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_maccs_corpus.py +0 -0
  75. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_maccs_tsv.py +0 -0
  76. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_perceive.py +0 -0
  77. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_pharmacophore.py +0 -0
  78. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_protomers.py +0 -0
  79. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_qed.py +0 -0
  80. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_qed_alerts_tsv.py +0 -0
  81. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_reaction_hydrogen_repair.py +0 -0
  82. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_reaction_passes.py +0 -0
  83. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_residues.py +0 -0
  84. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_resonance.py +0 -0
  85. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_resonance_tsv.py +0 -0
  86. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_salts.py +0 -0
  87. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_saturate.py +0 -0
  88. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_smarts.py +0 -0
  89. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_standardize_differential.py +0 -0
  90. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_standardize_groups_port.py +0 -0
  91. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_standardize_overvalent_nitrogen.py +0 -0
  92. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_standardize_rules_examples.py +0 -0
  93. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_standardize_rules_merges.py +0 -0
  94. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_standardize_rules_tsv.py +0 -0
  95. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_thiele_is_single_purpose.py +0 -0
  96. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_tpsa.py +0 -0
  97. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_tpsa_tsv.py +0 -0
  98. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_valence_report.py +0 -0
  99. {chython-3.0.2 → chython-3.1}/chython/chemistry/test/test_z_translation.py +0 -0
  100. {chython-3.0.2 → chython-3.1}/chython/core/RULES.md +0 -0
  101. {chython-3.0.2 → chython-3.1}/chython/core/__init__.py +0 -0
  102. {chython-3.0.2 → chython-3.1}/chython/core/_canonical.pxi +0 -0
  103. {chython-3.0.2 → chython-3.1}/chython/core/_core.pyx +0 -0
  104. {chython-3.0.2 → chython-3.1}/chython/core/_descriptors.pxi +0 -0
  105. {chython-3.0.2 → chython-3.1}/chython/core/_elements.pxi +0 -0
  106. {chython-3.0.2 → chython-3.1}/chython/core/_facade.py +0 -0
  107. {chython-3.0.2 → chython-3.1}/chython/core/_features.pxi +0 -0
  108. {chython-3.0.2 → chython-3.1}/chython/core/_fingerprints.pxi +0 -0
  109. {chython-3.0.2 → chython-3.1}/chython/core/_hydrogens.pxi +0 -0
  110. {chython-3.0.2 → chython-3.1}/chython/core/_inchi.pxi +0 -0
  111. {chython-3.0.2 → chython-3.1}/chython/core/_isomorphism.pxi +0 -0
  112. {chython-3.0.2 → chython-3.1}/chython/core/_kekule.pxi +0 -0
  113. {chython-3.0.2 → chython-3.1}/chython/core/_log.py +0 -0
  114. {chython-3.0.2 → chython-3.1}/chython/core/_ml.pxi +0 -0
  115. {chython-3.0.2 → chython-3.1}/chython/core/_molecule_arena.pxi +0 -0
  116. {chython-3.0.2 → chython-3.1}/chython/core/_molecule_container.pxi +0 -0
  117. {chython-3.0.2 → chython-3.1}/chython/core/_molecule_topology.pxi +0 -0
  118. {chython-3.0.2 → chython-3.1}/chython/core/_molecule_views.pxi +0 -0
  119. {chython-3.0.2 → chython-3.1}/chython/core/_morgan.pxi +0 -0
  120. {chython-3.0.2 → chython-3.1}/chython/core/_pach.pxi +0 -0
  121. {chython-3.0.2 → chython-3.1}/chython/core/_pach3.pxi +0 -0
  122. {chython-3.0.2 → chython-3.1}/chython/core/_query_arena.pxi +0 -0
  123. {chython-3.0.2 → chython-3.1}/chython/core/_query_boxes.pxi +0 -0
  124. {chython-3.0.2 → chython-3.1}/chython/core/_query_container.pxi +0 -0
  125. {chython-3.0.2 → chython-3.1}/chython/core/_query_seal.pxi +0 -0
  126. {chython-3.0.2 → chython-3.1}/chython/core/_reaction_passes.py +0 -0
  127. {chython-3.0.2 → chython-3.1}/chython/core/_rings.pxi +0 -0
  128. {chython-3.0.2 → chython-3.1}/chython/core/_smarts_read.pxi +0 -0
  129. {chython-3.0.2 → chython-3.1}/chython/core/_smirks_patch.pxi +0 -0
  130. {chython-3.0.2 → chython-3.1}/chython/core/_smirks_read.pxi +0 -0
  131. {chython-3.0.2 → chython-3.1}/chython/core/_sssr.pxi +0 -0
  132. {chython-3.0.2 → chython-3.1}/chython/core/_stereo.pxi +0 -0
  133. {chython-3.0.2 → chython-3.1}/chython/core/_thiele.pxi +0 -0
  134. {chython-3.0.2 → chython-3.1}/chython/core/_valence.pxi +0 -0
  135. {chython-3.0.2 → chython-3.1}/chython/core/elements.tsv +0 -0
  136. {chython-3.0.2 → chython-3.1}/chython/core/isotopes.tsv +0 -0
  137. {chython-3.0.2 → chython-3.1}/chython/core/reaction.py +0 -0
  138. {chython-3.0.2 → chython-3.1}/chython/core/test/__init__.py +0 -0
  139. {chython-3.0.2 → chython-3.1}/chython/core/test/arena_v4_corpus.bin.gz +0 -0
  140. {chython-3.0.2 → chython-3.1}/chython/core/test/bench_ml.py +0 -0
  141. {chython-3.0.2 → chython-3.1}/chython/core/test/chytorch_oracle.py +0 -0
  142. {chython-3.0.2 → chython-3.1}/chython/core/test/gen_element_tables.py +0 -0
  143. {chython-3.0.2 → chython-3.1}/chython/core/test/gen_modeling_view_corpus.py +0 -0
  144. {chython-3.0.2 → chython-3.1}/chython/core/test/gen_pach3_corpus.py +0 -0
  145. {chython-3.0.2 → chython-3.1}/chython/core/test/gen_reaction_pach_corpus.py +0 -0
  146. {chython-3.0.2 → chython-3.1}/chython/core/test/gen_v3_fixtures.py +0 -0
  147. {chython-3.0.2 → chython-3.1}/chython/core/test/gen_v4_fixtures.py +0 -0
  148. {chython-3.0.2 → chython-3.1}/chython/core/test/gen_valence_rules.py +0 -0
  149. {chython-3.0.2 → chython-3.1}/chython/core/test/modeling_view_corpus.json.gz +0 -0
  150. {chython-3.0.2 → chython-3.1}/chython/core/test/modeling_view_corpus.py +0 -0
  151. {chython-3.0.2 → chython-3.1}/chython/core/test/oracle.py +0 -0
  152. {chython-3.0.2 → chython-3.1}/chython/core/test/pach3_corpus.py +0 -0
  153. {chython-3.0.2 → chython-3.1}/chython/core/test/pach_corpus.py +0 -0
  154. {chython-3.0.2 → chython-3.1}/chython/core/test/pach_v0_corpus.bin.gz +0 -0
  155. {chython-3.0.2 → chython-3.1}/chython/core/test/pach_v0_native_corpus.bin.gz +0 -0
  156. {chython-3.0.2 → chython-3.1}/chython/core/test/pach_v2_corpus.bin.gz +0 -0
  157. {chython-3.0.2 → chython-3.1}/chython/core/test/pach_v3_corpus.bin.gz +0 -0
  158. {chython-3.0.2 → chython-3.1}/chython/core/test/pach_v4_corpus.bin.gz +0 -0
  159. {chython-3.0.2 → chython-3.1}/chython/core/test/reaction_pach_corpus.py +0 -0
  160. {chython-3.0.2 → chython-3.1}/chython/core/test/reaction_pach_v2_corpus.bin.gz +0 -0
  161. {chython-3.0.2 → chython-3.1}/chython/core/test/test_aggregates.py +0 -0
  162. {chython-3.0.2 → chython-3.1}/chython/core/test/test_alternative_spellings.py +0 -0
  163. {chython-3.0.2 → chython-3.1}/chython/core/test/test_apply_scratch_probe.py +0 -0
  164. {chython-3.0.2 → chython-3.1}/chython/core/test/test_arena_f60.py +0 -0
  165. {chython-3.0.2 → chython-3.1}/chython/core/test/test_arena_identity.py +0 -0
  166. {chython-3.0.2 → chython-3.1}/chython/core/test/test_arena_v3_compat.py +0 -0
  167. {chython-3.0.2 → chython-3.1}/chython/core/test/test_arena_v4_compat.py +0 -0
  168. {chython-3.0.2 → chython-3.1}/chython/core/test/test_aromatic_storage.py +0 -0
  169. {chython-3.0.2 → chython-3.1}/chython/core/test/test_canonical.py +0 -0
  170. {chython-3.0.2 → chython-3.1}/chython/core/test/test_canonical_mirror.py +0 -0
  171. {chython-3.0.2 → chython-3.1}/chython/core/test/test_cip_storage.py +0 -0
  172. {chython-3.0.2 → chython-3.1}/chython/core/test/test_clean_isotopes_and_coordinate_bonds.py +0 -0
  173. {chython-3.0.2 → chython-3.1}/chython/core/test/test_clean_stereo.py +0 -0
  174. {chython-3.0.2 → chython-3.1}/chython/core/test/test_conformers.py +0 -0
  175. {chython-3.0.2 → chython-3.1}/chython/core/test/test_container_log.py +0 -0
  176. {chython-3.0.2 → chython-3.1}/chython/core/test/test_copy_caches.py +0 -0
  177. {chython-3.0.2 → chython-3.1}/chython/core/test/test_derive.py +0 -0
  178. {chython-3.0.2 → chython-3.1}/chython/core/test/test_descriptors.py +0 -0
  179. {chython-3.0.2 → chython-3.1}/chython/core/test/test_element_tables.py +0 -0
  180. {chython-3.0.2 → chython-3.1}/chython/core/test/test_facade.py +0 -0
  181. {chython-3.0.2 → chython-3.1}/chython/core/test/test_features.py +0 -0
  182. {chython-3.0.2 → chython-3.1}/chython/core/test/test_featurizer_injection.py +0 -0
  183. {chython-3.0.2 → chython-3.1}/chython/core/test/test_fingerprints.py +0 -0
  184. {chython-3.0.2 → chython-3.1}/chython/core/test/test_geometry.py +0 -0
  185. {chython-3.0.2 → chython-3.1}/chython/core/test/test_h_unknown.py +0 -0
  186. {chython-3.0.2 → chython-3.1}/chython/core/test/test_hydrogens.py +0 -0
  187. {chython-3.0.2 → chython-3.1}/chython/core/test/test_inchi.py +0 -0
  188. {chython-3.0.2 → chython-3.1}/chython/core/test/test_interop_injection.py +0 -0
  189. {chython-3.0.2 → chython-3.1}/chython/core/test/test_isomorphism.py +0 -0
  190. {chython-3.0.2 → chython-3.1}/chython/core/test/test_kekule.py +0 -0
  191. {chython-3.0.2 → chython-3.1}/chython/core/test/test_log.py +0 -0
  192. {chython-3.0.2 → chython-3.1}/chython/core/test/test_magic.py +0 -0
  193. {chython-3.0.2 → chython-3.1}/chython/core/test/test_meta.py +0 -0
  194. {chython-3.0.2 → chython-3.1}/chython/core/test/test_ml_encoding.py +0 -0
  195. {chython-3.0.2 → chython-3.1}/chython/core/test/test_ml_reaction_transition.py +0 -0
  196. {chython-3.0.2 → chython-3.1}/chython/core/test/test_ml_state_view.py +0 -0
  197. {chython-3.0.2 → chython-3.1}/chython/core/test/test_ml_transition_view.py +0 -0
  198. {chython-3.0.2 → chython-3.1}/chython/core/test/test_ml_unpack_differential.py +0 -0
  199. {chython-3.0.2 → chython-3.1}/chython/core/test/test_modeling_view_frozen.py +0 -0
  200. {chython-3.0.2 → chython-3.1}/chython/core/test/test_molecule.py +0 -0
  201. {chython-3.0.2 → chython-3.1}/chython/core/test/test_morgan.py +0 -0
  202. {chython-3.0.2 → chython-3.1}/chython/core/test/test_no_chython_two_imports.py +0 -0
  203. {chython-3.0.2 → chython-3.1}/chython/core/test/test_oracle.py +0 -0
  204. {chython-3.0.2 → chython-3.1}/chython/core/test/test_pach.py +0 -0
  205. {chython-3.0.2 → chython-3.1}/chython/core/test/test_pach3.py +0 -0
  206. {chython-3.0.2 → chython-3.1}/chython/core/test/test_pack.py +0 -0
  207. {chython-3.0.2 → chython-3.1}/chython/core/test/test_query.py +0 -0
  208. {chython-3.0.2 → chython-3.1}/chython/core/test/test_r_edit.py +0 -0
  209. {chython-3.0.2 → chython-3.1}/chython/core/test/test_r_query.py +0 -0
  210. {chython-3.0.2 → chython-3.1}/chython/core/test/test_r_semantics.py +0 -0
  211. {chython-3.0.2 → chython-3.1}/chython/core/test/test_r_serialisation.py +0 -0
  212. {chython-3.0.2 → chython-3.1}/chython/core/test/test_r_smirks.py +0 -0
  213. {chython-3.0.2 → chython-3.1}/chython/core/test/test_r_storage.py +0 -0
  214. {chython-3.0.2 → chython-3.1}/chython/core/test/test_reaction_container.py +0 -0
  215. {chython-3.0.2 → chython-3.1}/chython/core/test/test_reaction_identity.py +0 -0
  216. {chython-3.0.2 → chython-3.1}/chython/core/test/test_reaction_pach.py +0 -0
  217. {chython-3.0.2 → chython-3.1}/chython/core/test/test_reaction_passes.py +0 -0
  218. {chython-3.0.2 → chython-3.1}/chython/core/test/test_reaction_patch_stereo.py +0 -0
  219. {chython-3.0.2 → chython-3.1}/chython/core/test/test_reaction_smiles.py +0 -0
  220. {chython-3.0.2 → chython-3.1}/chython/core/test/test_rings.py +0 -0
  221. {chython-3.0.2 → chython-3.1}/chython/core/test/test_rings_c60.py +0 -0
  222. {chython-3.0.2 → chython-3.1}/chython/core/test/test_set_element.py +0 -0
  223. {chython-3.0.2 → chython-3.1}/chython/core/test/test_sgroups.py +0 -0
  224. {chython-3.0.2 → chython-3.1}/chython/core/test/test_smarts_read.py +0 -0
  225. {chython-3.0.2 → chython-3.1}/chython/core/test/test_smiles_r.py +0 -0
  226. {chython-3.0.2 → chython-3.1}/chython/core/test/test_smiles_roundtrip.py +0 -0
  227. {chython-3.0.2 → chython-3.1}/chython/core/test/test_smiles_write_aromatic.py +0 -0
  228. {chython-3.0.2 → chython-3.1}/chython/core/test/test_smiles_write_cis_trans.py +0 -0
  229. {chython-3.0.2 → chython-3.1}/chython/core/test/test_smiles_write_detached.py +0 -0
  230. {chython-3.0.2 → chython-3.1}/chython/core/test/test_smiles_write_differential.py +0 -0
  231. {chython-3.0.2 → chython-3.1}/chython/core/test/test_smiles_write_h_unknown.py +0 -0
  232. {chython-3.0.2 → chython-3.1}/chython/core/test/test_smiles_write_stereo.py +0 -0
  233. {chython-3.0.2 → chython-3.1}/chython/core/test/test_smiles_write_sticky.py +0 -0
  234. {chython-3.0.2 → chython-3.1}/chython/core/test/test_smirks_filter.py +0 -0
  235. {chython-3.0.2 → chython-3.1}/chython/core/test/test_smirks_patch.py +0 -0
  236. {chython-3.0.2 → chython-3.1}/chython/core/test/test_smirks_read.py +0 -0
  237. {chython-3.0.2 → chython-3.1}/chython/core/test/test_smirks_report.py +0 -0
  238. {chython-3.0.2 → chython-3.1}/chython/core/test/test_smirks_stereo.py +0 -0
  239. {chython-3.0.2 → chython-3.1}/chython/core/test/test_stereo_acceptance.py +0 -0
  240. {chython-3.0.2 → chython-3.1}/chython/core/test/test_stereo_parity.py +0 -0
  241. {chython-3.0.2 → chython-3.1}/chython/core/test/test_stereo_perception.py +0 -0
  242. {chython-3.0.2 → chython-3.1}/chython/core/test/test_stereo_query.py +0 -0
  243. {chython-3.0.2 → chython-3.1}/chython/core/test/test_stereo_units.py +0 -0
  244. {chython-3.0.2 → chython-3.1}/chython/core/test/test_stereo_v2_differential.py +0 -0
  245. {chython-3.0.2 → chython-3.1}/chython/core/test/test_structure.py +0 -0
  246. {chython-3.0.2 → chython-3.1}/chython/core/test/test_thiele.py +0 -0
  247. {chython-3.0.2 → chython-3.1}/chython/core/test/test_title.py +0 -0
  248. {chython-3.0.2 → chython-3.1}/chython/core/test/test_topology.py +0 -0
  249. {chython-3.0.2 → chython-3.1}/chython/core/test/test_union_stereo.py +0 -0
  250. {chython-3.0.2 → chython-3.1}/chython/core/test/test_valence.py +0 -0
  251. {chython-3.0.2 → chython-3.1}/chython/core/test/test_view_surface.py +0 -0
  252. {chython-3.0.2 → chython-3.1}/chython/core/test/v3_fixtures.py +0 -0
  253. {chython-3.0.2 → chython-3.1}/chython/core/test/v4_fixtures.py +0 -0
  254. {chython-3.0.2 → chython-3.1}/chython/core/valence_rules.tsv +0 -0
  255. {chython-3.0.2 → chython-3.1}/chython/core/wedge.py +0 -0
  256. {chython-3.0.2 → chython-3.1}/chython/depict/__init__.py +0 -0
  257. {chython-3.0.2 → chython-3.1}/chython/depict/_config.py +0 -0
  258. {chython-3.0.2 → chython-3.1}/chython/depict/_hooks.py +0 -0
  259. {chython-3.0.2 → chython-3.1}/chython/depict/bonds.py +0 -0
  260. {chython-3.0.2 → chython-3.1}/chython/depict/colorbar.py +0 -0
  261. {chython-3.0.2 → chython-3.1}/chython/depict/colormap.py +0 -0
  262. {chython-3.0.2 → chython-3.1}/chython/depict/field.py +0 -0
  263. {chython-3.0.2 → chython-3.1}/chython/depict/figure.py +0 -0
  264. {chython-3.0.2 → chython-3.1}/chython/depict/label.py +0 -0
  265. {chython-3.0.2 → chython-3.1}/chython/depict/layout/__init__.py +0 -0
  266. {chython-3.0.2 → chython-3.1}/chython/depict/layout/clean2d.js +0 -0
  267. {chython-3.0.2 → chython-3.1}/chython/depict/layout/molecule.py +0 -0
  268. {chython-3.0.2 → chython-3.1}/chython/depict/layout/reaction.py +0 -0
  269. {chython-3.0.2 → chython-3.1}/chython/depict/metrics/__init__.py +0 -0
  270. {chython-3.0.2 → chython-3.1}/chython/depict/metrics/helvetica.tsv +0 -0
  271. {chython-3.0.2 → chython-3.1}/chython/depict/metrics/times.tsv +0 -0
  272. {chython-3.0.2 → chython-3.1}/chython/depict/overlay.py +0 -0
  273. {chython-3.0.2 → chython-3.1}/chython/depict/render/__init__.py +0 -0
  274. {chython-3.0.2 → chython-3.1}/chython/depict/render/svg.py +0 -0
  275. {chython-3.0.2 → chython-3.1}/chython/depict/scene.py +0 -0
  276. {chython-3.0.2 → chython-3.1}/chython/depict/style.py +0 -0
  277. {chython-3.0.2 → chython-3.1}/chython/depict/test/__init__.py +0 -0
  278. {chython-3.0.2 → chython-3.1}/chython/depict/test/test_bonds.py +0 -0
  279. {chython-3.0.2 → chython-3.1}/chython/depict/test/test_clean2d.py +0 -0
  280. {chython-3.0.2 → chython-3.1}/chython/depict/test/test_colorbar.py +0 -0
  281. {chython-3.0.2 → chython-3.1}/chython/depict/test/test_colormap.py +0 -0
  282. {chython-3.0.2 → chython-3.1}/chython/depict/test/test_field.py +0 -0
  283. {chython-3.0.2 → chython-3.1}/chython/depict/test/test_figure.py +0 -0
  284. {chython-3.0.2 → chython-3.1}/chython/depict/test/test_label.py +0 -0
  285. {chython-3.0.2 → chython-3.1}/chython/depict/test/test_metrics.py +0 -0
  286. {chython-3.0.2 → chython-3.1}/chython/depict/test/test_overlay.py +0 -0
  287. {chython-3.0.2 → chython-3.1}/chython/depict/test/test_package.py +0 -0
  288. {chython-3.0.2 → chython-3.1}/chython/depict/test/test_r_atom.py +0 -0
  289. {chython-3.0.2 → chython-3.1}/chython/depict/test/test_scene.py +0 -0
  290. {chython-3.0.2 → chython-3.1}/chython/depict/test/test_style.py +0 -0
  291. {chython-3.0.2 → chython-3.1}/chython/depict/test/test_svg.py +0 -0
  292. {chython-3.0.2 → chython-3.1}/chython/depict/test/test_wedge_draw.py +0 -0
  293. {chython-3.0.2 → chython-3.1}/chython/depict/test/test_x3dom.py +0 -0
  294. {chython-3.0.2 → chython-3.1}/chython/depict/wedge.py +0 -0
  295. {chython-3.0.2 → chython-3.1}/chython/depict/x3dom.py +0 -0
  296. {chython-3.0.2 → chython-3.1}/chython/exceptions.py +0 -0
  297. {chython-3.0.2 → chython-3.1}/chython/formats/__init__.py +0 -0
  298. {chython-3.0.2 → chython-3.1}/chython/formats/_text.py +0 -0
  299. {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/CTFILE.md +0 -0
  300. {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/__init__.py +0 -0
  301. {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/_ctab.py +0 -0
  302. {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/_errors.py +0 -0
  303. {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/_facade.py +0 -0
  304. {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/_hydrogens.py +0 -0
  305. {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/_rdf.py +0 -0
  306. {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/_rxn.py +0 -0
  307. {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/_sdf.py +0 -0
  308. {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/_sgroup.py +0 -0
  309. {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/_stream.py +0 -0
  310. {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/_tokens.py +0 -0
  311. {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/_v2000.py +0 -0
  312. {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/_v3000.py +0 -0
  313. {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/test/__init__.py +0 -0
  314. {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/test/conftest.py +0 -0
  315. {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/test/test_aromatic.py +0 -0
  316. {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/test/test_container_log.py +0 -0
  317. {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/test/test_data_labels.py +0 -0
  318. {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/test/test_facade.py +0 -0
  319. {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/test/test_fidelity.py +0 -0
  320. {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/test/test_hydrogens.py +0 -0
  321. {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/test/test_r_atom.py +0 -0
  322. {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/test/test_rdf.py +0 -0
  323. {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/test/test_rxn.py +0 -0
  324. {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/test/test_sdf.py +0 -0
  325. {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/test/test_sgroup.py +0 -0
  326. {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/test/test_stream.py +0 -0
  327. {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/test/test_tokens.py +0 -0
  328. {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/test/test_v2000.py +0 -0
  329. {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/test/test_v3000.py +0 -0
  330. {chython-3.0.2 → chython-3.1}/chython/formats/ctfile/test/test_wedge.py +0 -0
  331. {chython-3.0.2 → chython-3.1}/chython/formats/mol2.py +0 -0
  332. {chython-3.0.2 → chython-3.1}/chython/formats/pdb/__init__.py +0 -0
  333. {chython-3.0.2 → chython-3.1}/chython/formats/pdb/_builder.py +0 -0
  334. {chython-3.0.2 → chython-3.1}/chython/formats/pdb/_legacy.py +0 -0
  335. {chython-3.0.2 → chython-3.1}/chython/formats/pdb/_mmcif.py +0 -0
  336. {chython-3.0.2 → chython-3.1}/chython/formats/pdb/_records.py +0 -0
  337. {chython-3.0.2 → chython-3.1}/chython/formats/pdb/_star.py +0 -0
  338. {chython-3.0.2 → chython-3.1}/chython/formats/test/__init__.py +0 -0
  339. {chython-3.0.2 → chython-3.1}/chython/formats/test/conftest.py +0 -0
  340. {chython-3.0.2 → chython-3.1}/chython/formats/test/oracles.py +0 -0
  341. {chython-3.0.2 → chython-3.1}/chython/formats/test/test_conformance.py +0 -0
  342. {chython-3.0.2 → chython-3.1}/chython/formats/test/test_done_when.py +0 -0
  343. {chython-3.0.2 → chython-3.1}/chython/formats/test/test_isolation.py +0 -0
  344. {chython-3.0.2 → chython-3.1}/chython/formats/test/test_log_prefix.py +0 -0
  345. {chython-3.0.2 → chython-3.1}/chython/formats/test/test_mmcif.py +0 -0
  346. {chython-3.0.2 → chython-3.1}/chython/formats/test/test_mol2.py +0 -0
  347. {chython-3.0.2 → chython-3.1}/chython/formats/test/test_no_review_bookkeeping.py +0 -0
  348. {chython-3.0.2 → chython-3.1}/chython/formats/test/test_oracles.py +0 -0
  349. {chython-3.0.2 → chython-3.1}/chython/formats/test/test_pdb.py +0 -0
  350. {chython-3.0.2 → chython-3.1}/chython/formats/test/test_pdb_builder.py +0 -0
  351. {chython-3.0.2 → chython-3.1}/chython/formats/test/test_perceive_agreement.py +0 -0
  352. {chython-3.0.2 → chython-3.1}/chython/formats/test/test_read_only_facades.py +0 -0
  353. {chython-3.0.2 → chython-3.1}/chython/formats/test/test_xyz.py +0 -0
  354. {chython-3.0.2 → chython-3.1}/chython/formats/test/test_xyz_builder.py +0 -0
  355. {chython-3.0.2 → chython-3.1}/chython/formats/xml/__init__.py +0 -0
  356. {chython-3.0.2 → chython-3.1}/chython/formats/xml/_cml.py +0 -0
  357. {chython-3.0.2 → chython-3.1}/chython/formats/xml/_dialect.py +0 -0
  358. {chython-3.0.2 → chython-3.1}/chython/formats/xml/_errors.py +0 -0
  359. {chython-3.0.2 → chython-3.1}/chython/formats/xml/_facade.py +0 -0
  360. {chython-3.0.2 → chython-3.1}/chython/formats/xml/_mrv.py +0 -0
  361. {chython-3.0.2 → chython-3.1}/chython/formats/xml/_tree.py +0 -0
  362. {chython-3.0.2 → chython-3.1}/chython/formats/xml/test/__init__.py +0 -0
  363. {chython-3.0.2 → chython-3.1}/chython/formats/xml/test/conftest.py +0 -0
  364. {chython-3.0.2 → chython-3.1}/chython/formats/xml/test/test_cml.py +0 -0
  365. {chython-3.0.2 → chython-3.1}/chython/formats/xml/test/test_container_log.py +0 -0
  366. {chython-3.0.2 → chython-3.1}/chython/formats/xml/test/test_dialect.py +0 -0
  367. {chython-3.0.2 → chython-3.1}/chython/formats/xml/test/test_equivalence.py +0 -0
  368. {chython-3.0.2 → chython-3.1}/chython/formats/xml/test/test_facade.py +0 -0
  369. {chython-3.0.2 → chython-3.1}/chython/formats/xml/test/test_mrv.py +0 -0
  370. {chython-3.0.2 → chython-3.1}/chython/formats/xml/test/test_mrv_census.py +0 -0
  371. {chython-3.0.2 → chython-3.1}/chython/formats/xml/test/test_tree.py +0 -0
  372. {chython-3.0.2 → chython-3.1}/chython/formats/xyz.py +0 -0
  373. {chython-3.0.2 → chython-3.1}/chython/interop/__init__.py +0 -0
  374. {chython-3.0.2 → chython-3.1}/chython/interop/_cdk.py +0 -0
  375. {chython-3.0.2 → chython-3.1}/chython/interop/_cdpkit.py +0 -0
  376. {chython-3.0.2 → chython-3.1}/chython/interop/_indigo.py +0 -0
  377. {chython-3.0.2 → chython-3.1}/chython/interop/_iupac.py +0 -0
  378. {chython-3.0.2 → chython-3.1}/chython/interop/_java.py +0 -0
  379. {chython-3.0.2 → chython-3.1}/chython/interop/_openbabel.py +0 -0
  380. {chython-3.0.2 → chython-3.1}/chython/interop/_pandas.py +0 -0
  381. {chython-3.0.2 → chython-3.1}/chython/interop/_rdkit.py +0 -0
  382. {chython-3.0.2 → chython-3.1}/chython/interop/_records.py +0 -0
  383. {chython-3.0.2 → chython-3.1}/chython/interop/_stereo.py +0 -0
  384. {chython-3.0.2 → chython-3.1}/chython/interop/config.py +0 -0
  385. {chython-3.0.2 → chython-3.1}/chython/interop/conformers.py +0 -0
  386. {chython-3.0.2 → chython-3.1}/chython/interop/test/__init__.py +0 -0
  387. {chython-3.0.2 → chython-3.1}/chython/interop/test/conftest.py +0 -0
  388. {chython-3.0.2 → chython-3.1}/chython/interop/test/test_cdk.py +0 -0
  389. {chython-3.0.2 → chython-3.1}/chython/interop/test/test_cdpkit.py +0 -0
  390. {chython-3.0.2 → chython-3.1}/chython/interop/test/test_config.py +0 -0
  391. {chython-3.0.2 → chython-3.1}/chython/interop/test/test_conformers.py +0 -0
  392. {chython-3.0.2 → chython-3.1}/chython/interop/test/test_coordinate_honesty.py +0 -0
  393. {chython-3.0.2 → chython-3.1}/chython/interop/test/test_dispatch.py +0 -0
  394. {chython-3.0.2 → chython-3.1}/chython/interop/test/test_indigo.py +0 -0
  395. {chython-3.0.2 → chython-3.1}/chython/interop/test/test_iupac.py +0 -0
  396. {chython-3.0.2 → chython-3.1}/chython/interop/test/test_log_delivery.py +0 -0
  397. {chython-3.0.2 → chython-3.1}/chython/interop/test/test_openbabel.py +0 -0
  398. {chython-3.0.2 → chython-3.1}/chython/interop/test/test_pandas.py +0 -0
  399. {chython-3.0.2 → chython-3.1}/chython/interop/test/test_rdkit.py +0 -0
  400. {chython-3.0.2 → chython-3.1}/chython/interop/test/test_stereo.py +0 -0
  401. {chython-3.0.2 → chython-3.1}/chython/interop/test/test_v2_oracle.py +0 -0
  402. {chython-3.0.2 → chython-3.1}/chython/reactions/__init__.py +0 -0
  403. {chython-3.0.2 → chython-3.1}/chython/reactions/_enumerate.py +0 -0
  404. {chython-3.0.2 → chython-3.1}/chython/reactions/_numbering.py +0 -0
  405. {chython-3.0.2 → chython-3.1}/chython/reactions/_reconstruct.py +0 -0
  406. {chython-3.0.2 → chython-3.1}/chython/reactions/_stickers.py +0 -0
  407. {chython-3.0.2 → chython-3.1}/chython/reactions/_tables.py +0 -0
  408. {chython-3.0.2 → chython-3.1}/chython/reactions/attention/__init__.py +0 -0
  409. {chython-3.0.2 → chython-3.1}/chython/reactions/attention/_assign.py +0 -0
  410. {chython-3.0.2 → chython-3.1}/chython/reactions/attention/_encode.py +0 -0
  411. {chython-3.0.2 → chython-3.1}/chython/reactions/attention/_session.py +0 -0
  412. {chython-3.0.2 → chython-3.1}/chython/reactions/tables/protective.tsv +0 -0
  413. {chython-3.0.2 → chython-3.1}/chython/reactions/tables/reactions.tsv +0 -0
  414. {chython-3.0.2 → chython-3.1}/chython/reactions/tables/roles.tsv +0 -0
  415. {chython-3.0.2 → chython-3.1}/chython/reactions/test/__init__.py +0 -0
  416. {chython-3.0.2 → chython-3.1}/chython/reactions/test/gen_corpus_glossary.py +0 -0
  417. {chython-3.0.2 → chython-3.1}/chython/reactions/test/golden_subset.smi +0 -0
  418. {chython-3.0.2 → chython-3.1}/chython/reactions/test/test_attention.py +0 -0
  419. {chython-3.0.2 → chython-3.1}/chython/reactions/test/test_attention_assign.py +0 -0
  420. {chython-3.0.2 → chython-3.1}/chython/reactions/test/test_attention_encode.py +0 -0
  421. {chython-3.0.2 → chython-3.1}/chython/reactions/test/test_attention_isolation.py +0 -0
  422. {chython-3.0.2 → chython-3.1}/chython/reactions/test/test_corpus_glossary.py +0 -0
  423. {chython-3.0.2 → chython-3.1}/chython/reactions/test/test_dependency_direction.py +0 -0
  424. {chython-3.0.2 → chython-3.1}/chython/reactions/test/test_enumerate.py +0 -0
  425. {chython-3.0.2 → chython-3.1}/chython/reactions/test/test_functional.py +0 -0
  426. {chython-3.0.2 → chython-3.1}/chython/reactions/test/test_id_stability.py +0 -0
  427. {chython-3.0.2 → chython-3.1}/chython/reactions/test/test_numbering.py +0 -0
  428. {chython-3.0.2 → chython-3.1}/chython/reactions/test/test_probes.py +0 -0
  429. {chython-3.0.2 → chython-3.1}/chython/reactions/test/test_protective.py +0 -0
  430. {chython-3.0.2 → chython-3.1}/chython/reactions/test/test_reconstruct.py +0 -0
  431. {chython-3.0.2 → chython-3.1}/chython/reactions/test/test_roles.py +0 -0
  432. {chython-3.0.2 → chython-3.1}/chython/reactions/test/test_stickers.py +0 -0
  433. {chython-3.0.2 → chython-3.1}/chython/reactions/test/test_tables.py +0 -0
  434. {chython-3.0.2 → chython-3.1}/chython/test/__init__.py +0 -0
  435. {chython-3.0.2 → chython-3.1}/chython/test/test_code_hygiene.py +0 -0
  436. {chython-3.0.2 → chython-3.1}/chython/test/test_container_methods.py +0 -0
  437. {chython-3.0.2 → chython-3.1}/chython/test/test_doc_figures.py +0 -0
  438. {chython-3.0.2 → chython-3.1}/chython/test/test_doc_references.py +0 -0
  439. {chython-3.0.2 → chython-3.1}/chython/test/test_doc_samples.py +0 -0
  440. {chython-3.0.2 → chython-3.1}/chython/test/test_hydrogen_parity.py +0 -0
  441. {chython-3.0.2 → chython-3.1}/chython/test/test_libinchi_staging.py +0 -0
  442. {chython-3.0.2 → chython-3.1}/chython/test/test_log_records.py +0 -0
  443. {chython-3.0.2 → chython-3.1}/chython/test/test_optional_numpy.py +0 -0
  444. {chython-3.0.2 → chython-3.1}/chython/test/test_packaging.py +0 -0
  445. {chython-3.0.2 → chython-3.1}/chython/test/test_performance.py +0 -0
  446. {chython-3.0.2 → chython-3.1}/chython/test/test_r_atom_integration.py +0 -0
  447. {chython-3.0.2 → chython-3.1}/chython/test/test_release_build.py +0 -0
  448. {chython-3.0.2 → chython-3.1}/chython/test/test_stereo_bluebook.py +0 -0
  449. {chython-3.0.2 → chython-3.1}/chython/test/test_v2_boundary.py +0 -0
  450. {chython-3.0.2 → chython-3.1}/chython.egg-info/dependency_links.txt +0 -0
  451. {chython-3.0.2 → chython-3.1}/chython.egg-info/requires.txt +0 -0
  452. {chython-3.0.2 → chython-3.1}/chython.egg-info/top_level.txt +0 -0
  453. {chython-3.0.2 → chython-3.1}/setup.cfg +0 -0
  454. {chython-3.0.2 → chython-3.1}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chython
3
- Version: 3.0.2
3
+ Version: 3.1
4
4
  Summary: Library for processing molecules and reactions in python way
5
5
  Author-email: Ramil Nugmanov <nougmanoff@protonmail.com>
6
6
  License-Expression: LGPL-3.0-or-later
@@ -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()`. 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.
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 and then the metal-organic ones, then recomputes the implicit
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 and (and_groups or or_groups):
2322
- # ONLY BESIDE AN AND OR OR COLLECTION, where it says which centres are NOT in one. Alone it
2323
- # says only "these centres are absolute", which is what a configured atom outside any
2324
- # collection already means, so the field would separate two spellings of one structure -- a
2325
- # molfile that named STEABS and a SMILES that had nowhere to name it. The cost is that a
2326
- # round trip through SMILES turns a lone explicit ABS kind into an unspecified one.
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`: 249 rows, id -> the row it names.
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) == 249 and len(protective) == 103
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',)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chython
3
- Version: 3.0.2
3
+ Version: 3.1
4
4
  Summary: Library for processing molecules and reactions in python way
5
5
  Author-email: Ramil Nugmanov <nougmanoff@protonmail.com>
6
6
  License-Expression: LGPL-3.0-or-later