It’s possible to use Nimble to fit the parameters of your simulation to observed real-world data. We’re actively working on adding more features to Nimble, but so far we support backprop into mass and inertia properties.
This document will walk you through constructing a super simple example to illustrate using backprop to learn inertia properties. We’ll again use a simple box, just like in Backprop through Physics Timesteps. We’ll try to recover what the mass of our box should’ve been in order to reach a target position after 100 timesteps of a [10, 10] force in the x and y directions.
Note: Much like in Backprop through Physics Timesteps, solving for mass like this is trivial and could be solved with simble analytical models. You can do much more complex optimization in Nimble, but this is a tutorial, so we’re keeping it simple to start out.
If you’re the type of person who prefers to just look at complete Python code, here’s
what we'll be building.
Now that we’ve learned how to tune the initial velocity in Backprop through Physics Timesteps, let’s take a look at how to do the same for mass. The code is largely the same, except for a few changes. First, we need to register with Nimble that we’re going to be learning the mass of our box body:
bound = np.zeros((1,)) # This is not used in the PyTorch API world.getWrtMass().registerNode( boxBody, nimble.neural.WrtMassBodyNodeEntryType.MASS, bound, bound)
We also need to create a learnable torch.Tensor for mass:
mass: torch.Tensor = torch.tensor([1.0], requires_grad=True) # True mass is 2.0
Finally, pass the learnable mass tensor into the timestep function:
state = nimble.timestep(world, state, action, mass)
Since mass is an optional argument, we only need to pass it in when we want to optimize the mass value.
Here’s the complete code:
import torch import numpy as np import nimblephysics as nimble # Set up the world world = nimble.simulation.World() world.setGravity([0, -9.81, 0]) world.setTimeStep(0.01) # Set up initial conditions for optimization initial_position: torch.Tensor = torch.tensor([3.0, 0.0]) initial_velocity: torch.Tensor = torch.tensor([-3.0011, 4.8577]) mass: torch.Tensor = torch.tensor([1.0], requires_grad=True) # True mass is 2.0 goal: torch.Tensor = torch.Tensor([[2.4739, 2.4768]]) # We apply nonzero force so that mass can be determined from the trajectory. action: torch.Tensor = torch.tensor([10.0, 10.0]) # Set up the box box = nimble.dynamics.Skeleton() boxJoint, boxBody = box.createTranslationalJoint2DAndBodyNodePair() world.addSkeleton(box) bound = np.zeros((1,)) # This is not used, so we just pass in zeros world.getWrtMass().registerNode( boxBody, nimble.neural.WrtMassBodyNodeEntryType.MASS, bound, bound) while True: state: torch.Tensor = torch.cat((initial_position, initial_velocity), 0) states = [state] num_timesteps = 100 for i in range(num_timesteps): state = nimble.timestep(world, state, action, mass) states.append(state) # Our loss is just the distance to the origin at the final step final_position = state[:world.getNumDofs()] # Position is the first half of the state vector loss = (goal - final_position).norm() print('loss: '+str(loss)) loss.backward() # Manually update weights using gradient descent. Wrap in torch.no_grad() # because weights have requires_grad=True, but we don't need to track this # in autograd. with torch.no_grad(): learning_rate = 0.01 mass -= learning_rate * mass.grad mass.grad = None
Automatically Initializing Inertia¶
If you have custom colliders and you’d like to automatically compute inertia values for them, it’s a straightforward process.
Recall how in Backprop through Physics Timesteps you created
boxBody: nimble.dynamics.BodyNode and
To automatically compute and set inertia from the shape of colliders, all you need to do is:
massOfBox = 1.0 centerOfMass = [0.0, 0.0, 0.0] momentOfInertia = boxShape.getShape().computeInertia(massOfBox) boxBody.setInertia(nimble.dynamics.Inertia(massOfBox, centerOfMass, momentOfInertia))