Realistic Sphere Linkages
Redoing the calculations for configurations with mass distributed along the rods and at the vertices
Fifth 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.
The last two posts computed the dynamics and curvature of the configuration space , but with the simplest possible physical model: unit point masses at the interior hinges, massless rods. That’s the cleanest setting to set up the geometric machinery, but it’s a long way from a physical linkage you’d actually build. A real linkage will have both mass distributed along its rods (say they are made of plastic, or wood) and also mass at the hinges (metal bearings, and whatever else).
This post upgrades the physics. We pick two real parameters,
- — the total mass of each rod/edge,
- — the mass at each interior hinge/vertex,
with each rod assumed to have its mass distributed uniformly along its length. The previous posts are the special case .
The plan: derive the kinetic-energy metric on the unconstrained ambient space , then use it numerically as the inner product for the post 4 pullback machinery to get first and second fundamental forms on the sphere, and finally redo the curvature plots and geodesic dynamics with the new metric. The ambient metric is the only piece that genuinely changes; everything downstream is the same code, with a different inner product plugged in.
The unconstrained metric
We work in the unconstrained ambient with coordinates , the moving interior hinges. Pinned endpoints and have and don’t enter the dynamics regardless of mass.
Total kinetic energy is the sum of the hinge contributions and the rod contributions:
with the kinetic energy of rod . The hinge term is elementary; the rod term is where the work happens.
Stretchy rods
Here’s the subtlety with the rod term. In the constrained configuration space every rod has length , so saying “total mass per rod” is the same as saying “linear mass density per rod.” The two specifications agree on the constraint surface.
In the unconstrained ambient they don’t. The rod from to has length , which can be anything. Two natural conventions:
- Fixed total mass: rod has mass no matter what its length is. Stretching the rod thins it out; squashing concentrates the mass.
- Fixed linear density: rod has density , with total mass proportional to length. Stretching makes the rod heavier.
These give different ambient metrics on — but, crucially, they agree on the constraint surface , where every and the two specifications coincide. Since the ambient metric only enters our story through its restriction to , the pullback to the sphere is the same either way. We’re free to pick whichever ambient metric is more convenient to compute with.
We go with fixed total mass. Stretchy physical objects (rubber bands, springs) really do have this property — pulling on the band doesn’t conjure new material into being, it just spreads what’s there over a longer interval. Fixed linear density, by contrast, would have stretching create mass, which is harder to make sense of physically. And as we’ll see in a moment, the fixed-total-mass metric turns out to be configuration-independent — a single constant symmetric form on the whole ambient — which makes numerical pullback trivial.
So rod has uniform linear density
depending on configuration through , but with the configuration-dependence arranged so that the integrated mass is always .
The rod integral
Parameterize each rod by a material coordinate — a label for points along the rod that doesn’t change as the rod stretches. The point at material parameter on rod sits in the plane at
with velocity (linear in , regardless of the rod’s length or orientation)
The kinetic energy of the rod is the integral of against the mass element. In arclength the mass element is ; switching to the material coordinate gives , and
The two factors of cancel — the configuration-dependence of the density and the configuration-dependence of the arclength element annihilate each other. This is the geometric content of the fixed-total-mass spec, and it’s why this convention gives a clean metric.
So the rod- kinetic energy reduces to
an integral over a fixed interval against a constant measure.
Expanding the integrand,
we see that everything can be written in terms of two simple integrals!
Plugging these in,
Sanity checks. Two limiting cases pin this down.
Pure translation: . Then — the kinetic energy of a particle of mass moving at speed . ✓
Pure rotation about the rod’s center: . Then . Cross-check with elementary mechanics: the endpoints are at distance from the center moving at speed , so the angular velocity is . A uniform rod has moment of inertia about its center, giving . ✓
Assembling the kinetic energy
Sum the rod contributions and add the hinge term, with at the pinned endpoints:
This is correct, but the rod sum is awkward with the same variable appearing multiple places. To write it nicely as a quadratic form on the hinge positions we need to collect terms. It’s easier to see the pattern by working a small case first.
The 3-rod chain
Two interior hinges , three rods. The two boundary rods (1 and 3) each touch only one interior hinge — the other end is pinned and contributes nothing:
The middle rod (2) sits between both interior hinges and gives the full three-term expression:
Add the rod terms together: the diagonal appears in both and with coefficient each, totaling . Same for between and . The cross term comes from alone with coefficient . Adding the hinge masses:
The cross term is contributed entirely by the middle rod — the one that’s between the two interior hinges. The boundary rods don’t couple anything, because each of them has one end pinned. And the diagonal coefficient is the same for both hinges: from the two adjacent rods plus for the hinge itself.
Now to read off the metric tensor. The unconstrained ambient is , with linear coordinates and . Expanding the inner products in terms of these scalar components:
Substituting and writing where , the metric tensor is the matrix
(The off-diagonals are rather than because the cross term in gets distributed symmetrically between and .)
Look at where the entries actually sit. The diagonal is constant, the only off-diagonals are and , and the two zero blocks and tell us the metric never mixes the and components of any single hinge. In fact every block of is a scalar multiple of — the diagonal blocks are and the off-diagonal blocks are . That structure is exactly the tensor product:
The factor records the inter-hinge coupling, and records that each hinge has identical and decoupled structure. We’ll see this factorization appear at every , so for the rest of the post we’ll work with the smaller coupling matrices and tensor up to the full metric only when we need to.
The 4-rod chain
Three interior hinges , four rods. By analogy with the 3-rod case: the boundary rods (1 and 4) touch one interior hinge each, the interior rods (2 and 3) connect adjacent hinge pairs:
Each interior hinge’s appears in exactly two of these — the rods on either side — so the diagonal coefficient is again for every hinge, including the boundary ones and . (The boundary hinge only has one interior rod next to it — but the boundary rod still picks up its motion, contributing the same as any other rod-end. The pinned endpoint is what gets killed, not the live one.) The cross terms come from the two interior rods. Collecting:
The ambient is now with coordinates . Expanding the inner products as before, the metric tensor is the matrix
with and as before. Same structure as : zeros wherever and from the same or different hinges would mix, and a checkerboard of ‘s and ‘s on the diagonal-like positions where they don’t. The new feature is the missing ‘s connecting hinges 1 and 3 — at and — because the non-adjacent hinges and aren’t directly coupled. The matrix factors as
with the same tensor-product structure as before. is tridiagonal — nonzero only on the main diagonal and the two adjacent off-diagonals — reflecting that no single rod touches two non-adjacent hinges.
The general formula
The pattern extends verbatim to any . Every interior hinge contributes a diagonal term with coefficient , and every adjacent pair contributes a cross term with coefficient from the rod between them. The kinetic energy is
with corresponding coupling matrix
The full metric tensor on is then .
Configuration-independence. No appears anywhere in . The ambient kinetic metric is constant on — the same symmetric form at every configuration.
In code
// (n-1) x (n-1) symmetric tridiagonal coupling matrix M.
// The full ambient metric on (R^2)^{n-1} is M ⊗ I_2.
// Kinetic energy: T = (1/2) Σ_{i,j} M_{ij} <p_dot_i, p_dot_j>.
function ambientCouplingMatrix(n, m_e, m_v) {
const a = 2*m_e/3 + m_v;
const b = m_e/6;
const M = Array.from({length: n-1}, () => new Array(n-1).fill(0));
for (let i = 0; i < n-1; i++) {
M[i][i] = a;
if (i+1 < n-1) M[i][i+1] = M[i+1][i] = b;
}
return M;
}
// M-weighted inner product on (R^2)^{n-1}, viewing U, V as arrays
// of (n-1) vectors in R^2: <U, V>_M = Σ_{i,j} M_{ij} <U_i, V_j>.
function weightedDot(M, U, V) {
let s = 0;
for (let i = 0; i < U.length; i++) {
for (let j = 0; j < V.length; j++) {
const m = M[i][j];
if (m !== 0) s += m * (U[i][0]*V[j][0] + U[i][1]*V[j][1]);
}
}
return s;
}
This is the only piece of new infrastructure the upgrade requires. Everything in posts 3 and 4 — pullback to the sphere, fundamental forms, Christoffel symbols, geodesic ODEs, Brioschi, the Gauss equation — is already phrased in terms of an ambient inner product, and we just swap the Euclidean one for weightedDot(M, ·, ·). The rest of this post is the consequences.
Fundamental forms
The first and second fundamental forms on are computed exactly as in post 4, with the only change being the ambient inner product. Where post 4 used the standard dot product on , we now use — the -weighted inner product determined by the ambient metric tensor .
The embedding itself is unchanged — it’s the same from post 2, mapping into . We just measure inner products of vectors in the ambient differently. For convenience we’ll work with the embedding as a list of three hinges rather than flattening to , since weightedDot is naturally indexed that way.
The functions below all take the rod and hinge masses as direct arguments and build the coupling matrix internally on each call. This is overkill for performance but makes the functions self-contained, which is exactly what we want for hooking sliders up to interactive demos: changing a slider just changes the next argument value.
// Embedding S^2 -> (R^2)^3 as a list of three hinges.
function embedHinges(phi, t, L) {
const { p1, p2, p3 } = psi4Position(phi, t, L);
return [p1, p2, p3];
}
The first fundamental form
Pulling back the ambient metric along the embedding gives the metric on :
In code, this is exactly post 4’s firstFundamentalForm with the dot product replaced by weightedDot:
function firstFundamentalForm(phi, t, L, m_e, m_v) {
const M = ambientCouplingMatrix(4, m_e, m_v);
const eps = 1e-4;
const pPh = embedHinges(phi + eps, t, L);
const pPl = embedHinges(phi - eps, t, L);
const pTh = embedHinges(phi, t + eps, L);
const pTl = embedHinges(phi, t - eps, L);
// Central differences: ∂_φ p, ∂_t p as lists of 3 hinges
const dPp = pPh.map((p, k) => [
(p[0] - pPl[k][0]) / (2*eps),
(p[1] - pPl[k][1]) / (2*eps),
]);
const dTp = pTh.map((p, k) => [
(p[0] - pTl[k][0]) / (2*eps),
(p[1] - pTl[k][1]) / (2*eps),
]);
return {
E: weightedDot(M, dPp, dPp),
F: weightedDot(M, dPp, dTp),
G: weightedDot(M, dTp, dTp),
};
}
The second fundamental form
The second fundamental form is the normal component of in the ambient. “Normal” is defined relative to the ambient inner product — orthogonal complement of the tangent space — so the normal projection step also uses weightedDot.
The tangential component of an ambient vector has the form , with the coefficients satisfying
— same equation as post 4, but with all inner products -weighted. Subtracting the tangential piece from gives its normal component:
function secondFundamentalForm(phi, t, L, m_e, m_v) {
const M = ambientCouplingMatrix(4, m_e, m_v);
const eps = 1e-4, e2 = eps*eps;
const p = embedHinges(phi, t, L);
const pPh = embedHinges(phi + eps, t, L);
const pPl = embedHinges(phi - eps, t, L);
const pTh = embedHinges(phi, t + eps, L);
const pTl = embedHinges(phi, t - eps, L);
const pPP = embedHinges(phi + eps, t + eps, L);
const pPN = embedHinges(phi + eps, t - eps, L);
const pNP = embedHinges(phi - eps, t + eps, L);
const pNN = embedHinges(phi - eps, t - eps, L);
// Hinge-list central differences (each entry a 2-vector in R^2)
const fd = (a, b, h) => a.map((p, k) => [
(p[0] - b[k][0]) / h,
(p[1] - b[k][1]) / h,
]);
const fd2 = (a, c, b, h) => a.map((p, k) => [
(p[0] - 2*c[k][0] + b[k][0]) / h,
(p[1] - 2*c[k][1] + b[k][1]) / h,
]);
const dPp = fd(pPh, pPl, 2*eps);
const dTp = fd(pTh, pTl, 2*eps);
const dPPp = fd2(pPh, p, pPl, e2);
const dTTp = fd2(pTh, p, pTl, e2);
const dPTp = p.map((_, k) => [
(pPP[k][0] - pPN[k][0] - pNP[k][0] + pNN[k][0]) / (4*e2),
(pPP[k][1] - pPN[k][1] - pNP[k][1] + pNN[k][1]) / (4*e2),
]);
// 2x2 system from <q_a, q_b>_g
const E = weightedDot(M, dPp, dPp);
const F = weightedDot(M, dPp, dTp);
const G = weightedDot(M, dTp, dTp);
const D = E*G - F*F;
// Subtract the g-tangent part from v to get its g-normal component
const projectNormal = v => {
const bP = weightedDot(M, v, dPp);
const bT = weightedDot(M, v, dTp);
const cP = (G*bP - F*bT) / D;
const cT = (E*bT - F*bP) / D;
return v.map((vk, k) => [
vk[0] - cP*dPp[k][0] - cT*dTp[k][0],
vk[1] - cP*dPp[k][1] - cT*dTp[k][1],
]);
};
return {
II_pp: projectNormal(dPPp),
II_pt: projectNormal(dPTp),
II_tt: projectNormal(dTTp),
};
}
Note the asymmetry between the form of the metric components and the form of the normal vectors. The metric components are scalars — direct outputs of weightedDot. The normal vectors are still represented as lists of three hinges in — ambient vectors — and their lengths and inner products will need to be computed via weightedDot whenever they appear downstream.
Curvature
With the fundamental forms in hand, we can now compute the curvature scalars from post 4 — the Gaussian curvature (intrinsic, governing how geodesics diverge) and the total bending (extrinsic, the magnitude of the constraint force). Both formulas are unchanged from post 4; the only difference is that all inner products on ambient vectors now go through weightedDot.
Gaussian curvature
The Gauss equation expresses in terms of both fundamental forms:
Same expression as post 4, with the inner products now -weighted:
function gaussianCurvature(phi, t, L, m_e, m_v) {
const M = ambientCouplingMatrix(4, m_e, m_v);
const { E, F, G } = firstFundamentalForm(phi, t, L, m_e, m_v);
const { II_pp, II_pt, II_tt } = secondFundamentalForm(phi, t, L, m_e, m_v);
const numerator = weightedDot(M, II_pp, II_tt) - weightedDot(M, II_pt, II_pt);
return numerator / (E*G - F*F);
}
(As in post 4, this duplicates work between the two helper calls — both functions internally compute the first-derivative stencils. A merged version would run the stencil once and return both forms together. We’re keeping them separate here for clarity.)
Here’s a small simulation computing this curvature on the configuration space, in terms of sliders for , as well as and .
Total bending
Total bending is the squared norm of as a bilinear form. In an orthonormal tangent frame ,
Gram–Schmidt on the coordinate tangents gives the orthonormal frame, and changing the second fundamental form into this frame uses the same coefficients as post 4:
These are vectors in the ambient — linear combinations of the ‘s with scalar coefficients depending on . Their squared norms are again -weighted:
function totalBending(phi, t, L, m_e, m_v) {
const M = ambientCouplingMatrix(4, m_e, m_v);
const { E, F, G } = firstFundamentalForm(phi, t, L, m_e, m_v);
const { II_pp, II_pt, II_tt } = secondFundamentalForm(phi, t, L, m_e, m_v);
const D = E*G - F*F, sqrtD = Math.sqrt(D);
// Linear combination of hinge-list vectors with scalar coefficients
const combine = (cs, vs) => vs[0].map((_, k) => [
cs.reduce((s, c, i) => s + c * vs[i][k][0], 0),
cs.reduce((s, c, i) => s + c * vs[i][k][1], 0),
]);
const II_11 = combine([1/E], [II_pp]);
const II_12 = combine([-F/(E*sqrtD), 1/sqrtD], [II_pp, II_pt]);
const II_22 = combine([F*F/(E*D), -2*F/D, E/D], [II_pp, II_pt, II_tt]);
return weightedDot(M, II_11, II_11)
+ 2*weightedDot(M, II_12, II_12)
+ weightedDot(M, II_22, II_22);
}
Here’s the corresponding demo for the extrinsic curvature, again with sliders for both distance between the endpoints and the masses.
Dynamics
The force-free dynamics of the linkage are geodesics of the kinetic-energy metric on . Post 3 derived these two ways: the standard route via Christoffel symbols of the pulled-back metric, and a more elementary embedded route exploiting the fact that the geodesic acceleration must be normal to the surface in the ambient. The two were used as cross-checks on each other.
For our mass-weighted setting we’ll go straight to the embedded approach, which fits naturally with the machinery we just built. The condition is unchanged from post 3, except that “normal” is now defined relative to the ambient inner product:
A curve on is a geodesic iff its acceleration in the ambient is -normal to the surface — that is, .
Following post 3 §5: differentiate twice in to get
then enforce the two -normality conditions. They give a 2×2 linear system for the unknown second derivatives:
where is the velocity-quadratic part of . The 2×2 coefficient matrix is exactly — the first fundamental form, computed inline from the same partials.
The code re-uses the 9-point stencil from secondFundamentalForm. We keep the math entirely in the hinge-list representation:
function geodesicVectorField([phi, t, phidot, tdot], L, m_e, m_v) {
const M = ambientCouplingMatrix(4, m_e, m_v);
const eps = 1e-4, e2 = eps*eps;
// 9-point stencil
const p = embedHinges(phi, t, L);
const pPh = embedHinges(phi + eps, t, L);
const pPl = embedHinges(phi - eps, t, L);
const pTh = embedHinges(phi, t + eps, L);
const pTl = embedHinges(phi, t - eps, L);
const pPP = embedHinges(phi + eps, t + eps, L);
const pPN = embedHinges(phi + eps, t - eps, L);
const pNP = embedHinges(phi - eps, t + eps, L);
const pNN = embedHinges(phi - eps, t - eps, L);
// Hinge-list arithmetic helpers
const fd = (a, b, h) => a.map((p, k) => [(p[0]-b[k][0])/h, (p[1]-b[k][1])/h]);
const fd2 = (a, c, b, h) => a.map((p, k) => [(p[0]-2*c[k][0]+b[k][0])/h, (p[1]-2*c[k][1]+b[k][1])/h]);
const lc = (cs, vs) => vs[0].map((_, k) => [
cs.reduce((s, c, i) => s + c*vs[i][k][0], 0),
cs.reduce((s, c, i) => s + c*vs[i][k][1], 0),
]);
// First and second partials
const p_phi = fd(pPh, pPl, 2*eps);
const p_t = fd(pTh, pTl, 2*eps);
const p_phiphi = fd2(pPh, p, pPl, e2);
const p_tt = fd2(pTh, p, pTl, e2);
const p_phit = p.map((_, k) => [
(pPP[k][0] - pPN[k][0] - pNP[k][0] + pNN[k][0]) / (4*e2),
(pPP[k][1] - pPN[k][1] - pNP[k][1] + pNN[k][1]) / (4*e2),
]);
// Velocity-quadratic part of acceleration
const p_vv = lc(
[phidot*phidot, 2*phidot*tdot, tdot*tdot],
[p_phiphi, p_phit, p_tt],
);
// 2x2 system [<p_a, p_b>_g] (phiddot, tddot)^T = -[<p_a, p_vv>_g]
const E = weightedDot(M, p_phi, p_phi);
const F = weightedDot(M, p_phi, p_t);
const G = weightedDot(M, p_t, p_t);
const D = E*G - F*F;
const bP = -weightedDot(M, p_phi, p_vv);
const bT = -weightedDot(M, p_t, p_vv);
const phiddot = (G*bP - F*bT) / D;
const tddot = (E*bT - F*bP) / D;
return [phidot, tdot, phiddot, tddot];
}
Here’s the updated demo!