Tutorial 04 · 15 min
Grasp forces in friction cones.
Your first second-order-cone program: three fingers hold a ball, forces must balance the load and stay inside their friction cones, and we want the gentlest grip. Along the way, the one rule that matters for conic problems: sets that meet must be projected together.
The problem
Three fingertips touch a ball of radius 1, slightly below its equator, at 120° intervals. In each contact’s own frame, the force is : two tangential components and one along the inward normal. Coulomb friction with coefficient requires
for each finger.
The forces must hold the ball against an external wrench (gravity plus a small sideways push and twist), so force and torque balance give six linear equations , where maps the nine contact-force components to the net force and torque. Among all balancing forces we want the least effort:
subject to and for .
That is a second-order-cone program: a quadratic objective, linear equalities, and three cones.
The one rule: project sets that meet together
At the optimum, the equilibrium equations and at least two of the cones are
active at the same time. If you give them to the solver as four separate
candidates, each spike corrects one set and breaks another; the
winner-take-all sweep alternates between them. Wrap them in one
dykstra_projector instead. Dykstra’s algorithm returns the exact nearest
point of the intersection, so a spike lands on the right point in one go,
and the fixed point of the dynamics is the exact optimum.
End-to-end code
import numpy as np
from snn_opt import (AffineSubspaceProjector, OptimizationProblem, SNNSolver,
SolverConfig, dykstra_projector, scaled_soc_projector)
MU = 0.6
# Contact points on the unit sphere, and each contact's frame (t1, t2, n).
az = np.deg2rad([0.0, 120.0, 240.0]); el = np.deg2rad(-15.0)
points = np.stack([np.cos(az) * np.cos(el), np.sin(az) * np.cos(el), np.full(3, np.sin(el))], 1)
frames = []
for p in points:
n = -p # inward normal
t1 = np.cross([0.0, 0.0, 1.0], n); t1 /= np.linalg.norm(t1)
frames.append(np.stack([t1, np.cross(n, t1), n], 1))
# Grasp matrix: local contact forces -> net force and torque.
G = np.zeros((6, 9))
for i, (p, R) in enumerate(zip(points, frames)):
G[:3, 3*i:3*i+3] = R
G[3:, 3*i:3*i+3] = np.cross(p, R.T).T
w = np.array([0.3, 0.0, -1.0, 0.0, 0.05, 0.0]) # external force and torque
# One exact projector onto {G f = -w} ∩ cone_1 ∩ cone_2 ∩ cone_3.
cones = [scaled_soc_projector(3*i + 2, [3*i, 3*i + 1], MU) for i in range(3)]
grasp_set = dykstra_projector([AffineSubspaceProjector(G, -w), *cones])
problem = OptimizationProblem(np.eye(9), np.zeros(9), np.zeros((0, 9)), np.zeros(0),
nonlinear_candidates=(grasp_set,))
result = SNNSolver(problem, SolverConfig()).solve(np.zeros(9))
f = result.final_x
print(result.converged, result.iterations_used) # True 201
for i in range(3):
ft, fn = f[3*i:3*i+2], f[3*i+2]
print(f"finger {i+1}: friction use {np.linalg.norm(ft) / (MU * fn):.4f}")
What comes out
True 201
finger 1: friction use 0.9196
finger 2: friction use 1.0000
finger 3: friction use 1.0000
Fingers 2 and 3 end exactly on their cones: they are at the slip limit, and
any gentler grip would let them slide. Finger 1 has a little friction to
spare. The force-and-torque residual is , and the solution
sits within of a reference polished by Newton’s method on
the KKT system. Because the objective is strongly convex and the only
candidate is a Dykstra projector, the certificate behind converged=True is
a direct bound on the error in the state (see the
conic page).

The same problem, done wrong
Swap the last two lines of setup for separate candidates:
problem = OptimizationProblem(np.eye(9), np.zeros(9), np.zeros((0, 9)), np.zeros(0),
nonlinear_candidates=(AffineSubspaceProjector(G, -w), *cones))
result = SNNSolver(problem, SolverConfig(max_iterations=1500)).solve(np.zeros(9))
print(result.converged, result.kkt_residual / result.kkt_scale) # False, ~5e-2
It runs to the iteration cap and reports a relative KKT defect of about
; the forces it stops at are about two percent away from
the right ones. Nothing is wrong
with the solver; the spikes are simply correcting one set at a time. This is
the conic version of a lesson that also holds for plain linear constraints:
joint_projector(C, d) projects a block of rows exactly, and removes the
step-size offset the greedy row sweep leaves at a vertex.
Next
The full script, with a Clarabel cross-check and the figure, is
examples/example8_friction_cone_grasp.py.
For every built-in set and its options, see the
API reference.