Skip to content

Commit 0e80ff4

Browse files
authored
Revert "Fix/issue 52 large backbones"
1 parent 74407f0 commit 0e80ff4

6 files changed

Lines changed: 16 additions & 209 deletions

File tree

README.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,6 @@
77
Built on top of RDKit, the package walks the molecular graph, detects functional groups and ring systems, selects the principal parent, assigns locants, and constructs the corresponding substitutive IUPAC name. Every step is recorded in an inspectable
88
decision trace so the *why* of a name is recoverable, not just the *what*.
99

10-
Systematic backbone stems are supported for chains and rings containing from 1
11-
through 1000 skeletal atoms, following IUPAC Blue Book P-14.2.1.
12-
1310
> **Status:** beta. The naming engine handles a broad slice of organic
1411
> structures (alkanes/alkenes/alkynes, common functional groups, simple
1512
> heterocycles, fused/spiro/bridged systems, retained names from the Blue
@@ -286,4 +283,4 @@ If you are using OPSIN for verification, please cite the original OPSIN publicat
286283
doi = {10.1021/ci100384d},
287284
url = {https://doi.org/10.1021/ci100384d}
288285
}
289-
```
286+
```

src/openclatura/chains.py

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -462,24 +462,19 @@ def find_all_carbon_paths(mol: Molecule, exclude_atoms: set[int] = None) -> list
462462

463463
all_paths = []
464464

465-
def collect_paths(start: int) -> None:
466-
# Long unbranched backbones can exceed Python's recursion limit. Keep
467-
# the existing depth-first order without using the interpreter stack.
468-
stack = [(start, [start], {start})]
469-
while stack:
470-
current, path, visited = stack.pop()
471-
neighbors = [n for n in mol.get_neighbors(current) if n in valid_nodes and n not in visited]
472-
if not neighbors:
473-
all_paths.append(path)
474-
continue
475-
for neighbor in reversed(neighbors):
476-
stack.append((neighbor, path + [neighbor], visited | {neighbor}))
465+
def dfs(current: int, path: list[int], visited: set[int]):
466+
neighbors = [n for n in mol.get_neighbors(current) if n in valid_nodes and n not in visited]
467+
if not neighbors:
468+
all_paths.append(path)
469+
return
470+
for n in neighbors:
471+
dfs(n, path + [n], visited | {n})
477472

478473
endpoints = [n for n in valid_nodes if sum(1 for x in mol.get_neighbors(n) if x in valid_nodes) <= 1]
479474
start_nodes = endpoints if endpoints else valid_nodes
480475

481476
for start in start_nodes:
482-
collect_paths(start)
477+
dfs(start, [start], {start})
483478

484479
unique_paths = []
485480
seen = set()

src/openclatura/formatting.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,7 @@ def substituted_alkoxy_prefix(branch: str) -> str | None:
9898

9999
if "hydroxy" not in branch:
100100
return None
101-
for length in range(stems.MAX_STEM_LENGTH, stems.MIN_STEM_LENGTH - 1, -1):
102-
stem = stems.get(length)
101+
for stem in stems.STEMS.values():
103102
terminal = f"{stem.stem}yl"
104103
replacement = f"{stem.stem}oxy"
105104
if branch.endswith(terminal):

src/openclatura/ring_renderer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def von_baeyer_cycle_count(descriptor: str | None) -> int | None:
3636
for count, multiplier in multipliers.MULTIPLIERS.items():
3737
if prefix == multiplier.basic:
3838
return count
39-
for count in range(stems.MIN_STEM_LENGTH, stems.MAX_STEM_LENGTH + 1):
39+
for count, stem in stems.STEMS.items():
4040
if prefix == _basic_cycle_prefix(count):
4141
return count
4242
return None

src/openclatura/rules/stems.py

Lines changed: 5 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
"""
1010

1111
from dataclasses import dataclass
12-
from functools import lru_cache
1312

1413

1514
@dataclass(frozen=True)
@@ -21,8 +20,7 @@ class Stem:
2120

2221
# Stems 1-4 are retained (non-systematic) names.
2322
# Stems 5+ are derived from Greek/Latin numerical roots.
24-
# Retained and established spellings through 30. Larger stems are generated
25-
# from the basic numerical terms in Blue Book P-14.2.1.
23+
# Coverage up to 30; extend as needed (IUPAC defines stems well beyond this).
2624
STEMS: dict[int, Stem] = {
2725
1: Stem(1, "meth", retained=True),
2826
2: Stem(2, "eth", retained=True),
@@ -56,105 +54,12 @@ class Stem:
5654
30: Stem(30, "triacont", retained=False),
5755
}
5856

59-
MIN_STEM_LENGTH = 1
60-
MAX_STEM_LENGTH = 1000
61-
62-
_UNITS = {
63-
1: "hen",
64-
2: "do",
65-
3: "tri",
66-
4: "tetra",
67-
5: "penta",
68-
6: "hexa",
69-
7: "hepta",
70-
8: "octa",
71-
9: "nona",
72-
}
73-
74-
_TENS = {
75-
1: "deca",
76-
2: "icosa",
77-
3: "triaconta",
78-
4: "tetraconta",
79-
5: "pentaconta",
80-
6: "hexaconta",
81-
7: "heptaconta",
82-
8: "octaconta",
83-
9: "nonaconta",
84-
}
85-
86-
_HUNDREDS = {
87-
1: "hecta",
88-
2: "dicta",
89-
3: "tricta",
90-
4: "tetracta",
91-
5: "pentacta",
92-
6: "hexacta",
93-
7: "heptacta",
94-
8: "octacta",
95-
9: "nonacta",
96-
}
97-
98-
99-
def _validate_length(length: int) -> None:
100-
if isinstance(length, bool) or not isinstance(length, int):
101-
raise ValueError("Stem length must be an integer from 1 through 1000")
102-
if not MIN_STEM_LENGTH <= length <= MAX_STEM_LENGTH:
103-
raise ValueError("Stem length must be from 1 through 1000")
104-
105-
106-
def _under_one_hundred(value: int) -> str:
107-
"""Return the basic numerical term for a value from 1 through 99."""
108-
109-
if value == 11:
110-
return "undeca"
111-
112-
units = value % 10
113-
tens = value // 10
114-
unit_term = _UNITS.get(units, "")
115-
tens_term = _TENS.get(tens, "")
116-
117-
# The initial i of icosa is elided after a vowel (P-14.2.1.2), e.g.
118-
# do + icosa -> docosa, but hen + icosa -> henicosa.
119-
if unit_term and tens == 2 and unit_term[-1] in "aeiou":
120-
tens_term = tens_term[1:]
121-
return unit_term + tens_term
122-
123-
124-
def _numerical_term(length: int) -> str:
125-
"""Build the P-14.2.1 basic numerical term for ``length``."""
126-
127-
_validate_length(length)
128-
if length == 1000:
129-
return "kilia"
130-
131-
hundreds, remainder = divmod(length, 100)
132-
parts = []
133-
if remainder:
134-
parts.append(_under_one_hundred(remainder))
135-
if hundreds:
136-
parts.append(_HUNDREDS[hundreds])
137-
return "".join(parts)
138-
13957

14058
def get(length: int) -> Stem:
141-
"""Return the chain stem for a supported skeletal-atom count."""
142-
143-
_validate_length(length)
144-
return _get_cached(length)
145-
146-
147-
@lru_cache(maxsize=MAX_STEM_LENGTH)
148-
def _get_cached(length: int) -> Stem:
149-
"""Return a validated stem while caching generated values."""
150-
151-
if length in STEMS:
152-
return STEMS[length]
153-
numerical_term = _numerical_term(length)
154-
return Stem(length, numerical_term.removesuffix("a"), retained=False)
59+
"""Look up a stem by chain length. Raises KeyError if out of range."""
60+
return STEMS[length]
15561

15662

15763
def stem_for(length: int) -> str:
158-
"""Return just the stem string for a supported chain length."""
159-
160-
return get(length).stem
64+
"""Return just the stem string for a given chain length."""
65+
return STEMS[length].stem

src/openclatura/tests/test_stems.py

Lines changed: 0 additions & 89 deletions
This file was deleted.

0 commit comments

Comments
 (0)