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 fi=(ft1,ft2,fn)f_i = (f_{t1}, f_{t2}, f_n): two tangential components and one along the inward normal. Coulomb friction with coefficient μ=0.6\mu = 0.6 requires

∥(ft1,ft2)∥  ≤  μ fn\Vert (f_{t1}, f_{t2})\Vert \;\le\; \mu\, f_n

for each finger.

The forces must hold the ball against an external wrench ww (gravity plus a small sideways push and twist), so force and torque balance give six linear equations Gf+w=0G f + w = 0, where GG maps the nine contact-force components to the net force and torque. Among all balancing forces we want the least effort:

min⁡f 12∥f∥2\min_f \ \tfrac12 \Vert f\Vert^2

subject to Gf+w=0G f + w = 0 and fi∈coneif_i \in \text{cone}_i for i=1,2,3i = 1, 2, 3.

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 4×10−134\times10^{-13}, and the solution sits within 4×10−134\times10^{-13} 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).

Three fingers grasping a ball, each force inside its friction cone
The solution: each arrow is a finger force, ending at its contact and lying inside its friction cone.

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 5×10−25\times10^{-2}; 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.