Group Signals into Devices
Contents
Group Signals into Devices#
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.
Set up for tutorial#
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 status
with restart all
and run again.
!../supervisor/start_supervisor.sh status
decay RUNNING pid 2695, uptime 0:00:48
mini_beamline RUNNING pid 2696, uptime 0:00:48
random_walk RUNNING pid 2697, uptime 0:00:48
random_walk_horiz RUNNING pid 2698, uptime 0:00:48
random_walk_vert RUNNING pid 2699, uptime 0:00:48
simple RUNNING pid 2700, uptime 0:00:48
thermo_sim RUNNING pid 2701, uptime 0:00:48
trigger_with_pc RUNNING pid 2702, uptime 0:00:48
Define a Custom Device#
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:ophyd.Device
:
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 ('x'
, '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
RandomWalk(prefix='random-walk:horiz:', name='random_walk_horiz', read_attrs=['x', 'dt'], configuration_attrs=[])
It is conventional to name the Python variable on the left the same as the
value of 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
RandomWalk(prefix='random-walk:vert:', name='random_walk_vert', read_attrs=['x', 'dt'], configuration_attrs=[])
Use it with the Bluesky RunEngine#
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 random_walk_horiz
like random_walk_horiz.x
and use this to read them individually.
from bluesky.plans import count
RE(count([random_walk_horiz.x], num=3, delay=1))
+-----------+------------+---------------------+
| seq_num | time | random_walk_horiz_x |
+-----------+------------+---------------------+
| 1 | 21:09:03.7 | 1 |
| 2 | 21:09:04.7 | 1 |
| 3 | 21:09:05.7 | 1 |
+-----------+------------+---------------------+
generator count ['37ab875f'] (scan num: 1)
('37ab875f-0e27-49a2-9167-6599b609fafa',)
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))
+-----------+------------+---------------------+----------------------+
| seq_num | time | random_walk_horiz_x | random_walk_horiz_dt |
+-----------+------------+---------------------+----------------------+
| 1 | 21:09:06.9 | 0 | 3 |
| 2 | 21:09:07.9 | 0 | 3 |
| 3 | 21:09:08.9 | 0 | 3 |
+-----------+------------+---------------------+----------------------+
generator count ['29e10897'] (scan num: 2)
('29e10897-62f1-4966-881b-f09da45fc772',)
Assign a “Kind” to Components#
In the example just above, notice that we are recording random_walk_horiz_dt
in every row (i.e. every Event) because it is returned alongside
random_walk_horiz_x
in the reading.
random_walk_horiz.read()
OrderedDict([('random_walk_horiz_x',
{'value': 1.2681267613790572, 'timestamp': 1682197749.090841}),
('random_walk_horiz_dt',
{'value': 3.0, 'timestamp': 1682197694.985232})])
This is probably not necessary. Unless we have some reason to expect that it
could be changed, it would be more useful to record random_walk_horiz_dt
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
<Kind.config: 2>
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 read()
to
read_configuration()
:
random_walk_horiz.read()
OrderedDict([('random_walk_horiz_x',
{'value': 1.2681267613790572, 'timestamp': 1682197749.090841})])
random_walk_horiz.read_configuration()
OrderedDict([('random_walk_horiz_dt',
{'value': 3.0, 'timestamp': 1682197694.985232})])
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
a given device
is read.
Other possible values for Kind
are omitted
, normal
and 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.