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
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 friction | finger friction | gripper priority | what the solver used |
|---|---|---|---|
| 1.00 | 1.00 | on | 1.00 |
| 0.05 | 1.00 | on | 1.00 |
| 0.05 | 1.00 | off | 1.00 |
| 1.00 | 0.05 | on | 0.05 |
| 1.00 | 0.05 | off | 1.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 touching | contacts | normal force each | condim |
|---|---|---|---|
| three 0.75 mm tip spheres on the fixed jaw | 3 | 9.6 to 10.1 N | 6 |
| thin plates up the fixed finger | 6 | 1.0 to 1.7 N | 6 |
| the moving jaw’s inner plate and mesh | 4 | 11.9 to 12.2 N | 6 |
| the table it has not left yet | 4 | 1.5 to 6.3 N | 4 |
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 settings | lifted off the table | ended in the bin |
|---|---|---|
elliptic, impratio="10" (what the model ships) | 20/20 | 20/20 |
elliptic, impratio="1" | 20/20 | 20/20 |
pyramidal, impratio="10" | 2/20 | 0/20 |
pyramidal, impratio="1" (MuJoCo defaults) | 0/20 | 0/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.
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.
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.