Usage and Testing of rotation.jl

By: Yi-Xin Liu

Re-posted from: http://www.yxliu.group/2020/04/rotation-test

Numerical experiments are carried out to demonstrate the usage as well as serving as a test for the rotation.jl submodule. This is an appendix to the previous post. The format of this post mimics the Jupyter notebook. The output of a code block after run is captured by Literate.jl and rendered here as a markdown code block in the format of the text language.

First, we load some necessary packages

1
2
3
4
using Revise
using Scattering
using LinearAlgebra
using Test

Some Experiments on Rotation Operations

$\vu, \vv, \vw$ are the basis vectors of the UVW internal coordinate system with their components expressed in the reference XYZ coordinate system. Let $\vw$ point to the principle direction of a scatterer.

1
2
3
4
5
6
7
8
9
10
11
w = [1.0, 2.0, 3.0]
normalize!(w)
# pick a vector that is not parallel to w
a = [0, 1, 2]
# find u by cross product
u = cross(w, a)
normalize!(u)
# find v perpendicular to both w and u
v = cross(w, u)
normalize!(v)
w

1
2
3
4
3-element Array{Float64,1}:
 0.2672612419124244
 0.5345224838248488
 0.8017837257372732

$\mP$ is the rotation matrix which rotates XYZ coordinate system (basis vectors) to UVW coordinate system: $[\vu\;\vv\;\vw] = [\ve_x\;\ve_y\;\ve_z]\mP$.

1
P = hcat(u, v, w)

1
2
3
4
3×3 Array{Float64,2}:
  0.408248   0.872872  0.267261
 -0.816497   0.218218  0.534522
  0.408248  -0.436436  0.801784

$\mR$ is the rotation matrix which rotates a vector in XYZ system to UVW coordinate system: $\mR = \mP^{-1} = \mP^T$ and $\mR\vu = [1\;0\;0]^T, \mR\vv = [0\;1\;0]^T, \mR\vw = [0\;0\;1]^T$

1
2
@test transpose(P)  inv(P)
R = transpose(P)

1
2
3
4
3×3 LinearAlgebra.Transpose{Float64,Array{Float64,2}}:
 0.408248  -0.816497   0.408248
 0.872872   0.218218  -0.436436
 0.267261   0.534522   0.801784

1
@test det(R)  1

1
Test Passed

Verify that $\mR$ indeed transforms basis vectors of the XYZ coordinate system to $\vu, \vv, \vw$, whose components are expressed in the XYZ coordinate system.

1
2
3
@test R * u  [1, 0, 0]
@test R * v  [0, 1, 0]
@test R * w  [0, 0, 1]

1
Test Passed

1
tr(R)

1
1.4282499064371286

Compute the rotation angle:

1
2
θ = acos((tr(R)-1)/2)
rad2deg(θ)

1
77.63580469304388

Compute the rotation axis expressed in the XYZ system:

1
uu = [R[3,2]-R[2,3], R[1,3]-R[3,1], R[2,1]-R[1,2]] / (2sin(θ))

1
2
3
4
3-element Array{Float64,1}:
 0.49700656431759865
 0.07216735383016143
 0.8647406247345901

Alternative way to compute the rotation axis. Note that the result may be different from the one computed above with only a sign.

1
2
3
4
vals, vecs = eigen(R)
idx = findfirst(x -> x  one(x), vals)
uu2 = real(vecs[:, idx])
@test uu2  uu || uu2  -uu

1
Test Passed

Alternative way to compute the rotation angle

1
2
vv = [R[3,2]-R[2,3], R[1,3]-R[3,1], R[2,1]-R[1,2]]
@test asin(norm(vv)/2)  θ

1
Test Passed

A proper rotation matrix should have $\det(\mR) = +1$

1
det(R)

1
1.0000000000000002

1
transpose(R)

1
2
3
4
3×3 Array{Float64,2}:
  0.408248   0.872872  0.267261
 -0.816497   0.218218  0.534522
  0.408248  -0.436436  0.801784

1
inv(R)

1
2
3
4
3×3 LinearAlgebra.Transpose{Float64,Array{Float64,2}}:
  0.408248   0.872872  0.267261
 -0.816497   0.218218  0.534522
  0.408248  -0.436436  0.801784

A proper rotation matrix should be an orthogonal matrix

1
@test transpose(R)  inv(R)

1
Test Passed

Convert current rotation matrix representation to Euler angles representation. Convention used: Z1Y2Z3

Procedure:

  1. Rotate about +z by eta (counter-clockwise in x-y plane),
  2. Rotate about the former y-axis (which is y’), counter clockwise in x’-z plane. Then,
  3. Rotate about the former z-axis (which is z’), counter-clockwise in x’-y’ plane.

See wiki page:

  1. https://en.wikipedia.org/wiki/Rotation_matrix
  2. https://en.wikipedia.org/wiki/Euler_angles#Intrinsic_rotations
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
α = atan(R[2,3], R[1,3])
β = acos(R[3,3])
γ = atan(R[3,2], -R[3,1])
rad2deg.([α, β, γ])
c1 = cos(α)
c2 = cos(β)
c3 = cos(γ)
s1 = sin(α)
s2 = sin(β)
s3 = sin(γ)

Ra = zero(R)
Ra[1, 1] = c1*c2*c3 - s1*s3
Ra[1, 2] = -c3*s1 - c1*c2*s3
Ra[1, 3] = c1 * s2
Ra[2, 1] = c1*s3 + c2*c3*s1
Ra[2, 2] = c1*c3 - c2*s1*s3
Ra[2, 3] = s1*s2
Ra[3, 1] = -c3*s2
Ra[3, 2] = s2*s3
Ra[3, 3] = c2
@test Ra  R

1
Test Passed

1
rad2deg.([α, β, γ])

1
2
3
4
3-element Array{Float64,1}:
 -46.91127686463718
  36.69922520048988
 116.56505117707799

Convention: Z1X2Z3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
α = atan(R[1,3], -R[2,3])
β = acos(R[3,3])
γ = atan(R[3,1], R[3,2])
rad2deg.([α, β, γ])
c1 = cos(α)
c2 = cos(β)
c3 = cos(γ)
s1 = sin(α)
s2 = sin(β)
s3 = sin(γ)

Rb = zero(R)
Rb[1, 1] = c1*c3 - c2*s1*s3
Rb[1, 2] = -c1*s3 - c2*c3*s1
Rb[1, 3] = s1 * s2
Rb[2, 1] = c3*s1 + c1*c2*s3
Rb[2, 2] = c1*c2*c3 - s1*s3
Rb[2, 3] = -c1*s2
Rb[3, 1] = s2*s3
Rb[3, 2] = c3*s2
Rb[3, 3] = c2
@test Rb  R

1
Test Passed

Convention Z1Y2X3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
α = atan(R[2,1],R[1,1])
β = asin(-R[3,1])
γ = atan(R[3,2], R[3,3])
rad2deg.([α, β, γ])
c1 = cos(α)
c2 = cos(β)
c3 = cos(γ)
s1 = sin(α)
s2 = sin(β)
s3 = sin(γ)

Z1Y2X3 = zero(R)
Z1Y2X3[1, 1] = c1*c2
Z1Y2X3[1, 2] = c1*s2*s3 - c3*s1
Z1Y2X3[1, 3] = s1*s3 + c1*c3*s2
Z1Y2X3[2, 1] = c2*s1
Z1Y2X3[2, 2] = c1*c3 + s1*s2*s3
Z1Y2X3[2, 3] = c3*s1*s2 - c1*s3
Z1Y2X3[3, 1] = -s2
Z1Y2X3[3, 2] = c2*s3
Z1Y2X3[3, 3] = c2*c3
@test Z1Y2X3  R

1
Test Passed

Convention: Z1Y2Z3

1
2
3
4
5
6
7
8
9
Rp = hcat([-1, 0, 0], [0, 0, 1], [0, 1, 0])
α = atan(Rp[2,3], Rp[1,3])
β = acos(Rp[3,3])
γ = atan(Rp[3,2], -Rp[3,1])
rad2deg.([α, β, γ])

θp = acos((tr(Rp)-1)/2)
up =[Rp[3,2]-Rp[2,3], Rp[1,3]-Rp[3,1], Rp[2,1]-Rp[1,2]]
θpp = asin(norm(up)/2)

1
0.0

1
rad2deg(θp), rad2deg(θpp), up

1
(180.0, 0.0, [0, 0, 0])

Find rotation axis by diagonalization

1
2
3
vals, vecs = eigen(Rp)
idx = findfirst(x -> x  one(x), vals)
uu = real(vecs[:, idx])

1
2
3
4
3-element Array{Float64,1}:
 0.0
 0.7071067811865475
 0.7071067811865477

Testing and Usage of Scattering/rotation.jl

Testing EulerAngle constructors

1
2
using StaticArrays: SVector, FieldVector
α, β, γ = π/4, π/3, π/2

1
(0.7853981633974483, 1.0471975511965976, 1.5707963267948966)

Constructors of EulerAngle. Initialize by a vector

1
EulerAngle([α, β, γ])

1
Scattering.Rotation.EulerAngle{Float64}(0.7853981633974483, 1.0471975511965976, 1.5707963267948966)

1
EulerAngle(1.3, 0, 0)

1
Scattering.Rotation.EulerAngle{Float64}(1.3, 0.0, 0.0)

Initialize by three angles

1
euler = EulerAngle(α, β, γ)

1
Scattering.Rotation.EulerAngle{Float64}(0.7853981633974483, 1.0471975511965976, 1.5707963267948966)

Copy constructor

1
EulerAngle(euler)

1
Scattering.Rotation.EulerAngle{Float64}(0.7853981633974483, 1.0471975511965976, 1.5707963267948966)

Initialize by a static vector

1
2
sv = SVector(1.0, 2.0, 3.0)
EulerAngle(sv)

1
Scattering.Rotation.EulerAngle{Float64}(1.0, 2.0, 3.0)

Follow the Z1Y2Z3 convention

1
2
3
4
α = atan(v[3], u[3])
β = acos(w[3])
γ = atan(w[2], -w[1])
rad2deg.([α, β, γ])

1
2
3
4
3-element Array{Float64,1}:
 -46.91127686463718
  36.69922520048988
 116.56505117707799

Testing RotationMatrix constructors

Constructor of RotationMatrix initialized with a matrix

1
2
rotmat = RotationMatrix(Vector3D(u), Vector3D(v), Vector3D(w))
@test transpose(RotMatrix(hcat(u,v,w)))  rotmat.R

1
Test Passed

Convert an EulerAngle instance to a RotationMatrix instance

1
2
rotmat_euler = RotationMatrix(EulerAngle(α, β, γ))
@test rotmat_euler.R  rotmat.R

1
Test Passed

Convert a RotationMatrix instance to an EulerAngle instance

1
2
euler = EulerAngle(rotmat_euler)
@test [euler.α, euler.β, euler.γ]  [α, β, γ]

1
Test Passed

Testing EulerAxisAngle Constructors

Constructor of AxisAngleRepresentation initialized with a RotationMatrix instance: conversion from rotation matrix representation to axis-angle representation

1
axisangle = EulerAxisAngle(rotmat)

1
Scattering.Rotation.EulerAxisAngle{Float64}([0.49700656431759865, 0.07216735383016143, 0.8647406247345901], 1.3550004093288814, [0.0 -0.8647406247345901 0.07216735383016143; 0.8647406247345901 0.0 -0.49700656431759865; -0.07216735383016143 0.49700656431759865 0.0])

Convert Euler axis-angle representation to rotation matrix representation

1
2
rotmat_axisangle = RotationMatrix(axisangle)
@test rotmat_axisangle.R  rotmat.R atol=1e-15

1
Test Passed

1
2
axisangle2 = EulerAxisAngle(axisangle.ω, axisangle.θ)
@test axisangle2.K  axisangle.K

1
Test Passed

Testing conversion and promotion

1
euler

1
Scattering.Rotation.EulerAngle{Float64}(-0.8187562376025611, 0.6405223126794245, 2.0344439357957027)

1
axisangle

1
Scattering.Rotation.EulerAxisAngle{Float64}([0.49700656431759865, 0.07216735383016143, 0.8647406247345901], 1.3550004093288814, [0.0 -0.8647406247345901 0.07216735383016143; 0.8647406247345901 0.0 -0.49700656431759865; -0.07216735383016143 0.49700656431759865 0.0])

1
EulerAxisAngle(euler)

1
Scattering.Rotation.EulerAxisAngle{Float64}([0.4970065643175987, 0.07216735383016142, 0.86474062473459], 1.3550004093288814, [0.0 -0.86474062473459 0.07216735383016142; 0.86474062473459 0.0 -0.4970065643175987; -0.07216735383016142 0.4970065643175987 0.0])

1
RotationMatrix(euler) |> EulerAxisAngle

1
Scattering.Rotation.EulerAxisAngle{Float64}([0.4970065643175987, 0.07216735383016142, 0.86474062473459], 1.3550004093288814, [0.0 -0.86474062473459 0.07216735383016142; 0.86474062473459 0.0 -0.4970065643175987; -0.07216735383016142 0.4970065643175987 0.0])

1
EulerAngle(axisangle)

1
Scattering.Rotation.EulerAngle{Float64}(-0.8187562376025609, 0.6405223126794247, 2.0344439357957027)

1
convert(EulerAngle, axisangle)

1
Scattering.Rotation.EulerAngle{Float64}(-0.8187562376025609, 0.6405223126794247, 2.0344439357957027)

1
convert(EulerAngle, euler)

1
Scattering.Rotation.EulerAngle{Float64}(-0.8187562376025611, 0.6405223126794245, 2.0344439357957027)

Testing promotion, comparison, and unit rotation

1
@test euler == axisangle

1
Test Passed

1
@test euler == euler

1
Test Passed

1
@test promote(euler) == rotmat

1
Test Passed

1
@test promote(axisangle) == rotmat

1
Test Passed

1
one(rotmat)

1
Scattering.Rotation.RotationMatrix{Float64}([1.0 0.0 0.0; 0.0 1.0 0.0; 0.0 0.0 1.0])

1
one(euler)

1
Scattering.Rotation.EulerAngle{Float64}(0.0, 0.0, 0.0)

1
promote(one(euler))

1
Scattering.Rotation.RotationMatrix{Float64}([1.0 -0.0 0.0; 0.0 1.0 0.0; -0.0 0.0 1.0])

1
one(axisangle)

1
Scattering.Rotation.EulerAxisAngle{Float64}([1.0, 0.0, 0.0], 0.0, [0.0 -0.0 0.0; 0.0 0.0 -1.0; -0.0 1.0 0.0])

1
promote(one(axisangle))

1
Scattering.Rotation.RotationMatrix{Float64}([1.0 0.0 0.0; 0.0 1.0 0.0; 0.0 0.0 1.0])

Testing inv function

1
rot1 = rotmat

1
Scattering.Rotation.RotationMatrix{Float64}([0.408248290463863 -0.816496580927726 0.408248290463863; 0.8728715609439696 0.21821789023599242 -0.4364357804719848; 0.2672612419124244 0.5345224838248488 0.8017837257372732])

Test inv function for RotationMatrix

1
2
irot1 = inv(rot1)
@test transpose(rot1.R)  irot1.R

1
Test Passed

1
rot2 = EulerAxisAngle(rot1)

1
Scattering.Rotation.EulerAxisAngle{Float64}([0.49700656431759865, 0.07216735383016143, 0.8647406247345901], 1.3550004093288814, [0.0 -0.8647406247345901 0.07216735383016143; 0.8647406247345901 0.0 -0.49700656431759865; -0.07216735383016143 0.49700656431759865 0.0])

Test inv function for EulerAxisAngle

1
2
irot2 = inv(rot2)
@test irot2 == irot1

1
Test Passed

1
rot3 = EulerAngle(rot1)

1
Scattering.Rotation.EulerAngle{Float64}(-0.818756237602561, 0.6405223126794245, 2.0344439357957027)

Test inv function for EulerAngle

1
2
irot3 = inv(rot3)
@test irot3 == irot1

1
Test Passed

Testing multiplication of two AbstractRotation instances

Test multiplication of two RotationMatrix instances.

1
2
3
4
R1 = rotmat
M = RotationMatrix(Rp)
R2 = R1 * M
@test R2.R  R1.R*M.R

1
Test Passed

Test multiplication of a EulerAxisAngle instance and a RotationMatrix instance.

1
2
3
4
5
R1 = rotmat
R1a = EulerAxisAngle(R1)
M = RotationMatrix(Rp)
R2 = R1a * M
@test R2 == R1 * M

1
Test Passed

Test multiplication of a RotationMatrix instance and a EulerAxisAngle instance.

1
2
3
4
5
R1 = rotmat
M = RotationMatrix(Rp)
Ma = EulerAxisAngle(M)
R2 = R1 * Ma
@test R2 == R1 * M

1
Test Passed

Test multiplication of a EulerAxisAngle instance and a EulerAxisAngle instance.

1
2
3
4
5
6
R1 = rotmat
R1a = EulerAxisAngle(R1)
M = RotationMatrix(Rp)
Ma = EulerAxisAngle(M)
R2 = R1a * Ma
@test R2 == R1 * M

1
Test Passed

Testing computing power of a AbstractRotation instance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function pow1(rot::AbstractRotation, n::Integer)
    rot = promote(rot)
    result = rot
    for i in 1:(n-1)
        result *= rot
    end
    result
end

function pow(rot::RotationMatrix, n::Integer)
    if n == zero(n)
        return one(rot)
    elseif n == one(n)
        return rot
    else
        if isodd(n)
            return rot * pow(rot, n-1)
        else
            rothalf = pow(rot, n÷2)
            return rothalf * rothalf
        end
    end
end

pow(rot::AbstractRotation, n::Integer) = pow(promote(rot), n)

1
pow (generic function with 2 methods)

1
pow(euler, 0)

1
Scattering.Rotation.RotationMatrix{Float64}([1.0 0.0 0.0; 0.0 1.0 0.0; 0.0 0.0 1.0])

1
pow(euler, 1)

1
Scattering.Rotation.RotationMatrix{Float64}([0.40824829046386313 -0.8164965809277259 0.4082482904638629; 0.8728715609439694 0.21821789023599242 -0.4364357804719848; 0.26726124191242434 0.5345224838248487 0.8017837257372732])

1
pow(euler, 2)

1
Scattering.Rotation.RotationMatrix{Float64}([-0.4369210333151354 -0.2932896043722907 0.8503418245705543; 0.43018214432212887 -0.8983623348886722 -0.08881687880007882; 0.7899641342195538 0.32699590703939707 0.5186813505672173])

1
pow(euler, 3)

1
Scattering.Rotation.RotationMatrix{Float64}([-0.20711300761088602 0.7472703153125533 0.6314200487243415; -0.6322711178708509 -0.5947556020638506 0.4964866637886771; 0.7465503570320742 -0.29639981387703873 0.5956590591512387])

1
pow(euler, 4)

1
Scattering.Rotation.RotationMatrix{Float64}([0.7364715816744584 0.6696830270043788 0.09557328459445946; -0.644577211376976 0.651844177986726 0.3995239494677259; 0.2052555187063243 -0.35584239624735736 0.9117271308201462])

1
pow(euler, 5).R

1
2
3
4
3×3 StaticArrays.SArray{Tuple{3,3},Float64,2,9} with indices SOneTo(3)×SOneTo(3):
 0.910754   -0.404104   0.0850187
 0.412606    0.882094  -0.227304
 0.0168598   0.242097   0.970106

1
pow(euler, 100).R

1
2
3
4
3×3 StaticArrays.SArray{Tuple{3,3},Float64,2,9} with indices SOneTo(3)×SOneTo(3):
 -0.443094   0.414668   0.794807
 -0.277188  -0.906518   0.318422
  0.852546  -0.0792197  0.516614

1
euler^1

1
Scattering.Rotation.RotationMatrix{Float64}([0.40824829046386313 -0.8164965809277259 0.4082482904638629; 0.8728715609439694 0.21821789023599242 -0.4364357804719848; 0.26726124191242434 0.5345224838248487 0.8017837257372732])

1
euler^2

1
Scattering.Rotation.RotationMatrix{Float64}([-0.4369210333151354 -0.2932896043722907 0.8503418245705543; 0.43018214432212887 -0.8983623348886722 -0.08881687880007882; 0.7899641342195538 0.32699590703939707 0.5186813505672173])

1
euler^3

1
Scattering.Rotation.RotationMatrix{Float64}([-0.20711300761088602 0.7472703153125533 0.6314200487243415; -0.6322711178708509 -0.5947556020638506 0.4964866637886771; 0.7465503570320742 -0.29639981387703873 0.5956590591512387])

1
euler^4

1
Scattering.Rotation.RotationMatrix{Float64}([0.7364715816744584 0.6696830270043788 0.09557328459445946; -0.644577211376976 0.651844177986726 0.3995239494677259; 0.2052555187063243 -0.35584239624735736 0.9117271308201462])

1
@test pow(euler, 100).R  (euler^100).R

1
Test Passed

Benchmarks

1
2
3
using BenchmarkTools

@btime pow1($euler, 1000)

1
2
142.561 μs (3004 allocations: 172.31 KiB)
Scattering.Rotation.RotationMatrix{Float64}([-0.17617351956438138 0.7712758102440354 0.6116342988556001; -0.6592241548072412 -0.553880454852226 0.50856656934094; 0.7310173764848875 -0.31360814126062625 0.606022713280731])

The implementation of pow (or Scattering.^ which is the same as pow here)
is significantly faster than the naive implementation.

1
@btime $euler^1000

1
2
2.327 μs (49 allocations: 3.02 KiB)
Scattering.Rotation.RotationMatrix{Float64}([-0.1761735195643819 0.7712758102440348 0.6116342988555798; -0.6592241548072477 -0.5538804548522176 0.5085665693409378; 0.7310173764848691 -0.3136081412606315 0.6060227132807026])

Testing applying AbstractRotation instance to a vector

Test multiplication of a RotationMatrix instance and a Vector

1
2
3
M = RotationMatrix(Rp)
v = [1.0, 2.0, 3.0]
@test M*v  M.R * v

1
Test Passed

Test multiplication of a RotationMatrix instance and a RVector (SVector, QVector)

1
2
3
M = RotationMatrix(Rp)
r = RVector([1.0, 2.0, 3.0])
@test M*r  M.R * r

1
Test Passed

Test multiplication of a EulerAxisAngle instance and a Vector

1
2
3
4
M = RotationMatrix(Rp)
Ma = EulerAxisAngle(M)
v = [1.0, 2.0, 3.0]
@test Ma*v  M.R * v

1
Test Passed

Test multiplication of a EulerAxisAngle instance and a RVector (SVector, QVector)

1
2
3
4
M = RotationMatrix(Rp)
Ma = EulerAxisAngle(M)
v = RVector([1.0, 2.0, 3.0])
@test Ma*v  M.R * v

1
Test Passed

Test multiplication of a EulerAngle instance and a Vector

1
2
3
4
M = RotationMatrix(Rp)
Ma = EulerAngle(M)
v = [1.0, 2.0, 3.0]
@test Ma*v  M.R * v

1
Test Passed

Test multiplication of a EulerAngle instance and a RVector (SVector, QVector)

1
2
3
4
M = RotationMatrix(Rp)
Ma = EulerAngle(M)
v = RVector([1.0, 2.0, 3.0])
@test Ma*v  M.R * v

1
Test Passed

Acknowledgements

  • This work is partially supported by the General Program of the National Natural Science Foundation of China (No. 21873021).
  • The captured output text blocks in this post is automatically generated by using Literate.jl.

Usage and Testing of rotation.jl was originally published by Yi-Xin Liu at Yi-Xin Liu on April 19, 2020.