Differentiation#
This page describes how gradients are computed through the simulation, enabling gradient-based optimization of material properties, initial conditions, and other parameters.
Automatic Differentiation Overview#
torch-diffsim uses automatic differentiation (autodiff) via PyTorch’s autograd system to compute gradients of outputs with respect to inputs through the simulation.
Problem Setup#
Given:
Parameters \(\boldsymbol{\theta}\) (e.g., material properties \(E\), \(\nu\))
Initial state \(\mathbf{x}_0, \mathbf{v}_0\)
Loss function \(\mathcal{L}\) that depends on final or intermediate states
Goal: Compute \(\frac{\partial \mathcal{L}}{\partial \boldsymbol{\theta}}\)
This enables gradient-based optimization:
where \(\alpha\) is the learning rate.
Differentiable Simulation#
The simulation can be viewed as a computational graph:
For differentiation to work, every operation must:
Have a PyTorch autograd rule at the evaluated state
Maintain gradient information (no detach operations)
Allow backward pass through PyTorch’s autograd
Gradient Flow Through Time Steps#
Each simulation step involves:
Backward Pass#
By the chain rule, gradients flow backward:
Computing Force Gradients#
The key computation is \(\frac{\partial \mathbf{f}}{\partial \mathbf{x}}\) (force Jacobian) and \(\frac{\partial \mathbf{f}}{\partial \boldsymbol{\theta}}\).
Since forces come from energy:
The gradient is:
where \(\mathbf{H}\) is the Hessian of the energy (stiffness matrix in FEM terminology).
Energy-Based Force Computation#
In torch-diffsim, forces are computed as:
# Compute elastic energy
E_elastic = sum(psi(F_e) * V_e for all elements)
# Forces as negative energy gradient
forces = -autograd.grad(E_elastic, positions, create_graph=True)[0]
This approach:
Automatically handles material gradients: \(\frac{\partial \mathbf{f}}{\partial \boldsymbol{\theta}}\) is computed by PyTorch
Ensures correctness: Forces are guaranteed to be energy-consistent
Enables higher-order derivatives:
create_graph=Truemaintains the graph
Material Parameter Gradients#
For learnable material parameters \(E\) and \(\nu\):
The force derivative \(\frac{\partial \mathbf{f}}{\partial E}\) comes from:
Since \(\mu\) and \(\lambda\) depend on \(E\):
PyTorch autograd handles this automatically when parameters are torch.nn.Parameter.
Differentiable Contact#
Contact forces must also be differentiable. The ground-contact penalty is:
has gradient:
The potential is \(C^2\) at activation, so its force is \(C^1\).
IPCImplicitEulerSolver instead uses the IPC potential. Its contact gradient is using with the unprojected Hessian. Gradients are local and piecewise smooth: they are valid around a regular converged solution.
Smooth Operations for Differentiation#
Several operations are modified to maintain smoothness:
Velocity Clamping#
The standard solver uses a hard norm clamp, whose derivative changes abruptly at the threshold. The differentiable solver uses a smooth radial limiter:
Use smooth clamp via tanh:
Fixed Vertices#
Fixed degrees of freedom are constraints, so forces and velocities should not move them. The differentiable solver expresses this as an out-of-place mask:
where \(\mathbf{m}\) is a binary mask (\(m_i = 1\) for fixed vertices). The mask gives zero sensitivity from an update to a constrained output.
Memory-Efficient Backpropagation#
For long simulations (many time steps), storing the entire computational graph is memory-intensive.
Gradient Checkpointing#
Gradient checkpointing trades computation for activation memory. The current rollout utility can checkpoint selected step calls, but returning a complete trajectory still stores all requested states.
In torch-diffsim:
from torch.utils.checkpoint import checkpoint
for i in range(num_steps):
if i % checkpoint_every == 0:
state = checkpoint(step_fn, state, use_reentrant=False)
else:
state = step_fn(state)
Implicit Differentiation#
DifferentiableSolver differentiates through its unrolled explicit steps (don’t use use_implicit_diff=True). The separate IPCImplicitEulerSolver does use implicit differentiation because each time step is a converged nonlinear solve.
For a square stationarity residual \(\mathbf{r}(\mathbf{x}^*,\boldsymbol{\theta})=\mathbf{0}\), the derivative is:
In reverse mode, the IPC step solves
and then forms parameter gradients from \(-\boldsymbol{\lambda}^T \frac{\partial\mathbf{r}}{\partial\boldsymbol{\theta}}\). The Hessian is used here.
This requires:
Solving a dense linear system directly
Computing \(\frac{\partial \mathbf{r}}{\partial \boldsymbol{\theta}}\) at the solution
This avoids differentiating through Newton, line-search, and CCD control flow. The forward solve must actually converge.
Spatially Varying Materials#
For per-element material properties \(E_e\), the gradient is:
Each element has its own material parameter, but trajectory and loss sensitivities are globally coupled. Autograd therefor returns one derivative per element without assuming that those derivatives are independent.
To ensure positivity, we parameterize:
Then optimize \(\log E_e\) instead of \(E_e\) directly. The gradient transforms as:
Optimization Example#
Material parameter optimization:
Algorithm:
Forward: Run simulation with current \(E, \nu\) → get \(\mathbf{x}_T\)
Loss: Compute \(\mathcal{L} = \|\mathbf{x}_T - \mathbf{x}_{\text{target}}\|^2\)
Backward: Compute \(\frac{\partial \mathcal{L}}{\partial E}, \frac{\partial \mathcal{L}}{\partial \nu}\)
Update: \(E \leftarrow E - \alpha \frac{\partial \mathcal{L}}{\partial E}\)
Practical Considerations#
Gradient Explosion
If gradients become too large:
Reduce learning rate
Use gradient clipping:
torch.nn.utils.clip_grad_norm_(params, max_norm)Increase damping in simulation
Reduce time step
Gradient Vanishing
If gradients become too small:
Increase learning rate
Normalize loss by number of steps
Use adaptive optimizers (Adam, AdamW)
Check for numerical instabilities
Numerical Stability
Monitor element determinants in the explicit solver; reducing the step can avoid inversion.
Use a constitutive model appropriate for the expected deformation range
Apply velocity clamping to prevent blow-up
Use substepping for better stability
Verification#
To verify gradients are correct, use finite differences: