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 users of SPEC, 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.

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.

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
    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.