Skip to main content

Module contact_adjoint

Module contact_adjoint 

Source
Expand description

Trajectory adjoint through the convex contact solve — the same contact model the forward simulator runs.

§Why this module exists

crate::rollout’s adjoint differentiates a self-contained per-vertex ground-penalty contact model. The forward simulation path (phyz::Simulator::step_with_contacts) does not use that model: it detects ground contacts with a margin, assembles the convex problem (phyz_contact::assemble), and solves it with the active-set Newton in phyz_contact::convex under MuJoCo-style solref/solimp stabilization. A gradient of the penalty model is a gradient of different physics than the engine integrates — the third instance of the solver/gradient-disagreement bug class in this codebase, and the largest.

This module closes it. convex_adjoint_gradient runs exactly the forward step Simulator::step_with_contacts runs — same detection, same assembly, same warm-started solve — caches each step’s phyz_contact::ContactProblem and ContactSolution, and backpropagates through the implicit-function- theorem sensitivities of the actual solve the forward pass performed (phyz_contact::gradient::FixedPointSensitivity), at the regimes the solver converged to.

§How the backward pass works

One contacted step is

qdd     = ABA(q, v, u; π)                        (no contact forces)
v_free  = v + dt·qdd
f*      = argmin ½ fᵀ(A(q,π) + R(q,depth))f + fᵀ b(q,v_free)   s.t. cone
v'      = v_free + M⁻¹(q,π) Jᵀ(q) f*
q'      = Φ(q, v')                               (integrate_configuration)

Differentiating: dv' = dv_free + d(M⁻¹Jᵀ f*)|_{f fixed} + M⁻¹Jᵀ df*, and by the IFT at the converged fixed point with the active set held fixed, df* follows from the exact map linearization (FixedPointSensitivity::apply): the differential of the stationarity residual (A + R)f* + b − e_n·bias (impulses held fixed) on the sticking rows and sliding normal rows, plus the sliding tangential channel (slip-direction rotation) through each sliding contact’s own tangential block. Separating contacts respond to nothing, by construction.

The smooth factors (ABA, kinematics, assembly, Φ) are differentiated by central finite differences per lane — the same discipline crate::semi_implicit_step_jacobians documents for its ABA block. The contact solve itself is never finite-differenced: its derivative is the exact IFT sensitivity of the converged solution, which is what makes the gradient the derivative of the physics rather than of the solver iterations. Within a lane the contact set is frozen to the identities the nominal step detected (each contact is re-evaluated from its material anchor on the body), so a lane never re-runs detection and cannot flip the active set.

§Where the gradient is exact, one-sided, or undefined

  • Interior of a regime (a contact firmly separated beyond the margin, firmly loaded and sticking, or firmly sliding): the gradient is the exact derivative of the regularized contact model, up to central-difference error (~1e-9 relative) in the smooth blocks.
  • Contact activation: the margin band (see ContactMaterial::impedance_at) ramps the impedance smoothly to zero at the band’s outer edge, so a contact entering or leaving the detected set does so at zero force and the gradient is continuous across the crossing. This is precisely what the margin was added for. The exception is a manifold truncated at MAX_MANIFOLD_POINTS: a point excluded by the cap can still carry impedance, and the gradient is one-sided there.
  • Stick/slide and load/unload transitions: the true derivative does not exist at the exact transition; the IFT holds the regime the solver converged to, so the reported gradient is the one-sided derivative of that branch. This matches phyz_contact::gradient’s documented contract.
  • Impact timing: at a genuine contact-making event the gradient of the regularized model is biased relative to ideal rigid contact (Suh et al., ICML 2022); see phyz_contact::gradient’s module docs.

§Failure policy

A step whose contact solve did not converge is not a fixed point and has no IFT sensitivity; FixedPointSensitivity::at refuses it by design. This adjoint degrades loudly: it returns ConvexAdjointError::Unconverged (or ConvexAdjointError::SingularKkt for a degenerate active set) rather than substituting anything. A silent wrong gradient is the failure mode this module exists to eliminate.

§Scope

Ground and body-body contacts. A body-body contact is frozen as a feature pair (Anchor::Pair): both surface points in their own body’s frame, the normal in the frame of the body owning the reference face, and the contact point riding the other body’s vertex — so a lane sees the contact rotate and translate with both bodies. That is what carries the tangential-friction-between-two-moving-bodies channel — a foot on a board’s grip tape, a hand on a lever — which is the case a ground-only adjoint cannot express at all.

The per-vertex ∂J/∂(mesh vertex) channel of crate::rollout is not reproduced here; that adjoint remains the (clearly documented) penalty-model path for the vcad surface-gradient seam.

Re-exports§

pub use crate::rollout::N_INERTIA_PARAMS;

Structs§

ConvexAdjointGradients
Everything one unified backward pass produces.
ConvexContactRollout
A differentiable rollout through the forward simulator’s contact path.

Enums§

ConvexAdjointError
Why an adjoint pass refused to produce a gradient.

Functions§

convex_adjoint_gradient
One nominal rollout forward, one adjoint pass backward: J, dJ/dq0, dJ/dv0, dJ/du_t, and dJ/dπ per body — all through the convex contact model the forward simulator integrates.
convex_rollout_objective
Run the nominal rollout and return the objective only — the FD oracle for gates, and a cheap primal probe. The dynamics are identical to convex_adjoint_gradient’s forward pass (and to Simulator::step_with_contacts).