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:
ratio – name-length / heavy-atom-count heuristic (normalised 0-1)
atom_coverage – fraction of heavy atoms covered by the parent structure ONLY when
compute_confidenceis givenparent_atom_indices. See the PROVENANCE WARNING below: in the production path it never is.fg_recognition – fraction of detected FGs with known naming forms
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 tofactors['ratio'](provenanceestimated_name_length), ora hard
1.0from the retained-name boost (provenanceretained_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 BOTHfactors['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.pyTier 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
namepath and thename_with_confidencepath cannot disagree (they did:namereportedhandler='unknown', confidence=0.0;name_with_confidencefabricatedhandler='direct', confidence=1.0with all four factors at1.0, i.e. a perfect score on a name nothing had scored).confidenceisNone– not1.0(a fabricated pass) and not0.0(a fabricated fail).factorsis empty so no consumer can read anatom_coveragethat 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:
objectA 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:
- orthonym.assembly.coverage_scoring.select_best_candidate(candidates)#
Select the best candidate from a list of scored candidates.
- Selection criteria:
Highest confidence score wins.
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:
- 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={}.handlerstays'unknown'for that case because thenamequality gates usehandler != 'unknown'as their “was anything actually scored?” guard.confidencewas previously0.0here. That is a fabricated FAIL – the project invariant is that insufficient evidence yields ‘unverified’, never a verdict in either direction – so it is nowNone. Every production consumer already guards withis not None(namer.pyquality gates) or.get(..., default).
- orthonym.assembly.coverage_scoring.clear_confidence()#
Clear stored confidence metadata.