Python functions for rotation

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:

This entry was posted in Coordinate Geometry, Excel, Link to Python, Maths, Newton, PyXLL and tagged , , , , , , . Bookmark the permalink.

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.