orthonym.assembly.locant_omission#

Note

Internal API. Names and behaviour may change between releases.

– omission of locants. The ONE place the licences are decided.

Phase C tranche B. Pure module: RDKit mols in, booleans out. No I/O, no naming.

⚠ DENY BY DEFAULT. ```` “Citation of locants” (the Blue Book Blue Book) is the rule:

“In preferred IUPAC names, if any locants are essential for defining the structure

of the parent structure or of a unit of structure as defined by its appropriate enclosing marks, then all locants must be cited for the parent structure or that structural unit.”

```` (:2871) then grants narrow licences, and its own preamble says “for absolute clarity in preferred IUPAC names it is necessary to be prescriptive about when omission of locants is permissible.” So:

  • never write a locant-stripping pass – every omission is a positively-licensed structural predicate;

  • fail toward retaining the locant – anything this module cannot positively establish returns False (or True from:func:scope_forces_locants, which is the same direction);

  • a licence is evaluated per enclosing-mark scope. One essential locant anywhere in a scope restores every locant in that scope – which is why 1-chloro-2-(pentafluoroethyl)benzene elides inside the parentheses and keeps 1,2 outside.

★ THE SELF-VALIDATING BOUNDARY PAIR – the reason these predicates count HYDROGENS, never positions:

:7625 benzenehexol (PIN, (not benzenehexaol) -> OMITS
:54823 "Inositols, cyclohexane-1,2,3,4,5,6-hexols, are a specific
          group of cyclitols." -> RETAINS

Same six OH, same ring size. A benzene ring carbon has ONE substitutable H, so six OH completely substitutes the ring and ```` fires. A cyclohexane ring carbon has TWO, so six OH is partial substitution and the :3009 counter-clause restores every locant. A predicate that counts positions gets this pair wrong.

The class is OPEN (ring size, ring element and substituent identity are all unbounded), so a finite table of names or spellings would be wrong on its complement by construction. Structural predicates only.

orthonym.assembly.locant_omission.CHALCOGENS = frozenset({'O', 'S', 'Se', 'Te'})#

:3007 – “Except for hydrogen atoms attached to chalcogen atoms, such as in acids, alcohols,…”. Group 16: O, S, Se, Te (Po is not a nomenclature case).

orthonym.assembly.locant_omission.substitutable_h_count(mol, idx)#

How many hydrogens on atom idx are substitutable per :3007.

Zero for a hydrogen on a chalcogen (acid/alcohol/thiol OH, SH, SeH, TeH) and zero for the H of an aldehyde formyl carbon; otherwise the atom’s total H count.

Returns 0 – never raises – for a missing mol or an out-of-range index, so a caller that fails to establish the structure gets the deny-by-default answer.

orthonym.assembly.locant_omission.substitutable_positions(mol)#

:3007. Atom indices carrying at least one substitutable hydrogen.

orthonym.assembly.locant_omission.l5_uniform_complete(mol, *, decoration_of, counts=None)#

```` (:3007) plus its counter-clause (:3009).

“All locants are omitted in compounds or substituent groups in which all

substitutable positions are completely substituted or modified, for example, by hydro, in the same way.”

“In case of partial substitution or modification, all numerical prefixes must

be indicated.”

mol is the parent hydride / parent compound, not the decorated molecule – the rule speaks of its substitutable positions. decoration_of maps an atom index of that parent to a decoration key (the prefix or suffix morpheme).

True iff every substitutable position of mol has all of its substitutable hydrogens decorated, and every decoration is the SAME KIND.

counts is an optional per-atom decoration multiplicity, defaulting to 1. It exists because decoration_of maps one key per atom and cannot otherwise express a doubly decorated position – heptafluorobutanoic acid (:3017) puts two F on each of two CH2 carbons. Omitting it means “one decoration per atom”, so a position with two substitutable hydrogens is partial and the licence is denied: that is exactly the cyclohexane-1,2,3,4,5,6-hexols boundary (:54823).

Decorations at NON-substitutable positions are permitted and still count toward the “in the same way” test – decahydronaphthalene (:3013) hydrogenates the two bridgeheads, which carry no substitutable H of their own.

orthonym.assembly.locant_omission.l3_one_kind_of_substitutable_h(mol)#

```` (:2939).

“The locant is omitted in monosubstituted symmetrical parent hydrides or parent

compounds where there is only one kind of substitutable hydrogen.”

True iff every substitutable hydrogen of mol lies in ONE CanonicalRankAtoms(breakTies=False) orbit – benzene, cyclohexane, urea (:2943 methylurea), pyrazine (:2949 pyrazinecarboxylic acid).

This speaks only about the parent. The caller must separately establish that the substitution really is mono and that nothing else in the scope forces locants (scope_forces_locants()).

orthonym.assembly.locant_omission.l4_no_isomer_by_relocation(mol, parent_atoms, *, prefix_locants, suffix_locants, stereo_text, has_indicated_h, has_isotope, is_multiplicative=False, is_ring_assembly=False, has_skeletal_replacement=False)#

§**** (:2953) – the ISOMER-COUNT licence.

“Locants are omitted when no isomer can be generated by moving suffixes

and/or prefixes (if any) from their position to another or by interchanging them between two different positions.”

True => every locant in this scope is omitted.

mol is the DECORATED molecule and parent_atoms the atom indices of the parent hydride / parent compound within it; the decorations are then read off structurally (_l4_components()) rather than taken on trust.

The test is exactly the rule’s own two operations, run to exhaustion: place the decoration multiset over every position of the parent that bears a hydrogen, in every way the positions’ hydrogen counts allow, and compare the resulting constitutions. One distinct constitution => no isomer can be generated => omit.

⚠⚠ ``substitutable_positions`` IS DELIBERATELY NOT REUSED HERE, and that is the single most important line of this function. That helper implements :3007’s carve-out (“Except for hydrogen atoms attached to chalcogen atoms, such as in acids, alcohols, and to the carbon atoms of formyl groups”), which is a sentence of **** and has no counterpart in – this rule counts ISOMERS, and says nothing whatever about which hydrogens are “substitutable”. Routing through it would make the whole polysulfane family deny by construction: trisulfane is HS-S-SH, so EVERY hydrogen it has is on a chalcogen, substitutable_positions is empty, and l3_one_kind_of_substitutable_h /l5_uniform_complete() / l6_all_substitutable_h_share_one_locant() therefore all deny – yet :39335 prints CH3-S-S-SH methyltrisulfane (PIN) under §**** “Compounds with three or more contiguous identical chalcogen atoms are treated as parent hydrides in substitutive nomenclature”. Positions here are those bearing a hydrogen in the ORDINARY sense, and the reason no isomer exists for methyltrisulfane is a hydrogen COUNT, not a carve-out: the middle sulfur bears zero hydrogens, so S1/S3 are the only placements and they are one orbit. (⚠ And the :3007 carve-out must NOT be loosened to make this work: it is load-bearing for, where propanedioic acid’s two acid O-H must not count or chloropropanedioic acid (:2951) loses its licence.)

Derived against every example printed in the rule’s own block, positives and negatives, which is what fixes the two design points a simpler reading misses:

So single-orbit equivalence is NOT the test on either flank: it is too weak for a heterogeneous multiset (:2995) and too narrow to notice co-location (:2999).

⚠ The four spellings of :3005 – “As an exception the locant is not omitted from propan-2-one, butan-2-one, prop-2-enoic acid and prop-2-ynoic acid although unambiguous without a locant” – are exceptions to THIS licence (that sentence closes ‘s block). They are NOT enumerated here, deliberately: the set is demonstrably OPEN – prop-2-enamide (PIN) (:32724), ...prop-2-enenitrile (PIN) (:46790) and prop-2-enoyl (preferred prefix) (:30570) are all equally unambiguous without a locant and all printed with one. (prop-2-enal’s locanted spelling appears at :35068 only inside a [not 2-butylprop-2-enal...] clause that rejects the name on a different ground – parent selection – so it evidences the SPELLING, not a PIN endorsement; the three above carry the tag themselves.) A four-row table would therefore be wrong on its complement by construction. They stay correct because nothing routes an unsaturated chain suffix through this licence; a caller that ever does must handle the class, not the four names.

Deny-by-default, per this module’s docstring. It declines on all THREE of the things ARCH-a-licence-can-be-evaluated-on-the-wrong-molecule.md requires of a licence:locants_are_forced() and scope_has_isotopic_modification() here, and the fragment-boundary observation at its call site (kept in handlers/_handler_shared.py so this module stays a pure leaf). It also denies on any defined stereochemistry, since relocating a bond cannot be shown to preserve a descriptor.

orthonym.assembly.locant_omission.l3_monosubstituted_locant_omitted(parent, *, n_substitutions, prefix_locants, suffix_locants, parent_cites_locants, stereo_text, has_indicated_h, has_isotope, is_multiplicative=False, is_ring_assembly=False, has_skeletal_replacement=False)#

§**** (:2939) – the WHOLE licence, for ONE substitution.

“The locant is omitted in monosubstituted symmetrical parent hydrides or

parent compounds where there is only one kind of substitutable hydrogen.”

True => that single locant is omitted. The orbit test itself is NOT re-derived here: it is:func:l3_one_kind_of_substitutable_h, the one place the licence lives. This function adds the three things the orbit test deliberately leaves to its caller (its own docstring says so): that the substitution really is mono, that nothing else in the scope forces locants, and the two ambient scopes.

parent is the parent hydride or parent compound – the molecule with the ONE substitution REMOVED – because that is what the rule’s own examples measure:

★ The boundary the whole task turns on, and it falls out of :3007 with no special case: propanedioic acid’s two acid O-H are on a chalcogen and are NOT substitutable, leaving C2 as the only kind, so the licence fires. Propane**diamide** has C2 and two amide N-H – neither a chalcogen H nor a formyl H, so they count – giving two kinds, so it is denied and 2-methylpropanediamide (:2887) keeps its locant. :2889 N1,N3-dimethylpropanediamide (PIN) proves independently that an amide N-H is substitutable. ⚠ Do NOT “fix” the chalcogen exclusion to make trisulfane work: it is load-bearing HERE, and methyltrisulfane is licensed by a different sub-rule, unimplemented) – see internal notes.

⚠ This licence is orthogonal to (c) (_ring_suffix_locant_is_trivial), which is restricted to saturated all-carbon monocycles and therefore cannot reach a heteroarene. L3 is what licenses pyrazinecarboxylic acid while piperidine-1-carbonitrile (:34730) correctly keeps its locant – piperidine’s N-H, C2/C6, C3/C5 and C4 are FOUR orbits. Neither rule may be widened into the other; the orbit predicate is the only thing separating those two rings.

Parameters:
  • parent – the parent hydride / parent compound, substitution removed.

  • n_substitutions – how many decorations sit on parent. The rule says “monosubstituted”, so anything but exactly 1 denies. Passed separately from the locant lists because a decoration whose locant was already elided upstream contributes no locant, and counting locants alone would read a disubstituted parent as mono.

  • prefix_locants – locants of substituent prefixes in this scope.

  • suffix_locants – locants of suffixes in this scope.

  • parent_cites_locants – True when the parent name itself already cites a locant – a heteroatom locant set (1,4-dioxane), an added/indicated hydrogen, or an unsaturation locant. ```` (:2869) then restores every locant in the scope, so the licence declines. Every one of the Blue Book’s five printed L3 positives has a locant-free parent name (pyrazine, urea, disiloxane, coronene, propanedioic acid), so this is the deny-by-default side of a boundary the source does not print, and it is recorded as such rather than as a verified rule.

orthonym.assembly.locant_omission.l3_locant_omitted_for_parent_atoms(mol, parent_atoms, *, prefix_locants, suffix_locants, parent_cites_locants, stereo_text, is_multiplicative=False, is_ring_assembly=False, has_skeletal_replacement=False)#

§**** (:2939) where the parent is given as an ATOM SET.

The one entry point both live call sites use. It performs the parent surgery – delete every atom outside parent_atoms, letting RDKit restore the implicit hydrogens that the substituent had displaced – and derives the two arguments a caller must not be trusted with:

  • n_substitutions, proven by:func:_one_substituent_removed;

  • has_isotope, read off the real (undeleted) molecule, because the parent copy may not carry the label.

Returns False – retain the locant – on any failure to establish the structure, including a sanitization failure on the reconstructed parent.

⚠ parent_atoms is the parent hydride or compound in the rule’s sense: for pyrazinecarboxylic acid it is the six RING atoms only (the -carboxylic acid carbon is the substitution), while for chloropropanedioic acid it is the chain plus both -COOH groups (the chloro is the substitution). Getting that boundary wrong silently changes which molecule the orbit test measures.

orthonym.assembly.locant_omission.l6_all_substitutable_h_share_one_locant(mol, atom_to_locant)#

```` (:3031).

“All locants are omitted for parent compounds when all substitutable hydrogen

atoms have the same locant.”

Example :3037 difluoroacetic acid (PIN) (not 2,2-difluoroacetic acid) – acetic acid’s only substitutable hydrogens are the three on C-2, because the acid OH is a chalcogen H excluded by :3007.

Fail-closed: a substitutable atom missing from atom_to_locant denies, because its locant cannot be shown to coincide.

orthonym.assembly.locant_omission.l6_chain_parent_one_substitutable_atom(mol, chain_atoms, group_atoms=())#

```` (:3031) for an ACYCLIC parent compound named by a producer from its chain and the atoms of the characteristic groups its name expresses (the acid / acyl halide / thioacid / peroxoic acid group, the anhydride or ester oxygen): “All locants are omitted for parent compounds when all substitutable hydrogen atoms have the same locant.” – ‘cyanoacetyl chloride (PIN)’ (:5112), ‘amino(oxo)ethaneperoxoic acid (PIN)’ (:30182), ‘S-methyl (ethylsulfanyl) (sulfanylidene)ethanethioate (PIN)’ (:32019); ‘acetamide’ keeps ‘2-’ because its N-H are substitutable too (’N-carbamoyl-2-phenylacetamide (PIN)’,:33364).

The UNSUBSTITUTED parent compound is rebuilt on the graph: an atom of the chain, or a group atom bonded to the chain, has as many substitutable hydrogens as it carries now plus the bonds it has to atoms outside the chain and the groups (its substituents); hydrogens on chalcogens and on an aldehyde formyl carbon are not substitutable (:3007). True only when exactly one atom has any.

Deny-by-default, :2869): False under:func:locants_are_forced, in an isotopically modified scope, with an isotope or a stereodescriptor on the chain, for a ring atom on the chain, or on any error.

orthonym.assembly.locant_omission.l6_ring_parent_one_substitutable_atom(mol, ring_atoms)#

```` (:3031) for a RING parent hydride named with its substituent prefixes only (no suffix): the unsubstituted ring’s substitutable hydrogens all sit on one ring atom – ‘chlorotrioxetane (PIN) (not 4-chloro-1,2,3-trioxetane)’ (:3187), ‘dichlorotrioxetane (PIN)’ (:3029). A ring atom’s substitutable hydrogens are its hydrogens plus its bonds to atoms outside the ring; hydrogens on chalcogens are not substitutable (:3007). Deny-by-default: forced locants, an isotopic scope, an isotope or a stereo mark on the ring, a charge, or any error.

orthonym.assembly.locant_omission.mononuclear_parent_locant_required(features)#

True when a substituent on a ONE-ATOM chain parent must cite its locant ‘1’.

  1. (the Blue Book) omits the locant ‘1’ “in substituted

mononuclear parent hydrides” (‘chloromethane (PIN)’) – parent hydrides only; a parent with a suffix (‘methanamine’, a functionalized parent hydride,

4770) is decided by (:2957):

“Locants are omitted when

no isomer can be generated by moving suffixes and/or prefixes (if any) from their position to another or by interchanging them between two different positions”; (:7304) “Locants are required for related compounds where additional substitutable positions are available, for example acetamide”. The positions of the parent compound are the carbon and the suffix nitrogen(s):

  • a C-prefix could move to a suffix N that holds a hydrogen – ‘1-hydrazinylmethanamine (PIN)’ (:38535);

  • an N-prefix could move to the carbon when the carbon holds a hydrogen and the prefix keeps the parent (_movable_to_parent_carbon) – ‘(1E)-1-[…]-N-[…]methanimine (PIN)’ (:50452), ‘N,1-bis(4-chlorophenyl)- methanimine (PIN)’ (:26524); an acyclic-carbon N-substituent would change the parent, so ‘cyano-N,N-dimethylmethanamine N-oxide (PIN)’ (:36580) omits;

  • with every position substituted nothing can move, and (:3007) omits (‘(Z)-N-hydroxy(4-chlorophenyl)(phenyl)methanimine (PIN)’,:47666).

Hydrogens on a chalcogen are not substitutable, so ‘chloromethanol’ keeps its omission. Hydrogens are read with their graph neighbours, so an explicit [H] that carries a double-bond descriptor counts. Carbon atoms of a group match are the substituents’ attachment atoms (a ketone match spans both flanking carbons), not positions of the parent; ring atoms belong to a ring substituent. (The carbon test is defensive and mutation-surviving today: every flanking carbon found on a one-carbon parent was a ring atom, which the ring test already skips – ‘cyclohexyl(phenyl)methanone’ is the witness for the pair.) Deny-by-default: anything not established returns False, which keeps the existing omission.

orthonym.assembly.locant_omission.suffix_nitrogen_hydrogens_are_caps(movable=(True,))#

Declare that hydrogens of the suffix nitrogen in this naming are caps for substituents the caller adds back; movable holds one flag per cap (see the comment above).

orthonym.assembly.locant_omission.scope_forces_locants(*, prefix_locants, suffix_locants, stereo_text, has_indicated_h, has_isotope, is_multiplicative, is_ring_assembly, has_skeletal_replacement)#

```` (:2869) – the deny-default itself.

True => cite EVERY locant in this scope, licence or not.

Plain numeric locants alone never force: those are exactly what a ```` licence is permitted to omit. What forces is anything essential in the same scope – a letter or primed locant, a stereodescriptor that needs a locant, or one of the hard overrides of derivation Part C (indicated/added hydrogen; isotopic labels – :44180 §”” “if isotopic modification requires a locant to specify its position”; multiplicative names; ring assemblies; skeletal replacement – :6446 §””, whose sentence “Once a structure modified by skeletal replacement (‘a’) prefixes has been named and numbered, it is considered to be a new parent hydride. As locants assigned to heteroatoms are essential, all locants must be cited as defined in “ is the override; the same line opens with the unrelated element-seniority order, F > Cl > Br > ..., but that is not the clause being cited here).

Fail-closed: any argument that is None – i.e. the caller could not establish it – returns True.

orthonym.assembly.locant_omission.forced_locant_scope(reason)#

Declare that this naming scope contains an essential locant (````).

Every licence must consult:func:locants_are_forced and decline while this is active. reason is free text for debugging (e.g. "isotope"); it is never parsed.

orthonym.assembly.locant_omission.locants_are_forced()#

True when an enclosing:func:forced_locant_scope is active.

A licence that does not consult this will silently elide a locant the Blue Book requires, and neither (namer.py states verbatim that it “ignores isotopes”) nor the gold set can see it.

orthonym.assembly.locant_omission.forced_locant_reason()#

The active reason, or None. Diagnostics only.

orthonym.assembly.locant_omission.isotopic_naming_scope(reason='isotope')#

Declare that an isotopic descriptor will be spliced into this naming scope.

Entered UNCONDITIONALLY by rules/isotopes.py around its skeleton naming, because the labels are stripped before naming and are therefore invisible to every structural test further down.

orthonym.assembly.locant_omission.scope_has_isotopic_modification()#

True when an enclosing:func:isotopic_naming_scope is active.

A licence that would leave a scope with ZERO locants must decline on this , :44180). A licence whose locant-free form stays correct under

(all candidate positions in one orbit) must NOT – see the comment

above for why this is separate from:func:locants_are_forced.

orthonym.assembly.locant_omission.isotope_parent_positional_scope(reason='isotope')#

Declare that the PARENT skeleton being named carries a positional isotope label. Entered by rules/isotopes.py alongside forced_locant_scope(), and only when a labelled atom sits on the parent hydride itself, so a licence that restores the parent’s OWN substituent-position locant can decline without over-citing the cases where the label sits in a substituent (see the module comment above).

orthonym.assembly.locant_omission.parent_scope_has_positional_isotope()#

True when an enclosing:func:isotope_parent_positional_scope is active.

orthonym.assembly.locant_omission.isotope_labelled_original_scope(original)#

Declare the labelled molecule whose isotope-stripped skeleton is being named (entered by rules/isotopes.py with:func:isotope_parent_positional_scope).

orthonym.assembly.locant_omission.isotope_label_on_atoms(mol, atoms)#

True when a label of the declared labelled molecule sits on one of atoms of mol (the isotope-stripped molecule being named): a labelled atom, or a labelled hydrogen bonded to it. mol is mapped onto the labelled molecule by substructure match (every match, so a symmetric parent is read the same either way). False when no labelled molecule is declared or mol does not map.