orthonym.validation#

Note

Internal API. Names and behaviour may change between releases.

Orthonym Validation Module

Tools for validating generated IUPAC names against external resolvers (OPSIN, PubChem) and measuring name quality (atom coverage).

orthonym.validation.lookup_name_pubchem(name, cache, skip_api=False)#

Look up an IUPAC name via PubChem PUG-REST API with cache.

Cache-first: if the name is in cache, the cached value is returned immediately (even if it is None, indicating a previous negative result). Only makes an API call on cache miss.

Parameters:
  • name (str) – IUPAC name to look up.

  • cache (dict) – Mutable cache dictionary (updated in-place on API call).

  • skip_api (bool) – If True, return None on cache miss without calling API. Useful for offline/test mode.

Returns:

Dict with ‘smiles’ and ‘inchi’ keys on success, or None if the name cannot be resolved. Cached negative results also return None.

Return type:

Dict[str, str] | None

orthonym.validation.load_cache(cache_path=None)#

Load PubChem lookup cache from a JSON file.

Parameters:

cache_path (Path | None) – Path to cache file. Defaults to data/pubchem_cache.json.

Returns:

Dictionary mapping IUPAC names to lookup results (or None for cached negative results). Returns empty dict if file is missing or corrupt.

Return type:

dict

orthonym.validation.save_cache(cache, cache_path=None)#

Save PubChem lookup cache to a JSON file.

Parameters:
  • cache (dict) – Dictionary mapping names to results.

  • cache_path (Path | None) – Path to cache file. Defaults to data/pubchem_cache.json.

class orthonym.validation.DualResult(opsin_parsed=False, opsin_rt=False, pubchem_resolved=False, pubchem_rt=False, combined_rt=False, opsin_smiles=None, pubchem_inchi=None)#

Bases: object

Result of dual OPSIN+PubChem validation for a single compound.

opsin_parsed: bool = False#
opsin_rt: bool = False#
pubchem_resolved: bool = False#
pubchem_rt: bool = False#
combined_rt: bool = False#
opsin_smiles: str | None = None#
pubchem_inchi: str | None = None#
orthonym.validation.validate_compound(smiles, name, opsin_jar=None, pubchem_cache=None, skip_pubchem_api=False)#

Validate a generated IUPAC name using OPSIN (primary) and PubChem (fallback).

Strategy: 1. Compute InChI of the original SMILES. 2. Try OPSIN parse-back: parse name -> compute InChI -> compare. 3. If OPSIN fails to parse, try PubChem lookup: get InChI -> compare. 4. combined_rt = opsin_rt OR pubchem_rt.

Parameters:
  • smiles (str) – Original SMILES string.

  • name (str) – Generated IUPAC name to validate.

  • opsin_jar (str | None) – Path to OPSIN JAR file (auto-detected if None).

  • pubchem_cache (dict | None) – Mutable dict used as PubChem lookup cache.

  • skip_pubchem_api (bool) – If True, use only cached PubChem results.

Returns:

DualResult with all validation fields populated.

Return type:

DualResult

orthonym.validation.validate_name_format(name)#

Validate an IUPAC name for known OPSIN-incompatible patterns.

This is a diagnostic heuristic validator . It checks for patterns known to cause OPSIN parse failures. It is NOT a full OPSIN parser reimplementation.

Parameters:

name (str) – Generated IUPAC name string.

Returns:

Tuple of (is_valid, reason). If valid, returns (True, “ok”). If invalid, returns (False, “reason_code: details”).

Return type:

Tuple[bool, str]

Checks performed:
  1. Empty name

  2. Unbalanced brackets (parentheses, square brackets, braces)

  3. Empty parentheses

  4. Bare ‘oxy’ prefix (not part of a larger word)

  5. Double hyphens – (except within VB descriptors)

  6. Multi-word name validation against OPSIN word rules

  7. Bracket nesting hierarchy verification

orthonym.validation.opsin_parse(name, jar_version='2.9.0')#

Parse an IUPAC name with OPSIN CLI JAR.

Parameters:
  • name (str) – IUPAC name to parse.

  • jar_version (str) – OPSIN version to use (default “2.9.0”).

Returns:

SMILES string if OPSIN successfully parsed the name, or None if parsing failed.

Return type:

str | None

orthonym.validation.opsin_roundtrip_check(smiles, name, jar_version='2.9.0')#

Full round-trip validation: name -> OPSIN -> SMILES -> InChI comparison.

Parameters:
  • smiles (str) – Original SMILES string.

  • name (str) – Generated IUPAC name to validate.

  • jar_version (str) – OPSIN version to use.

Returns:

Dict with keys – - passed: bool – True if InChI matches - opsin_smiles: str or None – SMILES from OPSIN - inchi_match: bool – whether InChI strings match - error: str or None – error description if failed

Return type:

dict

class orthonym.validation.OpsinGrammar(stats=None)#

Bases: object

OPSIN-grammar pre-validator with bounded round-trip-gated repair.

Three responsibilities (internal notes <domain>):
  1. validate(name) -> bool — fast OPSIN-XML-driven check across three surfaces (bracket nesting, stereo position, hyphen placement). Layered ON TOP OF a phase’s format_validator.validate_name_format heuristic pre-screen per.

  2. suggest_fix(name, source_smiles=None) — bounded repair function with the LOCKED signature (name FIRST). Each repair candidate is gated through validate AND opsin_roundtrip_check(smiles, candidate) (SMILES-first inside the oracle). One attempt per repair class; NO retry loop (/).

  3. Per-instance telemetry via self._stats and get_validation_stats.

Construction: OpsinGrammar (or OpsinGrammar(stats=shared_dict) to share counters with an outer container per ref-pass pattern).

STAT_KEYS: Tuple[str, ...] = ('validate_passed', 'repair_succeeded_bracket', 'repair_succeeded_stereo', 'repair_succeeded_hyphen', 'repair_failed_validate', 'repair_failed_roundtrip', 'no_repair_offered')#
validate(name)#

Fast OPSIN-grammar pre-validation. Returns True on pass.

suggest_fix(name, source_smiles=None)#

Bounded round-trip-gated repair (LOCKED signature).

Parameters:
  • name (str) – The (validate-failing) IUPAC name to repair. FIRST positional arg per.

  • source_smiles (str | None) – Original SMILES for the round-trip gate. When None the round-trip gate is replaced by a validate-only check and an INFO log records the degraded path .

Returns:

(repaired_name, repair_class) if a repair fired AND re-validates AND (when source_smiles is given) round- trips through OPSIN. Otherwise (None, None).

Return type:

Tuple[str | None, str | None]

Notes

  • Tries each repair class once in deterministic order [bracket, hyphen, stereo]. NO retry loop (/).

  • The round-trip oracle is called as opsin_roundtrip_check(source_smiles, candidate) — SMILES-first per. The (smiles, name) arg order to the ORACLE is the OPPOSITE of suggest_fix’s own signature; this is the most common confusion source in the codebase and the grep gate enforces it.

get_validation_stats()#

Return a defensive copy of the per-instance counters .

last_repair_class()#

Diagnostic accessor: most recent successful repair class.

orthonym.validation.opsin_grammar_validate(name)#

Module-level convenience wrapper around OpsinGrammar.validate.

Backed by an internal singleton with its own _stats counter (NOT shared with any Orthonym instance per).

orthonym.validation.opsin_grammar_suggest_fix(name, source_smiles=None)#

Module-level convenience wrapper around OpsinGrammar.suggest_fix.

Return-shape divergence: OpsinGrammar.suggest_fix returns Tuple[Optional[str], Optional[str]] (per LOCKED); this helper drops the repair-class slot and returns Optional[str] only, for backward-compatible callers that just want the repaired name. Use the class API directly when the repair class is needed.

class orthonym.validation.CoverageResult(total_heavy_atoms, claimed_atoms, unclaimed_atoms, coverage_ratio, is_complete, method, claimed_atom_indices=<factory>, unclaimed_atom_indices=<factory>, extra_atoms=0, parsed_heavy_atoms=0, constitution_match=False, input_inchikey='', parsed_inchikey='', parsed_smiles='')#

Bases: object

Result of atom coverage validation.

Variables:
  • total_heavy_atoms (int) – Heavy atom count of the input molecule.

  • claimed_atoms (int) – Input heavy atoms with a counterpart in the parse-back, counted as the size of the element-multiset intersection.

  • unclaimed_atoms (int) – Input heavy atoms with NO counterpart (atoms the name dropped) = total_heavy_atoms - claimed_atoms.

  • coverage_ratio (float) – claimed / total (0.0 to 1.0). Diagnostic only – it is NOT what decides is_complete, and by construction it cannot see atom gain (see extra_atoms) or a rearrangement (see constitution_match).

  • is_complete (bool) – True only when the parse-back has the SAME CONSTITUTION as the input, i.e. constitution_match. Never a threshold on a count.

  • method (str) – ‘parse_back’, ‘trivial’, ‘parse_back_no_inchi’, or ‘unavailable’.

  • claimed_atom_indices (Set[int]) – Indices of atoms accounted for. Empty on the parse-back path, which compares whole structures rather than mapping atom to atom.

  • unclaimed_atom_indices (Set[int]) – Indices of atoms NOT accounted for. Empty on the parse-back path, as above.

  • extra_atoms (int) – Heavy atoms present in the parse-back that the INPUT does not have – atoms the name invented. Zero for a faithful name.

  • parsed_heavy_atoms (int) – Raw heavy-atom count of the parse-back, unclamped, so a caller can recover the true count ratio (which may exceed 1).

  • constitution_match (bool) – InChIKey skeleton block of input == that of the parse-back. Fixes formula, connectivity and H layer; does not fix stereo, isotopes or charge (module docstring, DECLARED SCOPE).

  • input_inchikey (str) – Full InChIKey of the input molecule (’’ if unavailable).

  • parsed_inchikey (str) – Full InChIKey of the parse-back (’’ if unavailable).

  • parsed_smiles (str) – OPSIN’s SMILES of the parse-back (’’ if unavailable).

total_heavy_atoms: int#
claimed_atoms: int#
unclaimed_atoms: int#
coverage_ratio: float#
is_complete: bool#
method: str#
claimed_atom_indices: Set[int]#
unclaimed_atom_indices: Set[int]#
extra_atoms: int = 0#
parsed_heavy_atoms: int = 0#
constitution_match: bool = False#
input_inchikey: str = ''#
parsed_inchikey: str = ''#
parsed_smiles: str = ''#
orthonym.validation.validate_atom_coverage(mol, name, opsin_jar=None)#

Validate how completely name describes mol.

Parses name back to a molecule via OPSIN and compares that molecule to mol by CONSTITUTION (InChIKey skeleton block). Heavy-atom counts are reported as diagnostics – dropped atoms as unclaimed_atoms, invented atoms as extra_atoms – but never decide is_complete, because a count cannot distinguish the same atoms from the same number of atoms.

Parameters:
  • mol – RDKit Mol object for the input molecule.

  • name (str) – Generated IUPAC name to validate.

  • opsin_jar (str | None) – Path to OPSIN JAR. If None, attempts auto-discovery. If OPSIN is unavailable, returns a result with method=’unavailable’.

Returns:

A:class:CoverageResult with coverage metrics.

Return type:

CoverageResult