Make a Pendulum#
This guide goes through the process of making a simple pendulum
OpenSim model using the opensim Python package. The utility
of building a model programmatically is that you can automate
and parameterize how things are built.
This guide roughly mirrors OpenSim Creator’s Make a Pendulum Tutorial, which explains building the same model, but through a GUI - and with more explanations of OpenSim’s coordinate systems. We recommend going through that tutorial if you want multiple perspectives on this process.
When building a model, it’s useful to keep OpenSim’s Model & Component Architecture
in mind, because building an OpenSim model is mostly about adding components to an
opensim.Model, setting their properties/sockets, and building a physics system of
the model.
Create a New Model#
Models are built through the opensim.Model class. The first step is to make a
blank Model:
import opensim as osim
model = osim.Model()
This creates a model with nothing in it (effectively, it just creates a ground frame).
Sidenote: Saving/Visualizing Models#
A useful thing to know for the rest of this guide is you can usually save a model
during the construction process using Model.printToXML, which lets you inspect
the resulting XML (.osim), open the osim file in other software, etc. to debug
what’s happening:
import opensim as osim
model = osim.Model()
# Writes `model` to a file called `blank-model.osim`.
model.finalizeConnections() # Sometimes necessary (described below).
model.printToXML("blank-model.osim")
# Example: visualize `blank-model.osim` in its initial state.
import opynsim as opyn
import opynsim.ui
opyn_model = opyn.read_osim("blank-model.osim").compile()
opyn.ui.show_model_in_state(opyn_model, opyn_model.initial_state(realized_to=opyn.STAGE_REPORT))
# Example: visualize `blank-model.osim` in OpenSim Creator.
import subprocess
subprocess.run(["osc", "blank-model.osim"])
Sometimes model.finalizeConnections() must be called before model.printToXML() to ensure
that the model’s sockets are written to the file. This is a technical detail. When in doubt,
finalize them.
Add a Body with a WeldJoint#
The next step is to add a body that the pendulum head swings on. We do this by
adding a WeldJoint and Body to the Model:
import opensim as osim
model = osim.Model()
pendulum_base = osim.Body( # Make pendulum base body
"pendulum_base", # Name (used for lookups)
1.0, # Mass (in kg)
osim.Vec3(0), # Center of Mass (w.r.t. body frame)
osim.Inertia(1, 1, 1, 0, 0, 0) # Inertia tensor (Ixx, Iyy, Izz, Ixy, Ixz, Iyz)
)
model.addBody(pendulum_base) # Add pendulum base body to model
brick = osim.Brick( # Make brick (optional: for visualization)
osim.Vec3(0.1) # Brick half-lengths in each axis
)
pendulum_base.attachGeometry(brick) # Attach brick to pendulum base
weld_joint = osim.WeldJoint( # Make joint
"base_weld", # Name (used for lookups)
model.getGround(), # Parent frame
osim.Vec3(0, 1.0, 0), # Offset in parent frame
osim.Vec3(0), # Orientation in parent frame
pendulum_base, # Child frame
osim.Vec3(0), # Offset in child frame
osim.Vec3(0), # Orientation in child frame
)
model.addJoint(weld_joint) # Add joint to model
Typically, whenever a body is added to an OpenSim model, you should also add a joint
so that it’s clear where the body should be placed and what degrees of freedom it
should have. Otherwise, when creating the physics system (described later), OpenSim
will automatically join the body to ground with a FreeJoint, which may not be
what you want.
Add the Pendulum Head#
The next step is to add a pendulum_head body connected to pendulum_base via
an OpenSim PinJoint (osim.PinJoint).
To ensure the pendulum hangs with its base 1 m above ground and its head 0.5 m above
ground, we position the joint so that the parent frame is located at pendulum_base
(zero offset when it’s the parent) and the child frame is offset by 0.5 m along the
Y-axis (with respect to the pendulum_head). We also attach a Sphere decoration
to the head:
import opensim as osim
model = osim.Model()
pendulum_base = osim.Body( # Make pendulum base body
"pendulum_base", # Name (used for lookups)
1.0, # Mass (in kg)
osim.Vec3(0), # Center of Mass (w.r.t. body frame)
osim.Inertia(1, 1, 1, 0, 0, 0) # Inertia tensor (Ixx, Iyy, Izz, Ixy, Ixz, Iyz)
)
model.addBody(pendulum_base) # Add pendulum base body to model
brick = osim.Brick( # Make brick (optional: for visualization)
osim.Vec3(0.1) # Brick half-lengths in each axis
)
pendulum_base.attachGeometry(brick) # Attach brick to pendulum base
weld_joint = osim.WeldJoint( # Make joint
"base_weld", # Name (used for lookups)
model.getGround(), # Parent frame
osim.Vec3(0, 1.0, 0), # Offset in parent frame
osim.Vec3(0), # Orientation in parent frame
pendulum_base, # Child frame
osim.Vec3(0), # Offset in child frame
osim.Vec3(0), # Orientation in child frame
)
model.addJoint(weld_joint) # Add joint to model
pendulum_head = osim.Body( # Make pendulum head body
"pendulum_head", # Name (used for lookups)
1.0, # Mass (in kg)
osim.Vec3(0), # Center of Mass (w.r.t. body frame)
osim.Inertia(1, 1, 1, 0, 0, 0) # Inertia tensor (Ixx, Iyy, Izz, Ixy, Ixz, Iyz)
)
model.addBody(pendulum_head) # Add pendulum head body to model
sphere = osim.Sphere( # Make sphere (optional: for visualization)
0.1 # Sphere radius
)
pendulum_head.attachGeometry(sphere) # Attach sphere to pendulum head
pin_joint = osim.PinJoint( # Make pin joint
"head_pin", # Name (used for lookups)
pendulum_base, # Parent frame
osim.Vec3(0), # Offset in parent frame
osim.Vec3(0), # Parent orientation
pendulum_head, # Child frame
osim.Vec3(0, 0.5, 0), # Offset in child frame (joint is 0.5 m above head)
osim.Vec3(0), # Child orientation
)
model.addJoint(pin_joint) # Add joint to model
Build Pendulum’s Physics System#
Up to this point, you have been defining the model specification (in OPynSim, referred to as ModelSpecification). This describes the components, joints, and geometry of the system, but does not yet instantiate the underlying multibody dynamics equations.
To build the physics system, call .initSystem() on the model:
# (assumes `model` has been built, as above)
state = model.initSystem() # Initialize the physics system
This assembles the model, allocates internal memory structures, and populates a default state (where all coordinates are set to their default values, typically zero). Once built, you can manipulate the state and compute system-level kinematics and dynamics.
Plot Quantities of Interest#
With the physics system built, you can manipulate the state to evaluate quantities of interest across different configurations, such as the acceleration of the pendulum head at various angles, without needing to run a full time-stepping forward dynamics simulation.
Here is how you set a coordinate value, realize the state to acceleration, and query the head’s linear acceleration vector:
# (assumes `model` has been built and physics system built, as above)
import matplotlib.pyplot as plt
import numpy as np
# Get pin joint coordinate
coord = model.getCoordinateSet().get("head_pin_coord_0")
# Define a sample grid (-π to π, 200 steps)
angles = np.linspace(-np.pi, np.pi, 200)
# Sample accelerations of the pendulum head for each angle
accelerations_x = []
for angle in angles:
coord.setValue(state, angle)
model.realizeAcceleration(state)
accel_x = pendulum_head.getLinearAccelerationInGround(state).get(0)
accelerations_x.append(accel_x)
# Plot
plt.plot(np.degrees(angles), accelerations_x)
plt.xlabel("Pendulum Angle (degrees)")
plt.ylabel("Head X-Acceleration ($m/s^2$)")
plt.grid(True, linestyle="--", alpha=0.6)
plt.show()
Note: Because we manually set the coordinate value and realized the state statically, the velocity and acceleration terms due to motion dynamics (like initial velocities) are zero unless explicitly configured. Forward dynamics simulations (covered separately) handle time-integration.
Make the Pendulum Look Nicer#
To make the model visually polished, we can refine the dimensions of the base and head decorations and introduce a cylinder representing the pendulum’s neck.
The neck is added by defining an OffsetFrame on pendulum_head shifted halfway up (0.25 m), and attaching a Cylinder to that offset frame:
import opensim as osim
model = osim.Model()
pendulum_base = osim.Body( # Make pendulum base body
"pendulum_base", # Name (used for lookups)
1.0, # Mass (in kg)
osim.Vec3(0), # Center of Mass (w.r.t. body frame)
osim.Inertia(1, 1, 1, 0, 0, 0) # Inertia tensor (Ixx, Iyy, Izz, Ixy, Ixz, Iyz)
)
model.addBody(pendulum_base) # Add pendulum base body to model
brick = osim.Brick( # Make brick (optional: for visualization)
osim.Vec3(0.1) # Brick half-lengths in each axis
)
pendulum_base.attachGeometry(brick) # Attach brick to pendulum base
weld_joint = osim.WeldJoint( # Make joint
"base_weld", # Name (used for lookups)
model.getGround(), # Parent frame
osim.Vec3(0, 1.0, 0), # Offset in parent frame
osim.Vec3(0), # Orientation in parent frame
pendulum_base, # Child frame
osim.Vec3(0), # Offset in child frame
osim.Vec3(0), # Orientation in child frame
)
model.addJoint(weld_joint) # Add joint to model
pendulum_head = osim.Body( # Make pendulum head body
"pendulum_head", # Name (used for lookups)
1.0, # Mass (in kg)
osim.Vec3(0), # Center of Mass (w.r.t. body frame)
osim.Inertia(1, 1, 1, 0, 0, 0) # Inertia tensor (Ixx, Iyy, Izz, Ixy, Ixz, Iyz)
)
model.addBody(pendulum_head) # Add pendulum head body to model
sphere = osim.Sphere( # Make sphere (optional: for visualization)
0.1 # Sphere radius
)
pendulum_head.attachGeometry(sphere) # Attach sphere to pendulum head
neck_frame = osim.PhysicalOffsetFrame( # NEW: add a frame for the pendulum's "neck"
"neck_frame", # Name (used for lookups)
pendulum_head, # Parent frame
osim.Transform(osim.Vec3(0, -0.25, 0)) # Transform in parent frame
)
model.addComponent(neck_frame) # Add neck frame to model
cylinder = osim.Cylinder( # Make Cylinder (neck geometry)
0.01, # Cylinder radius
0.25 # Cylinder half-lengths along Y
)
neck_frame.attachGeometry(cylinder) # Add cylinder to neck frame
pin_joint = osim.PinJoint( # Make pin joint
"head_pin", # Name (used for lookups)
pendulum_base, # Parent frame
osim.Vec3(0), # Offset in parent frame
osim.Vec3(0), # Parent orientation
pendulum_head, # Child frame
osim.Vec3(0, 0.5, 0), # Offset in child frame (joint is 0.5 m above head)
osim.Vec3(0), # Child orientation
)
model.addJoint(pin_joint) # Add joint to model
Complete Script#
Here is the complete, end-to-end script building the polished pendulum model from scratch and saving it to disk:
import opensim as osim
model = osim.Model()
pendulum_base = osim.Body( # Make pendulum base body
"pendulum_base", # Name (used for lookups)
1.0, # Mass (in kg)
osim.Vec3(0), # Center of Mass (w.r.t. body frame)
osim.Inertia(1, 1, 1, 0, 0, 0) # Inertia tensor (Ixx, Iyy, Izz, Ixy, Ixz, Iyz)
)
model.addBody(pendulum_base) # Add pendulum base body to model
brick = osim.Brick( # Make brick (optional: for visualization)
osim.Vec3(0.1) # Brick half-lengths in each axis
)
pendulum_base.attachGeometry(brick) # Attach brick to pendulum base
weld_joint = osim.WeldJoint( # Make joint
"base_weld", # Name (used for lookups)
model.getGround(), # Parent frame
osim.Vec3(0, 1.0, 0), # Offset in parent frame
osim.Vec3(0), # Orientation in parent frame
pendulum_base, # Child frame
osim.Vec3(0), # Offset in child frame
osim.Vec3(0), # Orientation in child frame
)
model.addJoint(weld_joint) # Add joint to model
pendulum_head = osim.Body( # Make pendulum head body
"pendulum_head", # Name (used for lookups)
1.0, # Mass (in kg)
osim.Vec3(0), # Center of Mass (w.r.t. body frame)
osim.Inertia(1, 1, 1, 0, 0, 0) # Inertia tensor (Ixx, Iyy, Izz, Ixy, Ixz, Iyz)
)
model.addBody(pendulum_head) # Add pendulum head body to model
sphere = osim.Sphere( # Make sphere (optional: for visualization)
0.1 # Sphere radius
)
pendulum_head.attachGeometry(sphere) # Attach sphere to pendulum head
neck_frame = osim.PhysicalOffsetFrame( # NEW: add a frame for the pendulum's "neck"
"neck_frame", # Name (used for lookups)
pendulum_head, # Parent frame
osim.Transform(osim.Vec3(0, -0.25, 0)) # Transform in parent frame
)
model.addComponent(neck_frame) # Add neck frame to model
cylinder = osim.Cylinder( # Make Cylinder (neck geometry)
0.01, # Cylinder radius
0.25 # Cylinder half-lengths along Y
)
neck_frame.attachGeometry(cylinder) # Add cylinder to neck frame
pin_joint = osim.PinJoint( # Make pin joint
"head_pin", # Name (used for lookups)
pendulum_base, # Parent frame
osim.Vec3(0), # Offset in parent frame
osim.Vec3(0), # Parent orientation
pendulum_head, # Child frame
osim.Vec3(0, 0.5, 0), # Offset in child frame (joint is 0.5 m above head)
osim.Vec3(0), # Child orientation
)
model.addJoint(pin_joint) # Add joint to model
state = model.initSystem() # Building finished: initialize physics system
# --- plot quantities of interest ---
import matplotlib.pyplot as plt
import numpy as np
# Get pin joint coordinate
coord = model.getCoordinateSet().get("head_pin_coord_0")
# Define a sample grid (-π to π, 200 steps)
angles = np.linspace(-np.pi, np.pi, 200)
# Sample accelerations of the pendulum head for each angle
accelerations_x = []
for angle in angles:
coord.setValue(state, angle)
model.realizeAcceleration(state)
accel_x = pendulum_head.getLinearAccelerationInGround(state).get(0)
accelerations_x.append(accel_x)
# Plot
plt.plot(np.degrees(angles), accelerations_x)
plt.xlabel("Pendulum Angle (degrees)")
plt.ylabel("Head X-Acceleration ($m/s^2$)")
plt.grid(True, linestyle="--", alpha=0.6)
plt.show()
# --- save output ---
model.finalizeConnections()
model.printToXML("pendulum.osim") # Output: save model to `pendulum.osim`