LaValle, Chapter 4

The Configuration Space

The abstraction that turns every motion planning problem into a point navigating through a manifold — from topology to SE(3).

Prerequisites: Linear algebra + Chapter 2. That’s it.
12
Chapters
8+
Simulations
12
Quizzes

Chapter 0: The Problem

Imagine you need to move a grand piano from one room to another through a narrow hallway. In the real world, you have to think about the piano’s exact shape, how to tilt it, how much clearance you need at each angle. Every possible position and orientation of the piano — every way it could be arranged in space — is a separate thing you might need to consider. This is motion planning in the workspace, and it is a geometric nightmare.

Here is the insight that unlocks all of Chapter 4. The piano has three degrees of freedom in 2D: its x position, its y position, and its orientation angle θ. Every possible arrangement of the piano is completely described by exactly three numbers — the triple (x, y, θ). Instead of thinking about the piano as a shape sweeping through 2D space, we can think about the triple (x, y, θ) as a single point moving through a 3-dimensional space. That three-dimensional space is the configuration space, or C-space.

The key insight (LaValle Ch. 4 opening): “Once the configuration space is clearly understood, many motion planning problems that appear different in terms of geometry and kinematics can be solved by the same planning algorithms.” The robot’s shape disappears. Only a point in a manifold remains.

Walls in the physical room become regions of C-space called Cobs — configurations where the piano would overlap an obstacle. Everything else is Cfree = C ∖ Cobs. The piano-moving problem becomes: find a continuous path for a point through Cfree. All the planning algorithms from Chapter 2 — graph search, RRTs, PRMs — can then operate on this abstract space without caring at all about the piano’s shape.

The canvas below makes this concrete. The left panel shows a 2D room with a rectangular piano (dark orange). Drag it around. The right panel shows the corresponding point in (x, y) C-space — colored by the current angle θ. The gray region in the right panel is Cobs: configurations that collide with the wall. Watch the point enter the gray region when the piano touches a wall.

Workspace vs C-Space: The Piano Problem

Drag the sliders to move and rotate the piano. Left: workspace view. Right: C-space view with Cobs shaded. The colored dot is the current configuration.

x 40
y 50
θ
Cfree — no collision

Why every DOF adds a dimension

The piano in 2D has 3 DOF, so its C-space is 3-dimensional. A degree of freedom (DOF) is an independent way the robot can move. A point robot sliding on a 2D plane has 2 DOF: it can move left-right and up-down. Its C-space is R2. A rod that can also spin has 3 DOF: x, y, θ. Its C-space is R2 × S1. A 6-DOF robotic arm — 6 revolute joints — has a 6-dimensional C-space.

This is why C-space is such a powerful abstraction. No matter how geometrically complex the robot is in the workspace, it is always a single point in C-space. A 6-DOF industrial arm with links, joints, and end-effectors becomes a point in R6 (or a 6-dimensional manifold, as we’ll see). The planning algorithms don’t need to know anything about the arm’s shape — they just navigate a 6D space.

The topology of C-space is not always Rn

Here is where beginners trip up. C-space has dimension equal to the number of DOF, but the topology is not always Rn. The translation DOF of a robot moving in an unbounded plane give a factor of R2 — that part really is flat and infinite. But a revolute joint contributes a factor of S1 (a circle), not R. A 2-link arm has C-space T2 = S1×S1, not R2. Confusing these leads to planning algorithms that miss valid paths, declare reachable configurations unreachable, or compute incorrect path lengths.

Chapters 1–3 of this lesson build the topological vocabulary needed to distinguish these cases: topological spaces, homeomorphisms, identification spaces, and the fundamental group. Chapter 4 applies that vocabulary to SE(2) and SE(3), the C-spaces of rigid bodies in 2D and 3D. From Chapter 6 onward, we handle kinematic chains, where multiple joints chain together multiplicatively.

Robot typeDOFC-spaceTopology
2D point robot2R2Flat plane
2D rigid body (piano)3R2×S1 = SE(2)Cylinder × line
2-link revolute arm2T2 = S1×S1Torus
3D rigid body6R3×SO(3) = SE(3)R3×RP3
6-DOF industrial arm6T6 = (S1)66-torus

Visualizing high-dimensional C-spaces

One of the hardest things about C-space is that humans cannot visualize spaces with more than 3 dimensions. A 6-DOF robot arm has a 6-dimensional C-space, and Cobs is a 6-dimensional region. There is no way to look at it directly. This is not a failure of imagination — it is a fundamental limitation, and it is why developing rigorous mathematical understanding of C-space topology matters. The math lets us reason about spaces we cannot see.

Practical approaches to high-dimensional C-space: (1) Projection: fix four joint values and vary two, plotting the 2D slice of Cobs. This reveals local structure but misses global connectivity. (2) Sampling: scatter random configurations and check collision; build a statistical picture of Cfree’s density. (3) Roadmaps: build a sparse graph of collision-free configurations and use it as a proxy for Cfree’s connectivity. This is the PRM algorithm of Chapter 5. (4) Topology: use theoretical results about the manifold structure (what we’re building in this chapter) to understand the global properties without needing to visualize them.

The curse of dimensionality hits C-space hard. To uniformly grid a k-dimensional space with n points per dimension, you need nk total points. A 6-DOF arm with 100 points per joint angle needs 1006 = 1012 configurations. This is why grid-based planners are infeasible for high-DOF systems, and why sampling-based planners (which don’t enumerate all of C-space) are the dominant approach for modern robotics.

From workspace to C-space: computing Cobs

Knowing the C-space structure is not enough. We also need to know which configurations are in collision (the C-space obstacle region, Cobs). Formally: a configuration q is in Cobs if the robot at configuration q intersects at least one obstacle. That is:

Cobs = { q ∈ C : A(q) ∩ O ≠ ∅ }

where A(q) is the set of points occupied by the robot at configuration q (the robot’s “swept volume” in the workspace), and O is the union of all obstacles. Cfree = C ∖ Cobs.

The direct computation of Cobs is expensive. For a polygon robot and polygon obstacles, Cobs can be computed exactly using the Minkowski sum (Chapter 9 of this lesson). But in higher-dimensional C-spaces, exact Cobs computation is intractable. This is why sampling-based planners (Chapter 5) avoid computing Cobs explicitly — they just check whether a sampled configuration q is in Cobs by checking A(q) ∩ O using a fast collision detection library.

The collision checker is the most expensive operation in a planner. In practice, 90%+ of a sampling-based planner’s runtime is spent in collision checking — computing whether a given configuration q has the robot intersecting an obstacle. The C-space framework turns this into a yes/no query that separates geometric computation (the collision checker) from topological computation (the planner). This separation is what makes the whole framework work in practice.

A worked numerical example: the piano

Let’s make C-space concrete with actual numbers. Say the piano is a rectangle 2.0 m long × 1.0 m wide. The hallway is 1.5 m wide. The piano’s configuration is (x, y, θ) where x, y are the center coordinates and θ is the rotation angle.

At θ = 0° (piano aligned with the hallway), the piano’s footprint is 2.0 m × 1.0 m. It fits in the 1.5 m hallway in the 1.0 m dimension but not the 2.0 m dimension. At θ = 90°, the footprint is 1.0 m × 2.0 m — the 1.0 m side fits, but now it’s 2.0 m in the hallway’s width direction. At θ = 45°, the bounding box is (2.0 sin 45° + 1.0 cos 45°) = √2(2+1)/√2 = √(22+12) × cos(45°−arctan(1/2)) — roughly 2.2 m at the diagonal.

In C-space: the x-y slice at each fixed θ shows a different “forbidden zone” shape (the piano’s footprint inflated by the Minkowski sum of the obstacle). As θ sweeps from 0 to 2π, the Cobs cross-section morphs continuously. The full Cobs is a 3D region in (x, y, θ)-space. Finding a path from (xstart, ystart, 0) to (xgoal, ygoal, 0) through Cfree corresponds exactly to finding the sequence of positions and rotations that lets the piano navigate the hallway without collision.

python
import numpy as np

def piano_corners(cx: float, cy: float, theta: float,
                    length: float = 2.0, width: float = 1.0) -> np.ndarray:
    """Four corners of the piano rectangle at configuration (cx, cy, theta).

    Returns (4, 2) array of corner positions in world frame.
    """
    # Corners in body frame (center at origin)
    half_l, half_w = length / 2, width / 2
    corners_body = np.array([
        [ half_l,  half_w],
        [ half_l, -half_w],
        [-half_l, -half_w],
        [-half_l,  half_w],
    ])  # shape (4, 2)

    # Rotation matrix for angle theta
    c, s = np.cos(theta), np.sin(theta)
    R = np.array([[c, -s], [s, c]])   # (2, 2)

    # Transform to world frame: rotate then translate
    corners_world = (R @ corners_body.T).T + np.array([cx, cy])
    return corners_world  # shape (4, 2)

def check_collision(cx, cy, theta, obstacles: list) -> bool:
    """Check if piano at (cx, cy, theta) intersects any obstacle polygon."""
    from shapely.geometry import Polygon
    piano = Polygon(piano_corners(cx, cy, theta))
    for obs in obstacles:
        if piano.intersects(Polygon(obs)):
            return True  # in C_obs
    return False  # in C_free

# Sample C-space to find C_free
hallway_walls = [
    [(-10, -10), (-10, 0.0), (10, 0.0), (10, -10)],  # bottom wall
    [(-10,  1.5), (-10, 10),  (10, 10),  (10,  1.5)],  # top wall
]

# For each (x, theta) slice, check collision
xs = np.linspace(-5, 5, 100)
thetas = np.linspace(0, 2*np.pi, 72)
c_free = np.zeros((100, 72), dtype=bool)

for i, x in enumerate(xs):
    for j, theta in enumerate(thetas):
        # y fixed at 0.75 (center of hallway)
        c_free[i, j] = not check_collision(x, 0.75, theta, hallway_walls)
# c_free[i,j] = True means config (xs[i], 0.75, thetas[j]) is collision-free
C-space dimensions = robot DOF. Count the independent numbers needed to fully specify the robot’s pose. That count is the dimension of C-space. A 2D point: 2D C-space. A 2D rigid body: 3D C-space. A 3D rigid body: 6D C-space. A 7-DOF arm: 7D C-space. This one-to-one mapping is what makes the abstraction exact, not just approximate.
A 2D rigid body (like a piano in a floor plan) can translate in x, translate in y, and rotate by angle θ. How many dimensions does its configuration space have?

Chapter 1: Topology — Why Angles Aren’t Numbers

Here is a quiet trap that breaks motion planners if you ignore it. A revolute joint can rotate through angle θ. You might represent θ as a real number, say θ ∈ [0, 2π). But wait — is θ = 0° the same configuration as θ = 360°? Yes, of course. They’re the same physical orientation. But on the number line, 0 and 2π are different points, 6.28 apart. If your planner treats θ as a number in [0, 2π), it thinks the path from θ = 0.01 to θ = 2π − 0.01 must traverse the entire interval — a 6.27-radian journey — when there’s a 0.02-radian shortcut right across the boundary.

The problem is that the number line is the wrong topological space for angles. The right space is the circle, written S1. On a circle, the two “endpoints” 0 and 2π are the same point, and the short path across that seam is perfectly valid. This is not a technicality — it changes which paths exist, which are shortest, and whether a planner finds them at all.

Topology captures what “near” means. Two spaces are topologically equivalent (homeomorphic) if one can be continuously deformed into the other without tearing or gluing. The interval (0, 1) is homeomorphic to all of R (via x → tan(π(x − ½))). But [0, 1) is NOT homeomorphic to S1: the interval has an endpoint that the circle lacks. This structural difference — not geometry, not distances — is what topology studies.

Topological spaces and open sets

A topological space is a set X together with a collection of “open sets” satisfying three axioms: the empty set and all of X are open; any union of open sets is open; any finite intersection of open sets is open. This seems abstract, but the open sets encode the neighborhood structure — they say which points are “close to” which other points, without needing a metric.

In R, the open sets are unions of open intervals (a, b). In contrast, [0, 1] is a closed set (it contains its boundary points 0 and 1). The interval [0, 1) is neither open nor closed — it contains the boundary point 0 but not 1. This distinction matters for C-space because joint limits create boundaries, and planners must handle them correctly.

Three properties of topological spaces that matter for planning

Not all topological spaces are equal for motion planning purposes. Three key properties determine what kinds of planners will work:

1. Hausdorff property: any two distinct points can be separated by disjoint open sets. Manifolds are Hausdorff. This ensures that two different robot configurations are genuinely distinct and cannot be confused by the topology. Without the Hausdorff property, a sequence of configurations could converge to two different limits simultaneously — a nightmare for any convergence-based planner.

2. Second countability: there is a countable basis for the topology (a countable collection of open sets such that every open set is a union of basis sets). This ensures that C-space is not “too large” for measure-theoretic arguments. All the manifolds in this chapter — Sn, Tn, SO(3), SE(3) — are second countable. This property underpins probabilistic completeness arguments for sampling-based planners: the space can be approximated arbitrarily well by a finite sample.

3. Paracompactness: every open cover has a locally finite refinement. This is a technical condition that allows for the existence of smooth partitions of unity, which are used to patch together local constructions (like local coordinate charts) into global ones. All manifolds are paracompact. For planning, this matters for gradient-based methods: the ability to do calculus globally on C-space requires paracompactness.

Continuity without a ruler

In topology, a function f: X → Y is continuous if the preimage of every open set in Y is open in X. This generalizes the calculus definition (“close inputs give close outputs”) to spaces where “close” is defined by open sets rather than by a metric. For robot motion planning, this means: a motion is a continuous path if small changes in time give small changes in configuration. In C-space, this is exactly the requirement that the path γ: [0,1] → Cfree is a continuous function.

The path γ must be continuous: there should be no teleportation. When γ stays within Cfree — every point on the path is collision-free — we have a valid motion plan. The fundamental planning question is: does such a γ exist, connecting qinitial to qgoal? This is the path existence problem, and its answer depends entirely on the topology of Cfree.

Four spaces every roboticist must know

SpaceNotationDescriptionExample DOF it models
Real lineRUnbounded, 1D, no boundaryPrismatic joint (infinite range)
Bounded interval[a, b]Bounded, 1D, has two boundary pointsPrismatic joint with hard limits
CircleS1Compact, 1D, no boundary, wraps aroundRevolute joint (full rotation)
2-SphereS2Compact, 2D, no boundarySpherical joint (2D direction)

The crucial fact about S1: it is compact (bounded and closed) but has no boundary. You can walk along a circle forever without reaching an edge. A number line has neither property. A closed interval [a, b] is compact but DOES have two boundary points. S1 is the unique 1-manifold that is compact and boundary-free.

Compact vs non-compact: what it means for paths

A space is compact if every open cover has a finite subcover — the topological generalization of “closed and bounded.” For motion planning, compactness has two practical consequences. First, compact C-spaces have a finite diameter: there is a maximum possible path length between any two configurations. This means planners can reason about worst-case exploration distances. Second, on a non-compact space like Rn, a path can escape to infinity; on a compact space like Tn or Sn, it cannot.

S1 is compact (it is closed and bounded as a subset of R2). R is not compact (it is unbounded). T2 is compact (product of compact spaces is compact). SE(2) = R2×S1 is not compact (the R2 factor is unbounded). In practice, we always restrict to a bounded workspace, making the effective C-space compact — but the underlying topology determines whether the boundaries are “hard walls” (joint limits, manifolds-with-boundary) or “wrap-arounds” (freely rotating joints, compact manifolds).

The distance problem: metrics on curved spaces

On a manifold, we need a distance function to measure how far apart two configurations are. On Rn, Euclidean distance works. On S1, the correct distance between angles θ1 and θ2 is the length of the shorter arc:

dS11, θ2) = min( |θ1 − θ2|, 2π − |θ1 − θ2| )

This is the geodesic distance on S1: the length of the shortest path between the two points staying on the circle. For T2 = S1×S1, the natural distance is the product metric: d((θ1, φ1), (θ2, φ2)) = √(dS11, θ2)2 + dS11, φ2)2). For planners like RRT that need nearest-neighbor queries, using the right distance function is critical for finding nearby configurations across identification boundaries.

R vs S1: Where Do Angles Live?

Drag the slider. On R (top), the point hits a wall at the endpoints. On S1 (bottom), the point wraps around — 0° and 360° are the same place. The red dot shows both representations of the same angle.

θ 30°

Homeomorphism: when two spaces are “the same”

A homeomorphism is a continuous bijection with a continuous inverse. If such a map exists between X and Y, we say X and Y are homeomorphic (written X ≅ Y) — they are topologically identical. The open interval (0, 1) ≅ R via f(x) = tan(π(x − ½)). The circle S1 ≅ any closed curve without self-intersections. The classic punchline: a donut is homeomorphic to a coffee cup — both have exactly one hole. You can continuously deform one into the other.

For C-space, homeomorphism tells us when two representations are equivalent. If we parameterize a revolute joint as [0, 2π) versus as S1, these are NOT homeomorphic — [0, 2π) has a boundary point (the gap at θ = 0/2π), while S1 does not. A planner using [0, 2π) will refuse to plan across that gap. A planner using S1 handles it naturally.

Manifolds: the smooth spaces robots live in

The spaces we care about in robotics are all manifolds: topological spaces that locally look like Rn. Zoom in close enough to any point on S1, and it looks like a line segment. Zoom in on any point on S2, and it looks like a flat patch of R2. The global topology can be complicated (sphere, torus, projective space), but locally everything is flat. This local flatness is what lets us do calculus on manifolds — and it is why motion planners can work with infinitesimally small steps even in curved C-spaces.

A manifold has a dimension equal to the dimension of the local Euclidean neighborhood. S1 is a 1-manifold (locally like R). S2 is a 2-manifold (locally like R2). SE(3) is a 6-manifold. A key fact: a manifold cannot have boundaries unless we explicitly declare them. S1 and T2 are closed manifolds (compact, no boundary). [0, 1] is a manifold with boundary. Revolute joints with mechanical stops are modeled as [a, b] (manifold with boundary); unconstrained revolute joints are S1 (closed manifold).

Practical test: to determine the correct topology for a C-space factor, ask: “If I reach the ‘maximum’ value of this DOF, does the robot physically wrap around to the ‘minimum’ value?” If yes, use S1 (or the appropriate sphere). If no — the joint hits a hard stop — use a closed interval [a, b]. If the range is unlimited and unbounded, use R.
Verified fact: S1 is NOT homeomorphic to [0, 1]. The topological invariant that distinguishes them is connectivity after removing a point. Remove any point from S1: the remainder is still connected (an open arc). Remove an interior point from [0, 1]: the remainder splits into two disconnected pieces. This “invariant under homeomorphism” technique is the main tool for proving two spaces are NOT homeomorphic.

Connectedness: can the robot get anywhere?

A topological space X is connected if it cannot be partitioned into two disjoint, non-empty open sets. Intuitively: you can travel between any two points. Cfree may or may not be connected. If Cfree is disconnected, there is no continuous path from qinitial to qgoal if they are in different connected components — the planning problem is infeasible.

A stronger notion is path-connectedness: X is path-connected if for any two points p, q ∈ X, there exists a continuous path γ: [0,1] → X with γ(0)=p and γ(1)=q. For “nice” spaces (manifolds, which all our C-spaces are), connectedness and path-connectedness are equivalent. So for robotics, we can use them interchangeably.

A planner like BFS or RRT implicitly tests path-connectedness: if the planner fails to find a path after exhaustive search, it is reporting that qinitial and qgoal are in different connected components of Cfree. Proving that no path exists (rather than merely failing to find one) requires completeness guarantees — a major theme in Chapter 5.

Connected Cfree: Any configuration is reachable from any other. Planning may still be hard (long path, complex obstacles), but a path exists. Example: a 2D point robot in a room with a central pillar — Cfree is connected (go around the pillar).
Disconnected Cfree: Some configurations are unreachable from others. No planner can find a path (because none exists). Example: a 2D point robot sealed inside a closed room with no door — Cfree inside the room is disconnected from Cfree outside.

Tangent spaces and velocities on manifolds

At any point q on a smooth manifold, the tangent space TqM is the set of all possible velocity vectors at q — the directions in which a path through q can be moving at that instant. For Rn, the tangent space at every point is just Rn itself (all directions are possible). For S1, the tangent space at each point is a 1D line tangent to the circle at that point.

Why do tangent spaces matter for planning? A path through C-space is a smooth map γ: [0,1] → C. Its derivative ˙γ(t) is a tangent vector in Tγ(t)C. For a robot with nonholonomic constraints, only certain tangent directions are admissible — the robot cannot move in arbitrary directions in C-space. Planning must stay within the admissible tangent cone at each point. For holonomic systems, all tangent directions are admissible, and any smooth path through Cfree is a valid plan.

For SE(2), the tangent space at any configuration (x, y, θ) is 3-dimensional: instantaneous rates of change (˙x, ˙y, ˙θ). For a differential-drive robot, the nonholonomic constraint says ˙x sin θ − ˙y cos θ = 0 — this restricts the admissible tangent vectors to a 2-dimensional subspace at each configuration (forward/backward and turning). The admissible subspace varies continuously as θ changes, creating the curved structure of nonholonomic motion.

Is the interval [0, 2π) the correct topological representation for a revolute joint angle?

Chapter 2: Building Manifolds by Identification

How do we construct spaces like S1? One elegant method is identification (also called “gluing”). Start with a simple, familiar space — like an interval or a square — and declare that certain points are “the same.” The resulting quotient space can have a completely different topology, even though it was built from something simple.

Example: take the unit interval [0, 1] and declare 0 ∼ 1 (zero and one are identified). Geometrically, you’re bending the interval into a loop and gluing the endpoints together. The result is S1. The identification creates the “wrap-around” topology that makes S1 different from [0, 1].

The unit square gives a whole zoo of manifolds. LaValle’s Figure 4.5 is one of the most beautiful illustrations in the book: starting from the same square (0,1)×(0,1), different identification patterns on the boundary give radically different topological spaces — a cylinder, a torus, a Möbius band, even a Klein bottle.

The square manifold zoo

IdentificationResultOrientable?C-space role
NoneR2 (plane)Yes2D translation
Left ≅ right: (0,y)∼(1,y)Cylinder R×S1YesOne translation + one rotation
Left ≅ right, flipped: (0,y)∼(1,1−y)Möbius bandNo— (non-orientable, rare in practice)
Both pairs: (0,y)∼(1,y) AND (x,0)∼(x,1)Torus T2 = S1×S1Yes2-link revolute arm!
Both pairs, one flipKlein bottleNo
Both pairs, both flippedRP2 (real projective plane)NoRelated to SO(3)

The Asteroids game: living on a torus

LaValle uses a wonderful analogy from his actual text: the flat torus T2 looks exactly like the universe in the classic Asteroids arcade game. When your spaceship flies off the right edge of the screen, it reappears on the left. When it flies off the top, it reappears at the bottom. The screen is a square with opposite edges identified — which is exactly the definition of T2 = S1×S1.

A 2-link robot arm, where each joint can rotate fully 360°, has two angles θ1 and θ2. Each angle lives on S1. The pair (θ1, θ2) lives on T2 = S1×S1. This is NOT just R2. If you plan on R2 instead of T2, you will miss paths that cross the θ = 0/2π boundary for either joint — topologically valid shortcuts that are physically equivalent to small rotations.

T2 ≠ R2. Both are 2-dimensional. But on R2, you can always shrink a loop to a point. On T2, a loop that winds around the torus once in the θ1 direction cannot be shrunk — it would have to cross the identified boundary. This difference in loop structure is captured by the fundamental group, which is the topic of the next chapter.

The flat torus vs the embedded torus: a crucial distinction

There are two ways to think about T2. The embedded torus is the donut surface in R3: {(x,y,z) : (√(x2+y2)−R)2 + z2 = r2}. The flat torus is the square [0,1]×[0,1] with opposite edges identified. These are homeomorphic (same topology), but they are NOT isometric (they have different metrics).

The flat torus is what we actually want for C-space. On the flat torus, distances are computed with the product metric on S1×S1: d((θ11),(θ22)) = √(dcirc12)2 + dcirc12)2). On the embedded torus, the inner circle (closer to the central axis) has a shorter circumference than the outer circle — the metric is not uniform. Using the embedded torus metric would incorrectly penalize configurations where the robot is near certain joint angles just because of how the torus happens to sit in 3D space, which has no physical meaning.

C-space geometry ≠ C-space topology. Two configurations can be topologically close (nearby on the flat torus) but metrically far in any embedding in Rn. The physically meaningful metric is the one that measures the actual joint angle differences — the product metric on Tn. Planners should always use the intrinsic C-space metric, not any metric inherited from an ambient embedding.
Square Identifications: Building Manifolds

Each mode shows a different identification on the unit square. A path that crosses an identified edge (shown in color) reappears on the opposite edge. Toggle modes to see how gluing creates different topologies.

No identification — flat plane R²

Why this matters for real planners

If you implement a path planner for a 2-link arm and you store the configuration as two floating-point angles in [0, 2π), then when θ1 is at 0.05 rad and the planner wants to decrease it to 2π − 0.05 rad (a tiny clockwise step), the planner sees a jump of nearly 2π — a giant step in its distance function. The correct representation stores (θ1, θ2) on T2 with a distance metric that knows the boundary is identified. The distance between 0.05 and 2π − 0.05 is 0.1 rad, not 6.18 rad.

Product spaces: building higher-dimensional C-spaces from pieces

Most robot C-spaces are product manifolds: you take the C-space factors for each independent DOF and multiply them together. The product X×Y is just the set of all pairs (x, y) with x ∈ X and y ∈ Y, with the product topology (open sets are products of open sets). This is exactly how R3 = R×R×R works, but now applied to non-Euclidean factors.

A 3-link arm where link 1 can fully rotate (S1), link 2 has a joint limit (interval [−π/2, π/2]), and link 3 can fully rotate (S1) has C-space S1×[−π/2, π/2]×S1. This is a 3-manifold with boundary (the middle factor has endpoints). Path planners on this space must handle the boundary correctly: configurations at θ2 = ±π/2 are physically reachable but the joint cannot move further.

The Asteroids interpretation again. On T2, a path for a 2-link arm that crosses the θ1 = 0/2π seam is physically valid — joint 1 completes a full revolution. On T2, this path is as short as any other. On R2 (the wrong space), the planner treats the seam as unreachable and refuses to plan through it. The Asteroids analogy: the spaceship should fly off the right edge and reappear on the left. A broken game engine that treats the screen edges as solid walls would refuse to let the ship move there. Many motion planners have exactly this bug.

Worked example: the 2-link arm on T2

Let’s work through a concrete planning scenario to feel the difference. A 2-link arm is at configuration qI = (0.1, 0.1) (both joints near 0 radians). The goal is qG = (6.1, 6.1) (both joints near 2π radians — barely past a full revolution, physically almost identical to qI).

On R2: the Euclidean distance from qI to qG is √((6.0)2 + (6.0)2) ≈ 8.49. The planner plans a long sweeping path across most of the configuration space, rotating both joints nearly 360°.

On T2: the toroidal distance is computed as min(|Δθ1|, 2π−|Δθ1|) for each joint separately. For both joints, |Δθ| = 6.0, and 2π−6.0 ≈ 0.28. So the toroidal distance is √((0.28)2 + (0.28)2) ≈ 0.40. The planner finds the short path that crosses the seam — a tiny movement of about 0.28 radians per joint — instead of the 360° detour.

The ratio is 8.49 / 0.40 ≈ 21x. A planner using the wrong topology finds a path 21 times longer than necessary in this case. In surgical robot applications with cable-driven joints, this difference directly translates to excess cable wear, slower movements, and potentially reaching joint limits unnecessarily.

CW complexes: building spaces from cells

Another way to build topological spaces is as CW complexes: start with a set of 0-cells (points), attach 1-cells (line segments, glued at endpoints), attach 2-cells (disks, glued at boundaries), and so on. S1 as a CW complex: one 0-cell (a point) and one 1-cell (an arc, with both endpoints identified to the 0-cell). T2 as a CW complex: one 0-cell, two 1-cells (the two circle factors, one in each direction), one 2-cell (the square, with boundary identified to the 1-skeleton).

This cell decomposition is useful for planning because it provides a finite combinatorial structure that approximates the continuous manifold. Cell decomposition planners (Chapter 6 of LaValle) work by decomposing Cfree into cells and building a roadmap on the cell adjacency graph. The topology of the cells and how they fit together determines whether the planner correctly handles the manifold structure.

The boundary of a manifold-with-boundary

Joint limits impose boundaries on C-space. A revolute joint limited to [−π, π] has C-space [−π, π] — a manifold-with-boundary. The boundary consists of the two endpoints {−π, π}. At a boundary configuration, the joint cannot move further in the constrained direction: the path must stay in the interior or return from the boundary.

Planners that sample C-space must handle boundaries. A configuration sampled uniformly from [a, b]×S1 might land exactly at x = a or x = b — a valid configuration, but where one direction of motion is blocked. The planner must not try to extend the tree “through” the boundary. In practice, sampled configurations are checked against joint limits, and steering functions clamp to the boundary when they would overshoot.

Higher identifications: the Klein bottle and RP2

For completeness, let’s briefly visit the non-orientable identifications from the table. The Klein bottle is constructed from a square by identifying the left-right edges normally ((0,y)∼(1,y)) but identifying the top-bottom edges with a flip ((x,0)∼(1−x,1)). The result is a closed, non-orientable surface that cannot be embedded in R3 without self-intersection. It has no C-space role in standard robotics but appears in topology textbooks as a canonical example.

The real projective plane RP2 identifies both pairs of opposite edges with flips. It is also non-orientable, also cannot be embedded in R3, and π1(RP2) = Z2. Its 3D analog RP3 = S3/∼ (antipodal identification on S3) IS the topology of SO(3), as we discuss in Chapter 5. Non-orientable spaces are rare in robot C-spaces (all the common ones — SE(2), SE(3), Tn — are orientable), but the identification technique that generates them is the same one that generates all the spaces we do use.

Implementing toroidal distance correctly in code

Here is a common implementation mistake and its fix. In an RRT for a 2-link arm, we need to find the nearest existing node to a randomly sampled configuration. If we use Euclidean distance on raw angles, we get wrong nearest-neighbors near the wrap-around boundary.

python
import numpy as np

# WRONG: Euclidean distance ignores wrap-around
def bad_distance(q1: np.ndarray, q2: np.ndarray) -> float:
    return np.linalg.norm(q1 - q2)  # ||(θ₁,θ₂) - (φ₁,φ₂)||₂
    # BUG: distance(0.1, 6.1) = 6.0, should be ~0.28

# CORRECT: Toroidal distance (product metric on T²)
def circle_dist(a: float, b: float) -> float:
    """Geodesic distance between two angles on S¹."""
    diff = abs(a - b) % (2 * np.pi)
    return min(diff, 2 * np.pi - diff)

def torus_distance(q1: np.ndarray, q2: np.ndarray) -> float:
    """Correct geodesic distance on T^k for k revolute joints."""
    return np.sqrt(sum(circle_dist(q1[i], q2[i])**2
                       for i in range(len(q1))))

# Example: q1=(0.1, 0.1), q2=(6.1, 6.1) (both just past 0, physically close)
q1 = np.array([0.1, 0.1])
q2 = np.array([6.1, 6.1])

d_bad    = bad_distance(q1, q2)    # ≈ 8.49 — wildly wrong
d_correct = torus_distance(q1, q2)  # ≈ 0.40 — correct
# 21x difference in nearest-neighbor priority for RRT!
A 2-link robot arm has two revolute joints, each freely rotating 360°. What manifold is its configuration space?

Chapter 3: Fundamental Group — Counting Holes

Two spaces can look similar — both are 2D, both are smooth — but have fundamentally different loop structures. A disk has no holes; a washer has one. You can shrink any loop on a disk to a point. On a washer, a loop that goes around the hole cannot be shrunk without cutting it. How do we make this precise? The answer is the fundamental group π1(X).

Fix a basepoint p ∈ X. A loop based at p is a continuous map γ: [0,1] → X with γ(0) = γ(1) = p. Two loops are homotopic if one can be continuously deformed into the other while keeping the basepoint fixed. Homotopy is an equivalence relation — it partitions the set of all loops into classes. The fundamental group π1(X) is the set of these homotopy classes, with group operation “traverse one loop then the other.”

Homotopy class = topological type of a loop. Two loops are in the same class if and only if one can be shrunk/stretched into the other without lifting off the space. The fundamental group counts how many genuinely different loop types exist. Its identity element is the constant loop (just sit at p). A non-trivial element is a loop that can’t be shrunk to a point.

The formal definition of π1

Fix a basepoint p ∈ X. The group operation on homotopy classes [γ1] and [γ2] is concatenation: first traverse γ1 at double speed, then γ2 at double speed — both completing in the unit time interval [0,1]. The inverse of [γ] is the reverse path γ−1(t) = γ(1−t). The identity is the constant path at p.

Two key facts: (1) π1 is well-defined (the group axioms hold up to homotopy), and (2) for path-connected spaces, changing the basepoint gives an isomorphic group (so the choice of p doesn’t matter for the algebraic structure, only for concrete element labeling). For all the spaces we care about, we can pick any convenient basepoint.

Computing π1 for the spaces we care about

R2 (or any Rn): Simply connected. Every loop can be continuously shrunk to the basepoint through the interior of Rn. π1(Rn) = {e} — the trivial group with one element.

S1 (the circle): A loop that winds once around S1 cannot be shrunk — there is no 2D interior to move through. Winding once counterclockwise and winding once clockwise are different classes. Winding twice counterclockwise is yet another class. In fact, each integer winding number gives a distinct homotopy class, and combining two loops adds their winding numbers. π1(S1) = Z (the integers under addition).

T2 = S1×S1 (the torus): Two independent circles, two independent winding numbers. π1(T2) = Z×Z = Z2. A loop that winds 3 times in the θ1 direction and −2 times in the θ2 direction belongs to the class (3, −2) ∈ Z2.

RP2 (real projective plane, relevant for SO(3)): Strange behavior. Winding twice is homotopic to winding zero times! π1(RP2) = Z2 = {0, 1} (the integers mod 2). The same phenomenon occurs in RP3, which is homeomorphic to SO(3).

Spaceπ1Interpretation
Rn{e} (trivial)No holes, every loop contractible
S1ZWinding number — infinitely many loop classes
S2{e}Simply connected (no 1D holes in the 2-sphere)
T2Z2Two independent winding numbers
RP2Z2Double cover: winding twice = trivial
SO(3) ≅ RP3Z2720° rotation contractible; 360° is not

Why π1 matters for planning

If π1(Cfree) is non-trivial, there are multiple homotopy classes of paths between two configurations. A planner that finds one path may have found the unique class — or it may have found the expensive class while missing a shorter one. On a cylinder R×S1 with an obstacle spanning the cylinder, the only path from qI to qG may go “over the seam” — crossing the S1 identification boundary. A planner that treats the S1 direction as a number line will declare the path impossible.

The plate trick (also called the Dirac belt trick) is the physical manifestation of π1(SO(3)) = Z2. Hold a plate flat on your palm and rotate it 360° around a vertical axis: your arm is twisted. Continue to 720°: your arm untwists to its original position. A 360° rotation of a rigid body is not contractible to no rotation, but a 720° rotation IS. This is a topological fact about RP3, and it has real consequences for cable-routing and tether management in robot systems.

Simply connected spaces: when π1 = trivial simplifies planning

When Cfree is simply connected1(Cfree) = {e}), every two paths with the same endpoints are homotopic. This means there is essentially one homotopy class of paths between any two configurations. A planner can find “the” path without worrying about missing a topologically distinct alternative. This is the common case for many practical robot systems: a robot arm in free space with no obstacles looping around any joint has simply connected Cfree.

But simply connected does NOT mean easy to plan in. A simply connected space can have very narrow passages, very high dimension, very complex geometry. It just means the topology is not adding extra difficulty beyond the geometry. The planning algorithms of Chapters 5 and 6 are designed to work in both simply connected and non-simply-connected Cfree; the topology affects whether they are complete and what guarantees they can provide.

Homotopy classes and planners

If Cfree has a non-trivial fundamental group, there are multiple homotopy classes of paths from qI to qG. This means: there are qualitatively different routes that cannot be continuously deformed into each other. One route might go “above” an obstacle and one “below,” where the obstacle is a hole in Cfree.

Standard planners (RRT, PRM) are single-homotopy-class planners. They find one path and stop. If the first path they find is in the expensive homotopy class, they miss the shorter path in a different class. Homotopy-aware planners — an active research area — explicitly track which class each candidate path belongs to, ensuring they explore all classes before declaring optimality.

LaValle’s Figure 4.8 gives the canonical example: a cylinder R×S1 with a wall-like obstacle that extends all the way across the S1 direction at a fixed height. The only path from bottom to top must cross the S1 identification boundary. π1 = Z tells us there are infinitely many homotopy classes — each number of times the path wraps around S1 gives a different class. The planner must understand this topology, or it will miss the path.

Topological planning is an open problem. For most practical robot systems, planners treat Cfree as if it were simply connected — ignoring homotopy classes beyond the first found. This works often enough in practice (many C-spaces are simply connected or the obstacles don’t create topological traps). But for cable-driven robots, tethered UAVs, surgical robots with tangling constraints, and winding through complex environments, homotopy-aware planning is essential and remains an active research frontier.

Higher homotopy groups: π2, π3

The fundamental group π1 counts distinct classes of loops. Higher homotopy groups count distinct classes of higher-dimensional maps. π2(X) counts equivalence classes of maps S2 → X (2-sphere mappings). For S2 itself: π2(S2) = Z, meaning 2-sphere maps into S2 have a winding number, just as loop maps into S1 do.

For robotics: π2(SO(3)) = 0 (trivial), but π3(SO(3)) = Z. This means that while all sphere-shaped configurations are contractible in SO(3), there are non-trivial 3-sphere families of rotations. This appears in the context of Hopf fibrations and has connections to spin representations in quantum mechanics — fascinating mathematics, but beyond the needs of most motion planners.

The key practical lesson: for the motion planning problems in LaValle’s book, we need π1 (loop structure, path connectivity) and the connectedness of Cfree. Higher homotopy groups appear in more advanced topics like topological complexity of motion planning (Farber’s TC invariant) and configuration spaces of many-robot systems.

Van Kampen’s theorem: computing π1 of compound spaces

For product spaces X×Y, the fundamental group is the product: π1(X×Y) = π1(X) × π1(Y). This is why π1(T2) = π1(S1×S1) = Z×Z. For more complex constructions — spaces formed by gluing two spaces along a common subspace — the Seifert-van Kampen theorem gives a way to compute π1 from the pieces.

The practical upshot: once you know the C-space decomposition into product factors, you can compute π1(C) by multiplying the π1s of each factor. A k-link arm with all revolute joints has C-space Tk = (S1)k, and π1(Tk) = Zk. The planner needs to be aware of k independent “winding number” choices for the path — 2k qualitatively distinct path classes to potentially explore.

RobotC-spaceπ1Distinct path classes between any qI, qG
Point in RnRn{e}1 (unique homotopy class)
1-link revolute armS1Z∞ (one per winding number)
2-link revolute armT2Z2∞ (pairs of winding numbers)
3D rigid bodySE(3)Z22 (short and long rotation path)
6-DOF armT6Z6∞ (6 independent winding numbers)
Loops on S1 — Winding Numbers

Each loop winds some integer number of times around the circle. Try to shrink winding-1 to winding-0 — you can’t without crossing the origin (which is not in the space). The winding number is a topological invariant: it doesn’t change under continuous deformations.

Wind 1
π1(S1) class: 1 ∈ Z
SO(3) has fundamental group Z2. A robot arm rotates exactly 360° and returns to its starting orientation. A cable is attached to the arm. Can the cable be untangled by continuing to rotate (rather than reversing)?

Chapter 4: C-Space for 2D Bodies — SE(2)

Time to get concrete. What exactly is the C-space for a rigid body that can move freely in a 2D plane? We need to build the mathematical structure, not just name it. The answer comes from understanding rigid motions as a matrix Lie group.

A rigid motion in 2D preserves distances and orientations. It consists of a rotation and a translation. The rotation part is a 2×2 matrix R(θ); the translation part is a 2D vector (x, y). LaValle derives the structure bottom-up, starting from the most general invertible matrices.

Deriving SO(2) from first principles

Start with GL(2): the set of all 2×2 invertible real matrices. This has 4 free entries and forms a 4-dimensional manifold (a Lie group). That’s too general — GL(2) includes stretching, shearing, and reflection, not just rotation.

Impose the constraint AAT = I (orthogonality: columns are orthonormal). For A = [[a, b], [c, d]], this gives three scalar equations: a2+b2=1 (first column unit), c2+d2=1 (second column unit), ac+bd=0 (columns orthogonal). Three constraints on four free parameters — by the implicit function theorem, the solution set is generically a 4−3=1 dimensional manifold. The result is O(2): the orthogonal group.

O(2) has two connected components, separated by the determinant. When det(A) = +1, the matrix is a pure rotation. When det(A) = −1, the matrix is a reflection. These two sets are topologically disconnected: you cannot continuously deform a rotation into a reflection while staying in O(2). Impose det(A) = 1 to isolate the rotations: the result is SO(2), the special orthogonal group.

The explicit parameterization: for any θ, the matrix R(θ) = [[cos θ, −sin θ], [sin θ, cos θ]] satisfies R(θ)R(θ)T = I and det(R(θ)) = cos2θ + sin2θ = 1. Conversely, any element of SO(2) has this form for some θ. The map θ → R(θ) is a bijection S1 → SO(2) that is continuous and has a continuous inverse — a homeomorphism. We have proven SO(2) ≅ S1.

SO(2) ≅ S1. Every element of SO(2) is a rotation matrix R(θ) = [[cos θ, −sin θ], [sin θ, cos θ]]. The parameter θ lives on a circle. Composing two rotations by θ and φ gives rotation by θ + φ — addition on S1. This is exactly why the correct C-space for a revolute joint is S1, not [0, 2π). The group multiplication IS addition modulo 2π.

SE(2): combining rotation and translation

A 2D rigid body can both rotate (by θ ∈ S1) and translate (by (x,y) ∈ R2). The Special Euclidean group SE(2) combines these: C = R2 × S1. Its elements are represented as 3×3 homogeneous transformation matrices:

T =
cos θ−sin θx
sin θcos θy
001

Why homogeneous coordinates? Because composing rigid motions becomes matrix multiplication: T1 · T2 applies T2 first, then T1. A point p = (px, py)T in the robot’s body frame transforms to T · (px, py, 1)T in the world frame. This is clean, composable, and directly implementable.

SE(2) is a 3D manifold

SE(2) has dimension 3: two translation DOF (x, y ∈ R) and one rotation DOF (θ ∈ S1). The topology is R2×S1: a cylinder (R×S1) extended into a second translational direction. It is not compact — translations can go to infinity. Its fundamental group is π1(R2×S1) = π1(S1) = Z, because π1(R2) is trivial.

Worked example: composing SE(2) transforms

Let’s trace through a concrete SE(2) computation. A robot starts at the origin facing right: T1 = se2(0, 0, 0). It drives forward 3 meters (in the +x direction of its body frame): this is a translation of (3, 0) in the robot’s body frame. Then it turns left 90° (counterclockwise). Then it drives forward 2 meters again.

Step 1: Tforward3 = se2(3, 0, 0). After applying this to T1: Tafter_forward = T1 · Tforward3 = se2(3, 0, 0). Robot is now at (3, 0) facing right.

Step 2: Tturn_left = se2(0, 0, π/2). After turning: Tafter_turn = Tafter_forward · Tturn_left = se2(3, 0, π/2). Robot is at (3, 0) facing up.

Step 3: Tforward2 = se2(2, 0, 0) (2 meters forward in current body frame direction). Apply in robot’s current frame: Tfinal = Tafter_turn · Tforward2. Since the robot faces π/2 (up), driving “forward” in body frame (+x body direction) moves it in the world’s +y direction: Tfinal = se2(3, 2, π/2). Robot ends at (3, 2) facing up.

The key: we always compose transforms as Tworld→new_pose = Tworld→old_pose · Told_pose→new_pose. The relative transform is always expressed in the robot’s current frame. This left-multiplication by the old pose correctly handles the rotation, which is why the 2m forward step went to (3,2) and not (5,0).

SO(2) as a complex number: the cleanest representation

There is an even cleaner way to think about SO(2). Instead of a 2×2 matrix with the constraint AAT = I, det(A) = 1, we can use a unit complex number: z = e = cos θ + i sin θ, with |z| = 1. Multiplication of two unit complex numbers corresponds to adding their angles: e · e = ei(α+β). This is rotation composition. The constraint |z| = 1 is one scalar equation on two reals — confining z to S1, which is exactly SO(2).

The same idea extends to 3D: SO(3) uses unit quaternions (unit elements of the 4D quaternion algebra), which we explore in Chapter 5. And in both cases, the algebraic structure (complex numbers, quaternions) exactly captures the rotation group structure, making these parameterizations natural rather than arbitrary.

What makes SE(2) a Lie group

SE(2) is not just a manifold — it is a Lie group: a group that is also a smooth manifold, where the group operations (multiplication and inversion) are smooth maps. The Lie group structure means SE(2) has a natural notion of “tangent space at the identity,” called the Lie algebra se(2). For motion planning, this matters because it gives us a principled way to do interpolation (moving from one configuration to another along a geodesic) and to define velocities (elements of the Lie algebra are instantaneous velocities).

The Lie algebra se(2) consists of 3×3 matrices of the form [[0, −ω, vx], [ω, 0, vy], [0, 0, 0]], where ω is the angular velocity and (vx, vy) is the linear velocity. The exponential map exp: se(2) → SE(2) converts instantaneous velocities into finite transformations. This is the foundation for screw theory and exponential coordinates, which appear throughout modern robot kinematics.

SE(2) and differential drive robots

A differential-drive robot (two powered wheels on a common axle) has three configuration variables (x, y, θ) but only two independently controllable motors — it cannot move sideways. This means the robot’s instantaneous velocity must satisfy the nonholonomic constraint: the velocity must be tangent to the direction the robot faces. Formally, ˙x sin θ − ˙y cos θ = 0 (no lateral motion).

This constraint limits which paths through SE(2) the robot can actually follow. Not all paths through Cfree are physically realizable — only those whose tangent vectors satisfy the constraint at every point. This is the key distinction between holonomic systems (any path through Cfree is realizable) and nonholonomic systems (only constraint-satisfying paths are realizable). The full C-space is still SE(2), but the reachable paths form a subset constrained by the robot’s dynamics.

Nonholonomic planning is harder. For a holonomic robot (like a robot arm), any continuous path through Cfree is a valid motion plan. For a nonholonomic robot (car, differential drive, unicycle), the path must also satisfy kinematic constraints everywhere — no sliding sideways. This rules out most paths in Cfree and requires planners specifically designed for nonholonomic constraints: RRT with kinodynamic steering, lattice planners with feasible primitives, or trajectory optimization. LaValle covers nonholonomic planning extensively in Chapters 13–15.

The inverse and interpolation in SE(2)

For group-aware planners, we need the inverse of a SE(2) transform. If T = se2(x, y, θ) is the pose of the robot in the world, then T−1 is the pose of the world in the robot’s frame. The formula:

T−1 = se2(−x cos θ − y sin θ,   x sin θ − y cos θ,   −θ)

In matrix form, since T = [[R, t], [0, 1]], we have T−1 = [[RT, −RTt], [0, 1]]. For SE(2), RT = R(−θ) = [[cos θ, sin θ], [−sin θ, cos θ]], and −RTt = (−x cos θ − y sin θ, x sin θ − y cos θ).

For interpolating between two SE(2) configurations q0 = (x0, y0, θ0) and q1 = (x1, y1, θ1), the natural interpolation is linear for translation and shortest-arc for rotation:

q(t) = (1−t) · x0 + t · x1,   (1−t) · y0 + t · y1,   θ0 + t · dS10, θ1)

where the third component uses the signed shortest arc from θ0 to θ1 (positive if counterclockwise, negative if clockwise). This gives a straight-line interpolation in (x,y) space with simultaneous shortest-arc rotation — the natural “straight line” in SE(2) for a holonomic robot.

python
import numpy as np

def se2_matrix(x: float, y: float, theta: float) -> np.ndarray:
    """SE(2) homogeneous transformation matrix.

    Args:
        x, y: translation (meters)
        theta: rotation (radians)
    Returns:
        3x3 homogeneous matrix T in SE(2)
    """
    c, s = np.cos(theta), np.sin(theta)
    return np.array([
        [c, -s, x],
        [s,  c, y],
        [0,  0, 1]
    ], dtype=np.float64)

def compose_se2(T1: np.ndarray, T2: np.ndarray) -> np.ndarray:
    """Compose two SE(2) transforms: apply T2 first, then T1."""
    return T1 @ T2  # 3x3 matrix multiplication

def apply_se2(T: np.ndarray, point: np.ndarray) -> np.ndarray:
    """Apply SE(2) transform to a 2D point."""
    p_hom = np.array([point[0], point[1], 1.0])
    result = T @ p_hom
    return result[:2]  # drop homogeneous coordinate

# Example: robot at (3, 2) rotated 45 degrees
T = se2_matrix(x=3.0, y=2.0, theta=np.pi/4)

# A point at (1, 0) in robot frame
p_body = np.array([1.0, 0.0])
p_world = apply_se2(T, p_body)
# p_world ≈ [3.707, 2.707] — 45° rotation + translation

# Composition: first move 1m in x, then rotate 90°
T1 = se2_matrix(0, 0, np.pi/2)   # 90° rotation
T2 = se2_matrix(1, 0, 0)         # 1m translation in x
T_composed = compose_se2(T1, T2)   # rotate AFTER translate
# Result: translation 1m in x, THEN 90° — origin at (0,1) facing left
SE(2) Explorer: Rigid Body in 2D C-Space

Left: a triangular robot in 2D workspace. Right: its position as a point in SE(2) C-space (x,y plane; color encodes θ). Adjust the sliders to move and rotate the robot.

x 1.0
y 0.0
θ
T = SE(2) matrix computed above
O(2) is the group of 2×2 matrices with AAT = I. It has two connected components. What distinguishes them?

Chapter 5: SE(3) and Quaternions

A rigid body in 3D space has 6 DOF: three translations (x, y, z ∈ R3) and three rotations. The C-space is SE(3) = R3×SO(3). Translations are easy — R3 is R3. The challenge is SO(3): the group of 3D rotations.

The naive approach — three Euler angles (roll, pitch, yaw) — seems like it should work. Three angles, three DOF, done. But it fails in the most inconvenient way possible, exactly at the worst configurations. Understanding why forces us to find a better parameterization: quaternions.

First: deriving SO(3) by the same dimensional argument

We can run the same derivation as SO(2), but now in 3D. Start with GL(3): all 3×3 invertible matrices, a 9-dimensional manifold. Impose AAT = I: six constraints (three unit-column constraints, three orthogonality constraints — the upper triangle of AAT = I is 6 equations). This leaves a 9−6 = 3-dimensional manifold: O(3). Impose det(A) = 1: one more constraint cutting O(3) in half, giving SO(3), a 3-dimensional manifold.

The three dimensions of SO(3) correspond to three independent ways to rotate a rigid body in 3D (around each of three axes, for example). What is the topology of SO(3)? The key claim from LaValle Section 4.2.2: SO(3) ≅ RP3. Every unit quaternion h ∈ S3 maps to a rotation, and h and −h give the same rotation, so SO(3) = S3 / (h ∼ −h) = RP3. This identification is what makes SO(3) topologically non-trivial — and it is the source of gimbal lock.

The Euler angle failure: gimbal lock

Represent a 3D rotation as three successive rotations: yaw (around Z), then pitch (around Y), then roll (around X). This works generically, but at pitch = ±90°, the roll and yaw axes align. You lose one degree of freedom. No matter how you change roll or yaw independently, they both produce the same effect — rotation around the same axis. This is gimbal lock.

Gimbal lock is not just a numerical issue — it’s a topological fact. You cannot globally parameterize SO(3) with three angles without singularities. This follows from the Hairy Ball Theorem (you cannot comb a 3-sphere flat). Any chart on SO(3) using three real-valued angles must have at least one singular point. Euler angles have infinitely many (the entire set of ±90° pitch configurations).

Gimbal lock killed astronauts. The Apollo 11 guidance computer used Euler angles. Engineers had to actively maneuver the spacecraft to avoid gimbal lock during critical burn phases. It was a known bug they worked around operationally. Quaternions solve this permanently — they are singularity-free.

Why we can't avoid the topology of SO(3)

The topological obstruction to gimbal-lock-free Euler angles is fundamental, not a matter of choosing a better angle convention. The Hairy Ball Theorem states that there is no continuous, non-vanishing tangent vector field on S2. A related result (which we won’t prove here) states that SO(3) cannot be covered by a single coordinate chart without singularities. Any attempt to describe all of SO(3) using three continuous angles must have degenerate points — configurations where the mapping from angles to rotations is not locally invertible.

Different Euler angle conventions (ZYX, ZYZ, XYZ, and 9 others) all have singularities, just at different configurations. ZYX (yaw-pitch-roll) locks at pitch ±90°. ZYZ (aerospace convention) locks at zero pitch. There is no singularity-free 3-angle representation of SO(3). The minimum-overhead solution is unit quaternions: 4 floats with one quadratic constraint, no singularities, direct geodesic interpolation.

Overconstrained representations are fine. The unit quaternion uses 4 numbers to represent 3 DOF — one extra dimension. The constraint is |h| = 1. This is fine! Numerical computation occasionally drifts off the constraint manifold, but renormalization (divide by |h|) is O(1) and keeps the representation valid. The 9-entry rotation matrix is even more overconstrained (9 entries, 6 constraints, 3 DOF), but this too is fine — you re-orthogonalize via Gram-Schmidt or SVD when needed. The topology of the target space (SO(3) ≅ RP3) is what matters, not the ambient dimension of the parameterization.

Quaternions: the right space for 3D rotations

A quaternion h = a + bi + cj + dk is a 4D number with three imaginary units i, j, k satisfying: i2 = j2 = k2 = ijk = −1. The key identities are ij = k, jk = i, ki = j (and their negatives when reversed). A unit quaternion satisfies a2 + b2 + c2 + d2 = 1, which means it lives on the 3-sphere S3 ⊂ R4.

Quaternion algebra: the rules

Quaternion multiplication is associative but not commutative: in general, h1 · h2 ≠ h2 · h1. This non-commutativity directly reflects the non-commutativity of 3D rotations: rotating 90° around X then 90° around Y gives a different result from rotating 90° around Y then 90° around X. The quaternion algebra exactly captures this.

The conjugate of h = a + bi + cj + dk is h* = a − bi − cj − dk. For unit quaternions, h−1 = h* (inverse = conjugate). This is analogous to how rotation matrices satisfy R−1 = RT. The product hh* = a2+b2+c2+d2 = 1 for unit quaternions, confirming the norm is preserved.

To rotate a 3D vector v using quaternion h, embed v as a pure quaternion p = 0 + vxi + vyj + vzk, then compute h · p · h*. The result is another pure quaternion whose imaginary part is the rotated vector. This formula is equivalent to applying the rotation matrix R(h), but works directly in quaternion space without expanding to a 3×3 matrix.

Unit quaternions parameterize 3D rotations via the axis-angle formula. For a rotation of angle θ around unit axis v = (v1, v2, v3):

h = cos(θ/2) + sin(θ/2)(v1i + v2j + v3k)

The corresponding 3×3 rotation matrix (LaValle eq. 4.20) is:

R(h) =
2(a2+b2)−1 2(bc−ad) 2(bd+ac)
2(bc+ad) 2(a2+c2)−1 2(cd−ab)
2(bd−ac) 2(cd+ab) 2(a2+d2)−1

The double-cover: why SO(3) ≅ RP3

Here is the crucial topological fact (LaValle Figure 4.10): h and −h give the same rotation. The quaternion (−a, −b, −c, −d) describes the rotation of angle (2π−θ) around axis −v, which is physically identical to rotating θ around v. Every rotation corresponds to exactly two antipodal points on S3: {h, −h}.

So SO(3) is not S3 — it is S3 with antipodal points identified: S3/~ = RP3. The unit quaternion sphere is a double cover of SO(3). This identification has the fundamental group consequence we saw in Chapter 3: π1(RP3) = Z2, explaining the plate trick.

Let’s verify the double-cover concretely. For the rotation h = cos(θ/2) + sin(θ/2) · v:

Quaternion interpolation (SLERP) is why games and robots use quaternions. Interpolating Euler angles produces uneven angular velocities and potential gimbal lock. Interpolating quaternions on S3 via spherical linear interpolation (SLERP) produces smooth, constant-speed rotation paths. This is directly exploitable by motion planners: SLERP gives a natural distance metric on SO(3), and the geodesic on S3 projected down to RP3 is the minimal rotation path.

SLERP: geodesics on SO(3)

Given two orientations represented as unit quaternions h0 and h1, the shortest rotation path between them is the geodesic on S3. This geodesic is parameterized by spherical linear interpolation (SLERP):

SLERP(h0, h1; t) = h0 · (h0−1 · h1)t,    t ∈ [0, 1]

Or equivalently, if Ω = arccos(h0 · h1) is the angle between them on S3:

SLERP(h0, h1; t) = (sin((1−t)Ω) / sin(Ω)) h0 + (sin(tΩ) / sin(Ω)) h1

SLERP produces constant angular velocity along the shortest rotation path. For motion planning, it provides a natural steering function: given two configurations qa = (ta, ha) and qb = (tb, hb) in SE(3), we can interpolate translations linearly and rotations via SLERP to get a valid path through C-space (assuming no obstacles). This is used directly in RRT-Connect and similar bidirectional planners operating in SE(3).

The double-cover and SLERP. Because h and −h represent the same rotation, SLERP can go the “long way around” S3 when it should go the short way. The fix: if h0 · h1 < 0, negate one quaternion before SLERPing. This ensures the interpolation travels through the shorter arc on S3, giving the minimal-angle rotation path. Forgetting this step causes robots to spin the long way around instead of the short way — a common implementation bug.

SE(3): the full 6-DOF C-space

The C-space of a 3D rigid body is SE(3) = R3×SO(3). Since SO(3) ≅ RP3, we have SE(3) ≅ R3×RP3. This is a 6-dimensional manifold. Its elements are represented as 4×4 homogeneous transformation matrices:

T =
—— R (3×3) —— x
y
z
0 0 0 1

where R is the 3×3 rotation matrix from the quaternion formula above, and (x, y, z) is the translation. Composition of rigid motions is again matrix multiplication. This is the standard format used by every robotic library: ROS uses SE(3) transforms, robotics simulators use SE(3) transforms, computer vision uses SE(3) transforms.

SE(3) in practice: the transform tree

A robot operating in 3D space maintains a transform tree: a directed graph of SE(3) transforms linking coordinate frames. Each node is a frame (world, robot base, each link, each camera). Each edge is a 4×4 T matrix. To compute the pose of the end-effector in world coordinates, you multiply the transforms along the path from world to end-effector: Tworld→ee = Tworld→base · Tbase→link1 · … · Tlink(n-1)→ee.

This chain of SE(3) multiplications is called forward kinematics — the topic of Chapter 6. For now, the key point is that each joint contributes one SE(3) transform parameterized by the joint’s configuration variable (angle for revolute, displacement for prismatic). The C-space of the entire chain is the product of the C-space factors for each joint.

ROS tf2 is SE(3) in production. The ROS transform library (tf2) maintains exactly this transform tree. Each transform is stored as a 3D translation vector plus a unit quaternion (7 floats total — 3 for translation, 4 for quaternion). When you call lookupTransform("world", "end_effector"), tf2 multiplies SE(3) transforms along the kinematic chain. The choice of quaternion over rotation matrix (9 floats, harder to normalize) is deliberate: quaternions are more compact, normalize cheaply (divide by norm), and SLERP interpolates smoothly.

Non-compactness and planning in SE(3)

SE(3) is not compact (translations can go to infinity). For planning a drone or autonomous vehicle, we operate in a bounded workspace — a compact subset of SE(3) — and the planner works within that subset. For a robot arm fixed to a base, the translations are constrained by the arm’s reach, effectively compactifying the reachable C-space. In practice, planners always work with bounded C-spaces, obtained by taking the physically meaningful subset of the full mathematical C-space.

The sampling distribution for sampling-based planners must respect the topology. For the translation part (R3), uniform sampling in a bounding box works fine. For the rotation part (SO(3) ≅ RP3), uniform sampling in SO(3) via uniform sampling on S3 (and then identifying antipodal points) gives the Haar measure — the unique rotation-invariant distribution on SO(3). To sample uniformly on S3, generate four independent Gaussians (a, b, c, d) ∼ N(0,1) and normalize: h = (a,b,c,d)/|(a,b,c,d)|. This is the standard method used in RRT and PRM implementations for SE(3) planning.

python
import numpy as np

def quat_multiply(h1: np.ndarray, h2: np.ndarray) -> np.ndarray:
    """Quaternion multiplication h1 * h2 (LaValle eq. 4.19).

    h = [a, b, c, d] where h = a + bi + cj + dk
    """
    a1, b1, c1, d1 = h1
    a2, b2, c2, d2 = h2
    return np.array([
        a1*a2 - b1*b2 - c1*c2 - d1*d2,   # a component
        a1*b2 + b1*a2 + c1*d2 - d1*c2,   # b (i) component
        a1*c2 - b1*d2 + c1*a2 + d1*b2,   # c (j) component
        a1*d2 + b1*c2 - c1*b2 + d1*a2,   # d (k) component
    ])

def axis_angle_to_quat(axis: np.ndarray, angle: float) -> np.ndarray:
    """Convert axis-angle to unit quaternion (LaValle eq. 4.21)."""
    axis = axis / np.linalg.norm(axis)  # ensure unit axis
    return np.array([
        np.cos(angle / 2),           # a = cos(θ/2)
        axis[0] * np.sin(angle / 2),  # b = v₁ sin(θ/2)
        axis[1] * np.sin(angle / 2),  # c = v₂ sin(θ/2)
        axis[2] * np.sin(angle / 2),  # d = v₃ sin(θ/2)
    ])

def quat_to_rotmat(h: np.ndarray) -> np.ndarray:
    """Quaternion to 3x3 rotation matrix (LaValle eq. 4.20)."""
    a, b, c, d = h
    return np.array([
        [2*(a**2+b**2)-1,  2*(b*c-a*d),    2*(b*d+a*c)  ],
        [2*(b*c+a*d),    2*(a**2+c**2)-1,  2*(c*d-a*b)  ],
        [2*(b*d-a*c),    2*(c*d+a*b),    2*(a**2+d**2)-1],
    ])

# Example: 90° rotation around Z axis
h_z90 = axis_angle_to_quat(np.array([0, 0, 1]), np.pi/2)
# h_z90 ≈ [0.707, 0, 0, 0.707] = cos(45°) + sin(45°)k

# Verify: h and -h give the same rotation matrix
R1 = quat_to_rotmat(h_z90)
R2 = quat_to_rotmat(-h_z90)
# np.allclose(R1, R2) → True  ← the double-cover in action

# Compose two rotations: 90° around Z, then 90° around X
h_x90 = axis_angle_to_quat(np.array([1, 0, 0]), np.pi/2)
h_composed = quat_multiply(h_x90, h_z90)  # Z first, then X
R_composed = quat_to_rotmat(h_composed)

def se3_matrix(t: np.ndarray, h: np.ndarray) -> np.ndarray:
    """SE(3) 4x4 homogeneous matrix from translation t and quaternion h."""
    T = np.eye(4)
    T[:3, :3] = quat_to_rotmat(h)
    T[:3,  3] = t
    return T
Quaternion Rotation Visualizer

Set an axis (vx, vy, vz) and angle θ. The quaternion components update live. Notice that θ = 0° and θ = 360° both give h = (1, 0, 0, 0), while θ = 180° gives h = (0, vx, vy, vz). Note that h and −h give the same rotation — the double cover.

vx 0
vy 0
vz 10
θ 90°
h = [a, b, c, d]

Comparing rotation representations

RepresentationDOF storedConstraintsSingularities?Best use
Euler angles (3)3 floatsNoneYes — gimbal lockHuman input only
Rotation matrix (9)9 floatsAAT=I, det=1 (6 constraints)NoComputation, composing transforms
Axis-angle (3)3 floatsNoneYes — at θ=0Small rotations, Lie algebra
Unit quaternion (4)4 floats|h|=1 (1 constraint)NoPlanning, interpolation (SLERP)

For motion planning, unit quaternions are almost always the right choice. They are singularity-free, overdetermined by only one constraint (easy to enforce by normalizing), support SLERP for geodesic interpolation, and have efficient multiplication formulas. Rotation matrices are excellent for applying transforms to points (matrix-vector multiply is fast). Euler angles should almost never appear inside a planner — only at the user interface for human-readable input/output.

Distance on SE(3) for planning

Planners like RRT need a distance function d(q1, q2) to choose which node in the tree to extend toward. On SE(3), the natural choice combines translation distance with rotation distance:

d(q1, q2) = √( α · |t1−t2|2 + β · dSO(3)(R1, R2)2 )

where dSO(3)(R1, R2) = arccos((trace(R1TR2)−1)/2) is the geodesic distance on SO(3), and α, β are weighting constants that depend on the problem scale (meters vs radians). Setting α and β correctly matters: if α >> β, the planner ignores rotations; if β >> α, it ignores translations. There is no universal correct weighting — it depends on the task.

In quaternion form, the geodesic distance is simpler to compute: dSO(3)(h1, h2) = 2 arccos(|h1 · h2|), where the absolute value handles the double-cover (h and −h have zero distance to each other). The factor of 2 converts from S3 arc length to SO(3) arc length. This formula is the one to use in any planner operating in SO(3) or SE(3).

Uniform sampling on SO(3)

Sampling-based planners need to sample configurations uniformly at random from C-space. For Rn, sampling uniformly in a bounding box is trivial. For SO(3), “uniform” means the Haar measure: the unique probability measure on SO(3) invariant under left and right multiplication by any fixed rotation. Sampling Euler angles uniformly from [0, 2π) does NOT give uniform rotations — you over-sample near the poles (gimbal-lock-adjacent regions).

The correct method: generate (a, b, c, d) with each component independently drawn from N(0,1), then normalize h = (a,b,c,d)/|(a,b,c,d)|. The resulting unit quaternions are uniform on S3 by the spherical symmetry of the Gaussian distribution. Each rotation appears twice (h and −h), but this is harmless: just use h as sampled.

python
import numpy as np

def sample_so3_uniform() -> np.ndarray:
    """Sample uniformly random rotation as unit quaternion [a,b,c,d].

    Haar measure on SO(3): invariant under rotations.
    Method: normalize 4 independent Gaussians.
    """
    h = np.random.randn(4)   # 4 independent N(0,1)
    h /= np.linalg.norm(h)    # project onto S³
    if h[0] < 0:
        h = -h                # canonical: a >= 0
    return h

def so3_geodesic_dist(h1: np.ndarray, h2: np.ndarray) -> float:
    """Geodesic distance between two SO(3) elements (as unit quaternions)."""
    dot = abs(np.dot(h1, h2))   # |h1·h2|, handles double-cover
    dot = np.clip(dot, 0.0, 1.0)
    return 2.0 * np.arccos(dot)  # angle in [0, pi]

def se3_distance(t1, h1, t2, h2, alpha=1.0, beta=1.0) -> float:
    """Weighted SE(3) configuration distance."""
    dt = np.linalg.norm(t1 - t2)
    dr = so3_geodesic_dist(h1, h2)
    return np.sqrt(alpha * dt**2 + beta * dr**2)

# Example: two SE(3) configurations
t1 = np.array([1.0, 0.0, 0.5])
h1 = axis_angle_to_quat(np.array([0, 0, 1]), 0)         # no rotation

t2 = np.array([2.0, 1.0, 0.5])
h2 = axis_angle_to_quat(np.array([0, 0, 1]), np.pi/2)   # 90° around Z

d = se3_distance(t1, h1, t2, h2, alpha=1.0, beta=1.0)
# d ≈ sqrt(1² + 1² + 0² + (π/2)²) ≈ sqrt(2 + 2.47) ≈ 2.11
# (1m translation in x, 1m in y, 90° rotation)

Summary: C-space topology reference table

Here is the complete reference for everything covered in Chapters 0–5 of this lesson. This table is the core “cheat sheet” for designing motion planners for different robot types:

DOF typeC-space factorManifoldπ1Compact?Correct distance
Unbounded prismaticR1D line{e}No|x1−x2|
Limited prismatic [a,b][a, b]1D interval w/ boundary{e}Yes|x1−x2|
Full revoluteS1CircleZYesmin(|Δθ|, 2π−|Δθ|)
2D translationR2Plane{e}No√(Δx2+Δy2)
2D rigid body (SE(2))R2×S13D manifoldZNoWeighted Euclidean+circle
3D rotation (SO(3))RP33D manifoldZ2Yes2 arccos(|h1·h2|)
3D rigid body (SE(3))R3×RP36D manifoldZ2NoWeighted Euclidean+SO(3)
k-link revolute armTk = (S1)kk-torusZkYesProduct of circle distances

Choosing a quaternion library: what to look for

When implementing SE(3) planning, you will use a quaternion library. Key features to verify: (1) Unit normalization: the library should normalize after every multiplication to prevent numerical drift off S3. (2) Double-cover handling: distance and interpolation functions should compare h with both h2 and −h2, using the shorter arc. (3) SLERP: the library should implement spherical linear interpolation correctly, including the special case when the two quaternions are nearly identical (use linear interpolation when |h1·h2| > 0.9995 to avoid division by sin(Ω) ≈ 0). (4) Consistent convention: the library should document whether quaternions are stored as (w, x, y, z) or (x, y, z, w) — a common source of bugs when mixing libraries.

Common Python options: scipy.spatial.transform.Rotation (the standard, wraps quaternions, supports SLERP, handles all edge cases), pyquaternion (lightweight, explicit), numpy-quaternion (fast, C extension). ROS uses its own convention (x, y, z, w in geometry_msgs/Quaternion). PyBullet uses (x, y, z, w). Eigen uses (w, x, y, z) internally. Always check the convention before integrating a library.

You represent a 3D robot orientation using Euler angles (yaw, pitch, roll). At pitch = 90°, changing yaw produces the same rotation as changing roll. What is this phenomenon called, and what parameterization avoids it?

Chapter 6: Kinematic Chains

A robot arm is not a single rigid body. It is a kinematic chain: a sequence of rigid links connected by joints. Each joint contributes one or more degrees of freedom, and those DOF multiply together to form the full C-space. Understanding how the pieces combine — topologically, not just numerically — is the difference between a planner that works and one that misses valid paths.

The rule is simple and powerful. If joint 1 has C-space factor C1 and joint 2 has factor C2, then the two-joint arm has C = C1 × C2. Each joint contributes its own topological piece independently. The C-space of the whole chain is the Cartesian product of the pieces.

The product rule: C = C1 × C2 × … × Cn. A revolute joint with full rotation contributes S1. A revolute joint with limited range contributes an interval [a, b]. A prismatic joint contributes R or [a, b]. Multiply the right pieces together and you have the correct C-space topology.

The canonical example: the 2-link planar arm

Two revolute joints, each with full 360° rotation. Joint 1 contributes S1. Joint 2 contributes S1. Therefore:

C = S1 × S1 = T2

T2 is the 2-torus: a donut shape. The C-space of this arm is not a flat square — it is a square with opposite edges identified, which is topologically a torus. Paths that cross the θ1=0/2π boundary or the θ2=0/2π boundary are perfectly valid; they wrap around the torus.

Forward kinematics: from angles to end-effector

Given joint angles (θ1, θ2), the forward kinematics maps them to the end-effector position in the workspace. For a planar 2-link arm with link lengths L1 and L2:

x = L1cos(θ1) + L2cos(θ1 + θ2)
y = L1sin(θ1) + L2sin(θ1 + θ2)

The elbow joint position is (L1cosθ1, L1sinθ1). The hand tip is that plus the second link rotated by the total angle θ12. One point in C-space (a pair of angles) maps to one point in the workspace. But one point in workspace can have two preimages in C-space — elbow-up and elbow-down. This is why planning in C-space is more natural: there is no ambiguity.

The torus wrapping in action: if your arm is at θ1=359° and you increase θ1 by 2°, it should reach θ1=1° via the short path across 0° — a 2° rotation. If your planner treats θ1 as a number, it tries to travel 358° the wrong way around. The torus topology forces the correct shortest-path distance.

Showcase: workspace and C-space side by side

The canvas below shows both views simultaneously. LEFT: the physical arm in 2D workspace with a circular obstacle. RIGHT: C-space as a square (toroidal identification shown by arrows on the edges). The orange dot is the current configuration (θ1, θ2). The dark region in the right panel is Cobs — precomputed on a 60×60 grid by checking collision at each (θ1,θ2). Click anywhere in the right (C-space) panel to jump to that configuration and watch the arm move. Notice how the dot can exit one edge and appear on the opposite edge — the torus in action.

2-Link Arm: Workspace + C-Space (Torus)

Sliders control joint angles. Click the C-space panel (right) to teleport. Dark region = Cobs. Dot wraps at edges — opposite edges are the same point on the torus.

θ1 45°
θ2 90°
Cfree — no collision
python
import numpy as np

def fk_2link(theta1: float, theta2: float,
              L1: float = 1.0, L2: float = 0.8) -> tuple:
    """Forward kinematics for a planar 2-link arm.
    Returns (elbow_x, elbow_y, hand_x, hand_y).
    """
    ex = L1 * np.cos(theta1)
    ey = L1 * np.sin(theta1)
    hx = ex + L2 * np.cos(theta1 + theta2)
    hy = ey + L2 * np.sin(theta1 + theta2)
    return ex, ey, hx, hy

def seg_dist(p, a, b) -> float:
    """Distance from point p to segment [a, b]."""
    ab = b - a
    t = np.clip(np.dot(p - a, ab) / (np.dot(ab, ab) + 1e-12), 0, 1)
    return np.linalg.norm(p - (a + t * ab))

def arm_collides(t1, t2, obs_c, obs_r, L1=1.0, L2=0.8) -> bool:
    ex, ey, hx, hy = fk_2link(t1, t2, L1, L2)
    orig  = np.array([0.0, 0.0])
    elbow = np.array([ex, ey])
    hand  = np.array([hx, hy])
    obs   = np.array(obs_c)
    return (seg_dist(obs, orig, elbow) < obs_r or
            seg_dist(obs, elbow, hand) < obs_r)

# Precompute C_obs on a 60×60 grid over [0, 2π) × [0, 2π)
N = 60
ts = np.linspace(0, 2*np.pi, N, endpoint=False)
cobs = np.array([[arm_collides(t1, t2, [1.2, 0.4], 0.35)
                  for t2 in ts] for t1 in ts])
# cobs.shape = (60, 60); True = collision at (ts[i], ts[j])
A 3-link planar arm has all three revolute joints with full 360° rotation. What is its C-space?

Chapter 7: C-Space Obstacles — Where Geometry Meets Topology

You now know what C-space is. The next question is: which configurations are forbidden? The answer is the C-space obstacle region, Cobs. This is the set of all configurations where the robot touches or overlaps at least one obstacle in the workspace.

Cobs = { q ∈ C  |  A(q) ∩ O ≠ ∅ }

Here A(q) is the set of workspace points occupied by the robot at configuration q. O is the union of all workspace obstacles. Cfree = C ∖ Cobs is the open set where motion is legal. The planning problem is: find a continuous path through Cfree.

Multi-link robots: the full collision formula

For a kinematic chain with links A1, …, Am, collisions come from two sources: each link might hit a workspace obstacle, and non-adjacent links might hit each other. Let P be the set of collision pairs — pairs [i, j] of links that are not permanently attached and could potentially intersect:

Cobs = ⋃i{ q  |  Ai(q) ∩ O ≠ ∅ }  ∪  ⋃[i,j]∈P{ q  |  Ai(q) ∩ Aj(q) ≠ ∅ }

Adjacent links share a joint and are always in contact at the joint point; they are NOT in P. Self-collision only becomes significant for 3+ link chains. For the 2-link arm, collision pairs are simply (link1, obstacle) and (link2, obstacle).

The Piano Mover’s Problem (Formulation 4.1)

LaValle’s Formulation 4.1 is the canonical statement of the motion planning problem in C-space:

Formulation 4.1 — The Piano Mover’s Problem:
Given: World W, obstacle set O ⊂ W, robot A with C-space C, qI ∈ Cfree, qG ∈ Cfree.
Find: A continuous path τ: [0,1] → Cfree with τ(0) = qI and τ(1) = qG,
or correctly report that no such path exists.

The “correctly report” clause is critical. A planner that always says “no path found” trivially satisfies the first condition but fails the second. This is the completeness requirement. The Piano Mover’s Problem is PSPACE-hard (Reif, 1979): as DOF grows, worst-case complexity explodes exponentially. No polynomial-time algorithm for the general case exists, which motivates sampling-based approximations.

What PSPACE-hard means practically: even checking whether a single configuration is in Cfree requires intersecting the robot’s geometry with all obstacles. Computing Cobs explicitly requires doing this for every possible configuration. For a 6-DOF arm with complex link geometry, this is totally infeasible. Sampling-based methods avoid it by only checking the specific configurations they visit.

Interactive: translating robot, C-space, Cobs

Cobs for a Translating Robot

Left: workspace with robot (orange square) and obstacle (gray rectangle). Right: C-space showing Cobs (dark). Dot turns red when in collision.

x 30
y 50
Cfree — no collision
A single convex polygon robot translates (no rotation) in 2D, and the obstacle is also a convex polygon. Is Cobs convex in C-space?

Chapter 8: Minkowski Sum — Computing Cobs Exactly

For a robot that only translates (no rotation), there is an elegant closed-form formula for Cobs. It uses the Minkowski sum. Master this in 1D first, then 2D.

The definition

A ⊕ B = { a + b  |  a ∈ A,  b ∈ B }

Geometrically: place a copy of A at every point in B (or equivalently, place a copy of B at every point in A) and union all the translates. The result is the set of all points reachable by adding one element from each set.

1D worked example (LaValle Example 4.13)

Robot A = [−1, 2] (occupies 3 units; reference point at 0, extends left 1 unit and right 2 units). Obstacle O = [0, 4]. We need Cobs = O ⊕ (−A).

Step 1: negate the robot. −A = [−2, 1]. Step 2: Minkowski sum. O ⊕ (−A) = {o + r | o ∈ [0,4], r ∈ [−2,1]} = [0−2, 4+1] = [−2, 5].

Verification: robot origin at position q occupies [q−1, q+2]. Collision when [q−1, q+2] ∩ [0,4] ≠ ∅: need q−1 ≤ 4 AND q+2 ≥ 0, i.e., −2 ≤ q ≤ 5. This exactly matches Cobs = [−2, 5].

2D convex polygons (LaValle Example 4.14)

For convex polygons in 2D, the Minkowski sum boundary is computed by merging the edge sequences of O and −A, sorted by the angle of their outward normals. If O has m edges and A has n edges, Cobs = O ⊕ (−A) has at most m+n edges and can be computed in O(m+n) time (after sorting).

Two types of contact contribute to the boundary: EV contact (an edge of A touches a vertex of O) produces an edge from A in the boundary. VE contact (a vertex of A touches an edge of O) produces an edge from O in the boundary. The merge procedure walks both polygons simultaneously, always advancing along the edge with the smaller outward normal angle.

Intuition: Cobs is the obstacle “inflated” by the robot’s shape. If the robot is a disc of radius r, every obstacle inflates uniformly by r — this is collision-sphere padding. For non-circular robots, the inflation is directional. This is exactly why many planners pre-inflate obstacles by the robot radius and then plan with a point robot.

Interactive: robot size → Cobs size

Minkowski Sum: Cobs = Obstacle ⊕ (−Robot)

Left: workspace with square robot (orange) and square obstacle (gray). Right: Cobs shown in orange. Grow the robot and watch Cobs expand. When the robot is a point, Cobs = the obstacle itself.

robot half-size 12
python
import numpy as np

def minkowski_convex(P: np.ndarray, Q: np.ndarray) -> np.ndarray:
    """Minkowski sum of two convex CCW polygons.

    Merge edge sequences sorted by outward-normal angle.
    O(n+m) once polygons are bottom-aligned.
    """
    # Start at bottom-most vertex of each polygon
    i0 = int(np.argmin(P[:, 1]))
    j0 = int(np.argmin(Q[:, 1]))
    n, m = len(P), len(Q)
    result, i, j = [], i0, j0

    for _ in range(n + m):
        result.append(P[i % n] + Q[j % m])
        ep = P[(i+1)%n] - P[i%n]   # current edge of P
        eq = Q[(j+1)%m] - Q[j%m]   # current edge of Q
        cross = ep[0]*eq[1] - ep[1]*eq[0]
        if   cross > 0: i += 1      # advance P
        elif cross < 0: j += 1      # advance Q
        else:            i += 1; j += 1  # parallel edges

    return np.array(result)

# Square robot half-side s, square obstacle half-side w:
# C_obs is a square with half-side (s + w)
# — Minkowski sum of two squares = larger square with summed side lengths
s, w = 0.2, 0.5
# C_obs side length = 2*(s+w) = 2*(0.2+0.5) = 1.4
A square robot with side length s translates (no rotation) near a square obstacle with side length w. What shape is Cobs and what are its dimensions?

Chapter 9: Why Explicit Cobs Breaks Down at Scale

The Minkowski sum is elegant for translation-only robots. But real robots rotate, and real robots have many joints. The moment you add rotation, explicit Cobs computation becomes intractable. This is not an engineering inconvenience — it is a fundamental barrier, and it is WHY sampling-based methods exist.

Adding rotation: Cobs becomes a 3D surface

For a robot that translates AND rotates in 2D, C = R2×S1. At each fixed angle θ, the 2D cross-section of Cobs is O ⊕ (−Aθ), where Aθ is the robot rotated by θ. Because −Aθ changes shape as θ varies, the Minkowski sum is different at every angle. Cobs is a 3D region in (x, y, θ)-space with a continuously morphing cross-section that cannot be computed efficiently for non-convex shapes.

High-DOF chains: Cobs in Tn

For an n-DOF revolute arm, Cobs lives in Tn. For n=6, that is a subset of a 6-dimensional torus. You cannot visualize it, store it, or compute its boundary. Even a coarse grid with 10 values per dimension needs 106 cells. At 100 per dimension: 1012 cells, each requiring a collision check. Completely infeasible.

The paradigm shift: sampling-based planners flip the order. They sample a configuration q, run a collision check to get a yes/no answer, and build a search graph incrementally in Cfree. The collision checker is a black box that works for ANY geometry. This is why the same RRT code plans for a 6-DOF industrial arm, a folding protein, and a video game character — they all just need a collision checker that returns a boolean.

The 2D cross-section view: rotation changes everything

The canvas shows a triangular robot rotating in front of a rectangular obstacle. The left panel shows the robot at the current angle θ. The right panel shows the 2D cross-section of Cobs at that θ — the Minkowski sum O ⊕ (−Aθ) computed on a grid. Slide θ and watch the Cobs cross-section morph. This is why you cannot just store Cobs once — it is different for every θ.

Cobs Cross-Section Changes with Rotation θ

Left: triangular robot at angle θ. Right: 2D Cobs cross-section at this θ (dark = forbidden). Drag θ to watch Cobs morph.

θ
A 7-DOF robot arm with polygonal links has C = T7. How many dimensions does Cobs have?

Chapter 10: Showcase — The C-Space Pipeline

Every motion planning system ever built for a real robot follows the same pipeline. You define the robot’s geometry, identify the C-space topology, build a collision checker, hand everything to a planner, and get back a path. The C-space abstraction is the seam that separates geometry from search — and that separation is what makes the whole thing work at scale.

The unification: a grand piano, a 6-DOF industrial arm, a folding protein, and a video game character avatar are all just points navigating through their respective Cfree. The SAME RRT or PRM code, given the right collision checker, plans for all of them. Change the robot, keep the planner.
ApproachThinks inAdvantagesDisadvantages
Workspace planningRobot geometry, link shapes, obstacle polygonsIntuitive, visually obviousAlgorithm must know robot shape; not generalizable
C-space planningA point in a manifoldAny graph search works; robot-independent; unified theoryHigh-dimensional; Cobs is implicit and complex

The pipeline from geometry to execution

1. Robot Geometry (Ch 3)
Define links A1,…,Am as polygons or meshes in body frame
2. C-Space Topology (Ch 4 — this lesson)
Identify C = C1×…×Cn and correct geodesic distance d(q1,q2)
3. Collision Checker
Black box: given q, compute A(q) and test A(q)∩O ≠ ∅. Returns boolean.
4. Sampling-Based Planner (Ch 5)
RRT or PRM: sample q, check collision, build graph in Cfree
5. Path in C-Space
Sequence τ(t) ∈ Cfree from qI to qG
6. Workspace Execution
Apply forward kinematics to each τ(t) → joint torques / motor commands
Pipeline Architecture

Each box is a computation module. Arrows show data flow. Note: the collision checker is the only module that knows about geometry. The planner and the path representations are robot-independent.

Why is it advantageous to separate the planning algorithm from the robot’s geometry using the C-space abstraction?

Chapter 11: Connections — Manifolds, Maps, and What Comes Next

You’ve built the full C-space vocabulary across 12 chapters. Here is the complete reference — every manifold, every distance formula, every topology — in one place. Use this as your cheat sheet when designing motion planners for new robot types.

Complete C-space reference table

RobotC-spaceDimNotes
Point in R2R22Trivial; Euclidean distance
Rigid body in R2SE(2) = R2×S13Translation + rotation; 3×3 matrix form
Rigid body in R3SE(3) = R3×RP36Quaternion parameterization avoids gimbal lock
n revolute joints (full range)Tn = (S1)nnn-torus; wrap-around at each joint boundary
n revolute joints (limited)[a1,b1]×…×[an,bn]nEach joint a closed interval; no wrap-around
n prismatic jointsRnnLinear motion; standard Euclidean distance
Mixed arm (k full revolute + m limited)Tk×[…]mk+mProduct topology; each factor handled separately

Key formulas

C-space obstacle:

Cobs = { q ∈ C  |  A(q) ∩ O ≠ ∅ }

Minkowski sum (translation only):

Cobs = O ⊕ (−A)

Geodesic on S1:

d = min(|Δθ|, 2π − |Δθ|)

SE(2) matrix:

T(x,y,θ) =
 [cosθ −sinθ x]
 [sinθ  cosθ y]
 [0      0      1]

SO(3) geodesic (quaternions):

d = 2 arccos(|h1 · h2|)

Limitations and what comes next

High dimensionality. A 7-DOF arm lives in a 7D manifold. Sampling-based planners work, but narrow passages in Cfree become exponentially harder to find as dimension grows. This is the active research frontier: narrow-passage sampling, bridge testing, workspace guidance.

Kinodynamic constraints. The C-space framework assumes the robot can stop anywhere and move in any direction. Real robots have velocity limits and momentum. Kinodynamic planning extends C-space to state space (q, q˙) and is covered in LaValle Chapter 14.

Uncertainty. Perfect configuration knowledge is assumed here. In practice, sensors are noisy and maps are imperfect. Belief-space planning (POMDPs) replaces the configuration with a probability distribution and is covered in Chapter 11.

Chapter connections

Ch 2: Discrete Planning
Graph search (BFS, Dijkstra, A*) in finite C-spaces → the algorithmic foundation
↓ built on by
Ch 3: Geometric Modeling
How robot geometry is represented → input to C-space construction
↓ built on by
Ch 4: Configuration Space (this lesson)
The abstraction that makes all planners robot-independent
↓ enables
Ch 5: Sampling-Based Planning
RRT and PRM operating in Cfree → practical workhorses of modern robotics
One sentence summary: a robot with n DOF IS a point moving through an n-dimensional manifold, an obstacle IS a region Cobs in that manifold, and once you see it that way, every path-finding algorithm you’ve ever learned applies directly. Lozano-Perez (1983) made this rigorous; it is the foundation of all modern motion planning.
A 3-joint arm has one revolute joint with full 360° rotation and two revolute joints limited to [−π/3, π/3]. What is the C-space topology?