seleneGuide
Please read the documentation of sinqMotor first: https://git.psi.ch/sinq-epics-modules/sinqmotor
Overview
This is a driver for the Selene guide which consists of three beams supported by two linear axes each. These six physical axes are transformed into 8 virtual axes:
- A virtual "lift axis" which represents the general elevation of the entire guide
- A virtual "angle axis" which represents the general tilt of the entire guide
- Six "offset axes" which represent deviations of individual axes from the general guide elevation and tilt.
The image below shows the used coordinate systems and the axes:
The dash-dotted line represents the guide, from which the individual grey beams may deviate due to an offset. The horizontal positions of the individual axes are defined such that axis 1 has the position 0, while the other axes have a position larger than zero. The x-position of the lift and angle axes is at half the distance between axis 1 and axis 6. These position values can be defined in the axes constructors in the IOC shell. The z-origins of the individual axes are transformed to a common baseline so that their individual position value is zero when both lift and angle axis position are also zero.
These definitions shall be illustrated by analyzing the example image: lift (z_{lift}
) and angle axis (\alpha
) both have a value smaller zero, offset axis 1 has a positive value \Delta z_1
, offset axis 2 a smaller negative value \Delta z_2
. The offset axes 3 (\Delta z_3
) and 4 (\Delta z_4
) are both zero, while 5 (\Delta z_5
) and 6 (\Delta z_6
) have a positive value.
Since we have 8 independent virtual axes, but only 6 underlying physical ones, the entire system is overdefined (different position values for individual axes can lead to the same resulting positions). In order to make the system well defined, the following "normalization" procedure is used: The offsets of axis 3 and 4 are defined to be zero and all other axis positions are calculated depending on this definition. In particular, this means that the lift and angle position are calculated as:
z_{lift} = 0.5 \cdot (z_3 + z_4)
\alpha_{angle} = \arctan((z_4 - z_3) / (x_4 - x_3))
The other offsets are then calculated as:
z_i = z_{lift} + \tan(\alpha_{angle}) \cdot (x_i - x_{lift})
This normalization procedure can be triggered from a special EPICS PV (see section user guide).
This driver is based on the sinqMotor shared library (https://git.psi.ch/sinq-epics-modules/sinqmotor). The header files contain detailed documentation for all public functions. The headers themselves are exported when building the library to allow other drivers to depend on this one.
User guide
As discussed in the overview section, the selene guide is represented by the six offset axes, the lift and the angle axes. To use the selene guide, all eight axes need to be initialized in EPICS. From the NICOS side, the axes can be treated as "normal" motor record axes (see section NICOS setup).
Two additional PVs are provided in db/seleneGuide.db
:
<motor PV>:AbsolutePositionRBV
: Holds the absolute position of the offset axes as reported from the encoder. For lift and angle axis, its value is undefined and should not be relied on.<motor PV>:Normalize
: Setting this to 1 triggers a normalization (even if the PV already has the value 1). Note that this does not move the motors, it is just a mathematical operation and therefore does not change the absolute motor positions.
Usage in IOC shell
seleneGuide exports the following IOC shell functions:
seleneGuideController
: Create a new controller object. This object is essentially aturboPmacController
object from the standard Turbo PMAC driver, but it supports the additional motor PVs for absolute position and normalization. This means that it can be used with "normal"turboPmacAxis
as well.seleneOffsetAxis
: Create a new offset axis object with the specified x- and z-offset from an arbitrary origin in mm.seleneVirtualAxes
: Create the virtual lift and the angle axes.
The full seleneGuide.cmd
file looks like this:
# Define the name of the controller and the corresponding port
epicsEnvSet("NAME","turboPmac1")
epicsEnvSet("ASYN_PORT","p$(NAME)")
# Create the TCP/IP socket used to talk with the controller. The socket can be adressed from within the IOC shell via the port name
drvAsynIPPortConfigure("$(ASYN_PORT)","172.28.101.24:1025")
# Create the controller object with the defined name and connect it to the socket via the port name.
# The other parameters are as follows:
# 10: Maximum number of axes
# 0.05: Busy poll period in seconds
# 1: Idle poll period in seconds
# 0.3: Socket communication timeout in seconds
seleneGuideController("$(NAME)", "$(ASYN_PORT)", 10, 0.05, 1, 0.3);
# Create the selene offset axes with indices 1 to 6 and the specified x- and
# z-offsets from axis 1 (therefore axis 1 has zero in both entries) in mm.
# The coordinate system of the second axis then has an x-offset from 1.5 m and
# a z-offset of 0 m. The other axes are positioned accordingly.
seleneOffsetAxis("$(NAME)",1, 0.0, 0.0);
seleneOffsetAxis("$(NAME)",2, 1529.43329, 0.0);
seleneOffsetAxis("$(NAME)",3, 2966.59203, 0.0);
seleneOffsetAxis("$(NAME)",4, 4496.35897, 0.0);
seleneOffsetAxis("$(NAME)",5, 5933.99477, 0.0);
seleneOffsetAxis("$(NAME)",6, 7463.87259, 0.0);
# These are two "normal" PMAC axes which work the same way they would with a turboPmacController.
turboPmacAxis("$(NAME)",7);
turboPmacAxis("$(NAME)",8);
# This function call creates the lift and the angle axes.
# The arguments on position 2 to 7 are the axis indices of the offset axes
# belonging to the virtual axes. The 8th index "9" is the index of the lift axis,
# the 9th index "10" is the index of the angle axis.
seleneVirtualAxes("$(NAME)", 1, 2, 3, 4, 5, 6, 9, 10);
# Parametrize the EPICS record database with the substitution file named after the motor controller.
# In order to provide the PVs for absolute position and normalization, the
# corresponding `seleneGuide.db` file is loaded here as well.
epicsEnvSet("SINQDBPATH","$(seleneGuide_DB)/sinqMotor.db")
dbLoadTemplate("$(TOP)/motors/$(NAME).substitutions", "INSTR=$(INSTR)$(NAME):,CONTROLLER=$(NAME)")
epicsEnvSet("SINQDBPATH","$(seleneGuide_DB)/turboPmac.db")
dbLoadTemplate("$(TOP)/motors/$(NAME).substitutions", "INSTR=$(INSTR)$(NAME):,CONTROLLER=$(NAME)")
epicsEnvSet("SINQDBPATH","$(seleneGuide_DB)/seleneGuide.db")
dbLoadTemplate("$(TOP)/motors/$(NAME).substitutions", "INSTR=$(INSTR)$(NAME):,CONTROLLER=$(NAME)")
dbLoadRecords("$(seleneGuide_DB)/asynRecord.db","P=$(INSTR)$(NAME),PORT=$(ASYN_PORT)")
Substitution file
Since the position value of the offset axes is relative to those of lift axis and angle axis, the limits are relative as well. Therefore they need to specified manually in the substitution file. As described in the limit calculation section for offset axes, these limits are shrunken accordingly if they would exceed the physical limits of the underlying physical axes.
The relative limits for the offset axes are specified via the DHLM and DLLM fields in engineering units EGU. Lift and angle axis are absolute axes and their limits get calculated directly from the physical axes limits, therefore any values set into the DHLM and DLLM columns get overwritten during IOC initialization as well as during each poll.
file $(SINQDBPATH)
{
pattern
{ AXIS, M, EGU, DIR, MRES, ENABLEMOVWATCHDOG, LIMITSOFFSET, DHLM, DLLM }
{ 1, "offsetAxis1", mm, Pos, 0.000001, 0, 0.0, 1.5, -1.5 }
{ 2, "offsetAxis2", mm, Pos, 0.000001, 0, 0.0, 1.5, -1.5 }
{ 3, "offsetAxis3", mm, Pos, 0.000001, 0, 0.0, 1.5, -1.5 }
{ 4, "offsetAxis4", mm, Pos, 0.000001, 0, 0.0, 1.5, -1.5 }
{ 5, "offsetAxis5", mm, Pos, 0.000001, 0, 0.0, 1.5, -1.5 }
{ 6, "offsetAxis6", mm, Pos, 0.000001, 0, 0.0, 1.5, -1.5 }
{ 7, "mot1", mm, Pos, 0.001, 1, 0.001, 0.0, 0.0 } # DHLM and DLLM get overwritten anyway
{ 8, "mot2", mm, Pos, 0.001, 1, 0.001, 0.0, 0.0 } # DHLM and DLLM get overwritten anyway
{ 9, "liftAxis", mm, Pos, 0.000001, 0, 0.0, 0.0, 0.0 } # DHLM and DLLM get overwritten anyway
{ 10, "angleAxis", degree, Pos, 0.000001, 0, 0.0, 0.0, 0.0 } # DHLM and DLLM get overwritten anyway
}
NICOS setup
The code block below shows the Selene guide setup in NICOS in a simplified for the substitution file given above:
description = 'Selene support stages'
display_order = 36
# Controller PV
pvprefix = '<controller PV name in EPICS>'
devices = dict(
# Offset motors are normal SinqMotor devices and are therefore configured
# in the same manner.
offsetMot1 = device('nicos_sinq.devices.epics.motor.SinqMotor',
motorpv = pvprefix + 'offsetAxis1',
unit='mm',
),
# The absolute positions are simple EpicsReadable devices which use the
# AbsolutePositionRBV PV from the associated axis. It is recommended to add
# a small pollinterval, because otherwise the absolute values will not be
# updated fast enough when moving the motor.
offsetMotAbs1 = device('nicos.devices.epics.pyepics.pyepics.EpicsReadable',
readpv = pvprefix + 'offsetAxis1:AbsolutePositionRBV',
pollinterval = 0.5,
unit='mm',
),
offsetMot2 = device('nicos_sinq.devices.epics.motor.SinqMotor',
motorpv = pvprefix + 'offsetAxis2',
unit='mm',
),
offsetMotAbs2 = device('nicos.devices.epics.pyepics.pyepics.EpicsReadable',
readpv = pvprefix + 'offsetAxis2:AbsolutePositionRBV',
pollinterval = 0.5,
unit='mm',
),
offsetMot3 = device('nicos_sinq.devices.epics.motor.SinqMotor',
motorpv = pvprefix + 'offsetAxis3',
unit='mm',
),
offsetMotAbs3 = device('nicos.devices.epics.pyepics.pyepics.EpicsReadable',
readpv = pvprefix + 'offsetAxis3:AbsolutePositionRBV',
pollinterval = 0.5,
unit='mm',
),
offsetMot4 = device('nicos_sinq.devices.epics.motor.SinqMotor',
motorpv = pvprefix + 'offsetAxis4',
unit='mm',
),
offsetMotAbs4 = device('nicos.devices.epics.pyepics.pyepics.EpicsReadable',
readpv = pvprefix + 'offsetAxis4:AbsolutePositionRBV',
pollinterval = 0.5,
unit='mm',
),
offsetMot5 = device('nicos_sinq.devices.epics.motor.SinqMotor',
motorpv = pvprefix + 'offsetAxis5',
unit='mm',
),
offsetMotAbs5 = device('nicos.devices.epics.pyepics.pyepics.EpicsReadable',
readpv = pvprefix + 'offsetAxis5:AbsolutePositionRBV',
pollinterval = 0.5,
unit='mm',
),
offsetMot6 = device('nicos_sinq.devices.epics.motor.SinqMotor',
motorpv = pvprefix + 'offsetAxis6',
unit='mm',
),
offsetMotAbs6 = device('nicos.devices.epics.pyepics.pyepics.EpicsReadable',
readpv = pvprefix + 'offsetAxis6:AbsolutePositionRBV',
pollinterval = 0.5,
unit='mm',
),
# Configuration of the lift axis. Again, since this is a "normal" motor
# record, it can be controlled with the normal SinqMotor device from NICOS.
liftAxis = device('nicos_sinq.devices.epics.motor.SinqMotor',
motorpv = pvprefix + 'liftAxis',
unit='mm',
),
# Configuration of the angle axis. Again, since this is a "normal" motor
# record, it can be controlled with the normal SinqMotor device from NICOS.
angleAxis = device('nicos_sinq.devices.epics.motor.SinqMotor',
motorpv = pvprefix + 'angleAxis',
unit='degree',
),
# This device writes to the "Normalize" PV in order to trigger a
# normalization. Since we want to offer the user a descriptive button, this
# device is hidden and used as the backend of a Switcher device.
normalize_num = device('nicos.devices.epics.pyepics.pyepics.EpicsDigitalMoveable',
readpv = pvprefix + 'liftAxis:Normalize',
writepv = pvprefix + 'liftAxis:Normalize',
visibility=(),
),
# This switcher is the frontend for the normalize_num device. It offers a
# single button "Normalize offsets" which upon pressing it sets the
# Normalize PV to 1 and therefore triggers a normalization.
normalize = device('nicos.devices.generic.switcher.Switcher',
moveable = 'normalize_num',
mapping = {
'Normalize offsets': 1,
},
precision = 0,
),
)
Developer guide
Limit calculation
Offset axes
As mentioned in the user guide, the limits of the offset axes are specified in the substitution file and depending on the physical limits further shrunken down if the relative limits would exceed the absolute limits. In the image given above, this is the case for axis 6. If an axis moves, the limits move accordingly:
z_{iH} = z_i + \mathrm{DHLM}_i
z_{iL} = z_i + \mathrm{DLLM}_i
If z_{iH}
would exceed the corresponding absolute high limit z_{iH,abs}
, it is set to z_{iH,abs}
. The same holds true for the lower limits.
Lift axis
For the lift axis, the limits are calculated by evaluating the available track length in both directions for each axis (see image)
and finding the shortest value for the distance to high and low limit (distances are always positive). In the example image, these values are \Delta z_{1L}
and \Delta z_{6H}
respectively. The limits of the lift axis are then calculated as:
z_H = z + \Delta z_{6H}
z_L = z - \Delta z_{1L}
Angle axis
The angle limits are also calculated by looping through each axis and deriving the limits individually, then selecting the smallest value for both upper and lower limit.
In order to derive the corresponding equations, let's take axis 1 as an example. Since the motor has an offset \Delta z_1
, the guide may be rotated in positive direction up to the green dashed line, which crosses the axis position x_1
at the height z_{1H} - \Delta z_1
. The corresponding angle \alpha_{1H}
can then be calculated from the corresponding triangle as:
\alpha_{1H} = -\arctan((z_{1H} - z_{guide} - \Delta z_1) / (x_1 - x_{guide}))
In the same manner, the lower limit is calculated as
\alpha_{1L} = -\arctan((z_{1L} - z_{guide} - \Delta z_1) / (x_1 - x_{guide}))
The minus signs result from the convention of the angle coordinate system, which is mathematically negative (hence we need to invert the limits as well).
For axes which are located behind the center of rotation (lever arm x_i - x_{guide}
is positive), the roles of the axes high and low limits in the calculation change. For example, the equations for axis 6 (orange) are:
\alpha_{6H} = -\arctan((z_{6L} - z_{guide} - \Delta z_6) / (x_6 - x_{guide}))
\alpha_{6L} = -\arctan((z_{6H} - z_{guide} - \Delta z_6) / (x_6 - x_{guide}))
Versioning
Please see the documentation for the module sinqMotor: https://git.psi.ch/sinq-epics-modules/sinqmotor/-/blob/main/README.md.
How to build it
Please see the documentation for the module sinqMotor: https://git.psi.ch/sinq-epics-modules/sinqmotor/-/blob/main/README.md.