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
The Frame2D connector
Every planar component communicates through Frame2D connectors. A Frame2D carries six variables, all resolved in the world frame:
| Variable | Kind | Description |
|---|---|---|
x, y | Potential | Position of the frame origin |
phi | Potential | Orientation angle (rad) |
fx, fy | Flow | Cut-force components |
tau | Flow | Cut-torque about the |
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 magnitudeg(default9.80665m/s²). AllBodycomponents automatically use the resulting gravitational acceleration .
The 2D rotation matrix
The standard 2D rotation matrix for an angle
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:
, whereis the (variable) joint coordinate. Positions are rigidly connected: , .FixedRotation:
, whereis a fixed parameter. Also rigidly connects positions.
When a joint is at its zero coordinate (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 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_aWe can instantiate, simplify and simulate this model in 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 revolute.phi is zero, so the rod extends horizontally (along the world r = [1, 0]). As the pendulum swings under gravity, the joint angle evolves:
sol(1.0, idxs = ssys.revolute.phi)-2.8814090449015586The body's world-frame position is available directly:
sol(1.0, idxs = ssys.body.r)2-element Vector{Float64}:
-0.9663427596223327
-0.25725798515399745We can also extract a rotation matrix from any frame using get_rot:
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.966343This
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
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 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
Conversion between orientation formats
You may convert between different representations of orientation using the appropriate constructors from Rotations.jl, for example:
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# 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# 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# 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# Convert E to a 3-vector
Evec = params(E)3-element SVector{3, Float64} with indices SOneTo(3):
0.0
0.0
0.0rotation_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
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, frame of reference
MultibodyComponents.get_rot Function
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, frame of reference
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
MultibodyComponents.get_trans Function
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
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, frame of reference
"Integrating Rotations using Non-Unit Quaternions", Caleb Rucker, https://par.nsf.gov/servlets/purl/10097724 ↩︎