Skip to content

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. Apply pre_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 but init, 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 scan
  • post_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.