Skip to main content
Version: 2.0.0

Sim hierarchy

You can build and run a complete workflow without touching an instrument. Every submission picks a run mode, and one of the three is pure simulation in your own process: no wire calls, no hardware, fast enough to iterate on a laptop.

The three run modes​

from orca.runtime.run_modes import WorkflowRunMode
ModeWhat it bindsWhat it needsUse it for
PURE_SIMEvery device runs against its in-process sim driver. No wire calls.Nothing but the framework. It is the only mode that skips driver loading entirely.Writing workflows, iterating, testing. Fast and local.
DEVICE_SIMThe live driver runs, so the wire is exercised. On the far end the on-prem bridge swaps in a sim backend per the stamped effective mode. Devices are not real.Drivers that load, and the gateway to the on-prem bridge.Exercising driver code and the wire without instruments.
LIVELive drivers on real hardware.Drivers, the gateway, and the instruments themselves.Production runs.

Only PURE_SIM picks the sim driver slot. DEVICE_SIM and LIVE both dispatch through the live driver, and what differs is the backend the bridge binds on the far end. That is why DEVICE_SIM still needs a driver that loads.

Mode is required on every submission. There is no deployment-level default. runtime.submit(workflow, mode=...) and runtime.submit_workflow(name, mode=...) both take it, and orca run <workflow> --run-mode <mode> rejects a submission that does not pass one. Getting the mode wrong is a class of bug worth refusing rather than defaulting. See Submissions.

The --sim flag on a mount​

orca topology mount <spec> --sim does not pick a run mode. It is a property of the mount, not of any run, and every submission still has to say which world it means:

orca topology mount my_lab.topology:build_topology --sim   # once, at the mount
orca run my_workflow --run-mode PURE_SIM --wait # on every submission

--sim does two things:

  • Turns on the daemon's sim coroutine diagnostics. A missing await on an async call is silent: the bare call returns a truthy coroutine, so if not ctx.labware.can_continue(): becomes dead code and nothing raises. With the flag on, asyncio debug mode records where each coroutine was created, and the coroutine ... was never awaited warning reaches the log with that traceback rather than arriving later from the garbage collector with no call site attached.
  • Marks the mount as sim. GET /health and orca status report it, so an operator can see which kind of bench they are on.

That is all of it. --sim binds no driver, refuses no LIVE submission, and changes nothing about how a PURE_SIM submission runs. A mount without it runs simulated workflows exactly the same way.

The flag is deployment-wide rather than per submission because asyncio debug mode and the warning filter are process-global, on the event loop the whole daemon shares. Gating them on one submission's run mode would leave the debug overhead on for every later run in that daemon, live ones included. For a script that drives the runtime without the daemon, set ORCA_SIM_COROUTINE_DIAGNOSTICS=1 instead; 1, true, yes and on all count.

Pinning one device to simulation​

You do not have to choose one world for the whole lab. Any device can declare a sim_override in the topology, which pins that device's dispatch whatever the submission picks:

shaker = Shaker("shaker_01", sim_override=WorkflowRunMode.PURE_SIM)

Common cases:

  • A device is offline for maintenance: pin it to PURE_SIM so workflows keep running against the rest of the lab.
  • A device is awaiting calibration: pin it to PURE_SIM until it is validated.
  • You are bringing a new device online: pin it to DEVICE_SIM to exercise driver code without touching hardware.

The rule that combines the two, in one sentence: the submission mode is the ceiling, and per-device overrides can ratchet toward sim but never toward live. A device pinned to LIVE under a PURE_SIM submission still dispatches PURE_SIM.

The override is declared at topology construction time, on Device and on Transporter alike. sim_override=None, the default, means follow the submission's mode.

A LIVE submission against a device pinned toward sim is refused unless you say you meant it. See the confirmation gate.

Advanced​

Everything below is for deploying to hardware, or for building an operator surface that has to say which world a command reaches.

The resolver​

resolve_effective_mode_for_device(submission_mode, device_sim_override) is the whole resolver. It returns a ResolvedDeviceMode(resolved, warning).

Submission modeDevice sim_overrideEffective modeWarning?
PURE_SIM(any)PURE_SIMno
DEVICE_SIMNoneDEVICE_SIMno
DEVICE_SIMPURE_SIMPURE_SIM (ratchet down)no
DEVICE_SIMDEVICE_SIMDEVICE_SIMno
DEVICE_SIMLIVEDEVICE_SIM (override silently inert)no
LIVENoneLIVEno
LIVEPURE_SIMPURE_SIM (ratchet down)YES
LIVEDEVICE_SIMDEVICE_SIM (ratchet down)YES
LIVELIVELIVEno

Twelve rows when you fully enumerate device_sim_override over {None, PURE_SIM, DEVICE_SIM, LIVE} against the three submission modes. The collapsed version above is the operative summary.

Two rules govern the table:

  1. Overrides only ratchet toward sim. The submission ceiling wins.
  2. A LIVE submission warns about sim-direction overrides. Any device pinned to PURE_SIM or DEVICE_SIM under a LIVE submission fires a warning, so you do not deploy live with half the lab still in sim.

SimulationManager.mode_under(base) is the one place a base mode is combined with a device's override. The driver swap, the operator snapshots, and the effective_mode stamped onto every wire command all resolve through it, so those answers cannot drift apart. That matters because the sim versus live choice is made twice, in two processes: orca picks sim driver versus wire, and the on-prem bridge picks sim backend versus real off the mode this stamps.

The topology's declared surface carries the override too: the topology registry reads it, TopologyCard.topology_sim_override reports it, and SimulationManager is threaded with it so every dispatch routes through the resolver.

The LIVE confirmation gate​

A LIVE submission against any device with a sim-direction sim_override is refused unless the submission carries acknowledge_warnings=True. The in-process call raises LiveSubmissionWithSimOverridesUnacknowledgedError, whose .devices attribute is the full list of offending devices as (device_name, sim_override, resolved_mode) tuples:

# error.devices
[
("shaker_01", WorkflowRunMode.PURE_SIM, WorkflowRunMode.PURE_SIM),
("sealer_01", WorkflowRunMode.DEVICE_SIM, WorkflowRunMode.DEVICE_SIM),
]

You see exactly which devices the LIVE submission would skip before deciding whether to proceed. To proceed:

SurfaceHow to acknowledge
CLIorca run <workflow> --run-mode LIVE --confirm
Pythonruntime.submit(workflow, mode=WorkflowRunMode.LIVE, acknowledge_warnings=True)

Acknowledging applies to all sim-direction overrides on that submission. It is a single boolean, not a per-device list. To run one of the pinned devices live, remove its sim_override first: a topology edit, then a runtime rebuild.

Bringing devices up, once per world​

Device bring-up is lazy: it happens at the first execution entry, not at build. System.ensure_runtime_initialized(workflow) runs from each execution entrypoint after current_run_mode is seeded, walks each liquid handler to push its deck config and reconcile occupancy, then initializes the fresh non-sim device worlds in play.

Two properties of that walk matter:

  • Once per run mode and scope, not once per process. The mode picks which world a dispatch reaches, so a PURE_SIM dry run must not satisfy a later LIVE run: the live instrument would start with no deck and no device init. Per-device gates key on the resolved per-device mode, so a sim_override-pinned device is not reconfigured or re-initialized by a second submission mode that resolves it to the same world.
  • It covers the devices a workflow declares, not every device on the bench. Bringing an arm up takes its session and resets what it was tracking, and a workflow that never moves labware between devices has no business doing that to one. On the bench an unscoped walk drove a transporter into a neighbouring instrument during a run that only used the liquid handler. The deck walk is not narrowed, because laying a deck out moves nothing.

The walk also survives partial failure: the (mode, scope) pair is marked done only when the whole walk succeeds, a freshly configured world keeps a pending-reconcile obligation until a reconcile completes, and each device world is marked initialized on its own, so a retry re-initializes only what failed.

Which world an operator command means​

Outside an execution there is no submission to read, so the caller states what it means. Two named bases:

ConstantValueUsed by
UNSEEDED_FALLBACK_MODEPURE_SIMMetadata reads. Reading a device must not move it.
OPERATOR_DEVICE_WRITE_BASELIVEOperator lifecycle verbs (initialize, connect, disconnect, reconcile-deck) and operator world writes. Asking a device to move means the real one.

Both go through orca.gateway.mode_resolution.mode_of(..., when_unseeded=...), and the topology override ratchets on top either way, so a device declared sim is never reached live. Resolving an operator verb against the read fallback was a real defect: the verb drove the in-process simulator and reported success.

Every device verb also takes an explicit --mode to say which world it means for that one call:

orca device connect flex_1 --mode DEVICE_SIM
orca device send pf400 home --mode LIVE

Internally that is mode_scope(base), which seeds the base for one call and restores the previous value on exit, so an operator verb cannot leak its base into unrelated reads.

With no live system there is nothing to combine a base with, so the question is refused (ModeUnresolvableError) rather than answered from the base alone. Guessing there is how a device that asked for sim gets driven live. A system that is readable and simply does not declare the device is a different case, and that one does take the base.

The current_run_mode ContextVar​

The resolver's base is propagated through current_run_mode: ContextVar[WorkflowRunMode]. It is seeded at each execution entrypoint and re-seeded per thread at thread start. asyncio.Task inherits a snapshot of the parent context, so child tasks see the seed automatically.

You do not normally read it directly. SimulationManager.driver is the consumer at dispatch time.

Where it is enforced​

LayerWhat it checks
Submission validation (runtime.submit / runtime.submit_workflow)Computes resolved modes for every device declared in topology, asserts each is runnable under the mode, and refuses a LIVE submission carrying a sim-direction override without acknowledge_warnings=True.
Dispatch (SimulationManager.driver)Per-call lookup of the current run mode; returns the sim driver under PURE_SIM and the live driver otherwise.
The wireEvery command carries the stamped effective_mode, so the on-prem bridge binds a matching backend.

Pitfalls​

  • A device pinned to LIVE is not a no-op, but it is inert toward live. It says this device will never go to sim. The submission mode is still the ceiling, so a PURE_SIM submission still dispatches PURE_SIM. The only thing it changes is the warning.
  • --confirm is per-submission. It does not change the topology. The next submission without it refuses again. To remove the friction permanently, edit the topology to drop the sim_override.
  • DEVICE_SIM still requires drivers. It is the live driver running against a sim backend, so if the driver itself does not load (import error, missing config) DEVICE_SIM fails the way LIVE would.
  • A device snapshot does not tell you what a workflow will do. orca device list resolves against the operator write base, so it can report LIVE for a device a PURE_SIM submission will dispatch PURE_SIM to. Read sim_override for how a device is configured, and the submission's run mode for what a run will do.
  • The sim and live decision is per-action, not per-thread. A thread can pick a plate up from a LIVE-resolved device and drop it on a PURE_SIM-resolved device in the same method. The transporter resolves per action too.

See also​

  • Submissions: the submission that carries the mode.
  • Runtime: the service you submit to.
  • Devices: where sim_override slots into device construction, and the lifecycle verbs that take --mode.
  • CLI: run a workflow: --run-mode and --confirm from a terminal.
  • Quick Start: a whole workflow running in PURE_SIM, start to finish.