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 thering_replacementmodule 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 invonbaeyer_universal.pyrequires the main-bridge selection to be fixed first); the best-effort tier takesBEST_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_ATOMSat the best-effort tier; the monocycle branch is additionally bounded bydata.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).
– ring multiple-bond locant citation.
/ – the
-ylfree valence and its lowest locant.
- class orthonym.rules.terminal_ring.TerminalRingName(name, numbering, basis)#
Bases:
objectOne audited terminal ring name.
namethe emitted string – the parent hydride whenfree_valenceis None, else the…-<loc>-ylsubstituent token.numberingatom idx -> ring locant, the SAME map the name was spelledfrom (never re-derived), so a consumer can place its own substituent locants consistently.
basiswhich 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 undernumbering. 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;
numberingis 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.
molMUST already be kekulized:render_ring_unsaturationreadsGetBondType, 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) whenfree_valence_locantis 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
Noneonly whenring_atomsis 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).Nonewhen 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_skeletonis 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_atomsmust be the skeletal atoms of a single connected ring system (the caller owns the partition).free_valence_atommakes it a-ylsubstituent token numbered per;Noneyields the parent hydride.Returns
Noneonly 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.