orthonym.assembly.composer#

Note

Internal API. Names and behaviour may change between releases.

Name assembly - combining fragments into complete IUPAC names.

Assembly order: 1. Stereodescriptors (R/S, E/Z) at start in parentheses 2. Locanted prefixes (substituents, alphabetized) 3. Parent name (with unsaturation modifiers) 4. Locanted suffix (principal group)

class orthonym.assembly.composer.ComplexRingResult(name, ring_atoms, atom_to_locant, substituents_included)#

Bases: tuple

atom_to_locant#

Alias for field number 2

name#

Alias for field number 0

ring_atoms#

Alias for field number 1

substituents_included#

Alias for field number 3

class orthonym.assembly.composer.HandlerResult(name, handler_id, parent_atoms=<factory>, accounted_atoms=<factory>, total_heavy_atoms=0)#

Bases: object

Observational coverage metric for naming handlers (a phase).

Tracks what fraction of the molecule’s heavy atoms are accounted for in the generated name. Low coverage indicates potential substituent drops. This is OBSERVATIONAL ONLY – does not affect naming behavior.

name: str#
handler_id: str#
parent_atoms: Set[int]#
accounted_atoms: Set[int]#
total_heavy_atoms: int = 0#
property coverage: float#

Return fraction of heavy atoms accounted for (0.0 to 1.0).

orthonym.assembly.composer.get_saturation_prefix_for_fused_ring(mol, aromatic_parent_name=None, atom_to_locant=None)#

Generate saturation prefix for a fused ring system.

This is a helper function for the composer that handles the complete workflow of detecting partial saturation and formatting the prefix.

IUPAC 2013 ordering for partially saturated fused heterocycles: [substituents]-[saturation prefix]-[indicated H]-[parent] Example: 5-methyl-2,3-dihydro-1H-indole

Parameters:
  • mol – RDKit Mol object

  • aromatic_parent_name (str | None) – Name of the aromatic parent (e.g., ‘quinoline’) If not provided, attempts to auto-detect from molecular structure.

  • atom_to_locant (Dict[int, Any] | None) – Optional mapping from atom index to IUPAC locant. If provided, generates locants in the prefix.

Returns:

Formatted saturation prefix string (e.g., ‘2,3-dihydro’, ‘1,2,3,4-tetrahydro’), or None if no saturation detected or parent not found.

Return type:

str | None

Examples

>>> mol = Chem.MolFromSmiles('c1ccc2c(c1)CCCN2') # tetrahydroquinoline
>>> get_saturation_prefix_for_fused_ring(mol, 'quinoline')
'1,2,3,4-tetrahydro' # if atom_to_locant provided
orthonym.assembly.composer.assemble_fused_ring_with_saturation(core_name, saturation_prefix, substituent_prefixes=None, indicated_h=None)#

Assemble a fused ring name with saturation prefix in correct IUPAC order.

IUPAC 2013 ordering rule for partially saturated fused heterocycles: [substituents]-[saturation prefix]-[indicated H]-[parent]

Parameters:
  • core_name (str) – Parent name (e.g., ‘indole’, ‘quinoline’)

  • saturation_prefix (str | None) – Saturation prefix (e.g., ‘2,3-dihydro’, ‘tetrahydro’)

  • substituent_prefixes (str | None) – Optional substituent prefixes (e.g., ‘5-methyl’)

  • indicated_h (str | None) – Optional indicated hydrogen (e.g., ‘1H’)

Returns:

Assembled IUPAC name

Return type:

str

Examples

>>> assemble_fused_ring_with_saturation('indole', '2,3-dihydro', None, '1H')
'2,3-dihydro-1H-indole'
>>> assemble_fused_ring_with_saturation('indole', '2,3-dihydro', '5-methyl', '1H')
'5-methyl-2,3-dihydro-1H-indole'
class orthonym.assembly.composer.NameFragment(text, locants=(), priority=0, fragment_type='prefix', count=1, text_without_locants=None, atoms=None)#

Bases: object

A fragment of an IUPAC name.

text: str#
locants: tuple = ()#
priority: int = 0#
fragment_type: str = 'prefix'#
count: int = 1#
text_without_locants: str | None = None#

The SAME prefix rendered from its parts with no locants cited.

Most prefix producers hand back text WITHOUT locants and carry the locants as data in locants, so a licence can withhold them at print time. _generate_alkyl_prefixes cannot: it renders through format_substituent_prefix, which BAKES 2- into the string. That is why the licence could not reach 2-methylpropanedioic acid (measured: it declined at the “text begins with a digit” guard, while the structurally identical chloro prefix reached the licence and fired).

A producer that bakes locants into text therefore also supplies the locant-free spelling HERE, produced by the same renderer from name and count – never by editing the formatted string, which would be the string band-aid internal notes forbids and would be wrong on the complement (2-methylpentanedioic acid legitimately keeps its locant). None means “text cites no locants of its own”.

atoms: frozenset | None = None#

the heavy-atom indices this fragment ACCOUNTS FOR in the molecule (the parent’s skeletal atoms, a substituent’s frag_atoms, the suffix’s characteristic-group atoms). None = “this producer did not report its atoms” — the handler’s atom-coverage close then SKIPS the check for the whole name (breadth-safe: an un-instrumented producer is never false-voided). A stereo fragment carries no atoms and is exempt. When EVERY parent/suffix/prefix fragment reports atoms, their union is the name-side accounting the general-acyclic handler verifies against the whole molecule, so a silently-dropped substituent (e.g. the carbon-free sulfate ester of COS(=O)(=O)O, dropped as substituent_is_bare_functional_group → methane) is caught at construction instead of shipping a wrong molecule when the OPSIN jar is absent.

Type:

task-W2 (Witness B)

orthonym.assembly.composer.assemble_name(features, style='pin', _composing_ion=False)#

Assemble complete IUPAC name from molecular features.

PUBLIC API + RECURSION-SAFE WRAPPER (a phase drift fix, 2026-04-23).

Each invocation gets its own CandidatePool scope per IUPAC “selection of a preferred parent structure is based on the seniority of classes” — applied per-molecule, single-pass. The wrapper pushes a fresh pool on the per-thread stack before delegating to the body, and pops it in a finally clause so every exit path (normal return, early return, exception) restores the previous pool for the calling frame.

This fixes the byte-identical drift discovered in Plan 04 where recursive name_compound calls (N-oxide handler, fragment naming, substituent enumeration, decomposition fallback) shared a single thread-local pool with the outer molecule, causing inner candidates to pollute pool[0] and beat the outer molecule’s correct candidate under selection_mode=’first_applicable’. See: internal notes.

Parameters:
  • features (Any) – MolecularFeatures object with extracted features

  • style (str) – Naming style (“pin”, “general”, “cas”)

  • _composing_ion (bool) – Internal recursion guard. When True, ion aspect composition is skipped to prevent infinite loops. Do not set manually – it is used by _try_ion_aspect_composition.

Returns:

Complete IUPAC name string

Return type:

str

orthonym.assembly.composer.ring_anchored_pg_atoms(mol, pg_matches, ring_atom_set)#

Atoms of principal-group matches ANCHORED to the parent ring.

A match is ring-anchored when it contains a ring atom (inline suffixes: ring C=O ketone, ring C-OH alcohol) or when one of its carbons is bonded directly to a ring atom (appended suffixes: -carbaldehyde, -carboxylic acid, -carbonitrile, whose match atoms are all exocyclic).

co-delivery (.1 S4): only ring-anchored matches are

expressed as the ring suffix. A PG match wholly inside a demoted chain substituent (e.g., the terminal CHO of a 7-oxoheptyl chain) must stay with the substituent and be named there (oxo/cyano/… prefix).

orthonym.assembly.composer.format_locants(locants)#

Format a tuple of locants for name insertion.

Example: (2, 3) -> “2,3-”

orthonym.assembly.composer.assemble_ion_name(features, mol, style='pin')#

Assemble name for ionic or radical species.

Routes to appropriate naming function based on species_type. This function is the entry point for the composer to handle non-neutral molecules.

Parameters:
  • features (Any) – MolecularFeatures with species_type populated

  • mol – RDKit Mol object

  • style (str) – ‘pin’ for preferred names

Returns:

IUPAC name for the ion/radical, or empty string on failure

Return type:

str

Example

>>> # For a salt:
>>> assemble_ion_name(features, mol)
'sodium acetate'
>>> # For a radical:
>>> assemble_ion_name(features, mol)
'methyl'
orthonym.assembly.composer.get_multiplier(count, is_complex=False)#

Get the appropriate multiplier prefix for a count.

Parameters:
  • count (int) – Number of occurrences

  • is_complex (bool) – True if the substituent name is complex (has locants/hyphens)

Returns:

Multiplier string (e.g., “di”, “tris”)

Return type:

str