40 min

Contact and grasping: the parameters you did not write

A grasp is a sustained force balance across a dozen contact points, governed by friction, priority and cone settings that mostly came from somebody else's XML.

Where you are. You have a task scene and a scripted pick that puts the cube in the bin every time. This lesson is about why it works, which turns out to have very little to do with the code you wrote.

Make the cube slippery. Go on.

Your grasp works. You want to know how much margin you have, so you decide to take the friction away and watch it fail.

Open the scene and change the cube’s friction from 1 to 0.05. Run it. The cube goes in the bin.

Assume a typo. Change it to 0.001, which is friction you would struggle to stand up on. Run it. The cube goes in the bin.

Now print what the solver is actually using, from the live contact list:

cube <-> fixed_jaw_sph_tip1   dist -0.855 mm  dim 6  mu 1.000  normal force  9.560 N

You wrote 0.001. The solver is using 1.000. It never read your number.

The idea in one paragraph

Every contact in MuJoCo is a small negotiation between two geoms, and the parameters that govern it are chosen by a rule, not by whichever geom you happened to edit. When two geoms have equal priority, friction is the element-wise maximum of the two, so the rougher surface wins and making one side slippery does nothing at all. When one geom has higher priority, its parameters are used outright and the other’s are discarded. The SO-101’s fingers carry priority 1, so at the fingertips only the finger’s numbers exist. On top of that sits a global choice, the shape of the friction cone, which decides how faithfully tangential forces are limited, and that single option is worth the difference between a grasp that never fails and one that never succeeds. None of these are in your control code. All of them decide whether your robot can hold anything.

Nobody asked your cube

the cube friction you wrote the finger friction the model ships same priority? geom_priority yes no element-wise maximum the rougher surface wins every component making one side slippery changes nothing the higher priority wins outright its friction, condim and solref are used the other geom's values are ignored the SO-101 gripper geoms carry priority 1, so on this arm the second branch is the live one and the only friction that matters at the fingertips is the finger's
How MuJoCo picks a contact's parameters: equal priority takes the element-wise maximum of the two geoms' friction, higher priority wins outright

Wider than the screen; scroll it sideways.

Five runs, changing only which side is slippery and whether the gripper keeps its priority. The last column is the friction coefficient the solver reported at the fingertip contacts, read straight out of data.contact[i].friction[0].

cube frictionfinger frictiongripper prioritywhat the solver used
1.001.00on1.00
0.051.00on1.00
0.051.00off1.00
1.000.05on0.05
1.000.05off1.00

Read rows two and three together: with the gripper’s priority on, the finger’s parameters win by priority; with it off, the maximum rule saves the finger anyway. Both routes lead to the same place, so a slippery cube is unreachable from either direction. Only row four gets through, and it needs the priority to be on.

What is actually touching

Print the contact list at the moment the fingers close on the cube and there are 17 contacts in the whole scene, every one of them involving the cube.

what the cube is touchingcontactsnormal force eachcondim
three 0.75 mm tip spheres on the fixed jaw39.6 to 10.1 N6
thin plates up the fixed finger61.0 to 1.7 N6
the moving jaw’s inner plate and mesh411.9 to 12.2 N6
the table it has not left yet41.5 to 6.3 N4

Two things are worth staring at. First, the gripper contacts are condim 6 and the table contacts are condim 4, on the same cube, in the same step. The gripper’s priority carried its condim across, so the fingers resist twisting and the table does not. Second, none of those contacts involve the smooth finger mesh you see on screen. They are hand-placed boxes and spheres, and Lesson 2.5 showed you why: primitive against primitive gives a small number of well-separated contact points, and mesh against mesh gives a handful of badly conditioned ones that let the object pivot.

The dist column is negative everywhere. The fingers are inside the cube, by about one millimetre at the deepest.

You can trade that overlap against compute. Shortening the cube’s solref time constant from 0.01 s to 0.002 s cuts the resting penetration from 0.061 mm to 0.033 mm, and pushing it out to 0.02 s doubles it to 0.108 mm. A stiffer constraint needs more solver work and a smaller timestep to stay stable, which is the same bargain Lesson 2.2 made with the timestep.

The cone is the whole ball game

The remaining knob is global, and it is the one that matters.

Here is the honest version of this experiment, including the part that did not work.

Run the four combinations on the scene as it stands, with the 64 g cube and grippy fingers, and all four score 10 out of 10. You cannot tell them apart, because a firm pinch on a light object forgives everything. So I made the task marginal instead: a 400 g cube, fingertip friction dropped to 0.12, and the carry compressed from 1.2 s to 0.5 s so the cube gets swung rather than ferried.

contact settingslifted off the tableended in the bin
elliptic, impratio="10" (what the model ships)20/2020/20
elliptic, impratio="1"20/2020/20
pyramidal, impratio="10"2/200/20
pyramidal, impratio="1" (MuJoCo defaults)0/200/20

n = 20 trials · pick a 400 g cube with mu 0.12 fingers and carry it to the bin in 0.5 s · 2026-08-09

Total success to total failure, from one attribute. And note what the table does not say: on this task impratio barely moved anything, while the cone decided everything. The documentation recommends the combination of an elliptic cone, the Newton solver and a large impratio, and both SO-ARM models in Menagerie ship exactly cone="elliptic" impratio="10". My measurement supports the cone strongly and is silent on the rest, and I would rather tell you that than quietly report the tidy version.

The window is not where you think it is

Now the part that will humble whatever intuition you brought from two dimensions.

You have been picturing a parallel-jaw gripper as two plates that close on a line, so the tolerance on your approach point should be symmetric: half a centimetre of slack in each direction, centred on the cube. It is not, and the reason is visible the moment you plot where the moving jaw actually goes.

fixed finger never moves; the arm descends right alongside it cube 4 cm open, 1.20 rad 0.73 rad 0.50 rad 0.30 rad, first touch moving jaw tip a hinge, not a slide: it comes down and back, sweeping a long way in front of the fixed finger approach a few millimetres too near and the fixed finger shoves the cube away before the jaw ever closes; approach three centimetres too far and the sweeping jaw still gathers it in
Side view of the closing gripper: the fixed finger is a vertical wall, and the moving jaw's tip travels an arc that starts high and far forward before sweeping down and back into the cube

Wider than the screen; scroll it sideways.

The fixed finger is a wall. It does not move, ever, and the arm descends with it a couple of millimetres from the cube’s near face. The moving jaw is on a hinge, so as it closes its tip travels down and backward, starting 7.6 cm in front of the fixed finger and 3.5 cm above the grasp line. It sweeps a huge region in front of the cube and none at all behind it.

Sweep the approach offset and the asymmetry falls out.

fail cube ends up in the bin fail 0 6 12 24 36 approach offset behind the cube centre, mm 8 mm, the value in the script 8 mm of margin 31 mm of margin
A measured band of approach offsets: everything below zero fails, zero to 39 millimetres places the cube, and the operating point at 8 millimetres sits 8 millimetres from one edge and 31 from the other

Wider than the screen; scroll it sideways.

At an offset of -3 mm the arm reaches slightly past the cube, the fixed finger clips its near face on the way down, the cube skids away, and the jaw closes on nothing. At +39 mm the arm stops nearly 4 cm short and the sweeping jaw still gathers the cube in and pins it against the fixed finger. The usable band is 0 to 39 mm and the script sits at 8 mm, with 8 mm of margin on one side and 31 mm on the other.

That fact is worth more than it looks. It says the useful thing to randomise in Lesson 2.16 is the direction with 8 mm of margin, not the one with 31 mm. It says a perception system that reports the cube 5 mm too near is a catastrophe and one that reports it 20 mm too far is fine. And it says the tolerance of a gripper is a property you measure, per object, rather than a number you assume.

Review

Nobody asked your cube

Every contact is a small negotiation between two shapes, and the parameters that govern it are chosen by a rule rather than by whichever one you happened to edit. When two shapes have equal priority, friction is the element-wise maximum of the two, so the rougher surface wins and making one side slippery does nothing at all. When one has higher priority, its parameters are used outright and the other’s are discarded. The arm’s fingers carry priority one, so at the fingertips only the finger’s numbers exist. You can make the cube as slippery as you like and the solver will keep reporting the finger’s friction, because your cube was never asked.

The cone is the whole ball game

On top of the per-contact negotiation sits a global choice: the shape of the friction cone, which decides how faithfully tangential forces are limited. That single option is worth the difference between a grasp that never fails and one that never succeeds, twenty out of twenty against zero out of twenty, with nothing else changed. None of these settings are in your control code and most of them arrived in somebody else’s model file, and all of them decide whether your robot can hold anything. A grasp is not an event, it is a sustained force balance across a dozen contact points, so the parameters governing those points are the grasp.

Check yourself

1. You set friction="0.02" on an object and its grasp does not change at all. Name both mechanisms that could be hiding your change, and the one experiment that distinguishes them.

Either the gripper geoms have higher priority, in which case their parameters replace yours entirely, or priorities are equal and MuJoCo took the element-wise maximum of the two friction vectors, so the grippier finger won. To tell them apart, set model.geom_priority[:] = 0 and rerun. If the friction the solver reports changes, priority was the mechanism; if it stays put, the maximum rule was. On the SO-101 both are in play, which is why lowering the object’s friction is a dead end from either side.

2. The contact list shows dim 6 for the fingertips and dim 4 for the table, on the same cube in the same step. How can one object have two contact dimensionalities?

condim is a property of the contact, not of the object. Each contact takes its condim from the winning geom, so the finger contacts inherit the gripper class’s 6, which resists sliding, torsional and rolling motion, while the table contacts fall back to the cube’s own 4. That is intentional: torsional resistance at the fingertips is what stops a grasped object twisting out, and paying for it everywhere else would be waste.

3. Your grasp slips. You are about to reduce the timestep to make contact stiffer. Is that the first thing to try?

No. Check <option cone=...> first, because that one attribute took this task from 20 out of 20 to 0 out of 20, and a smaller timestep costs you proportional compute on every step of every episode forever. The rough order is: cone, then the collision geometry, then impratio and the solver parameters, then the timestep. Penetration and slip look similar from the outside and have different fixes.

4. Why does the same cube sink 0.061 mm into the table at rest and 1.067 mm into the fingers during a grasp?

Because penetration is the price of force, and the fingers push far harder than gravity. A soft contact produces a normal force that grows with overlap, so more force needs more overlap. At rest the only force is the cube’s weight; in the grasp it is around 10 N per fingertip contact. The overlap is not an error to remove, it is how the pinch exists at all.

5. From its operating point the grasp tolerates 31 mm of error one way and only 8 mm the other. Where does the asymmetry come from, and what does it tell your perception system?

From the mechanism. The fixed finger never moves, so the arm has to descend beside the cube without touching it, and a couple of millimetres of over-reach means the finger shoves the object away before the jaw closes. The moving jaw is a hinge that sweeps in from far in front, so it can gather a cube that is much too far away. For perception this means the error that matters is signed: reporting the object nearer than it is will destroy you, and reporting it further away costs almost nothing.

6. All four cone and impratio combinations score 10 out of 10 on the shipped scene. Why is that not evidence that the settings do not matter?

Because the shipped scene is easy: a 64 g cube, high fingertip friction and a slow carry give the grasp enormous margin, so every setting looks identical. The differences appear only when the task is marginal, which is exactly the regime a real robot spends its life in. A test that passes under every configuration is not telling you the configurations are equivalent; it is telling you the test is not sensitive.

Do this

code/grasp_lab.py has one TODO(you), in episode(). Fill it in, then run the whole file. It takes about four minutes and prints all five experiments. Budget 45 minutes including the follow-ups.

1. Read the contact list before you change anything. Run contact_report() and count the contacts, the forces and the condim values. Predict, before you scroll, whether the fixed jaw or the moving jaw is carrying more force. The answer is a fact about this specific gripper and it is not obvious.

2. Reproduce the friction puzzle. Run priority_demo(). Then add a sixth row of your own where both the cube and the fingers are set to 0.05, and predict the answer first.

3. Run the matrix, then make it boring. Run contact_matrix() as shipped and watch the bottom two rows collapse. Then call it with grip_mu=1.0, mass=0.064, carry_seconds=1.2 and watch all four rows read 10 out of 10. Write one sentence explaining why the second run is the more dangerous result to have seen first.

4. Find your own window. Run grasp_window(). Then change the cube to a 3 cm cube and rerun it. The window moves, and the direction it moves in tells you something about which jaw is doing the work.

5. Find the floor under the penetration. In penetration(), add rows with solref time constants of 0.2 s and 0.0005 s. Softening behaves exactly as you would guess: 0.2 s sinks 1.48 mm, twenty-four times the default. Stiffening does not. At 0.0005 s the cube sinks 0.029 mm against the default’s 0.061 mm, and going stiffer from there buys nothing. So solref sets how quickly a violation is pulled back, and something else is setting the depth it is pulled back to. Sweep the five numbers in model.geom_solimp one at a time until you find which one moves that floor. One of them halves the penetration; the rest barely register.

What you can now do

You can read MuJoCo’s live contact list and say, for any contact, which geom supplied its friction and condim and why. You can explain why lowering an object’s friction is unreachable from both the priority rule and the maximum rule. You know that penetration is the mechanism rather than an error, roughly how much of it to expect, and what it costs to reduce. And you have watched one attribute in <option> take a grasp from twenty successes out of twenty to zero, which is the specific reason grasping in simulation has its reputation.

What you can now do

You can read the live contact list, say which geom's parameters won and why, and flip a grasp from 20 out of 20 to 0 out of 20 by changing one option.