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