Skip to content

Commit e80e682

Browse files
AdrianM0claude
andcommitted
Keep both oxygens and the acyl carbon of a peroxy ester
A peroxy ester rendered as a prefix lost an oxygen and gained a carbon. `-C(=O)-O-O-tBu` came out as `tert-butoxycarbonyl`, which is `-C(=O)-O-tBu`, and the acyl carbon stayed in the parent chain on top of that, so `COC(=O)CCC(=O)OOC(C)(C)C` was named `methyl 4-(tert-butoxycarbonyl)butanoate` -- ten carbons and four oxygens for a molecule with nine and five. Two causes. `ester_prefix_handler` is shared with peroxy esters and spells a single `…oxycarbonyl`; peroxy esters now get their own `…peroxycarbonyl`. And `chain_external_carbonyl_groups` listed `ester` but not `peroxy_ester`, so the acyl carbon was never moved out of the parent the way a plain ester's is -- which is why the same molecule came out as a butanoate where the plain ester gives a propanoate. Of the three names this changes across 200k molecules, two go from wrong to correct and none get worse; every probe round-trips through OPSIN. One benzoyl peroxy ester still fails to name, but it failed before this change too. Refutations 24 -> 23. Both benchmarks otherwise unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 0379bd1 commit e80e682

3 files changed

Lines changed: 48 additions & 1 deletion

File tree

src/openclatura/data/namer_rules.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,8 @@
492492
"ring_carboxylic_acid",
493493
"ester",
494494
"ring_carboxylate",
495+
"peroxy_ester",
496+
"ring_peroxy_ester",
495497
"amide",
496498
"ring_amide",
497499
"thioamide",

src/openclatura/functional_prefixes.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@
66
from dataclasses import dataclass
77

88
from .assembly_parts import NameTokenBinding, SubstituentItem, rendered_substituent_text
9-
from .formatting import format_counted_prefixes, is_complex_prefix, oxy_prefix_from_branch, strip_outer_parentheses
9+
from .formatting import (
10+
format_counted_prefixes,
11+
format_multiplier,
12+
is_complex_prefix,
13+
oxy_prefix_from_branch,
14+
strip_outer_parentheses,
15+
)
1016
from .group_atom_roles import amide_nitrogen, ester_or_peroxy_single_oxygen
1117
from .molecule import Molecule
1218
from .naming_protocols import RecursiveSubgraphNamer
@@ -70,7 +76,35 @@ def amide_prefix_from_group(
7076
return f"({format_counted_prefixes(sub_names)}{base})"
7177

7278

79+
def peroxy_ester_prefix_from_group(
80+
mol: Molecule,
81+
group: PerceivedGroup,
82+
sub_exclude: set[int],
83+
branch_namer: RecursiveSubgraphNamer,
84+
) -> str:
85+
"""Return a ``…peroxycarbonyl`` prefix for a peroxy ester.
86+
87+
``oxycarbonyl`` spells one single-bonded oxygen, and a peroxy ester has
88+
two, so ``-C(=O)-O-O-tBu`` came out as ``tert-butoxycarbonyl`` -- an oxygen
89+
short, and a carbon long once the parent absorbed the acyl carbon anyway.
90+
"""
91+
92+
single_o = ester_or_peroxy_single_oxygen(mol, group)
93+
if single_o is None:
94+
return ""
95+
r_group_c = next((n for n in mol.get_neighbors(single_o) if n not in group.atoms_involved), None)
96+
if r_group_c is None:
97+
return ""
98+
branch_name = branch_namer(mol, r_group_c, sub_exclude | {single_o}, upstream_atom=single_o)
99+
if not branch_name:
100+
return ""
101+
branch_name = strip_outer_parentheses(rendered_substituent_text(branch_name))
102+
return f"({format_multiplier(branch_name, 1)}peroxycarbonyl)"
103+
104+
73105
def ester_prefix_handler(context: PrefixContext, group: PerceivedGroup) -> str:
106+
if group.key in RULES.prefixes.peroxy_ester_groups:
107+
return peroxy_ester_prefix_from_group(context.mol, group, context.sub_exclude, context.branch_namer)
74108
return ester_prefix_from_group(context.mol, group, context.sub_exclude, "carbonyl", context.branch_namer)
75109

76110

src/openclatura/tests/test_analysis.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5677,3 +5677,14 @@ def test_azinic_acid_is_distinguished_from_a_nitro_group():
56775677
assert name_smiles("CC=[N+]([O-])O") == "(ethylidene)azinic acid"
56785678
assert name_smiles("c1ccccc1[N+](=O)[O-]") == "nitrobenzene"
56795679
assert name_smiles("C[N+](=O)[O-]") == "nitromethane"
5680+
5681+
5682+
def test_peroxy_ester_prefix_keeps_both_oxygens_and_its_carbon():
5683+
# "oxycarbonyl" spells one single-bonded oxygen and a peroxy ester has two,
5684+
# and the acyl carbon was left in the parent chain as well, so the prefix
5685+
# was an oxygen short and a carbon long.
5686+
assert name_smiles("OC(=O)CCC(=O)OOC") == "3-(methylperoxycarbonyl)propanoic acid"
5687+
assert name_smiles("COC(=O)CCC(=O)OOC(C)(C)C") == "methyl 3-((tert-butylperoxy)carbonyl)propanoate"
5688+
# The plain ester and the peroxy-ester suffix are both unaffected.
5689+
assert name_smiles("OC(=O)CCC(=O)OC") == "3-(methoxycarbonyl)propanoic acid"
5690+
assert name_smiles("CC(=O)OOC(C)(C)C") == "tert-butyl ethaneperoxoate"

0 commit comments

Comments
 (0)