Dynamics for Sphere Linkages
Computing a metric for which the natural physical motion of the linkages are geodesics
Third in a series on spherical configuration spaces of planar linkages, joint work with Aaron Abrams, Dave Bachmann, and Edmund Harriss. The setup is in the first post.
In the previous post we built explicit smooth coordinates on — a homeomorphism for every in the regime . Combined with the topological result of post 1, we can now name a configuration by a point on a sphere, drag it in code, and watch the linkage flex.
What we haven’t yet asked is the question that turns this from a topological curiosity into physics: how does the linkage actually move?
Pin the endpoints to a frictionless table, give the linkage a push, and let it go. With no friction and no potential, Lagrangian mechanics tells us the trajectory is the path minimizing the action , where is the linkage’s kinetic energy.
Here is the geometric reframing — the new content of this post. At each configuration, kinetic energy is a positive-definite symmetric bilinear form on the tangent space, quadratic in the velocity. That’s the same thing as a Riemannian metric :
The action of a path now reads
which up to the factor of is the Riemannian energy functional. Its critical curves — the paths picked out as physical trajectories — are exactly the constant-speed geodesics of . Lagrange’s variational principle and the variational principle for geodesics coincide.
Theorem. Force-free trajectories of a mechanical system are constant-speed geodesics of the kinetic-energy metric on its configuration space.
So to understand how the linkage moves, we equip with its kinetic metric and find the geodesics. The metric depends on the physical model — where the mass lives — which we set up in §1. Section 2 pulls it back along to a metric on the sphere; §3 computes it explicitly for . The geodesics themselves we approach two ways: §4 hands the §3 metric to a standard Christoffel-based integrator, while §5 derives them directly from the embedding into , never touching the metric formulas. The two serve as independent cross-checks on each other.
The kinetic metric on
A chain has vertices connected by unit-length rigid rods, with and pinned. The interior hinges are the moving parts.
To set up dynamics we specify where the mass lives. The most general choice has
- a point mass at each interior hinge , and
- a linear mass density along each rod, parameterized by arclength .
(Mass on the pinned endpoints doesn’t move, so it doesn’t enter the dynamics.)
The rod-distributed mass is determined by the motion of its hinges — that’s what rigidity buys us. A material point at arclength on rod sits at
with velocity , linear in . The total kinetic energy is
Expanding inside the integral gives a quadratic form in and — with a cross term from the part of the expansion. So in general the kinetic-energy metric on has nearest-neighbor hinge correlations, and the metric matrix is block-tridiagonal rather than diagonal.
For this post we make the simplest choice: massless rods () and unit hinge masses (). The cross terms vanish, and
This is exactly half the squared length of in the standard Euclidean metric on . With this physical choice, the kinetic-energy metric on the unconstrained system is just Euclidean.
Our actual configuration space sits inside as a high-codimension submanifold: an -dimensional sphere in a -dimensional ambient. The kinetic metric on is the restriction of the Euclidean metric to tangent vectors of — equivalently, the pullback of the Euclidean metric along the inclusion .
Pulling back to
Composing the spherical parameterization from post 2 (position-space version) with the inclusion gives a smooth embedding
sending each sphere coordinate to its tuple of interior hinge positions . Pulling the Euclidean metric back along this composite gives the kinetic metric on the sphere.
For a Euclidean metric, the pullback is just the sum of inner products of partials with respect to the sphere coordinates:
The task is now concrete: take partials of the hinge positions with respect to sphere coordinates, then sum the inner products. We do this for in the next section.
The metric on
We apply the pullback at , working directly with the position-space hinges from post 2. The plan: write down the hinge derivatives recursively, identify the named scalar partials that appear, assemble .
Hinge positions
Citing the boxed position-space formula from post 2 directly:
with , , and the helper definitions of from post 2. Three observations from there will do work below:
- — the last rod direction (unit vector).
- — the third rod direction (unit vector).
- — the first hinge lies on the unit circle around the origin.
Derivatives
Three chain-rule identities, one for each hinge formula above. Below, stands for either of the sphere coordinates or ; each acts on the formula’s helpers via their own dependence on .
. Differentiating :
The factor rotates , so is perpendicular to the last rod with magnitude . Geometrically: a sphere coordinate that affects swings the last rod about its pinned end at .
. Differentiating uses the unit-vector identity where is the third rod’s angle in the ambient frame:
Two contributions: the next hinge moves (), plus the third rod swings about its now-moving end at .
. Since , the innermost hinge moves on the unit circle around the origin: for some real angular velocity . From post 2, , so . The smooth replacement from post 2’s -trick eliminates :
Here is a linear functional of — one cleanly named scalar carries everything we need to know about how the elbow swings.
The recursion above bottoms out in five named scalar partials, one for each helper that depends on :
(With since these depend on alone, and on the valid range .)
The chain inherits the factor — the only derivative in the problem that doesn’t have a clean geometric interpretation. Everything else propagates through it.
Hinge velocities
Substituting the recursion into the auxiliary partials:
with (and inside it) evaluated on each side from the corresponding :
Each hinge velocity is now a linear combination of the rod-direction unit vectors (with -factors making them perpendicular) — except for , which by unit-circle confinement is just times the scalar .
The metric components
Plug into . Two simplifications fall out for free:
- and , since the rod directions are unit vectors.
- on either side (the angular velocity squared) since , and .
For the contributions involving , the cross between the two rod-direction terms produces — the (signed) cosine of the angle between the third and fourth rods. Carrying this through:
(In , the two factors of come from and the -piece of — varying moves and also drags via the dependency .)
A clean structural fact falls out: depends only on inner-level helpers () — no , no , no last-rod direction. The reason is geometric: varying at fixed leaves the outer rotation frozen and the outer endpoint stationary, so the inner config simply executes its own motion at the inherited sub-base length . Rotation invariance of the Euclidean inner product gives — the circle metric, evaluated at the (slowly -varying) sub-base length.
The components above feed on a stack of helper formulas. Most appeared in the derivation, but laying them out together — angular-velocity functionals, velocity vectors, named scalar partials, and the helpers from posts 1–2 — gives a self-contained reference for what metric4 consumes.
The angular-velocity functionals and the velocity vectors they consume:
The named scalar partials:
And the helpers from posts 1–2:
In code
function metric4(phi, t, L) {
// Outer-level helpers and their phi-partials
const alpha = Math.acos((L*L - 8) / (2*L));
const cphi = Math.cos(phi), sphi = Math.sin(phi);
const theta = alpha * sphi;
const ctheta = Math.cos(theta), stheta = Math.sin(theta);
const d = Math.sqrt(L*L - 2*L*ctheta + 1);
const R = Math.atan2(-stheta, L - ctheta);
const dthetaP = alpha * cphi;
const ddP = (L * stheta / d) * dthetaP;
const dRP = ((1 - L * ctheta) / (d * d)) * dthetaP;
// Inner-level helpers
const alpha_in = Math.acos((d*d - 3) / (2*d));
const sin_ai = Math.sqrt((d*d - 1) * (9 - d*d)) / (2*d);
const ct = Math.cos(t), st = Math.sin(t);
const theta_in = alpha_in * st;
const ctin = Math.cos(theta_in), stin = Math.sin(theta_in);
const dthetainT = alpha_in * ct;
const dalphainP = -((d*d + 3) / (2 * d * d * sin_ai)) * ddP;
const dthetainP = dalphainP * st;
// Hinges (as (re, im) pairs)
const p3 = [L - ctheta, -stheta];
const u4 = [ctheta, stheta]; // last rod
const u3_arg = R + theta_in;
const u3 = [Math.cos(u3_arg), Math.sin(u3_arg)]; // third rod
const p2 = [p3[0] - u3[0], p3[1] - u3[1]];
const p2_abs = Math.hypot(p2[0], p2[1]);
// nu_in: smooth signed amplitude. The ratio (4 - r^2)/cos^2(t) extends
// to d * alpha_in * sin(alpha_in) at cos t = 0; use a Taylor branch there.
const nu_in = Math.abs(ct) < 1e-6
? ct * Math.sqrt(d * alpha_in * sin_ai)
: ct * Math.sqrt((4 - p2_abs * p2_abs) / (ct * ct));
// Hinge velocities (as (re, im) pairs); -i * u * s = (u_im * s, -u_re * s)
const dt_p2 = [u3[1] * dthetainT, -u3[0] * dthetainT];
const c1 = dthetaP, c2 = dRP + dthetainP;
const dphi_p2 = [u4[1]*c1 + u3[1]*c2, -(u4[0]*c1 + u3[0]*c2)];
// Omega functional applied to a velocity v of p2
const Omega = (v) => {
// Im(v / p2) = (p2_re * v_im - p2_im * v_re) / |p2|^2
const im = (p2[0]*v[1] - p2[1]*v[0]) / (p2_abs * p2_abs);
// <p2, v> / (|p2| * nu_in)
const re = (p2[0]*v[0] + p2[1]*v[1]) / (p2_abs * nu_in);
return im - re;
};
const Omega_t = Omega(dt_p2);
const Omega_phi = Omega(dphi_p2);
// Inner products
const u34_dot = u3[0]*u4[0] + u3[1]*u4[1]; // <u3, u4>
const h_tt = Omega_t * Omega_t + dthetainT * dthetainT;
const h_pp = Omega_phi * Omega_phi
+ 2 * dthetaP * dthetaP
+ (dRP + dthetainP) * (dRP + dthetainP)
+ 2 * dthetaP * (dRP + dthetainP) * u34_dot;
const h_pt = Omega_t * Omega_phi
+ dthetainT * (dthetaP * u34_dot + dRP + dthetainP);
return [[h_pp, h_pt], [h_pt, h_tt]];
}
The metric matrix is what we hand to a geodesic solver in §4.
Geodesics
Physical trajectories of the linkage are constant-speed geodesics of . Writing for derivative with respect to the geodesic parameter (distinct from our coordinate ), and following the 2D geodesic cheatsheet, the geodesic ODEs in our chart are
The Christoffel symbols are determined by together with the inverse-metric components , , (where ):
Six metric partials are needed: . These could be hand-derived by chain-ruling through the §3 helpers, but the result is six unwieldy symbolic expressions; we instead compute them numerically by central differences of the metric4 function.
function geodesicVectorField([phi, t, phidot, tdot], L) {
const eps = 1e-5;
const m = metric4(phi, t, L);
const mPh = metric4(phi + eps, t, L);
const mPl = metric4(phi - eps, t, L);
const mTh = metric4(phi, t + eps, L);
const mTl = metric4(phi, t - eps, L);
const E = m[0][0], F = m[0][1], G = m[1][1];
const Ep = (mPh[0][0] - mPl[0][0]) / (2*eps);
const Fp = (mPh[0][1] - mPl[0][1]) / (2*eps);
const Gp = (mPh[1][1] - mPl[1][1]) / (2*eps);
const Et = (mTh[0][0] - mTl[0][0]) / (2*eps);
const Ft = (mTh[0][1] - mTl[0][1]) / (2*eps);
const Gt = (mTh[1][1] - mTl[1][1]) / (2*eps);
// Inverse metric components (script E, F, G in cheatsheet)
const D = E*G - F*F;
const Ei = G / D;
const Fi = -F / D;
const Gi = E / D;
// Christoffel symbols: Gam_<upper>_<lower lower>
const Gam_p_pp = 0.5 * (Ei*Ep + Fi*(2*Fp - Et));
const Gam_t_pp = 0.5 * (Fi*Ep + Gi*(2*Fp - Et));
const Gam_p_tt = 0.5 * (Ei*(2*Ft - Gp) + Fi*Gt);
const Gam_t_tt = 0.5 * (Fi*(2*Ft - Gp) + Gi*Gt);
const Gam_p_pt = 0.5 * (Ei*Et + Fi*Gp);
const Gam_t_pt = 0.5 * (Fi*Et + Gi*Gp);
// Geodesic accelerations
const phiddot = -(phidot*phidot * Gam_p_pp
+ 2*phidot*tdot * Gam_p_pt
+ tdot*tdot * Gam_p_tt);
const tddot = -(phidot*phidot * Gam_t_pp
+ 2*phidot*tdot * Gam_t_pt
+ tdot*tdot * Gam_t_tt);
return [phidot, tdot, phiddot, tddot];
}
Hand this to any standard solver with initial state , and the integrated trajectory is the linkage’s force-free motion. One coordinate caveat: the chart is singular at , where and several helpers diverge — a coordinate-not-geometric artifact, like polar coordinates at the origin. Trajectories that approach a pole need to be integrated in a different chart and transferred back across the overlap; but we do not do that here. Instead, if your trajectory hits exactly the north or south pole, the simulation just breaks…but this is of course rare.
Demo
Here’s a small program running these dynamics. Click and drag-to-release the point on configuraiton space: this sets an initial position and velocity, which is then integrated to produce the geodesic of the kinetic energy metric.
Geodesics from the embedding
The pullback metric and geodesic equations are quite complicated, leaving plenty of room to make a small error. Even after going through it all carefully, I was still worried an errant minus sign could easily have snuck through, and decided a good test would be to compute an independent derivation of the dynamics, and compare the two. Happily, the extrinsic geometry of our configuration space provides exactly such a means.
From §1, with our physical choice of unit point masses at the hinges and massless rods, the kinetic metric on the unconstrained system is the standard Euclidean metric on . So sits inside a 6-dimensional Euclidean space as a 2-dimensional surface, with metric induced from the ambient. That setting opens a second route to the geodesic equations — one that bypasses the §3 metric formulas entirely and gives us an independent computation we can cross-check against.
There’s a fact from undergraduate differential geometry that’s tailor-made for this situation. Take any surface in — a sphere, a torus — and ask which curves on it are geodesics. The answer: a curve is a geodesic iff at every point its acceleration is normal to . The canonical picture is a great circle on the sphere: at each point the velocity bends toward the center, and the centripetal acceleration points radially inward — perpendicular to the sphere.
The same statement works in any Euclidean dimension. Our setting just has two cosmetic differences from the textbook one. The ambient is rather than , so points have six coordinates instead of three. And the surface is 2-dimensional in 6 (codimension 4) rather than 2-dimensional in 3 (codimension 1), so the normal space at each point is 4-dimensional rather than 1-dimensional. Neither matters for the principle. “Normal to the surface” still means “no tangential component,” and the surface still has two tangent directions, so the normality condition produces exactly two scalar equations — the right number for a second-order ODE on a 2-manifold.
So the plan is concrete: compute the acceleration of a candidate curve in , project it onto the two tangent directions of the surface, and demand both projections vanish.
Step 1: the acceleration
Pick a curve in in our chart coordinates, . Pushing through the parameterization — which we’ll just write , the §3 hinge positions stacked into one vector — gives the corresponding curve in :
The acceleration is just the second derivative with respect to . Chain-ruling once gives , and differentiating again:
Step 2: impose normality
A vector in is normal to the surface at when it’s orthogonal to both basis tangent vectors and . So our geodesic condition unpacks as two scalar equations:
Plug in the expression for from Step 1 and distribute the inner products:
Each equation is linear in the unknown second derivatives — everything else is determined by the curve’s current position and velocity. Moving the velocity-quadratic pieces to the right and stacking the two equations:
A 2×2 linear system. Inverting solves for — the second-order ODE on that integrates to a geodesic.
The coefficient matrix on the left is, of course, the metric tensor from §3 — its entries are exactly the components we worked out there. We don’t need that observation to solve the system, but it’s worth noting the connection.
In code
The whole right-hand side is finite-difference operations on psi4Position from post 2.
function geodesicVectorFieldEmbedded([phi, t, phidot, tdot], L) {
const eps = 1e-4;
// q : (phi, t) -> R^6, using psi4Position from post 2.
const Q = (ph, tt) => {
const {p1, p2, p3} = psi4Position(ph, tt, L);
return [p1[0], p1[1], p2[0], p2[1], p3[0], p3[1]];
};
// R^6 vector helpers
const sub = (a, b) => a.map((x, i) => x - b[i]);
const add = (...vs) => vs.reduce((s, v) => s.map((x, i) => x + v[i]));
const scale = (a, c) => a.map(x => c * x);
const dot = (a, b) => a.reduce((s, x, i) => s + x * b[i], 0);
// 3x3 stencil of position samples
const Q00 = Q(phi, t);
const Qpp = Q(phi + eps, t), Qpm = Q(phi - eps, t);
const Qtp = Q(phi, t + eps), Qtm = Q(phi, t - eps);
const Qpt_pp = Q(phi + eps, t + eps), Qpt_pm = Q(phi + eps, t - eps);
const Qpt_mp = Q(phi - eps, t + eps), Qpt_mm = Q(phi - eps, t - eps);
// First and second partials by central differences
const q_phi = scale(sub(Qpp, Qpm), 1/(2*eps));
const q_t = scale(sub(Qtp, Qtm), 1/(2*eps));
const q_phiphi = scale(add(Qpp, scale(Q00, -2), Qpm), 1/(eps*eps));
const q_tt = scale(add(Qtp, scale(Q00, -2), Qtm), 1/(eps*eps));
const q_phit = scale(sub(sub(Qpt_pp, Qpt_pm), sub(Qpt_mp, Qpt_mm)), 1/(4*eps*eps));
// Velocity-quadratic part of qddot
const qdd_vel = add(
scale(q_phiphi, phidot * phidot),
scale(q_phit, 2 * phidot * tdot),
scale(q_tt, tdot * tdot),
);
// 2x2 system [<q_a, q_b>] (phiddot, tddot)^T = -[<q_a, qdd_vel>]
const g_pp = dot(q_phi, q_phi);
const g_pt = dot(q_phi, q_t);
const g_tt = dot(q_t, q_t);
const det = g_pp * g_tt - g_pt * g_pt;
const bP = -dot(q_phi, qdd_vel);
const bT = -dot(q_t, qdd_vel);
const phiddot = (g_tt * bP - g_pt * bT) / det;
const tddot = (g_pp * bT - g_pt * bP) / det;
return [phidot, tdot, phiddot, tddot];
}
This shares no code path with §4’s geodesicVectorField past psi4Position. Run both integrators from the same initial state: agreement validates the §3 pipeline (named partials, the functional, the metric components, the Christoffel formulas) all at once. Disagreement diagnoses a bug somewhere along that pipeline — by elimination, since the embedded version exercises none of it.
One practical note. The singularity at never enters here, provided psi4Position uses the smooth (sinc-form) expression for from post 2. The whole geodesic integration then sees a smooth with no removable singularity to worry about.
Comparison demo
The two integrators in this post — geodesicVectorField from §4 and geodesicVectorFieldEmbedded from §5 — should produce the same trajectories from the same initial state. Below, both run side-by-side: the trajectories drawn over each other on , and records their coordinate divergence over integration time.