Expose ophyd (Bluesky) devices as Labwire instruments
Project description
labwire-ophyd
Expose any ophyd device as a Labwire instrument, so AI agents can discover and drive the largest existing body of Python instrument drivers through one protocol, with the units, safety classes, and signed results ophyd itself does not carry.
ophyd abstracts hardware for Python programmers. Labwire describes hardware to AI agents. This bridge composes them; Labwire does not reimplement drivers.
Five-minute quickstart
Nothing here needs hardware. From a checkout with make setup done:
# 1. See what the bridge works out on its own, and what it cannot
uv run labwire-ophyd annotate ophyd.sim:motor -o labwire-ophyd.yaml
# 2. Fill in every TODO in that file (ophyd.sim reports no units at all),
# then check what Labwire would expose
uv run labwire-ophyd check ophyd.sim:motor --annotations labwire-ophyd.yaml
# 3. Watch a full scan run through the protocol, with signed evidence
make demo-ophyd
labwire-ophyd check prints the resolved instrument: every channel with its
UCUM unit, every command with its safety class:
OK: SynAxis (motor)
2 channel(s), 4 component(s), 3 command(s)
S1 read
S2 move
S0 stop
motor: mm
motor_setpoint: mm
Serving it is a few lines:
from labwire.bridges.ophyd import OphydInstrument, load_annotations
from labwire.core import InstrumentServer
instrument = OphydInstrument(device, load_annotations(Path("labwire-ophyd.yaml")))
server = InstrumentServer(instrument, confirmation_token="operator-grant")
async with server.serve_websocket("127.0.0.1", 9520):
...
The annotation file
ophyd knows a device's structure but not its meaning: ophyd.sim devices
report no units, EPICS reports free-text .EGU strings, and ophyd has no
concept of a safety class at all. The annotation file supplies exactly that
layer, and it is what makes a device safe for an agent to operate.
version: 1
devices:
ophyd.sim.SynAxis: # keyed by class; subclasses inherit it
description: A sample stage.
intent_tags: [motion]
components:
readback:
unit: mm # UCUM, never guessed for you
qudt_quantity_kind: Length
dtype: float64 # ophyd infers dtype from the current value
setpoint: {unit: mm, dtype: float64}
velocity: {unit: mm/s}
acceleration: {unit: mm/s2}
commands:
trigger: {exclude: true} # meaningless for a stage
move: {estimated_duration_s: 2.0}
instances:
fine_stage: # keyed by Device.name; overrides the class
components:
setpoint: {limits: {low: -5.0, high: 5.0}}
Rules worth knowing:
- Merging is per field, along the class MRO and then the instance, so an override touches only what it names.
- Unknown keys, components, and commands are errors. A typo that silently annotated nothing would be worse than a failure.
- Missing units are refused, naming the exact component and the YAML path
that would fix it.
--allow-partialomits those components instead, and still reports every one. - Annotated
limitsintersect with the device's own control limits: the tightest bound on each side wins. settable: falsekeeps a channel but removes its actuation command, for the readings ophyd technically lets you write, such as a detector's computed value.
A working example lives at
examples/ophyd_scan/labwire-ophyd.yaml.
Mapping
| ophyd | Labwire |
|---|---|
Device.name |
instrument serial number (model = class name) |
Kind.hinted / Kind.normal |
telemetry channel |
Kind.config |
descriptor metadata |
Kind.omitted |
skipped |
describe()[…]["units"] |
UCUM unit, after EGU translation |
lower_ctrl_limit / upper_ctrl_limit |
parameter minimum / maximum |
Device.set() on a positioner |
move, safety class S2 |
signal.set() elsewhere |
set_<attr>, safety class S2 |
Device.trigger() |
trigger, safety class S1 |
Device.stop() |
stop, safety class S0 |
command/cancel |
device.stop(), and the run ends canceled |
| ophyd failures | HardwareFaultError / DeviceTimeoutError |
Safety defaults lean toward friction, because ophyd carries no safety
information and guessing low is the failure that moves hardware: actuation is
S2 (an agent must present an operator confirmation), acquisition is S1, and
stop is S0 so recovery stays available even while an interlock is tripped.
Changing a class takes an explicit annotation, never a heuristic.
DESIGN.md has the full reasoning, including why positioners are moved rather than poked.
Why this bridge declares no resources
Protocol v0.3 added resources for instrument state that is a tree with
nowhere else to live. An ophyd device has none: its structure is fixed by
its class and is already fully expressed as the descriptor plus scalar
channels, so inventing a resource here would be adding surface to prove a
point. The bridge declares resources: [], which costs a signal-shaped
instrument exactly nothing, and that asymmetry is useful evidence about the
primitive itself.
One v0.3 change does reach this bridge: a CommandAnnotation.safety_class
of S3 now genuinely bites. An S3 command requires an operator grant from
a server-side store (labwire grant), and a server with S3 commands and no
store refuses to start; see SPEC 8.6 before raising a command's class.
LIMITATIONS
Read this before believing anything above.
- No physical hardware, ever. The bridge is exercised against
ophyd.simdevices and against a pure-Python EPICS soft IOC (caproto) over Channel Access. It has never been connected to a real instrument, a production IOC, or any beamline, and no claim is made about NSLS-II, APS, Diamond, or any other facility. - Classic synchronous ophyd only.
ophyd-asyncis not supported. - Units are validated for presence, not grammar. The EGU→UCUM table covers conventional beamline spellings and is convention-based; it has not been checked against a UCUM implementation.
- Cancellation is best-effort and device-dependent. Labwire's cancel
calls
device.stop()and ends the run immediately; a simulatedophyd.simaxis ignoresstop()and still arrives at its target. - ophyd is synchronous. Every call runs in a worker thread, so the server keeps answering, but a badly behaved device still occupies a thread.
- Array- and enum-valued signals are refused: Labwire v0.2 channels carry scalars, and non-numeric channels still need an explicit unit.
- Move progress is not reported.
MoveStatus.watch()yields no useful intermediate fractions on simulated devices, so the bridge does not fabricate one.
Licensing
ophyd is a separate BSD-3 project from the Bluesky collaboration (Brookhaven
National Laboratory and contributors), and caproto is BSD-3 as well. Both are
optional dependencies here: not vendored, not forked, not modified. See
the repository NOTICE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file labwire_ophyd-0.3.0.tar.gz.
File metadata
- Download URL: labwire_ophyd-0.3.0.tar.gz
- Upload date:
- Size: 32.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a89c5491150b93f13e3f46f7d1ff47ac9bd2fb056569a9a5ea8ebd65411dd595
|
|
| MD5 |
5ffcfe549093865947aa9e915abf3455
|
|
| BLAKE2b-256 |
83317f830e6a60ef520a1500ab024d5a5de5b3fd3ca419e22d6af73e1367c24c
|
Provenance
The following attestation bundles were made for labwire_ophyd-0.3.0.tar.gz:
Publisher:
release.yml on benchwire/labwire
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
labwire_ophyd-0.3.0.tar.gz -
Subject digest:
a89c5491150b93f13e3f46f7d1ff47ac9bd2fb056569a9a5ea8ebd65411dd595 - Sigstore transparency entry: 2260406894
- Sigstore integration time:
-
Permalink:
benchwire/labwire@0762ad57b8f190dc77bdbf9fbb6df7ac5d9f679e -
Branch / Tag:
refs/heads/main - Owner: https://github.com/benchwire
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0762ad57b8f190dc77bdbf9fbb6df7ac5d9f679e -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file labwire_ophyd-0.3.0-py3-none-any.whl.
File metadata
- Download URL: labwire_ophyd-0.3.0-py3-none-any.whl
- Upload date:
- Size: 24.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7bfe0d85ee3cbd2ff22d1822da81c3f4e5d9497933f13882283713377684af5f
|
|
| MD5 |
fa8356be4372879d30254e3fb9a12f4f
|
|
| BLAKE2b-256 |
2c5935d89cf3281e1b7951bbfc7cd32b54841ac171bda24798e92a663fd689da
|
Provenance
The following attestation bundles were made for labwire_ophyd-0.3.0-py3-none-any.whl:
Publisher:
release.yml on benchwire/labwire
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
labwire_ophyd-0.3.0-py3-none-any.whl -
Subject digest:
7bfe0d85ee3cbd2ff22d1822da81c3f4e5d9497933f13882283713377684af5f - Sigstore transparency entry: 2260407385
- Sigstore integration time:
-
Permalink:
benchwire/labwire@0762ad57b8f190dc77bdbf9fbb6df7ac5d9f679e -
Branch / Tag:
refs/heads/main - Owner: https://github.com/benchwire
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0762ad57b8f190dc77bdbf9fbb6df7ac5d9f679e -
Trigger Event:
workflow_dispatch
-
Statement type: