{"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":["
"]},"metadata":{"tags":[],"needs_background":"light"}}]},{"cell_type":"markdown","metadata":{"id":"7-UOsFojWSSG"},"source":["## The Mixed-Integer Optimization Problem\n","\n","It's time to write the core of the footstep planner: the MIQP.\n","We start by defining the decision variables.\n","We do this inside of a function, since we ultimately want to define a `footstep_planner` function that given a `terrain` (and a couple of additional parameters) returns the optimal plan to walk through it.\n","\n","Here is the meaning of the variables in the following function:\n","- `n_steps` is the maximum number of steps that the robot can take to reach the goal.\n","- The 2d array `position_left[t]` contains the Cartesian coordinates of the left foot at step `t`.\n","Similarly for `position_right[t]`.\n","- If the binary variable `stone_left[t, i]` is one then, at step `t`, the left foot must be placed on stone `i`.\n","Since a foot cannot be in two stepping stones at the same time, below we will also add the constraint `sum(stone_left[t]) == 1`.\n","Similarly for `stone_right[t, i]`.\n","- The binary `first_left` is one if the first step is taken with the left foot, zero if it is taken with the right foot."]},{"cell_type":"code","metadata":{"id":"Grm5WnPpWSSG","executionInfo":{"status":"ok","timestamp":1620251758600,"user_tz":240,"elapsed":9937,"user":{"displayName":"Manmeet Bhabra","photoUrl":"","userId":"11686790358753401934"}}},"source":["def add_decision_variables(prog, terrain, n_steps):\n"," \n"," # number of stones in the terrain\n"," n_stones = len(terrain.stepping_stones)\n"," \n"," # position of each foot at each step\n"," position_left = prog.NewContinuousVariables(rows=n_steps+1, cols=2)\n"," position_right = prog.NewContinuousVariables(rows=n_steps+1, cols=2)\n","\n"," # binaries that assign feet to stones for each step\n"," stone_left = prog.NewBinaryVariables(rows=n_steps+1, cols=n_stones)\n"," stone_right = prog.NewBinaryVariables(rows=n_steps+1, cols=n_stones)\n","\n"," # which foot to move first\n"," first_left = prog.NewBinaryVariables(1)[0]\n"," \n"," return position_left, position_right, stone_left, stone_right, first_left"],"execution_count":7,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"b1B_wnFMWSSH"},"source":["We let the initial position of the feet be close to the center of the `initial` stepping stone.\n","The same for the goal.\n","Then we require that the first and last entries in the `position_left` and `position_right` variables defined above agree with these values."]},{"cell_type":"code","metadata":{"id":"8fRREW5jWSSH","executionInfo":{"status":"ok","timestamp":1620251758601,"user_tz":240,"elapsed":9933,"user":{"displayName":"Manmeet Bhabra","photoUrl":"","userId":"11686790358753401934"}}},"source":["def set_initial_and_goal_position(prog, terrain, decision_variables):\n"," \n"," # unpack only decision variables needed in this function\n"," position_left, position_right = decision_variables[:2]\n"," \n"," # initial position of the feet of the robot\n"," foot_offset = np.array([0, .2])\n"," center = terrain.get_stone_by_name('initial').center\n"," initial_position_left = center\n"," initial_position_right = center - foot_offset\n","\n"," # goal position of the feet of the robot\n"," center = terrain.get_stone_by_name('goal').center\n"," goal_position_left = center\n"," goal_position_right = center - foot_offset\n","\n"," # enforce initial position of the feet\n"," prog.AddLinearConstraint(eq(position_left[0], initial_position_left))\n"," prog.AddLinearConstraint(eq(position_right[0], initial_position_right))\n","\n"," # enforce goal position of the feet\n"," prog.AddLinearConstraint(eq(position_left[-1], goal_position_left))\n"," prog.AddLinearConstraint(eq(position_right[-1], goal_position_right))"],"execution_count":8,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"yxjkgL61WSSH"},"source":["Since the robot has kinematic limits, its steps cannot be too long.\n","More specifically, at each step `t` from `0` to `n_steps`, we want each foot to lie in a square centered at the other foot.\n","The float `step_span` represents the side of this square (the whole side, not half of it).\n","\n","Your turn to code: in the following cell complete the function `relative_position_limits` to enforce the constraint we just described.\n","So far we have only unpacked the decision variables you need to constrain.\n","\n","P.s.: You might need the constructor `prog.AddLinearConstraint(le(x, y))` to enforce the constraint that the array `x` must be `l`ower than or `e`qual to the array `y`.\n","`ge` can be used similarly."]},{"cell_type":"code","metadata":{"id":"nGMIvWnXWSSI","executionInfo":{"status":"ok","timestamp":1620251758601,"user_tz":240,"elapsed":9928,"user":{"displayName":"Manmeet Bhabra","photoUrl":"","userId":"11686790358753401934"}}},"source":["def relative_position_limits(prog, n_steps, step_span, decision_variables):\n"," \n"," # unpack only decision variables needed in this function\n"," position_left, position_right = decision_variables[:2]\n"," \n"," # modify here\n","\n"," # We want to enforce that at every time, the position of the\n"," # left foot to lie in a square centered at the right foot (at that\n"," # given time) and vice-versa.\n","\n"," n_times = n_steps + 1\n","\n"," # Vector going to the corner from the center\n"," square_corner_vec = 0.5 * np.array([step_span, step_span])\n","\n"," for t in range(n_times):\n"," # Loop over all the times and enforce the constraint\n","\n"," # (1) Left Foot Box -> Constraint on the right foot\n"," prog.AddLinearConstraint(le(position_right[t,:], position_left[t,:] + square_corner_vec))\n"," prog.AddLinearConstraint(ge(position_right[t,:], position_left[t,:] - square_corner_vec))\n","\n"," # (2) Right Foot Box -> Constraint on the left foot\n"," prog.AddLinearConstraint(le(position_left[t,:], position_right[t,:] + square_corner_vec))\n"," prog.AddLinearConstraint(ge(position_left[t,:], position_right[t,:] - square_corner_vec))\n"],"execution_count":9,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"rHkMebKAWSSI"},"source":["The following constraint is somewhat involved to implement, but the idea behind it is straightforward.\n","If we take the first step with, e.g., the left foot (`first_left == 1`), then we implicitly associated each step in the plan to a foot; using zero-based indexing: 0th step left, 1st step right, 2nd step left, 3rd step right, etc.\n","The following function ensures that, if we move the left foot first, then the left foot will not move during an \"odd\" step and the right foot will not move during an \"even\" step.\n","Similarly if we decide to take the first step with the right foot.\n","\n","Here is how we implemented it.\n","At each step `t`, we require the next position of a foot to lie in a square centered at its current position.\n","We then use the binaries `first_left` and `first_right = 1 - first_left` to make the side of this square sufficiently large or zero, depending on whether the foot is allowed to move at step `t`.\n","Take a couple of minutes to double check that we got the implementation right!"]},{"cell_type":"code","metadata":{"id":"7HuW-AfaWSSJ","executionInfo":{"status":"ok","timestamp":1620251758857,"user_tz":240,"elapsed":10178,"user":{"displayName":"Manmeet Bhabra","photoUrl":"","userId":"11686790358753401934"}}},"source":["def step_sequence(prog, n_steps, step_span, decision_variables):\n"," \n"," # unpack only decision variables needed in this function\n"," position_left, position_right = decision_variables[:2]\n"," first_left = decision_variables[-1]\n"," \n"," # variable equal to one if first step is with right foot\n"," first_right = 1 - first_left\n"," \n"," # note that the step_span coincides with the maximum distance\n"," # (both horizontal and vertical) between the position of\n"," # a foot at step t and at step t + 1\n"," step_limit = np.ones(2) * step_span\n","\n"," # sequence for the robot steps implied by the binaries first_left and first_right\n"," # (could be written more compactly, but would be harder to read)\n"," for t in range(n_steps):\n","\n"," # lengths of the steps\n"," step_left = position_left[t + 1] - position_left[t]\n"," step_right = position_right[t + 1] - position_right[t]\n","\n"," # for all even steps\n"," if t % 2 == 0:\n"," limit_left = step_limit * first_left # left foot can move iff first_left\n"," limit_right = step_limit * first_right # right foot can move iff first_right\n","\n"," # for all odd steps\n"," else:\n"," limit_left = step_limit * first_right # left foot can move iff first_right\n"," limit_right = step_limit * first_left # right foot can move iff first_left\n","\n"," # constraints on left-foot relative position\n"," prog.AddLinearConstraint(le(step_left, limit_left))\n"," prog.AddLinearConstraint(ge(step_left, - limit_left))\n"," \n"," # constraints on right-foot relative position\n"," prog.AddLinearConstraint(le(step_right, limit_right))\n"," prog.AddLinearConstraint(ge(step_right, - limit_right))"],"execution_count":10,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"LvOf2O3kWSSJ"},"source":["Now it's your turn to code again.\n","As mentioned before, a foot cannot be in two stepping stones at the same time.\n","This can be enforced by requiring that, for each step `t`, the binaries `stone_left[t]` sum up to one.\n","The same for `stone_right[t]`.\n","Implement this linear constraint in the following function."]},{"cell_type":"code","metadata":{"id":"kwFhja7VWSSJ","executionInfo":{"status":"ok","timestamp":1620251758858,"user_tz":240,"elapsed":10174,"user":{"displayName":"Manmeet Bhabra","photoUrl":"","userId":"11686790358753401934"}}},"source":["def one_stone_per_foot(prog, n_steps, decision_variables):\n"," \n"," # unpack only decision variables needed in this function\n"," stone_left, stone_right = decision_variables[2:4]\n"," \n"," # modify here\n"," n_times = n_steps + 1\n","\n"," for t in range(n_times):\n"," # Enforce the step + stone condition at all times\n"," prog.AddLinearConstraint(np.sum(stone_left[t,:]) == 1)\n"," prog.AddLinearConstraint(np.sum(stone_right[t,:]) == 1)\n","\n"],"execution_count":11,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"x5HvN8bIWSSJ"},"source":["One more constraint to add: \"if the binary `stone_left[t, i]` is one, then the left foot at step `t` must lie on the `i`th stepping stone\".\n","To enforce this constraint we use the [the big-M method](https://optimization.mccormick.northwestern.edu/index.php/Disjunctive_inequalities).\n","\n","Think of each stepping stone $S_i$ as a subset of $\\mathbb R^2$.\n","Using the halfspace representation,\n","$$\n","S_i = \\{ x \\in \\mathbb R^2 : A_i x \\leq b_i \\}.\n","$$\n","Let $x_t$ be the position of, e.g., the left foot at step $t$.\n","The constraint $x_t \\in \\bigcup_i S_i$ can be enforced as follows.\n","We define a binary $\\delta_{t, i} \\in \\{0, 1\\}$ per stepping stone (these are the binaries `stone_left[t, i]` we defined above).\n","We add the constraint\n","$$\n","A_i x_t \\leq b_i + (1 - \\delta_{t, i}) M_i\n","$$\n","for all $t$ and $i$, where $M_i$ is a vector of \"sufficiently large\" positive constants.\n","Additionally, as we already did in the last cell, we require\n","$$\n","\\sum_i \\delta_{t, i} = 1\n","$$\n","for all $t$.\n","\n","Do you see how these two constraints do the job?\n","If $\\delta_{t, i} = 1$, from the last equation, we must have $\\delta_{t, j} = 0$ for all $j \\neq i$.\n","This implies\n","$$\n","A_i x_t \\leq b_i, \\quad\n","A_j x_t \\leq b_j + M_j.\n","$$\n","Therefore $x_t$ belongs to $S_i$, while, since $M_j$ is a vector of large constants, the second inequality is redundant and does not constrain the value of $x_t$.\n","\n","It's very important to notice that these constraints are linear in our decision variables ($x_t$ and $\\delta_{t, i}$).\n","Mixed-integer programs with nonlinear constraints are extremely hard problems, especially if some of the constraints are nonconvex.\n","\n","One issue left: what's the magic value for the constants $M_i$?\n","One would be tempted to put a huge number there, but unfortunately the larger the $M_i$ the harder the problem is to be solved via branch and bound.\n","In the following cell we wrote a function that returns a vector $M$ that can be used for all $i$.\n","(This is possible since our stepping stones are rectangles and they all have the same number of facets, 4, which is equal to the number of rows in $A_i$).\n","If you are not a MIP enthusiast, do not feel the need to understand the logic behind the following function."]},{"cell_type":"code","metadata":{"id":"9fVe7VK2WSSK","executionInfo":{"status":"ok","timestamp":1620251758859,"user_tz":240,"elapsed":10168,"user":{"displayName":"Manmeet Bhabra","photoUrl":"","userId":"11686790358753401934"}}},"source":["# parameter for the big-M method\n","# carefully chosen for the terrain above\n","def get_big_M(terrain):\n"," \n"," # big-M parameter for the horizontal axis\n"," initial = terrain.get_stone_by_name('initial')\n"," goal = terrain.get_stone_by_name('goal')\n"," M = [goal.center[0] - initial.center[0]]\n"," \n"," # big-M parameter for the vertical axis\n"," lateral = terrain.get_stone_by_name('lateral')\n"," M.append(lateral.top_right[1] - initial.center[1])\n"," \n"," return np.array(M * 2)"],"execution_count":12,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"sOqYQjkzWSSK"},"source":["In the following cell, for each step $t$ and stone $i$, implement the big-M constraint\n","$$\n","A_i x_t \\leq b_i + (1 - \\delta_{t, i}) M,\n","$$\n","where $x_t$ represents the variable `position_left[t]` (respectively `position_right[t]`), $\\delta_{t, i}$ is `stone_left[t, i]` (respectively `stone_right[t, i]`), and $M$ is obtained via the function `get_big_M` we just defined.\n","The arrays $A_i$ and $b_i$ are stored as attributes of the class `SteppingStone`.\n","Stones are assumed to be ordered as in the list `terrain.stepping_stones`, meaning that the $i$th stone here is `terrain.stepping_stones[i]`."]},{"cell_type":"code","metadata":{"id":"jucsj6vqWSSK","executionInfo":{"status":"ok","timestamp":1620251758859,"user_tz":240,"elapsed":10163,"user":{"displayName":"Manmeet Bhabra","photoUrl":"","userId":"11686790358753401934"}}},"source":["def foot_in_stepping_stone(prog, terrain, n_steps, decision_variables):\n"," \n"," # unpack only decision variables needed in this function\n"," position_left, position_right, stone_left, stone_right = decision_variables[:4]\n"," \n"," # big-M vector\n"," M = get_big_M(terrain)\n"," \n"," # modify here\n"," n_times = n_steps + 1\n"," n_stones = stone_left.shape[1]\n","\n"," # Get the instances of the stepping stone objects\n"," stepping_stone_objs = terrain.stepping_stones\n","\n"," for t in range(n_times):\n"," # Loop over all the times\n","\n"," for i in range(n_stones):\n"," # Loop over all the stones\n","\n"," A_i = stepping_stone_objs[i].A\n"," b_i = stepping_stone_objs[i].b\n","\n"," # (1) Left Foot\n"," x_t_left = position_left[t,:].reshape((-1,1))\n"," delta_t_i_left = stone_left[t,i]\n"," prog.AddLinearConstraint(le(A_i.dot(x_t_left)[:,0], b_i + (1 - delta_t_i_left)*M))\n","\n"," # (2) Right Foot\n"," x_t_right = position_right[t,:].reshape((-1,1))\n"," delta_t_i_right = stone_right[t,i]\n"," prog.AddLinearConstraint(le(A_i.dot(x_t_right)[:,0], b_i + (1 - delta_t_i_right)*M))\n"," \n"],"execution_count":13,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"5EiA7jbdWSSK"},"source":["The last piece of code you are asked to write.\n","In order to promote small steps, add a quadratic objective that minimizes the step length.\n","More precisely, for each step `t`, add to the cost function the square of the two norm of `position_left[t + 1] - position_left[t]` and the square of the two norm of `position_right[t + 1] - position_right[t]`.\n","To do this, you can use the `MathematicalProgram` method `AddQuadraticCost`.\n","\n","For the moment we added a dummy objective function.\n","This is just needed to let the notebook run without errors: erase it to write your own."]},{"cell_type":"code","metadata":{"id":"CLk6AHpvWSSK","executionInfo":{"status":"ok","timestamp":1620251758860,"user_tz":240,"elapsed":10159,"user":{"displayName":"Manmeet Bhabra","photoUrl":"","userId":"11686790358753401934"}}},"source":["def minimize_step_length(prog, n_steps, decision_variables):\n"," \n"," # unpack only decision variables needed in this function\n"," position_left, position_right = decision_variables[:2]\n","\n"," # dummy objective needed to let the notebook run without errors # erase here\n"," #prog.AddQuadraticCost(position_left.flatten().dot(position_left.flatten())) # erase here\n"," #prog.AddQuadraticCost(position_right.flatten().dot(position_right.flatten())) # erase here\n"," \n"," # modify here\n","\n"," n_times = n_steps + 1\n","\n"," for t in range(n_times - 1):\n"," prog.AddQuadraticCost( (position_left[t+1,:] - position_left[t,:]).flatten().dot((position_left[t+1,:] - position_left[t,:]).flatten()) )\n"," prog.AddQuadraticCost( (position_right[t+1,:] - position_right[t,:]).flatten().dot((position_right[t+1,:] - position_right[t,:]).flatten()) )\n"],"execution_count":14,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"Rkx7u-FlWSSL"},"source":["The footstep planner is ready!\n","In the following cell we just put together all the pieces we wrote so far and solve the optimization."]},{"cell_type":"code","metadata":{"id":"OZ1de5p1WSSL","executionInfo":{"status":"ok","timestamp":1620251758860,"user_tz":240,"elapsed":10153,"user":{"displayName":"Manmeet Bhabra","photoUrl":"","userId":"11686790358753401934"}}},"source":["def footstep_planner(terrain, n_steps, step_span):\n"," \n"," # initialize optimization problem\n"," prog = MathematicalProgram()\n"," \n"," # optimization variables\n"," decision_variables = add_decision_variables(prog, terrain, n_steps)\n"," \n"," # constraints\n"," set_initial_and_goal_position(prog, terrain, decision_variables)\n"," relative_position_limits(prog, n_steps, step_span, decision_variables)\n"," step_sequence(prog, n_steps, step_span, decision_variables)\n"," one_stone_per_foot(prog, n_steps, decision_variables)\n"," foot_in_stepping_stone(prog, terrain, n_steps, decision_variables)\n"," \n"," # objective function\n"," minimize_step_length(prog, n_steps, decision_variables)\n"," \n"," # solve\n"," bb = branch_and_bound.MixedIntegerBranchAndBound(prog, OsqpSolver().solver_id())\n"," result = bb.Solve()\n"," \n"," # ensure that the problem is feasible\n"," if result != result.kSolutionFound:\n"," raise ValueError('Infeasible optimization problem.')\n","\n"," # retrieve result of the optimization\n"," decision_variables_opt = [bb.GetSolution(v) for v in decision_variables]\n"," objective_opt = bb.GetOptimalCost()\n"," \n"," return decision_variables_opt, objective_opt"],"execution_count":15,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"h2vER5M-WSSL"},"source":["## Try the Footstep Planner on two Terrains"]},{"cell_type":"markdown","metadata":{"id":"vblIBmBbWSSL"},"source":["We consider the following two terrains."]},{"cell_type":"code","metadata":{"id":"dy4UA7wqWSSL","colab":{"base_uri":"https://localhost:8080/","height":551},"executionInfo":{"status":"ok","timestamp":1620251760262,"user_tz":240,"elapsed":11550,"user":{"displayName":"Manmeet Bhabra","photoUrl":"","userId":"11686790358753401934"}},"outputId":"f933956a-104a-4705-cb7b-84e13d3ce43d"},"source":["# complete bridge\n","terrain_A = Terrain([1, 1, 1, 1])\n","terrain_A.plot('Terrain A')\n","plt.show()\n","\n","# one stepping stone missing in the bridge\n","terrain_B = Terrain([1, 1, 1, 0])\n","terrain_B.plot('Terrain B')\n","plt.show()"],"execution_count":16,"outputs":[{"output_type":"display_data","data":{"image/png":"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\n","text/plain":["
"]},"metadata":{"tags":[],"needs_background":"light"}},{"output_type":"display_data","data":{"image/png":"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\n","text/plain":["
"]},"metadata":{"tags":[],"needs_background":"light"}}]},{"cell_type":"markdown","metadata":{"id":"1w8J61mkWSSL"},"source":["These are the additional parameters for the footstep planner.\n","`n_steps` is chosen to make the optimizations solvable in a few seconds, `step_span` to achieve a specific behaviour you'll see in a couple of cells."]},{"cell_type":"code","metadata":{"id":"iKJM0wuOWSSM","executionInfo":{"status":"ok","timestamp":1620251760265,"user_tz":240,"elapsed":11545,"user":{"displayName":"Manmeet Bhabra","photoUrl":"","userId":"11686790358753401934"}}},"source":["# maximum number of steps to reach the goal\n","n_steps = 8\n","\n","# side of the square that limits each step\n","step_span = .8"],"execution_count":17,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"9Mr3rxeBWSSM"},"source":["Here is an animation function that you can use to check your result."]},{"cell_type":"code","metadata":{"id":"EpAhb36sWSSM","executionInfo":{"status":"ok","timestamp":1620251760266,"user_tz":240,"elapsed":11542,"user":{"displayName":"Manmeet Bhabra","photoUrl":"","userId":"11686790358753401934"}}},"source":["def animate_footstep_plan(terrain, step_span, position_left, position_right, title=None):\n","\n"," # initialize figure for animation\n"," fig, ax = plt.subplots()\n","\n"," # plot stepping stones\n"," terrain.plot(title=title, ax=ax)\n","\n"," # initial position of the feet\n"," left_foot = ax.scatter(0, 0, color='r', zorder=3, label='Left foot')\n"," right_foot = ax.scatter(0, 0, color='b', zorder=3, label='Right foot')\n","\n"," # initial step limits\n"," left_limits = plot_rectangle(\n"," [0 ,0], # center\n"," step_span, # width\n"," step_span, # eight\n"," ax=ax,\n"," edgecolor='b',\n"," label='Left-foot limits'\n"," )\n"," right_limits = plot_rectangle(\n"," [0 ,0], # center\n"," step_span, # width\n"," step_span, # eight\n"," ax=ax,\n"," edgecolor='r',\n"," label='Right-foot limits'\n"," )\n","\n"," # misc settings\n"," plt.close()\n"," ax.legend(loc='upper left', bbox_to_anchor=(0, 1.3), ncol=2)\n","\n"," def animate(n_steps):\n","\n"," # scatter feet\n"," left_foot.set_offsets(position_left[n_steps])\n"," right_foot.set_offsets(position_right[n_steps])\n","\n"," # limits of reachable set for each foot\n"," c2c = np.ones(2) * step_span / 2\n"," right_limits.set_xy(position_left[n_steps] - c2c)\n"," left_limits.set_xy(position_right[n_steps] - c2c)\n","\n"," # create ad display animation\n"," ani = FuncAnimation(fig, animate, frames=n_steps+1, interval=1e3)\n"," display(HTML(ani.to_jshtml()))\n"," \n","def generate_and_animate_footstep_plan(terrain, n_steps, step_span, title=None):\n"," \n"," # run footstep planner\n"," decision_variables, objective = footstep_planner(terrain, n_steps, step_span)\n"," \n"," # animate result\n"," animate_footstep_plan(terrain, step_span, *decision_variables[:2], title)"],"execution_count":18,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"63DJdUW9WSSM"},"source":["Here is the animation of the footstep planner for the two benchmark terrains.\n","If you did things correctly, you should see the robot crossing the bridge for `terrain_A`.\n","Whereas the max step limit requires it to take the less efficient route at the top of the terrain (`lateral` stone) in case of `terrain_B`.\n","Note that a local solver (as opposed to branch and bound which is a global optimizer) would have had a very hard time realizing this!"]},{"cell_type":"code","metadata":{"id":"IdLovSFIWSSM","colab":{"base_uri":"https://localhost:8080/","height":897},"executionInfo":{"status":"ok","timestamp":1620251766099,"user_tz":240,"elapsed":17368,"user":{"displayName":"Manmeet Bhabra","photoUrl":"","userId":"11686790358753401934"}},"outputId":"2b5d027a-f03c-4fbb-f447-4bfe0b7ff926"},"source":["generate_and_animate_footstep_plan(terrain_A, n_steps, step_span, 'Terrain A')\n","generate_and_animate_footstep_plan(terrain_B, n_steps, step_span, 'Terrain B')"],"execution_count":19,"outputs":[{"output_type":"display_data","data":{"text/html":["\n","\n","\n","\n","\n","\n","
\n"," \n","
\n"," \n","
\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
\n","
\n"," \n"," \n"," \n"," \n"," \n"," \n","
\n","
\n","
\n","\n","\n","\n"],"text/plain":[""]},"metadata":{"tags":[]}},{"output_type":"display_data","data":{"text/html":["\n","\n","\n","\n","\n","\n","
\n"," \n","
\n"," \n","
\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
\n","
\n"," \n"," \n"," \n"," \n"," \n"," \n","
\n","
\n","
\n","\n","\n","\n"],"text/plain":[""]},"metadata":{"tags":[]}}]},{"cell_type":"markdown","metadata":{"id":"yFby6sApWSSN"},"source":["## Final Comments\n","\n","In this notebook we used a \"simple\" branch-and-bound solver (written in Drake by Hongkai Dai).\n","If you want to solve this problem for larger terrains, you might need something more advanced.\n","Drake supports two state-of-the-art solvers for mixed-integer programming (both free for academic use): [Gurobi](https://www.gurobi.com) and [Mosek](https://www.mosek.com).\n","With these, you should be able to approximately double the stepping stones in these terrains and the maximum number of steps.\n","\n","Here we used the big-M method, which is the simplest way in which we can transcribe the the footstep planning problem into an MIQP.\n","Much more efficient transcription techniques are available.\n","If you are interested, have a look at [this very nice survey paper](https://dspace.mit.edu/bitstream/handle/1721.1/96480/Vielma-2015-Mixed%20Integer%20Linear.pdf?sequence=1&isAllowed=y)."]},{"cell_type":"markdown","metadata":{"id":"CEc95fTpWSSN"},"source":["## Autograding\n","\n","You can check your work by running the following cell."]},{"cell_type":"code","metadata":{"id":"vm8jYmS4WSSN","colab":{"base_uri":"https://localhost:8080/"},"executionInfo":{"status":"ok","timestamp":1620251776393,"user_tz":240,"elapsed":27655,"user":{"displayName":"Manmeet Bhabra","photoUrl":"","userId":"11686790358753401934"}},"outputId":"dfcd5bae-cbd8-4b7a-9f8d-72ae49c3f8b0"},"source":["from underactuated.exercises.humanoids.footstep_planning.test_footstep_planning import TestFootstepPlanning\n","from underactuated.exercises.grader import Grader\n","Grader.grade_output([TestFootstepPlanning], [locals()], 'results.json')\n","Grader.print_test_results('results.json')"],"execution_count":20,"outputs":[{"output_type":"stream","text":["Total score is 16/16.\n","\n","Score for Tests that binaries imply correctly the positioning of the feet is 5/5.\n","\n","Score for Tests that the positioning of the feet minimized the sum of the is 4/4.\n","\n","Score for Tests that a foot is assigned to a single stepping stone is 3/3.\n","\n","Score for Tests the constraints on the relative position of the feet is 4/4.\n"],"name":"stdout"}]},{"cell_type":"code","metadata":{"id":"vpLpk5ibhrIa","executionInfo":{"status":"ok","timestamp":1620251776394,"user_tz":240,"elapsed":27654,"user":{"displayName":"Manmeet Bhabra","photoUrl":"","userId":"11686790358753401934"}}},"source":[""],"execution_count":20,"outputs":[]}]}