orthonym.rules.ring_substituents#

Note

Internal API. Names and behaviour may change between releases.

Ring-as-substituent naming according to IUPAC 2013 (Blue Book).

Implements IUPAC: Standard substituent names for rings when they become substituents on a chain parent structure.

Examples: - benzene -> phenyl (4-phenylbutanoic acid) - cyclohexane -> cyclohexyl (4-cyclohexylbutanoic acid) - naphthalene -> naphthyl (position-specific: 1-naphthyl, 2-naphthyl) - pyridine -> pyridyl (position-specific: 2-pyridyl, 3-pyridyl, 4-pyridyl)

orthonym.rules.ring_substituents.identify_ring_system(mol, ring_atoms)#

Identify ring system name from molecular structure.

Examines the ring atoms to determine what type of ring system it is based on size, aromaticity, and heteroatom composition.

Parameters:
  • mol – RDKit Mol object

  • ring_atoms (Tuple[int, ...]) – Tuple of atom indices in the ring

Returns:

Ring system name string (e.g., ‘benzene’, ‘cyclohexane’, ‘pyridine’), or None if the ring cannot be identified

Return type:

str | None

orthonym.rules.ring_substituents.pin_heteroaryl_substituent_name(mol, ring_atoms, attachment_atom)#

PIN substituent name for a monocyclic heteroaromatic ring substituent.

Computes free-valence-aware numbering per IUPAC 2013: when a ring is detached as a substituent it is renumbered so low locants go, in order of decreasing priority, to (1) the heteroatoms as a set, (2) the heteroatoms in element-seniority order, (3) the indicated hydrogen, and (4) the free valence (point of attachment). The result is {nH-}{stem}-{locant}-yl.

Example: c1cnc[nH]1 attached at the carbon next to the NH numbers as 1H-imidazol-5-yl — the indicated H at locant 1 outranks the lower free-valence locant that the alternative 3H-imidazol-4-yl numbering would give.

Deliberately guarded: returns None (so the caller keeps its existing locant-less form, guaranteeing zero regression) whenever the PIN locant is not provably correct —

  • the ring is not one of the supported monocyclic heteroarenes;

  • it is a pyrazole (which identify_ring_system reports as ‘imidazole’);

  • the ring carries any substituent other than the single attachment;

  • the ring is not a simple aromatic monocycle;

  • attachment_atom is not a ring atom.

Parameters:
  • mol – RDKit Mol of the whole molecule.

  • ring_atoms (Tuple[int, ...]) – Atom indices forming the candidate ring.

  • attachment_atom (int) – Ring atom index bearing the free valence (bonded to the parent structure).

Returns:

The PIN substituent name, or None when not provably PIN-correct.

Return type:

str | None

orthonym.rules.ring_substituents.polycyclic_core_numbering(mol, ring_atoms, attachment_atom, deco_carriers, allow_mancude=False)#

: return (pos_map, bare_tail) for a POLYCYCLIC ring core so the recursive substituent composer can place decoration locants on a fused / bridged / cage core (the biggest drug-like breadth lever).

pos_map is {orig_atom_idx: int_locant} and bare_tail the ...-<fv>-yl core token — BOTH derived from ONE numbering, so the free valence and every decoration locant are mutually consistent. Any single VALID numbering is -safe: a wrong-locant name is suppressed by the production OPSIN round-trip gate, never shipped, so the composer ATTEMPTS a covered candidate rather than fail-closing on numbering uncertainty.

Covered classes: retained fused CARBOCYCLIC aromatics (naphthalene, anthracene, phenanthrene, pyrene…), von-Baeyer CAGES (adamantane / tricyclo+…) and — — retained fused HETEROCYCLES (indole, quinoline, benzimidazole, benzothiophene, purine…) via the data.fused_heterocycles catalog numbering. Returns None (clean abstain) for every other polycyclic core class — spiro, partial-hydro fused, non-cataloged fusions — which stay deferred (the composer then fails closed, never a wrong locant).

Precondition (M3, Composer #1 final review): complete/best-effort tier only. The sole production caller (the recursive decoration composer in substituent_enumerator.py) is itself gated on allow_mancude and never reaches this function with allow_mancude=False; a hypothetical future caller passing allow_mancude=False gets a clean None from every branch below rather than a PAH numbering it did not ask for.

orthonym.rules.ring_substituents.name_ring_system_substituent(mol, frag_atoms, attach_idx, allow_enumerator_fallback=True, allow_mancude=False, pos_out=None)#

Name a RING-CONTAINING substituent fragment (task 9 chokepoint).

The single delegate used by every ring-parent path (fused-heterocycle parents, PAH parents) when a substituent fragment contains ring atoms. Chooses the producer:

  • fragment IS exactly one ring system rooted at a ring atom -> get_ring_substituent_name (PIN free-valence locant,: naphthalen-2-yl, pyridin-2-yl, 1H-indol-2-yl,…);

  • anything else (ring + chain linker, chain-rooted) -> the universal substituent_enumerator.name_substituent.

Returns None when no trustworthy name can be produced — callers must treat None as “do not emit”, never fabricate a carbon-count alkyl name for a ring fragment (the historical phenyl->’hexyl’ corruption).

pos_out: optional caller-owned dict, filled with the CARRIER chain numbering when (and only when) the ring-on-chain producer emitted a name that cites carrier locants — see _compound_ring_on_chain_substituent for why the stereo emitter needs it. Every other producer here numbers a RING, not a chain, and leaves the map empty; an empty map is exactly the pre-existing behaviour, so no caller is affected by passing one.

orthonym.rules.ring_substituents.get_ring_substituent_name(mol, ring_atoms, attachment_point=None, allow_mancude=False)#

Get the substituent name for a ring when it becomes a substituent on a chain.

This is the main entry point for ring-as-substituent naming.

Parameters:
  • mol – RDKit Mol object

  • ring_atoms (Tuple[int, ...]) – Tuple of atom indices in the ring

  • attachment_point (int | None) – Optional ring atom index where the ring attaches to chain. Used for position-specific names (e.g., 2-pyridyl vs 4-pyridyl).

  • allow_mancude (bool) – opt-in (complete/best-effort engine tier only). When True, an unretained multi-ring cage the narrow PIN namers decline (tricyclo+/adamantane, and mancude fused-aromatic systems) is named via the universal von-Baeyer cage engine as a ...-<loc>-yl polyene. Default False keeps every existing caller byte-identical.

Returns:

Substituent name string (e.g., ‘phenyl’, ‘cyclohexyl’, ‘2-pyridyl’), or None when the ring system cannot be named by a provable rule (a phase SUBST-01: fail-closed — never a monocycle size-guess for a polycyclic).

Return type:

str | None

orthonym.rules.ring_substituents.get_ring_attachment_locant(mol, ring_atoms, chain_atoms, atom_to_locant)#

Find which chain position the ring is attached to.

The ring connects to the chain via a bond between a ring atom and a chain atom. This function finds that chain atom and returns its locant.

Parameters:
  • mol – RDKit Mol object

  • ring_atoms (Tuple[int, ...]) – Tuple of atom indices in the ring

  • chain_atoms (List[int]) – List of atom indices in the principal chain

  • atom_to_locant (Dict[int, int]) – Mapping from chain atom index to locant (1-indexed)

Returns:

Locant (1-indexed position) where the ring attaches to the chain

Raises:

ValueError – If no connection found between ring and chain

Return type:

int

orthonym.rules.ring_substituents.get_ring_attachment_atom(mol, ring_atoms, chain_atoms)#

Find the ring atom that attaches to the chain.

Parameters:
  • mol – RDKit Mol object

  • ring_atoms (Tuple[int, ...]) – Tuple of atom indices in the ring

  • chain_atoms (List[int]) – List of atom indices in the principal chain

Returns:

Ring atom index that connects to chain, or None if not found

Return type:

int | None

orthonym.rules.ring_substituents.decorated_ring_substituent_name(mol, ring_atoms, attachment_atom, expected_atoms=None)#

PIN substituent name for a MONOCYCLIC ring substituent carrying its own substituent prefixes: 2-nitrothiophen-3-yl, 2-oxocyclohexyl, 3-chloro-2-methylphenyl.

When expected_atoms is given, the name is returned only if the ring plus the detected decoration atoms account for EXACTLY that set — callers naming a specific fragment use this so the emitted name can never cover more or less than the fragment.

This is the.2 demoted-ring emitter: when parent selection demotes a decorated ring to a substituent, its decoration must be carried (the Phase-171 / rt75_0582 class of regression: a parent flip that silently drops the demoted ring’s groups).

Numbering per (the Blue Book), applied in order:
  1. ring heteroatoms low (set, then element seniority) — criterion (a);

  2. free valence (attachment) low — criterion (c), which OUTRANKS the detachable prefixes (cf. ‘6-carboxynaphthalen-2-yl’,:3262);

  3. detachable-prefix locant set low (first point of difference) — (f);

  4. first-cited (alphabetically) prefix low — (g).

Deliberately GUARDED — returns None (caller keeps its legacy form, so the change is zero-regression by construction) when:

  • the ring is not a simple monocycle (fused/spiro/bridged atoms);

  • the ring is an N-H azole or otherwise needs indicated hydrogen;

  • the ring has mixed saturation (would need ene-locants in the stem);

  • any ring atom carries an exocyclic group outside the supported table;

  • the ring carries no supported decoration at all (bare rings keep their existing forms — this function only DECORATES);

  • the stem is not confidently known.

orthonym.rules.ring_substituents.anilino_preferred_prefix(ring_prefix, n_substituent=None, *, enclose=True)#

The Blue Book PREFERRED PREFIX for a (fully substitutable) C6H5-NH- group.

Governing rule, verbatim, under the heading chain ## AMINES / ### Primary amines / ### Retained names / **** (the Blue Book):

“Aniline, for C6H5-NH2, is the only name for a primary amine retained as a

preferred IUPAC name for which full substitution is permitted on the ring and the nitrogen atom…. The prefix name ‘anilino’ is retained as the preferred prefix for C6H5-NH- with full substitution allowed. The name ‘phenylamino’ may be used in general nomenclature.”

Corroborated by (heading the Blue Book, “The substituent prefix name ‘anilino’ is a preferred IUPAC prefix and substitution is allowed”), the retained-prefix tables (the Blue Book / the Blue Book / the Blue Book) and the worked explanation (the Blue Book, “‘anilino’ is chosen as retained prefix preferred to ‘phenylamino’”).

The Blue Book’s own two-column pairs — PREFERRED PREFIX | general nomenclature:

the Blue Book anilino | phenylamino the Blue Book 4-chloroanilino | (4-chlorophenyl)amino the Blue Book 4-methylanilino | (4-methylphenyl)amino (not p-toluidino)

so (<X>phenyl)amino -> <X>anilino, LOCANTS UNCHANGED. The locants coincide by construction: decorated_ring_substituent_name numbers a carbocyclic ring with the free valence at locant 1 (the else: attachment_locant = 1 branch above), and aniline’s C-1 is the N-bearing carbon — the same atom. That is why all three Blue Book pairs carry identical locants.

This is a HEAD-MORPHEME substitution on an already-CONSTRUCTED ring-substituent name, not a lookup, because the class is OPEN: the set of substituents a ring may carry is unbounded, so any finite table of anilino spellings would be wrong on its complement by construction.

Enclosure, from the Blue Book vs the Blue Book — a prefix carrying its own locant(s) takes enclosing marks, one carrying none does not:

the Blue Book 3-anilinobenzoic acid (PIN) | 3-(phenylamino)benzoic acid the Blue Book 3-(N-methylanilino)phenol (PIN) | 3-[methyl(phenyl)amino]phenol

★ SCOPE. This function only SPELLS a prefix. It must be called only where an anilino-family prefix is already the chosen construction; it must never be used to promote one over a senior name-selection criterion. Two Blue Book boundary rows pin that limit: the Blue Book — multiplicative nomenclature beats BOTH substitutive forms) and the Blue Book — maximum prefix count declines an anilino prefix outright, and marks the anilino-bearing alternative ‘not’).

Parameters:
  • ring_prefix (str | None) – the ring substituent name, 'phenyl' or '<X>phenyl' (e.g. '4-chlorophenyl', '2,3-dimethylphenyl') — normally the return value of decorated_ring_substituent_name. Anything else fails closed.

  • n_substituent (str | None) – the OTHER substituent on the nitrogen, as a substituent prefix name ('methyl', 'phenyl'). An anilino nitrogen carries at most one (parent + ring + this), so a single name, not a list.

  • enclose (bool) – apply the enclosing marks when the prefix carries its own locant(s). Callers that wrap the result themselves pass False.

Returns:

The preferred prefix, or None when the class boundary is not met — in which case the caller keeps its own construction, so routing a site through this helper can never silently drop an atom.

Return type:

str | None

orthonym.rules.ring_substituents.anilino_prefix_from_aniline_name(aniline_name, *, enclose=True)#

'<X>aniline' -> the preferred prefix '<X>anilino'.

Third entry point for anilino_preferred_prefix, for callers that hold an ASSEMBLED aniline parent name rather than a ring name plus branch names.

The head-morpheme substitution leaves the prefix sequence untouched, and that is a derived fact, not an assumption: the Blue Book prints the parent and prefix forms with IDENTICAL decoration —

the Blue Book 4-chloroaniline (PIN) the Blue Book 4-chloroanilino (preferred prefix) | (4-chlorophenyl)amino the Blue Book 4-methylaniline (PIN) the Blue Book 4-methylanilino (preferred prefix) | (4-methylphenyl)amino

— and orders detachable prefixes among THEMSELVES; the head morpheme does not participate. So whatever order is correct for the aniline parent is correct for the anilino prefix, and this function inherits the ordering that rules/benzene.py’s aniline joiner already computes (which merges N- and ring-locant prefixes into one alphanumerical sequence per. That is what lets this path serve the ring-AND-nitrogen-substituted case that anilino_preferred_prefix declines: here the merge is not invented, it is inherited from a producer that already ships the parent form.

Why it exists: assembly/substituent_naming.py’s parent_to_prefix reached the heterocyclic ‘-ine’ -> ‘-inyl’ rule with ‘4-methyl-N-methylaniline’ and produced ‘4-methyl-N-methylanilinyl’. Aniline is not a heterocycle and ‘anilinyl’ is not a Blue Book morpheme; the preferred prefix is the retained ‘anilino’ (the Blue Book).

Returns None (fail closed) for anything that is not an aniline-family name.

orthonym.rules.ring_substituents.anilino_prefix_from_n_branch(mol, n_idx, branch_atoms, *, enclose=True)#

anilino_preferred_prefix derived from the GRAPH rather than from a name.

Entry point for the emission sites that had no unsubstituted-ring check at all and returned the bare literal "anilino" as soon as a 6-membered isolated all-carbon aromatic ring was found INSIDE the substituent — assembly/composer.py (two sites) and assembly/substituent_enumerator.py. Requiring the ring atoms to be inside the branch says nothing about the ring’s own substituents, which are also inside the branch and were never examined, so every ring substituent was silently dropped and the emitted name described a DIFFERENT molecule.

The atom-drop is closed by construction here: the ring name is built by decorated_ring_substituent_name with expected_atoms set to the WHOLE branch minus the nitrogen, and that function returns None unless the ring plus its detected decoration accounts for EXACTLY that set.

Parameters:
  • mol – the molecule.

  • n_idx (int) – the amine nitrogen.

  • branch_atoms – the complete N-substituent branch (the nitrogen itself may be included or not — it is normalised in).

  • enclose (bool) – as for anilino_preferred_prefix.

Returns:

The preferred prefix, or None — fail closed — when the branch is not exactly a nitrogen plus one isolated benzene ring plus that ring’s own nameable decoration.

Return type:

str | None

orthonym.rules.ring_substituents.ring_atom_fg_prefixes(mol, ring_atom_idx, ring_atom_set, return_atoms=False)#

Characteristic-group prefixes carried by a single ring atom when its ring is demoted to a substituent.

When a ring is named as a substituent of a chain parent, the ring’s own characteristic groups must still appear as prefixes inside the enclosing marks (e.g. 4-(2-oxocyclohexyl)butanoic acid, not the FG-dropped 4-cyclohexylbutanoic acid). This is the shared chemistry primitive that every ring-as-substituent emitter consults so the rule is applied once and identically.

Scope (the previously dropped characteristic groups):
  • oxo — the ring atom is a carbonyl carbon: an exocyclic double bond to an oxygen that bears no H and is otherwise terminal.

  • cyano — the ring atom bears an exocyclic nitrile carbon (single bond to a C that is triple-bonded to a terminal N).

  • carboxy — the ring atom bears an exocyclic carboxylic-acid carbon (an exocyclic C with =O carrying no H AND -OH). The S2 parent chokepoint (commit 835faffa) now demotes the ring-acid parent, so this branch is reachable. Esters -C(=O)OR are EXCLUDED (the second O carries no H) and stay alkoxycarbonyl / out of scope.

Intentionally NOT handled here:
  • hydroxy / alkoxy / halogen / amino / alkyl — already emitted by the existing composer / benzene substituent branches; claiming them here would double-count.

  • ester -C(=O)O- carbonyls — excluded by the carboxy hydroxyl-O guard; they belong to the alkoxycarbonyl / ester pathway, not here.

Parameters:
  • mol – RDKit Mol.

  • ring_atom_idx (int) – the ring atom to inspect.

  • ring_atom_set (Set[int]) – atom indices of the whole ring system (to identify which neighbours are exocyclic).

Returns:

Sorted list of prefix strings (```` when the atom carries none). With return_atoms=True, returns (prefixes, claimed_atoms) where claimed_atoms is the set of exocyclic atom indices the emitted prefixes account for (Wave2 conservation accounting — callers use it to prove every branch atom is represented in the name).