Simple Rotations

Author: Doug sweetser@alum.mit.edu

A deep leason from special relativity is that all measurements start as events in space-time. A measurment such as the length of a rod would appear to only involve space. Yet one needs a signal from both ends that gets received by the observer. And who is the observer? Every other system in the Universe is allowed. Since observers can be in very different states, that means the raw data these different observers collect from the same signals can be different. Special relaitivty helps determine what can be agreed upon, namely the interval for two inertial reference frame observers.

Spatial Rotations

In this iPython notebook, the one thing quaternions are known for - doing 3D spatial rotations - will be examined. When working with quaternions, it is not possible to work in just three spatial dimensions. There is always a fourth dimension, namely time. Even when thinking about spatial rotations, one must work with events in space-time.

Create a couple of quaternions to play with.


In [1]:
%%capture
from Q_tool_devo import Q8;
U=Q8([1,2,-3,4])
V=Q8([4,-2,3,1])
R=Q8([5,6,7,-8])

Use the Q8 class that places these 4 numbers in 8 slots like so:


In [2]:
print(U)
print(R)


(1, 0)_I0,2  (2, 0)_i1,3  (0, 3)_j1,3  (4, 0)_k1,3  Q
(5, 0)_I0,2  (6, 0)_i1,3  (7, 0)_j1,3  (0, 8)_k1,3  Q

If you are unfamiliar with this notation, the $I^2 = -1,\, i^3=-i,\, j^3=-j,\, k^3=-k$. Only positive numbers are used, with additive inverse put in these placeholders.

To do a rotation, one needs to pre-multiply by a quaternion, then post-multiply by its inverse. By doing so, the norm of R will not change because quaternions are a normed division algebra. A quaternion times its inverse has a norm of unity.


In [3]:
def rotate_R_by_U(R, U):
    """Given a space-time number R, rotate it by Q."""
    return U.triple_product(R, U.invert())

R_rotated = rotate_R_by_U(R,U)

Should we expect the first term to change? Look into the triple product first term where I use Capital variable for 3-vectors to simplify the presentation:

$$\begin{align}(u, W)&(t, R)(u, -W)/(u^2 + W \cdot W)\\ &= (u t - W \cdot R, u R + W t + W \times R)(u, -W)/(u^2 + W \cdot W)\\ &=(u^2 t - uW \cdot R + u W \cdot R + W \cdot W t - W \cdot W \times R, ...)/(u^2 + W \cdot W)\\ &=\left(\frac{u^2 + W \cdot W}{u^2 + W \cdot W}t,...\right) = (t, ...)\end{align}$$

Another way to see this is that the norm of $||R||$ does not change. That means the scalar plust the 3-vector norm do not change. While the 3-vector can shift who has values, the first term only has one term so should not change.

Now look at the rotated event.


In [4]:
print(R_rotated)
print(R_rotated.reduce())


(13.033333333333333, 8.033333333333333)_I0,2  (4.933333333333334, 16.266666666666666)_i1,3  (13.233333333333334, 8.9)_j1,3  (10.466666666666667, 11.8)_k1,3  QxQxQ.^-1
(5.0, 0)_I0,2  (0, 11.333333333333332)_i1,3  (4.333333333333334, 0)_j1,3  (0, 1.333333333333334)_k1,3  QxQxQ.^-1.reduce

The first term does change! At least in the non-reduced Q8 format, there is a change because it is composed of the positive and negative terms we saw in the algebra problem. For example, there is the vector identity W.WxR=0. The cross product makes a vector that is 90 degrees to both W and R. The dot product of that cross product with W is zero because nothing is in the direction of W anymore. This shows up algebraically because the 6 terms of the cross product have three positive terms and three negative terms that exactly cancel when dotted to W. But the values remain in the $I^0$ and $I^2$ terms until Q8 is reduced.

When the Q8 is reduced, it ends up being a 5 as expected. This may be of interest because we keep more information about the change with the eight positions to fill in the Q8 representation (none of which are empty after the rotation).

We expect the square of the norms to be identical in the reduce form:


In [5]:
print(R.norm_squared())
print(R_rotated.norm_squared())
print(R_rotated.norm_squared().reduce())


(174, 0)_I0,2  (0, 0)_i1,3  (0, 0)_j1,3  (0, 0)_k1,3  Q.norm_squared
(1026.4666666666667, 852.4666666666667)_I0,2  (0, 0)_i1,3  (0, 0)_j1,3  (0, 0)_k1,3  QxQxQ.^-1.norm_squared
(174.0, 0)_I0,2  (0, 0)_i1,3  (0, 0)_j1,3  (0, 0)_k1,3  QxQxQ.^-1.norm_squared.reduce

If squared, the reduced interval should be the same too:


In [6]:
print(R.square().reduce())
print(R_rotated.square())
print(R_rotated.square().reduce())


(0, 124)_I0,2  (60, 0)_i1,3  (70, 0)_j1,3  (0, 80)_k1,3  Q.Q_sq.reduce
(877.4666666666667, 1001.4666666666667)_I0,2  (389.94666666666666, 503.28)_i1,3  (487.94222222222226, 444.60888888888894)_j1,3  (462.4177777777778, 475.7511111111112)_k1,3  QxQxQ.^-1.QxQxQ.^-1_sq
(0, 124.0)_I0,2  (0, 113.33333333333331)_i1,3  (43.333333333333314, 0)_j1,3  (0, 13.333333333333371)_k1,3  QxQxQ.^-1.QxQxQ.^-1_sq.reduce

But what should we make of these non-reduced calculations? Here is my speculation. In classical physics, one always, always, always uses the reduced form of a Q8 quaternion measurement. Classical physics involves one thing doing something. Physics gets odd when dealing with relativistic quantum feild theory. That is a rare sport played only when a one packet of protons collides with another inside an atom smasher. In those entirely odd situations, one must start thinking about multiple particles because we cannot know what happened, there is too much energy around, so we sum over all possible histories.

It is simple to move an event to another place in space-time the same distance from the origin. Because it is a transient event, it feels fleeting, which it should.

Rotations as a Well-behaved Function

Do rotations preserve the group structure of quaternions with multiplication? If it did, then:

$$\rm{Rot}(V*U) R = \rm{Rot}(V) *\rm{Rot}(U) R $$

The product of V*U into the rotation function is identical to doing one after the other.


In [7]:
product_UV = rotate_R_by_U(R, V.product(U))
product_rotations = rotate_R_by_U(rotate_R_by_U(R, V), U)
print(product_UV)
print(product_rotations)
print(product_UV.reduce())
print(product_rotations.reduce())


(38.55777777777778, 33.55777777777777)_I0,2  (32.315555555555555, 39.871111111111105)_i1,3  (37.84, 34.41777777777778)_j1,3  (40.64222222222222, 31.686666666666667)_k1,3  QxQxQxQxQ.^-1
(38.55777777777778, 33.55777777777778)_I0,2  (37.92666666666667, 34.260000000000005)_i1,3  (41.19555555555556, 31.06222222222222)_j1,3  (33.297777777777775, 39.03111111111111)_k1,3  QxQxQxQ.^-1xQ.^-1
(5.000000000000007, 0)_I0,2  (0, 7.55555555555555)_i1,3  (3.4222222222222243, 0)_j1,3  (8.955555555555556, 0)_k1,3  QxQxQxQxQ.^-1.reduce
(5.0, 0)_I0,2  (3.6666666666666643, 0)_i1,3  (10.133333333333336, 0)_j1,3  (0, 5.733333333333334)_k1,3  QxQxQxQ.^-1xQ.^-1.reduce

This looks well-behaved because the the U and V if the U and V form a product before being applied, it results in the same answer as doing one after the other. I was a bit surprised this work without having to reduce the results.

A Rotation of Time and Space

A rotation in time is commonly called a boost. The idea is that one gets a boost in speed, and that will change measurements of both time and distance. If one rushes toward the source of a signal, both the measurement of time and distance will get shorter in a way that keeps the interval the same.

There are published claims in the literature that a boost cannot be done with real-valued quaternions. This may be because people followed the form of rotations in space too closely. It is true that swapping hyperbolic cosines for cosines, and hyperbolic sines for sines does not create a Lorentz boost. Rotations are known as a compact Lie group while boosts form a group that is not compact. A slightly more complicated combination of the hyperbolic trig functions does do the work:

$$\begin{align*} b \rightarrow b' = &(\cosh(\alpha), \sinh(\alpha) (t, R) (\cosh(\alpha), -\sinh(\alpha) \\&- \frac{1}{2}(((\cosh(\alpha), \sinh(\alpha) (\cosh(\alpha), \sinh(\alpha) (t,R))^* -((\cosh(\alpha), -\sinh(\alpha) (\cosh(\alpha), -\sinh(\alpha) (t,R))^*)\\ &=(\cosh(\alpha) t - \sinh(\alpha) R, \cosh(\alpha) R - \sinh(\alpha) t)\end{align*}$$

In [8]:
R_boosted=R.boost(0.01,0.02, 0.003)
print("boosted: {}".format(R_boosted.reduce()))
print(R.square().reduce())
print(R_boosted.square())
print(R_boosted.square().reduce())


boosted: (4.65291333288644, 0)_I0,2  (5.903470866671137, 0)_i1,3  (6.80694173334227, 0)_j1,3  (0, 8.028958739998657)_k1,3  Q.boost.reduce
(0, 124)_I0,2  (60, 0)_i1,3  (70, 0)_j1,3  (0, 80)_k1,3  Q.Q_sq.reduce
(332.92404269673744, 456.9240426967374)_I0,2  (168.7613021070281, 113.8246254953465)_i1,3  (193.37159851076186, 130.0273786162631)_j1,3  (146.4900636716656, 221.20616201273532)_k1,3  Q.boost.Q.boost_sq
(0, 123.99999999999994)_I0,2  (54.93667661168159, 0)_i1,3  (63.344219894498764, 0)_j1,3  (0, 74.71609834106971)_k1,3  Q.boost.Q.boost_sq.reduce

The reduced interval is $124 \,I^2$, whether boosted or not. The norm will shrink because all the number are a little smaller, no longer quite (5, 6, 7, 8).


In [9]:
print(R.norm_squared().reduce())
print(R_boosted.norm_squared())
print(R_boosted.norm_squared().reduce())


(174, 0)_I0,2  (0, 0)_i1,3  (0, 0)_j1,3  (0, 0)_k1,3  Q.norm_squared.reduce
(478.5736451800898, 311.27444021338505)_I0,2  (0, 0)_i1,3  (0, 0)_j1,3  (0, 0)_k1,3  Q.boost.norm_squared
(167.29920496670474, 0)_I0,2  (0, 0)_i1,3  (0, 0)_j1,3  (0, 0)_k1,3  Q.boost.norm_squared.reduce

Rotations in Space and Time

Quaternions are just numbers. This makes combining transformations trivial. A measurement can be rotated and boosted. The only thing that should be unchanged is the interval:


In [10]:
R_rotated_and_boosted = R_rotated.boost(0.01,0.02, 0.003)
print("rotated and boosted: {}".format(R_rotated_and_boosted.reduce()))
print(R.square().reduce())
print(R_rotated_and_boosted.square())
print(R_rotated_and_boosted.square().reduce())


rotated and boosted: (5.066457160027788, 0)_I0,2  (0, 11.43399790493362)_i1,3  (4.132004190132783, 0)_j1,3  (0, 1.3635327048134158)_k1,3  QxQxQ.^-1.boost.reduce
(0, 124)_I0,2  (60, 0)_i1,3  (70, 0)_j1,3  (0, 80)_k1,3  Q.Q_sq.reduce
(4220.926982084568, 4344.926982084568)_I0,2  (1987.3008154686133, 2103.1605365750006)_i1,3  (2147.719618485641, 2105.850374056915)_j1,3  (2132.0756217717103, 2145.892181842178)_k1,3  QxQxQ.^-1.boost.QxQxQ.^-1.boost_sq
(0, 124.0)_I0,2  (0, 115.85972110638727)_i1,3  (41.86924442872623, 0)_j1,3  (0, 13.816560070467858)_k1,3  QxQxQ.^-1.boost.QxQxQ.^-1.boost_sq.reduce

Because of the rotation, the z value was larger. It is a safe bet that the norm turns out to be smaller as happened before:


In [11]:
print(R.norm_squared().reduce())
print(R_rotated_and_boosted.norm_squared())
print(R_rotated_and_boosted.norm_squared().reduce())


(174, 0)_I0,2  (0, 0)_i1,3  (0, 0)_j1,3  (0, 0)_k1,3  Q.norm_squared.reduce
(4370.5959702389655, 4195.257993930171)_I0,2  (0, 0)_i1,3  (0, 0)_j1,3  (0, 0)_k1,3  QxQxQ.^-1.boost.norm_squared
(175.33797630879417, 0)_I0,2  (0, 0)_i1,3  (0, 0)_j1,3  (0, 0)_k1,3  QxQxQ.^-1.boost.norm_squared.reduce

Ratios at Work

An angle is a ratio, this side over that side. The velocity used in a boost is a ratio of distance over time. Say we have a reference observer who measures the interval between two events. If another observer see the same events, but was standing on his head, we migh expect the headstand to change how the crazy observer places his numbers into the three spatial slots. Yet the two observers should agree to the inteval, and they do.

The same happens if there is an observer travelling at a constant velocity relative to the referene observer. It is not surprizing that the math machinery is different because a spatial rotation is different from moving along at certain velocity.

Combining the spatial rotations and boosts can be done, creating messy results, except for the interval that remains the same.


In [12]:
print(R.product(U).dif(U.product(R)))


(70, 70)_I0,2  (72, 64)_i1,3  (22, 102)_j1,3  (28, 92)_k1,3  QxQ-QxQ

In [13]:
print(R.vahlen_conj().product(U.vahlen_conj()).dif(U.vahlen_conj().product(R.vahlen_conj())))


(70, 70)_I0,2  (72, 64)_i1,3  (22, 102)_j1,3  (28, 92)_k1,3  Q.vc-xQ.vc--Q.vc-xQ.vc-

In [14]:
print(R.vahlen_conj("'").product(U.vahlen_conj("'")).dif(U.vahlen_conj("'").product(R.vahlen_conj("'"))))


(70, 70)_I0,2  (64, 72)_i1,3  (102, 22)_j1,3  (28, 92)_k1,3  Q.vc'xQ.vc'-Q.vc'xQ.vc'

In [ ]: