orthonym.perception.structure_conservation#
Note
Internal API. Names and behaviour may change between releases.
Tier-B structure-conservation checks (Wave-8 P10 – the “E1 atom-coverage certificate” from the cross-tool audit).
Java-free, source-level structural checks that close leaks where the winning
naming path emits a name for a DIFFERENT molecule than the input (atom-drop,
charge-drop) and only OPSIN’s constitutional gate
(namer._final_opsin_validity_gate / _self_consistency_decision) would
otherwise have caught it. FAILS OPEN when no JAR is present
(_validity_gate_jar_present returns False), so in a no-Java deployment
these leaks ship as confidently-wrong names. The functions here are called
UNCONDITIONALLY from namer.py::Orthonym._name_impl – i.e. NOT gated by
self._disable_opsin_validity_gate – so they protect the raw path too.
Design principle (accuracy-first, precision-over-recall, per
internal notes): every check here is scoped as
NARROWLY as the evidence supports. A check that cannot be made precise
(false-veto risk on a currently-correct name) is deliberately left
unimplemented rather than shipped – see the docstring on
partial_sat_sp3_substituent_drop for a worked example of the
investigation this required (an initial, broader version of that check
false-vetoed 17/25 real-corpus hexahydro-/octahydronaphthalene names before
being narrowed to the exact shape it is proven safe for).
- orthonym.perception.structure_conservation.input_invariants(mol)#
Java-free structural invariants of the INPUT molecule.
Returns
(heavy_atom_count, dou, bond_order_sum):heavy_atom_count:mol.GetNumHeavyAtoms(authoritative).dou: rings + sum(bond_order - 1) over heavy-heavy bonds. A pure GRAPH invariant, deliberately NOT the classical H-count degree-of-unsaturation formula (which conflates formal charges/ radicals and would misfire on ions/radicals). Using the SUM of (order - 1) rather than a discrete count of “multiple bonds” is what makes this exact for aromatic rings too: RDKit reports an aromatic bond’s order as 1.5 (GetBondTypeAsDouble), so each contributes 0.5 – benzene’s 6 aromatic C-C bonds contribute 6*0.5=3, plus 1 ring, for the expected DoU of 4 (matching the Kekule double-bond count).bond_order_sum: rounded sum of heavy-heavy bond orders – a coarse diagnostic signal, not consumed by any veto below.
Pure RDKit. No OPSIN import, no subprocess – safe to call unconditionally on the no-Java path.
- orthonym.perception.structure_conservation.net_formal_charge(mol)#
Net formal charge of the molecule (Java-free, always available).
- orthonym.perception.structure_conservation.looks_like_ionic_name(name)#
Coarse recognizer: does
nametextually encode ionic character via a recognised / charge suffix?Deliberately LENIENT (broad match), because a false NEGATIVE here (a correctly-charged name this regex fails to recognise) only means a leak is missed – safe, per precision-over-recall. Every currently-shipping net-charged PIN this task could enumerate – the ~45 net-charged rows of
benchmarks/the gold set/gold_pins.json(azanylium, silylium,…anium, …ide,…ate, bis(…ide)/bis(…ate)/bis(…ium) forms) – matches this pattern.Cation suffixes (‘-ium’, ‘-ylium’, ‘-anium’, ‘-onium’) all end in the literal substring “ium”. Anion suffixes (‘-ide’, ‘-uide’, ‘-ate’, ‘-oate’, ‘-olate’, ‘-thiolate’, ‘-aminide’,…) all end in “ide” or “ate”. A trailing
)is allowed for a multiplied bis(…)/tris(…) form (e.g.ethane-1,2-bis(aminium)).
- orthonym.perception.structure_conservation.charge_dropped(mol, name)#
True iff the input carries a nonzero NET formal charge, a charge-bearing atom is a main-group centre Orthonym’s charged routing does not fully cover (see
_has_main_group_charge_centre), ANDnamedoes not textually encode ionic character (seelooks_like_ionic_name).Scope: NET charge only – a genuinely charged single ion (or an ionic assembly whose charges do not cancel). A net-ZERO zwitterion (nitro, N-oxide, amino-acid zwitterion, sulfonium/ammonium ylide,…) never reaches this check (
net_formal_charge(mol) == 0short-circuits), so the entire (large) neutral-by-charge-separation naming surface is untouched by construction. The main-group-centre guard further excludes every routine ionic centre (ammonium/choline N+, carboxylate/alkoxide O-, carbanide C-, thiolate/sulfonium S), which have dedicated charged routes – so this fires ONLY on a genuine main-group charge drop.Callers MUST exclude an already-descriptive-fallback
name(seeerrors.is_failure_name) before calling this – an honest abstention (‘unknown organic compound’, ‘<metal> compound (not supported)’,…) is not a “dropped charge” and must not be double-suppressed.
- orthonym.perception.structure_conservation.partial_sat_sp3_substituent_drop(mol, name)#
R12 spillover (2026-07-17, W8-P1/R12 adversarial-verify workflow; closed here per W8-P10).
⚠ This veto’s ORIGINAL premise no longer holds, and the rewrite is the point. It used to reason:
rules.polycyclics.name_partially_saturated_- carbocycle“NEVER bakes a saturated (sp3)-ring substituent into its own bare parent name – by construction, its_partial_sat_substituent_prefixhelper explicitly skips every atom insat_set”, so the ONLY route for an sp3-ring substituent was the composer’s enrichment pass, andname == barealongside an sp3-ring off-ring substituent was therefore a deterministic proof of drop.The hand-off that premise described was itself the defect. It derived a SECOND numbering (placing tetralin’s 2-methyl at locant 1) and it split one substituent set across two prefix formatters (
2-methyl-6-methyl-whererequires
2,6-dimethyl-). Both faults are fixed by making theproducer the single speller of every ring substituent prefix, so
bareNOW encodes sp3-ring substituents, and the old test would false-veto every correct name in the class.Detection, restated for the current construction and still structural rather than heuristic: the producer publishes the off-ring atoms its name actually spells (
PartialSatName.spelled_offring_atoms– prefixes plus the exocyclic atoms of any principal-characteristic-group suffix such as-2-carboxylic acid/-2-ol). An off-ring heavy atom on an sp3 ring position that is in NEITHER that set NOR the enrichment the shipped name added is unaccounted for, and that is a drop. Subtracting the published set is not a loosening: before it was subtracted, this veto suppressed the very names that carry those atoms – every ring-COOH member of the class emittedunknown organic compoundwhilebarealready held the correct1,2,3,4-tetrahydronaphthalene-2-carboxylic acid.Scope guard (precision-over-recall): restricted to the exact “fully-aromatic-ring + fully-saturated-ring” (tetralin-class) shape – every ring atom must be EITHER aromatic OR in
sat_set. An initial, broader version of this check (usingsat_setalone, without this guard) FALSE-VETOED 17 of 25 real hits on a corpus sweep ofbenchmark_multi_corpus_results.csv– all of them mixed alkene/hydro systems (hexahydro-/octahydronaphthalene, hydroazulene,…) whose substituents are, in fact, already correctly spelled by_partial_sat_substituent_prefixand which this function must not second-guess. Narrowing to the clean two-ring aromatic/saturated split eliminated every false positive found (0/91 on the combined synthetic + corpus validation set) while still catching the 3 documented target leaks plus 2 additional real-corpus leaks the narrower check newly surfaced.IUPAC cite: / (partially saturated carbocycles; detachable-prefix placement); mirrors the existing R12 fusion-atom veto inside
name_partially_saturated_carbocycleitself (ring-FUSION atom substituents), which this function does NOT duplicate (that path already returnsNone–bare– for those cases, so this function is a no-op on them).
- orthonym.perception.structure_conservation.fused_ring_atom_drop(mol, name)#
True iff name is a bare (unsubstituted, optionally stereo-prefixed) monocyclic cycloalkane/cycloalkene name for a molecule whose rings span 2+ SSSR rings sharing at least one atom (fused, bridged, or spiro).
Such a name is provably wrong: a single “cyclo<stem>ane” parent cannot represent a second ring’s atoms, regardless of what those atoms are (carbocyclic or heteroatom-containing). See module-level comment above for the full design record.
- orthonym.perception.structure_conservation.oxo_ene_valence_illegal(name)#
-A source-level (Java-free) valence guard.
True iff a ring carbon is cited as BOTH a ring double-bond terminus AND an oxo/-one carbon – a five-bond carbon (the caffeine-class
2,4-dioxo-...-1,7-dieneinvalid name). Pure string parse of the emitted von-Baeyer locants; no RDKit/OPSIN. Runs unconditionally downstream so best-effort is safe even when the OPSIN RT gate is unavailable (no jar). Fail-closed on the cumulated-carbon edge (an oxo carbon truly cannot also hold a ring double bond).M4: SCOPE-AWARE. The collision must be within ONE numbering scope –
the old whole-string union falsely flagged an oxo cited inside a substituent (or in a different salt component) against a parent-ring ene, suppressing 33 valid salt/decorated names. Split into salt words then substituent scopes (
_scope_substrings()) and test each independently; the caffeine-class parent-scope collision still fires, and every recovered name stays OPSIN-round-trip gated downstream so 0-wrong is unchanged.The components of a adduct are separate compounds, each with its own numbering, the Blue Book, “Names are formed by citing the names of individual compounds in the order of the formula connected by long (em) dashes”), so the em dash separates scopes exactly as the space between salt words does: the ‘6-en’ of one component and the ‘3-oxo’ of the next are not one carbon (PubChem-1M row: five ketone / enamine components, each valid).