Motion hooks¶
A Motion hook is a way to execute piece of specific code at particular moments of standard axes movments.
A hook can be attached to a motor or a list of motors.
Note
For brave old SPEC users, motion hooks can replace cdef()
To link an axis with a specific hook, a motion_hooks
section has to
be added to an axis YAML configuration. It should be a list of
references to hooks defined somewhere else (see example below).
One hook can be associated with multiple axes.
One axis can have multiple hooks.
A new motion hook definition is a python class which inherits from the
base hook class bliss.common.hook.MotionHook
.
The MotionHook
class has 5 methods that can be overwritten:
init()
pre_move()
post_move()
pre_scan()
post_scan()
The init()
method is called once, when the motion hook is activated for the
first time.
pre_move()
and post_move()
receive a list of bliss.common.axis.Motion
objects, each Motion
object corresponds to an axis being moved, for the
corresponding hook.
post_move()
works in pair with pre_move()
: it is called for each
pre_move()
call, even if pre_move()
fails.
pre_move()
can be used to prevent a motion from occuring if a certain
condition is not satisfied. In this case pre_move()
should raise an
exception explaining the reason.
Warning
Care has to be taken not to trigger a movement of a motor which
is being moved in one of the init()
, pre_move()
or post_move()
method. Doing so will most likely result in an infinite recursion error.
Warning
post_move()
is executed even if pre_move()
fails. User has to
consider possible failures in his code.
pre_scan()
and post_scan()
are called at beginning of a scan (“prepare”
phase) and at the end of a scan (after scan is done), respectively, for each
hooked axis involved in a scan. pre_scan()
and post_scan()
receive the
list of axes of the scan.
In the case of a virtual axis, both the virtual axis and corresponding real axes are passed.
Note
Please note that in case of calculational motor, the hook must be placed on real axis to be called in case of move of both real and virtual axis.
Hooks provided by BLISS¶
BLISS provides some motion hooks that can be found in
bliss.controllers.motors.hooks
:
SleepHook
: Wait a specified amount of time before and/or after motion.WagoHook
: Wago generic value hook. Applypre_move
/post_move
values before/after moving.AirpadHook
: Wago air-pad hook. Turn on/off air-pad before/after moving.WagoAirHook
: Wago air hook. Turn on/off air (pad; brake; etc.) before/after moving.ScanWagoHook
: Multi axis WagoHook butinit
,pre_move
,post_move
are not compulsory.
SleepHook¶
Wait a specified amount of time before and/or after motion. Useful when you cannot query the control system when a pre or post move condition has finished (ex: when using air-pads you cannot usualy query when the air-pad has finished inflating/deflating so you need to wait an arbitrary time after you ask it to inflate/deflate)
Configuration example:
name: ngy_airpad
class: SleepHook
module: motors.hooks
pre_move_wait: 0.5 # optional (default: 0s)
post_move_wait: 0.3 # optional (default: 0s)
WagoHook¶
Wago generic value hook. Apply pre_move value before moving and post_move value after moving.
Configuration example:
name: ngy_airpad
class: WagoHook
module: motors.hooks
wago: $wcid00a
channel: ngy_air
pre_move:
value: 1
wait: 1 # optional (default: 0s)
post_move:
value: 0
wait: 1 # optional (default: 0s)
AirpadHook¶
Wago air-pad hook. Turn on air-pad before moving. Turn off air-pad after moving.
Configuration example:
name: ngy_airpad
class: AirpadHook
module: motors.hooks
wago: $wcid00a
channel: ngy_air
pre_move:
wait: 1 # optional (default: 0s)
post_move:
wait: 2 # optional (default: 0s)
WagoAirHook¶
Wago air hook. Turn on air (pad/brake/…) before moving. Turn off air (pad/brake/…) after moving.
- Optionally a channel_in can be added to get an hardware check like pressostat device which tells if air is really on or off.
- Optionally a direction can be specified to limit the hook to one motion direction: positive (+1), negative (-1) or for both (0).
Configuration example:
name: ccm_brake
class: WagoAirHook
module: motors.hooks
wago: $wcid10b
channel: ccmbrk
channel_in: ccmpress # optional
direction: 1 # optional 1/0/-1 (default: 0)
pre_move:
wait: 1 # optional (default: 0s)
post_move:
wait: 2 # optional (default: 0s)
ScanWagoHook¶
Wago generic value hook with special behaviour during scans.
Apply:
init
before starting motion, but only first motion of a scan, (and before checking limits)pre_move
before moving, but only at first motion of a scanpost_move
after moving, but only after last motion of a scan
Main differences with WagoHook:
- the same hook can be attached to several axis
- init, pre_move, post_move are not compulsory, only the defined ones will be executed
Configuration example:
name: ngy_airpad
class: ScanWagoHook
module: motors.hooks
wago: $wcid00a
channel: ngy_air
init:
value: 1
wait: 1 # optional (default: 0s)
post_move:
value: 0
wait: 1 # optional (default: 0s)
Example: motor with air-pad¶
Imagine a motor m1
that move a heavy granite table. Before it moves, an
air-pad must be filled with air by triggering a PLC and after the motion ends,
the air-pad must be emptied. Further, since there is no pressure meter, it has
been determined empirically that after the air-pad fill command is sent to the
PLC, a 1s wait must be done for the pressure to reach a good value before moving
and wait 2s after the motion is finished.
So the hook implementation will look something like this:
# bliss/controllers/motors/airpad.py
import gevent
from bliss.common.hook import MotionHook
class AirpadHook(MotionHook):
"""air-pad motion hook"""
def __init__(self, name, config):
self.config = config
self.name = name
self.plc = config['plc']
self.channel = config['channel']
super().__init__()
def pre_move(self, motion_list):
self.plc.set(self.channel, 1)
gevent.sleep(1)
def post_move(self, motion_list):
self.plc.set(self.channel, 0)
gevent.sleep(2)
Here is the corresponding YAML configuration:
# motors.yml
plcs:
- name: plc1
# here follows PLC configuration
hooks:
- name: airpad_hook
plugin: bliss
package: bliss.controllers.motors.airpad
class: AirpadHook
plc: $plc1
motors:
- controller: Mockup
plugin: emotion
axes:
- name: m1
# here follows motor configuration
motion_hooks:
- $airpad_hook
Note that in this example only one hook was used for the m1
motor. A list of
hooks can be defined to be executed if needed.
The hooks are executed in the order given in the motion_hooks
list.
Example: preventing collisions¶
Hooks can be used to prevent a motion from occuring if certain conditions are not met.
Lets consider two detectors which can move in the XY plane and collisions between them have to be avoided.
det1 can only move in the Y axis using motor det1y
an det2 can
move in the X and Y axis using motors det2x
and det2y
.
Lets say that det1 is located at X1=10, Y1=200 when det1y=0
. For
collision purposes it is suficient to approximate the detector geometry
by a sphere of radius R1=5.
Lets say that det2 is located at X1=10, Y1=10 when det2x = 0
and det2y = 0
. For collision purposes it is suficient to approximate
the detector geometry by a sphere of radius R1=15.
So, every time that at least one of the three motors det1y
, det2x
or
det2y
moves, a pre-check needs to be made to be sure the motion is not
going to collide the two detectors.
The code should look like:
# bliss/controllers/motors/coldet.py
import math
import collections
Point = collections.namedtuple('Point', 'x y')
from bliss.common.hook import MotionHook
class DetectorSafetyHook(MotionHook):
"""Equipment protection of pair of detectors"""
D1_REF = Point(10, 200)
D2_REF = Point(10, 10)
SAFETY_DISTANCE = 5 + 15
class SafetyError(Exception):
pass
def __init__(self, name, config):
self.axes_roles = {}
super(DetectorSafetyHook, self).__init__()
def init(self):
# store which axis has which
# roles in the system
for axis in self.axes.values():
tags = axis.config.get('tags')
if 'd1y' in tags:
self.axes_roles[axis] = 'd1y'
elif 'd2x' in tags:
self.axes_roles[axis] = 'd2x'
elif 'd2y' in tags:
self.axes_roles[axis] = 'd2y'
else:
raise KeyError('detector motor needs a safety role')
def pre_move(self, motion_list):
# determine desired positions of all detector motors:
# - if motor in this motion, get its target position
# - otherwise, get its current position
target_pos = dict([(axis, axis.position) for axis in self.axes_roles])
for motion in motion_list:
if motion.axis in target_pos:
target_pos[motion.axis] = motion.target_pos \
/ motion.axis.steps_per_unit \
* motion.axis.sign
# build target positions by detector motor role
target_pos_role = dict([(self.axes_roles[axis], pos)
for axis, pos in target_pos.items()])
# calculate where detectors will be in space
d1 = Point(self.D1_REF.x,
self.D1_REF.y + target_pos_role['d1y'])
d2 = Point(self.D2_REF.x + target_pos_role['d2x'],
self.D2_REF.y + target_pos_role['d2y'])
# calculate distance between center of each detector
distance = math.sqrt((d2.x - d1.x)**2 + (d2.y - d1.y)**2)
if distance < self.SAFETY_DISTANCE:
raise self.SafetyError('Cannot move: motion would result ' \
'in detector collision')
Warning
Note that motion.target_pos
is in controler units.
Warning
motion.user_target_pos
does NOT take backlash into account.
Find below the corresponding YAML configuration:
hooks:
- name: det_hook
class: DetectorSafetyHook
module: motors.coldet
plugin: bliss
controllers:
- name: det1y
acceleration: 10
velocity: 10
steps_per_unit: 1
low_limit: -1000
high_limit: 1000
tags: d1y
unit: mm
motion_hooks:
- $det_hook
- name: det2x
acceleration: 10
velocity: 10
steps_per_unit: 1
low_limit: -1000
high_limit: 1000
tags: d2x
unit: mm
motion_hooks:
- $det_hook
- name: det2y
acceleration: 10
velocity: 10
steps_per_unit: 1
low_limit: -1000
high_limit: 1000
tags: d2y
unit: mm
motion_hooks:
- $det_hook
Note
For demonstration purposes, these examples are minimalistic and do no error checking for example. Feel free to use this code but please take this into account.