Studying Eigenvalues of Rotation Group Matrices

Table of Contents

  1. $ \text{SU}(2) $ Matrices
    1. Pulling random elements of the quaternions
    2. Mapping to $ \text{SU}(2) $ matrices
  2. $ \text{SO}(3) $ Matrices
  3. Spectra comparison
    1. $ \text{SU}(2) $
    2. $ \text{SO}(3) $
  4. Determinants In this exploration, I mess around with the spectra of two different rotation groups. I wanted to develop intuition for the elements of each group, and especially to discover through experimentation how \( \text{SU}(2) \) is a double cover of \( \text{SO}(3) \). Because my background is in numerical methods and not mathematics, I like to develop this through code, empirical visualization, and experimentation.

This is also a live test of some tweaking I’ve been doing to my tweaks to Org-Jekyll-Lite in order to run code and \(\LaTeX\) in Org documents and export it to web page.

\( \text{SU}(2) \) Matrices

Pulling random elements of the quaternions

Since elements of the group \( \text{SU}(2) \) are isomorphic to the unit quaternions, \(\mathbb{H}\), we will draw a unit quaternion and map it onto the matrix representation of SU(2).

import numpy as np
def random_quaternion() -> np.ndarray:
    u1, u2, u3 = np.random.random(3)
    q0 = np.sqrt(1 - u1) * np.sin(2 * np.pi * u2)
    q1 = np.sqrt(1 - u1) * np.cos(2 * np.pi * u2)
    q2 = np.sqrt(u1) * np.sin(2 * np.pi * u3)
    q3 = np.sqrt(u1) * np.cos(2 * np.pi * u3)
    return np.array([q0, q1, q2, q3])

Mapping to \( \text{SU}(2) \) matrices

The general form of the \( \text{SU}(2) \) matrix for a quaternion \( (q_0, q_1, q_2, q_3) \) is given by:

\( \begin{bmatrix} q_0 + iq_3 & q_2 + iq_1 \ -q_2 + iq_1 & q_0 - iq_3 \end{bmatrix} \)

def quaternion_to_su2(quaternion: np.ndarray) -> np.ndarray:
    # Convert a quaternion into its SU(2) matrix representation.
    q0, q1, q2, q3 = quaternion
    return np.array([[q0 + 1j*q3, q2 + 1j*q1],
                     [-q2 + 1j*q1, q0 - 1j*q3]])

def draw_random_su2() -> np.ndarray:
    return quaternion_to_su2(random_quaternion())

import pandas as pd

pd.DataFrame(quaternion_to_su2(random_quaternion()))

  0 1
0 -0.706679+0.640604j 0.141797+0.264810j
1 -0.141797+0.264810j -0.706679-0.640604j
pd.DataFrame(quaternion_to_su2(random_quaternion()))

  0 1
0 0.020562+0.599385j -0.636772+0.484599j
1 0.636772+0.484599j 0.020562-0.599385j

\( \text{SO}(3) \) Matrices

\( \text{SO}(3) \) matrices are 3x3 orthogonal matrices with determinant 1. They represent rotations in 3D space.

def quaternion_to_so3(quat: np.ndarray) -> np.ndarray:
    w, x, y, z = quat
    return np.array([
        [1 - 2*y**2 - 2*z**2,     2*x*y - 2*z*w,       2*x*z + 2*y*w],
        [2*x*y + 2*z*w,           1 - 2*x**2 - 2*z**2, 2*y*z - 2*x*w],
        [2*x*z - 2*y*w,           2*y*z + 2*x*w,       1 - 2*x**2 - 2*y**2]
    ])

def draw_random_so3() -> np.ndarray:
    quat = random_quaternion()
    return quaternion_to_so3(quat)

pd.DataFrame(draw_random_so3())

  0 1 2
0 -0.387516 -0.605739 -0.694918
1 0.468365 0.519912 -0.714371
2 0.794019 -0.602305 0.082233
pd.DataFrame(draw_random_so3())

  0 1 2
0 -0.965783 -0.034937 -0.256986
1 -0.036723 0.999323 0.002155
2 0.256737 0.011518 -0.966413

Spectra comparison

\( \text{SU}(2) \)

from numpy.linalg import eigvals
su2_eigs =np.array([eigvals(draw_random_su2()) for _ in range(10)])
pd.DataFrame(su2_eigs)

  0 1
0 -0.206546+0.978437j -0.206546-0.978437j
1 -0.388936+0.921265j -0.388936-0.921265j
2 0.024305-0.999705j 0.024305+0.999705j
3 0.025415+0.999677j 0.025415-0.999677j
4 -0.629142-0.777290j -0.629142+0.777290j
5 0.483896-0.875125j 0.483896+0.875125j
6 -0.498570+0.866850j -0.498570-0.866850j
7 -0.136583+0.990629j -0.136583-0.990629j
8 -0.331002+0.943630j -0.331002-0.943630j
9 -0.224566+0.974459j -0.224566-0.974459j

So the eigenvalues of \( \text{SU}(2) \) matrices are always complex conjugates of each other, and lie on the unit circle.

import pandas as pd
import numpy as np
pd.DataFrame(np.abs(su2_eigs[:5]))

  0 1
0 1.0 1.0
1 1.0 1.0
2 1.0 1.0
3 1.0 1.0
4 1.0 1.0

\( \text{SO}(3) \)

from numpy.linalg import eigvals
so3_eigs = np.array([eigvals(draw_random_so3()) for _ in range(10)])
pd.DataFrame(so3_eigs)

  0 1 2
0 -0.368758+0.929525j -0.368758-0.929525j 1.000000+0.000000j
1 -0.197081+0.980387j -0.197081-0.980387j 1.000000+0.000000j
2 0.436682+0.899616j 0.436682-0.899616j 1.000000+0.000000j
3 1.000000+0.000000j -0.935815+0.352491j -0.935815-0.352491j
4 -0.318919+0.947782j -0.318919-0.947782j 1.000000+0.000000j
5 -0.739021+0.673682j -0.739021-0.673682j 1.000000+0.000000j
6 1.000000+0.000000j -0.997412+0.071897j -0.997412-0.071897j
7 1.000000+0.000000j -0.019389+0.999812j -0.019389-0.999812j
8 -0.816935+0.576729j -0.816935-0.576729j 1.000000+0.000000j
9 1.000000+0.000000j -0.692390+0.721524j -0.692390-0.721524j

Two of the eigenvalues of \( \text{SO}(3) \) matrices are also always complex conjugates of each other, with the other unity.

import numpy as np
np.abs(pd.DataFrame(so3_eigs[:5]))

  0 1 2
0 1.0 1.0 1.0
1 1.0 1.0 1.0
2 1.0 1.0 1.0
3 1.0 1.0 1.0
4 1.0 1.0 1.0

Also, the eigenvalues of \( \text{SO}(3) \) also lie on the unit circle. So far, though, I have not yet seen how \( \text{SU}(2) \) is a covers \( \text{SO}(3) \) doubly.

Determinants

\( \text{SU}(2) \) matrices have determinant 1 by definition (“Special”). \( \text{SO}(3) \) matrices also have determinant 1 by definition (“Orthogonal”). So this is not a good way to distinguish between the two groups.

pd.DataFrame({"SU2":np.linalg.det([draw_random_su2() for _ in range(5)]), "SO3":np.linalg.det([draw_random_so3() for _ in range(5)])})

  SU2 SO3
0 1.0+0.0j 1.0
1 1.0+0.0j 1.0
2 1.0+0.0j 1.0
3 1.0+0.0j 1.0
4 1.0-0.0j 1.0



Enjoy Reading This Article?

Here are some more articles you might like to read next:

  • Why we can interpret softmax scores as probabilities
  • Literate emacs config as a webpage
  • Indicating which blocks are loaded in webpage literate elisp
  • Hosting my CV with github actions
  • Company Announcement about LLM Project