Mean values

The two examples noted below are taken from Moakher (2002) "Means and averaging in the group of rotations", and correspond to cases where the approximate and exact means can be solved for analytically. We will test that the results found by the Quaternions module

Average of constant function


In [15]:
import random
import Quaternions

random.seed(14)

theta_average = 0.
theta_sigma = pi/2.
N = 100
theta = array([random.gauss(theta_average, theta_sigma)]*N)

v = Quaternions.Quaternion(0., random.random()-1, random.random()-1, random.random()-1).normalized()
R = array([Quaternions.exp(-v*(theta_n/2.)) for theta_n in theta])

sumcostheta = sum([cos(theta_n) for theta_n in theta])
sumsintheta = sum([sin(theta_n) for theta_n in theta])
r = sqrt( sumcostheta**2 + sumsintheta**2 )

Theta_a = arctan2(sumsintheta, sumcostheta)
R_a_analytical = Quaternions.exp(-v*(Theta_a/2.))

Theta_g = sum([theta_n for theta_n in theta])/float(N)
R_g_analytical = Quaternions.exp(-v*(Theta_g/2.))

R_a_numerical1 = Quaternions.ApproximateMeanRotor(R)
R_a_numerical2 = Quaternions.ApproximateMeanRotor(R, range(N))
R_g_numerical1 = Quaternions.MeanRotor(R)
R_g_numerical2 = Quaternions.MeanRotor(R, range(N))

R_a_analytical, R_a_numerical1, R_a_numerical2, \
    R_g_analytical, R_g_numerical1, R_g_numerical2


Out[15]:
(Quaternion(0.575339383223685, 0.351410057865493, 0.0602393721593643, 0.73611601217784),
 Quaternion(0.575339383223685, 0.351410057865493, 0.0602393721593642, 0.73611601217784),
 Quaternion(0.575339383223685, 0.351410057865493, 0.0602393721593642, 0.73611601217784),
 Quaternion(0.575339383223686, 0.351410057865493, 0.0602393721593643, 0.73611601217784),
 Quaternion(0.575339383223685, 0.351410057865493, 0.0602393721593642, 0.73611601217784),
 Quaternion(0.575339383223685, 0.351410057865493, 0.0602393721593642, 0.73611601217784))

The fact that these are all the same is good: All methods give identical results for the average value of a constant.

Example 1 from Moakher (2002)

The first example uses an arbitrary series of rotations in a one-parameter subgroup of $SO(3)$. Let $\hat{v}$ be an arbitrary unit vector in $\mathbb{R}^3$, and let $\{\theta_n\}$ be a collection of angles we will be rotating by. We will choose them randomly distributed by a Gaussian around 0, with standard deviation small compared to $\pi$. Moakher shows that the solution for the "projected arithmetic mean" (Quaternions.ApproximateMeanRotor) is given by a rotation about $\hat{v}$ by an angle $\Theta_a$, where \begin{gather} r = \sqrt{\left( \sum_n \cos\theta_n \right)^2 + \left( \sum_n \sin\theta_n \right)^2}~, \\ \cos\Theta_a = \frac{1}{r} \sum_n \cos\theta_n~, \\ \sin\Theta_a = \frac{1}{r} \sum_n \sin\theta_n~. \end{gather}


In [1]:
import random
import Quaternions

random.seed(14)

theta_average = 0.
theta_sigma = pi/2.
N = 10
theta = array([random.gauss(theta_average, theta_sigma) for n in range(N)])

v = Quaternions.Quaternion(0., random.random()-1, random.random()-1, random.random()-1).normalized()
R = array([Quaternions.exp(-v*(theta_n/2.)) for theta_n in theta])

sumcostheta = sum([cos(theta_n) for theta_n in theta])
sumsintheta = sum([sin(theta_n) for theta_n in theta])
r = sqrt( sumcostheta**2 + sumsintheta**2 )

Theta_a = arctan2(sumsintheta, sumcostheta)
R_a_analytical = Quaternions.exp(-v*(Theta_a/2.))

Theta_g = sum([theta_n for theta_n in theta])/float(N)
R_g_analytical = Quaternions.exp(-v*(Theta_g/2.))

R_a_numerical1 = Quaternions.ApproximateMeanRotor(R)
R_a_numerical2 = Quaternions.ApproximateMeanRotor(R, range(N))
R_g_numerical1 = Quaternions.MeanRotor(R)
R_g_numerical2 = Quaternions.MeanRotor(R, range(N))

R_a_analytical, R_a_numerical1, R_a_numerical2, \
    R_g_analytical, R_g_numerical1, R_g_numerical2


Out[1]:
(Quaternion(0.670889621455291, 0.410007671240754, 0.151194962623278, 0.599116773780084),
 Quaternion(0.998142688092219, -0.0336823964510889, -0.0124207643653927, -0.0492178320319982),
 Quaternion(0.960547804929253, -0.153770000855322, -0.0567044257039037, -0.224693812527487),
 Quaternion(0.996079897196627, -0.0489085740873778, -0.018035589453049, -0.0714668265321507),
 Quaternion(0.996079897080697, -0.0489085808108435, -0.0180355913363401, -0.0714668230714535),
 Quaternion(0.975095814786421, -0.12262458877002, -0.0452192107271683, -0.179183105219774))

Note that the third and sixth rotors represent the numerical algorithm using the time-step sizes, and thus should not agree with the other results.

The numerical and analytical results for the geometric mean agree quite nicely. Unfortunately, the results for the arithmetic mean do not agree. I suspect there is an error in Moakher's formula, though I don't see where.

Example 2 from Moakher (2002)

This example seems to work quite nicely. I suspect that there is just something wrong in Moakher's example 1.


In [4]:
import random
import Quaternions

N = 10
theta = random.random()*pi/2.
alpha = random.random()*pi/2.
beta = array([2*(n-1)*pi/N for n in range(N)])

R = array([Quaternions.exp(Quaternions.Quaternion(0,sin(alpha)*cos(beta_n),sin(alpha)*sin(beta_n),cos(alpha))*(theta/2))
           for beta_n in beta])

Theta_a = 2*arctan(cos(alpha)*tan(theta/2))

R_a_analytical = Quaternions.exp(Quaternions.zHat*(Theta_a/2.))
R_a_numerical1 = Quaternions.ApproximateMeanRotor(R)
R_a_numerical2 = Quaternions.ApproximateMeanRotor(R, range(N))

R_g_analytical = R_a_analytical
R_g_numerical1 = Quaternions.MeanRotor(R)
R_g_numerical2 = Quaternions.MeanRotor(R, range(N))

R_a_analytical, R_a_numerical1, R_a_numerical2, \
    R_g_analytical, R_g_numerical1, R_g_numerical2


Out[4]:
(Quaternion(0.999627191102123, 0, 0, 0.0273034578264333),
 Quaternion(0.999627191102123, 0, 8.45319776956282e-18, 0.0273034578264333),
 Quaternion(0.99884551649956, -0.012218177067289, 0.0376036824142877, 0.0272821074472769),
 Quaternion(0.999627191102123, 0, 0, 0.0273034578264333),
 Quaternion(0.999627191102123, 0, 8.45319776956283e-18, 0.0273034578264333),
 Quaternion(0.999439936727261, -0.00598100565349732, 0.0184076302326812, 0.0272983441841984))

In [5]:
for R in [R_g_numerical1, R_g_numerical2] :
    print((R-R_g_analytical).normsquared())
for R in [R_a_numerical1, R_a_numerical2] :
    print((R-R_a_analytical).normsquared())


8.34936146838e-35
0.000374648369761
7.14565525313e-35
0.00156393225299

The basic numerical means agree with the analytical results to within $10^{-32}$. The means that use the time-step information disagree at a level consistent with the fact that integration is done using midpoints rather than abscissas.

Alignments

We can construct two copies of a funky rotation, and then shift one copy in time and/or rotation, then test to see if the alignment routines can undo the shifts. The funky rotation can be constructed by composing two simple rotations about different axes.


In [6]:
import random
import Quaternions

random.seed(14)

N = 100
v1 = Quaternions.Quaternion(0., random.random()-1, random.random()-1, random.random()-1).normalized()
v2 = Quaternions.Quaternion(0., random.random()-1, random.random()-1, random.random()-1).normalized()
alphadot = 0.084
betadot = alphadot/3.9
Ti = 0.
Tf = 10.
T = linspace(Ti, Tf, num=N)
R = array([Quaternions.exp(v1*(alphadot*t/2.))*Quaternions.exp(v2*(betadot*t/2.))
           for t in T])

Input is shifted in time only


In [7]:
deltat_input = 0.123
Ra = R
Rb = array([Quaternions.exp(v1*(alphadot*t/2.))*Quaternions.exp(v2*(betadot*t/2.))
            for t in T+deltat_input])
T_common = T[(T>=T[0]+deltat_input)&(T<=T[-1]-deltat_input)]

Corrected by rotation only

Obviously, this won't be able to eliminate the difference, but we would like to see it improve.


In [8]:
R1 = Quaternions.ApproximateOptimalAlignmentRotor(Ra, Rb, T, Ti+deltat_input, Tf-deltat_input)

print([R1[i] for i in range(4)])
semilogy(T, Quaternions.angle(Quaternions.RDelta(R1*Rb, Ra)), label='With rotation')
semilogy(T, Quaternions.angle(Quaternions.RDelta(Rb, Ra)), label='Without rotation')
xlabel('Time')
ylabel(r'$\Phi_\Delta$')
legend();


[0.9999823574614009, 0.004747703834191283, 0.002698237052138282, 0.002337432577127874]

That is indeed a huge improvement over no correction, and it approaches zero right in the middle of the alignment region.

Corrected by optimizing time shift, while using ApproximateOptimalAlignmentRotor

This should actually be able to eliminate all the difference (up to numerics), since the required rotation is just the identity, and ApproximateOptimalAlignmentRotor can find that exactly.


In [9]:
deltat2,R2 = Quaternions.ApproximateOptimalAlignment(4., 6., Ra, T, Rb, T)

print(deltat2, [R2[i] for i in range(4)])
Rb2 = Quaternions.Squad(R2*Rb, T+deltat2, T_common)
Ra2 = Quaternions.Squad(Ra, T, T_common)
semilogy(T_common, Quaternions.angle(Quaternions.RDelta(Rb2, Ra2)), label='With rotation')
xlabel('Time')
ylabel(r'$\Phi_\Delta$')
legend();


(0.12300005072243984, [1.0, -2.0347655048829533e-09, -6.775161566884586e-10, -7.928035585051429e-10])

This is indeed successful at eliminating the difference, finding the correct time offset to within $\sqrt{\epsilon}$, and the correction rotation to roughly the same level. I think that blip at the beginning must come from either a bad Squad or just be an unavoidable issue in the initial value of the Squad.

Corrected by optimizing time and rotation shifts


In [6]:
deltat4,R4 = Quaternions.OptimalAlignment(4., 6., Ra, T, Rb, T)

print(deltat4, [R4[i] for i in range(4)])
Rb4 = Quaternions.Squad(R4*Rb, T+deltat4, T_common)
Ra4 = Quaternions.Squad(Ra, T, T_common)
semilogy(T_common, Quaternions.angle(Quaternions.RDelta(Rb4, Ra4)), label='With rotation')
xlabel('Time')
ylabel(r'$\Phi_\Delta$')
legend();


(0.12300005071984751, [1.0, -2.0346654575370147e-09, -6.774587202030604e-10, -7.927541815576947e-10])

This gives us the same result as ApproximateOptimalAlignment, up to numerics.

Input is shifted in rotation only


In [7]:
Rd = Quaternions.exp(Quaternions.Quaternion(0.0,0.1,2.3,4.5))
Ra = R
Rb = Rd*R
print([Rd[i] for i in range(4)])


[0.33566566122434527, -0.01863574584080239, -0.428622154338455, -0.8386085628361076]

Corrected by rotation only

Hopefully, this will be able to completely eliminate the rotation.


In [8]:
R1 = Quaternions.ApproximateOptimalAlignmentRotor(Ra, Rb, T, Ti, Tf)

print([R1[i] for i in range(4)])
semilogy(T, Quaternions.angle(Quaternions.RDelta(R1*Rb, Ra)), label='With rotation')
semilogy(T, Quaternions.angle(Quaternions.RDelta(Rb, Ra)), label='Without rotation')
xlabel('Time')
ylabel(r'$\Phi_\Delta$')
legend();


[0.33566566122434527, 0.018635745840802406, 0.42862215433845474, 0.8386085628361076]

The printed result is almost exactly the input quantity, and the plot clearly shows that the numerical difference is flirting with zero.

Corrected by optimizing time shift, while using ApproximateOptimalAlignmentRotor

This can only do worse than the above, since we are giving it a certain amount of freedom that isn't necessary. But hopefully it won't do too badly.


In [9]:
deltat2,R2 = Quaternions.ApproximateOptimalAlignment(4., 6., Ra, T, Rb, T)

print(deltat2, [R2[i] for i in range(4)])
Rb2 = Quaternions.Squad(R2*Rb, T+deltat2, T_common)
Ra2 = Quaternions.Squad(Ra, T, T_common)
semilogy(T_common, Quaternions.angle(Quaternions.RDelta(Rb2, Ra2)), label='With rotation')
xlabel('Time')
ylabel(r'$\Phi_\Delta$')
legend();


(0.0, [0.3356656612243452, 0.018635745840802367, 0.4286221543384549, 0.8386085628361075])

Pretty good.

Corrected by optimizing time and rotation shifts

Again, this can only do worse than optimizing over rotation alone. But hopefully not much worse.


In [10]:
deltat4,R4 = Quaternions.OptimalAlignment(2., 6., Ra, T, Rb, T)
Rb4 = Quaternions.Squad(R4*Rb, T+deltat4, T_common)
Ra4 = Quaternions.Squad(Ra, T, T_common)
print(deltat4, [R4[i] for i in range(4)])
semilogy(T_common, Quaternions.angle(Quaternions.RDelta(Rb4, Ra4)), label='With rotation')
xlabel('Time')
ylabel(r'$\Phi_\Delta$')
legend();


(-7.667812759582698e-13, [0.3356656612243212, 0.01863574584082182, 0.4286221543384386, 0.8386085628361252])

Again, pretty darn good.

Input is shifted in time and rotation


In [11]:
deltat_input = 0.123
Rd = Quaternions.exp(Quaternions.Quaternion(0.0,0.1,2.3,4.5))
Ra = R
Rb = Rd*array([Quaternions.exp(v1*(alphadot*t/2.))*Quaternions.exp(v2*(betadot*t/2.))
               for t in T+deltat_input])
T_common = T[(T>=T[0]+deltat_input)&(T<=T[-1]-deltat_input)]

Corrected by rotation only

The results here should be almost identical to what we see in the first case.


In [12]:
R1 = Quaternions.ApproximateOptimalAlignmentRotor(Ra, Rb, T, Ti, Tf)

print([R1[i] for i in range(4)])
semilogy(T, Quaternions.angle(Quaternions.RDelta(R1*Rb, Ra)), label='With rotation')
semilogy(T, Quaternions.angle(Quaternions.RDelta(Rb, Ra)), label='Without rotation')
xlabel('Time')
ylabel(r'$\Phi_\Delta$')
legend();


[0.332455336714335, 0.021489325765444184, 0.42558156436335787, 0.8413631736903759]

Indeed, they are almost identical.

Corrected by optimizing time shift, while using ApproximateOptimalAlignmentRotor

The results here should be much like they were in the time-shifted case.


In [13]:
deltat2,R2 = Quaternions.ApproximateOptimalAlignment(4., 6., Ra, T, Rb, T)

print(deltat2, [R2[i] for i in range(4)])
Rb2 = Quaternions.Squad(R2*Rb, T+deltat2, T_common)
Ra2 = Quaternions.Squad(Ra, T, T_common)
semilogy(T_common, Quaternions.angle(Quaternions.RDelta(Rb2, Ra2)), label='With rotation')
xlabel('Time')
ylabel(r'$\Phi_\Delta$')
legend();


(0.12300005072232557, [0.3356656622175119, 0.018635744929446446, 0.4286221558026306, 0.8386085617104737])

As expected, these are much like the results from the time-shifted case.

Corrected by optimizing time and rotation shifts

As above, these results should be much like the results from the time-shifted case.


In [14]:
deltat4,R4 = Quaternions.OptimalAlignment(4., 6., Ra, T, Rb, T)
Rb4 = Quaternions.Squad(R4*Rb, T+deltat4, T_common)
Ra4 = Quaternions.Squad(Ra, T, T_common)
print(deltat4, [R4[i] for i in range(4)])
semilogy(T_common, Quaternions.angle(Quaternions.RDelta(Rb4, Ra4)), label='With rotation')
xlabel('Time')
ylabel(r'$\Phi_\Delta$')
legend();


(0.12300005072232557, [0.33566566221751154, 0.018635744929446443, 0.4286221558026305, 0.8386085617104737])

And again, these are basically the same results.

Conclusions on alignments

The alignment routines all behave exactly as I expected. I have every reason, then, to call these "passed tests".