Skip to content

Add an IPC-style integrator for penetration-free flex contact - #3420

Open
smallquail wants to merge 5 commits into
google-deepmind:mainfrom
smallquail:ipc-edge
Open

Add an IPC-style integrator for penetration-free flex contact#3420
smallquail wants to merge 5 commits into
google-deepmind:mainfrom
smallquail:ipc-edge

Conversation

@smallquail

@smallquail smallquail commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

What

A new opt-in integrator, integrator="ipc", providing penetration-free contact for deformable flexes: flex–flex contact including self-collision (vertex–triangle and edge–edge) and flex-vs-static-geom. All other constraints — rigid contacts, limits, friction, equality — remain on the native pipeline, solved simultaneously. The integrator is fully opt-in: models that don't select it are unaffected.

How it works

IPC methods prevent penetration by combining an implicit position-level step with continuous collision detection (CCD). This implementation makes that work inside MuJoCo's existing solver architecture rather than beside it:

  1. Per-step implicit minimization. Each step minimizes an incremental potential over the flex vertex positions (and coupled joint dofs). Flex elasticity is carried by the implicit effective metric M + K.

  2. Augmented-Lagrangian contact (why not just a barrier). Classic IPC enforces non-penetration with a stiff log-barrier, which brings severe ill-conditioning and barrier-parameter tuning. Here each contact pair instead carries a persistent Lagrange multiplier — a running contact-force estimate — and a few outer iterations per step alternate between solving the contact-augmented problem and updating the multipliers. Forces converge to the right values at moderate, fixed stiffness: no barrier, no conditioning cliff.

  3. MuJoCo's own solver as the inner solver. Each outer iteration's subproblem is exactly one MuJoCo constraint solve (the CG solver on the standard primal objective): the IPC contact terms enter through a small hook as additional quadratic terms in the objective, so native constraints and IPC contact are resolved together by the existing machinery. There is no separate optimizer.

  4. CCD-bounded commits. Candidate pairs are gathered with a conservative motion margin; every accepted position update advances only along a verified intersection-free path, and a step commits only once its full motion has been absorbed — so the committed trajectory never tunnels, and a step that cannot complete warns rather than silently losing motion.

Scope and limitations

  • Dim-2 flexes, with either edge-equality or FEM elasticity.
  • IPC ownership covers flex–flex (incl. self-collision) and flex-vs-static-geom; flex-vs-moving-rigid and rigid–rigid contacts remain native.
  • Models with no dim-2 flex are still stepped by this integrator, which drives its penalty from the native contact set; there is no fallback to another integrator.

Testing

engine_ipc_test covers the distance kernels (point–triangle, segment–segment, vertex–geom), pinned-vertex handling, and contact scenarios; exercised on cloth/bag models with heavy self-contact.

Thin deformables in sustained contact are the case MuJoCo's constraint
solver handles least well: a cloth pressed against another cloth needs
the contact to be resolved to a tolerance the soft-constraint model does
not target, and the failure mode is interpenetration rather than a
visible instability.

Add `integrator="ipc"`, a barrier-free augmented-Lagrangian integrator
for flex contact (Li et al., arXiv 2512.12151). An outer AL loop owns
the contact multipliers and a conservative-advancement CCD that keeps
the committed configuration intersection-free; the inner problem is the
model's own primal CG over the monolithic system, so contact is solved
in the same metric as the smooth dynamics rather than as separate rows.

Contact enters as a force plus a stiffness rather than as constraint
rows: each pair contributes D*ref*w to the smooth force and D*w*w^T to
the effective metric, which reaches the gradient and both line-search
coefficients with no per-iteration row walk. The pair list is published
in mjData.efm_contact for the duration of a solve, so the metric's
consumers -- the CSR assembly, the shift, the matrix-free operator and
the preconditioner fold -- all read one representation, and passive flex
contact and this integrator produce it the same way.

The active set is assembled and scored from a single persistent set:
the injected rows and the line-search merit had been drawn from two
different sets, which on a fast approach disagreed badly enough that
the line search rejected steps that were optimal for the problem it had
been handed, and the truncated iterate was committed through the CCD
blend -- total momentum changed by 6.1x its correct value at contact
onset. IpcTest.SelfContactConservesMomentum guards it.

Geometry lives in engine_collision_continuous: point-triangle and
edge-edge distance with barycentric coordinates and the swept-segment
time of impact, which the discrete collision generators do not expose.
Contact was reaching the metric two ways: assembled into the effective
stiffness CSR, which mjd_effMulAdd then applied, and separately by
mj_extraStiffMulAdd at the solver's two operator sites. Both computed
the same D*w*w^T from the same published pair list.

Apply it once, inside mjd_effMulAdd. Contact is a sum of rank-1 terms,
so applying it matrix-free costs O(npt) per pair against the O(npt^2)
that assembling it costs to store and to apply -- measured on a bag
model, 5457 pairs would have added ~786k nonzeros to a 74k-entry CSR.
The CSR's three contact passes and the solver's two direct calls go
away with it.

mjd_effMulAdd takes flg_contact because its consumers differ: the linear
solves want the contact term, while an energy evaluation that accounts
for contact separately would double-count it. It is also now safe to
call with the metric inactive, which is what lets the solver reach it on
a model that has contact but no assembled flex stiffness.

The publication no longer has to be cleared between the metric build and
the solve, since one consumer cannot double-apply it.
The merged continuous-collision module carries geometry only: the pair
struct holds the type, participants and geom id, and the AL state that
used to ride in it belongs to the solver. Define that solver-side pair
here, with the geometry members leading so a pair is passed to the
mjc_* API by cast rather than by copy, and move the AL warm-start into
the active-set seed, since the candidate array is geometry-only.

Follow the renames: pairGap, pairVerts returning the participant count,
pairBand, standoff with an explicit cap, advance with an optional
per-pair TOI output. The standoff cap and detection band constants move
here from the module, where review left no solver-specific constants.
Flex-flex pairs measure their gap at the midsurface: mjc_pairGap does not
subtract the flex radii, so a pair whose mesh geometry is tighter than the
combined radii still has a valid gap and stays under CCD coverage. Flex-vs-geom
pairs (types 2/3/4) kept subtracting the flex radius, so the same configuration
that motivated the flex-flex convention -- geometry closer than the radius, such
as a liner resting on the surface it wraps -- produced a permanently negative
gap there, leaving those pairs unusable.

Return the raw distance for the three geom pair types in mjc_pairGap and in
conGapAdv, which must agree with it for conservative advancement. A geom has no
radius to fold in, so the gap is now flex-midsurface to geom surface.

bandOffset gains the three geom cases so each flex participant contributes its
radius to the broad-phase reach, leaving detection range unchanged; the standoff
target, min(band, cap), is untouched.
The injection loop skipped every non-flex-flex barrier pair whenever the model
contained an articulated or rigid body, leaving those contacts to the native
constraint rows. The pairs stayed in the active set regardless: their multipliers
kept ascending and conservative advancement kept enforcing them, while no
corresponding force ever entered the solve. Flex resting on a static geom in a
model that also contains a free body therefore had no restoring force at all. It
settled onto the surface, and the advance then froze, since every step from there
intersects.

Inject the full active set in that case as well. Candidate generation is
flex-only and its geom pairs are restricted to static geoms, so this adds no
contact class: it restores rows that are already barriered whenever the same flex
touches the same geom in a model without rigid bodies.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants