orthonym.rules.charged_router#

Note

Internal API. Names and behaviour may change between releases.

The single mandatory parent-selection chokepoint for charged species.

a phase Plan 03 (CHOKE-01, CHOKE-02). ROOT CAUSE of the flat round-trip wall (V20 audit): every charged class (alkoxide / phenolate / carbanion / thiolate / carbenium / onium / diazonium / aminium-fallback / radical) decided its own parent through a parallel carbon-counting stub that ignores connectivity, substituents and unsaturation — the literal heptanolate bug (CCCCCCC[O-] -> heptanolate, dropping the locant and every substituent). Those stubs BYPASS the sound select_parent cascade for 91% of wrong-parent failures.

This module makes the chokepoint STRUCTURAL: route_charged(mol, style) is the ONE funnel every charged dispatch handler delegates to. It GENERALIZES the proven _name_oxoacid_anion template (ions.py:911, the 169.5 fix that round-trips):

neutralize the chosen fragment -> re-enter the FULL pipeline Orthonym(style, _disable_opsin_validity_gate=True).name(neutral_smi) -> re-apply the class-correct ionic suffix via apply_ion_suffix_to_name.

A correct parent auto-corrects the locants (95% co-occurrence) on the same molecule — this is the multi-defect-collapsing fix, not a per-class patch.

The four IUPAC-2013 guards (SYNTHESIS-authoritative-cascade.md; /

/, applied IN ORDER:

GUARD 1 FG-class-before-suffix (the heptanolate fix). A -S(=O)2-O- is

an acid anion -> -sulfonate, NOT a hydroxy anion -> -olate. classify_anion picks the FG class; the per-class allowed_suffixes subset gates the textual seam so a sulfonate stem can NEVER mis-fire to -olate.

GUARD 2 ionic-center-count-first a-c / a-b). On a multi-center

ion the parent maximizes anionic / ide / uide center count BEFORE length is considered (dicarboxylate dianions). Realized by neutralizing ALL same-sign centers and letting the re-entered pipeline name the multi-suffix parent (butanedioate), exactly as the proven anion seam already does.

GUARD 3 skeletal-charge element seniority d / c):

N > P > As > Sb > Bi > Si > Ge > Sn > Pb > B > Al > Ga > In > Tl > O > S > Se > Te > C. When the charge sits on a skeletal heteroatom the senior element bearing it is the parent, not the longest carbon chain.

GUARD 4 zwitterion anion-is-parent override. The anion is FORCED as

the parent; a separable cation, the betaine quaternary ammonium) is demoted to a structured (…azaniumyl) substituent prefix (substituent_naming.cation_to_prefix); a skeletal cation , a ring N+ of pyridinium-2-carboxylate) is deferred to the legacy path (the cumulative ium+ate suffix is out of scope this plan). 169.6-04 (was a Plan-03 detect-and-defer seam).

Returns ‘’ on any failure / out-of-scope shape (metal complex, multi-fragment salt, zwitterion, malformed re-entered parent) so the caller falls through to the existing retained/legacy cascade — preserving the byte-identical no-crash contract.

NO carbon-counting. NO .replace. NO molecule-specific branch. The router is GENERAL (fix-methodology.md).

orthonym.rules.charged_router.classify_charged_pcg(mol, sites)#

Charge-first PCG classifier (.3, /). Runs on the ORIGINAL (un-neutralized) mol. Returns the FG-name to FORCE as the principal characteristic group (a detect_functional_groups KEY: ‘carboxylic_acid’, ‘sulfonic_acid’, ‘sulfinic_acid’, ‘phosphonic_acid’), or None when the charged class has NO neutral FG anchor (carbanion/alkoxide/thiolate/ aminide/phenolate -> handled by the.2 emit_parent_hydride_cumulative_ suffix primitive / the existing suffix seam, NOT this override) or when a different route owns the molecule.

Ordering (; anions / cations / zwitterion / radical):
  1. radical present -> None radical>anion>cation; the -yl primitive owns it)

  2. mixed-sign (zwitterion) -> None _route_zwitterion owns it)

  3. anion(s): senior ionized acid class per -> its acid-FG key

  4. cation(s) only -> None (the aminium / class-keyed cation transforms own it)

The forced FG MUST be a features.functional_groups key (namer.py:1821) or it silently no-ops; for a class with no neutral FG anchor return None. the Blue Book (radical>anion>cation seniority).

orthonym.rules.charged_router.emit_secondary_amine_azanide(mol, anion_idx, style)#

(the Blue Book, acetylazanide (PIN)): a DISUBSTITUTED azanide R-[N-]-R’ — a deprotonated secondary amide / sulfonamide / secondary amine named as azanide (NH2-) carrying two N-substituent prefixes. point (3) disallows the ...aminide method-(1) form for amides, so azanide IS the PIN for the acyl/sulfonyl arms; for a pure secondary-amine arm the PIN is technically ...aminide , and this azanide form is then a correct RT-verified best-effort — either way the emitted structure is exactly the input.

Names each N-arm as a substituent prefix (name_substituent_fragment), alphabetises, escalates the enclosing marks via apply_enclosing_marks), and appends azanide. RT-gated on the FULL InChIKey (_cation_name_rt_ok) — fail closed to ‘’ (never a wrong or label-dropping name). Scoped to the ACYCLIC secondary-N-anion shape: a non-aromatic N-, formal charge -1, no H, exactly two heavy neighbours that fall into DISJOINT fragments once the two N-bonds are severed (a ring bridging both arms back to N is an in-ring N-anion, a different class). A trisubstituted N- (azanediide) is out of scope here.

orthonym.rules.charged_router.emit_primary_amine_anide(mol, anion_idx, style)#

(the Blue Book, benzenaminide (PIN)): a PRIMARY amine anion R-[NH-] named on the SYSTEMATIC amine parent, converting the -amine suffix to -aminide.

The default ionize seam converts amine->``aminide`` on the re-entered NEUTRAL name; that already covers a CHAIN amine (methanaminide, ethanaminide, because the neutral is the systematic methanamine etc.). But an AROMATIC / ring primary amine re-enters as the RETAINED headline aniline – which carries no -amine suffix token to convert, and the engine never emits the systematic benzenamine (rules/benzene.py returns aniline; rules/radicals.py fails closed on exactly this gap). So build the systematic amine parent here: delete the amine N, name the remaining parent hydride (`` -> benzene), append amine (eliding the parent’s trailing e), then convert amine->``aminide``.

FULL-InChIKey RT-gated (0-wrong): a substituted / heteroatom parent whose bare (locant-omitted) amine spelling does NOT round-trip – toluenamine, pyridinamine – is REJECTED and the caller abstains. Scope: a single ACYCLIC primary-amine N (charge -1, exactly one H, exactly one heavy neighbour, a carbon) whose parent fragment (N removed) is one connected component the pipeline can name. Fail-closed (‘’) otherwise. Placed AFTER the ionize seam so it only ever converts a would-be ABSTAIN (never renames an already-valid aminide).

orthonym.rules.charged_router.emit_acylium(mol, cation_idx, style)#

(BB 41623 PIN): name an acylium cation R-C(+)=O.

Reconstructs the ACID (adds an -OH back onto the C+, restoring R-COOH), names it, then applies the acid->acylium class-keyed transform (‘carboxylic acid’->’carbonylium’, ‘oic acid’->’oylium’, ‘ic acid’-> ‘ylium’; apply_ion_suffix_to_name / _CLASS_KEYED_CATION_TRANSFORMS ['acylium']). Deliberately NOT the generic add_h_for_cation path used for a plain carbenium ylium – THAT restores an ALDEHYDE (R-CHO), not an acid, which would misname ‘acetylium’ as an aldehyde-ylium.

Mirrors _name_diazonium (sever/reconstruct, name, class-keyed transform), but ADDS an atom (the reconstructed -OH) instead of severing one. Returns ‘’ on any decline – the caller’s generic cascade then runs (add_h_for_cation there cannot mis-fire either: an aldehyde name never ends in an acid suffix, so apply_ion_suffix_to_name also returns ‘’ on that fallback path – fail-closed, never a wrong name).

orthonym.rules.charged_router.route_charged(mol, style='pin')#