In this tutorial we will group multiple Signals into a simple custom Device, which enables us to conveniently connect to them and read them in batch.
We'll start our IOCs connected to simulated hardware, some of which implement a random walk that we will use.
The IOCs may already be running in the background. Run this command to verify
that they are running: it should produce output with STARTING or RUNNING on each line.
In the event of a problem, edit this command to replace
restart all and run again.
It's common to have more than one instance of a given piece of hardware and to present each instance in EPICS with different "prefixes" as in:
# Device 1: random-walk:horiz:dt random-walk:horiz:x # Device 2: random-walk:vert:dt random-walk:vert:x
Ophyd makes it easy to take advantage of this nested naming convention of PV names,
where applicable. Define a subclass of :class:
from ophyd import Component, Device, EpicsSignal, EpicsSignalRO class RandomWalk(Device): x = Component(EpicsSignalRO, 'x') dt = Component(EpicsSignal, 'dt')
Up to this point we haven't actually created any signals yet or connected
to any hardware. We have only defined the structure of this device and
provided the suffixes (
'dt') of the relevant PVs.
Now, we create an instance of the device, providing the PV prefix that identifies one of our IOCs.
random_walk_horiz = RandomWalk('random-walk:horiz:', name='random_walk_horiz') random_walk_horiz.wait_for_connection() random_walk_horiz
It is conventional to name the Python variable on the left the same as the
name, but not required. That is, this is conventional...
a = RandomWalk("...", name="a")
a = RandomWalk("...", name="b") # local variable different from name a = RandomWalk("...", name="some name with spaces in it") a = b = RandomWalk("...", name="b") # two local variables
In the same way we can connect to the other IOC. We create a second instance of the same class.
random_walk_vert = RandomWalk('random-walk:vert:', name='random_walk_vert') random_walk_vert.wait_for_connection() random_walk_vert
The signals can be used by the Bluesky RunEngine. Let's configure a RunEngine to print a table.
from bluesky import RunEngine from bluesky.callbacks import LiveTable RE = RunEngine() token = RE.subscribe(LiveTable(["random_walk_horiz_x", "random_walk_horiz_dt"]))
We can access the components of
and use this to read them individually.
from bluesky.plans import count RE(count([random_walk_horiz.x], num=3, delay=1))
We can also read
random_walk_horiz in its entirety as a unit, treating it as
a composite "detector".
RE(count([random_walk_horiz], num=3, delay=1))
In the example just above, notice that we are recording
in every row (i.e. every Event) because it is returned alongside
random_walk_horiz_x in the reading.
This is probably not necessary. Unless we have some reason to expect that it
could be changed, it would be more useful to record
once per Run as part of the device's configuration.
Ophyd enables us to do this like so:
from ophyd import Kind random_walk_horiz.dt.kind = Kind.config
As a shorthand, a string alias is also accepted and normalized to enum member of that name.
random_walk_horiz.dt.kind = "config" random_walk_horiz.dt.kind
Equivalently, we could have set the
kind when we first defined the device, like so:
class RandomWalk(Device): x = Component(EpicsSignalRO, 'x') dt = Component(EpicsSignal, 'dt', kind="config")
Again, either enum
Kind.config or string
"config" are accepted.
The result is that
random_walk_horiz_dt is moved from
In Bluesky's Document Model, the result of
device.read() is placed in an
Event Document, and the result of
device.read_configuration() is placed in
an Event Descriptor document. The Bluesky RunEngine always calls
device.read_configuration() and captures that information the first time
device is read.
Other possible values for
hinted. For more details, see the Ophyd documentation for Signal.
For a larger example of Kind being used on a real device, see the source code for EpicsMotor.