Skip to content

Commit 20287f6

Browse files
AdrianM0claude
andcommitted
Use the retained azole names instead of spelling the rings out
Hantzsch-Widman retained names cover five-membered rings, but eight of them had no entry in the monocycle table, so the ring namer fell through to replacement nomenclature: 2-methyl-1,3,4-thiadiazole came out as 2-methyl-1-thia-3,4-diazacyclopenta-2,4-diene. That is 4,539 names in a 200k sample, 2.3% of everything generated. The existing discriminators could not tell the isomers apart. Both the gap and the distance multiset are symmetric over all heteroatoms, and 1,2,4- and 1,3,4-oxadiazole share a gap multiset of 1,2,2; so do 1,2,3- and 1,2,5- with 1,1,3. Measuring from the chalcogen instead separates each pair -- 1,2 vs 2,2, and 1,2 vs 1,1 -- and combined with the gap multiset it identifies all four isomers of each series uniquely. The O and S series then only differ by the symbol counts the specs already carry. Every one of the 4,539 changed names was checked against OPSIN independently of the self-audit: 4,516 resolve to their input, and the remaining 23 were already unparseable or wrong under their old names -- not one name got worse. Five of the eight were missing from the audit's ring-stem table too, which cost 862 confirmations until they were added. A new test asserts the two tables agree, and it immediately turned up 1,2,3-triazine as a pre-existing instance of the same omission; adding it recovers 3 more. zinc22 unchanged at 97.63%, pubchem 96.21% -> 96.22%, refutations 27. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 5264cff commit 20287f6

4 files changed

Lines changed: 216 additions & 0 deletions

File tree

src/openclatura/audit/substituent_reconstruction.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,9 +179,13 @@
179179
"1,2,3-triazole": ("[nH]1nncc1", ["1", "2", "3", "4", "5"]),
180180
"1,2,4-triazole": ("[nH]1ncnc1", ["1", "2", "3", "4", "5"]),
181181
"tetrazole": ("[nH]1nnnc1", ["1", "2", "3", "4", "5"]),
182+
"1,2,3-oxadiazole": ("o1nncc1", ["1", "2", "3", "4", "5"]),
182183
"1,2,4-oxadiazole": ("o1ncnc1", ["1", "2", "3", "4", "5"]),
184+
"1,2,5-oxadiazole": ("o1nccn1", ["1", "2", "3", "4", "5"]),
183185
"1,3,4-oxadiazole": ("o1cnnc1", ["1", "2", "3", "4", "5"]),
186+
"1,2,3-thiadiazole": ("s1nncc1", ["1", "2", "3", "4", "5"]),
184187
"1,2,4-thiadiazole": ("s1ncnc1", ["1", "2", "3", "4", "5"]),
188+
"1,2,5-thiadiazole": ("s1nccn1", ["1", "2", "3", "4", "5"]),
185189
"1,3,4-thiadiazole": ("s1cnnc1", ["1", "2", "3", "4", "5"]),
186190
# benzo-fused five-membered heterocycles (labels 1,2,3,3a,4,5,6,7,7a)
187191
"indole": ("[nH]1ccc2ccccc21", ["1", "2", "3", "3a", "4", "5", "6", "7", "7a"]),
@@ -226,6 +230,7 @@
226230
"1,2-oxazolidine": ("O1NCCC1", ["1", "2", "3", "4", "5"]),
227231
# six-membered triazines
228232
"1,3,5-triazine": ("n1cncnc1", ["1", "2", "3", "4", "5", "6"]),
233+
"1,2,3-triazine": ("n1nnccc1", ["1", "2", "3", "4", "5", "6"]),
229234
"1,2,4-triazine": ("n1ncncc1", ["1", "2", "3", "4", "5", "6"]),
230235
# benzo-fused carbocycle / 2,3-dihydro azole (labels 1,2,3,3a,4,5,6,7,7a)
231236
"indoline": ("N1CCc2ccccc21", ["1", "2", "3", "3a", "4", "5", "6", "7", "7a"]),

src/openclatura/data/namer_rules.json

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2016,6 +2016,158 @@
20162016
2
20172017
],
20182018
"no_cumulated_double_bonds": true
2019+
},
2020+
{
2021+
"name": "1,2,3-oxadiazole",
2022+
"size": 5,
2023+
"double_bonds": 2,
2024+
"symbols": {
2025+
"N": 2,
2026+
"O": 1
2027+
},
2028+
"hetero_gap_multiset": [
2029+
1,
2030+
1,
2031+
3
2032+
],
2033+
"chalcogen_nitrogen_distance_multiset": [
2034+
1,
2035+
2
2036+
],
2037+
"no_cumulated_double_bonds": true
2038+
},
2039+
{
2040+
"name": "1,2,4-oxadiazole",
2041+
"size": 5,
2042+
"double_bonds": 2,
2043+
"symbols": {
2044+
"N": 2,
2045+
"O": 1
2046+
},
2047+
"hetero_gap_multiset": [
2048+
1,
2049+
2,
2050+
2
2051+
],
2052+
"chalcogen_nitrogen_distance_multiset": [
2053+
1,
2054+
2
2055+
],
2056+
"no_cumulated_double_bonds": true
2057+
},
2058+
{
2059+
"name": "1,2,5-oxadiazole",
2060+
"size": 5,
2061+
"double_bonds": 2,
2062+
"symbols": {
2063+
"N": 2,
2064+
"O": 1
2065+
},
2066+
"hetero_gap_multiset": [
2067+
1,
2068+
1,
2069+
3
2070+
],
2071+
"chalcogen_nitrogen_distance_multiset": [
2072+
1,
2073+
1
2074+
],
2075+
"no_cumulated_double_bonds": true
2076+
},
2077+
{
2078+
"name": "1,3,4-oxadiazole",
2079+
"size": 5,
2080+
"double_bonds": 2,
2081+
"symbols": {
2082+
"N": 2,
2083+
"O": 1
2084+
},
2085+
"hetero_gap_multiset": [
2086+
1,
2087+
2,
2088+
2
2089+
],
2090+
"chalcogen_nitrogen_distance_multiset": [
2091+
2,
2092+
2
2093+
],
2094+
"no_cumulated_double_bonds": true
2095+
},
2096+
{
2097+
"name": "1,2,3-thiadiazole",
2098+
"size": 5,
2099+
"double_bonds": 2,
2100+
"symbols": {
2101+
"N": 2,
2102+
"S": 1
2103+
},
2104+
"hetero_gap_multiset": [
2105+
1,
2106+
1,
2107+
3
2108+
],
2109+
"chalcogen_nitrogen_distance_multiset": [
2110+
1,
2111+
2
2112+
],
2113+
"no_cumulated_double_bonds": true
2114+
},
2115+
{
2116+
"name": "1,2,4-thiadiazole",
2117+
"size": 5,
2118+
"double_bonds": 2,
2119+
"symbols": {
2120+
"N": 2,
2121+
"S": 1
2122+
},
2123+
"hetero_gap_multiset": [
2124+
1,
2125+
2,
2126+
2
2127+
],
2128+
"chalcogen_nitrogen_distance_multiset": [
2129+
1,
2130+
2
2131+
],
2132+
"no_cumulated_double_bonds": true
2133+
},
2134+
{
2135+
"name": "1,2,5-thiadiazole",
2136+
"size": 5,
2137+
"double_bonds": 2,
2138+
"symbols": {
2139+
"N": 2,
2140+
"S": 1
2141+
},
2142+
"hetero_gap_multiset": [
2143+
1,
2144+
1,
2145+
3
2146+
],
2147+
"chalcogen_nitrogen_distance_multiset": [
2148+
1,
2149+
1
2150+
],
2151+
"no_cumulated_double_bonds": true
2152+
},
2153+
{
2154+
"name": "1,3,4-thiadiazole",
2155+
"size": 5,
2156+
"double_bonds": 2,
2157+
"symbols": {
2158+
"N": 2,
2159+
"S": 1
2160+
},
2161+
"hetero_gap_multiset": [
2162+
1,
2163+
2,
2164+
2
2165+
],
2166+
"chalcogen_nitrogen_distance_multiset": [
2167+
2,
2168+
2
2169+
],
2170+
"no_cumulated_double_bonds": true
20192171
}
20202172
]
20212173
},

src/openclatura/rules/retained.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -941,6 +941,11 @@ def _match_data_monocycle_retained(
941941
expected_gaps = spec.get("hetero_gap_multiset")
942942
if expected_gaps is not None and _hetero_gap_multiset(mol, path) != sorted(expected_gaps):
943943
continue
944+
expected_chalcogen = spec.get("chalcogen_nitrogen_distance_multiset")
945+
if expected_chalcogen is not None and _chalcogen_nitrogen_distance_multiset(mol, path) != sorted(
946+
expected_chalcogen
947+
):
948+
continue
944949
return spec["name"]
945950
return None
946951

@@ -1031,6 +1036,21 @@ def _hetero_distance_multiset(mol: Molecule, path: list[int]) -> list[int]:
10311036
return sorted(distances)
10321037

10331038

1039+
def _chalcogen_nitrogen_distance_multiset(mol: Molecule, path: list[int]) -> list[int]:
1040+
"""Ring distances from each chalcogen to each nitrogen.
1041+
1042+
The gap multiset is symmetric over all heteroatoms, so it cannot tell
1043+
1,2,4-oxadiazole from 1,3,4-oxadiazole -- both are gaps 1,2,2. Measuring
1044+
from the chalcogen separates them: 1 and 2 against 2 and 2.
1045+
"""
1046+
1047+
size = len(path)
1048+
chalcogens = [idx for idx, atom_idx in enumerate(path) if mol.atoms[atom_idx].symbol in {"O", "S"}]
1049+
nitrogens = [idx for idx, atom_idx in enumerate(path) if mol.atoms[atom_idx].symbol == "N"]
1050+
distances = [min(abs(left - right), size - abs(left - right)) for left in chalcogens for right in nitrogens]
1051+
return sorted(distances)
1052+
1053+
10341054
def _hetero_gap_multiset(mol: Molecule, path: list[int]) -> list[int]:
10351055
hetero_indices = sorted(idx for idx, atom_idx in enumerate(path) if mol.atoms[atom_idx].symbol in {"N", "O", "S"})
10361056
if len(hetero_indices) < 3:

src/openclatura/tests/test_rules_tables.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,42 @@ def test_uncitable_added_hydrogen_keeps_the_von_baeyer_parent():
140140
name = name_smiles("N#CC[C@H](O)CNC(=O)c1c[nH]c2ccccc2c1=O")
141141
assert "quinoline" not in name
142142
assert "bicyclo[4.4.0]" in name
143+
144+
145+
def test_oxadiazoles_and_thiadiazoles_use_their_retained_names():
146+
"""Replacement nomenclature spelt these out as 1-thia-3,4-diazacyclopenta-
147+
2,4-diene; Hantzsch-Widman retained names cover 5-rings, so they should not."""
148+
149+
from openclatura import name_smiles
150+
151+
cases = {
152+
"c1cnno1": "1,2,3-oxadiazole",
153+
"c1ncno1": "1,2,4-oxadiazole",
154+
"c1nonc1": "1,2,5-oxadiazole",
155+
"c1nnco1": "1,3,4-oxadiazole",
156+
"c1cnns1": "1,2,3-thiadiazole",
157+
"c1ncns1": "1,2,4-thiadiazole",
158+
"c1nsnc1": "1,2,5-thiadiazole",
159+
"c1nncs1": "1,3,4-thiadiazole",
160+
}
161+
for smiles, expected in cases.items():
162+
assert name_smiles(smiles) == expected, smiles
163+
164+
# The isomers must stay distinguishable once substituted: a symmetric gap
165+
# multiset cannot separate 1,2,4- from 1,3,4-, which is what the chalcogen
166+
# distance criterion is for.
167+
assert name_smiles("Cc1nnc(C)s1") == "2,5-dimethyl-1,3,4-thiadiazole"
168+
assert name_smiles("Cc1ncns1") == "5-methyl-1,2,4-thiadiazole"
169+
assert name_smiles("Cc1nnc(-c2ccccc2)o1") == "2-methyl-5-phenyl-1,3,4-oxadiazole"
170+
171+
172+
def test_audit_models_every_retained_monocycle_the_namer_emits():
173+
"""A retained ring the audit has no template for abstains, so enabling one
174+
without the matching template silently stops it being verified."""
175+
176+
from openclatura.audit.reconstruction import _ALL_PARENT_TEMPLATES
177+
from openclatura.nomenclature import RULES
178+
179+
emitted = {spec["name"] for spec in RULES.retained.monocycle_specs}
180+
missing = sorted(name for name in emitted if name not in _ALL_PARENT_TEMPLATES)
181+
assert missing == []

0 commit comments

Comments
 (0)