orthonym.rules.vonbaeyer_universal#
Note
Internal API. Names and behaviour may change between releases.
: universal von-Baeyer cage analysis.
Names ANY bridged/fused polycyclic cage — including AROMATIC cages — by
kekulizing a canonical copy and expressing every former-aromatic bond as an
explicit ene locant unsaturation). The emitted ...-polyene cage
re-parses (OPSIN) to a kekule structure whose canonicalization re-aromatizes
to the SAME molecule, so structural fidelity is preserved; this is the
universal ring fallback for the opt-in general engine ONLY. The default
PIN path’s aromatic-cage refusal (polycyclic.py:2789) is deliberately
untouched.
Determinism: kekulization is atom-order dependent, so we ALWAYS canonical- reparse first and work in canonical indices; results map back to original indices via GetSubstructMatch.
- orthonym.rules.vonbaeyer_universal.MAX_CAGE_ATOMS = 40#
Implementation ceilings on the cage this module will analyse. The Blue Book sets NO upper size limit on von Baeyer nomenclature is construction rules only), so both numbers are ours, not nomenclature’s. They guard only this module – the default PIN path (
polycyclic.name_polycyclic_complete) caps nothing abovering_count < 2.a phase measured what raising them buys, over the 1203 ring molecules of
benchmarks/pubchem_2000.csv, and the answer is NOTHING: with both caps set to 200 the emitted name is byte-identical for every molecule the caps touch. So the VALUES stay and the fix went to what was being COUNTED –MAX_CAGE_RINGSwas compared against RDKit’s symmetrized ring-set cardinality instead of the ring number (seepolycyclic.von_baeyer_ring_count), which made this cap refuse heptacyclo cages for being “more than 8 rings”.Do not raise
MAX_CAGE_RINGSwithout first fixing main-bridge selection:_find_main_ringonly ever offers a 0- or 1-atom main bridge, so 7 of 19 Blue Book von Baeyer PIN descriptors come back with a non-preferred main bridge (locked intests/unit/rules/test_v29_p2_vb_ring_count.py). Raising the cap would broaden that class, not add PIN coverage.
- orthonym.rules.vonbaeyer_universal.BEST_EFFORT_MAX_CAGE_ATOMS = 100#
The best-effort tier’s ceilings. A best-effort name need not be the PIN, only a verified systematic name, so the main-bridge concern above does not apply there; the work of each analysis is bounded by the per-name budgets (
fragment_naming._PERF_BUDGET/_ANALYSIS_CALL_BUDGET). The values are measured, in two rounds.a performance pass (TRIAGE ‘Large molecules – part 3’; the large-molecule census, the large-polycycle ladder and its two corpora): with no ceiling at all every ring system named has at most 82 skeletal atoms and 11 rings, and no system of more rings was named. That sample held no ring system of 12 or more rings that the engine could name, so 11 rings was the highest value it could show.
a performance pass (lane UN-B1, 2026-10-21): the 477 PubChem rows of the census-2 unnamed panel that hold a ring system of 12 or more rings (circuit rank 8-43), run at the best-effort tier under six values of the ring ceiling, every name read back by an independent OPSIN call (full standard InChIKey):
ring ceiling 11 15 20 30 40 100 named 0 87 86 86 86 86 read back - 87 86 86 86 86 (no name was wrong) median s 10.2 16.7 17.8 17.8 17.8 17.7 p95 s 25.0 29.5 31.4 30.8 32.8 32.3 TIMEOUT@120 0 2 2 2 2 2
Every whole ring system that names has at most 15 rings (12: 33 rows, 13: 50, 14: 2, 15: 1); the one other name is a spiro union of two polycyclic parts that is 16 rings as one system. So a ceiling above 15 never names a row that 15 does not, and it costs: the 139 rows whose system has 16 or more rings run their whole-system analysis for 6-8 s more each and name nothing, and the spiro union (CID140891835) loses its name. Spied in fresh processes at 15 and at 20: at 20 the whole system (16 rings) is analysed first and raises
PerfBudgetExceededafter 7.9 s, and its parts are never named; at 15 the whole system is refused at once and the parts are analysed and named. So 15 is the ceiling that names the most and costs the least. The two TIMEOUT rows are 15-ring systems (CID59886507, CID101381505) whose main-ring search runs past 120 s inpolycyclic._find_main_ring_impl(CID59886507: 83 of 88 s in_consider_multiatom_bridges) without the perf budget stopping it; they cost 2.5 s and 6.3 s at 11, and bounding that search is the search speed’s work. A faster main-ring search can name systems of more rings: measure again then.The atom ceiling stays 100: of the 13 of those rows that hold a ring system of more than 100 skeletal atoms (102-124), a ceiling of 200 names one (an 8-ring macrocycle of 102 atoms, CID11147635) and costs up to 100 s more on another.
a performance pass (the identity panels, the 2,000 named-control and 2,793 unnamed-panel PubChem rows at both tiers): the plain ceiling of 15 lost 21 names the ceiling of 11 gave (0 of the 2,000 control rows; 21 of the 5,586 panel rows). Each of the 21 hands the analysis a ring-atom set of 12 to 15 rings that is not one ring system: 14 are several ring systems joined by chains (the circuit rank sums over them, 2 to 9 separate systems, 46 to 74 atoms), and 7 are one system with a spiro union (a spiro atom joins two polycyclic parts, DEFINITIONS: “The following recommendations apply only to the naming of parent hydrides containing free spiro unions”, and MONOSPIRO RING SYSTEMS WITH DIFFERENT RING COMPONENTS, AT LEAST ONE OF WHICH IS A POLYCYCLIC RING SYSTEM: such a system is named from its parts, not as one von Baeyer system). The ceiling of 11 refused these at once and another producer named the parts; at 15 the whole-system analysis ran first, ended in
PerfBudgetExceeded(traced in fresh processes) and the parts were never named. So aboveBEST_EFFORT_MAX_UNION_RINGSthe whole-system analysis is only for one biconnected ring system (connected through ring bonds, no atom whose removal splits it):within_ring_ceiling. None of the 86 whole systems that name at 12 to 15 rings (a performance pass) has such an atom, so the rule costs no name there, and it spares the budget the parts need: the 477 rows of a performance pass under the ceiling of 15 with the rule name 92 (all read back, none wrong; median 15.0 s, p95 25.5 s, one TIMEOUT@120), and the 21 lost panel rows name again with their former names. The PIN tier keeps the two ceilings above.
- orthonym.rules.vonbaeyer_universal.BEST_EFFORT_MAX_UNION_RINGS = 11#
The ceiling a best-effort ring-atom set keeps when it is not one biconnected ring system (several ring systems, or a spiro union of parts): the value of a performance pass.
- orthonym.rules.vonbaeyer_universal.pin_ceilings_scope()#
Within the body every von Baeyer analysis takes the PIN-tier ceilings (
MAX_CAGE_ATOMS/MAX_CAGE_RINGS), whatever the tier of the request.The degraded run of a part whose analysis under the best-effort ceilings ran its hang budgets out: a ring system beyond the PIN ceilings is refused at once, as before the best-effort ceilings existed, and another producer names the part (
assembly.budget_rescue.pin_ceiling_name, a component of an adduct or an ion of a salt).
- orthonym.rules.vonbaeyer_universal.cage_caps()#
(atoms, rings)ceilings for the tier of the running request: the best-effort values in a best-effort request (provenance.best_effort_request_ctx), elseMAX_CAGE_ATOMS/MAX_CAGE_RINGS– in a PIN or complete request, on every path, the best-effort recoveries such a request runs included, and outside a request. Always the PIN values insidepin_ceilings_scope.
- orthonym.rules.vonbaeyer_universal.is_biconnected_ring_system(mol, atoms)#
True iff
atomsis ONE ring system with no free spiro union: connected through ring bonds, and no atom whose removal splits it: a spiro atom is “a single atom common to both rings”, so it is such a cut atom). A set of several ring systems, or a spiro union of polycyclic parts, is not a von Baeyer systemnames the latter from its parts).
- orthonym.rules.vonbaeyer_universal.within_ring_ceiling(mol, atoms, n_rings, ceiling)#
True iff a ring-atom set of
n_ringsrings is analysed underceiling(cage_caps[1]). Up toBEST_EFFORT_MAX_UNION_RINGS(and at the PIN tier, whose ceiling is below it) the count alone decides; above it only one biconnected ring system is analysed (see the a performance pass note atBEST_EFFORT_MAX_CAGE_RINGS).
- orthonym.rules.vonbaeyer_universal.beyond_pin_caps(n_atoms, n_rings=None)#
True iff a ring system of this size is analysed only under the best-effort ceilings. A name built on such a system is not labelled a PIN.
- orthonym.rules.vonbaeyer_universal.record_beyond_pin_caps(fragment)#
Record
fragment(the ring descriptor or ring name a producer built on a ring system beyond the PIN ceilings) as a part that is never a PIN, so a name carrying it is labelled below the PIN (provenance.record_non_pin_fragment).
- class orthonym.rules.vonbaeyer_universal.RingAnalysis(descriptor, total_atoms, hetero_prefix, unsaturation, cage_atoms, atom_to_locant, canon_match, is_mancude=False, hetero_per_atom=())#
Bases:
objectThe ONE field contract
_emit_ring_from_analysisconsumes.Why this base exists (a phase follow-up)#
UniversalCage(von Baeyer) andSpiroSystem(spiro) are two ring analysis forms feeding ONE shared emission tail, so the tail is written against a field shape rather than a class. That shape used to be stated TWICE –SpiroSystemre-declaredUniversalCage’s fields by hand – and the two copies drifted every time either producer was touched:: the skeletal-replacement TOTALITY gate was added to the spiro analyzer only, so a mercury von-Baeyer ring shipped as ``bicyclo[3.3.0]octane– a hydrocarbon name;: ``hetero_per_atomwas added to the cage only, so the spiro sibling kept theUNBOUND_MORPHEMEfinding that commit existed to remove.
Opposite directions, same cause: a contract maintained in parallel. It is declared here ONCE. The subclasses add NO fields – they exist for their names (repr,
isinstance, per-form docs), not to extend the shape – so a field added to the emission contract reaches BOTH producers by construction. Declaring a field is not populating it, so the other half of the guarantee is a test asserting each form actually fills it (tests/unit/validation/test_v29_p2_ring_parent_bindings.py).- descriptor: str#
- total_atoms: int#
- hetero_prefix: str#
- unsaturation: dict#
- cage_atoms: Tuple[int, ...]#
- atom_to_locant: Dict[int, int]#
- canon_match: Tuple[int, ...]#
- is_mancude: bool = False#
- hetero_per_atom: Tuple[Tuple[int, str], ...] = ()#
- class orthonym.rules.vonbaeyer_universal.UniversalCage(descriptor, total_atoms, hetero_prefix, unsaturation, cage_atoms, atom_to_locant, canon_match, is_mancude=False, hetero_per_atom=())#
Bases:
RingAnalysisvon Baeyer polycyclic analysis. Adds no field to
RingAnalysis.canon_matchhere is the canonical-copy index map: canon idx -> orig idx.
- orthonym.rules.vonbaeyer_universal.parse_von_baeyer_descriptor(descriptor)#
'tricyclo[3.3.1.1^3,7]'->([3, 3, 1], [(1, 3, 7)]); None if the string is not a well-formed von Baeyer descriptor.The three leading integers are the bicyclic system (two main-ring branches then the main bridge, “cited in descending numerical order”); each remaining term is
length^low,highfor one secondary bridge.
- orthonym.rules.vonbaeyer_universal.reconstruct_von_baeyer_skeleton(descriptor, secondary_order=None)#
Rebuild the ring skeleton a von Baeyer descriptor STRING denotes.
Returns
(total_atoms, frozenset{(low_locant, high_locant)})– the exact bond set the emitted name encodes, in locant space – orNonewhen the string is malformed.The numbering is not a convention of ours; it is read straight off the Blue Book, which is what makes this a proof rather than a heuristic:
** “Numbering bicyclic alicyclic hydrocarbons”** (
the Blue Book Blue Book): “The bicyclic ring system is numbered starting with one of the bridgeheads and proceeding first along the longer segment of the main ring to the second bridgehead, then back to the first bridgehead along the unnumbered segment of the main ring. Numbering is completed by numbering the main bridge beginning with the atom next to the first bridgehead.”** “Numbering the secondary bridge”** (
:9623): “After the main ring and main bridge have been numbered, the independent secondary bridge is numbered continuing from the higher numbered bridgehead of the main ring.”** “Numbering of secondary bridges”** (
:9711): “the numbering continues from the highest number of the main ring and main bridge. Each secondary bridge is numbered in turn starting with the independent secondary bridge linked to the highest numbered bridgehead atom of the main ring, then the independent secondary bridge linked to the next highest bridgehead atom, and so on. Each atom of a secondary bridge is numbered starting with the atom next to the higher numbered bridgehead.”
secondary_orderoverrides the order in which the secondary bridges consume locants. fixes that order by descending attachment bridgehead, but leaves ties to; the caller uses this to try the remaining orderings rather than false-reject a cage whose only deviation is a tie-break, which is a preference question and not a legality one.
- orthonym.rules.vonbaeyer_universal.audit_von_baeyer_descriptor(mol, cage_atoms, numbering, descriptor)#
Java-free structural correctness floor for a von-Baeyer descriptor.
Rebuild the ring skeleton the EMITTED DESCRIPTOR STRING denotes (Blue Book numbering rules,
reconstruct_von_baeyer_skeleton) and require SET EQUALITY with the input’s actual cage bond set in locant space. Any mismatch – a bridge the descriptor fails to cite, an atom count the brackets do not account for, or a numbering inconsistent with the string – returnsFalseso the caller fails closed.Why the string and not our own bridge list#
This audit used to reconstruct from
PolycyclicDescriptor.bridge_info_list– internal bookkeeping – rather than from the descriptor that actually gets spelled. Measured over 8,201 enumerated cages (N<=12) and 1,623 corpus cages, that was wrong in both directions at once:False rejections.
BridgeInfo.atomsfor a secondary bridge is stored in the opposite order to the bond path (numbering runs from the higher numbered bridgehead,, so the walk crossed a non-bonded pair and the audit refused 556/8,201 enumerated and 14/1,623 corpus cages whose emitted name was perfectly correct.False acceptances. The bridge list can agree with itself while the spelled string does not describe the cage at all – 6 corpus cages passed the old audit with a descriptor denoting a different skeleton.
Set-equality against the string catches both, and is what the name is actually judged on. It is deliberately independent of OPSIN: both downstream gates (the validity gate and) are documented FAIL-OPEN when Java is unavailable (
namer.py:588,:608), and with them off 30 enumerated cages ship a descriptor whose brackets account for fewer atoms than its own stem counts (e.g.tetracyclo[5.1.1.2^3,6]dodecane– 11 bracketed atoms,dodecane= 12). This floor refuses those without asking Java.This proves LEGALITY (the name denotes this molecule under this numbering), never PREFERENCE (that it is the PIN among legal alternatives).
- orthonym.rules.vonbaeyer_universal.analyze_cage_universal(mol, cage_atoms=None, allow_mancude=False, spiro_atom=None)#
Deterministic universal cage analysis; None on any refusal.
spiro_atom(default None, original-mol atom index) is the spiro-junction atom when this cage is a COMPONENT of a spiro ring system. When passed, the von-Baeyer numbering selection gives that atom the lowest locant, above the heteroatom criteria; it is mapped into the canonical index space used internally before being threaded toVonBaeyerAnalyzer.analyze. Every whole-molecule caller leaves it None, and the result is byte-identical.: when
allow_mancudeis True the aromatic/mancude-cage refusal below is LIFTED – the cage is kekulized (already done above) and every former-aromatic bond is emitted as an explicit von-Baeyer polyene ene locant unsaturation). When False (the default / PIN path) the refusal fires exactly as before, so that path is byte-identical.Every returned descriptor is put through
audit_von_baeyer_descriptor(a Java-free skeleton edge-audit): the descriptor+numbering must assert exactly the molecular-graph ring/bridge bond set or the cage is discarded (fail-closed). This floor holds for the saturated/valid tier too and is what makes the mancude polyene emission trustworthy independent of OPSIN.
- class orthonym.rules.vonbaeyer_universal.SpiroSystem(descriptor, total_atoms, hetero_prefix, unsaturation, cage_atoms, atom_to_locant, canon_match, is_mancude=False, hetero_per_atom=())#
Bases:
RingAnalysisSpiro ring-system analysis. Adds no field to
RingAnalysis.The field shape is INHERITED, not re-declared: this class used to restate
UniversalCage’s fields by hand and the two lists drifted twice (seeRingAnalysis).descriptoris"spiro[4.5]"/"dispiro[3.2.3.2]"andcanon_matchis the sorted original cage-atom tuple (the analysis works on a ring-only submol, so there is no separate canonical copy to map).
- orthonym.rules.vonbaeyer_universal.audit_spiro_descriptor(mol, cage_atoms, numbering, spiro_atoms, descriptor)#
Java-free structural correctness floor for a spiro descriptor+numbering.
Mirrors
audit_von_baeyer_descriptor’s set-equality contract, adapted to spiro topology. Fail-closed (False) on any of:- (bijection) numbering is not a 1-1 map of the cage atoms onto {1..N}
(rejects a numbering that maps two atoms to the same locant);
- (coverage) the SSSR rings contained in the cage do not union to exactly
the cage atom set (a ring atom silently dropped);
- (pure-spiro) two cage rings share >1 atom (fused/bridged mis-routed here),
a shared atom is not a declared spiro atom, a spiro atom is not in exactly 2 cage rings, a non-spiro atom is not in exactly 1, or n_rings != n_spiro + 1;
- (simple-cycle) any cage atom has != 2 ring-neighbours within one of its
rings (a real bond the descriptor’s cycle would assert is missing, or a bridge edge is present);
- (arithmetic) the descriptor’s segment integers + n_spiro != N (a wrong
ring-size descriptor for the audited skeleton).
- orthonym.rules.vonbaeyer_universal.analyze_spiro_universal(mol, cage_atoms=None, allow_mancude=False, free_valence_atoms=None, prefix_atoms=None)#
Deterministic universal spiro analysis; None on any refusal.
cage_atomsselects the spiro ring system insidemol(defaults to all ring atoms).free_valence_atoms(original indices) biases the monospiro numbering to give the free valence the lowest locant, substituent use).prefix_atoms(original indices, one per prefix) adds (f) and then (g) to the numbering, (g) asked ofmol, the molecule that carries the prefixes.allow_mancudelifts the aromatic-spiro refusal, emitting the kekulized ene-locant form; when False an aromatic spiro fails closed (deferring to the retained-ring-name PIN path). Every result is put throughaudit_spiro_descriptor(fail-closed on any structural mismatch).