Skip to content

Writing a virtual axes controller

A virtual axes controller is designed to build virtual axes over real axes.

  • It can have multiple real axes as entries and can produce multiple virtual axes as outputs

  • Real axes positions can be combined and transformed to compute virtual axes positions

  • A motion command on virtual axes triggers the motion of real axes

  • Motion of real axes automatically updates the virtual axes positions

Screenshot

CalcController base class

YAML configuration of a CalcController

- class: FooCalcController # object class (inheriting from CalcController)
  plugin: generic          # CalcController works with generic plugin
  module: foo_module       # module where the FooCalcController class can be found
  name: foo                # a name for the controller (optional)

  axes:
    - name: $axis_x        # a reference to a real axis (input)
      tags: real rx        # tags for identification within the controller
    - name: $axis_y        # a reference to another real axis (input)
      tags: real ry        # tags for identification within the controller

    - name: radax          # a name for a virtual axis (output)
      tags: radius         # a tag for identification within the controller

About virtual axes type

Virtual axes produced by a CalController are of the type CalcAxis (child class of the Axis class). They behave like a usual real axis and can be moved/manipulated/scanned in exactly the same way.

Class signature

The signature of a CalcController takes a single argument config. It could be a ConfigNode object (from YAML configuration file) or a standard dictionary.

Signature

class CalcController:
    def __init__(self, config):

Inherit from

from bliss.controllers.motor import CalcController

class FooCalcController(CalcController):
    def __init__(self, config):
        super().__init__(config)

Computing methods

While writing a CalcController child class, developers must override the two following methods.

calc_from_real (to be overridden)

def calc_from_real(self, real_user_positions):
    """Computes pseudo dial positions from real user positions.

    => pseudo_dial_positions = self.calc_from_real(real_user_positions)

    Args:
      real_user_positions: { real_tag: real_user_positions, ... }
    Return:
      a dict: {pseudo_tag: pseudo_dial_positions, ...}
    """
    raise NotImplementedError
  • Compute the virtual axes dial positions from the knowledge of real axes user positions
  • Called when the real axes positions have been modified (through motion or update)
  • Must return a dictionary with new positions for all virtual axes

calc_to_real (to be overridden)

def calc_to_real(self, pseudo_dial_positions):
    """Computes reals user positions from pseudo dial positions.

    => real_user_positions = self.calc_to_real(pseudo_dial_positions)

    Args:
      pseudo_dial_positions: {pseudo_tag: pseudo_dial_positions, ...}
    Return:
      a dict: { real_tag: real_user_positions, ... }
    """
    raise NotImplementedError
  • Compute the real axes user positions from the knowledge of virtual axes dial positions
  • Called when the virtual axes are requested to move to new target positions
  • Must return a dictionary with new positions for all real axes

Warning

Those 2 functions must be able to operate on multiple positions per axis passed as numpy arrays, not only scalar values. For example, the check limits feature, performed before a motion, needs to execute the calculation functions with numpy arrays.

About scanning with virtual axes and real axes data

During step scans involving virtual axes, the associated real axes positions will be emitted as additional acquisition channels of the virtual axes, unless emit_real_position is set to False in the controller configuration.

Code example

Slits

For example, the slits controller computes the gap width between two blades and its center’s offset from the positions of the real motors driving the blades.

YAML configuration

  - class: Slits
    axes:
      - name: $s1u
        tags: real up
      - name: $s1d
        tags: real down
      - name: s1g
        tags: gap
      - name: s1o
        tags: offset

Slits class

from bliss.controllers.motor import CalcController

class Slits(CalcController):

    def calc_from_real(self, positions_dict):
        calc_dict = { "offset": (positions_dict["up"] - positions_dict["down"]) / 2.0,
                    "gap"   :  positions_dict["up"] + positions_dict["down"],
                    }
        return calc_dict

    def calc_to_real(self, positions_dict):
        real_dict = { "up"  : (positions_dict["gap"] / 2.0) + positions_dict["offset"],
                    "down": (positions_dict["gap"] / 2.0) - positions_dict["offset"],
                    }
        return real_dict

Usage

>>> config.get('s1g')
>>> move(s1vg, 2)

EnergyCalc

Another common example is the energy controller which computes an energy from the knowledge of a Bragg angle and a type of crystal.

YAML configuration

  - name: ene_calc
    class: EnergyCalcController
    xtals: $crystal_manager
    axes:
      - name: $theta
        tags: real bragg
      - name: axene
        tags: energy

EnergyCalcController class

from bliss.controllers.motor import CalcController

class EnergyCalcController(CalcController):
    def __init__(self, xtals, cfg):
        self._xtals = None
        super().__init__(cfg)

    @property
    def xtals(self):
        if self._xtals is None:
            self._xtals = self.config.config_dict['xtals']
        return self._xtals

    def energy2bragg(self, energy):
        return self.xtals.energy2bragg(energy)

    def bragg2energy(self, bragg):
        return self.xtals.bragg2energy(bragg)

    def calc_to_real(self, positions_dict):
        return {"bragg": self.energy2bragg(positions_dict["energy"])}

    def calc_from_real(self, positions_dict):
        return {"energy": self.bragg2energy(positions_dict["bragg"])}

Usage

>>> config.get('axene')
>>> move(axene, 8.045)