Structured Simulators and Neural Operators › Differentiable Simulators
World Models HandbookAll chapters Structured Simulators and Neural Operators › Differentiable SimulatorsOne section · Sections in this chapter3 / 11 ↓
Reading tools Definitions & inline links
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
A simulator is a world model you wrote yourself. It takes a state and an action and produces a next state through hand-coded physics:
where:
- , : the state and the action at time
- : the next state produced by the hand-coded physics
- : the hand-coded transition function that advances the state one step
- : the physical parameters, collecting masses, lengths, friction coefficients, stiffnesses
The distinction between the structure of and the values of is the whole design. The structure encodes your physical understanding: which forces act, how they compose, what integration scheme advances the state. The parameters encode what you do not know precisely: the true mass of the payload, the actual coefficient of friction on the table, the real stiffness of a contact. Many legacy or externally wrapped simulators expose only forward outputs, even though classical sensitivity machinery may exist. A differentiable simulator additionally exposes derivatives of the implemented computation through analytic, automatic, implicit, adjoint, or custom derivative rules. For the rollout above, the quantities of interest are the following partial derivatives:
where:
- : the rollout horizon, the number of simulation steps taken from the initial state
- : the simulator state after steps, the output whose sensitivity we compute
- : how the final state changes with each physical parameter
- : how the final state changes with the initial condition
- : how the final state changes with the entire sequence of actions applied over the rollout
Each of these three gradients powers a different capability. The parameter gradient drives system identification, the initial-condition gradient drives sensitivity analysis and state estimation, and the action gradient drives trajectory optimization. One derivative mechanism enables all three, which makes differentiability valuable for a world model.
A physics engine or solver implementation that exposes derivatives of its implemented outputs (states, contacts, sensor readings) with respect to inputs, parameters, and initial conditions, using analytic, automatic, implicit, adjoint, or custom derivative rules.
Why does this matter? Three reasons dominate.
System identification becomes gradient descent. Part II, Chapter 3 treated system identification as an optimization problem and surveyed gradient-free and linear methods. Those methods are workhorses, but they can scale poorly with dimension and nonlinear dynamics. When the simulator is differentiable, identification of reduces to minimizing a loss by backpropagation. The method avoids choosing parameter updates from forward evaluations alone; each iteration still simulates the trajectory, compares it with data, backpropagates through the rollout, and updates the parameters. The gradient combines information from each step of the rollout.
Planning becomes gradient descent. If you can differentiate the accumulated reward or a target cost through the rollout, you can optimize an action sequence directly. This is the modern descendant of shooting methods from classical optimal control (Part II, Chapter 4), and we will do exactly this in the worked example. Automatic differentiation replaces much of the system-specific derivative transcription with loss.backward(), provided every operation on the executed path has a valid derivative. The resulting gradient is exact for the implemented computation, not necessarily for omitted or idealized physics.
Learning can wrap around the simulator. Gradients flowing through simulation let you fit residual neural networks on top of physics, fit simulator parameters jointly with policy parameters, or backpropagate a learned perception module into a physical model. This pattern appears in some hybrid differentiable-simulation pipelines: the simulator supplies a structured backbone, while learned components model residual effects and end-to-end derivatives coordinate the trainable pieces. It is one design option for sim-to-real work, not a description of every sim-to-real pipeline.
How gradients flow through a rollout
The rollout is a deep computation graph: applying once is one layer, so a horizon- rollout is a -layer network. Everything you know about training deep networks applies, with the twist that the layers are physics steps and the depth is dictated by the horizon you care about, not by an architecture choice. There are two standard ways to get gradients through it.
Unrolled backpropagation treats the whole trajectory as a feedforward graph and applies reverse-mode autodiff. This is what torch.autograd does automatically. It is exact and simple, and because reverse-mode autodiff computes the gradient of a scalar loss with respect to all inputs in a single backward pass whose cost is comparable to the forward pass, it is remarkably efficient per unit of information. Its weakness is memory: without checkpointing or recomputation, it stores intermediate states and memory grows linearly with . For a long rollout of a high-dimensional state, memory can become the binding constraint, depending on state size, per-step compute, hardware, and the checkpointing or recomputation strategy.
The adjoint method avoids storing the trajectory by solving an augmented system backward in time. For continuous dynamics and a terminal cost , which measures the terminal state, define the adjoint state by
where:
- : the adjoint state, a costate variable that accumulates sensitivity information as it is integrated backward in time from to
- : the Jacobian of the vector field with respect to the state, evaluated along the forward trajectory
- : the terminal condition, equal to the gradient of the terminal cost with respect to the final state
and then the gradient is an integral along the backward pass:
where:
- : the gradient of the terminal cost with respect to the parameters
- : the adjoint state (transposed), propagated backward from to along the reverse integration
- : the Jacobian of the vector field with respect to the parameters
- The integral runs from down to because the adjoint system is solved backward in time; the minus sign comes from that reversed direction of integration
The intuition behind the adjoint state is worth pausing on: answers the question "if you perturb the state slightly at time , how does the final cost change?" It starts at holding exactly the cost's sensitivity to the terminal state, and as it integrates backward, each step multiplies it by the transpose Jacobian of the dynamics, which converts sensitivity one step later into sensitivity one step earlier. The parameter gradient then follows by accumulating, along the backward sweep, how each parameter perturbation at each moment feeds into that running sensitivity. In the ideal continuous-adjoint formulation, storage can be independent of the number of accepted steps when the trajectory can be reconstructed. Practical implementations retain solver workspace or checkpoints and trade storage for recomputation. There is a pleasing unity here: discrete unrolled backpropagation is in fact an adjoint method for the discretized system; the distinction is practical, not fundamental. Which side wins depends on state dimension, horizon, numerical stability, and whether the integrator can reconstruct or restart the trajectory reliably.
Where differentiability breaks
Physics is not everywhere smooth, and this is the central engineering difficulty. The clean calculus of the previous section assumes is differentiable, and the most interesting parts of physical interaction are precisely where it is not.
- Contacts and impacts create hybrid maps whose event activation, timing, contact set, and normal can change non-smoothly with state or parameters. Within a fixed impact mode, a restitution map such as is linear and continuous in the incoming normal velocity; the derivative difficulty comes from switching between modes and moving event times.
- Friction and stiction involve complementarity conditions (either the object sticks with force below a threshold, or it slips with force at the threshold), which are non-smooth by nature. The switch between sticking and slipping is a set-valued force law, and set-valued maps have no single Jacobian at the switching surface.
- Chaotic dynamics amplify perturbations along unstable directions approximately as over the regime where linearization applies. After several Lyapunov times, long-horizon prediction and gradient-based optimization can become badly conditioned. The derivative can still be mathematically correct; the problem is that it describes extreme sensitivity to initial conditions, parameters, and numerical error.
Practical responses depend on the engine and contact pipeline. Some differentiable simulators use compliant or softened contact, while others differentiate constraint, convex, or position-based formulations; frameworks such as DiffTaichi and Warp support more than one simulator formulation. Any surrogate changes the relationship between the implemented gradient and ideal rigid-contact physics. Against chaotic sensitivity, practitioners often differentiate over shorter windows and re-fit or re-plan frequently, exactly the receding-horizon pattern we will see in the worked example. An adjoint changes memory and computation, not the underlying sensitivity.
We can isolate one mechanism by computing parameter sensitivity in two linear systems. One transition contracts perturbations and the other amplifies them. This is not a chaos experiment; it shows how repeated Jacobian products can make an additive parameter sensitivity saturate or grow with rollout length.
import numpy as np
rng_grad = np.random.default_rng(7)
## Two 2D systems: A_stable has eigenvalues of modulus < 1; A_unstable amplifies.
A_stable = np.array([[0.95, 0.1], [-0.1, 0.9]])
A_unstable = np.array([[1.05, 0.1], [-0.1, 1.02]])
## Fixed parameter entering the dynamics linearly, so d x_T / d theta is the
## composition of the per-step Jacobians times a constant direction.
b_dir = np.array([0.3, -0.2])
horizons = np.arange(1, 61)
grad_stable = np.zeros(len(horizons))
grad_unstable = np.zeros(len(horizons))
for i, T in enumerate(horizons):
g = np.zeros_like(b_dir)
gs = np.zeros_like(b_dir)
for _ in range(T):
g = A_unstable @ g + b_dir
gs = A_stable @ gs + b_dir
grad_unstable[i] = np.linalg.norm(g)
grad_stable[i] = np.linalg.norm(gs)
## Reference exponential fits for the annotation: log-gradient slope per step.
slope_unstable = np.polyfit(horizons, np.log(grad_unstable + 1e-12), 1)[0]
slope_stable = np.polyfit(horizons, np.log(grad_stable + 1e-12), 1)[0]
Let us build a minimal differentiable simulator and use it for identification. The system is a damped pendulum with gravity unknown:
where:
- : the pendulum angle, the configuration we observe
- : the angular velocity, the rate of change of the angle
- : the gravitational acceleration, the unknown parameter to be identified
- : the pendulum length, assumed known
- : the linear damping coefficient, assumed known
integrated with semi-implicit Euler steps. Updating velocity first and then position generally behaves better than explicit Euler for oscillatory mechanics. The undamped conservative update is symplectic; the damping term makes the full map dissipative rather than symplectic. Because every step is built from differentiable torch operations, autodiff gives us directly.
import numpy as np
import torch
torch.manual_seed(0)
L, c, dt = 1.0, 0.15, 0.02
g_true = 9.81
theta0, omega0 = 1.2, 0.0
def simulate_pendulum(g, T=200):
"""Semi-implicit Euler rollout of a damped pendulum."""
theta = g.new_tensor(float(theta0))
omega = g.new_tensor(float(omega0))
traj = [theta]
for _ in range(T):
alpha = -(g / L) * torch.sin(theta) - c * omega
omega = omega + dt * alpha
theta = theta + dt * omega
traj.append(theta)
return torch.stack(traj)Before trusting autodiff through a 50-step rollout, we check one local derivative against finite differences. This is a useful habit: a mismatch can reveal a bug that affects the chosen input, output, and perturbation direction. Agreement is narrower evidence. It does not validate unvisited branches, every parameter or state direction, or the physical model itself.
g_try = torch.tensor(9.0, dtype=torch.float64, requires_grad=True)
final = simulate_pendulum(g_try, T=50)[-1]
final.backward()
grad_autodiff = g_try.grad.item()
eps = 1e-5
up = simulate_pendulum(torch.tensor(9.0 + eps, dtype=torch.float64), T=50)[
-1
].item()
down = simulate_pendulum(torch.tensor(9.0 - eps, dtype=torch.float64), T=50)[
-1
].item()
grad_finite = (up - down) / (2 * eps)Autodiff gradient of final angle wrt g: -0.063487 Finite-difference gradient: -0.063487
The two values agree to several digits, which is what we want for this local check. It supports the derivative of the final angle with respect to at the chosen input after fifty composed steps; it does not certify every gradient path through the simulator. Now the identification task: simulate with the true , corrupt the angles with measurement noise, and recover by gradient descent through the simulator. This is gradient-based system identification, and it is the direct differentiable-simulator answer to the methods of Part II, Chapter 3. Where those chapters treated the simulator as a forward map to be probed, here the simulator is itself the differentiable objective, and the data does the rest.
rng = np.random.default_rng(0)
with torch.no_grad():
true_traj = simulate_pendulum(torch.tensor(g_true), T=200).numpy()
obs = true_traj + 0.01 * rng.standard_normal(true_traj.shape)
obs_t = torch.tensor(obs)
g_hat = torch.tensor(6.0, requires_grad=True)
opt = torch.optim.Adam([g_hat], lr=0.05)
losses = []
for step in range(300):
opt.zero_grad()
pred = simulate_pendulum(g_hat, T=200)
loss = torch.mean((pred - obs_t) ** 2)
loss.backward()
opt.step()
losses.append(loss.item())
g_est = g_hat.item()Estimated gravity: 9.809 (true: 9.810) Final training MSE: 9.21e-05
The estimate lands close to the true value despite the noise. Two things are worth noticing. First, the loss is the full-trajectory matching error, so gradients flow through 200 compositional steps: unrolled backpropagation in its simplest form. For this small scalar-parameter example, storing the trajectory is inexpensive; larger states or longer horizons may favor checkpointing or an adjoint. Second, this worked because the pendulum is smooth and well conditioned over the chosen horizon. For a chaotic system, useful responses include shorter training windows, frequent state feedback, regularization, or methods designed for chaotic sensitivities. An adjoint can reduce memory use, and clipping can bound an update, but neither restores the information lost to long-horizon chaotic sensitivity.
g_grid = np.linspace(4.0, 16.0, 121)
landscape = np.zeros_like(g_grid)
for i, gv in enumerate(g_grid):
with torch.no_grad():
lg = torch.mean(
(simulate_pendulum(torch.tensor(float(gv)), T=200) - obs_t) ** 2
)
landscape[i] = lg.item()

The landscape is smooth and has a single clear sampled basin around the true value. Within at spacing 0.1, the scan shows no comparably low sampled alternative, but it does not certify a global optimum outside that interval or between samples. Missing physics can leave systematic residual error or distort the parameter landscape; whether this appears as a floor, a flat direction, or local minima depends on the data, loss, and compensating parameters. Inspecting the loss landscape and residual behavior is therefore useful evidence for identification, but not a proof that the model class is correct.