orthonym.rules.terminal_ring#

Note

Internal API. Names and behaviour may change between releases.

-T1b: the TERMINAL ring namer – the audited systematic generator that stands where the 'substituent' refusal sentinel used to.

Why this module exists#

errors.py:224 calls the bare word substituent “a REFUSAL, not a name”, and it is what the ring-bearing branch of assembly/substituent_enumerator._descriptive_fallback returns when every narrow producer has declined. Our tables ARE the path, so a table miss has nowhere to fall. This module is the fall-through: a systematic name built from the ring graph itself, so a table miss degrades to an UGLIER name instead of a refusal.

What is NEW here and what is reused#

The polycyclic half already existed and is reused unchanged – vonbaeyer_universal.analyze_cage_universal / analyze_spiro_universal already kekulize, build the von Baeyer / spiro descriptor, apply the TOTAL skeletal-replacement prefix (with λ) and cite every ring multiple bond, and already gate every result on a reconstruction audit (audit_von_baeyer_descriptor / audit_spiro_descriptor). Measured 2026-08-03: they name rank-2..8 cages including hetero and mancude ones. What they cannot do – by definition, at vonbaeyer_universal.py:424 – is a MONOCYCLE, because von Baeyer nomenclature starts at two rings.

So the new code below is the MONOCYCLE branch: skeletal (‘a’) replacement nomenclature over a cyclo-alkane stem, with a locant for every non-carbon skeletal atom, λ for hypervalence, and an explicit locant for every ring multiple bond – plus its own reconstruction audit, built to the same contract as the von Baeyer one: parse the EMITTED STRING back into (ring size, {locant: element}, {bond-order edges}) and require SET EQUALITY with the molecular graph under the emitted numbering.

Scope (stated explicitly, per the task contract)#

  • Elements – carbon plus the 18 ring ‘a’-prefix rows this project admits (ring_replacement.HETEROATOM_PREFIXES: O S Se Te N P As Sb Bi Si Ge Sn Pb B Al Ga In Tl). The Blue Book’s replacement set is Table 1.5, which is a CLOSED list, so an off-table skeletal element (Zn/Cd/Hg/Fe/…) has no morpheme and MUST refuse – see the ring_replacement module docstring.

  • Cycle rank – 1 (this module’s monocycle branch) and 2..8 (delegated to the universal analyzers; 8 is vonbaeyer_universal.MAX_CAGE_RINGS, kept at the PIN tier: its own comment in vonbaeyer_universal.py requires the main-bridge selection to be fixed first); the best-effort tier takes BEST_EFFORT_MAX_CAGE_RINGS (vonbaeyer_universal.cage_caps).

  • Size – at most 40 skeletal atoms (MAX_CAGE_ATOMS) at the PIN tier, BEST_EFFORT_MAX_CAGE_ATOMS at the best-effort tier; the monocycle branch is additionally bounded by data.chain_names.get_chain_prefix. A name built on a ring system beyond the PIN ceilings is recorded as not a PIN.

  • Charge – a charged skeletal ring atom REFUSES. A ring cation/anion is

    (cation_words / ion_retained_names), not replacement

    nomenclature; [n+] in a ring is a different naming class and inventing a neutral ‘a’-prefix name for it would name a DIFFERENT species.

Within that scope, and for a name that passes the reconstruction audit, this module does not return None for a connected ring system. The audit is part of the contract, not an escape hatch: a name that fails it is refused, and the caller keeps its existing behaviour. Every refusal is logged at INFO with the reason so the refused count is measurable.

IUPAC references#

  • “Skeletal replacement (‘a’) nomenclature” for monocyclic rings.

    • Table 1.5 – the closed replacement-prefix set.

  • – λ placement (immediately after the locant, no hyphen).

    1. – ring multiple-bond locant citation.

  • / – the -yl free valence and its lowest locant.

class orthonym.rules.terminal_ring.TerminalRingName(name, numbering, basis)#

Bases: object

One audited terminal ring name.

name the emitted string – the parent hydride when

free_valence is None, else the …-<loc>-yl substituent token.

numbering atom idx -> ring locant, the SAME map the name was spelled

from (never re-derived), so a consumer can place its own substituent locants consistently.

basis which generator + audit produced it: 'monocycle',

'von_baeyer' or 'spiro'.

name: str#
numbering: Dict[int, int]#
basis: str#
orthonym.rules.terminal_ring.audit_monocycle_replacement_name(mol, ring_atoms, numbering, name, free_valence_atom=None)#

Reconstruction audit for a monocycle replacement name (fail-closed).

Same contract as vonbaeyer_universal.audit_von_baeyer_descriptor: parse the EMITTED STRING back into the skeleton it denotes and require SET EQUALITY with the molecule’s own ring skeleton under numbering. Checks, every one of which returns False:

  • the string is not in the emitted grammar;

  • the ring size the stem counts != the number of skeletal atoms;

  • numbering is not a bijection of the ring onto 1..N;

  • the ring bonds are not the cycle 1-2-…-N-1 the name asserts;

  • an element at a locant differs from the molecule’s atom there (including a heteroatom the name does not mention – the silent-drop shape);

  • a double/triple bond the name cites is not that bond order in the molecule, or a multiple bond in the molecule is not cited;

  • the free-valence locant does not land on the attachment atom.

orthonym.rules.terminal_ring.build_monocycle_replacement_name(mol, ring_atoms, numbering, free_valence_locant=None)#

The replacement name for a simple monocycle, or None.

mol MUST already be kekulized: render_ring_unsaturation reads GetBondType, and an aromatic bond is neither DOUBLE nor TRIPLE, so an un-kekulized mancude ring would silently lose every one of its double bonds.

Returns the parent hydride (1-thiacyclohexane) when free_valence_locant is None, else the substituent token (1-thiacyclohexan-4-yl).

orthonym.rules.terminal_ring.monocycle_numbering(mol, ring_atoms, free_valence_atom=None)#

Deterministic -style numbering of a simple monocycle.

Lowest-locant key, applied in order: heteroatoms as a SET, then heteroatoms in element-seniority order ranks, shared with the von Baeyer prefix builder so the two do not disagree), then the free valence , then the ring multiple bonds. Every starting atom and both directions are enumerated, so the answer is independent of RDKit atom order.

Returns None only when ring_atoms is not a single simple cycle.

orthonym.rules.terminal_ring.parse_monocycle_replacement_name(name)#

'1-thiacyclohexan-4-yl' -> (6, {1: 'S'}, frozenset, frozenset, 4): (ring size, {locant: element}, double-bond edges, triple-bond edges, free-valence locant or None). None when the string is not in the closed grammar this module emits.

Reads the string ONLY – no access to the molecule – so the audit that consumes it is a genuine independent reconstruction, exactly as reconstruct_von_baeyer_skeleton is for the cage sibling.

orthonym.rules.terminal_ring.terminal_ring_name(mol, ring_atoms, free_valence_atom=None)#

The audited terminal name for one connected ring system.

ring_atoms must be the skeletal atoms of a single connected ring system (the caller owns the partition). free_valence_atom makes it a -yl substituent token numbered per; None yields the parent hydride.

Returns None only when the ring system is out of the module’s stated scope (see the module docstring) or when the emitted name FAILS its reconstruction audit. Both are logged at INFO.