Source code for contrast.motors.LC400
"""
Provides a Motor subclass for the Npoint LC400 piezo driver, and a
helper class for generating waveforms. Relies on the Tango server
developed at MAX IV (available on request!):
* https://gitlab.maxiv.lu.se/kits-maxiv/lib-maxiv-npoint-lc400
* https://gitlab.maxiv.lu.se/kits-maxiv/dev-maxiv-npoint-lc400
"""
import PyTango
from . import Motor
import math
import json
import numpy as np
[docs]class LC400Motor(Motor):
"""
Single axis on the LC400.
"""
def __init__(self, device, axis, **kwargs):
"""
:param device: Path to the underlying Tango device.
:type device: str
:param axis: Axis number on the Tango controller
:param ``**kwargs``: Passed on to the base class constructor
"""
super(LC400Motor, self).__init__(**kwargs)
assert axis in (1, 2, 3)
self.proxy = PyTango.DeviceProxy(device)
self.proxy.set_source(PyTango.DevSource.DEV)
self.axis = axis
self._format = '%.3f'
@property
def dial_position(self):
if self.axis == 1:
val = self.proxy.axis1_position
elif self.axis == 2:
val = self.proxy.axis2_position
elif self.axis == 3:
val = self.proxy.axis3_position
return val
@dial_position.setter
def dial_position(self, pos):
if self.axis == 1:
self.proxy.axis1_position = pos
elif self.axis == 2:
self.proxy.axis2_position = pos
elif self.axis == 3:
self.proxy.axis3_position = pos
def busy(self):
attribute = 'axis%d_position_on_target' % self.axis
on_target = self.proxy.read_attribute(attribute).value
return not on_target
def stop(self):
self.proxy.stop_waveform()
self.proxy.stop_recording()
[docs]class LC400Waveform(object):
"""
Class to generate waveform configs for the LC400 piezo controller.
It generates a JSON string, that can be send to the LC400 Tango
Server to configure the waveform.
"""
# constants
# 24 µs, internal clock cycle of LC.400
CLOCKCYCLE = 0.000024
# maximum number of points in a waveform
MAXPOINTS = 83333
def __init__(self, axis, startpoint, endpoint, scanpoints, exposuretime,
latencytime, accelerationtime, decelerationtime=None,
startvelocity=None, endvelocity=None):
# waveform parameters
self.axis = axis
self.axisname = "axis%i" % axis
stepsize = (endpoint - startpoint) / (scanpoints - 1)
print("step size: ", stepsize)
self.startpoint = startpoint
self.endpoint = endpoint + stepsize
self.scanpoints = scanpoints # scanpoints is intervals+1
self.exposuretime = exposuretime
self.latencytime = latencytime
self.accelerationtime = accelerationtime
if decelerationtime is None:
# deceleration time not set, use acceleration time
self.decelerationtime = accelerationtime
else:
self.decelerationtime = decelerationtime
# calculate linear velocity
self.velocity = ((self.endpoint - self.startpoint)
/ ((self.scanpoints) * (exposuretime + latencytime)))
print(f"velocity: {self.velocity:0.3} microns/s")
# set start velocity
if startvelocity is None:
# start velocity not set, use 0 as start velocity
self.startvelocity = 0
else:
self.startvelocity = startvelocity
# set end velocity
if endvelocity is None:
# end velocity not set, use start velocity
self.endvelocity = self.startvelocity
else:
self.endvelocity = endvelocity
# TODO check for valid start and end velocities, e.g.
# startvelocity < velocity
# calculate total time of line
# (acceleraton phase + linear regime + deceleration phase)
self.lineartime = (self.scanpoints
* (self.exposuretime + self.latencytime))
self.totaltime = (self.accelerationtime + self.lineartime
+ self.decelerationtime)
# deterimine clock cycle delay of LC.400, 0 means no delay
self.clockcycledelay = math.ceil(
math.floor(self.totaltime / self.CLOCKCYCLE) / self.MAXPOINTS) - 1
print("clock cycle delay: ", self.clockcycledelay)
# determine optimal number of points in waveform
self.effectiveclockcycle = self.CLOCKCYCLE * (self.clockcycledelay + 1)
self.waveformpoints = math.floor(self.totaltime
/ self.effectiveclockcycle)
# define start cycle of linear phase.
# This is the time when the LC 400 creates the trigger to start
# the pandabox for position recording and triggering of other
# detectors
self.linearstartindex = math.ceil(self.accelerationtime
/ self.effectiveclockcycle)
print("triggger start index: ", self.linearstartindex)
# start point of the waveform in absolute coordinates of the scanner
self.absolutstartposition = self.accelerationphase(0)
print("start position of line is at:", self.absolutstartposition)
print("scan points: ", self.scanpoints)
def accelerationphase(self, t):
# calculate parameters for trajectory
a = (self.velocity - self.startvelocity) / self.accelerationtime
b = 1 / self.accelerationtime
c = (self.velocity - self.startvelocity) / self.accelerationtime
d = self.startvelocity
e = (self.startpoint - self.startvelocity * self.accelerationtime
- (0.5 + 1 / (4 * np.pi**2))
* (self.velocity - self.startvelocity)
* self.accelerationtime)
# calculate trajectory
x = (a / (4 * np.pi**2 * b**2) * np.cos(2 * np.pi * b * t)
+ c / 2 * t**2 + d * t + e)
return x
def linearphase(self, t):
x = self.startpoint + self.velocity * (t - self.accelerationtime)
return x
def decelerationphase(self, t):
# calculate parameters for trajectory
T_e = self.accelerationtime + self.lineartime
a = (self.velocity - self.endvelocity) / self.decelerationtime
b = 1 / self.decelerationtime
c = (self.velocity - self.endvelocity) / self.decelerationtime
# d1 and d2 should be equivalent
d1 = self.velocity + c * T_e
d2 = self.endvelocity + c * (T_e + self.decelerationtime)
d = d2
e = (self.endpoint + a / (4 * np.pi**2 * b**2) + 1 / 2 * c * T_e**2
- d * T_e)
# calculate trajectory
x = (-a / (4 * np.pi**2 * b**2) * np.cos(2 * np.pi * b * (t - T_e))
- c / 2 * t**2 + d * t + e)
return x
def time(self):
t = np.arange(0, self.totaltime, step=self.effectiveclockcycle)
return t
def waveform(self):
x = []
for t in self.time():
if t < self.accelerationtime:
x.append(self.accelerationphase(t))
elif (t >= self.accelerationtime
and t <= self.accelerationtime + self.lineartime):
x.append(self.linearphase(t))
elif t > self.accelerationtime + self.lineartime:
x.append(self.decelerationphase(t))
else:
print("not used: ", t)
if len(x) > self.MAXPOINTS:
raise Exception("waveform too long")
print(f"points in wafeform : {len(x)}")
# offset whole waveform so it starts at 0.
# Waveforms in the LC400 are relative motions with respect to
# the physical start position of the motor
offset = self.accelerationphase(0)
res = [i - offset for i in x]
return res
[docs] def reset_json(self):
"""
Create three waveform jsons which deconfigure each channel:
r1, r2, r2 = reset_json()
"""
js = []
for ax in ('axis1', 'axis2', 'axis3'):
data = {}
data["generator"] = "pointlist2"
data["version"] = "2.0"
# a bug in the LC400 Tango Server substracts 1, se we have to add 1
data["ClockScaling"] = 1 + 1
data["TotalPoints"] = 1
data["EndLinePoint"] = 1 # is this needed?
data["FlyScanAxis"] = int(ax[-1])
# Trigger on/off indeces
triggers = {}
triggers["count"] = 1
triggers["on"] = [0]
triggers["off"] = [0]
# function definition of output pins
# we use pin 9 (Out 4) to create the start trigger
output_pins = {}
output_pins["9"] = {"polarity": 0,
"function": 0}
data[ax] = {"triggers": triggers,
"output_pins": output_pins,
"positions": [0.]}
js.append(json.dumps(data))
return js
def json(self):
# create json string for LC400 waveform configuration
data = {}
data["generator"] = "pointlist2"
data["version"] = "2.0"
# a bug in the LC400 Tango Server substracts 1, se we have to add 1
data["ClockScaling"] = self.clockcycledelay + 1
data["TotalPoints"] = self.waveformpoints
data["EndLinePoint"] = self.waveformpoints # is this needed?
data["FlyScanAxis"] = self.axis
# Trigger on/off indeces
triggers = {}
triggers["count"] = 1
triggers["on"] = [self.linearstartindex]
triggers["off"] = [self.linearstartindex + 1]
# function definition of output pins
# we use pin 9 (Out 4) to create teh start trigger
output_pins = {}
output_pins["9"] = {"polarity": 0,
"function": 3}
data[self.axisname] = {"triggers": triggers,
"output_pins": output_pins,
"positions": self.waveform()}
return json.dumps(data)