Important

You can run this notebook in a live session Binder or view it on nbviewer or GitHub.

Epics Signal

In this notebook you will:

  • Connect to some simulated hardware using EpicsSignal

  • Explore the EpicsSignal interface.

Recommend Prerequisites:

Simulated Hardware

Below, we will connect to EPICS IOC(s) controlling simulated hardware in lieu of actual motors, detectors. The IOCs should already be running in the background. Run this command to verify that they are running: it should produce output with RUNNING on each line. In the event of a problem, edit this command to replace status with restart all and run again.

[1]:
!supervisorctl -c supervisor/supervisord.conf status
decay                            RUNNING   pid 4977, uptime 0:00:29
mini_beamline                    RUNNING   pid 4978, uptime 0:00:29
random_walk                      RUNNING   pid 4979, uptime 0:00:29
random_walk_horiz                RUNNING   pid 4980, uptime 0:00:29
random_walk_vert                 RUNNING   pid 4981, uptime 0:00:29
simple                           RUNNING   pid 4982, uptime 0:00:29
thermo_sim                       RUNNING   pid 4983, uptime 0:00:29
trigger_with_pc                  RUNNING   pid 4984, uptime 0:00:29

Hello, EpicsSignal

An EpicsSignal is ophyd’s representation of a single EPICS channel or a pair of channels, one readable and one writeable.

[2]:
from ophyd import EpicsSignal
[3]:
a = EpicsSignal('simple:A', name='a')
[4]:
a
[4]:
EpicsSignal(read_pv='simple:A', name='a', timestamp=1594844735.6694374, auto_monitor=False, string=False, write_pv='simple:A', limits=False, put_complete=False)
[5]:
a.name  # human-friendly label, which will be encoded in 'documents' emitted by bluesky
[5]:
'a'
[6]:
a.wait_for_connection()
[7]:
a.connected
[7]:
True
[8]:
a.get()
[8]:
1
[9]:
a.put(3)
a.get()
[9]:
3
[10]:
a.read()
[10]:
{'a': {'value': 3, 'timestamp': 1594844735.727883}}
[11]:
a.describe()
[11]:
{'a': {'source': 'PV:simple:A',
  'dtype': 'integer',
  'shape': [],
  'units': '',
  'lower_ctrl_limit': 0,
  'upper_ctrl_limit': 0}}

Exercise

Instaniate an EpicsSignal that is connect to some made-up PV that does not exist, as in:

broken = EpicsSignal('THIS_IS_NOT_A_THING', name='broken')

What does broken.connected do? What about broken.wait_for_connection()? And broken.read()?

[ ]:

[ ]:

[ ]:

[ ]:

Read-only

The EpicsSignalRO represents a read-only signal. It can’t be put to.

[12]:
from ophyd import EpicsSignalRO

x = EpicsSignalRO('random_walk:x', name='x')
[13]:
x.get()
[13]:
0.8747314744180399

Try putting a value to x It will raise a ReadOnlyError.

[ ]:

A read–write pair

Sometimes the readback and setpoint are different PVs. We can group them into one EpicsSignal.

[14]:
temp = EpicsSignal('thermo:I', write_pv='thermo:SP', name='temp')
temp
[14]:
EpicsSignal(read_pv='thermo:I', name='temp', timestamp=1594844735.7948515, auto_monitor=False, string=False, write_pv='thermo:SP', limits=False, put_complete=False)
[15]:
temp.get()
[15]:
100.43863446716048
[16]:
temp.put(105)
[17]:
temp.get()
[17]:
100.43863446716048

This IOC simulates the oscillations of a temperature controller and will take some time to settle to the desired value. Executing the cell above several times will return varied values. This illustrates the importance of tracking “done”-ness, which we will address in the tutorial on Devices.

More on status later.

Subscribe

The actions on an EPICS channel are:

  • read (get)

  • write (put)

  • monitor (subscribe, “event add”)

To subscribe is to say, “Send me updates asynchronously whenever the value changes.” To process these changes, we write a function that will be called each time a new value arrives and register than function with the Signal.

[18]:
x = EpicsSignal('random_walk:x', name='x')

def callback(value, old_value, **kwargs):
    print(f"Value changed from {old_value} to {value}.")

token = x.subscribe(callback)
Value changed from <object object at 0x7fc964a26b00> to 0.8747314744180399.
[19]:
token  # We can use this to unsubscribe.
[19]:
0
[20]:
x.unsubscribe(token)

Exercise

Define and subscribe a callback that prints “+” when the value changes in the positive direction and “-” when it changes in the negative direction.

[ ]:

[21]:
%load solutions/callback_print_sign.py

set is put like with a way to know when the action is complete.

put is the low-level method that actually communicates with hardware. set is a higher-level method that calls put and then tracks when the action initiated by put has completed, either by using Channel Access “put completion” or by polling the signal on a background thread.

[22]:
temp.tolerance = 0.05
[23]:
status = temp.set(273)
status.add_callback(lambda *args, **kwargs: print('done!'))
# Wait several seconds and then 'done!' will be printed by a background thread.

More about status and exactly what is happening here in the notebook on Device.

Accessing the PV name for debugging

When we have many signals in play, it can be useful to as a Signal which PV it is connected to (or attempting to connect to).

[24]:
a.pvname  # PV name we gave above
[24]:
'simple:A'

If ophyd is failing to connect, we can try to isolate the problem by using another Channel Access client like caget or caproto-get.

[25]:
!caproto-get 'simple:A'
simple:A                                  [3]

We can add verbose output to learn more about which server this is from, etc.

[26]:
!caproto-get -v 'simple:A'
[D 20:25:36.453       client:   59] Registering with the Channel Access repeater.
[D 20:25:36.454       client:   69] Searching for 'simple:A'....
[D 20:25:36.456       client:  133] Found 'simple:A' at 10.20.0.90:55382
[D 20:25:36.459       client:  187] 10.20.0.90:41726 <<<--- 10.20.0.90:55382 16B VersionResponse(version=13)
[D 20:25:36.459       client:  187] 10.20.0.90:41726 <<<--- 10.20.0.90:55382 16B AccessRightsResponse(cid=0, access_rights=<AccessRights.WRITE|READ: 3>)
[D 20:25:36.460       client:  187] 10.20.0.90:41726 <<<--- 10.20.0.90:55382 16B CreateChanResponse(data_type=<ChannelType.LONG: 5>, data_count=1, cid=0, sid=0)
[I 20:25:36.460       client:  191] simple:A Channel connected.
[D 20:25:36.460       client:  204] 10.20.0.90:55382 simple:A Detected native data_type <ChannelType.LONG: 5>.
[D 20:25:36.461       client:  228] 10.20.0.90:41726 <<<--- 10.20.0.90:55382 20B simple:A ReadNotifyResponse(data=array([3], dtype=int32), data_type=<ChannelType.LONG: 5>, data_count=1, status=CAStatusCode(name='ECA_NORMAL', code=0, code_with_severity=1, severity=<CASeverity.SUCCESS: 1>, success=1, defunct=False, description='Normal successful completion'), ioid=0, metadata=None)
simple:A                                  [3]
[27]:
!caproto-get -vvv 'simple:A'
[D 20:25:36.777     repeater:  221] Another repeater is already running; will not spawn one.
[D 20:25:36.777       client:   59] Registering with the Channel Access repeater.
[D 20:25:36.777 _broadcaster:  103] (1 of 1) RepeaterRegisterRequest(client_address='0.0.0.0')
[D 20:25:36.778       client:   69] Searching for 'simple:A'....
[D 20:25:36.779 _broadcaster:  103] (1 of 2) VersionRequest(priority=0, version=13)
[D 20:25:36.779 _broadcaster:  101] (2 of 2) SearchRequest(name='simple:A', cid=0, version=13, reply=5)
[D 20:25:36.780       client:   88] 0.0.0.0:41169 --->>> 255.255.255.255:5064 2 commands 48B
[D 20:25:36.781 _broadcaster:  143] 0.0.0.0:41169 <<<--- 10.20.0.90:5065 16B RepeaterConfirmResponse(repeater_address='10.20.0.90')
[D 20:25:36.781 _broadcaster:  143] 0.0.0.0:41169 <<<--- 10.20.0.90:5064 16B VersionResponse(version=13)
[D 20:25:36.781 _broadcaster:  143] 0.0.0.0:41169 <<<--- 10.20.0.90:5064 24B SearchResponse(port=55382, ip='255.255.255.255', cid=0, version=13)
[D 20:25:36.782       client:  133] Found 'simple:A' at 10.20.0.90:55382
[D 20:25:36.784     _circuit:  166] 10.20.0.90:41728 --->>> 10.20.0.90:55382 16B simple:A VersionRequest(priority=0, version=13)
[D 20:25:36.784     _circuit:  166] 10.20.0.90:41728 --->>> 10.20.0.90:55382 64B HostNameRequest(name='travis-job-b64c8166-9b3a-46f5-8573-007875c1a7b7')
[D 20:25:36.784     _circuit:  166] 10.20.0.90:41728 --->>> 10.20.0.90:55382 24B ClientNameRequest(name='travis')
[D 20:25:36.785     _circuit:  166] 10.20.0.90:41728 --->>> 10.20.0.90:55382 32B simple:A CreateChanRequest(name='simple:A', cid=0, version=13)
[D 20:25:36.785       client:  187] 10.20.0.90:41728 <<<--- 10.20.0.90:55382 16B VersionResponse(version=13)
[D 20:25:36.786       client:  187] 10.20.0.90:41728 <<<--- 10.20.0.90:55382 16B AccessRightsResponse(cid=0, access_rights=<AccessRights.WRITE|READ: 3>)
[D 20:25:36.787       client:  187] 10.20.0.90:41728 <<<--- 10.20.0.90:55382 16B CreateChanResponse(data_type=<ChannelType.LONG: 5>, data_count=1, cid=0, sid=0)
[I 20:25:36.787       client:  191] simple:A Channel connected.
[D 20:25:36.787       client:  204] 10.20.0.90:55382 simple:A Detected native data_type <ChannelType.LONG: 5>.
[D 20:25:36.787     _circuit:  166] 10.20.0.90:41728 --->>> 10.20.0.90:55382 16B simple:A ReadNotifyRequest(data_type=<ChannelType.LONG: 5>, data_count=0, sid=0, ioid=0)
[D 20:25:36.788       client:  228] 10.20.0.90:41728 <<<--- 10.20.0.90:55382 20B simple:A ReadNotifyResponse(data=array([3], dtype=int32), data_type=<ChannelType.LONG: 5>, data_count=1, status=CAStatusCode(name='ECA_NORMAL', code=0, code_with_severity=1, severity=<CASeverity.SUCCESS: 1>, success=1, defunct=False, description='Normal successful completion'), ioid=0, metadata=None)
[D 20:25:36.789     _circuit:  166] 10.20.0.90:41728 --->>> 10.20.0.90:55382 16B simple:A ClearChannelRequest(sid=0, cid=0)
simple:A                                  [3]

So many names

In summary, we have:

  • The pvname, an address for machines

  • The name, a label for humans and downstream analysis code that wants 'temperature' not 'PV:asdfoijefopefpoaewaopivjapoefijaeftep'.

  • The name of the variable in Python, the name we use in the code. There could be multiple of these pointing to the same object, as in a = b = EpicsSignal(...).