I have recently needed code to rotate 2D coordinates, either about the origin, or some specified other point, using pyxll to call the code from Excel. My first attempt was a direct translation of some VBA code, that also worked with 3D coordinates, with rotation about any of the 3 axes:
@xl_func
@xl_arg('Rcoords', 'numpy_array')
@xl_arg('Axis', 'int')
def py_Rotate(Rcoords, Rotation, Axis, result = 0):
return Rotate(Rcoords, Rotation, Axis, result)
def Rotate(Rcoords, Rotation, Axis, result = 0):
# # Rotate about the origin, Rotation in radians
Plane = np.zeros(2, dtype = int)
if Rotation != 0:
if Axis == 1:
Plane[0] = 1
Plane[1] = 2
elif Axis == 2:
Plane[0] = 2
Plane[1] = 0
elif Axis == 3:
Plane[0] = 0
Plane[1] = 1
else:
return "Invalid Axis"
NumRows = Rcoords.shape[0]
# Rotate
for i in range(0, NumRows):
try:
if (Rcoords[i, Plane[0]] == 0 and Rcoords[i, Plane[1]] == 0) == False:
r = (Rcoords[i, Plane[0]]** 2 + Rcoords[i, Plane[1]]** 2)** 0.5
Theta = atan2(Rcoords[i, Plane[1]], Rcoords[i, Plane[0]])
Theta = Theta + Rotation
Rcoords[i, Plane[0]] = r * cos(Theta)
Rcoords[i, Plane[1]] = r * sin(Theta)
except:
pass
if result == 0:
return Rcoords
else:
return Rcoords[0, result-1]
This was greatly simplified, and speeded up, by using 2D coordinates for the input, simplifying the rotation formula, and using Numpy procedures to operate on entire columns, rather than looping through row by row:
@xl_func
@xl_arg('Rcoords', 'numpy_array')
def Rotate2(Rcoords, Rotation):
# 2D Rotate about the origin, Rotation in radians
sinR = sin(Rotation)
cosR = cos(Rotation)
Rcoords2 = np.zeros((Rcoords.shape[0],2))
Rcoords2[:,0] = Rcoords[:,0]*cosR - Rcoords[:,1]*sinR
Rcoords2[:,1] = Rcoords[:,0]*sinR + Rcoords[:,1]*cosR
return Rcoords2
Performing the rotation by using matrix multiplication gave a further speed improvement:
@xl_func
@xl_arg('Rcoords', 'numpy_array')
def RotateM(Rcoords, Rotation):
# 2D Rotate about the origin, Rotation in radians
sinR = sin(Rotation)
cosR = cos(Rotation)
rotnA = np.array([[cosR, sinR],[-sinR, cosR]])
Rcoords2 = np.matmul(Rcoords, rotnA)
return Rcoords2
To rotate about a point other than the origin the 3 functions above were modified by:
- Translating the coordinates to move the rotation point to the origin.
- Rotate the coordinates about the origin.
- Translate the rotated coordinates back by the same amount.
@xl_func()
@xl_arg('Rcoords', 'numpy_array')
@xl_arg('RotnPt', 'numpy_array')
@xl_arg('Axis', 'int')
def RotateC(Rcoords, Rotation, RotnPt, Axis, result = 0):
# Rotate about RotnPt, Rotation in radians
Tol = 0.000000000001
NumRows = Rcoords.shape[0]
if abs(Rcoords[0, 0] - Rcoords[NumRows-1, 0]) + abs(Rcoords[0, 1] - Rcoords[NumRows-1, 1]) < Tol: NumRows = NumRows - 1
NumRCols = Rcoords.shape[1]
if RotnPt.shape[0] > RotnPt.shape[1]:
Rotnpt2 = RotnPt
NumCCols = RotnPt.shape[0]
RotnPt = np.zeros((1, NumCCols))
RotnPt[0, :] = Rotnpt2[:, 0]
NumCCols = RotnPt.shape[1]
if NumRCols < NumCCols:
NumCols = NumRCols
else:
NumCols = NumCCols
OCoords = np.zeros((NumRows, NumRCols))
for i in range(0, NumRows):
for j in range(0, NumCols):
OCoords[i, j] = Rcoords[i, j] - RotnPt[0, j]
OCoords = Rotate(OCoords, Rotation, Axis)
for i in range(0, NumRows):
for j in range(0, NumCols):
OCoords[i, j] = OCoords[i, j] + RotnPt[0, j]
OCoords[0:NumRows, NumCols:NumRCols] = Rcoords[0:NumRows, NumCols:NumRCols]
return OCoords
@xl_func()
@xl_arg('Rcoords', 'numpy_array')
@xl_arg('RotnPt', 'numpy_array')
@xl_arg('Axis', 'int')
def RotateC2(Rcoords, Rotation, RotnPt, Axis, result = 0):
# Rotate about RotnPt, Rotation in radians; with numpy array calculations
Tol = 0.000000000001
NumRows = Rcoords.shape[0]
if abs(Rcoords[0, 0] - Rcoords[NumRows-1, 0]) + abs(Rcoords[0, 1] - Rcoords[NumRows-1, 1]) < Tol: NumRows = NumRows - 1
NumRCols = Rcoords.shape[1]
if RotnPt.shape[0] > RotnPt.shape[1]:
Rotnpt2 = RotnPt
NumCCols = RotnPt.shape[0]
RotnPt = np.zeros((1, NumCCols))
RotnPt[0, :] = Rotnpt2[:, 0]
NumCCols = RotnPt.shape[1]
if NumRCols < NumCCols:
NumCols = NumRCols
else:
NumCols = NumCCols
OCoords = Rcoords - RotnPt
OCoords = Rotate2(OCoords, Rotation)
OCoords = OCoords + RotnPt
OCoords[0:NumRows, NumCols:NumRCols] = Rcoords[0:NumRows, NumCols:NumRCols]
return OCoords
@xl_func()
@xl_arg('Rcoords', 'numpy_array')
@xl_arg('RotnPt', 'numpy_array')
def RotateCM(Rcoords, Rotation, RotnPt):
# Rotate about RotnPt, Rotation in radians; with numpy array calculations
OCoords = Rcoords - RotnPt
OCoordsr = RotateM(OCoords, Rotation)
OCoordsr = OCoordsr + RotnPt
return OCoordsr
Finally for comparison and to check results some code was adapted from https://gist.github.com/LyleScott/ . Note that this code treats clockwise rotations as positive, whereas the other functions use the standard approach of anti-clockwise rotation being positive.
@xl_func()
@xl_arg('xy', 'numpy_array')
@xl_arg('radians', 'float')
@xl_arg('origin', 'numpy_array', ndim=1)
def rotate_around_point_highperf(xy, radians, origin=(0, 0)):
"""Rotate a point around a given point.
From: https://gist.github.com/LyleScott/
I call this the "high performance" version since we're caching some
values that are needed >1 time. It's less readable than the previous
function but it's faster.
"""
x = xy[:,0]
y = xy[:,1]
offset_x, offset_y = origin[:]
adjusted_x = (x - offset_x)
adjusted_y = (y - offset_y)
cos_rad = cos(radians)
sin_rad = sin(radians)
qx = offset_x + cos_rad * adjusted_x + sin_rad * adjusted_y
qy = offset_y + -sin_rad * adjusted_x + cos_rad * adjusted_y
res = np.zeros((qx.shape[0],2))
res[:,0] = qx
res[:,1] = qy
return res
Results and execution times for rotation of a simple shape are shown in the screen-shot below (click on the image for full-size view):

The functions using matrix multiplication are significantly faster, and this improvement is increased with a longer list of coordinates:
