orthonym.assembly.coverage_scoring#

Note

Internal API. Names and behaviour may change between releases.

Graduated confidence scoring for coverage gate candidate selection.

Replaces the binary accept/reject coverage gate (binary_accept_reject_coverage_gate) with a continuous multi-factor scoring system. Each handler (complex_ring, heterocycle, benzene) produces a CandidateName with a 0.0-1.0 confidence score derived from four factors. The best candidate is selected and returned; no computed name is ever discarded.

Four scoring factors:
  1. ratio – name-length / heavy-atom-count heuristic (normalised 0-1)

  2. atom_coverage – fraction of heavy atoms covered by the parent structure ONLY when compute_confidence is given parent_atom_indices. See the PROVENANCE WARNING below: in the production path it never is.

  3. fg_recognition – fraction of detected FGs with known naming forms

  4. substituent_completeness – fraction of substituents reflected in the name

Thread-local confidence store allows callers to retrieve metadata after assemble_name returns without changing its str return type.

PROVENANCE WARNING (C4) – factors['atom_coverage'] is NOT a coverage measurement in the production path. CandidatePool.add (candidate_pool.py, “Risk 1” mitigation) deliberately calls compute_confidence WITHOUT parent_atom_indices in order to keep confidence byte-identical, and attaches parent_atom_indices to the CandidateName only POST-HOC. Consequently atom_coverage is either

  • ratio_score – a pure name-LENGTH proxy, numerically identical to factors['ratio'] (provenance estimated_name_length), or

  • a hard 1.0 from the retained-name boost (provenance retained_name_boost).

It is a real fraction-of-atoms measurement (provenance measured) only when a caller passes parent_atom_indices – which no production caller does. Every record therefore carries coverage_provenance so that no consumer can mistake the estimate for a measurement. Do not remove that field, and do not present atom_coverage as verified coverage without checking it.

MEASURED REACH (Task Z2, 2026-08-02) – fresh process per molecule, 40 molecules, 20 of them over 25 heavy atoms:

  • ratio_raw (the name-length proxy) is computed 114 times across 29 of the 40 molecules – the hottest of the nine sites that carried this formula, and it feeds BOTH factors['ratio'] and, in production, factors['atom_coverage'].

  • composer._confidence_gate – the function whose docstring defines the accept/reject rule – recorded 0 calls; it is off the execution path entirely. candidate_pool.py Tier B (policy.tier == 'ring_b') also recorded 0. Both re-confirmed by Task Z3 (48 molecules, 0 and 0).

⚠ CORRECTION (Task Z3, 2026-08-02) – THIS RATIO DOES GATE A LIVE DECISION. Task Z2 concluded “nothing observed acted on the result” after checking those two sites. It checked the wrong two. The live consumer is ``namer.py:3810``:

atom_cov = _factors.get(‘atom_coverage’) # namer.py:3802 elif atom_cov < 0.55: # namer.py:3810

… return decomp_name # namer.py:3827

It reads factors['atom_coverage'] DIRECTLY, so it never touches confidence and neither of the two gates Z2 examined could ever have revealed it. Because that factor is ratio_score in the production path, atom_cov < 0.55 is exactly len(name) / heavy_atoms < 0.825 – a character-count gate stricter than any of the three in decomposition/engine.py, wearing the name “atom coverage”. Measured by Task Z3 over 48 molecules, fresh process each: ratio_raw computed 205 times across 32 molecules; namer.py:3810 REACHED 6 times on 6 molecules and rejected 0. So it is live and reachable, and merely happened not to fire on that sample – which is a different and much weaker statement than “nothing acts on it”.

It is still a CHARACTER COUNT standing in for coverage, and it is anti-correlated with coverage (correct cholesterol 0.393; a name that invents atoms 5.333). It is STILL left unchanged here, for the reason Z2 gave and Z3 confirms: atom_coverage carries weight 0.20 in FACTOR_WEIGHTS_V17, so altering it moves confidence and therefore candidate selection, which requires a full gate run to certify. Do not “fix” the factor without one, and do not cite factors['ratio'] or factors['atom_coverage'] as evidence about atoms. Real coverage: validation/atom_coverage.py (constitution by InChIKey skeleton) – wired into the decomposition/engine.py guards by Task Z3. Full audits: internal notes and internal notes.

orthonym.assembly.coverage_scoring.unmeasured_confidence(name='', handler='unmeasured')#

The single honest record for “nothing measured this name’s coverage”.

One shared constructor so the name path and the name_with_confidence path cannot disagree (they did: name reported handler='unknown', confidence=0.0; name_with_confidence fabricated handler='direct', confidence=1.0 with all four factors at 1.0, i.e. a perfect score on a name nothing had scored).

confidence is None – not 1.0 (a fabricated pass) and not 0.0 (a fabricated fail). factors is empty so no consumer can read an atom_coverage that was never computed.

class orthonym.assembly.coverage_scoring.CandidateName(name, handler, confidence=0.0, factors=<factory>, coverage_provenance=None, parent_atom_indices=None, parent_pcg_count=None, ring_info=None, tree=None, node_scores=None, atom_to_locant=None, is_phenol_benzene=None)#

Bases: object

A candidate IUPAC name with multi-factor confidence metadata.

name: str#
handler: str#
confidence: float | None = 0.0#
factors: Dict[str, float]#
coverage_provenance: str | None = None#
parent_atom_indices: set | None = None#
parent_pcg_count: int | None = None#
ring_info: Dict[str, Any] | None = None#
tree: NameTreeNode | None = None#
node_scores: dict | None = None#
atom_to_locant: Dict[int, int] | None = None#
is_phenol_benzene: bool | None = None#
orthonym.assembly.coverage_scoring.compute_confidence(name, handler, features, parent_atom_indices=None)#

Score a candidate name on the 4 confidence factors.

Parameters:
  • name (str) – The generated IUPAC name string.

  • handler (str) – Which handler produced this name (e.g. ‘complex_ring’).

  • features (Any) – MolecularFeatures object with perceived molecular data.

  • parent_atom_indices (Set[int] | None) – Set of atom indices covered by the parent name. When None, atom_coverage is estimated from name length.

Returns:

CandidateName with individual factor scores and aggregate confidence.

Return type:

CandidateName

orthonym.assembly.coverage_scoring.select_best_candidate(candidates)#

Select the best candidate from a list of scored candidates.

Selection criteria:
  1. Highest confidence score wins.

  2. On tie (within EPSILON=0.01): prefer more specific handler (complex_ring > heterocycle > benzene > chain).

Parameters:

candidates (List[CandidateName]) – Non-empty list of CandidateName objects.

Returns:

The CandidateName with the highest score (or best tiebreaker).

Raises:

AssertionError – If candidates list is empty.

Return type:

CandidateName

orthonym.assembly.coverage_scoring.log_confidence(candidate)#

Log confidence at structured severity levels.

  • confidence >= CONFIDENCE_HIGH: DEBUG

  • confidence >= CONFIDENCE_MEDIUM: INFO

  • confidence < CONFIDENCE_MEDIUM: WARNING

orthonym.assembly.coverage_scoring.store_confidence(candidate)#

Store confidence metadata for current naming call (top-level only).

orthonym.assembly.coverage_scoring.retrieve_confidence()#

Retrieve stored confidence metadata as a dict.

Returns a dict with keys: name, confidence, verification, factors, coverage_provenance, handler (plus atom_to_locant / is_phenol_benzene).

When no metadata has been stored the record is the shared honest “unmeasured” record (C4): confidence=None, verification='unverified', factors={}. handler stays 'unknown' for that case because the name quality gates use handler != 'unknown' as their “was anything actually scored?” guard.

confidence was previously 0.0 here. That is a fabricated FAIL – the project invariant is that insufficient evidence yields ‘unverified’, never a verdict in either direction – so it is now None. Every production consumer already guards with is not None (namer.py quality gates) or .get(..., default).

orthonym.assembly.coverage_scoring.clear_confidence()#

Clear stored confidence metadata.