{"nbformat":4,"nbformat_minor":0,"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.7.7"},"colab":{"name":"footstep_planning.ipynb","provenance":[{"file_id":"https://github.com/RussTedrake/underactuated/blob/master/exercises/humanoids/footstep_planning/footstep_planning.ipynb","timestamp":1620248702271}],"collapsed_sections":[]}},"cells":[{"cell_type":"markdown","metadata":{"id":"N8DJ_SzSWSR-"},"source":["# Footstep Planning via Mixed-Integer Optimization"]},{"cell_type":"markdown","metadata":{"id":"F7TZWa6BWSR_"},"source":["## Notebook Setup \n","The following cell will install Drake, checkout the underactuated repository, and set up the path (only if necessary).\n","- On Google's Colaboratory, this **will take approximately two minutes** on the first time it runs (to provision the machine), but should only need to reinstall once every 12 hours. Colab will ask you to \"Reset all runtimes\"; say no to save yourself the reinstall.\n","- On Binder, the machines should already be provisioned by the time you can run this; it should return (almost) instantly.\n","\n","More details are available [here](http://underactuated.mit.edu/drake.html)."]},{"cell_type":"code","metadata":{"id":"-_HmWO9uWSSA","colab":{"base_uri":"https://localhost:8080/"},"executionInfo":{"status":"ok","timestamp":1620251756672,"user_tz":240,"elapsed":8048,"user":{"displayName":"Manmeet Bhabra","photoUrl":"","userId":"11686790358753401934"}},"outputId":"96bac093-cb96-4a9b-c282-3e1bc9abfd0f"},"source":["try:\n"," import pydrake\n"," import underactuated\n","except ImportError:\n"," !curl -s https://raw.githubusercontent.com/RussTedrake/underactuated/master/scripts/setup/jupyter_setup.py > jupyter_setup.py\n"," from jupyter_setup import setup_underactuated\n"," setup_underactuated()"],"execution_count":1,"outputs":[{"output_type":"stream","text":["/content/jupyter_setup.py:13: UserWarning: jupyter_setup.py is deprecated. Please use setup_underactuated_colab.py instead.\n"," warnings.warn(\"jupyter_setup.py is deprecated. Please use\"\n"],"name":"stderr"},{"output_type":"stream","text":["HEAD is now at 2a15bde minor reword on exercise 7.2 (#428)\n","\n","\n","WARNING: apt does not have a stable CLI interface. Use with caution in scripts.\n","\n","\n"],"name":"stdout"}]},{"cell_type":"code","metadata":{"id":"VzcioBh6WSSA","executionInfo":{"status":"ok","timestamp":1620251757882,"user_tz":240,"elapsed":9250,"user":{"displayName":"Manmeet Bhabra","photoUrl":"","userId":"11686790358753401934"}}},"source":["# python libraries\n","import numpy as np\n","import matplotlib.pyplot as plt\n","from IPython.display import HTML, display\n","from matplotlib.animation import FuncAnimation\n","from matplotlib.patches import Rectangle\n","\n","# drake imports\n","from pydrake.all import MathematicalProgram, OsqpSolver, eq, le, ge\n","from pydrake.solvers import branch_and_bound\n","\n","# increase default size matplotlib figures\n","from matplotlib import rcParams\n","rcParams['figure.figsize'] = (10, 5)"],"execution_count":2,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"_uTmMXFjWSSB"},"source":["## Problem Description\n","\n","In this notebook we will implement a simplified footstep planner for a humanoid robot: we will use the method proposed [in this paper](https://groups.csail.mit.edu/robotics-center/public_papers/Deits14a.pdf).\n","The idea is straightforward: we need to plan where to place the feet of the robot in order to move from point A to point B.\n","In doing so, we are allowed to place the feet only in certain safe areas (\"stepping stones\") and each step cannot exceed a certain length.\n","To solve this problem, we will use Mixed-Integer Quadratic Programming (MIQP).\n","\n","MIQP is a relatively nice class of optimization problems.\n","The [branch and bound algorithm](https://en.wikipedia.org/wiki/Branch_and_bound) allows to solve these problems to global optimality, whenever a solution exists, and it certifies infeasibility otherwise.\n","The drawback, however, is that computation times scale exponentially with the number of integer variables in the problem.\n","\n","You will be asked to code most of the components of this MIQP:\n","- The constraint that limit the maximum step length.\n","- The constraint for which a foot cannot be in two stepping stones at the same time.\n","- The constraint that assign each foot to a stepping stone, for each step of the robot.\n","- The objective function that minimizes the sum of the squares of the step lengths.\n","\n","Before moving on, take a look at the following videos to see the Atlas robot using this algorithm!"]},{"cell_type":"code","metadata":{"id":"SaH1vL-EWSSB","colab":{"base_uri":"https://localhost:8080/","height":655},"executionInfo":{"status":"ok","timestamp":1620251757887,"user_tz":240,"elapsed":9249,"user":{"displayName":"Manmeet Bhabra","photoUrl":"","userId":"11686790358753401934"}},"outputId":"5f4015af-47a5-4ad5-8181-c16715c804dd"},"source":["from IPython.display import IFrame, display\n","display(IFrame(src='https://www.youtube.com/embed/hGhCTPQuMy4', width='560', height='315'))\n","display(IFrame(src='https://www.youtube.com/embed/_6WQxXH-bB4', width='560', height='315'))"],"execution_count":3,"outputs":[{"output_type":"display_data","data":{"text/html":["\n"," \n"," "],"text/plain":[""]},"metadata":{"tags":[]}},{"output_type":"display_data","data":{"text/html":["\n"," \n"," "],"text/plain":[""]},"metadata":{"tags":[]}}]},{"cell_type":"markdown","metadata":{"id":"d6TCBJLtWSSC"},"source":["## Building the Terrain\n","\n","We start by constructing the terrain in which the robot will walk.\n","For simplicity, we let the stepping stones be rectangles in the plane.\n","\n","We define each stepping stone by its `center` (2d vector), its `width` (float), and its `height` (float), but we also store [its halfspace representation](https://en.wikipedia.org/wiki/Convex_polytope#Intersection_of_half-spaces).\n","In this representation, a stepping stone is described by a matrix $A$ and a vector $b$ such that a point $x \\in \\mathbb R^2$ lies inside the stepping stone iff $A x \\leq b$.\n","Each row of the matrix $A$ represents one of the four halfspaces that delimit a 2d rectangle.\n","We will need these matrices later in the notebook when we will use an MIP technique known as [the big-M method](https://optimization.mccormick.northwestern.edu/index.php/Disjunctive_inequalities)."]},{"cell_type":"code","metadata":{"id":"IJ342Un9WSSC","executionInfo":{"status":"ok","timestamp":1620251757888,"user_tz":240,"elapsed":9243,"user":{"displayName":"Manmeet Bhabra","photoUrl":"","userId":"11686790358753401934"}}},"source":["class SteppingStone(object):\n","\n"," def __init__(self, center, width, height, name=None):\n"," \n"," # store arguments\n"," self.center = center\n"," self.width = width\n"," self.height = height\n"," self.name = name\n"," \n"," # distance from center to corners\n"," c2tr = np.array([width, height]) / 2\n"," c2br = np.array([width, - height]) / 2\n"," \n"," # position of the corners\n"," self.top_right = center + c2tr\n"," self.bottom_right = center + c2br\n"," self.top_left = center - c2br\n"," self.bottom_left = center - c2tr\n"," \n"," # halfspace representation of the stepping stone\n"," self.A = np.array([[1, 0], [0, 1], [-1, 0], [0, -1]])\n"," self.b = np.concatenate([c2tr] * 2) + self.A.dot(center)\n"," \n"," def plot(self, **kwargs):\n"," return plot_rectangle(self.center, self.width, self.height, **kwargs)\n"," \n","# helper function that plots a rectangle with given center, width, and height\n","def plot_rectangle(center, width, height, ax=None, frame=.1, **kwargs):\n"," \n"," # make black the default edgecolor\n"," if not 'edgecolor' in kwargs:\n"," kwargs['edgecolor'] = 'black'\n"," \n"," # make transparent the default facecolor\n"," if not 'facecolor' in kwargs:\n"," kwargs['facecolor'] = 'none'\n"," \n"," # get current plot axis if one is not given\n"," if ax is None:\n"," ax = plt.gca()\n"," \n"," # get corners\n"," c2c = np.array([width, height]) / 2\n"," bottom_left = center - c2c\n"," top_right = center + c2c\n"," \n"," # plot rectangle\n"," rect = Rectangle(bottom_left, width, height, **kwargs)\n"," ax.add_patch(rect)\n"," \n"," # scatter fake corners to update plot limits (bad looking but compact)\n"," ax.scatter(*bottom_left, s=0)\n"," ax.scatter(*top_right, s=0)\n"," \n"," # make axis scaling equal\n"," ax.set_aspect('equal')\n"," \n"," return rect"],"execution_count":4,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"hT6LGxmyWSSD"},"source":["Now that we have the building block for the construction of the robot's terrain, we place the stepping stones.\n","The idea is to replicate the task that Atlas performs in the first video above (at time 1:24).\n","\n","The following class that takes a list of boolean values (e.g. `bool_bridge = [0, 1, 1, 0, 0, 1]`) and generates a collection of stepping stones.\n","We have the `initial` stepping stone on the left, the `goal` stepping stone on the right, the `lateral` stepping stone at the top, and a set of `bridge` stepping stones that connect the `initial` stone to the `goal`.\n","With all the `bridge` stepping stones in place, there would be an easy path for the robot to reach the `goal`.\n","However, out of the potential `len(bool_bridge)` stepping stones forming the bridge, only the ones with entry equal to `1` are actually there.\n","\n","If this description is not super clear, quickly run the next couple of cells and play with the list of booleans in the line `Terrain([1, 0, 1, 1, 0, 1]).plot()`.\n"]},{"cell_type":"code","metadata":{"id":"rwGapgGLWSSE","executionInfo":{"status":"ok","timestamp":1620251758599,"user_tz":240,"elapsed":9949,"user":{"displayName":"Manmeet Bhabra","photoUrl":"","userId":"11686790358753401934"}}},"source":["class Terrain(object):\n"," \n"," # parametric construction of the stepping stones\n"," # the following code adapts the position of each stepping\n"," # stone depending on the size and the sparsity of bool_bridge\n"," def __init__(self, bool_bridge):\n"," \n"," # ensure that bool_bridge has only boolean entries\n"," if any(i != bool(i) for i in bool_bridge):\n"," raise ValueError('Entry bool_bridge must be a list of boolean value.')\n"," \n"," # initialize internal list of stepping stones\n"," self.stepping_stones = []\n"," \n"," # add initial stepping stone to the terrain\n"," initial = self.add_stone([0, 0], 1, 1, 'initial')\n"," \n"," # add bridge stepping stones to the terrain\n"," # gap between bridge stones equals bridge stone width\n"," width_bridge = .2\n"," center = initial.bottom_right + np.array([width_bridge * 1.5, initial.height / 4])\n"," centers = [center + np.array([i * 2 * width_bridge, 0]) for i in np.where(bool_bridge)[0]]\n"," self.add_stones(\n"," centers,\n"," [width_bridge] * sum(bool_bridge),\n"," [initial.height / 2] * sum(bool_bridge),\n"," 'bridge'\n"," )\n"," \n"," # add goal stepping stone to the terrain\n"," # same dimensions of the initial one\n"," center = initial.center + np.array([initial.width + (len(bool_bridge) * 2 + 1) * width_bridge, 0])\n"," goal = self.add_stone(center, initial.width, initial.height, 'goal')\n"," \n"," # add lateral stepping stone to the terrain\n"," height = .4\n"," clearance = .1\n"," c2g = goal.center - initial.center\n"," width = initial.width + c2g[0]\n"," center = initial.center + c2g / 2 + np.array([0, (initial.height + height) / 2 + clearance])\n"," self.add_stone(center, width, height, 'lateral')\n"," \n"," # adds a stone to the internal list stepping_stones\n"," def add_stone(self, center, width, height, name=None):\n"," stone = SteppingStone(center, width, height, name=name)\n"," self.stepping_stones.append(stone)\n"," return stone\n"," \n"," # adds multiple stones to the internal list stepping_stones\n"," def add_stones(self, centers, widths, heights, name=None):\n"," \n"," # ensure that inputs have coherent size\n"," n_stones = len(centers)\n"," if n_stones != len(widths) or n_stones != len(heights):\n"," raise ValueError('Arguments have incoherent size.')\n"," \n"," # add one stone per time\n"," stones = []\n"," for i in range(n_stones):\n"," stone_name = name if name is None else name + '_' + str(i)\n"," stones.append(self.add_stone(centers[i], widths[i], heights[i], name=stone_name))\n"," \n"," return stones\n","\n"," # returns the stone with the given name\n"," # raise a ValueError if no stone has the given name\n"," def get_stone_by_name(self, name):\n","\n"," # loop through the stones\n"," # select the first with the given name\n"," for stone in self.stepping_stones:\n"," if stone.name == name:\n"," return stone\n"," \n"," # raise error if there is no stone with the given name\n"," raise ValueError(f'No stone in the terrain has name {name}.')\n"," \n"," # plots all the stones in the terrain\n"," def plot(self, title=None, **kwargs):\n"," \n"," # make light green the default facecolor\n"," if not 'facecolor' in kwargs:\n"," kwargs['facecolor'] = [0, 1, 0, .1]\n"," \n"," # plot stepping stones disposition\n"," labels = ['Stepping stone', None]\n"," for i, stone in enumerate(self.stepping_stones):\n"," stone.plot(label=labels[min(i, 1)], **kwargs)\n"," \n"," # set title\n"," plt.title(title)"],"execution_count":5,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"LdGI7b3rWSSF"},"source":["Use the next cell to play with the list of booleans and make the stones in the bridge appear and disappear.\n","You can also modify the length of the list and the position of the stepping stones will adapt automatically.\n","\n","At the end of the notebook, we will focus on two specific setups: `bool_bridge = [1, 1, 1, 1]` and `bool_bridge = [1, 1, 1, 0]`.\n","In the first case, we expect the robot to walk straight through the bridge to arrive at the goal.\n","In the second, given the strict limits we will enforce on the maximum step length, the robot will have to use the lateral stepping stone."]},{"cell_type":"code","metadata":{"id":"QepLs6UcWSSF","colab":{"base_uri":"https://localhost:8080/","height":230},"executionInfo":{"status":"ok","timestamp":1620251758600,"user_tz":240,"elapsed":9944,"user":{"displayName":"Manmeet Bhabra","photoUrl":"","userId":"11686790358753401934"}},"outputId":"3f56d330-6f41-44cb-e770-5c74d92d93d0"},"source":["Terrain([1, 0, 1, 1, 0, 1]).plot()"],"execution_count":6,"outputs":[{"output_type":"display_data","data":{"image/png":"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\n","text/plain":["