{
"cells": [
{
"cell_type": "markdown",
"metadata": {
"nbsphinx": "hidden"
},
"source": [
"This notebook is part of the `clifford` documentation: https://clifford.readthedocs.io/."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Application to Robotic Manipulators\n",
"\n",
"This notebook is intended to expand upon the ideas in part of the presentation [Robots, Ganja & Screw Theory](https://slides.com/hugohadfield/game2020)\n",
"\n",
"## Serial manipulator\n",
"\n",
"\n",
"\n",
"[(slides)](https://slides.com/hugohadfield/game2020#/serial)\n",
"\n",
"Let's consider a 2-link 3 DOF arm. We'll model the links within the robot with rotors, which transform to the coordinate frame of the end of each link. This is very similar to the approach that would classically be taken with 4×4 matrices.\n",
"\n",
"We're going to define our class piecewise as we go along here.\n",
"To aid that, we'll write a simple base class to let us do just that.\n",
"In your own code, there's no need to do this."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"class AddMethodsAsWeGo:\n",
" @classmethod\n",
" def _add_method(cls, m):\n",
" if isinstance(m, property):\n",
" name = (m.fget or m.fset).__name__\n",
" else:\n",
" name = m.__name__\n",
" setattr(cls, name, m)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's start by defining some names for the links, and a place to store our parameters:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from enum import Enum\n",
"\n",
"class Links(Enum):\n",
" BASE = 'b'\n",
" SHOULDER = 's'\n",
" UPPER = 'u'\n",
" ELBOW = 'e'\n",
" FOREARM = 'f'\n",
" ENDPOINT = 'n'\n",
" \n",
" \n",
"class SerialRobot(AddMethodsAsWeGo):\n",
" def __init__(self, rho, l):\n",
" self.l = l\n",
" self.rho = rho\n",
" self._thetas = (0, 0, 0)\n",
"\n",
" @property\n",
" def thetas(self):\n",
" return self._thetas"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Forward kinematics\n",
"[(slides)](https://slides.com/hugohadfield/game2020#/serial-forward-rotors)\n",
"\n",
"As a reminder, we can construct rotation and translation motors as:\n",
"$$\n",
"\\begin{align}\n",
"T(a)\n",
" &= \\exp \\left(\\frac{1}{2} n_{\\infty} \\wedge a \\right) \\\\\n",
" &= 1 + \\frac{1}{2}n_{\\infty} \\wedge a \\\\\n",
"R(\\theta, \\hat B)\n",
" &= \\exp (\\frac{1}{2} \\theta \\hat B) \\\\\n",
" &= \\cos \\frac{\\theta}{2} + \\sin \\frac{\\theta}{2} \\hat B\n",
"\\end{align}\n",
"$$\n",
"\n",
"Applying these to our geometry, we get\n",
"\n",
"$$\n",
"\\begin{align}\n",
"R_{\\text{base} \\gets \\text{shoulder}} &= R(\\theta_0, e_1 \\wedge e_3) \\\\\n",
"R_{\\text{shoulder} \\gets \\text{upper arm}} &= R(\\theta_1, e_1 \\wedge e_2) \\\\\n",
"R_{\\text{upper arm} \\gets \\text{elbow}} &= T(\\rho e_1) \\\\\n",
"R_{\\text{elbow} \\gets \\text{forearm}} &= R(\\theta_2, e_1 \\wedge e_2) \\\\\n",
"R_{\\text{forearm} \\gets \\text{endpoint}} &= T(-l e_1)\\\\\n",
"\\end{align}\n",
"$$\n",
"\n",
"From which we can get the overall rotor to the frame of the endpoint, and the positions $X$ and $Y$:\n",
"\n",
"$$\n",
"\\begin{align}\n",
"R_{\\text{base} \\gets \\text{elbow}}\n",
" &= R_{\\text{base} \\gets \\text{shoulder}} R_{\\text{shoulder} \\gets \\text{upper arm}} R_{\\text{upper arm} \\gets \\text{elbow}} \\\\\n",
"X &= R_{\\text{base} \\gets \\text{elbow}} n_0 \\tilde R_{\\text{base} \\gets \\text{elbow}} \\\\\n",
"R_{\\text{base} \\gets \\text{endpoint}}\n",
" &= R_{\\text{base} \\gets \\text{shoulder}} R_{\\text{shoulder} \\gets \\text{upper arm}} R_{\\text{upper arm} \\gets \\text{elbow}} R_{\\text{elbow} \\gets \\text{forearm}} R_{\\text{forearm} \\gets \\text{endpoint}} \\\\\n",
"Y &= R_{\\text{base} \\gets \\text{endpoint}} n_0 \\tilde R_{\\text{base} \\gets \\text{endpoint}} \\\\\n",
"\\end{align}\n",
"$$\n",
"\n",
"We can write this as:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from clifford.g3c import *\n",
"from clifford.tools.g3c import generate_translation_rotor, apply_rotor\n",
"from clifford.tools.g3 import generate_rotation_rotor\n",
"\n",
"def _update_chain(rotors, a, b, c):\n",
" rotors[a, c] = rotors[a, b] * rotors[b, c]\n",
"\n",
"@SerialRobot._add_method\n",
"@SerialRobot.thetas.setter\n",
"def thetas(self, value):\n",
" theta0, theta1, theta2 = self._thetas = value\n",
" # shorthands for brevity\n",
" R = generate_rotation_rotor\n",
" T = generate_translation_rotor\n",
"\n",
" rotors = {}\n",
" rotors[Links.BASE, Links.SHOULDER] = R(theta0, e1, e3)\n",
" rotors[Links.SHOULDER, Links.UPPER] = R(theta1, e1, e2)\n",
" rotors[Links.UPPER, Links.ELBOW] = T(self.rho * e1)\n",
" rotors[Links.ELBOW, Links.FOREARM] = R(theta2, e1, e2)\n",
" rotors[Links.FOREARM, Links.ENDPOINT] = T(-self.l * e1)\n",
"\n",
" _update_chain(rotors, Links.BASE, Links.SHOULDER, Links.UPPER)\n",
" _update_chain(rotors, Links.BASE, Links.UPPER, Links.ELBOW)\n",
" _update_chain(rotors, Links.BASE, Links.ELBOW, Links.FOREARM)\n",
" _update_chain(rotors, Links.BASE, Links.FOREARM, Links.ENDPOINT)\n",
" self.rotors = rotors\n",
"\n",
"@SerialRobot._add_method\n",
"@property\n",
"def y_pos(self):\n",
" return apply_rotor(eo, self.rotors[Links.BASE, Links.ENDPOINT])\n",
"\n",
"\n",
"@SerialRobot._add_method\n",
"@property\n",
"def x_pos(self):\n",
" return apply_rotor(eo, self.rotors[Links.BASE, Links.ELBOW])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's write a renderer so we can check this all works"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from pyganja import GanjaScene\n",
"\n",
"def add_rotor(sc: GanjaScene, r, *, label=None, color=None, scale=0.1):\n",
" \"\"\" show how a rotor transforms the axes at the origin \"\"\"\n",
" y = apply_rotor(eo, r)\n",
" y_frame = [\n",
" apply_rotor(d, r)\n",
" for d in [up(scale*e1), up(scale*e2), up(scale*e3)]\n",
" ]\n",
" sc.add_object(y, label=label, color=color)\n",
" sc.add_facet([y, y_frame[0]], color=(255, 0, 0))\n",
" sc.add_facet([y, y_frame[1]], color=(0, 255, 0))\n",
" sc.add_facet([y, y_frame[2]], color=(0, 0, 255))\n",
" \n",
"\n",
"@SerialRobot._add_method\n",
"def to_scene(self):\n",
" sc = GanjaScene()\n",
" axis_scale = 0.1\n",
" link_scale = 0.05\n",
" arm_color = (192, 192, 192)\n",
"\n",
" base_obj = (up(0.2*e1)^up(0.2*e3)^up(-0.2*e1)).normal()\n",
" sc.add_object(base_obj, color=0)\n",
"\n",
" shoulder_axis = [\n",
" apply_rotor(p, self.rotors[Links.BASE, Links.UPPER])\n",
" for p in [up(axis_scale*e3), up(-axis_scale*e3)]\n",
" ]\n",
" sc.add_facet(shoulder_axis, color=(0, 0, 128))\n",
" shoulder_angle = [\n",
" apply_rotor(eo, self.rotors[Links.BASE, Links.SHOULDER]),\n",
" apply_rotor(up(axis_scale*e1), self.rotors[Links.BASE, Links.SHOULDER]),\n",
" apply_rotor(up(axis_scale*e1), self.rotors[Links.BASE, Links.UPPER]),\n",
" ]\n",
" sc.add_facet(shoulder_angle, color=(0, 0, 128))\n",
"\n",
" upper_arm_points = [\n",
" apply_rotor(up(link_scale*e3), self.rotors[Links.BASE, Links.UPPER]),\n",
" apply_rotor(up(-link_scale*e3), self.rotors[Links.BASE, Links.UPPER]),\n",
" apply_rotor(up(link_scale*e3), self.rotors[Links.BASE, Links.ELBOW]),\n",
" apply_rotor(up(-link_scale*e3), self.rotors[Links.BASE, Links.ELBOW])\n",
" ]\n",
" sc.add_facet(upper_arm_points[:3], color=arm_color)\n",
" sc.add_facet(upper_arm_points[1:], color=arm_color)\n",
"\n",
" elbow_axis = [\n",
" apply_rotor(p, self.rotors[Links.BASE, Links.ELBOW])\n",
" for p in [up(axis_scale*e3), up(-axis_scale*e3)]\n",
" ]\n",
" sc.add_facet(elbow_axis, color=(0, 0, 128))\n",
"\n",
" forearm_points = [\n",
" apply_rotor(up(link_scale*e3), self.rotors[Links.BASE, Links.FOREARM]),\n",
" apply_rotor(up(-link_scale*e3), self.rotors[Links.BASE, Links.FOREARM]),\n",
" apply_rotor(up(link_scale*e3), self.rotors[Links.BASE, Links.ENDPOINT]),\n",
" apply_rotor(up(-link_scale*e3), self.rotors[Links.BASE, Links.ENDPOINT])\n",
" ]\n",
" sc.add_facet(forearm_points[:3], color=arm_color)\n",
" sc.add_facet(forearm_points[1:], color=arm_color)\n",
"\n",
" add_rotor(sc, self.rotors[Links.BASE, Links.ELBOW], label='x', color=(128, 128, 128))\n",
" add_rotor(sc, self.rotors[Links.BASE, Links.ENDPOINT], label='y', color=(128, 128, 128))\n",
"\n",
" return sc"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can now instantiate our robot"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"serial_robot = SerialRobot(rho=1, l=0.5)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Choose a trajectory"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import math\n",
"theta_traj = [\n",
" (math.pi/6 + i*math.pi/12, math.pi/3 - math.pi/12*i, 3*math.pi/4)\n",
" for i in range(3)\n",
"]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"And plot the robot in each state, using `ipywidgets` ([docs](https://ipywidgets.readthedocs.io/)) to let us plot ganja side-by-side.\n",
"Unfortunately, `pyganja` provides no mechanism to animate these plots from python.\n",
"\n",
"