orthonym.rules.polycyclic#
Note
Internal API. Names and behaviour may change between releases.
Von Baeyer polycyclic descriptor generation (IUPAC 2013,.
Implements the von Baeyer algorithm (through) for generating correct polycyclic descriptors for any ring count (bicyclo through decacyclo).
This module replaces the broken descriptor generation in tricyclo.py and polycyclic_bridged.py, which incorrectly used SSSR for main ring finding.
Key algorithm: 1. Ring count via cycle rank formula (edges - vertices + 1) 2. Main ring via longest-path between bridgehead pairs (NOT SSSR) 3. Main bridge between main bridgeheads through non-main-ring atoms 4. Secondary bridges: independent before dependent, descending by length 5. numbering: main ring (longer path first) -> main bridge -> secondary bridges 6. Verification: sum(bridge_lengths) + 2 == total_ring_atoms
Reference: IUPAC 2013 Blue Book, through.
- orthonym.rules.polycyclic.fusion_nomenclature_applies(mol, ring_atoms)#
True when the ring system
ring_atomsholds an ortho-fused cluster with at least two rings of five or more members, i.e. when a fusion (or bridged fusion) name exists and a von Baeyer name of the system is therefore NOT its PIN.“Five-membered ring requirement” (the Blue Book,:23710): “Fusion
nomenclature gives preferred IUPAC names only to compounds having at least two rings of at least five or more members. […] When fusion names are not allowed, unsaturated von Baeyer ring system names are preferred IUPAC names.” (:19532) ranks (c) fused and (d) bridged fused ring systems above (e) nonfused bridged (von Baeyer) systems, and:23883 “the bridged fused ring name is preferred to the von Baeyer name”. BB rows: ‘decahydronaphthalene (PIN)’ over ‘bicyclo[4.4.0]decane’ (:24233); ‘hexadecahydro-1H-8,12-methanobenzo[13]annulene (PIN)’ over ‘tricyclo[12.3.1.0^5,10]octadecane’ (:23875-23879); ‘hexahydro-1H- 4,7-methanoindene (PIN)’ (:49311, the tricyclo[5.2.1.0^2,6] skeleton); ‘tetrahydro-4,8-ethanopyrano[4,3-c]pyran-…-tetrone (PIN) {not 4,9-dioxatricyclo [4.4.2.0^2,7]dodecane-…}’ (:32535). Boundary rows where the von Baeyer name IS the PIN (this returns False for each): ‘bicyclo[4.1.0]hepta-1,3,5-triene’, ‘bicyclo[4.2.0]octa-1,3,5,7-tetraene’ (:23718,:23725; one ring of five or more), ‘tetracyclo[3.2.0.0^2,7.0^4,6]heptane’ (:10824: its two five-membered rings meet a four-membered ring each by one bond but share three atoms with each other, so they are bridged, not fused), ‘cubane (PIN) pentacyclo[4.2.0.0^2,5.0^3,8.0^4,7] octane’ (:9889), ‘tetracyclo[2.2.0.0^2,6.0^3,5]hexane (PIN)’ (:9897), ‘3,6,8-trioxatricyclo[3.2.1.0^2,4]octane (PIN)’ (:48763).
A cluster is grown along rings that share exactly one bond (ortho-fusion); every ring in it must meet every other ring in at most one atom or exactly one bond, as in a fused ring system. Smallest rings come from RDKit’s symmetrized SSSR. A miss only leaves a label as it was; it never changes a name.
A cluster that holds every ring atom while the system has more rings than the cluster closes those rings by bonds only. (:14025): “An atom or group of atoms is named as a bridge”, so such a cluster gives no bridged fused name (a decalin with a cyclobutane closed across it, tricyclo[4.4.0.0^5,10]decane); the search goes on for a cluster that leaves a bridge atom or is the whole system.
- orthonym.rules.polycyclic.retained_von_baeyer_parent(mol, ring_atoms)#
‘adamantane’ / ‘cubane’ when the all-carbon ring system
ring_atomsis that retained parent hydride, else None. A ring heteroatom returns None.
- orthonym.rules.polycyclic.von_baeyer_ring_count(mol, cage_atoms)#
The number of rings counts over
cage_atoms, orNone.(
the Blue Book) DEFINES the quantity: “A ‘polycyclicsystem’ contains a number of rings equal to the minimum number of scissions required to convert the system into an acyclic skeleton.” Restated at (
:9645): “The number of rings is equal to the number of bond cuts necessary to transform the polycyclic system into an acyclic skeleton.” That is the graph’s circuit rank,E - V + Cover the induced cage subgraph (C= connected components), and it is exactly the numbercyclo_ring_count_wordspells – the two are the count and its word, so they live together here.Why this is NOT
GetRingInfo.NumRings(a phase T3b)#RDKit’s ring info is the symmetrized SSSR, which deliberately keeps extra symmetry-equivalent smallest rings, so its cardinality OVER-COUNTS the
number on exactly the symmetric cages von Baeyer nomenclature is
for:
adamantane – Blue Book
tricyclo[3.3.1.1^3,7]decane(PIN,:9840), 3 rings – symmetrized ring count 4;cubane –
pentacyclo[4.2.0.0^2,5.0^3,8.0^4,7]octane(PIN,:9889), 5 rings – symmetrized ring count 6;a 12-atom bridged cage whose reference name is
heptacyclo[...]dodecane, 7 rings – symmetrized ring count 11.
MAX_CAGE_RINGSwas compared against the symmetrized count, so a cap meant to bound “8 rings” refused heptacyclo cages.Two other places in the tree already compute this quantity correctly but privately –
VonBaeyerAnalyzer._get_ring_count(asE - V + 1, so it under-counts a DISCONNECTED atom set) andrules/bicyclo.py:163, whose comment had already diagnosed the hazard in prose (“cycle_rank… is always reliable regardless of SSSR issues”). Neither was the bug, so neither is rerouted here:_get_ring_counthas 7 PIN-path call sites and swapping its disconnected-set behaviour is a separate, separately-gated change. New callers should use THIS function.Returns
Nonewhencage_atomsis empty, so callers fail closed rather than treat “no cage” as a ring count.
- orthonym.rules.polycyclic.cyclo_ring_count_word(ring_count)#
The von Baeyer ring-count term for
ring_countrings, orNone.(
the Blue Book): “The number of rings is indicated by thenondetachable prefix ‘bicyclo’ (not dicyclo), ‘tricyclo’, ‘tetracyclo’, etc.” – restated at (
:9645). Both sentences end in “etc.”: the series is OPEN-ENDED and the Blue Book prints no table of these words (the highest one attested anywhere in the text ishexacyclo,:9731). The term is therefore COMPUTED – simple multiplying prefix (Table 1.4, +cyclo– with the single irregularity that 2 isbi, notdi. No vowel elision applies:cyclostarts with a consonant.CYCLO_PREFIXESis consulted first purely as a fast, human-auditable path; it agrees with the composition at every entry it holds (asserted bytest_table_agrees_with_composition), so it is a cache of the rule and not a second, competing definition of it.Returns
Nonewhen no word can be formed, so callers FAIL CLOSED rather than emit the non-word the previous f-string fallback produced ("21cyclo"for 21 rings). That fallback was NOT latent: the PIN path (name_polycyclic_complete) caps nothing abovering_count < 2, and a 21-ring cage really did emit21cyclo[...]tetratetracontane. TheMAX_CAGE_RINGS = 8ceiling guards only the opt-in general-engine path invonbaeyer_universal, which is a different caller.
- class orthonym.rules.polycyclic.BridgeInfo(atoms, length, start_bh, end_bh, is_secondary=False, is_dependent=False, locant_low=None, locant_high=None)#
Bases:
objectInformation about a single bridge in a polycyclic system.
- atoms: List[int]#
- length: int#
- start_bh: int#
- end_bh: int#
- is_secondary: bool = False#
- is_dependent: bool = False#
- locant_low: int | None = None#
- locant_high: int | None = None#
- class orthonym.rules.polycyclic.PolycyclicDescriptor(ring_count, bridge_info_list, numbering, total_atoms, descriptor_string, bridge_lengths=<factory>, legality=None)#
Bases:
objectComplete von Baeyer descriptor for a polycyclic system.
- ring_count: int#
- bridge_info_list: List[BridgeInfo]#
- numbering: Dict[int, int]#
- total_atoms: int#
- descriptor_string: str#
- bridge_lengths: List[int]#
- legality: bool | None = None#
Verdict of
VonBaeyerAnalyzer._legality_verified– True only whendescriptor_string, read by the Blue Book’s own numbering rules, rebuilds exactly this cage undernumbering.Nonemeans NOT ADJUDICATED (a raw_analyze_implcandidate);analyzesets it on every return path. Anything that spells a name from this object must require ``legality is True`` – a descriptor that fails is not a worse name, it is a name for a different molecule.
- orthonym.rules.polycyclic.find_longest_path(mol, start, end, allowed_atoms)#
Find the longest simple path from start to end through allowed atoms.
Uses DFS with backtracking. For polycyclic ring systems (<50 atoms), exhaustive search is feasible and fast. The search is bounded by
_MAX_DFS_EXPANSIONSso a pathological dense cage cannot hang; on abort the best path found so far is returned.- Parameters:
mol – RDKit Mol object
start (int) – Starting atom index
end (int) – Target atom index
allowed_atoms (Set[int]) – Set of atom indices that the path may traverse
- Returns:
List of atom indices from start to end (inclusive), or empty list if no path exists.
- Return type:
List[int]
- class orthonym.rules.polycyclic.VonBaeyerAnalyzer#
Bases:
objectImplements IUPAC von Baeyer nomenclature, through).
Pipeline: 1. Ring count: cycle_rank = edges - vertices + 1 2. Main ring: largest ring through a bridgehead pair 3. Main bridge: longest path between main bridgeheads not through main ring 4. Secondary bridges: remaining connections, independent before dependent 5. Numbering: main ring -> main bridge -> secondary bridges 6. Descriptor: prefix[bridge_lengths]
Verification: sum(bridge_lengths) + 2 = total_skeletal_atoms
- analyze(mol, ring_atoms, spiro_atom=None)#
Main entry point: analyze a polycyclic system and produce its descriptor.
spiro_atom(default None) is the atom index of the spiro junction when this cage is a COMPONENT of a spiro ring system; passing it makes the numbering selection give that atom the lowest locant, ABOVE the heteroatom criteria. Every whole-molecule caller leaves it None and the analysis is byte-identical. Only the substituted/heteroatom branch honours it – a spiro component is always “substituted” (its spiro atom bonds into the other component), so it never takes the pure-cage renumber branch; if it somehow did, the criterion is a safe no-op there.Determinism : for an UNSUBSTITUTED cage the cascade is run on a copy renumbered into a total RDKit canonical-rank order (identical for every SMILES spelling), making the descriptor spelling-independent; the numbering is then mapped back to the caller’s atom indices. If the canonical run yields a malformed descriptor the cascade falls back to the original atom order. SUBSTITUTED / heteroatom cages are NOT renumbered (that shifts substituent locants and breaks round-trips); they still tie-break on canonical ranks inside
_find_main_ring.The fallback carries no quality guarantee. This docstring used to claim the fallback “is never worse than the un-renumbered path” – it IS the un-renumbered path, so the sentence was true only vacuously, and it read as a validation that did not exist: the fallback result was returned with no check at all, which is how descriptors whose brackets do not account for every skeletal atom reached the namer .
What IS guaranteed: every return path is adjudicated by
_legality_verifiedand the verdict published asPolycyclicDescriptor.legality.analyzestill returns its best analysis when that verdict is False – the numbering remains useful to callers that only inspect the cage – so every caller that SPELLS A NAME must require ``legality is True`` and fail closed otherwise.- Parameters:
mol – RDKit Mol object
ring_atoms (Set[int]) – Set of atom indices in the ring system
- Returns:
PolycyclicDescriptor with all VB information,
legalityset, or None when the cascade could not produce an analysis at all.- Return type:
- orthonym.rules.polycyclic.vonbaeyer_cage_has_aromaticity(mol, cage_atoms)#
G0 fail-closed safety (DD7 S1 — “fail closed, never hallucinate”).
Von Baeyer and bicyclo nomenclature describe SATURATED bridged ring skeletons; unsaturation is expressible only as
-ene/-ynewith locants, and aromaticity CANNOT be represented at all. Naming a cage that contains aromatic ring atoms therefore silently DROPS the aromaticity and emits a structurally WRONG (de-aromatised) cage — e.g. benzonorbornadieneC1C2C=CC1c1ccccc12->tricyclo[4.4.0.1(2,5)]undec-3-ene(the benzo ring desaturated). The correct PIN is a bridged-fused name, e.g.1,4-dihydro-1,4-methanonaphthalene), a Phase-G1 build; until then the caller must fail closed (raise the limit ->unknown organic compound/OrthonymLimitError) rather than emit the wrong saturated cage.cage_atomsMUST be the EXACT atom set the namer numbers — the von-Baeyer descriptor’snumberingkeys forname_polycyclic_complete, orget_complete_bicyclo_data['ring_atoms']for the bicyclo path — NOT the molecule’s largest connected ring component. Keying off the actual cage means a PENDANT aromatic ring joined by a single (non-ring) bond — e.g. a naphthyl on norbornane, even when the naphthyl is LARGER than the cage — is never part of the cage and so can never trip the guard (it is correctly named as a substituent). (.)
- orthonym.rules.polycyclic.generate_polycyclic_name(mol)#
Generate the base IUPAC name for a polycyclic bridged system.
Returns “prefix[descriptor]parentname” (e.g., “tricyclo[3.3.1.1(3,7)]decane”). Only base name – no substituents, unsaturation, or stereo.
This function is an internal helper that will be called by name_polycyclic_complete in Plan 16-03.
- Parameters:
mol – RDKit Mol object
- Returns:
Base name string, or None if not a polycyclic system
- Return type:
str | None
- orthonym.rules.polycyclic.is_polycyclic_system(mol)#
Detect whether a molecule has a bridged polycyclic ring system with ring_count >= 3 (tricyclo+).
Returns True for tricyclo and higher bridged systems. Returns False for: - bicyclo (ring_count == 2) - purely fused aromatic systems (naphthalene, perylene, coronene) - spiro systems - monocyclic rings
This function is used by the composer for routing.
- Parameters:
mol – RDKit Mol object
- Returns:
True if molecule has a tricyclo+ bridged polycyclic system
- Return type:
bool
- orthonym.rules.polycyclic.get_heteroatom_replacement_prefix(mol, numbering, ring_atoms)#
Generate the ‘a’ replacement-nomenclature prefix for ring heteroatoms.
Thin wrapper over
rules/ring_replacement.build_replacement_prefix, which owns the construction (element table, Table-2.8 citation order, λ tokens, multiplying prefixes, the no-trailing-hyphen rule of. The string returned here is byte-identical to what this function built inline beforea phase — the three PIN callers (
name_polycyclic_complete,bicyclo.py) see no change.- Parameters:
mol – RDKit Mol object
numbering (Dict[int, int]) – Dict mapping atom_idx -> VB locant (1-indexed)
ring_atoms (Set[int]) – Set of atom indices in the ring system
- Returns:
Formatted prefix string (e.g. “7-oxa”);
""when there is no heteroatom to express; orNonewhen some skeletal atom CANNOT be expressed, in which case the caller must refuse.- Return type:
str | None
The
Nonecase is the fix for what this docstring used to concede: the old-> strsignature could not report an atom it failed to express, so an off-table skeletal element was dropped from the name while the ring stem kept counting it. Its three PIN von Baeyer callers (name_polycyclic_with_heteroatoms,name_polycyclic_complete,bicyclo.get_complete_bicyclo_data) rested on an unverified assumption that such an element never reaches them; they now get the signal instead of the proof obligation.""andNoneare deliberately distinct –if not prefixwould conflate “carbocycle” with “refuse”, so callers testis None.
- orthonym.rules.polycyclic.detect_polycyclic_lactone(mol, ring_system_atoms)#
Detect if a polycyclic system contains a lactone (cyclic ester).
A polycyclic lactone has: - An ester group [-C(=O)-O-] where both the carbonyl carbon AND
the ester oxygen are part of the ring system
The carbonyl oxygen (=O) is exocyclic (double-bonded to carbonyl C)
- Parameters:
mol – RDKit Mol object
ring_system_atoms (Set[int]) – Set of atom indices in the polycyclic ring system
- Returns:
- Dict with lactone info if found – carbonyl_c: atom index of carbonyl carbon
ring_oxygen: atom index of ring (ester) oxygen carbonyl_oxygen: atom index of exocyclic carbonyl oxygen
None if no polycyclic lactone found
- Return type:
Dict | None
- orthonym.rules.polycyclic.name_polycyclic_with_heteroatoms(mol)#
Generate IUPAC name for a polycyclic system containing ring heteroatoms.
Uses “a” replacement nomenclature for ring heteroatoms (oxa, aza, thia) and pseudoketone naming for polycyclic lactones (oxa- prefix + -one suffix).
Format: “{hetero_prefix}bicyclo[descriptor]{parent_name}” or with -one suffix Example: “7-oxabicyclo[2.2.1]heptane” or “3-oxabicyclo[3.2.1]octan-2-one”
- Parameters:
mol – RDKit Mol object
- Returns:
Complete IUPAC name with heteroatom prefixes, or None if not applicable
- Return type:
str | None
- orthonym.rules.polycyclic.get_polycyclic_substituents(mol, ring_atoms, numbering, exclude_atoms=None)#
Detect substituents attached to a polycyclic ring system.
For each ring atom, checks neighbors not in ring_atoms. Traces each substituent branch and determines its name using get_alkyl_name.
- Parameters:
mol – RDKit Mol object
ring_atoms (Set[int]) – Set of atom indices in the polycyclic ring system
numbering (Dict[int, int]) – Dict mapping atom_idx -> VB locant (1-indexed)
exclude_atoms (Set[int] | None) – Optional set of non-ring atom indices to skip (e.g., atoms that are part of functional groups)
- Returns:
List of substituent info dicts, each containing –
‘locant’: VB locant where substituent attaches
’name’: substituent name (e.g., ‘methyl’, ‘ethyl’)
’atom_indices’: list of atom indices in the substituent
- Return type:
List[Dict]
Note
Skips exocyclic double bonds (=O, =S) as those are handled as suffixes.
- orthonym.rules.polycyclic.get_polycyclic_unsaturation(mol, ring_atoms, numbering)#
Detect double and triple bonds within a polycyclic ring system.
Scans all bonds where BOTH atoms are in ring_atoms and identifies multiple bonds. Returns the lower VB locant for each bond.
- Parameters:
mol – RDKit Mol object
ring_atoms (Set[int]) – Set of atom indices in the polycyclic ring system
numbering (Dict[int, int]) – Dict mapping atom_idx -> VB locant (1-indexed)
- Returns:
Dict with –
‘double_bonds’: list of locants for double bonds
’triple_bonds’: list of locants for triple bonds
- Return type:
Dict
- orthonym.rules.polycyclic.get_polycyclic_stereo(mol, numbering)#
Collect and format stereodescriptors for a polycyclic system.
Uses the VB numbering to generate IUPAC locants for R/S stereocenters.
- Parameters:
mol – RDKit Mol object (stereochemistry should already be assigned)
numbering (Dict[int, int]) – Dict mapping atom_idx -> VB locant (1-indexed)
- Returns:
Formatted stereodescriptor string like “(1R,4S)-” or empty string if no stereodescriptors found.
- Return type:
str
- orthonym.rules.polycyclic.name_polycyclic_complete(mol, features=None)#
Generate the complete IUPAC name for a polycyclic bridged system.
This is the FINAL public API for polycyclic naming. It supersedes generate_polycyclic_name which only generates base names.
Produces complete names including: - Stereodescriptors: “(1R,4S)-” - Substituent prefixes: “3-methyl-” - Functional group prefixes: “5-hydroxy-” (non-principal groups) - Heteroatom replacement: “7-oxa-” (when Plan 16-02 implemented) - Descriptor: “bicyclo[2.2.1]” - Parent name with unsaturation: “hept-2-ene” - Functional group suffix: “-2-one”, “-1-ol”, “-carboxylic acid”
Example output: “(1R,4S)-5-hydroxy-3-methyl-7-oxabicyclo[2.2.1]heptan-2-one”
- Parameters:
mol – RDKit Mol object
features – Optional MolecularFeatures object for functional group detection. If None, FG detection is performed directly via SMARTS.
- Returns:
Tuple of (name, ring_atoms, atom_to_locant, substituents_included) where substituents_included is True (polycyclic handler discovers substituents via get_polycyclic_substituents + _detect_ring_functional_groups), or None if not a polycyclic system.