Skip to content
LIBRARY

Orientations and coordinate conventions

MultibodyComponents models mechanical systems as networks of frames (coordinate systems) attached to bodies and connected through joints. Understanding how frames relate to each other is essential for correctly setting up models, interpreting results and specifying initial conditions.

Planar mechanics (2D)

In 2D planar mechanics, all motion is confined to the -plane. Orientation is described by a single scalar angle , measuring rotation about the -axis (out of plane).

The Frame2D connector

Every planar component communicates through Frame2D connectors. A Frame2D carries six variables, all resolved in the world frame:

VariableKindDescription
x, yPotentialPosition of the frame origin
phiPotentialOrientation angle (rad)
fx, fyFlowCut-force components
tauFlowCut-torque about the -axis

The World component

Every planar model must contain exactly one World component. It defines:

  • The origin of the inertial reference frame at with  .

  • Gravity, specified by a direction vector n (default [0, -1], i.e. pointing downward) and a magnitude g (default 9.80665 m/s²). All Body components automatically use the resulting gravitational acceleration  .

The 2D rotation matrix

The standard 2D rotation matrix for an angle is

Two functions are available for working with this matrix:

  • ori_2d(phi) returns  , which transforms a vector from the world frame to the local frame:   .

  • get_rot(sol, frame, t) returns  , which transforms a vector from the local frame to the world frame:   .

How joints affect orientation

Orientations are additive through joints. When two frames are connected by a joint, the relation between their orientation angles is

This applies to:

  • Revolute:   , where is the (variable) joint coordinate. Positions are rigidly connected:  ,  .

  • FixedRotation:   , where is a fixed parameter. Also rigidly connects positions.

When a joint is at its zero coordinate (  or  ), frame_a and frame_b coincide in both position and orientation.

Vectors in local vs. world frames

Some components specify vectors in the body-fixed (local) frame. For example, FixedTranslation(r = [1, 0]) specifies a displacement of 1 unit along the local -axis. To find where frame_b ends up in the world frame, this vector is rotated by the current orientation of frame_a:

The Body component's position and velocity state variables (r, v, phi, w) are all expressed directly in the world frame, so no transformation is needed when reading them from a solution.

Example: a simple pendulum

The PendulumTest example connects a World, a Revolute joint, a FixedTranslation rod and a Body:

World.frame_b ─→ Revolute.frame_a
                  Revolute.frame_b ─→ FixedTranslation.frame_a
                                       FixedTranslation.frame_b ─→ Body.frame_a

We can instantiate, simplify and simulate this model in Julia:

julia
using ModelingToolkit, OrdinaryDiffEq
using MultibodyComponents
using MultibodyComponents: multibody

@named model = MultibodyComponents.PlanarMechanics.examples.PendulumTest()
ssys = multibody(model)
prob = ODEProblem(ssys, [], (0.0, 3.0))
sol = solve(prob, Tsit5())

import GLMakie
render(model, sol; filename = "pendulum.gif") # Use "pendulum.mp4" for a video file
("pendulum.gif", LScene (8 plots), Scene(1 children, 0 plots))

The revolute joint angle tells us the pendulum's orientation relative to the world. At   the joint coordinate revolute.phi is zero, so the rod extends horizontally (along the world -axis, matching r = [1, 0]). As the pendulum swings under gravity, the joint angle evolves:

julia
sol(1.0, idxs = ssys.revolute.phi)
-2.8814090449015586

The body's world-frame position is available directly:

julia
sol(1.0, idxs = ssys.body.r)
2-element Vector{Float64}:
 -0.9663427596223327
 -0.25725798515399745

We can also extract a rotation matrix from any frame using get_rot:

julia
R = get_rot(sol, ssys.rod.frame_b, 1.0)
2×2 RotMatrix2{Float64} with indices SOneTo(2)×SOneTo(2):
 -0.966343  -0.257258
  0.257258  -0.966343

This is , so multiplying a vector expressed in the rod's local frame by yields the same vector in world coordinates.

Working with orientation and rotation in 3D

Orientations and rotations in 3D can be represented in multiple different ways. Components which (may) have a 3D angular state, such as Body, BodyShape and Spherical, offer the user to select the orientation representation, either Euler angles or quaternions, via the orientation_state structural parameter.

Euler angles

Euler angles represent orientation using rotations around three axes, and thus uses three numbers to represent the orientation. The benefit of this representation is that it is minimal (only three numbers used), but the drawback is that any 3-number orientation representation suffers from a kinematic singularity. This representation is beneficial when you know that the singularity will not come into play in your simulation.

Most components that may use Euler angles also allow you to select the sequence of axis around which to perform the rotations, e.g., sequence = [1,2,3] performs rotations around first, then and .

Quaternions

A quaternion represents an orientation using 4 numbers. This is a non-minimal representation, but in return it is also singularity free. MultibodyComponents uses non-unit quaternions[1] to represent orientation when orientation_state = Quaternion() is selected on components that support this option. The convention used for quaternions is , sometimes also referred to as , i.e., the real/scalar part comes first, followed by the three imaginary numbers. When quaternions are used, state variables Q_hat denote non-unit quaternions, while normalized unit quaternions are available as observable variables Q. The use of non-unit quaternions allows MultibodyComponents to integrate rotations without using dynamic state selection or introducing algebraic equations.

MultibodyComponents depends on Rotations.jl which in turn uses Quaternions.jl for quaternion computations. If you manually create quaternions using these packages, you may convert them to a vector to provide, e.g., initial conditions, using Rotations.params(Q) (see Conversion between orientation formats below).

A freely-moving body using a quaternion orientation state appears in the Getting started tutorial (the spring–mass pendulum example).

Rotation matrices

Rotation matrices represent orientation using a   matrix . These are used in the equations of multibody components and connectors, but should for the most part be invisible to the user. In particular, they should never appear as state variables after simplification.

Conversion between orientation formats

You may convert between different representations of orientation using the appropriate constructors from Rotations.jl, for example:

julia
using MultibodyComponents.Rotations
using MultibodyComponents.Rotations: params
using MultibodyComponents.Rotations.Quaternions
using LinearAlgebra

R = RotMatrix{3}(I(3))
3×3 RotMatrix3{Bool} with indices SOneTo(3)×SOneTo(3):
 1  0  0
 0  1  0
 0  0  1
julia
# Convert R to a quaternion
Q = QuatRotation(R)
3×3 QuatRotation{Float64} with indices SOneTo(3)×SOneTo(3)(QuaternionF64(1.0, 0.0, 0.0, 0.0)):
 1.0  0.0  0.0
 0.0  1.0  0.0
 0.0  0.0  1.0
julia
# Convert Q to a 4-vector
Qvec = params(Q)
4-element SVector{4, Float64} with indices SOneTo(4):
 1.0
 0.0
 0.0
 0.0
julia
# Convert R to Euler angles in the sequence XYZ
E = RotXYZ(R)
3×3 RotXYZ{Float64} with indices SOneTo(3)×SOneTo(3)(0.0, 0.0, 0.0):
 1.0  -0.0   0.0
 0.0   1.0  -0.0
 0.0   0.0   1.0
julia
# Convert E to a 3-vector
Evec = params(E)
3-element SVector{3, Float64} with indices SOneTo(3):
 0.0
 0.0
 0.0
julia
rotation_axis(R), rotation_angle(R) # Get an axis-angle representation
([1.0, 0.0, 0.0], 0.0)

Conventions for modeling

See Orientations and directions in the Getting started tutorial.

Reference

MultibodyComponents.ori_2d Function
julia
R_F_W = ori_2d(frame)
ori_2d(phi)

2D orientation matrix from angle phi (in radians). This is the inverse of get_rot.

The rotation matrix returned, , is such that when a vector , expressed in the world frame, is multiplied by as , the resulting vector is expressed in the local frame of reference :

MultibodyComponents.get_rot Function
julia
R_W_F = get_rot(sol, frame, t)

Extract a 3×3 rotation matrix ∈ SO(3) from a solution at time t.

The rotation matrix returned, , is such that when a vector expressed in the local frame of reference is multiplied by as , the resulting vector is expressed in the world frame:

This is the inverse (transpose) of the rotation matrix stored in frame connectors (e.g. ori(frame).R = get_rot(sol, frame, t)').

The columns of Double subscripts: use braces to clarify indicate are the basis vectors of the frame expressed in the world coordinate frame.

See also get_trans, get_frame.

MultibodyComponents.get_trans Function
julia
get_trans(sol, frame, t)

Extract the translational part of a frame from a solution at time t. See also get_rot, get_frame.

MultibodyComponents.get_frame Function
julia
T_W_F = get_frame(sol, frame, t)

Extract a 4×4 transformation matrix ∈ SE(3) from a solution at time t.

The transformation matrix returned, , is such that when a homogenous-coordinate vector , expressed in the local frame of reference is multiplied by as , the resulting vector is expressed in the world frame:

See also get_trans and get_rot.


  1. "Integrating Rotations using Non-Unit Quaternions", Caleb Rucker, https://par.nsf.gov/servlets/purl/10097724 ↩︎