orthonym.wallclock#

Note

Internal API. Names and behaviour may change between releases.

A wall-clock limit on one call, for the tools that name many molecules.

The engine bounds its own work with deterministic operation budgets (assembly.fragment_naming.PerfBudgetExceeded) and never reads a clock; nothing under src/orthonym arms an alarm. The measurement tools (eval/harness.py, scripts/pin_conformance_eval.py, the naming-scale shard workers,…) add a per-molecule wall-clock limit on top, so that one slow molecule costs one row and not the run.

That limit must be a BaseException. The naming path holds hundreds of except Exception handlers. An Exception raised from a SIGALRM handler is caught by whichever of them is active when the alarm fires, and naming carries on from a half-finished choice: VonBaeyerAnalyzer._choose_lowest_locant_numbering keeps its incumbent numbering when its candidate search is cut off, and a molecule that abstains in every unloaded run was named in a loaded one. The outcome of a slow molecule then depends on the load of the machine, and the tool’s TIMEOUT row is never written.:class:WallClockTimeout passes every except Exception on its way out (like PerfBudgetExceeded; the package has no bare except: and no except BaseException), and the tool catches it by name.

Use:

from orthonym.wallclock import WallClockTimeout, clear_leaked_naming_state, wall_clock_limit

try:
    with wall_clock_limit(30):
        res = namer.name_tiered(smiles)
except WallClockTimeout:
    leaked = clear_leaked_naming_state # empty: the limit put the state back
    row = {"tier": "TIMEOUT",...}

Put the with inside the try: the alarm is then disarmed before the except branch runs, so it can never fire inside the tool’s own bookkeeping.

exception orthonym.wallclock.WallClockTimeout(seconds=None)#

Bases: BaseException

A tool’s wall-clock limit on one call was exceeded.

Derives from BaseException (NOT Exception) so that no except Exception on the naming path can catch it and let the cascade continue from a half-finished choice. The tool that armed the limit catches it by name.

restored names the process-global naming state the interrupted call had changed and the limit put back (see:class:wall_clock_limit).

class orthonym.wallclock.wall_clock_limit(seconds)#

Bases: object

with wall_clock_limit(seconds): raises:class:WallClockTimeout in the body once seconds of wall-clock time have passed.

  • Arms ITIMER_REAL with a SIGALRM handler that raises the timeout, and fires again every:data:REFIRE_INTERVAL_S until the body has left.

  • If the alarm fired, the with statement raises WallClockTimeout, even when the body went on to return normally or to raise something else (the first timeout was dropped or converted on the way): a limit that was exceeded always reads as a timeout.

  • On every exit the timer is disarmed and the previous SIGALRM handler is put back. A limit armed by an enclosing caller is re-armed with what is left of it, so limits nest.

  • On a timeout, the orthonym ContextVars and module-level thread-locals the interrupted call changed are put back to their values at entry (the timeout’s restored lists them). The engine resets this state in finally blocks, but an alarm that lands inside such a block, or between a set and the try that guards it, skips the reset (seen: an alarm in name’s finally left session_depth at 1), and every later naming in the process would run with it. State of a module first imported during the call is left as it is.

seconds of None or <= 0 means no limit. Off the main thread, or on a platform without SIGALRM, the body runs without a limit (a signal cannot be delivered there), as the tools did before this helper.

orthonym.wallclock.can_limit()#

True when this thread can arm a limit: a POSIX SIGALRM on the main thread (Python runs signal handlers on the main thread only).

orthonym.wallclock.leaked_naming_state()#

The known process-global naming state of the calling thread that is not at its value outside a naming call (empty when clean). Reads only modules already imported; imports nothing.

orthonym.wallclock.reset_naming_state()#

Put the state:func:leaked_naming_state reads back to its value outside a naming call.

orthonym.wallclock.clear_leaked_naming_state()#

After a:class:WallClockTimeout: return the known naming state still left behind (empty when clean, which it is once:class:wall_clock_limit has put the state back) and reset it, so the next molecule in this process is named as in a fresh one. A tool records a non-empty result on its TIMEOUT row.