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:
BaseExceptionA tool’s wall-clock limit on one call was exceeded.
Derives from
BaseException(NOTException) so that noexcept Exceptionon 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.restorednames 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:
objectwith wall_clock_limit(seconds):raises:class:WallClockTimeout in the body oncesecondsof wall-clock time have passed.Arms
ITIMER_REALwith aSIGALRMhandler that raises the timeout, and fires again every:data:REFIRE_INTERVAL_S until the body has left.If the alarm fired, the
withstatement raisesWallClockTimeout, 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
SIGALRMhandler 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
restoredlists them). The engine resets this state infinallyblocks, but an alarm that lands inside such a block, or between asetand thetrythat guards it, skips the reset (seen: an alarm inname’sfinallyleftsession_depthat 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.
secondsofNoneor<= 0means no limit. Off the main thread, or on a platform withoutSIGALRM, 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
SIGALRMon 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.