Skip to content

Commit ade7a0c

Browse files
authored
Merge pull request #38 from lamalab-org/unifying-branches
Unifying branches
2 parents 72d2f16 + 2400a0d commit ade7a0c

12 files changed

Lines changed: 372 additions & 247 deletions

src/openclatura/assembly_parts.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,17 @@ class NameTokenBinding:
2727
right_context: str = ""
2828

2929

30+
class RenderedSubstituentName(str):
31+
"""A rendered substituent carrying construction-time boundary metadata."""
32+
33+
outer_parentheses_optional: bool
34+
35+
def __new__(cls, value: str, *, outer_parentheses_optional: bool = False):
36+
rendered = super().__new__(cls, value)
37+
rendered.outer_parentheses_optional = outer_parentheses_optional
38+
return rendered
39+
40+
3041
@dataclass
3142
class SubstituentItem:
3243
name: str

src/openclatura/assembly_prefixes.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import re
44

55
from .assembly_charge import inferred_ionic_retained_parent, single_charged_replacement_locants
6-
from .assembly_parts import AssemblyParts, SubstituentItem
6+
from .assembly_parts import AssemblyParts, RenderedSubstituentName, SubstituentItem
77
from .assembly_utils import is_fully_enclosed, needs_hyphen, parse_locant
88
from .formatting import is_complex_prefix
99
from .nomenclature import RULES
@@ -156,13 +156,13 @@ def format_substituent_prefixes(parts: AssemblyParts, spiro_subs) -> str:
156156
mult = (multipliers.complex_(count) if is_complex else multipliers.basic(count)) if count > 1 else ""
157157
loc_str = substituent_locant_string(parts, locs, len(grouped), spiro_subs)
158158

159-
name_to_use = name
160-
if is_complex and not is_fully_enclosed(name):
159+
name_to_use = _omit_optional_outer_parentheses(parts, name, count, loc_str, len(grouped))
160+
if is_complex and not is_fully_enclosed(name_to_use):
161161
if count > 1 or loc_str:
162-
name_to_use = f"({name})"
163-
elif not loc_str and len(grouped) > 1 and not is_fully_enclosed(name):
162+
name_to_use = f"({name_to_use})"
163+
elif not loc_str and len(grouped) > 1 and not is_fully_enclosed(name_to_use):
164164
if name not in ["fluoro", "chloro", "bromo", "iodo"]:
165-
name_to_use = f"({name})"
165+
name_to_use = f"({name_to_use})"
166166
prefix_parts.append(f"{loc_str}-{mult}{name_to_use}" if loc_str else f"{mult}{name_to_use}")
167167

168168
prefix_str = prefix_parts[0]
@@ -171,6 +171,28 @@ def format_substituent_prefixes(parts: AssemblyParts, spiro_subs) -> str:
171171
return prefix_str
172172

173173

174+
def _omit_optional_outer_parentheses(
175+
parts: AssemblyParts,
176+
name: str,
177+
count: int,
178+
locant_text: str,
179+
grouped_count: int,
180+
) -> str:
181+
"""Unwrap a directly rendered fragment when its parent boundary is clear."""
182+
183+
if (
184+
parts.is_substituent
185+
or count != 1
186+
or locant_text
187+
or grouped_count != 1
188+
or not isinstance(name, RenderedSubstituentName)
189+
or not name.outer_parentheses_optional
190+
or not is_fully_enclosed(name)
191+
):
192+
return name
193+
return name[1:-1]
194+
195+
174196
def format_replacement_prefixes(parts: AssemblyParts) -> str:
175197
if not parts.a_prefixes:
176198
return ""

src/openclatura/component_modifiers.py

Lines changed: 4 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,25 @@
11
"""Data-driven component modifiers attached after parent numbering."""
22

3-
from typing import Literal, Protocol, overload
4-
53
from .assembly_parts import AssemblyParts, NameTokenBinding, SubstituentItem
64
from .formatting import strip_outer_parentheses
75
from .group_atom_roles import ester_or_peroxy_single_oxygen
86
from .locants import parse_locant
97
from .molecule import DecisionTrace, Molecule
8+
from .naming_protocols import RecursiveSubgraphNamer
109
from .nomenclature import RULES
1110
from .perception import PerceivedGroup
1211
from .subgraph_tools import subgraph_component
1312
from .substituent_tokens import graph_bound_substituent_tokens
1413
from .trace_helpers import add_substituent_trace, bond_ids_within, decision_trace_data
1514

1615

17-
class BranchNamer(Protocol):
18-
"""Recursive branch namer with simple and traced/tree return modes."""
19-
20-
@overload
21-
def __call__(
22-
self,
23-
mol: Molecule,
24-
start_idx: int,
25-
exclude_atoms: set[int],
26-
*,
27-
upstream_atom: int | None = None,
28-
return_trace: Literal[False] = False,
29-
return_tree: Literal[False] = False,
30-
decision_trace: DecisionTrace | None = None,
31-
) -> str: ...
32-
33-
@overload
34-
def __call__(
35-
self,
36-
mol: Molecule,
37-
start_idx: int,
38-
exclude_atoms: set[int],
39-
*,
40-
upstream_atom: int | None = None,
41-
return_trace: Literal[True],
42-
return_tree: Literal[False] = False,
43-
decision_trace: DecisionTrace | None = None,
44-
) -> tuple[str, list[dict]]: ...
45-
46-
@overload
47-
def __call__(
48-
self,
49-
mol: Molecule,
50-
start_idx: int,
51-
exclude_atoms: set[int],
52-
*,
53-
upstream_atom: int | None = None,
54-
return_trace: Literal[True],
55-
return_tree: Literal[True],
56-
decision_trace: DecisionTrace | None = None,
57-
) -> tuple[str, list[dict], dict | None]: ...
58-
59-
@overload
60-
def __call__(
61-
self,
62-
mol: Molecule,
63-
start_idx: int,
64-
exclude_atoms: set[int],
65-
*,
66-
upstream_atom: int | None = None,
67-
return_trace: Literal[False] = False,
68-
return_tree: Literal[True],
69-
decision_trace: DecisionTrace | None = None,
70-
) -> tuple[str, dict | None]: ...
71-
72-
7316
def add_component_front_modifiers(
7417
mol: Molecule,
7518
parts: AssemblyParts,
7619
perceived_groups: list[PerceivedGroup],
7720
principal_key: str | None,
7821
sub_exclude: set[int],
79-
branch_namer: BranchNamer,
22+
branch_namer: RecursiveSubgraphNamer,
8023
) -> None:
8124
"""Add ester/sulfonate front modifiers such as the alcohol component name."""
8225

@@ -121,7 +64,7 @@ def add_component_n_substituents(
12164
numbered_path: list[int],
12265
get_loc,
12366
sub_exclude: set[int],
124-
branch_namer: BranchNamer,
67+
branch_namer: RecursiveSubgraphNamer,
12568
) -> None:
12669
"""Add N-substituent prefixes and N/N' locants for principal groups."""
12770

@@ -240,7 +183,7 @@ def _nitrogen_substituent_name(
240183
nitrogen: int,
241184
substituent: int,
242185
sub_exclude: set[int],
243-
branch_namer: BranchNamer,
186+
branch_namer: RecursiveSubgraphNamer,
244187
decision_trace: DecisionTrace | None = None,
245188
) -> tuple[str, list, dict | None]:
246189
"""Render graph-bound N-substituents on principal nitrogen groups."""

src/openclatura/component_namer.py

Lines changed: 20 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Connected-component naming pipeline."""
22

33
from collections.abc import Callable
4-
from typing import Literal, Protocol, overload
54

65
from .assembly_parts import AssemblyParts, NameAtomBinding, SubstituentItem
76
from .chains import find_all_carbon_paths, find_ring_systems, get_cyclic_atoms
@@ -17,6 +16,7 @@
1716
from .name_bindings import binding_trace_data, refresh_name_atom_bindings
1817
from .naming_audit import UnnamedAtomError, assert_component_fully_named
1918
from .naming_context import ComponentNamingState, NamingIntent
19+
from .naming_protocols import RecursiveSubgraphNamer
2020
from .parent_pipeline import build_parent_assembly_plan, resolve_retained_parent
2121
from .parent_selection import select_principal_parent
2222
from .principal_groups import (
@@ -48,68 +48,12 @@
4848
assembly_substituent_tree,
4949
assembly_trace_segments,
5050
bond_ids_within,
51+
build_shortcut_tree_node,
5152
decision_trace_data,
5253
functional_group_trace_data,
5354
trace_decision,
5455
)
5556

56-
57-
class SubgraphNamer(Protocol):
58-
"""Recursive subgraph namer with simple and traced/tree return modes."""
59-
60-
@overload
61-
def __call__(
62-
self,
63-
mol: Molecule,
64-
start_idx: int,
65-
exclude_atoms: set[int],
66-
*,
67-
upstream_atom: int | None = None,
68-
return_trace: Literal[False] = False,
69-
return_tree: Literal[False] = False,
70-
decision_trace: DecisionTrace | None = None,
71-
) -> str: ...
72-
73-
@overload
74-
def __call__(
75-
self,
76-
mol: Molecule,
77-
start_idx: int,
78-
exclude_atoms: set[int],
79-
*,
80-
upstream_atom: int | None = None,
81-
return_trace: Literal[True],
82-
return_tree: Literal[False] = False,
83-
decision_trace: DecisionTrace | None = None,
84-
) -> tuple[str, list[dict]]: ...
85-
86-
@overload
87-
def __call__(
88-
self,
89-
mol: Molecule,
90-
start_idx: int,
91-
exclude_atoms: set[int],
92-
*,
93-
upstream_atom: int | None = None,
94-
return_trace: Literal[True],
95-
return_tree: Literal[True],
96-
decision_trace: DecisionTrace | None = None,
97-
) -> tuple[str, list[dict], dict | None]: ...
98-
99-
@overload
100-
def __call__(
101-
self,
102-
mol: Molecule,
103-
start_idx: int,
104-
exclude_atoms: set[int],
105-
*,
106-
upstream_atom: int | None = None,
107-
return_trace: Literal[False] = False,
108-
return_tree: Literal[True],
109-
decision_trace: DecisionTrace | None = None,
110-
) -> tuple[str, dict | None]: ...
111-
112-
11357
SpiroSubgraphNamer = Callable[[Molecule, int, set[int]], SpiroAssembly]
11458
ParentAssembler = Callable[..., str]
11559

@@ -133,7 +77,7 @@ def collect_component_branch_substituents(
13377
base_exclude: set[int],
13478
sub_exclude: set[int],
13579
*,
136-
name_subgraph: SubgraphNamer,
80+
name_subgraph: RecursiveSubgraphNamer,
13781
name_spiro_subgraph: SpiroSubgraphNamer,
13882
emit_metadata: bool = True,
13983
) -> None:
@@ -298,7 +242,7 @@ def name_component(
298242
return_trace: bool = False,
299243
return_tree: bool = False,
300244
decision_trace: DecisionTrace | None = None,
301-
name_subgraph: SubgraphNamer,
245+
name_subgraph: RecursiveSubgraphNamer,
302246
name_spiro_subgraph: SpiroSubgraphNamer,
303247
assemble_parent_name: ParentAssembler,
304248
token_debug: bool = False,
@@ -332,11 +276,11 @@ def name_component(
332276
},
333277
)
334278
if return_trace and return_tree:
335-
return name, [], _shortcut_tree(name, component_atoms, bindings, token_spans)
279+
return name, [], _component_shortcut_tree(name, component_atoms, bindings, token_spans)
336280
if return_trace:
337281
return name, []
338282
if return_tree:
339-
return name, _shortcut_tree(name, component_atoms, bindings, token_spans)
283+
return name, _component_shortcut_tree(name, component_atoms, bindings, token_spans)
340284
return name
341285

342286
def name_component_again(next_mol: Molecule, next_atoms: set[int], is_substituent: bool = False):
@@ -376,11 +320,11 @@ def name_component_again(next_mol: Molecule, next_atoms: set[int], is_substituen
376320
},
377321
)
378322
if return_trace and return_tree:
379-
return name, [], _shortcut_tree(name, component_atoms, bindings, token_spans)
323+
return name, [], _component_shortcut_tree(name, component_atoms, bindings, token_spans)
380324
if return_trace:
381325
return name, []
382326
if return_tree:
383-
return name, _shortcut_tree(name, component_atoms, bindings, token_spans)
327+
return name, _component_shortcut_tree(name, component_atoms, bindings, token_spans)
384328
return name
385329

386330
state = ComponentNamingState(component_atoms=set(component_atoms), is_substituent=is_substituent)
@@ -431,11 +375,11 @@ def name_component_again(next_mol: Molecule, next_atoms: set[int], is_substituen
431375
},
432376
)
433377
if return_trace and return_tree:
434-
return name, [], _shortcut_tree(name, state.component_atoms, bindings, token_spans)
378+
return name, [], _component_shortcut_tree(name, state.component_atoms, bindings, token_spans)
435379
if return_trace:
436380
return name, []
437381
if return_tree:
438-
return name, _shortcut_tree(name, state.component_atoms, bindings, token_spans)
382+
return name, _component_shortcut_tree(name, state.component_atoms, bindings, token_spans)
439383
return name
440384

441385
state.exclude_atoms = set(mol.atoms.keys()) - state.component_atoms
@@ -654,21 +598,15 @@ def name_component_again(next_mol: Molecule, next_atoms: set[int], is_substituen
654598
return name
655599

656600

657-
def _shortcut_tree(name: str, component_atoms: set[int], bindings: list[dict], token_spans: list[dict]) -> dict:
601+
def _component_shortcut_tree(
602+
name: str, component_atoms: set[int], bindings: list[dict], token_spans: list[dict]
603+
) -> dict:
658604
"""Return a minimal component tree for shortcut component names."""
659605

660-
return {
661-
"kind": "component",
662-
"name": name,
663-
"atoms": sorted(component_atoms),
664-
"bonds": [],
665-
"parent": None,
666-
"principal_group": None,
667-
"substituents": [],
668-
"replacement_prefixes": [],
669-
"unsaturations": [],
670-
"trace_segments": [],
671-
"nested_decisions": [],
672-
"name_atom_bindings": bindings,
673-
"name_token_spans": token_spans,
674-
}
606+
return build_shortcut_tree_node(
607+
kind="component",
608+
name=name,
609+
atom_ids=component_atoms,
610+
name_atom_bindings=bindings,
611+
name_token_spans=token_spans,
612+
)

0 commit comments

Comments
 (0)