In [1]:
# this line makes Jupyter show figures in the notebook
%matplotlib inline
Vector
represents a Euclidean vector; it is implemented using a NumPy array of coordinates and a reference to the frame those coordinates are defined in.
In [2]:
class FrameError(ValueError):
"""Indicates an error related to Frames."""
class Vector:
def __init__(self, array, frame=None):
"""A vector is an array of coordinates and a frame of reference.
array: sequence of coordinates
frame: Frame object
"""
self.array = np.asarray(array)
self.frame = frame
def __str__(self):
if self.frame == None:
return '^{O}%s' % (str(self.array), )
else:
return '^{%s}%s' % (str(self.frame), str(self.array))
def __repr__(self):
return 'Frame(%s, %s)' % (str(self.frame), str(self.array))
def __add__(self, other):
if self.frame != other.frame:
raise FrameError("Vectors must be relative to the same frame.")
return Vector(self.array + other.array, self.frame)
Rotation
represents a rotation matrix, one of several kinds of transformation matrices. We'll use it as part of the implementation of Transform
.
In [3]:
class Rotation:
def __init__(self, array):
self.array = array
def __str__(self):
return 'Rotation\n%s' % str(self.array)
__repr__ = __str__
def __neg__(self):
return Rotation(-self.array)
def __mul__(self, other):
"""Apply the rotation to a Vector."""
return np.dot(self.array, other.array)
__call__ = __mul__
@staticmethod
def from_axis(axis, theta):
x, y, z = np.ravel(axis.array)
c = np.cos(theta)
u = 1.0-c
s = np.sqrt(1.0-c*c)
xu, yu, zu = x*u, y*u, z*u
v1 = [x*xu + c, x*yu - z*s, x*zu + y*s]
v2 = [x*yu + z*s, y*yu + c, y*zu - x*s]
v3 = [x*zu - y*s, y*zu + x*s, z*zu + c]
return Rotation(np.array([v1, v2, v3]))
def to_axis(self):
# return the equivalent angle-axis as (khat, theta)
pass
def transpose(self):
return Rotation(np.transpose(self.array))
inverse = transpose
A Transform
is a rotation (represented by a Rotation
object) and an origin (represented by a Vector
). The destination of the transform is the frame of the origin vector. The source of the transform is provided as an argument.
When you create a transform, it adds itself to the source frame.
In [4]:
class Transform:
"""Represents a transform from one Frame to another."""
def __init__(self, rot, org, source=None):
"""Instantiates a Transform.
rot: Rotation object
org: origin Vector
source: source Frame
"""
self.rot = rot
self.org = org
self.dest = org.frame
self.source = source
self.source.add_transform(self)
def __str__(self):
"""Returns a string representation of the Transform."""
if self.dest == None:
return '%s' % self.source.name
return '_{%s}^{O}T' % self.source.name
else:
return '_{%s}^{%s}T' % (self.source.name, self.dest.name)
__repr__ = __str__
def __mul__(self, other):
"""Applies a Transform to a Vector or Transform."""
if isinstance(other, Vector):
return self.mul_vector(other)
if isinstance(other, Transform):
return self.mul_transform(other)
__call__ = __mul__
def mul_vector(self, p):
"""Applies a Transform to a Vector.
p: Vector
Returns: Vector
"""
if p.frame != self.source:
raise FrameError(
"The frame of the vector must be the source of the transform")
return Vector(self.rot * p, self.dest) + self.org
def mul_transform(self, other):
"""Applies a Transform to another Transform.
other: Transform
Returns Transform
"""
if other.dest != self.source:
raise FrameError(
"This frames source must be the other frame's destination.")
rot = Rotation(self.rot * other.rot)
t = Transform(rot, self * other.org, other.source)
return t
def inverse(self):
"""Computes the inverse transform.
Returns: Transform
"""
irot = self.rot.inverse()
iorg = Vector(-(irot * self.org), self.source)
t = Transform(irot, iorg, self.dest)
return t
A Frame
has a name and a dictionary that includes the frames we can reach directly from this frame, and the transform that gets there.
The roster
is a list of all frames.
In [5]:
class Frame:
"""Represents a frame of reference."""
# list of Frames
roster = []
def __init__(self, name):
"""Instantiate a Frame.
name: string
"""
self.name = name
self.transforms = {}
Frame.roster.append(self)
def __str__(self):
return self.name
__repr__ = __str__
def add_transform(self, transform):
"""A frames is defined by a Transform relative to another Frame.
transform: Transform object
"""
if transform.source != self:
raise FrameError("Source of the transform must be this Frame.")
if transform.dest:
self.transforms[transform.dest] = transform
def dests(self):
"""Returns a list of the Frames we know how to Transform to."""
return self.transforms.keys()
We'll start with one frame that is not defined relative to any other frame.
In [6]:
origin = Frame('O')
origin
Out[6]:
Now we'll create Frame A
, which is defined by a transform relative to O
.
The string representation of a Frame
is in LaTex.
In [7]:
import numpy as np
theta = np.pi/2
xhat = Vector([1, 0, 0], origin)
rx = Rotation.from_axis(xhat, theta)
a = Frame('A')
t_ao = Transform(rx, xhat, a)
t_ao
Out[7]:
We can use IPython.display
to render the LaTeX:
In [8]:
from IPython.display import Math
def render(obj):
return Math(str(obj))
Here's the usual notation for the transform from A
to O
.
In [9]:
render(t_ao)
Out[9]:
Here's Frame B
, defined relative to A
by a rotation around the yhat
axis.
In [10]:
yhat = Vector([0, 1, 0], a)
ry = Rotation.from_axis(yhat, theta)
b = Frame('B')
t_ba = Transform(ry, yhat, b)
render(t_ba)
Out[10]:
A Frame C
, defined relative to B
by a rotation around the zhat
axis.
In [11]:
zhat = Vector([0, 0, 1], b)
rz = Rotation.from_axis(zhat, theta)
c = Frame('C')
t_cb = Transform(rz, zhat, c)
render(t_cb)
Out[11]:
Now let's make a vector defined in C
.
In [12]:
p_c = Vector([1, 1, 1], c)
render(p_c)
Out[12]:
And we can transform it to B
:
In [13]:
p_b = t_cb(p_c)
render(p_b)
Out[13]:
Then to A
:
In [14]:
p_a = t_ba(p_b)
render(p_a)
Out[14]:
And finally to O
.
In [15]:
p = t_ao(p_a)
render(p)
Out[15]:
If we didn't know how to get from one frame to another, we could search for the shortest path from the start frame to the destination. I'll use NetworkX.
In [16]:
import networkx as nx
The following function adds the edges from a given frame to the graph.
In [17]:
def add_edges(G, frame):
for neighbor, transform in frame.transforms.items():
G.add_edge(frame, neighbor, transform=transform)
And here's how we can make a graph from a list of frames.
In [18]:
def make_graph(frames):
G = nx.DiGraph()
for frame in frames:
add_edges(G, frame)
return G
Here's the list of frames:
In [19]:
frames = Frame.roster
frames
Out[19]:
And a dictionary that maps from each frame to its label:
In [20]:
labels = dict([(frame, str(frame)) for frame in frames])
labels
Out[20]:
So we can show the frames, and transforms between them, graphically.
In [21]:
G = make_graph(Frame.roster)
nx.draw(G, labels=labels)
In [22]:
nx.shortest_path(G, c, origin)
Out[22]:
When we apply a transform to a vector, we get a vector in a new frame.
When we apply a transform to another transform, we get a new transform that composes the two transforms.
For example cbao
, below, composes the transforms from C to B, C to A, and A to O. The result is a transform directly from C to O.
In [23]:
cbao = t_ao(t_ba(t_cb))
render(cbao)
Out[23]:
In [24]:
p = cbao(p_c)
render(p)
Out[24]:
When we create the new transform, it gets added to the network, creating shortcuts.
If we draw the network again, we can see the new links.
In [25]:
G = make_graph([origin, a, b, c])
nx.draw(G, labels=labels)
And if we find the shortest path, its shorter now.
In [26]:
nx.shortest_path(G, c, origin)
Out[26]:
We can also compute an inverse transform that goes in the other direction.
In [27]:
inv = cbao.inverse()
render(inv)
Out[27]:
And confirm that it gets us back where we started.
In [28]:
p_c = inv(p)
render(p_c)
Out[28]:
In [ ]: