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:
objectObservational 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:
objectA 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
textWITHOUT locants and carry the locants as data inlocants, so a licence can withhold them at print time._generate_alkyl_prefixescannot: it renders throughformat_substituent_prefix, which BAKES2-into the string. That is why the licence could not reach2-methylpropanedioic acid(measured: it declined at the “text begins with a digit” guard, while the structurally identicalchloroprefix reached the licence and fired).A producer that bakes locants into
texttherefore also supplies the locant-free spelling HERE, produced by the same renderer fromnameandcount– 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 acidlegitimately keeps its locant).Nonemeans “textcites 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). Astereofragment 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