from .Scan import SoftwareScan
from .AScan import DScan
from ..environment import macro, MacroSyntaxError
from ..motors import all_are_motors
import numpy as np
import time
from .Scan import SoftwareScan
from .AScan import DScan, AScan
from ..environment import macro, MacroSyntaxError
from ..motors import all_are_motors
import numpy as np
import time
[docs]
@macro
class SpiralScan(DScan):
"""
Software scan across a 2D Archimedes spiral centered on the
current position. ::
spiralscan <motor1> <motor2> <stepsize> <positions> <exp_time>
"""
def __init__(self, m1, m2, stepsize, npos, exptime, **kwargs):
# Parse arguments. We're inheriting DScan to get its nice run()
# method, but we'll call the SoftwareScan constructor anyway bacause
# we're not interested in DScan's way of parsing arguments.
try:
SoftwareScan.__init__(self, float(exptime))
self.motors = [m1, m2]
self.stepsize = float(stepsize)
self.n_positions = int(npos)
assert all_are_motors(self.motors)
except:
raise MacroSyntaxError
def _generate_positions(self):
starting = [m.position() for m in self.motors]
for t in range(self.n_positions):
A = self.stepsize * np.sqrt(t/np.pi)
B = np.sqrt(4*np.pi*t)
yield {self.motors[0].name: starting[0] + A * np.cos(B),
self.motors[1].name: starting[1] + A * np.sin(B)}
[docs]
@macro
class SpiralAScan(DScan):
"""
Software scan across a 2D Archimedes spiral centered on a
given position. :
spiralascan <motor1> <pos1> <motor2> <pos2> <stepsize> <positions> <exp_time>
"""
def __init__(self, m1, pos1, m2, pos2, stepsize, npos, exptime, **kwargs):
try:
SoftwareScan.__init__(self, float(exptime))
self.motors = [m1, m2]
self.starting = [pos1, pos2]
self.stepsize = float(stepsize)
self.n_positions = int(npos)
assert all_are_motors(self.motors)
except:
raise MacroSyntaxError
def _generate_positions(self):
for t in range(self.n_positions):
A = self.stepsize * np.sqrt(t/np.pi)
B = np.sqrt(4*np.pi*t)
yield {self.motors[0].name: self.starting[0] + A * np.cos(B),
self.motors[1].name: self.starting[1] + A * np.sin(B)}
[docs]
@macro
class FermatScan(AScan):
"""
Software scan in the shape of a fermat spiral (good for ptychography scan).
The center of the spiral will be the center of the given rectangle.
Both motors have to be in the same units. ::
fermatscan <motor1> <start> <stop> <motor2> <start> <stop> <stepsize> <exp_time>
optional keyword arguments:
optimize: boolean ... will try to solve the traveling salesman problem
WARNING: can be slow for large scans!
"""
def __init__(self, m1, l1_l, l1_u, m2, l2_l, l2_u, stepsize, exptime, sort_by_axis=1, **kwargs):
try:
SoftwareScan.__init__(self, float(exptime))
self.motors = [m1, m2]
self.limits = [[l1_l, l1_u],[l2_l, l2_u]]
self._check_limits()
self.stepsize = float(stepsize)
self.sort_by_axis = sort_by_axis
self.optimize = False
self.calc_positions()
self.n_positions = int(len(self.pos_12))
assert all_are_motors(self.motors)
except Exception as E:
raise MacroSyntaxError
def _check_limits(self):
# just making sure the given upper and lower limits to the motors are the right way around
if self.limits[0][0] > self.limits[0][1]:
#print('inverted limits in the first motor... switching them')
self.limits[0][0], self.limits[0][1] = self.limits[0][1], self.limits[0][0]
if self.limits[1][0] > self.limits[1][1]:
#print('inverted limits in the second motor... switching them')
self.limits[1][0], self.limits[1][1] = self.limits[1][1], self.limits[1][0]
# check that the upper and lower limits are not identical
if (self.limits[0][0] == self.limits[0][1]) or (self.limits[1][0] == self.limits[1][1]):
print('[!] upper and lower limits are identical for at least one of the motors')
raise MacroSyntaxError
def calc_positions(self):
# scaling factors and angular step width
c_0 = 0.524 # 3rd closest neighbor is on average one step away
c = c_0*self.stepsize
phi = 0.5*(1+np.sqrt(5))
phi_0 = 2*np.pi/(1+phi)
# center and size of the rectangular scan field
center = [0.5*(l[0]+l[1]) for l in self.limits]
size = [np.abs(l[0]-l[1]) for l in self.limits]
# max radius of and scan points in the spiral
r_max = 0.5*np.sqrt(size[0]**2+size[1]**2)
n_max = int(np.ceil((r_max/c)**2))
# calculate all positions until n_max (and thus r_max)
n = np.linspace(0,n_max,n_max+1, endpoint=True)
pos_1 = c*np.sqrt(n)*np.sin(n*phi_0)+center[0]
pos_2 = c*np.sqrt(n)*np.cos(n*phi_0)+center[1]
# remove positions outside the scan rectangle
pos_12 = []
for i, p1 in enumerate(pos_1):
p2 = pos_2[i]
if not(p1>self.limits[0][0]):
continue
if not(p1<self.limits[0][1]):
continue
if not(p2>self.limits[1][0]):
continue
if not(p2<self.limits[1][1]):
continue
pos_12.append([p1,p2])
pos_12 = np.array(pos_12)
# finding a short(er) scan path
if self.optimize:
# basically... solving the TSP problem
best_path = self.two_opt(pos_12)
self.pos_12 = pos_12[best_path]
else:
# sort on the first motor axis
best_path = np.argsort(pos_12[:,self.sort_by_axis])
self.pos_12 = pos_12[best_path]
def _generate_positions(self):
#generate the positions in the improved order
for i, pos in enumerate(self.pos_12):
yield {self.motors[0].name: pos[0],
self.motors[1].name: pos[1]}
def two_opt(self, cities, improvement_threshold=0.5, max_iter=4):
# 2-opt Algorithm adapted from https://en.wikipedia.org/wiki/2-opt
# from https://stackoverflow.com/questions/25585401/travelling-salesman-in-scipy
# Calculate the euclidian distance in n-space of the route r
# traversing cities c, ending at the path start.
def path_distance(r,c):
return np.sum([np.linalg.norm(c[r[p]]-c[r[p-1]]) for p in range(len(r))])
# Reverse the order of all elements from element i to element k in array r.
def two_opt_swap(r,i,k):
return np.concatenate((r[0:i],r[k:-len(r)+i-1:-1],r[k+1:len(r)]))
route = np.arange(cities.shape[0])
improvement_factor = 1
iterations = 0
best_distance = path_distance(route,cities)
while improvement_factor > improvement_threshold and iterations<max_iter:
distance_to_beat = best_distance
for swap_first in range(1,len(route)-2):
for swap_last in range(swap_first+1,len(route)):
new_route = two_opt_swap(route,swap_first,swap_last)
new_distance = path_distance(new_route,cities)
if new_distance < best_distance:
route = new_route
best_distance = new_distance
improvement_factor = 1 - best_distance/distance_to_beat
iterations += 1
return route
[docs]
@macro
class FermatScanPlus(AScan):
"""
Software scan in the shape of a fermat spiral (good for ptychography scan).
The center of the spiral will be the center of the given rectangle.
The first motors have to be in the same units. ::
A third motor with a different step size is used as the inner most loop.
fermatscan <motor1> <start> <stop>
<motor2> <start> <stop> <stepsize12>
<motor3> <start> <stop> <stepsize3>
<exp_time>
optional keyword arguments:
optimize: boolean ... will try to solve the traveling salesman problem
WARNING: can be slow for large scans!
"""
def __init__(self, m1, l1_l, l1_u,
m2, l2_l, l2_u, stepsize12,
m3, l3_l, l3_u, stepsize3,
exptime, sort_by_axis=1, **kwargs):
try:
SoftwareScan.__init__(self, float(exptime))
self.motors = [m1, m2, m3]
self.limits = [[l1_l, l1_u],[l2_l, l2_u],[l3_l, l3_u]]
self._check_limits()
self.stepsize12 = float(stepsize12)
self.stepsize3 = float(stepsize3)
self.sort_by_axis = sort_by_axis
self.optimize = False
self.calc_positions()
self.n_positions = int(len(self.pos_12))*int(len(self.pos_3))
assert all_are_motors(self.motors)
except Exception as E:
print(E)
raise MacroSyntaxError
def _check_limits(self):
# just making sure the given upper and lower limits to the motors are the right way around
if self.limits[0][0] > self.limits[0][1]:
#print('inverted limits in the first motor... switching them')
self.limits[0][0], self.limits[0][1] = self.limits[0][1], self.limits[0][0]
if self.limits[1][0] > self.limits[1][1]:
#print('inverted limits in the second motor... switching them')
self.limits[1][0], self.limits[1][1] = self.limits[1][1], self.limits[1][0]
if self.limits[2][0] > self.limits[2][1]:
#print('inverted limits in the third motor... switching them')
self.limits[2][0], self.limits[2][1] = self.limits[2][1], self.limits[2][0]
# check that the upper and lower limits are not identical
if (self.limits[0][0] == self.limits[0][1]) or (self.limits[1][0] == self.limits[1][1]):
print('[!] upper and lower limits are identical for at least one of the motors')
raise MacroSyntaxError
def calc_positions(self):
# generate positions for innermost loop
N_3 = int((self.limits[2][1] - self.limits[2][0]) / self.stepsize3) + 1
self.pos_3 = np.linspace(self.limits[2][0], self.limits[2][1], N_3, endpoint=True)
# scaling factors and angular step width
c_0 = 0.524 # 3rd closest neighbor is on average one step away
c = c_0*self.stepsize12
phi = 0.5*(1+np.sqrt(5))
phi_0 = 2*np.pi/(1+phi)
# center and size of the rectangular scan field
center = [0.5*(l[0]+l[1]) for l in self.limits]
size = [np.abs(l[0]-l[1]) for l in self.limits]
# max radius of and scan points in the spiral
r_max = 0.5*np.sqrt(size[0]**2+size[1]**2)
n_max = int(np.ceil((r_max/c)**2))
# calculate all positions until n_max (and thus r_max)
n = np.linspace(0,n_max,n_max+1, endpoint=True)
pos_1 = c*np.sqrt(n)*np.sin(n*phi_0)+center[0]
pos_2 = c*np.sqrt(n)*np.cos(n*phi_0)+center[1]
# remove positions outside the scan rectangle
pos_12 = []
for i, p1 in enumerate(pos_1):
p2 = pos_2[i]
if not(p1>self.limits[0][0]):
continue
if not(p1<self.limits[0][1]):
continue
if not(p2>self.limits[1][0]):
continue
if not(p2<self.limits[1][1]):
continue
pos_12.append([p1,p2])
pos_12 = np.array(pos_12)
# finding a short(er) scan path
if self.optimize:
# basically... solving the TSP problem
best_path = self.two_opt(pos_12)
self.pos_12 = pos_12[best_path]
else:
# sort on the first motor axis
best_path = np.argsort(pos_12[:,self.sort_by_axis])
self.pos_12 = pos_12[best_path]
def _generate_positions(self):
#generate the positions in the improved order
for i, pos12 in enumerate(self.pos_12):
for j, p3 in enumerate(self.pos_3):
yield {self.motors[0].name: pos12[0],
self.motors[1].name: pos12[1],
self.motors[2].name: p3}
def two_opt(self, cities, improvement_threshold=0.5, max_iter=4):
# 2-opt Algorithm adapted from https://en.wikipedia.org/wiki/2-opt
# from https://stackoverflow.com/questions/25585401/travelling-salesman-in-scipy
# Calculate the euclidian distance in n-space of the route r
# traversing cities c, ending at the path start.
def path_distance(r,c):
return np.sum([np.linalg.norm(c[r[p]]-c[r[p-1]]) for p in range(len(r))])
# Reverse the order of all elements from element i to element k in array r.
def two_opt_swap(r,i,k):
return np.concatenate((r[0:i],r[k:-len(r)+i-1:-1],r[k+1:len(r)]))
route = np.arange(cities.shape[0])
improvement_factor = 1
iterations = 0
best_distance = path_distance(route,cities)
while improvement_factor > improvement_threshold and iterations<max_iter:
distance_to_beat = best_distance
for swap_first in range(1,len(route)-2):
for swap_last in range(swap_first+1,len(route)):
new_route = two_opt_swap(route,swap_first,swap_last)
new_distance = path_distance(new_route,cities)
if new_distance < best_distance:
route = new_route
best_distance = new_distance
improvement_factor = 1 - best_distance/distance_to_beat
iterations += 1
return route