Dynamic Limits for Two Motors#
Overview#
Some instrument designs need protection against accidental collision between moving parts during routine operations. In such cases, each of the axes may be operating between its valid range but an interim (in-motion) state can pose the possibility of a collision.
One such possible collision is when arms of a diffractometer (such as \(\theta\) and \(2\theta\), known here as theta
and ttheta
, respectively) collide causing damage to beam transport apparatus and consequential instrumental downtime.
To prevent the collision in this case, the \(2\theta\) axis must be at least \(\delta\) degrees above the \(\theta\) axis. Empirically, \(\delta\) of 3 degrees is sufficient protection.
From a controls safety view, we provide an EPICS PV calculation that is zero when the move is not permitted and one when: (\(2\theta - \theta) \ge \delta\). We’ll monitor that PV in Bluesky to add a suspender that can interrupt the scan (via the bluesky.RunEngine) if the permit is removed. When the RunEngine handles an interruption involving movable devices, it sends a stop to each of the movables involved. Thus, when the dynamic limit permit is removed, both motors are stopped and the scan pauses, waiting for external interaction to clear the condition.
Here is where bluesky tells the motors to stop:
if justification is not None:
print("Justification for this suspension:\n%s" % justification)
for current_run in self._run_bundlers.values():
current_run.record_interruption('resume')
# During suspend, all motors should be stopped. Call stop() on
# every object we ever set().
self._stop_movable_objects(success=True)
If the RunEngine is started while the dynamic limit permit calculation is zero, the RunEngine will pause immediately. Here is an example:
In [21]: uid = RE(th_tth_scan([noisy, th_tth_permit], 8, 6, points=4, min_sep=3))
At least one suspender has tripped. The plan will begin when all suspenders are ready. Justification:
1. Signal th_tth_permit is low
Suspending... To get to the prompt, hit Ctrl-C twice to pause.
We might need to write our own suspender if none of the provided suspenders will do the job we want.
Summary#
Any time the motors are moved by the bluesky RunEngine, they will be stopped if the dynamic limit permit calculation goes to zero and the scan will pause.
EPICS setup#
We start with two motor axes defined in EPICS. Here, we run the docker image prjemian/synApps to make the EPICS IOC simulator run in a docker container with IOC prefix gp:
. These are motor PVs: gp:m1
and gp:m2
as shown.
To compute a dynamic limit between the two motor axes, we use a userCalc (EPICS swait record), gp:userCalc1
with settings as shown.
Set the description field to describe what this does.
Monitor each motor’s readback (
.RBV
) value. The readback value is the motor record’s best knowledge of the actual motor position.gp:userCalc1.INAN
=sky:m1.RBV
, the value will be inA
once the motor moves.gp:userCalc1.INBN
=sky:m2.RBV
, the value will be inB
once the motor moves.Change the calculation’s
.SCAN
field from Passive (calculates only when requested) to I/O Intr (calculate when any input changes, based on each field’s TRIGGER? setting).Enter the angle of minimum approach (3) into the
gp:userCalc1.C
field. This will be the PV to change this number.Enter the permit calculation:
(B-A)>=C
The calculated result (in gp:userCalc1.VAL
, gp:userCalc1
for short) once either of the motors move.
To scan, we want a “detector”. Let’s use another userCalc (gp:userCalc2
) to simulate a noisy detector with a random number generator. We’ll update this detector only when either motor moves (same as with the permit calculation) setting its .SCAN
to I/O Intr. The setup is shown in the next screen view image:
Bluesky setup#
[1]:
# put the instrument package into the path
import pathlib, sys
sys.path.append(str(pathlib.Path.home() / "bluesky"))
# start the instrument package for data collection
from instrument.collection import *
from bluesky import plans as bp
from bluesky import plan_stubs as bps
from ophyd import Component, Device, EpicsMotor, EpicsSignal
bec.disable_plots() # not interested in graphics in this notebook
RE.waiting_hook = None # disable the progress bar, looks awful in notebooks
/home/prjemian/bluesky/instrument/_iconfig.py
Activating auto-logging. Current session state plus future input saved.
Filename : /home/prjemian/Documents/projects/BCDA-APS/bluesky_training/docs/source/howto/.logs/ipython_console.log
Mode : rotate
Output logging : True
Raw input log : False
Timestamping : True
State : active
I Tue-18:47:27 - ############################################################ startup
I Tue-18:47:27 - logging started
I Tue-18:47:27 - logging level = 10
I Tue-18:47:27 - /home/prjemian/bluesky/instrument/session_logs.py
I Tue-18:47:27 - /home/prjemian/bluesky/instrument/collection.py
I Tue-18:47:27 - CONDA_PREFIX = /home/prjemian/.conda/envs/bluesky_2023_2
Exception reporting mode: Minimal
I Tue-18:47:27 - xmode exception level: 'Minimal'
I Tue-18:47:27 - /home/prjemian/bluesky/instrument/mpl/notebook.py
I Tue-18:47:27 - #### Bluesky Framework ####
I Tue-18:47:27 - /home/prjemian/bluesky/instrument/framework/check_python.py
I Tue-18:47:27 - /home/prjemian/bluesky/instrument/framework/check_bluesky.py
I Tue-18:47:27 - /home/prjemian/bluesky/instrument/framework/initialize.py
I Tue-18:47:27 - RunEngine metadata saved in directory: /home/prjemian/Bluesky_RunEngine_md
I Tue-18:47:28 - using databroker catalog 'training'
I Tue-18:47:28 - using ophyd control layer: pyepics
I Tue-18:47:28 - /home/prjemian/bluesky/instrument/framework/metadata.py
I Tue-18:47:28 - /home/prjemian/bluesky/instrument/epics_signal_config.py
I Tue-18:47:28 - Using RunEngine metadata for scan_id
I Tue-18:47:28 - #### Devices ####
I Tue-18:47:28 - /home/prjemian/bluesky/instrument/devices/area_detector.py
I Tue-18:47:28 - /home/prjemian/bluesky/instrument/devices/calculation_records.py
I Tue-18:47:30 - /home/prjemian/bluesky/instrument/devices/fourc_diffractometer.py
I Tue-18:47:30 - /home/prjemian/bluesky/instrument/devices/ioc_stats.py
I Tue-18:47:30 - /home/prjemian/bluesky/instrument/devices/kohzu_monochromator.py
I Tue-18:47:30 - /home/prjemian/bluesky/instrument/devices/motors.py
I Tue-18:47:30 - /home/prjemian/bluesky/instrument/devices/noisy_detector.py
I Tue-18:47:30 - /home/prjemian/bluesky/instrument/devices/scaler.py
I Tue-18:47:31 - /home/prjemian/bluesky/instrument/devices/shutter_simulator.py
I Tue-18:47:31 - /home/prjemian/bluesky/instrument/devices/simulated_fourc.py
I Tue-18:47:31 - /home/prjemian/bluesky/instrument/devices/simulated_kappa.py
I Tue-18:47:31 - /home/prjemian/bluesky/instrument/devices/sixc_diffractometer.py
I Tue-18:47:32 - /home/prjemian/bluesky/instrument/devices/temperature_signal.py
I Tue-18:47:32 - #### Callbacks ####
I Tue-18:47:32 - /home/prjemian/bluesky/instrument/callbacks/spec_data_file_writer.py
I Tue-18:47:32 - #### Plans ####
I Tue-18:47:32 - /home/prjemian/bluesky/instrument/plans/lup_plan.py
I Tue-18:47:32 - /home/prjemian/bluesky/instrument/plans/peak_finder_example.py
I Tue-18:47:32 - /home/prjemian/bluesky/instrument/utils/image_analysis.py
I Tue-18:47:32 - #### Utilities ####
I Tue-18:47:32 - writing to SPEC file: /home/prjemian/Documents/projects/BCDA-APS/bluesky_training/docs/source/howto/20230411-184732.dat
I Tue-18:47:32 - >>>> Using default SPEC file name <<<<
I Tue-18:47:32 - file will be created when bluesky ends its next scan
I Tue-18:47:32 - to change SPEC file, use command: newSpecFile('title')
I Tue-18:47:32 - #### Startup is complete. ####
symbol |
PV (here) |
meaning |
---|---|---|
|
gp:m1 |
( |
|
gp:m2 |
( |
|
gp:userCalc1.VAL |
result of EPICS calculation (swait record: 1=permit, 0=not), updates when either motor moves |
|
gp:userCalc1.C |
minimum permitted |
|
gp:userCalc2.VAL |
detector (random number generator) - integer, updates when either motor moves |
|
gp:userCalc2.C |
scale factor for |
Get the IOC prefix from the instrument configuration (iconfig
). If not available, use "gp:"
as the default.
[2]:
GP_IOC = iconfig.get("GP_IOC_PREFIX", "gp:")
Define the objects we’ll use in this demo.
[3]:
noisy = EpicsSignal(f"{GP_IOC}userCalc2.VAL", name="noisy")
noisy_scale = EpicsSignal(f"{GP_IOC}userCalc2.C", name="noisy_scale")
th_tth_min = EpicsSignal(f"{GP_IOC}userCalc1.C", name="th_tth_min")
th_tth_permit = EpicsSignal(f"{GP_IOC}userCalc1.VAL", name="th_tth_permit")
noisy.wait_for_connection()
noisy_scale.wait_for_connection()
th_tth_min.wait_for_connection()
th_tth_permit.wait_for_connection()
[4]:
# We do this ONLY in the simulator. Real instrument motors may need different settings!
# For this demo, we want to set the theta & ttheta motor backlash to the default condition
# when the IOC is first created. We'll change this later in the demo.
class BacklashMotor(EpicsMotor):
backlash_distance = Component(EpicsSignal, ".BDST")
backlash_velocity = Component(EpicsSignal, ".BVEL")
theta = BacklashMotor(f"{GP_IOC}m1", name="theta", labels=["motors",])
ttheta = BacklashMotor(f"{GP_IOC}m2", name="ttheta", labels=["motors",])
theta.wait_for_connection()
ttheta.wait_for_connection()
%mov ttheta.backlash_distance 0 ttheta.backlash_velocity 2 ttheta.velocity 2
%mov theta.backlash_distance 0 theta.backlash_velocity 1 theta.velocity 2
Setup the two swait records as shown above.
[5]:
calcs.calc1.reset()
calcs.calc1.description.put("(tth-th) permit")
calcs.calc1.channels.A.input_pv.put(theta.user_readback.pvname)
calcs.calc1.channels.B.input_pv.put(ttheta.user_readback.pvname)
calcs.calc1.channels.C.input_value.put(2.8)
calcs.calc1.calculation.put("(B-A)>=C")
calcs.calc1.precision.put(5)
calcs.calc1.scanning_rate.put("I/O Intr")
calcs.calc2.reset()
calcs.calc2.description.put("noisy")
calcs.calc2.channels.A.input_pv.put(theta.user_readback.pvname)
calcs.calc2.channels.B.input_pv.put(ttheta.user_readback.pvname)
calcs.calc2.channels.C.input_value.put(100_000)
calcs.calc2.calculation.put("floor(RNDM*C+.5)")
calcs.calc2.precision.put(0)
calcs.calc2.scanning_rate.put("I/O Intr")
Custom Bluesky plan for \(\theta:2\theta\) scan#
Define a plan for a coupled theta:2theta scan.
[6]:
def th_tth_scan(detectors, tth_start, tth_end, points=11, min_sep=None):
"""
run a coupled theta:2theta scan
"""
min_sep = abs(min_sep or 2.4)
old_sep = th_tth_min.get()
# check end points first!
if abs(tth_start/2) < min_sep:
print(
"Starting point below allowed minimum:"
f" |{tth_start/2:.4f}| < |{min_sep:.4f}|")
return
if abs(tth_end/2) < min_sep:
print(
"Ending point below allowed minimum:"
f" |{tth_end/2:.4f}| < |{min_sep:.4f}|")
return
yield from bps.mv(th_tth_min, min_sep)
yield from bp.scan(
detectors,
ttheta, tth_start, tth_end,
theta, tth_start/2, tth_end/2,
points
)
# reset the previous minimum
yield from bps.mv(th_tth_min, old_sep)
Try a scan that we know will fail the test for minimum separation.
[7]:
RE(th_tth_scan([noisy, th_tth_permit], 5, 25, points=11, min_sep=3))
Starting point below allowed minimum: |2.5000| < |3.0000|
[7]:
()
Swap the two end points, that also fails.
[8]:
RE(th_tth_scan([noisy, th_tth_permit], 25, 5, points=11, min_sep=3))
Ending point below allowed minimum: |2.5000| < |3.0000|
[8]:
()
This scan is successful.
[9]:
RE(th_tth_scan([noisy, th_tth_permit], 12, 8, points=11, min_sep=3))
Transient Scan ID: 926 Time: 2023-04-11 18:47:33
Persistent Unique Scan ID: '6ddca39f-503d-4481-bd83-03ecfbe21ae7'
New stream: 'label_start_motor'
New stream: 'primary'
+-----------+------------+------------+------------+------------+---------------+
| seq_num | time | ttheta | theta | noisy | th_tth_permit |
+-----------+------------+------------+------------+------------+---------------+
| 1 | 18:47:36.5 | 12.0000 | 6.0000 | 47659 | 1.00000 |
| 2 | 18:47:36.9 | 11.6000 | 5.8000 | 13759 | 1.00000 |
| 3 | 18:47:37.4 | 11.2000 | 5.6000 | 963 | 1.00000 |
| 4 | 18:47:37.9 | 10.8000 | 5.4000 | 10716 | 1.00000 |
| 5 | 18:47:38.4 | 10.4000 | 5.2000 | 62858 | 1.00000 |
| 6 | 18:47:38.9 | 10.0000 | 5.0000 | 79985 | 1.00000 |
| 7 | 18:47:39.4 | 9.6000 | 4.8000 | 18714 | 1.00000 |
| 8 | 18:47:39.9 | 9.2000 | 4.6000 | 65821 | 1.00000 |
| 9 | 18:47:40.4 | 8.8000 | 4.4000 | 12596 | 1.00000 |
| 10 | 18:47:40.9 | 8.4000 | 4.2000 | 86804 | 1.00000 |
| 11 | 18:47:41.4 | 8.0000 | 4.0000 | 6307 | 1.00000 |
+-----------+------------+------------+------------+------------+---------------+
generator scan ['6ddca39f'] (scan num: 926)
[9]:
('6ddca39f-503d-4481-bd83-03ecfbe21ae7',)
So far, have not registered a permit denied. Have not encountered a condition where permit would be denied.
Try to provoke a permit denied#
Check the backlash parameters for 2theta motor. Use the custom motor class that provides the backlash parameters.
[10]:
# these two motors have backlash support
print(f"{theta.backlash_distance.get()=} ({theta.motor_egu.get()})")
print(f"{theta.backlash_velocity.get()=} ({theta.motor_egu.get()}/s)")
print(f"{ttheta.backlash_distance.get()=} ({ttheta.motor_egu.get()})")
print(f"{ttheta.backlash_velocity.get()=} ({ttheta.motor_egu.get()}/s)")
theta.backlash_distance.get()=0.0 (degrees)
theta.backlash_velocity.get()=1.0 (degrees/s)
ttheta.backlash_distance.get()=0.0 (degrees)
ttheta.backlash_velocity.get()=2.0 (degrees/s)
Change (just) the backlash velocity and set a backlash distance for ttheta
.
[11]:
%mov ttheta.backlash_distance 0.5 ttheta.backlash_velocity 0.2
print(f"{ttheta.backlash_distance.get()=} ({ttheta.motor_egu.get()})")
print(f"{ttheta.backlash_velocity.get()=} ({ttheta.motor_egu.get()}/s)")
ttheta.backlash_distance.get()=0.5 (degrees)
ttheta.backlash_velocity.get()=0.2 (degrees/s)
Scan again over a shorter range.
[12]:
RE(th_tth_scan([noisy, th_tth_permit], 7, 6, points=4, min_sep=3))
Transient Scan ID: 927 Time: 2023-04-11 18:47:42
Persistent Unique Scan ID: '1d45b5ef-9268-494b-8fd7-37af3599b833'
New stream: 'label_start_motor'
New stream: 'primary'
+-----------+------------+------------+------------+------------+---------------+
| seq_num | time | ttheta | theta | noisy | th_tth_permit |
+-----------+------------+------------+------------+------------+---------------+
| 1 | 18:47:46.0 | 7.0000 | 3.5000 | 47643 | 1.00000 |
| 2 | 18:47:49.6 | 6.6667 | 3.3333 | 20673 | 1.00000 |
| 3 | 18:47:53.2 | 6.3333 | 3.1667 | 79528 | 1.00000 |
| 4 | 18:47:56.8 | 6.0000 | 3.0000 | 96895 | 0.00000 |
+-----------+------------+------------+------------+------------+---------------+
generator scan ['1d45b5ef'] (scan num: 927)
[12]:
('1d45b5ef-9268-494b-8fd7-37af3599b833',)
Let’s monitor the signal during the scan so we can see if it changes and how often.
[13]:
sd.monitors.append(th_tth_permit)
Repeat the scan, collecting the new info. We’ll inspect the monitors after the scan is done. (The RunEngine returns a list of all the run uids created by the plan. Capture this list.)
[14]:
uids = RE(th_tth_scan([noisy, th_tth_permit], 7, 6, points=3, min_sep=3))
Transient Scan ID: 928 Time: 2023-04-11 18:47:57
Persistent Unique Scan ID: '991bdcb1-f8d4-4e2f-a0eb-1dc16330481b'
New stream: 'label_start_motor'
New stream: 'th_tth_permit_monitor'
New stream: 'primary'
+-----------+------------+------------+------------+------------+---------------+
| seq_num | time | ttheta | theta | noisy | th_tth_permit |
+-----------+------------+------------+------------+------------+---------------+
| 1 | 18:48:00.6 | 7.0000 | 3.5000 | 61886 | 1.00000 |
| 2 | 18:48:04.3 | 6.5000 | 3.2500 | 45252 | 1.00000 |
| 3 | 18:48:08.0 | 6.0000 | 3.0000 | 14221 | 0.00000 |
+-----------+------------+------------+------------+------------+---------------+
generator scan ['991bdcb1'] (scan num: 928)
[15]:
cat[uids[0]].th_tth_permit_monitor.read()["th_tth_permit"].plot.scatter()
[15]:
<matplotlib.collections.PathCollection at 0x7f34d8ff9c60>
Aha! The first plot shows the monitored values. It is clear the signal does go to 0 and then come back to 1.
Look at the monitored data. First get the run object from databroker (indexed by the first uid in the list). From the run object, return the monitored data in a table. Python prints this object if it is not assigned. All this in one step.
[16]:
cat.v1[uids[0]].table("th_tth_permit_monitor")
[16]:
time | th_tth_permit | |
---|---|---|
seq_num | ||
1 | 2023-04-11 23:47:57.239300013 | 0.0 |
2 | 2023-04-11 23:47:57.804551840 | 1.0 |
3 | 2023-04-11 23:48:01.110384226 | 0.0 |
4 | 2023-04-11 23:48:02.918014526 | 1.0 |
5 | 2023-04-11 23:48:04.721409321 | 0.0 |
We see some zero values (and then return to one) indicating occasional removal of the dynamic limit calculation permit. Since we only saw these when we added a backlash correction, we understand these dynamic violations of the limits are exactly what we hoped to intercept.
Create a suspender#
Block the RunEngine when the permit fails.
N.B. Might need to consider the special case where the permit fails when first starting the run. Why did that fail? (We’ll answer that soon.)
The Bluesky project shows how to suspend a plan when the source X-ray intensity drops too low.
Following this example, we’ll do similar. In our case, the signal is a boolean that indicates no permit when low. The suspender interrupts the RE
as long as the signal is invalid and automatically resumes if the signal becomes valid again. Let’s see how this works here.
[17]:
from bluesky.suspenders import SuspendBoolLow
sus = SuspendBoolLow(th_tth_permit)
RE.install_suspender(sus)
Repeat the scan. Set the minimum separation low enough that the motor backlash will not trip the suspender near the end of the scan. After the previous move, the two motors are close enough that the suspender may trip at the start of the next scan, as the motors are sent to the first position. Let’s move theta
a little lower to avoid that.
[18]:
# Set a smaller minimum separation
%xmode Minimal
print(f"{theta.position=} {ttheta.position=}")
%movr theta -0.2
print(f"{theta.position=} {ttheta.position=}")
uids = RE(th_tth_scan([noisy, th_tth_permit], 7, 6, points=3, min_sep=2.4))
Exception reporting mode: Minimal
theta.position=3.0000000000000004 ttheta.position=6.0
Suspender SuspendBoolLow(EpicsSignal(read_pv='gp:userCalc1.VAL', name='th_tth_permit', value=1.0, timestamp=1681256888.82624, tolerance=1e-05, auto_monitor=True, string=False, write_pv='gp:userCalc1.VAL', limits=False, put_complete=False), sleep=0, pre_plan=None, post_plan=None,tripped_message=) reports a return to nominal conditions. Will sleep for 0 seconds and then release suspension at 2023-04-11 18:48:08.
theta: 81%|████████████████████▏ | 0.1616/0.2 [00:00<00:00, 1.11degrees/s]
theta: 100%|████████████████████████████| 0.2/0.2 [00:00<00:00, 1.23s/degrees]
theta [In progress. No progress bar available.]
theta.position=2.8000000000000003 ttheta.position=6.0
Transient Scan ID: 929 Time: 2023-04-11 18:48:09
Persistent Unique Scan ID: '52b208be-f1a3-42df-8cb4-9c790954e96c'
New stream: 'label_start_motor'
New stream: 'th_tth_permit_monitor'
New stream: 'primary'
+-----------+------------+------------+------------+------------+---------------+
| seq_num | time | ttheta | theta | noisy | th_tth_permit |
+-----------+------------+------------+------------+------------+---------------+
| 1 | 18:48:12.5 | 7.0000 | 3.5000 | 76393 | 1.00000 |
| 2 | 18:48:16.2 | 6.5000 | 3.2500 | 33643 | 1.00000 |
| 3 | 18:48:19.9 | 6.0000 | 3.0000 | 73523 | 1.00000 |
+-----------+------------+------------+------------+------------+---------------+
generator scan ['52b208be'] (scan num: 929)
This succeeded because the minimum separation distance was set smaller than usual, to 2.4
, which allows for a backlash correction (of 0.5 in the ttheta
motor) at the last point.
[19]:
# As in the previous step:
%movr theta -0.2
# set the minimum separation to 2.5
# Likely the scan will fail due to the suspender when the m2 motor is taking a backlash correction and (m2-m1)<3.
uids = RE(th_tth_scan([noisy, th_tth_permit], 7, 6, points=3, min_sep=2.5))
theta: 100%|████████████████████████████| 0.2/0.2 [00:00<00:00, 1.08s/degrees]
theta [In progress. No progress bar available.]
Transient Scan ID: 930 Time: 2023-04-11 18:48:34
Persistent Unique Scan ID: '64c1bf12-041a-4e1c-ba92-0aef01a1e4e7'
New stream: 'label_start_motor'
New stream: 'th_tth_permit_monitor'
New stream: 'primary'
+-----------+------------+------------+------------+------------+---------------+
| seq_num | time | ttheta | theta | noisy | th_tth_permit |
+-----------+------------+------------+------------+------------+---------------+
| 1 | 18:48:37.5 | 7.0000 | 3.5000 | 29638 | 1.00000 |
| 2 | 18:48:41.3 | 6.5000 | 3.2500 | 4031 | 1.00000 |
Suspending....To get prompt hit Ctrl-C twice to pause.
Suspension occurred at 2023-04-11 18:48:42.
Justification for this suspension:
Signal th_tth_permit is low
Suspender SuspendBoolLow(EpicsSignal(read_pv='gp:userCalc1.VAL', name='th_tth_permit', value=1.0, timestamp=1681256930.19391, tolerance=1e-05, auto_monitor=True, string=False, write_pv='gp:userCalc1.VAL', limits=False, put_complete=False), sleep=0, pre_plan=None, post_plan=None,tripped_message=) reports a return to nominal conditions. Will sleep for 0 seconds and then release suspension at 2023-04-11 18:48:50.
| 3 | 18:48:53.1 | 6.0000 | 3.0000 | 32567 | 1.00000 |
+-----------+------------+------------+------------+------------+---------------+
generator scan ['64c1bf12'] (scan num: 930)
The suspender tripped during the scan at the last move when ttheta
was performing its backlash correction. Exactly what we wanted to happen. While moving the motors to the last point of the scan, (m2-m1)<2.5
was satisfied and the suspender tripped. The command line was not responsive. To allow the scan to progress, the ``theta`` motor was moved from some other EPICS client outside of bluesky, allowing the scan to progress. If the suspender delays the scan long enough, the scan will
timeout with a FailedStatus
exception.
When the suspender trips, motion (in bluesky) is not permitted due to our computation of dynamic limits. We must move the motors so that the limit permit is restored before we can scan. We can move the motor from the command line or a GUI application. (This works since other EPICS clients such as the command line do not use this RunEngine instance. The suspender is checked only by this RunEngine.)
The signal (for the suspender) does not automatically clear since it is only computed when the motor readback value changes. We can clear this manually by moving the theta
motor away from the ttheta
motor. Yet, still, we encounter the problem when the two motors are close together either, as in this example.
Conclusion#
We can avoid an anticipated collision of instrument hardware by providing a RunEngine suspender tied to the value of an EPICS PV.