In addition to RigidBodyDynamics, we'll be using the StaticArrays package, used throughout RigidBodyDynamics, which provides stack-allocated, fixed-size arrays:

In [1]:
using Pkg
  Updating registry at `~/.julia/registries/General`
  Updating git-repo ``
Precompiling project...
In [2]:
using RigidBodyDynamics
using LinearAlgebra
using StaticArrays

Creating a double pendulum Mechanism

We're going to create a simple Mechanism that represents a double pendulum. The Mechanism type can be thought of as an interconnection of rigid bodies and joints.

We'll start by creating a 'root' rigid body, representing the fixed world, and using it to create a new Mechanism:

In [3]:
g = -9.81 # gravitational acceleration in z-direction
world = RigidBody{Float64}("world")
doublependulum = Mechanism(world; gravity = SVector(0, 0, g))
Spanning tree:
Vertex: world (root)
No non-tree joints.

Note that the RigidBody type is parameterized on the 'scalar type', here Float64.

We'll now add a second body, called 'upper link', to the Mechanism. We'll attach it to the world with a revolute joint, with the $y$-axis as the axis of rotation. We'll start by creating a SpatialInertia, which stores the inertial properties of the new body:

In [4]:
axis = SVector(0., 1., 0.) # joint axis
I_1 = 0.333 # moment of inertia about joint axis
c_1 = -0.5 # center of mass location with respect to joint axis
m_1 = 1. # mass
frame1 = CartesianFrame3D("upper_link") # the reference frame in which the spatial inertia will be expressed
inertia1 = SpatialInertia(frame1, moment=I_1 * axis * axis', com=SVector(0, 0, c_1), mass=m_1)
SpatialInertia expressed in "upper_link":
mass: 1.0
center of mass: Point3D in "upper_link": [0.0, 0.0, -0.5]
moment of inertia:
[0.0 0.0 0.0; 0.0 0.333 0.0; 0.0 0.0 0.0]

Note that the created SpatialInertia is annotated with the frame in which it is expressed (in the form of a CartesianFrame3D). This is a common theme among RigidBodyDynamics objects. Storing frame information with the data obviates the need for the complicated variable naming conventions that are used in some other libraries to disambiguate the frame in which quantities are expressed. It also enables automated reference frame checks.

We'll now create the second body:

In [5]:
upperlink = RigidBody(inertia1)
RigidBody: "upper_link"

and a new revolute joint called 'shoulder':

In [6]:
shoulder = Joint("shoulder", Revolute(axis))
Joint "shoulder": Revolute joint with axis [0.0, 1.0, 0.0]

Creating a Joint automatically constructs two new CartesianFrame3D objects: a frame directly before the joint, and one directly after. To attach the new body to the world by this joint, we'll have to specify where the frame before the joint is located on the parent body (here, the world):

In [7]:
before_shoulder_to_world = one(Transform3D, frame_before(shoulder), default_frame(world))
Transform3D from "before_shoulder" to "world":
rotation: 0.0 rad about [1.0, 0.0, 0.0], translation: [0.0, 0.0, 0.0]

Now we can attach the upper link to the world:

In [8]:
attach!(doublependulum, world, upperlink, shoulder, joint_pose = before_shoulder_to_world)
Spanning tree:
Vertex: world (root)
  Vertex: upper_link, Edge: shoulder
No non-tree joints.

which changes the tree representation of the Mechanism.

We can attach the lower link in similar fashion:

In [9]:
l_1 = -1. # length of the upper link
I_2 = 0.333 # moment of inertia about joint axis
c_2 = -0.5 # center of mass location with respect to joint axis
m_2 = 1. # mass
inertia2 = SpatialInertia(CartesianFrame3D("lower_link"), moment=I_2 * axis * axis', com=SVector(0, 0, c_2), mass=m_2)
lowerlink = RigidBody(inertia2)
elbow = Joint("elbow", Revolute(axis))
before_elbow_to_after_shoulder = Transform3D(frame_before(elbow), frame_after(shoulder), SVector(0, 0, l_1))
attach!(doublependulum, upperlink, lowerlink, elbow, joint_pose = before_elbow_to_after_shoulder)
Spanning tree:
Vertex: world (root)
  Vertex: upper_link, Edge: shoulder
    Vertex: lower_link, Edge: elbow
No non-tree joints.

Now our double pendulum Mechanism is complete.

Note: instead of defining the Mechanism in this way, it is also possible to load in a URDF file (an XML file format used in ROS), using the parse_urdf function, e.g.:

In [10]:
urdf = joinpath(dirname(pathof(RigidBodyDynamics)), "..", "test", "urdf", "Acrobot.urdf")
Spanning tree:
Vertex: world (root)
  Vertex: upper_link, Edge: shoulder
    Vertex: lower_link, Edge: elbow
No non-tree joints.

The state of a Mechanism

A Mechanism stores the joint/rigid body layout, but no state information. State information is separated out into a MechanismState object:

In [11]:
state = MechanismState(doublependulum)
MechanismState{Float64, Float64, Float64, …}(…)

Let's first set the configurations and velocities of the joints:

In [12]:
set_configuration!(state, shoulder, 0.3)
set_configuration!(state, elbow, 0.4)
set_velocity!(state, shoulder, 1.)
set_velocity!(state, elbow, 2.);

Important: a MechanismState contains cache variables that depend on the configurations and velocities of the joints. These need to be invalidated when the configurations and velocities are changed. To do this, call

In [13]:

The joint configurations and velocities are stored as Vectors (denoted $q$ and $v$ respectively in this package) inside the MechanismState object:

In [14]:
q = configuration(state)
2-element SegmentedVector{JointID,Float64,Base.OneTo{JointID},Array{Float64,1}}:
In [15]:
v = velocity(state)
2-element SegmentedVector{JointID,Float64,Base.OneTo{JointID},Array{Float64,1}}:


We are now ready to do kinematics. Here's how you transform a point at the origin of the frame after the elbow joint to world frame:

In [16]:
transform(state, Point3D(frame_after(elbow), zero(SVector{3})), default_frame(world))
Point3D in "world": [-0.29552, 0.0, -0.955336]

Other objects like Wrenches, Twists, and SpatialInertias can be transformed in similar fashion.

You can also ask for the homogeneous transform to world:

In [17]:
transform_to_root(state, frame_after(elbow))
Transform3D from "after_elbow" to "world":
rotation: 0.7000000000000001 rad about [7.08918e-63, 1.0, 0.0], translation: [-0.29552, 0.0, -0.955336]

Or a relative transform:

In [18]:
relative_transform(state, frame_after(elbow), frame_after(shoulder))
Transform3D from "after_elbow" to "after_shoulder":
rotation: 0.39999999999999997 rad about [1.22357e-62, 1.0, 0.0], translation: [0.0, 0.0, -1.0]

and here's the center of mass of the double pendulum:

In [19]:
Point3D in "world": [-0.382695, 0.0, -0.907713]


A MechanismState can also be used to compute quantities related to the dynamics of the Mechanism. Here we compute the mass matrix:

In [20]:
2×2 Symmetric{Float64,Array{Float64,2}}:
 2.58706  0.79353
 0.79353  0.333  

Note that there is also a zero-allocation version, mass_matrix! (the ! at the end of a method is a Julia convention signifying that the function is 'in-place', i.e. modifies its input data).

We can do inverse dynamics as follows (note again that there is a non-allocating version of this method as well):

In [21]:
 = similar(velocity(state)) # the joint acceleration vector, i.e., the time derivative of the joint velocity vector v
[shoulder][1] = 1
[elbow][1] = 2
inverse_dynamics(state, )
2-element SegmentedVector{JointID,Float64,Base.OneTo{JointID},Array{Float64,1}}:


Let's simulate the double pendulum for 5 seconds, starting from the state we defined earlier. For this, we can use the basic simulate function:

In [22]:
ts, qs, vs = simulate(state, 5., Δt = 1e-3);

simulate returns a vector of times (ts) and associated joint configurations (qs) and velocities (vs). You can of course plot the trajectories using your favorite plotting package (see e.g. Plots.jl). The MeshCatMechanisms or RigidBodyTreeInspector packages can also be used for 3D animation of the double pendulum in action. See also RigidBodySim.jl for a more full-fledge simulation environment.

A lower level interface for simulation/ODE integration with more options is also available. Consult the documentation for more information.