Skip to main content
Version: 2.2.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 inside Orca: no orca-client, 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_SIMEach device uses a sim driver inside Orca. orca-client is not used.Nothing but Orca.Writing workflows, iterating, testing. Fast and local.
DEVICE_SIMEach device goes through orca-client, which simulates the instrument. No hardware moves.orca-client connected, with drivers that load.Testing drivers and the orca-client connection without instruments.
LIVEEach device goes through orca-client to the real instrument.orca-client connected, and the instruments.Production runs.

Only PURE_SIM uses the sim driver inside Orca. DEVICE_SIM and LIVE both send commands through the real driver. The difference is whether orca-client drives a simulated instrument or the real one. 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 --sim does not choose a run mode. Each submission still chooses its own:

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

--sim does two things:

  • Warns about a missing await. An async call without await does nothing and raises no error. With --sim, the daemon log shows a warning with the line that made the call.
  • Marks the system as simulated. orca status and GET /health show it.

It does nothing else. It does not change drivers, does not block LIVE submissions, and does not change how PURE_SIM runs.

To get the same warnings in a script that runs Orca without the daemon, set ORCA_SIM_COROUTINE_DIAGNOSTICS=1.

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 command sent to orca-client 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 its own sim driver or orca-client, and orca-client picks a simulated or real instrument from 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.
orca-clientEvery command carries the stamped effective_mode, so orca-client 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.