Skip to content

Commit 8b68a0e

Browse files
AdrianM0claude
andcommitted
Cite a mancude parent's surplus saturation as added hydrogen
A mancude parent hydride supports a fixed number of indicated hydrogens -- purine's nine skeletal atoms and four double bonds leave exactly one, which is why it is cited as 7H- or 9H-purine. Saturating further positions, as a 2,6-dione does, is *added* hydrogen and takes a hydro prefix. openclatura instead emitted one indicated-hydrogen citation per saturated position, and joined them as a single locant set: `1,7H-purine-2,6-dione`, which is not a readable name -- OPSIN rejects it outright. Three changes: * Carry the supported indicated-hydrogen count on the retained parent metadata, taken from the template that declares its own positions (1H-indole, 9H-xanthene) or else from the mancude double-bond count. * Move saturation beyond that count into a hydro operation, keeping the lowest locants indicated. A hydro prefix saturates whole double bonds, so it can only absorb an even surplus; an odd one stays indicated, as `1H,9H-purin-6-one`, which is still readable. * Count a saturated ring nitrogen whether it holds the hydrogen or a substituent. The positions are a property of the parent hydride, not of what occupies them: theophylline is 1,3-dimethyl-3,7-dihydro-1H-purine-2,6-dione even though N1 and N3 are both methylated. Xanthine, theophylline and caffeine now match the reference tool exactly, and every name involved round-trips through OPSIN to its input. The caffeine test pinned `1,3,7-trimethylpurine-2,6-dione`; that name also resolves to caffeine, so it was incomplete rather than wrong, and is updated. Two corpus names improve; both were already correct and remain so. They stop being confirmed only because the audit models neither multiple indicated hydrogens nor added hydrogen yet. zinc22 97.63% unchanged, pubchem 96.33%, refutations unchanged at 27. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent c54038f commit 8b68a0e

5 files changed

Lines changed: 80 additions & 4 deletions

File tree

src/openclatura/additive.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Explicit additive/replacement feature collection for selected parents."""
22

33
from .assembly_parts import AssemblyParts, SubstituentItem
4+
from .locants import parse_locant
45
from .molecule import Molecule
56
from .name_operations import HydroOperation
67
from .namer_config import INDICATED_H_RETAINED_NAMES
@@ -28,7 +29,16 @@ def add_indicated_hydrogens(mol: Molecule, parts: AssemblyParts, numbered_path:
2829
if metadata is not None and default_indicated_h and atom.is_carbon and locant not in default_indicated_h:
2930
continue
3031
if oxo_derivative:
31-
if atom.explicit_h_count + atom.total_h_count <= 0:
32+
# A ring nitrogen at a saturated position is a hydrogenated position
33+
# of the parent hydride whether it holds the hydrogen or a
34+
# substituent, so theophylline is
35+
# 1,3-dimethyl-3,7-dihydro-1H-purine-2,6-dione: N1 and N3 count even
36+
# though both are methylated.
37+
ring_bond_order = sum(
38+
bond.order for n in mol.get_neighbors(idx) if n in numbered_path and (bond := mol.get_bond(idx, n))
39+
)
40+
substituted_ring_nitrogen = atom.symbol == "N" and ring_bond_order == 2
41+
if atom.explicit_h_count + atom.total_h_count <= 0 and not substituted_ring_nitrogen:
3242
continue
3343
# Hydrogen introduced next to =O is implied by ``-one``/``-dione``
3444
# and must not become a new indicated-H locant. Carbon is allowed
@@ -77,6 +87,28 @@ def add_indicated_hydrogens(mol: Molecule, parts: AssemblyParts, numbered_path:
7787
)
7888
return
7989

90+
# A mancude parent hydride supports a fixed number of indicated hydrogens.
91+
# Saturated positions beyond that are *added* hydrogen and take a hydro
92+
# prefix, so xanthine is 3,7-dihydro-1H-purine-2,6-dione rather than a run
93+
# of indicated-H citations. The lowest locants stay indicated.
94+
# A hydro prefix saturates whole double bonds, so it can only absorb an even
95+
# surplus. An odd one stays as indicated hydrogen (1H,9H-purin-6-one),
96+
# which is still a readable name.
97+
supported = metadata.indicated_hydrogen_count if metadata is not None else len(candidates)
98+
if metadata is not None and len(candidates) > supported and (len(candidates) - supported) % 2 == 0:
99+
candidates.sort(key=lambda candidate: parse_locant(candidate[0]))
100+
surplus = candidates[supported:]
101+
candidates = candidates[:supported]
102+
parts.hydro_operations.append(
103+
HydroOperation(
104+
key="additive_hydrogen",
105+
reason="Saturation beyond the parent's indicated hydrogen is added hydrogen.",
106+
locants=tuple(locant for locant, _ in surplus),
107+
atom_ids=tuple(atom_idx for _, atom_idx in surplus),
108+
operation_kind="additive_hydrogen",
109+
)
110+
)
111+
80112
for locant, atom_idx in candidates:
81113
parts.indicated_hydrogens.append(locant)
82114
parts.hydro_operations.append(

src/openclatura/assembler.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -362,10 +362,17 @@ def _add_indicated_hydrogen_prefix(parts: AssemblyParts, core_name: str) -> str:
362362
return core_name
363363
if indicated_hydrogens:
364364
indicated_hydrogens = sorted(set(indicated_hydrogens), key=parse_locant)
365-
core_name = ",".join(indicated_hydrogens) + "H-" + core_name
365+
# Every cited position carries its own H -- ``1H,7H-purine``, not
366+
# ``1,7H-purine``, which is not a readable name.
367+
core_name = ",".join(f"{locant}H" for locant in indicated_hydrogens) + "-" + core_name
366368
if additive_hydrogens:
367369
additive_hydrogens = sorted(set(additive_hydrogens), key=parse_locant)
368-
core_name = f"{','.join(additive_hydrogens)}-{format_multiplier('hydro', len(additive_hydrogens))}{core_name}"
370+
# A locant may not butt against the prefix before it, so a hydro prefix
371+
# followed by indicated hydrogen needs a separator:
372+
# ``3,7-dihydro-1H-purine-2,6-dione``.
373+
separator = "-" if core_name[:1].isdigit() else ""
374+
hydro = format_multiplier("hydro", len(additive_hydrogens))
375+
core_name = f"{','.join(additive_hydrogens)}-{hydro}{separator}{core_name}"
369376
return core_name
370377

371378

src/openclatura/assembly_parts.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ class RetainedParentMetadata:
116116
default_indicated_h: tuple[str, ...] = ()
117117
fusion_locants: tuple[str, ...] = ()
118118
derivative_stem: str | None = None
119+
# How many indicated hydrogens the mancude parent hydride itself supports.
120+
# Saturated positions beyond this many are *added* hydrogen and are cited as
121+
# a hydro prefix: xanthine is 3,7-dihydro-1H-purine-2,6-dione, never
122+
# 1H,3H,7H-purine-2,6-dione.
123+
indicated_hydrogen_count: int = 0
119124

120125

121126
@dataclass

src/openclatura/retained_fused_production.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,28 @@ def production_retained_fused_parent(
9292
default_indicated_h=template.default_indicated_h,
9393
fusion_locants=template.fusion_atoms,
9494
derivative_stem=template.derivative_stem,
95+
indicated_hydrogen_count=_indicated_hydrogen_count(template),
9596
),
9697
)
9798

9899

100+
def _indicated_hydrogen_count(template) -> int:
101+
"""How many indicated hydrogens this mancude parent hydride supports.
102+
103+
A parent that declares its own positions (1H-indole, 9H-xanthene) supports
104+
exactly that many. Otherwise the mancude double-bond count leaves the
105+
skeletal atoms it cannot pair: purine's nine atoms and four double bonds
106+
leave one, which is why purine is cited as 7H- or 9H-. A parent with
107+
neither is fully mancude and supports none.
108+
"""
109+
110+
if template.default_indicated_h:
111+
return len(template.default_indicated_h)
112+
if template.mancude_double_bonds:
113+
return len(template.atoms) - 2 * template.mancude_double_bonds
114+
return 0
115+
116+
99117
def _neutral_component(mol: Molecule, atoms: set[int]) -> bool:
100118
return all(mol.atoms[atom].charge == 0 for atom in atoms)
101119

src/openclatura/tests/test_analysis.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1318,7 +1318,21 @@ def test_repeated_grouped_locants_use_following_structural_scope():
13181318
def test_caffeine_uses_the_retained_purine_dione_parent():
13191319
analysis = analyze_smiles("CN1C=NC2=C1C(=O)N(C(=O)N2C)C")
13201320

1321-
assert analysis.name == "1,3,7-trimethylpurine-2,6-dione"
1321+
# Purine supports one indicated hydrogen; the 2,6-dione saturates two more
1322+
# positions, which are added hydrogen and take a hydro prefix. Those
1323+
# positions count even though all three nitrogens here carry a methyl
1324+
# rather than a hydrogen.
1325+
assert analysis.name == "1,3,7-trimethyl-3,7-dihydro-1H-purine-2,6-dione"
1326+
1327+
1328+
def test_purine_diones_cite_added_hydrogen_as_a_hydro_prefix():
1329+
assert name_smiles("O=c1[nH]c(=O)c2[nH]cnc2[nH]1") == "3,7-dihydro-1H-purine-2,6-dione"
1330+
assert name_smiles("Cn1c(=O)c2[nH]cnc2n(C)c1=O") == "1,3-dimethyl-3,7-dihydro-1H-purine-2,6-dione"
1331+
# An odd surplus cannot be spelt with a hydro prefix, so it stays indicated.
1332+
assert name_smiles("O=c1[nH]cnc2[nH]cnc12") == "1H,9H-purin-6-one"
1333+
# A parent needing no added hydrogen is unaffected.
1334+
assert name_smiles("c1ncc2[nH]cnc2n1") == "7H-purine"
1335+
assert name_smiles("Nc1ncnc2[nH]cnc12") == "9H-purin-6-amine"
13221336

13231337

13241338
def test_purinone_fusion_carbon_hydrogen_uses_a_dihydro_prefix():

0 commit comments

Comments
 (0)