orthonym.perception.metals#
Note
Internal API. Names and behaviour may change between releases.
Organometallic complex detection + hapticity perception (a phase).
This module provides RDKit-graph-based detection of metal-organometallic complexes per IUPAC 2013 Blue Book § + IUPAC Red Book § + Salzer 1999 IUPAC Recommendations.
All functions are PREDICATE-PURE per internal notes + a phase + a phase hard invariant: - NO mol mutation (no SanitizeMol, no UpdatePropertyCache, no
AssignStereochemistry, no any other RDKit mutating method).
NO module-global state read/write.
NO exception swallowing (RDKit exceptions propagate; ValueError raised by compute_hapticity is caught at the handler layer per internal notes).
The integrity test test_side_effect_inventory_is_empty parametrized
over list(StoutClass) at tests/unit/routing/test_dispatch_table.py
will auto-extend to ORGANOMETALLIC after Plan-02 ships and assert ````
for the new entry.
a phase (first scope-expansion phase per -07).
- orthonym.perception.metals.is_metal_element(symbol)#
Predicate: is the given element symbol a metal/semimetal?
PURE: read-only lookup in METAL_ELEMENT_SYMBOLS frozenset.
- class orthonym.perception.metals.LigandGroup(metal_atom_idx, ligand_atom_indices, hapticity_n, ligand_smarts_key=None, ligand_canonical_smiles=None)#
Bases:
objectA contiguous set of ligand atoms coordinated to a single metal.
- metal_atom_idx: int#
- ligand_atom_indices: Tuple[int, ...]#
- hapticity_n: int#
- ligand_smarts_key: str | None = None#
- ligand_canonical_smiles: str | None = None#
- class orthonym.perception.metals.MetalComplex(metal_atom_indices, ligand_groups, formal_charges, is_multimetal)#
Bases:
objectA complete metal-organometallic complex: one or more metals + ligands.
- metal_atom_indices: Tuple[int, ...]#
- ligand_groups: Tuple[LigandGroup, ...]#
- formal_charges: Tuple[int, ...]#
- is_multimetal: bool#
- class orthonym.perception.metals.MetallacycleInfo(metal_atom_idx, ring_atom_indices, exocyclic_atom_indices)#
Bases:
objectW8-P9 Task 9.5: a metal atom that is itself a RING member.
ring_atom_indiceslists the ring atoms in TRAVERSAL order starting at the metal (index 0), in one of the two possible ring-walk directions (the assembler tries both and picks the one giving the lowest locants).exocyclic_atom_indicesis one tuple of atom indices per exocyclic substituent group hanging directly off the metal (e.g. the 2 Cl atoms on Pt in Cl[Pt]1(Cl)C(C)=C(C)C(C)=C1C).- metal_atom_idx: int#
- ring_atom_indices: Tuple[int, ...]#
- exocyclic_atom_indices: Tuple[Tuple[int, ...], ...]#
- orthonym.perception.metals.detect_metal_complex(mol)#
Detect any organometallic complex in the given mol.
Plan-03 Tier-1 implementation: detects dot-separated metallocene topology (1 metal cation + 2 anionic Cp rings = sandwich complex).
Plan-03 commits 03-02/03/04 extend with Tier-3 (σ-bonded single-component main-group), Tier-2 (dot-separated metal carbonyls), and Tier-4 (mixed η-bonded / half-sandwich) topology branches.
PURE per internal notes: no mol mutation; only reads via GetAtoms / GetBonds / GetSymbol / GetFormalCharge / GetBondType / GetIsAromatic / GetOtherAtomIdx / GetBondTypeAsDouble / GetAtomWithIdx / GetMolFrags. NEVER: SanitizeMol, UpdatePropertyCache, AssignStereochemistry.
- orthonym.perception.metals.compute_hapticity(mol, metal_atom_idx, ligand_atom_indices)#
Compute the hapticity of a ligand group bound to a metal.
Plan-03 implementation per internal notes hybrid: 1. SMARTS-template fast path: consult LIGAND_ETA_DEFAULTS from
data/organometallics.py; on hit, return catalog value.
Graph-walk fallback (added in 03-04 Tier-4 commit): BFS over ligand_atom_indices counting contiguous π-system atoms.
Edge case σ-bonded (single atom or no π-system): return 1.
PURE per internal notes.
- orthonym.perception.metals.enumerate_metal_ligand_groups(mol)#
For each metal atom in mol, partition coordinated atoms into ligand groups.
STUB BODY (Plan-02): returns always. Plan-03 implements per RESEARCH lines 549-567.
Algorithm (Plan-03 implementation): 1. Identify all metal atoms via is_metal_element(atom.GetSymbol). 2. For each metal: collect bond-distance-1 neighbors (ligand-shell). 3. For each ligand-shell atom: BFS over non-metal bonds to find
contiguous ligand component.
Each component → LigandGroup with hapticity from compute_hapticity.
PURE per internal notes. O(V+E) per metal.
- orthonym.perception.metals.has_covalent_metal_carbon_bond(mol)#
W8-P9 Task 9.2: True iff a TRUE metal atom (see
_TRUE_METAL_SYMBOLS_FOR_VETO) shares a DIRECT bond with a carbon atom (same connected fragment) — the structural definition of a organometallic compound (BB: “Organometallic compounds are compounds having at least one bond between one metal atom and one carbon atom”), scoped to metals that have NO legitimate alternate substitutive nomenclature system in this codebase.Deliberately excludes dot-separated ionic topologies (e.g. ferrocene [Fe+2].c1cc[cH-]c1.c1cc[cH-]c1): RDKit
GetBondsonly enumerates bonds that exist in the graph, so a metal cation and an anionic Cp ring in SEPARATE fragments (no bond between them) correctly return False here — those compounds legitimately cascade to SALT@100 (ionic complex, no covalent M-C bond) and must keep doing so.Also deliberately excludes Group 13-16 metalloids (Si/Ge/Sn/Pb/B/As/Sb/ Bi/Ga/In/Tl/Te/Po) even though they ARE metals/semimetals per
is_metal_element— those have a legitimate alternate substitutive path / MONONUCLEAR_HYDRIDE / -04 heterocycle defer) that names the whole molecule via a DIFFERENT CFR class; flagging them here would false-positive-suppress correct names like ‘trimethylarsane’, ‘trimethylgallane’, or ‘1-methyl-1H-silole’.Used as a source-level (no-OPSIN-required) veto: any compound for which this returns True IS a organometallic that must be named by the organometallic handler or fail closed — never silently renamed from a decomposed sub-fragment that drops the metal (the C[Ti](Cl)(Cl)Cl -> ‘methane’ / Cl[Pt]1(Cl)… -> ‘…ole’ leaks).
PURE per internal notes: read-only GetBonds/GetBeginAtom/GetEndAtom/ GetSymbol; no mol mutation.
- orthonym.perception.metals.detect_metallacycle(mol)#
W8-P9 Task 9.5: detect a metallacycle — a metal atom (Group 2-12, per
data.organometallics.METALLACYCLE_A_PREFIX) that is itself a RING member.Scope THIS CYCLE (monocyclic, all-carbon backbone; narrow and conservative — fails closed, returns None, everywhere outside it):
exactly one metal ring atom (>1 -> polymetallic ring, out of scope);
the metal is in exactly ONE ring (fused/bridged/bicyclic metallacycles — e.g. the BB’s own titanabicyclo[3.2.0]heptane example — are EXPLICITLY deferred per the plan’s open question; a metal shared between 2+ rings returns None here);
ring size 4-8 (common metallacycle range);
every OTHER ring atom is carbon (mixed-heteroatom-backbone metallacycles, e.g. the BB’s 1-sila-2-ferracyclopentane example, are out of scope this cycle);
the metal carries a formal charge of 0.
Does NOT collide with the -04 Group-14/13 Hantzsch-Widman ring defer (
_GROUP_14_13_RING_DEFER) — that set (Si/Ge/Sn/Pb/B) is disjoint fromMETALLACYCLE_A_PREFIX(Group 2-12) by construction.PURE per internal notes: read-only RingInfo/GetAtoms/GetBonds/GetNeighbors; no mol mutation.