orthonym.assembly.candidate_pool#

Note

Internal API. Names and behaviour may change between releases.

CandidatePool — a phase behavior-preserving extraction of composer.py handler-cascade dispatch.

PURPOSE#

a phase (this module): factors out the in-line handler cascade in composer.assemble_name into an explicit pool. Pool runs in selection_mode=’first_applicable’ which reproduces the current sequential ‘if applies: return’ flow bit-for-bit. SHIP GATE: byte-identical generated name strings on baseline_v17_all_corpora.csv (7,500 rows).

a phase (downstream consumer) was DESIGNED to flip selection_mode to ‘score_based’, raise chain priority, set the three gate thresholds to None to delete the IUPAC-non-conformant ratio gates, and recalibrate FACTOR_WEIGHTS. NOTE (a phase SCORE-04, audit §Stale-Comment Inventory): that production flip never landed — production stays ‘first_applicable’. The RT-moving cutover is now the default-OFF ‘score_based_per_substring’ mode, deferred to a downstream documented-delta phase . All four 146 changes are 1-line edits to HANDLER_POLICIES + FACTOR_WEIGHTS.

DESIGN CONTRACT (, from 145.1-internal notes): - HandlerPolicy is a dataclass with declarative gate fields. a phase

populates them from current composer.py constants. a phase sets gates to None and flips chain priority.

  • HANDLER_POLICIES is a module-level dict, single source of truth for handler tier/priority/gate metadata. Pulls priorities from coverage_scoring.HANDLER_PRIORITY to avoid divergence ( remediation: Plan 02 Task 2 extends HANDLER_PRIORITY with n_oxide, amine, simple_molecule entries so EVERY priority pulls from a single source — no hardcoded literals in this dict).

  • CandidatePool runs in ‘first_applicable’ mode for 145.1: pool.best returns self._candidates[0] (first-added wins). Equivalent to the sequential cascade in composer.py:1183-1338 when handlers add candidates in current dispatch order.

  • Thread-local _pool_store mirrors coverage_scoring._confidence_store. Lifecycle: clear_pool at assemble_name prologue (Plan 03 wires it next to existing clear_confidence at composer.py:674).

BYTE-IDENTICAL RISK MITIGATIONS (RESEARCH): - Risk 1: pool.add must NOT pass parent_atom_indices into

compute_confidence (would change atom_coverage). Set field POST-HOC on the returned CandidateName instead.

  • Risk 2: parent_correctness in FACTOR_WEIGHTS must be inserted at LAST position (Plan 02). Mid-position changes float summation order.

  • Risk 3: store_confidence(pool.best) and log_confidence(pool.best) must be preserved at the new return site so name_with_confidence keeps working (Plan 03 enforces this).

PERFORMANCE (remediation): - ParentCorrectnessScorer is bound at MODULE LOAD via try/except

ImportError (single binding, not re-imported per call). pool.add references the module-level binding directly. Eliminates the 7,500x per-call import overhead that the original per-call try-import would have incurred during full byte-identical runs.

class orthonym.assembly.candidate_pool.HandlerPolicy(handler_id, tier, priority, direct_return=False)#

Bases: object

Declarative policy for one handler’s pool participation.

Fields:

handler_id: Handler name (matches HANDLER_PRIORITY keys). tier: ‘ring_a’ | ‘ring_b’ | ‘chain’ | ‘direct_return’. priority: Tiebreak priority (pulled from HANDLER_PRIORITY). direct_return: True for Tier B and direct-return handlers

(first-applicable wins); False for Tier A pool handlers (compete via select_best_candidate) and chain (now first-class peer of ring_a per a phase).

a phase: cascade_ratio_min, min_ratio_accept, gate_threshold fields REMOVED. Cascade competition is now handled by _best_two_tier (two-tier selector) and the inline V17 gate at composer.py preserves byte-identical V17 soak behavior. Tier B handlers no longer carry a per-handler gate threshold; the confidence gate is enforced by the handler-internal call to _confidence_gate against CONFIDENCE_GATE_THRESHOLD.

handler_id: str#
tier: str#
priority: int#
direct_return: bool = False#
class orthonym.assembly.candidate_pool.CandidatePool(selection_mode='first_applicable')#

Bases: object

Holds scored candidate names from one assemble_name invocation.

a phase mode: ‘first_applicable’ (literal re-encoding of current handler-cascade in composer.py:assemble_name). Pool.best returns self._candidates[0] (first-added wins). Equivalent to the sequential ‘if applies: return’ cascade.

a phase mode: ‘score_based’ (delegates to select_best_candidate over full pool). Enables real chain-vs-ring competition.

SCORING TIMING : parent_correctness factor is computed inline in pool.add via the module-level ParentCorrectnessScorer binding. In 145.1, FACTOR_WEIGHTS[‘parent_correctness’] = 0.0 ensures the factor is recorded but does NOT influence cand.confidence (IEEE 754: x+0.0=x).

Tier B gate semantics (preserves _confidence_gate at composer.py:279):

tier=’ring_b’ handlers → if cand.confidence < CONFIDENCE_GATE_THRESHOLD (0.40), pool.add returns None (gate-rejected); caller falls through to next handler. Post a phase, the gate threshold is read from module-level CONFIDENCE_GATE_THRESHOLD (not the deleted policy.gate_threshold field).

Direct-return handlers: pool.add ALSO sets self._direct_return_winner to short-circuit pool.best (: store all + early-exit flag — saves work; both byte-identical-equivalent in first_applicable mode).

add(name, handler_id, features, parent_atom_indices=None, ring_info=None, tree=None, coverage_measured=False)#

Score and add a candidate. Returns the candidate, or None if gate-rejected (Tier B handlers only).

BYTE-IDENTICAL CONTRACT (RESEARCH Risk 1):

parent_atom_indices is set POST-HOC on the returned CandidateName (NOT passed into compute_confidence). This guarantees confidence is byte-identical to current composer.py code.

Parameters:
  • name (str) – The IUPAC name produced by the handler.

  • handler_id (str) – Key into HANDLER_POLICIES. Unknown handler_id is accepted with no policy (no gate, no direct-return flag).

  • features (Any) – MolecularFeatures (passed through to compute_confidence and to ParentCorrectnessScorer).

  • parent_atom_indices (Set[int] | None) – Optional set of atom indices comprising the parent structure. Used by ParentCorrectnessScorer; does NOT affect compute_confidence (Risk 1).

  • coverage_measured (bool) – True when the producer MEASURED a complete atom partition for name (every heavy atom bound to one of its fragments; _w2_atom_coverage_verdict). The RATIO_REJECT_FLOOR is a character-count stand-in for coverage, so it is skipped then: numerical-term chain names are short by design (‘octacontane’, ratio 0.092). Does not affect the confidence.

Returns:

The added CandidateName on success. None if Tier B gate rejected (cand.confidence < CONFIDENCE_GATE_THRESHOLD).

Return type:

CandidateName | None

best()#

Select winning candidate per selection_mode.

BYTE-IDENTICAL CONTRACT (a phase drift fix, 2026-04-23):

In selection_mode=’first_applicable’, the original (pre-Plan-01) composer.py used an if X applies: return X cascade where each direct-return handler short-circuited the entire dispatch. The Plan-03 refactor preserved this intent by tracking _direct_return_winner (set by add when a direct_return=True handler fires), but the original Plan 01 implementation of best ignored this field and just returned _candidates[0]. This caused a byte-identical drift on CHEBI:85380 (COc1cc…c4ccc1c2c43): the Tier A complex_ring handler (direct_return=False) added 1-methoxypyrene first, then the polycyclic handler (direct_return=True) added the correct 2-methoxypyrene, but best returned _candidates[0] = the wrong-locant complex_ring candidate.

FIX: when a direct-return handler has fired, return ITS candidate (matches the pre-Plan-01 “if X applies: return X” semantics). Otherwise, return the first added candidate (Tier A subset compete-by-position for first_applicable).

a phase’s planned flip to ‘score_based’ as the production default never landed (a phase SCORE-04, audit §Stale-Comment Inventory): production stays ‘first_applicable’. The default-OFF ‘score_based_per_substring’ mode (the deferred cutover) competes the full candidate set via _best_two_tier(per_substring=True) -> select_best_candidate.

all_candidates()#

Return all collected candidates (for logging/diagnostics).

orthonym.assembly.candidate_pool.push_pool(features=None)#

Push a fresh pool onto the per-thread stack and return it.

Called by assemble_name prologue (composer.py). MUST be paired with pop_pool in a finally clause so the pool is popped on every exit path (normal return, exception, early return statements).

a phase: selection_mode defaults to the ORTHONYM_SELECTION_MODE env var (default ‘first_applicable’ for V17 byte-identical soak). Set ORTHONYM_SELECTION_MODE=score_based to activate the two-tier selector end-to-end without a code revert.

(code review 2026-05-30): when features is provided, the a phase

controller flag/oracle are bound NOW (construction) off that authoritative per-call features object, instead of being lifted lazily off the first add call. features=None (get_current_pool auto-push, tests) leaves the pool’s flag at its safe default and the add fallback binds it later.

Returns the new pool that is now the active cascade scope for the currently-executing assemble_name call.

orthonym.assembly.candidate_pool.pop_pool()#

Pop the top pool from the per-thread stack.

Called by assemble_name epilogue (composer.py, in finally clause). Safe to call when the stack is empty (no-op) — defensive against any code path that pops without a matching push.

orthonym.assembly.candidate_pool.get_current_pool()#

Return the top of the per-thread pool stack — the active cascade for the currently-executing assemble_name call.

Auto-pushes a fresh pool if the stack is empty. This covers two cases: (1) a handler called get_current_pool outside an assemble_name scope (e.g., from a unit test that invokes pool.add directly), and (2) backward compatibility with any legacy caller that expected the pre-fix lazy-init behavior.

orthonym.assembly.candidate_pool.clear_pool(features=None)#

BACKWARD-COMPAT: replace the top of the stack with a fresh pool.

Pre-fix code (Plan 03) called clear_pool in assemble_name’s prologue to reset state. Post-fix, push_pool is the right primitive (the wrapping try/finally handles pop). This alias preserves the call-site signature so any handler still calling clear_pool directly does not break.

Behavior: if the stack is empty, push a fresh pool (same as pre-fix lazy init); if non-empty, replace the top so the current cascade restarts cleanly without affecting outer cascades on the stack.

(code review 2026-05-30): binds the a phase controller flag/oracle

off features at construction (see push_pool) when provided.

orthonym.assembly.candidate_pool.compute_chain_candidate(features, style='iupac')#

Compute a chain-handler candidate from features, or None if infeasible.

Per: idempotent. Called from composer.py’s Tier A integration point UNCONDITIONALLY when V18 mode is active (selection_mode=’score_based’). In V17 mode (selection_mode=’first_applicable’), the existing chain naming path still runs only when ring handlers have all failed — this helper’s output is ignored because Tier A direct-return ring handlers short-circuit pool.best in first_applicable dispatch.

The function defers actual chain-name construction to composer.py’s existing chain machinery. When a usable chain-naming entry point is not importable (e.g., during isolated unit tests of this module), the function returns None safely — chain never becomes a competing candidate, so ring handlers continue to win by default.

Parameters:
  • features (Any) – MolecularFeatures with principal_chain populated.

  • style (str) – naming style (‘iupac’ / ‘pin’), forwarded to chain namer.

Returns:

CandidateName with handler=’chain’ and parent_atom_indices set, OR None if the chain pipeline cannot produce a viable name for these features.

Return type:

CandidateName | None

Source: https://iupac.qmul.ac.uk/BlueBook/P4.html