Orientation of Reference Frames¶
Note
You can download this example as a Python script:
orientation.py
or Jupyter Notebook:
orientation.ipynb
.
import sympy as sm
sm.init_printing(use_latex='mathjax')
Learning Objectives¶
After completing this chapter readers will be able to:
Define a reference frame with associated unit vectors.
Define a direction cosine matrix between two oriented reference frames.
Derive direction cosine matrices for simple rotations.
Derive direction cosine matrices for successive rotations.
Manage orientation and direction cosine matrices with SymPy.
Rotate reference frames using Euler Angles.
Reference Frames¶
In the study of multibody dynamics, we are interested in observing motion of connected and interacting objects in three dimensional space. This observation necessitates the concept of a frame of reference, or reference frame. A reference frame is an abstraction which we define as the set of all points in Euclidean space that are carried by and fixed to the observer of any given state of motion. Practically speaking, it is useful to imagine your eye as an observer of motion. Your eye can orient itself in 3D space to view the motion of objects from any direction and the motion of objects will appear differently in the set of points associated with the reference frame attached to your eye depending on your eye’s orientation.
It is important to note that a reference frame is not equivalent to a coordinate system. Any number of coordinate systems (e.g., Cartesian or spherical) can be used to describe the motion of points or objects in a reference frame. The coordinate system offers a system of measurement in a reference frame. We will characterize a reference frame by a right-handed set of mutually perpendicular unit vectors that can be used to describe its orientation relative to other reference frames and we will align a Cartesian coordinate system with the unit vectors to allow for easy measurement of points fixed or moving in the reference frame.
Unit Vectors¶
Vectors have a magnitude, direction, and sense (\(\pm\)) but notably not a position. Unit vectors have a magnitude of 1. Unit vectors can be fixed, orientation-wise, to a reference frame. For a reference frame named \(N\) we will define the three mutually perpendicular unit vectors as \(\hat{n}_x, \hat{n}_y, \hat{n}_z\) where these right-handed cross products hold:
Note
Unit vectors will be designated using the “hat”, e.g. \(\hat{v}\).
These unit vectors are fixed in the reference frame \(N\). If a second reference frame \(A\) is defined, also with its set of right-handed mutually perpendicular unit vectors \(\hat{a}_x, \hat{a}_y, \hat{a}_z\) then we can establish the relative orientation of these two reference frames based on the angles among the two frames’ unit vectors.
Simple Orientations¶
Starting with two reference frames \(N\) and \(A\) in which their sets of unit vectors are initially aligned, the \(A\) frame can then be simply oriented about the common parallel \(z\) unit vectors of the two frames. We then say “reference frame \(A\) is oriented with respect to reference frame \(N\) about the shared \(z\) unit vectors through an angle \(\theta\). A visual representation of this orientation looking from the direction of the positive \(z\) unit vector is:
From the above figure these relationships between the \(\hat{a}\) and \(\hat{n}\) unit vectors can be deduced:
These equations can also be written in a matrix form:
This matrix uniquely describes the orientation between the two reference frames and so we give it its own variable:
This matrix \({}^A\mathbf{C}^N\) maps vectors expressed in the \(N\) frame to vectors expressed in the \(A\) frame. This matrix has an important property, which we will demonstrate with SymPy. Start by creating the matrix:
theta = sm.symbols('theta')
A_C_N = sm.Matrix([[sm.cos(theta), sm.sin(theta), 0],
[-sm.sin(theta), sm.cos(theta), 0],
[0, 0, 1]])
A_C_N
If we’d like the inverse relationship between the two sets of unit vectors and \({}^A\mathbf{C}^N\) is invertible, then:
SymPy can find this matrix inverse:
sm.trigsimp(A_C_N.inv())
SymPy can also find the transpose of this matrix;
A_C_N.transpose()
Notably, the inverse and the transpose are the same here. This indicates that this matrix is a special orthogonal matrix. All matrices that describe the orientation between reference frames are orthogonal matrices. Following the notation convention, this holds:
Exercise
Write \({}^A\mathbf{C}^N\) for simple rotations about both the shared \(\hat{n}_x\) and \(\hat{a}_x\) and shared \(\hat{n}_y\) and \(\hat{a}_y\) axes, rotating \(A\) with respect to \(N\) through angle \(\theta\).
Solution
For a \(x\) orientation:
For a \(y\) orientation:
Direction Cosine Matrices¶
If now \(A\) is oriented relative to \(N\) and the pairwise angles between each \(\hat{a}\) and \(\hat{n}\) mutually perpendicular unit vectors are measured, a matrix for an arbitrary orientation can be defined. For example, the figure below shows the three angles \(\alpha_{xx},\alpha_{xy},\alpha_{xz}\) relating \(\hat{a}_x\) to each \(\hat{n}\) unit vector.
Similar to the simple example above, we can write these equations if the \(\alpha_y\) and \(\alpha_z\) angles relate the \(\hat{a}_y\) and \(\hat{a}_z\) unit vectors to those of \(N\):
Since we are working with unit vectors the cosine of the angle between each pair of vectors is equivalent to the dot product between the two vectors, so this also holds:
Now the matrix relating the orientation of \(A\) with respect to \(N\) can be formed:
where
We call \({}^A\mathbf{C}^N\) the “direction cosine matrix” as a general description of the relative orientation of two reference frames. This matrix uniquely defines the relative orientation between reference frames \(N\) and \(A\), it is invertible, and its inverse is equal to the transpose, as shown above in the simple example. The determinant of the matrix is also always 1, to ensure both associated frames are right-handed. The direction cosine matrix found in the prior section for a simple orientation is a specific case of this more general definition. The direction cosine matrix is also referred to as a “rotation matrix” or “orientation matrix” in some texts.
Successive Orientations¶
Successive orientations of a series of reference frames provides a convenient way to manage orientation among more than a single pair. Below, an additional auxiliary reference frame \(B\) is shown that is simply oriented with respect to \(A\) in the same way that \(A\) is from \(N\) above in the prior section.
We know from the prior sections that we can define these two relationships between each pair of reference frames as follows:
Now, substitute (23) into (24) to get:
showing that the direction cosine matrix between \(B\) and \(N\) results from matrix multiplying the intermediate direction cosine matrices.
This holds for any series of general three dimensional successive orientations and the relation is shown in the following theorem:
where frames \(A\) through \(Z\) are succesively oriented.
Using Fig. 4 as an explicit example of this property, we start with the already defined \({}^A\mathbf{C}^N\):
A_C_N
\({}^B\mathbf{C}^A\) can then be defined similarly:
alpha = sm.symbols('alpha')
B_C_A = sm.Matrix([[sm.cos(alpha), sm.sin(alpha), 0],
[-sm.sin(alpha), sm.cos(alpha), 0],
[0, 0, 1]])
B_C_A
Finally, \({}^B\mathbf{C}^N\) can be found by matrix multiplication:
B_C_N = B_C_A*A_C_N
B_C_N
Simplifying these trigonometric expressions shows the expected result:
sm.trigsimp(B_C_N)
Exercise
If you are given \({}^B\mathbf{C}^N\) and \({}^A\mathbf{C}^N\) from the prior example, how would you find \({}^A\mathbf{C}^B\)?
Solution
SymPy Mechanics¶
As shown above, SymPy nicely handles the formulation of direction cosine
matrices, but SymPy also offers a more useful tool for tracking orientation
among reference frames. The sympy.physics.mechanics
module includes numerous objects and functions that ease the
bookkeeping and mental models needed to manage various aspects of multibody
dynamics. We will import the module as in this text:
import sympy.physics.mechanics as me
class ReferenceFrame(me.ReferenceFrame):
def __init__(self, *args, **kwargs):
kwargs.pop('latexs', None)
lab = args[0].lower()
tex = r'\hat{{{}}}_{}'
super(ReferenceFrame, self).__init__(*args,
latexs=(tex.format(lab, 'x'),
tex.format(lab, 'y'),
tex.format(lab, 'z')),
**kwargs)
me.ReferenceFrame = ReferenceFrame
sympy.physics.mechanics
includes a way to define and orient
reference frames. To create a reference frame, use
ReferenceFrame
and provide a
name for your frame as a string.
N = me.ReferenceFrame('N')
The right-handed mutually perpendicular unit vectors associated with a
reference frame are accessed with the attributes .x
,
.y
, and .z
, like so:
N.x, N.y, N.z
Using Fig. 4 again as an example, we can define all three reference frames by additionally creating \(A\) and \(B\):
A = me.ReferenceFrame('A')
B = me.ReferenceFrame('B')
N, A, B
(N, A, B)
We have already defined the direction cosine matrices for these two successive orientations. For example:
A_C_N
relates \(A\) and \(N\). ReferenceFrame
objects can be oriented
with respect to one another. The
orient_explicit()
method allows you to set the direction cosine matrix between two frames
explicitly:
N.orient_explicit(A, A_C_N)
Warning
Note very carefully what version of the direction cosine matrix you pass to
.orient_explicit()
. Check its docstring with N.orient_explicit?
.
Now you can ask for the direction cosine matrix of \(A\) with respect to
\(N\), i.e. \({}^A\mathbf{C}^N\), using the
dcm()
method:
A.dcm(N)
The direction cosine matrix of \(N\) with respect to \(A\) is found by reversing the order of the arguments:
N.dcm(A)
Exercise
Orient reference frame \(D\) with respect to \(F\) with a simple
rotation about \(y\) through angle \(\beta\) and set this
orientation with
orient_explicit()
.
Solution
beta = sm.symbols('beta')
D = me.ReferenceFrame('D')
F = me.ReferenceFrame('F')
F_C_D = sm.Matrix([[sm.cos(beta), 0, -sm.sin(beta)],
[0, 1, 0],
[sm.sin(beta), 0, sm.cos(beta)]])
F.orient_explicit(D, F_C_D.transpose())
F.dcm(D)
orient_explicit()
requires you to form the direction cosine matrix yourself, but there are also
methods that relieve you of that necessity. For example,
orient_axis()
allows you to define simple orientations between reference frames more
naturally. You provide the frame to orient from, the angle to orient through,
and the vector to orient about and the correct direction cosine matrix will be
formed. As an example, orient \(B\) with respect to \(A\) through
\(\alpha\) about \(\hat{a}_z\) by:
B.orient_axis(A, alpha, A.z)
Now the direction cosine matrix is automatically calculated and is returned
with the .dcm()
method:
B.dcm(A)
The inverse is also defined on A
:
A.dcm(B)
So each pair of reference frames are aware of its orientation partner (or partners).
Now that we’ve established orientations between \(N\) and \(A\) and \(A\) and \(B\), we might want to know the relationships between \(B\) and \(N\). Remember that matrix multiplication of the two successive direction cosine matrices provides the answer:
sm.trigsimp(B.dcm(A)*A.dcm(N))
But, the answer can also be found by calling
dcm()
with just
the two reference frames in question, \(B\) and \(N\). As long as there
is a successive path of intermediate, or auxiliary, orientations between the
two reference frames, this is sufficient for obtaining the desired direction
cosine matrix and the matrix multiplication is handled internally for you:
sm.trigsimp(B.dcm(N))
Lastly, recall the general definition of the direction cosine matrix. We showed
that the dot product of pairs of unit vectors give the entries to the direction
cosine matrix. mechanics
has a
dot()
function that can
calculate the dot product of two vectors. Using it on two of the unit vector
pairs returns the expected direction cosine matrix entry:
sm.trigsimp(me.dot(B.x, N.x))
Exercise
Orient reference frame \(D\) with respect to \(C\) with a simple rotation through angle \(\beta\) about the shared \(-y\) axis. Use the direction cosine matrix from this first orientation to set the orientation of reference frame \(E\) with respect to \(D\). Show that both pairs of reference frames have the same relative orientations.
Solution
beta = sm.symbols('beta')
C = me.ReferenceFrame('C')
D = me.ReferenceFrame('D')
E = me.ReferenceFrame('E')
D.orient_axis(C, beta, -C.y)
D.dcm(C)
E.orient_explicit(D, C.dcm(D))
E.dcm(D)
Euler Angles¶
The camera stabilization gimbal shown in Fig. 5 has three revolute joints that orient the camera \(D\) relative to the handgrip frame \(A\).
If we introduce two additional auxiliary reference frames, \(B\) and \(C\), attached to the intermediate camera frame members, we can use three successive simple orientations to go from \(A\) to \(D\). We can formulate the direction cosine matrices for the reference frames using the same technique for the successive simple orientations shown in Successive Orientations, but now our sequence of three orientations will enable us to orient \(D\) in any way possible relative to \(A\) in three dimensional space.
Watch this video to get a sense of the orientation axes for each intermediate auxiliary reference frame:
We first orient \(B\) with respect to \(A\) about the shared \(z\) unit vector through the angle \(\psi\), as shown below:
In SymPy, use ReferenceFrame
to establish the relative orientation:
psi = sm.symbols('psi')
A = me.ReferenceFrame('A')
B = me.ReferenceFrame('B')
B.orient_axis(A, psi, A.z)
B.dcm(A)
Now orient \(C\) with respect to \(B\) about their shared \(x\) unit vector through angle \(\theta\).
theta = sm.symbols('theta')
C = me.ReferenceFrame('C')
C.orient_axis(B, theta, B.x)
C.dcm(B)
Finally, orient the camera \(D\) with respect to \(C\) about their shared \(y\) unit vector through the angle \(\phi\).
phi = sm.symbols('varphi')
D = me.ReferenceFrame('D')
D.orient_axis(C, phi, C.y)
D.dcm(C)
With all of the intermediate orientations defined, when can now ask for the relationship \({}^D\mathbf{C}^A\) of the camera \(D\) relative to the handgrip frame \(A\):
D.dcm(A)
With these three successive orientations the camera can be rotated arbitrarily relative to the handgrip frame. These successive \(z\textrm{-}x\textrm{-}y\) orientations are a standard way of describing the orientation of two reference frames and are referred to as Euler Angles [1].
There are 12 valid sets of successive orientations that can arbitrarily orient one reference frame with respect to another. These are the six “Proper Euler Angles”:
and the six “Tait-Bryan Angles”:
Different sets can be more or less suitable for the kinematic nature of the
system you are describing. We will also refer to these 12 possible orientation
sets as “body fixed orientations”. As we will soon see, a rigid body and a
reference frame are synonymous from an orientation perspective and each
successive orientation rotates about a shared unit vector fixed in both of the
reference frames (or bodies), thus “body fixed orientations”. The method
orient_body_fixed()
can be used to establish the relationship between \(A\) and \(D\)
without the need to create auxiliary reference frames \(B\) and \(C\):
A = me.ReferenceFrame('A')
D = me.ReferenceFrame('D')
D.orient_body_fixed(A, (psi, theta, phi), 'zxy')
D.dcm(A)
Exercise
Euler discovered 6 of the 12 orientation sets. One of these sets is shown in this figure:
Take the acute angles between \(A\) and \(B\) to be \(\psi\),
\(B\) and \(C\) to be \(\theta\), and \(C\) and \(D\) to
be \(\varphi\). Determine what Euler angle set this is and then
calculate \({}^D\mathbf{C}^A\) using
orient_axis()
and then with
orient_body_fixed()
showing that you get the same result.
Solution
The Euler angle set is \(z\textrm{-}x\textrm{-}z\).
psi, theta, phi = sm.symbols('psi, theta, varphi')
With orient_axis()
:
A = me.ReferenceFrame('A')
B = me.ReferenceFrame('B')
C = me.ReferenceFrame('C')
D = me.ReferenceFrame('D')
B.orient_axis(A, psi, A.z)
C.orient_axis(B, theta, B.x)
D.orient_axis(C, phi, C.z)
D.dcm(A)
With orient_body_fixed()
:
A = me.ReferenceFrame('A')
D = me.ReferenceFrame('D')
D.orient_body_fixed(A, (psi, theta, phi), 'zxz')
D.dcm(A)
Alternatives for Representing Orientation¶
In the previous section, Euler-angles were used to encode the orientation of a frame or body. There are many alternative approaches to representing orientations. Three such representations, which will be used throughout this book, were already introduced:
Euler-angles themselves, which provides a minimal representation (only 3 numbers), and a relatively straightforward way to compute the change in orientation from the angular velocity (see Angular Kinematics).
the direction cosine matrix, which allow easy rotations or vectors and consecutive rotations, both via matrix multiplication,
the axis-angle representation (used in the
orient_axis()
method), which is often an intuitive way to describe the orientation for manual input, and is useful when the axis of rotation is fixed.
Each representation also has downsides. For example, the direction cosine matrix consists of nine elements; more to keep track of than three Euler angles. Furthermore, not all combinations of nine elements form a valid direction cosine matrix, so we have to be careful to check and enforce validity when writing code.
Learn more¶
One more frequently used approach to representing orientations is based on so called quaternions. Quaternions are like imaginary numbers, but with three imaginary constants: \(i\), \(j\) and \(k\). These act as described by the rule
A general quaternion \(q\) can thus be written in terms of its components \(q_0\), \(q_i\) \(q_j\), \(q_k\) which are real numbers:
The
orient_quaternion()
method enables orienting a reference frame using a quaternion in sympy:
N = me.ReferenceFrame('N')
A = me.ReferenceFrame('A')
q_0, qi, qj, qk = sm.symbols('q_0 q_i q_j q_k')
q = (q_0, qi, qj, qk)
A.orient_quaternion(N, q)
A.dcm(N)
A rotation of an angle \(\theta\) around a unit vector \(\hat{e}\) can be converted to a quaternion representation by having \(q_0 = \cos\left(\frac{\theta}{2}\right)\), and the other components equal to a factor \(\sin\left(\frac{\theta}{2}\right)\) times the components of the axis of rotation \(\hat{e}\). For example, if the rotation axis is \(\hat{n}_x\), we get:
q = (sm.cos(theta/2), sm.sin(theta/2), 0, 0)
A.orient_quaternion(N, q)
sm.trigsimp(A.dcm(N))
The length of a quaternion is the square root of the sum of the squares of its components. For a quaternion representing an orientation, this length must always be 1.
It turns out that the multiplication rules for (unit) quaternions provide an efficient way to compose multiple rotations, and to numerically integrate the orientation when given an angular velocity. Due to the interpretation related to the angle and axis representation, it is also a somewhat intuitive representation. However, the integration algorithm needs to take an additional step to ensure the quaternion always has unit length.
The representation of orientations in general, turns out to be related to an area of mathematics called Lie-groups. The theory of Lie-groups has further applications to the mechanics and control of multibody systems. An example application is finding a general method for simplifying the equations for symmetric systems, so this can be done more easily and to more systems. The Lie-group theory is not used in this book. Instead, the interested reader can look up the 3D rotation group as a starting point for further study.
Footnotes