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 4872, uptime 0:00:28
mini_beamline                    RUNNING   pid 4873, uptime 0:00:28
random_walk                      RUNNING   pid 4874, uptime 0:00:28
random_walk_horiz                RUNNING   pid 4875, uptime 0:00:28
random_walk_vert                 RUNNING   pid 4876, uptime 0:00:28
simple                           RUNNING   pid 4877, uptime 0:00:28
thermo_sim                       RUNNING   pid 4878, uptime 0:00:28
trigger_with_pc                  RUNNING   pid 4879, uptime 0:00:28

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=1591371824.6116264, 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': 1591371824.670393}}
[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]:
2.186841954215672

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=1591371824.736026, auto_monitor=False, string=False, write_pv='thermo:SP', limits=False, put_complete=False)
[15]:
temp.get()
[15]:
100.03927400117867
[16]:
temp.put(105)
[17]:
temp.get()
[17]:
100.03927400117867

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 0x7f589a337ad0> to 2.186841954215672.
Value changed from 2.186841954215672 to 2.186841954215672.
[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 15:43:45.397       client:   59] Registering with the Channel Access repeater.
[D 15:43:45.398       client:   69] Searching for 'simple:A'....
[D 15:43:45.400       client:  133] Found 'simple:A' at 10.20.0.15:63021
[D 15:43:45.403       client:  187] 10.20.0.15:46970 <<<--- 10.20.0.15:63021 16B VersionResponse(version=13)
[D 15:43:45.404       client:  187] 10.20.0.15:46970 <<<--- 10.20.0.15:63021 16B AccessRightsResponse(cid=0, access_rights=<AccessRights.WRITE|READ: 3>)
[D 15:43:45.404       client:  187] 10.20.0.15:46970 <<<--- 10.20.0.15:63021 16B CreateChanResponse(data_type=<ChannelType.LONG: 5>, data_count=1, cid=0, sid=0)
[I 15:43:45.404       client:  191] simple:A Channel connected.
[D 15:43:45.404       client:  204] 10.20.0.15:63021 simple:A Detected native data_type <ChannelType.LONG: 5>.
[D 15:43:45.405       client:  228] 10.20.0.15:46970 <<<--- 10.20.0.15:63021 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 15:43:45.715     repeater:  221] Another repeater is already running; will not spawn one.
[D 15:43:45.715       client:   59] Registering with the Channel Access repeater.
[D 15:43:45.715 _broadcaster:  103] (1 of 1) RepeaterRegisterRequest(client_address='0.0.0.0')
[D 15:43:45.716       client:   69] Searching for 'simple:A'....
[D 15:43:45.717 _broadcaster:  103] (1 of 2) VersionRequest(priority=0, version=13)
[D 15:43:45.717 _broadcaster:  101] (2 of 2) SearchRequest(name='simple:A', cid=0, version=13, reply=5)
[D 15:43:45.718       client:   88] 0.0.0.0:56596 --->>> 255.255.255.255:5064 2 commands 48B
[D 15:43:45.719 _broadcaster:  143] 0.0.0.0:56596 <<<--- 10.20.0.15:5065 16B RepeaterConfirmResponse(repeater_address='10.20.0.15')
[D 15:43:45.719 _broadcaster:  143] 0.0.0.0:56596 <<<--- 10.20.0.15:5064 16B VersionResponse(version=13)
[D 15:43:45.719 _broadcaster:  143] 0.0.0.0:56596 <<<--- 10.20.0.15:5064 24B SearchResponse(port=63021, ip='255.255.255.255', cid=0, version=13)
[D 15:43:45.719       client:  133] Found 'simple:A' at 10.20.0.15:63021
[D 15:43:45.721     _circuit:  166] 10.20.0.15:46972 --->>> 10.20.0.15:63021 16B simple:A VersionRequest(priority=0, version=13)
[D 15:43:45.722     _circuit:  166] 10.20.0.15:46972 --->>> 10.20.0.15:63021 64B HostNameRequest(name='travis-job-a7bea89f-5dc6-4c63-bbe7-5dd1c04d0f62')
[D 15:43:45.722     _circuit:  166] 10.20.0.15:46972 --->>> 10.20.0.15:63021 24B ClientNameRequest(name='travis')
[D 15:43:45.722     _circuit:  166] 10.20.0.15:46972 --->>> 10.20.0.15:63021 32B simple:A CreateChanRequest(name='simple:A', cid=0, version=13)
[D 15:43:45.723       client:  187] 10.20.0.15:46972 <<<--- 10.20.0.15:63021 16B VersionResponse(version=13)
[D 15:43:45.767       client:  187] 10.20.0.15:46972 <<<--- 10.20.0.15:63021 16B AccessRightsResponse(cid=0, access_rights=<AccessRights.WRITE|READ: 3>)
[D 15:43:45.768       client:  187] 10.20.0.15:46972 <<<--- 10.20.0.15:63021 16B CreateChanResponse(data_type=<ChannelType.LONG: 5>, data_count=1, cid=0, sid=0)
[I 15:43:45.768       client:  191] simple:A Channel connected.
[D 15:43:45.768       client:  204] 10.20.0.15:63021 simple:A Detected native data_type <ChannelType.LONG: 5>.
[D 15:43:45.768     _circuit:  166] 10.20.0.15:46972 --->>> 10.20.0.15:63021 16B simple:A ReadNotifyRequest(data_type=<ChannelType.LONG: 5>, data_count=0, sid=0, ioid=0)
[D 15:43:45.769       client:  228] 10.20.0.15:46972 <<<--- 10.20.0.15:63021 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 15:43:45.770     _circuit:  166] 10.20.0.15:46972 --->>> 10.20.0.15:63021 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(...).