orthonym.assembly.retained_substitution#
Note
Internal API. Names and behaviour may change between releases.
a phase — Continuous Triviality Controller (P2; AUTONOM-derived tree visitor).
- Implements /02/03 via a pure functional transform
NameTreeNode -> NameTreeNode': Depth-first leaves-first traversal (Pitfall 4 avoidance: rewrite children BEFORE the parent’s swap-decision so the parent sees the post-swap children).
Per-Type runtime dispatch (;..3): Type 1 unconditional, Type 2a principal-group-bound, Type 2b SMARTS closed-list, Type 2c per-entry-override-or-Type-3, Type 3 bare-only + locant_context.
Multiplier-feedback after every swap (;): re-derive the di<->bis multiplier on the swapped node via
get_multiplier_prefix(which consultsis_complex_substituent) — never a static per-entry flag.Re-alphabetization of the node whose prefixes changed, applied when that node is re-emitted :
_alphabetize_prefixes(NOT a sibling-reorder at the swapped node’s own level — the leaves-first walk re-alphabetizes a parent when the parent is visited).runtime OPSIN-RT cache (;; memoized per
(post_swap_subtree_str, pre_swap_canon)in a plain dict). On RT mismatch: silently keep systematic form + emitControllerEvent(kind='swap_reject_rt_unsafe').
CRITICAL serializer invariant (#2 fix, reviews iter 1): the Pass-2 serializer short-circuits
on a non-None fragment_legacy (name_tree_to_string.py:96) and IGNORES node.prefixes. So
ANY rewrite that changes parent_stem (the swap) OR changes a child
(new_prefixes != node.prefixes) MUST set fragment_legacy=None, or the serializer
renders the stale legacy string and discards the rewrite. The helper
_replace_preserving_or_resetting_legacy encodes the conditional: reset when a child
changed, preserve when nothing changed.
SMILES recovery is a 3-path cascade (#3 fix, reviews iter 1): an earlier hint-based
reverse-mapping path was REMOVED — the per-atom locant hint is NOT a field of the frozen
NameTreeNode (name_tree.py:90-103); it lives on NamingResult (name_tree.py:128) and is
never forwarded onto nodes, so the lookup always returned None and that path was dead code.
Recovery therefore starts at token-match (labelled Path B for cascade continuity), then
reverse-OPSIN (Path C), then bail (Path D).
Recovery reach is bounded (#4, reviews iter 1): match_token_atoms_in_mol cannot map a
specific IR node to a specific physical duplicate fragment; for duplicate substituents it picks
a deterministic-but-arbitrary match. The recovered fragment SMILES is correct for true
duplicates (same canonical SMILES), but recovery can MISS for nested substituents — so
controller_fired_count may trail controller_reach_count even for seed members. Plan-04
reports BOTH counts (internal notes) and never claims reach the recovery cannot deliver.
Thread-safety (#6 REJECTED, reviews iter 1): the OPSIN-RT cache is a plain per-instance dict. The benchmark (benchmark_multi_corpus.py) is single-threaded over rows (max_workers=1 timeout wrapper at:191; serial rows at:446; no –threads arg) and each OpsinOracle belongs to one Orthonym instance — no lock needed.
Module boundary preserved per: this module is the ONLY new code in the assembly package; name_tree.py, name_tree_to_string.py, naming_utils.py stay UNTOUCHED.
- class orthonym.assembly.retained_substitution.ControllerEvent(kind, canonical_smiles, seed_entry_name, substitution_type, rt_oracle_result, p_section_cite)#
Bases:
objecta phase diagnostic event (RESEARCH section 4.5).
- kind: str#
- canonical_smiles: str | None#
- seed_entry_name: str | None#
- substitution_type: str | None#
- rt_oracle_result: bool | None#
- p_section_cite: str | None#
- class orthonym.assembly.retained_substitution.OpsinOracle(opsin_jar=None)#
Bases:
objectruntime OPSIN-RT cache (+ RESEARCH R-02 + Pitfall 5).
Plain per-instance dict cache keyed by
(post_swap_subtree_str, pre_swap_canon). #6 REJECTED (reviews iter 1): no lock needed — the benchmark is single-threaded over rows.- rt_safe(pre_swap_canon, post_swap_subtree_str)#
internal notes T2: parse
post_swap_subtree_strvia OPSIN, canonicalize, compare topre_swap_canon. Cached per(post_swap_subtree_str, pre_swap_canon).
- name_to_smiles(name)#
Path C reverse-OPSIN helper. Returns canonical SMILES or None on failure.
A DEFINITIVE result (a real SMILES, OPSIN’s rejection, or a canonicalisation failure on OPSIN’s output) is cached; a transient invocation failure (timeout /
OSError) is NOT cached (, code review 2026-06-02), so a one-off failure never poisons later lookups of the same name.
- name_to_opsin_smiles(name)#
OPSIN’s own SMILES of
name, byte for byte as OPSIN wrote it (-r -osmi), or None when OPSIN rejected the name or could not be consulted.name_to_smilesreturns RDKit’s canonical SMILES of this string, which is RDKit’s reading written back by RDKit; a check that must read the name’s structure as OPSIN built it with another toolkit (namer. _lone_pair_configuration_verified) needs the string itself. Shares the parse withname_to_smiles(cached on the same definitive outcomes; a transient failure is not cached).
- parse_status(name)#
Three-valued OPSIN parse outcome for the validity gate .
- Returns one of:
"parsed"— OPSIN accepted the name (emitted SMILES);"rejected"— OPSIN ran and definitively rejected it (no SMILES);"unavailable"— the parse could not be performed (no JAR / timeout /OSError).
"unavailable"is NOT a rejection: it says nothing about the name. The oldname_to_smiles is not Nonecheck collapsed the two . What a caller does with it is the caller’s decision: the validity gate treats it as “unverified” and fails CLOSED (TRIAGE g7 C01, 2026-09-27; it used to fail open and ship the unverified candidate, a wrong molecule when the candidate was wrong), after_one_shot’s retry ladder has given a slow OPSIN its chance.Only definitive outcomes are cached, so a transient failure never poisons a later lookup of the same name .
- orthonym.assembly.retained_substitution.apply_triviality_controller(tree, mol, principal_group, opsin_oracle=None, *, enabled=False)#
a phase entry point (internal notes +).
When
enabled=False: returns the input tree UNCHANGED (Stage A default-OFF invariant per).When
enabled=True+is_coarse_node(tree): returns the input tree UNCHANGED (coarse passthrough).Otherwise: depth-first leaves-first walk; per-Type dispatch; rewrite-on-match + RT-gate.