{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Vectors, Frames, and Transforms\n", "\n", "A quick example using NetworkX to implement a basic graph algorithm.\n", "\n", "Allen B. Downey\n", "\n", "[MIT License](https://opensource.org/licenses/MIT)" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "# this line makes Jupyter show figures in the notebook\n", "%matplotlib inline" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`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." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "class FrameError(ValueError):\n", " \"\"\"Indicates an error related to Frames.\"\"\"\n", "\n", "class Vector:\n", " def __init__(self, array, frame=None):\n", " \"\"\"A vector is an array of coordinates and a frame of reference.\n", "\n", " array: sequence of coordinates\n", " frame: Frame object\n", " \"\"\"\n", " self.array = np.asarray(array)\n", " self.frame = frame\n", "\n", " def __str__(self):\n", " if self.frame == None:\n", " return '^{O}%s' % (str(self.array), )\n", " else:\n", " return '^{%s}%s' % (str(self.frame), str(self.array))\n", " \n", " def __repr__(self):\n", " return 'Frame(%s, %s)' % (str(self.frame), str(self.array))\n", "\n", " def __add__(self, other):\n", " if self.frame != other.frame:\n", " raise FrameError(\"Vectors must be relative to the same frame.\")\n", "\n", " return Vector(self.array + other.array, self.frame)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`Rotation` represents a rotation matrix, one of several kinds of transformation matrices. We'll use it as part of the implementation of `Transform`." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "class Rotation:\n", " def __init__(self, array):\n", " self.array = array\n", " \n", " def __str__(self):\n", " return 'Rotation\\n%s' % str(self.array)\n", " \n", " __repr__ = __str__\n", "\n", "\n", " def __neg__(self):\n", " return Rotation(-self.array)\n", "\n", " def __mul__(self, other):\n", " \"\"\"Apply the rotation to a Vector.\"\"\"\n", " return np.dot(self.array, other.array)\n", "\n", " __call__ = __mul__\n", "\n", " @staticmethod\n", " def from_axis(axis, theta):\n", " x, y, z = np.ravel(axis.array)\n", " c = np.cos(theta)\n", " u = 1.0-c\n", " s = np.sqrt(1.0-c*c)\n", " xu, yu, zu = x*u, y*u, z*u\n", " v1 = [x*xu + c, x*yu - z*s, x*zu + y*s]\n", " v2 = [x*yu + z*s, y*yu + c, y*zu - x*s]\n", " v3 = [x*zu - y*s, y*zu + x*s, z*zu + c]\n", " return Rotation(np.array([v1, v2, v3]))\n", "\n", " def to_axis(self):\n", " # return the equivalent angle-axis as (khat, theta)\n", " pass\n", "\n", " def transpose(self):\n", " return Rotation(np.transpose(self.array))\n", "\n", " inverse = transpose\n", " \n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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.\n", "\n", "When you create a transform, it adds itself to the source frame." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "class Transform:\n", " \"\"\"Represents a transform from one Frame to another.\"\"\"\n", "\n", " def __init__(self, rot, org, source=None):\n", " \"\"\"Instantiates a Transform.\n", "\n", " rot: Rotation object\n", " org: origin Vector\n", " source: source Frame\n", " \"\"\"\n", " self.rot = rot\n", " self.org = org\n", " self.dest = org.frame\n", " self.source = source\n", " self.source.add_transform(self)\n", "\n", " def __str__(self):\n", " \"\"\"Returns a string representation of the Transform.\"\"\"\n", " if self.dest == None:\n", " return '%s' % self.source.name\n", " return '_{%s}^{O}T' % self.source.name\n", " else:\n", " return '_{%s}^{%s}T' % (self.source.name, self.dest.name)\n", " \n", " __repr__ = __str__\n", " \n", " def __mul__(self, other):\n", " \"\"\"Applies a Transform to a Vector or Transform.\"\"\"\n", " if isinstance(other, Vector):\n", " return self.mul_vector(other)\n", "\n", " if isinstance(other, Transform):\n", " return self.mul_transform(other)\n", "\n", " __call__ = __mul__\n", "\n", " def mul_vector(self, p):\n", " \"\"\"Applies a Transform to a Vector.\n", "\n", " p: Vector\n", "\n", " Returns: Vector\n", " \"\"\"\n", " if p.frame != self.source:\n", " raise FrameError(\n", " \"The frame of the vector must be the source of the transform\")\n", " return Vector(self.rot * p, self.dest) + self.org\n", "\n", " def mul_transform(self, other):\n", " \"\"\"Applies a Transform to another Transform.\n", "\n", " other: Transform\n", "\n", " Returns Transform\n", " \"\"\"\n", " if other.dest != self.source:\n", " raise FrameError(\n", " \"This frames source must be the other frame's destination.\")\n", "\n", " rot = Rotation(self.rot * other.rot)\n", " t = Transform(rot, self * other.org, other.source)\n", " return t\n", "\n", " def inverse(self):\n", " \"\"\"Computes the inverse transform.\n", "\n", " Returns: Transform\n", " \"\"\"\n", " irot = self.rot.inverse()\n", " iorg = Vector(-(irot * self.org), self.source)\n", " t = Transform(irot, iorg, self.dest)\n", " return t\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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.\n", "\n", "The `roster` is a list of all frames." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "class Frame:\n", " \"\"\"Represents a frame of reference.\"\"\"\n", "\n", " # list of Frames\n", " roster = []\n", " \n", " def __init__(self, name):\n", " \"\"\"Instantiate a Frame.\n", "\n", " name: string\n", " \"\"\"\n", " self.name = name\n", " self.transforms = {}\n", " Frame.roster.append(self)\n", "\n", " def __str__(self): \n", " return self.name\n", " \n", " __repr__ = __str__\n", "\n", " def add_transform(self, transform):\n", " \"\"\"A frames is defined by a Transform relative to another Frame.\n", "\n", " transform: Transform object\n", " \"\"\"\n", " if transform.source != self:\n", " raise FrameError(\"Source of the transform must be this Frame.\")\n", "\n", " if transform.dest:\n", " self.transforms[transform.dest] = transform\n", "\n", " def dests(self):\n", " \"\"\"Returns a list of the Frames we know how to Transform to.\"\"\"\n", " return self.transforms.keys()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We'll start with one frame that is not defined relative to any other frame." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "O" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "origin = Frame('O')\n", "origin" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we'll create Frame `A`, which is defined by a transform relative to `O`.\n", "\n", "The string representation of a `Frame` is in LaTex." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "_{A}^{O}T" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import numpy as np\n", "\n", "theta = np.pi/2\n", "xhat = Vector([1, 0, 0], origin)\n", "rx = Rotation.from_axis(xhat, theta)\n", "a = Frame('A')\n", "t_ao = Transform(rx, xhat, a)\n", "t_ao" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can use `IPython.display` to render the LaTeX:" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "from IPython.display import Math\n", "\n", "def render(obj):\n", " return Math(str(obj))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here's the usual notation for the transform from `A` to `O`." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/latex": [ "$\\displaystyle _{A}^{O}T$" ], "text/plain": [ "" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "render(t_ao)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here's Frame `B`, defined relative to `A` by a rotation around the `yhat` axis." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/latex": [ "$\\displaystyle _{B}^{A}T$" ], "text/plain": [ "" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "yhat = Vector([0, 1, 0], a)\n", "ry = Rotation.from_axis(yhat, theta)\n", "b = Frame('B')\n", "t_ba = Transform(ry, yhat, b)\n", "render(t_ba)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A Frame `C`, defined relative to `B` by a rotation around the `zhat` axis." ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/latex": [ "$\\displaystyle _{C}^{B}T$" ], "text/plain": [ "" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "zhat = Vector([0, 0, 1], b)\n", "rz = Rotation.from_axis(zhat, theta)\n", "c = Frame('C') \n", "t_cb = Transform(rz, zhat, c)\n", "render(t_cb)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now let's make a vector defined in `C`." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/latex": [ "$\\displaystyle ^{C}[1 1 1]$" ], "text/plain": [ "" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p_c = Vector([1, 1, 1], c)\n", "render(p_c)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And we can transform it to `B`:" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/latex": [ "$\\displaystyle ^{B}[-1. 1. 2.]$" ], "text/plain": [ "" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p_b = t_cb(p_c)\n", "render(p_b)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Then to `A`:" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/latex": [ "$\\displaystyle ^{A}[2. 2. 1.]$" ], "text/plain": [ "" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p_a = t_ba(p_b)\n", "render(p_a)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And finally to `O`." ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "text/latex": [ "$\\displaystyle ^{O}[ 3. -1. 2.]$" ], "text/plain": [ "" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p = t_ao(p_a)\n", "render(p)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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." ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [], "source": [ "import networkx as nx" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The following function adds the edges from a given frame to the graph." ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [], "source": [ "def add_edges(G, frame):\n", " for neighbor, transform in frame.transforms.items():\n", " G.add_edge(frame, neighbor, transform=transform)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And here's how we can make a graph from a list of frames." ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [], "source": [ "def make_graph(frames):\n", " G = nx.DiGraph()\n", " for frame in frames:\n", " add_edges(G, frame)\n", " return G" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here's the list of frames:" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[O, A, B, C]" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "frames = Frame.roster\n", "frames" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And a dictionary that maps from each frame to its label:" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{O: 'O', A: 'A', B: 'B', C: 'C'}" ] }, "execution_count": 20, "metadata": {}, "output_type": "execute_result" } ], "source": [ "labels = dict([(frame, str(frame)) for frame in frames])\n", "labels" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So we can show the frames, and transforms between them, graphically." ] }, { "cell_type": "code", "execution_count": 21, "metadata": { "scrolled": true }, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "G = make_graph(Frame.roster)\n", "nx.draw(G, labels=labels)" ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[C, B, A, O]" ] }, "execution_count": 22, "metadata": {}, "output_type": "execute_result" } ], "source": [ "nx.shortest_path(G, c, origin)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "When we apply a transform to a vector, we get a vector in a new frame.\n", "\n", "When we apply a transform to another transform, we get a new transform that composes the two transforms.\n", "\n", "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." ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "data": { "text/latex": [ "$\\displaystyle _{C}^{O}T$" ], "text/plain": [ "" ] }, "execution_count": 23, "metadata": {}, "output_type": "execute_result" } ], "source": [ "cbao = t_ao(t_ba(t_cb))\n", "render(cbao)" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [ { "data": { "text/latex": [ "$\\displaystyle ^{O}[ 3. -1. 2.]$" ], "text/plain": [ "" ] }, "execution_count": 24, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p = cbao(p_c)\n", "render(p)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "When we create the new transform, it gets added to the network, creating shortcuts.\n", "\n", "If we draw the network again, we can see the new links." ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "G = make_graph([origin, a, b, c])\n", "nx.draw(G, labels=labels)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And if we find the shortest path, its shorter now." ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[C, O]" ] }, "execution_count": 26, "metadata": {}, "output_type": "execute_result" } ], "source": [ "nx.shortest_path(G, c, origin)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can also compute an inverse transform that goes in the other direction." ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [ { "data": { "text/latex": [ "$\\displaystyle _{O}^{C}T$" ], "text/plain": [ "" ] }, "execution_count": 27, "metadata": {}, "output_type": "execute_result" } ], "source": [ "inv = cbao.inverse()\n", "render(inv)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And confirm that it gets us back where we started." ] }, { "cell_type": "code", "execution_count": 28, "metadata": {}, "outputs": [ { "data": { "text/latex": [ "$\\displaystyle ^{C}[1. 1. 1.]$" ], "text/plain": [ "" ] }, "execution_count": 28, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p_c = inv(p)\n", "render(p_c)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.7" } }, "nbformat": 4, "nbformat_minor": 1 }