> ## Documentation Index
> Fetch the complete documentation index at: https://aegean.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# The rotation group SO(2) as a circle

> How 2x2 rotation matrices trace a one-dimensional circle inside four-dimensional matrix space as the angle changes.

<a href="https://colab.research.google.com/github/pantelis/eng-ai-agents/blob/main/notebooks/kinematics/planar-motion/so2-manifold/index.ipynb" target="_blank" rel="noopener noreferrer">
  <img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open In Colab" style={{ marginBottom: "1rem" }} />
</a>

A $2 \times 2$ matrix has four entries, so you can think of it as a single point in $\mathbb{R}^4$. Most of those points are not rotations. A rotation matrix must satisfy three constraints: each column has unit length (two constraints), and the two columns are orthogonal (one more). Four numbers minus three constraints leaves one degree of freedom, the angle $\theta$.

$$
R(\theta) = \begin{bmatrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \end{bmatrix}
$$

As $\theta$ changes, $R(\theta)$ moves through $\mathbb{R}^4$ and traces a curve. That curve is $SO(2)$. It is a one-dimensional manifold living inside a four-dimensional space. In this section you build the curve numerically, find out what shape it is, and see how a velocity along it gives an element of $\mathfrak{so}(2)$.

## Background on Lie groups and their algebras

Rigid-body configurations are encoded by 4×4 homogeneous transformation matrices. Rotation matrices $R \in SO(3)$ represent orientation. Six-dimensional twists describe spatial velocities. Six-dimensional wrenches represent spatial forces.

The table lists the spaces used in rigid-body motion representations, with the notation of Lynch and Park (Kevin M. Lynch, 2017).

| Space | Elements | Dimension | Represents |
| - | - | - | - |
| $SO(2)$ | $2 \times 2$ matrices $R$ with $R^T R = I$, $\det R = 1$ | 1 | Planar orientation |
| $\mathfrak{so}(2)$ | $2 \times 2$ skew-symmetric matrices $\begin{bmatrix} 0 & -\omega \\ \omega & 0 \end{bmatrix}$ | 1 | Planar angular velocity |
| $SE(2)$ | $3 \times 3$ matrices $\begin{bmatrix} R & \mathbf{p} \\ 0 & 1 \end{bmatrix}$, $R \in SO(2)$, $\mathbf{p} \in \mathbb{R}^2$ | 3 | Planar configuration $(x, y, \theta)$ |
| $\mathfrak{se}(2)$ | $3 \times 3$ matrices $\begin{bmatrix} [\omega] & v \\ 0 & 0 \end{bmatrix}$, $[\omega] \in \mathfrak{so}(2)$, $v \in \mathbb{R}^2$ | 3 | Planar twist |
| $SO(3)$ | $3 \times 3$ matrices $R$ with $R^T R = I$, $\det R = 1$ | 3 | Orientation in 3D |
| $\mathfrak{so}(3)$ | $3 \times 3$ skew-symmetric matrices $[\omega]$ | 3 | Angular velocity, exponential coordinates $[\hat{\omega}]\theta$ |
| $SE(3)$ | $4 \times 4$ matrices $T = \begin{bmatrix} R & \mathbf{p} \\ 0 & 1 \end{bmatrix}$, $R \in SO(3)$, $\mathbf{p} \in \mathbb{R}^3$ | 6 | Configuration (pose) in 3D |
| $\mathfrak{se}(3)$ | $4 \times 4$ matrices $[V] = \begin{bmatrix} [\omega] & v \\ 0 & 0 \end{bmatrix}$, $[\omega] \in \mathfrak{so}(3)$, $v \in \mathbb{R}^3$ | 6 | Twist, screw axis $[\mathcal{S}]\theta$ |
| $\mathbb{R}^3$ | Vectors | 3 | Position $\mathbf{p}$, angular velocity $\omega$, exponential coordinates $\hat{\omega}\theta$ |
| $\mathbb{R}^6$ | Vectors | 6 | Twist $V = (\omega, v)$, wrench $F = (\tau, f)$ |
| $S^3$ | Unit quaternions $q$ with $\lVert q \rVert = 1$ | 3 | Orientation; $q$ and $-q$ give the same rotation |

$SO(n)$ and $SE(n)$ are Lie groups, the curved spaces where configurations live. $\mathfrak{so}(n)$ and $\mathfrak{se}(n)$ are their Lie algebras, the flat tangent spaces at the identity where velocities live. The matrix exponential maps a velocity to a configuration: $\exp: \mathfrak{so}(3) \to SO(3)$ and $\exp: \mathfrak{se}(3) \to SE(3)$. The matrix logarithm goes the other way, from a configuration to a velocity that produces it in unit time: $\log: SO(3) \to \mathfrak{so}(3)$ and $\log: SE(3) \to \mathfrak{se}(3)$. The exponential is many-to-one, so a rotation has many logarithms. In the plane, the angles $\theta$ and $\theta + 2\pi$ give the same rotation; choosing the principal angle in $(-\pi, \pi]$ makes the planar logarithm a function, and on that branch it inverts the exponential. In three dimensions, restricting the rotation angle to $[0, \pi]$ is not enough on its own: a rotation by exactly $\pi$ still has two logarithms, because the axes $\hat{\omega}$ and $-\hat{\omega}$ give the same rotation. There the logarithm is a single-valued inverse for angles below $\pi$, and a rotation by $\pi$ needs a sign convention for the axis. The $[\cdot]$ operator turns a vector in $\mathbb{R}^3$ or $\mathbb{R}^6$ into its matrix form in $\mathfrak{so}(3)$ or $\mathfrak{se}(3)$.

This section studies the first two rows of the table, $SO(2)$ and $\mathfrak{so}(2)$. They are small enough that you can compute and draw everything about them.

## Sampling the curve

Each sample of $\theta$ gives one rotation matrix. Flattening the matrix row by row gives its point in $\mathbb{R}^4$, with coordinates $(r_{11}, r_{12}, r_{21}, r_{22})$.

```python theme={null}
import matplotlib.pyplot as plt
import numpy as np


def rot(theta):
    c, s = np.cos(theta), np.sin(theta)
    return np.array([[c, -s], [s, c]])


thetas = np.linspace(0, 2 * np.pi, 400)
Rs = np.stack([rot(t) for t in thetas])  # shape (400, 2, 2)
points = Rs.reshape(len(thetas), 4)  # (r11, r12, r21, r22) per row

# Every sample satisfies the rotation constraints
orth_err = np.abs(np.einsum("nji,njk->nik", Rs, Rs) - np.eye(2)).max()
det_err = np.abs(np.linalg.det(Rs) - 1).max()
print(f"max |R^T R - I| = {orth_err:.1e}")
print(f"max |det R - 1| = {det_err:.1e}")
```

```output theme={null}
max |R^T R - I| = 2.2e-16
max |det R - 1| = 2.2e-16
```

## The shape of the curve

The four coordinates of $R(\theta)$ are $(\cos\theta, -\sin\theta, \sin\theta, \cos\theta)$. Two of them repeat the other two: $r_{22} = r_{11}$ and $r_{12} = -r_{21}$. These two linear relations confine the curve to a two-dimensional plane inside $\mathbb{R}^4$. Within that plane, the remaining constraint $r_{11}^2 + r_{21}^2 = 1$ is the equation of a circle.

So $SO(2)$ is a circle. Its distance from the origin of $\mathbb{R}^4$ is the Frobenius norm of $R$, which is $\sqrt{2}$ for every rotation. The curve is also closed: $\theta$ and $\theta + 2\pi$ give the same matrix, so walking once around brings you back to where you started.

```python theme={null}
r11, r12, r21, r22 = points.T

print(f"max |r22 - r11|        = {np.abs(r22 - r11).max():.1e}")
print(f"max |r12 + r21|        = {np.abs(r12 + r21).max():.1e}")
print(f"distance from origin   = {np.linalg.norm(points, axis=1).min():.4f} to {np.linalg.norm(points, axis=1).max():.4f}")
print(f"sqrt(2)                = {np.sqrt(2):.4f}")
print(f"closes on itself       = {np.allclose(rot(0), rot(2 * np.pi))}")
```

```output theme={null}
max |r22 - r11|        = 0.0e+00
max |r12 + r21|        = 0.0e+00
distance from origin   = 1.4142 to 1.4142
sqrt(2)                = 1.4142
closes on itself       = True
```

## Drawing the curve

You cannot draw $\mathbb{R}^4$, but for rotations you do not need to. Because $r_{22}$ always equals $r_{11}$, dropping it loses no information. The plot below uses the three coordinates $(r_{11}, r_{21}, r_{12})$, and the color shows the angle $\theta$. The curve is a tilted circle centered on the origin.

<img src="https://mintcdn.com/aegeanaiinc/-0uotr_0Tspiho82/aiml-common/lectures/kinematics/planar-motion/so2-manifold/images/cell_3_output_1.png?fit=max&auto=format&n=-0uotr_0Tspiho82&q=85&s=bf21c5f968c4157695a0bba9b8074d0c" alt="Output from cell 3" width="904" height="811" data-path="aiml-common/lectures/kinematics/planar-motion/so2-manifold/images/cell_3_output_1.png" />

## The determinant as signed area

The *unit square* has corners $(0,0)$, $(1,0)$, $(1,1)$ and $(0,1)$. Its sides are the basis vectors $\mathbf{e}_1 = (1, 0)$ and $\mathbf{e}_2 = (0, 1)$, and its area is 1. A matrix sends $\mathbf{e}_1$ to its first column and $\mathbf{e}_2$ to its second column, so it turns the unit square into the parallelogram spanned by its two columns. For

$$
A = \begin{bmatrix} a & b \\ c & d \end{bmatrix}
$$

that parallelogram has signed area $ad - bc$, which is $\det A$. Because the square started with area 1, the determinant is the factor by which the matrix scales area. The same factor applies to every region, since any region can be tiled with small squares.

The sign records orientation. Walking around the unit square from $\mathbf{e}_1$ to $\mathbf{e}_2$ is a counterclockwise turn. If the image of $\mathbf{e}_2$ is still counterclockwise from the image of $\mathbf{e}_1$, the determinant is positive. If the matrix flips the square over like a mirror, that turn becomes clockwise and the determinant is negative. A determinant of zero means the square is flattened onto a line and the matrix cannot be undone.

The determinant is not a measure of how large a matrix is. $\begin{bmatrix} 3 & 0 \\ 0 & 1/3 \end{bmatrix}$ stretches one direction by a factor of 3, yet its determinant is 1, because the squeeze in the other direction cancels the stretch in area. How far a matrix can stretch a vector is measured by its norm or its singular values instead.

```python theme={null}
examples = {
    "rotation by 30°": rot(np.pi / 6),
    "reflection": np.array([[np.cos(np.pi / 3), np.sin(np.pi / 3)], [np.sin(np.pi / 3), -np.cos(np.pi / 3)]]),
    "uniform scaling by 1.5": 1.5 * np.eye(2),
    "stretch and squeeze": np.array([[3, 0], [0, 1 / 3]]),
    "singular": np.array([[1, 0.5], [1, 0.5]]),
}

unit_square = np.array([[0, 0], [1, 0], [1, 1], [0, 1]]).T  # corners as columns


def signed_area(corners):
    # Shoelace formula; corners has shape (2, N) in walking order
    x, y = corners
    return 0.5 * np.sum(x * np.roll(y, -1) - np.roll(x, -1) * y)


for name, A in examples.items():
    image = A @ unit_square
    print(f"{name:24s} det = {np.linalg.det(A):+.3f}   signed area of image = {signed_area(image):+.3f}")
```

```output theme={null}
rotation by 30°          det = +1.000   signed area of image = +1.000
reflection               det = -1.000   signed area of image = -1.000
uniform scaling by 1.5   det = +2.250   signed area of image = +2.250
stretch and squeeze      det = +1.000   signed area of image = +1.000
singular                 det = +0.000   signed area of image = +0.000
```

<img src="https://mintcdn.com/aegeanaiinc/-0uotr_0Tspiho82/aiml-common/lectures/kinematics/planar-motion/so2-manifold/images/cell_5_output_1.png?fit=max&auto=format&n=-0uotr_0Tspiho82&q=85&s=ab393707c5e49d9abb7927ec69491d01" alt="Output from cell 5" width="2385" height="543" data-path="aiml-common/lectures/kinematics/planar-motion/so2-manifold/images/cell_5_output_1.png" />

In the rotation, the red and blue arrows keep their counterclockwise order and the square keeps its area. In the reflection, the area is unchanged but the blue arrow now sits clockwise from the red one. The uniform scaling multiplies the area by $1.5^2 = 2.25$. The stretch and squeeze changes the shape a lot but keeps the area. The singular matrix sends both columns onto the same line, so the square has no area left.

## Rotations and reflections

Orthonormal columns alone do not make a rotation. The matrices

$$
F(\theta) = \begin{bmatrix} \cos\theta & \sin\theta \\ \sin\theta & -\cos\theta \end{bmatrix}
$$

also have orthonormal columns, but $\det F = -1$. Each one is a reflection across a line through the origin. As $\theta$ changes, they trace a second circle in $\mathbb{R}^4$. Together the two circles make up $O(2)$, the group of all orthogonal $2 \times 2$ matrices, and the condition $\det R = +1$ selects the circle that contains the identity.

No continuous path of orthogonal matrices can turn a rotation into a reflection, because $\det$ would have to jump from $+1$ to $-1$. So $SO(2)$ is a manifold in its own right, not just half of $O(2)$. The code below confirms that every rotation is at exactly the same distance, 2, from every reflection.

```python theme={null}
def refl(theta):
    c, s = np.cos(theta), np.sin(theta)
    return np.array([[c, s], [s, -c]])


Fs = np.stack([refl(t) for t in thetas])
print(f"det of reflections: {np.linalg.det(Fs).min():.3f} to {np.linalg.det(Fs).max():.3f}")

# Distance in R^4 between every rotation and every reflection
F_points = Fs.reshape(len(thetas), 4)
dists = np.linalg.norm(points[:, None, :] - F_points[None, :, :], axis=2)
print(f"rotation-to-reflection distance: min {dists.min():.4f}, max {dists.max():.4f}")
```

```output theme={null}
det of reflections: -1.000 to -1.000
rotation-to-reflection distance: min 2.0000, max 2.0000
```

## Seeing both circles

The coordinates $(r_{11}, r_{12}, r_{21}, r_{22})$ hide the geometry. A better set of axes for $\mathbb{R}^4$ comes from the relations each family satisfies:

$$
a = \tfrac{r_{11} + r_{22}}{\sqrt{2}}, \quad
b = \tfrac{r_{21} - r_{12}}{\sqrt{2}}, \quad
c = \tfrac{r_{11} - r_{22}}{\sqrt{2}}, \quad
d = \tfrac{r_{21} + r_{12}}{\sqrt{2}}
$$

These four axes are orthonormal, so they describe the same space with no distortion. A rotation has $c = d = 0$ and lands at $(a, b) = \sqrt{2}(\cos\theta, \sin\theta)$. A reflection has $a = b = 0$ and lands at $(c, d) = \sqrt{2}(\cos\theta, \sin\theta)$. So $\mathbb{R}^4$ splits into two perpendicular planes. The rotations fill a circle of radius $\sqrt{2}$ in the $(a, b)$ plane, and the reflections fill a circle of the same radius in the $(c, d)$ plane.

Each circle collapses to the origin of the other plane. That also explains the constant distance: any rotation is $\sqrt{2}$ from the origin in one plane, any reflection is $\sqrt{2}$ from the origin in the perpendicular plane, and by Pythagoras they are $\sqrt{2 + 2} = 2$ apart.

```python theme={null}
def plane_coords(M):
    # M has shape (N, 2, 2); returns (N, 4) with columns a, b, c, d
    r11, r12, r21, r22 = M[:, 0, 0], M[:, 0, 1], M[:, 1, 0], M[:, 1, 1]
    return np.stack([r11 + r22, r21 - r12, r11 - r22, r21 + r12], axis=1) / np.sqrt(2)


rot_abcd = plane_coords(Rs)
ref_abcd = plane_coords(Fs)
print(f"rotations:   max |c|, |d| = {np.abs(rot_abcd[:, 2:]).max():.1e}")
print(f"reflections: max |a|, |b| = {np.abs(ref_abcd[:, :2]).max():.1e}")
```

```output theme={null}
rotations:   max |c|, |d| = 0.0e+00
reflections: max |a|, |b| = 0.0e+00
```

<img src="https://mintcdn.com/aegeanaiinc/-0uotr_0Tspiho82/aiml-common/lectures/kinematics/planar-motion/so2-manifold/images/cell_8_output_1.png?fit=max&auto=format&n=-0uotr_0Tspiho82&q=85&s=a22cbb0b8a21ea07453f4a2de005e528" alt="Output from cell 8" width="1635" height="824" data-path="aiml-common/lectures/kinematics/planar-motion/so2-manifold/images/cell_8_output_1.png" />

## One column is enough

The first column of $R(\theta)$ is where the rotation sends the unit vector $\mathbf{e}_1$. It is the point $(\cos\theta, \sin\theta)$ on the unit circle of the plane. Once you know the first column, orthogonality and $\det R = +1$ fix the second: it is the first column turned by $90°$ counterclockwise. So every rotation corresponds to exactly one point on the unit circle, and every point on the unit circle to exactly one rotation. This is the precise sense in which $SO(2)$ *is* a circle.

The top panel follows the first column as $\theta$ grows. The row below shows the full rotated frame $\{R\mathbf{e}_1, R\mathbf{e}_2\}$ at each of the same angles.

<img src="https://mintcdn.com/aegeanaiinc/-0uotr_0Tspiho82/aiml-common/lectures/kinematics/planar-motion/so2-manifold/images/cell_9_output_1.png?fit=max&auto=format&n=-0uotr_0Tspiho82&q=85&s=d3b3dfa020b3b2e42fdc1c349c329dbd" alt="Output from cell 9" width="1425" height="930" data-path="aiml-common/lectures/kinematics/planar-motion/so2-manifold/images/cell_9_output_1.png" />

## The tangent at the identity

A one-dimensional manifold looks like a straight line when you zoom in close enough. At the identity ($\theta = 0$), that line is the tangent to the curve. Its direction is the derivative of $R(\theta)$ at $\theta = 0$:

$$
\left.\frac{dR}{d\theta}\right|_{\theta=0} = \begin{bmatrix} -\sin 0 & -\cos 0 \\ \cos 0 & -\sin 0 \end{bmatrix} = \begin{bmatrix} 0 & -1 \\ 1 & 0 \end{bmatrix}
$$

This is a skew-symmetric matrix, an element of $\mathfrak{so}(2)$. Scaling it by an angular rate $\omega$ gives every possible velocity at the identity, so $\mathfrak{so}(2)$ is one-dimensional, just like $SO(2)$. The matrix exponential maps back onto the curve: exponentiating $\theta$ times this tangent matrix reproduces $R(\theta)$.

```python theme={null}
from scipy.linalg import expm

h = 1e-6
dR = (rot(h) - rot(-h)) / (2 * h)  # central difference at theta = 0
print("dR/dtheta at identity:")
print(np.round(dR, 6))
print("skew-symmetric:", np.allclose(dR, -dR.T))

theta = 1.2
print(f"\nexp({theta} * dR) equals R({theta}):", np.allclose(expm(theta * dR), rot(theta)))
```

```output theme={null}
dR/dtheta at identity:
[[ 0. -1.]
 [ 1.  0.]]
skew-symmetric: True

exp(1.2 * dR) equals R(1.2): True
```

**Key references**: (The Matrix Cookbook, n.d.)

## References

* Kevin M. Lynch, F. (2017). *Modern Robotics*.
* (n.d.). *The Matrix Cookbook*.

***

<Callout icon="pen-to-square" iconType="regular">
  [Edit this page on GitHub](https://github.com/aegean-ai/eaia/edit/main/src/aiml-common/lectures/kinematics/planar-motion/so2-manifold/index.mdx) or [file an issue](https://github.com/aegean-ai/eaia/issues/new/choose).
</Callout>
