From ffded1c96d8364bebe9bdbf18615dc62e2a534e3 Mon Sep 17 00:00:00 2001
From: Russ Tedrake
Date: Thu, 25 Apr 2024 08:27:17 -0400
Subject: [PATCH] move remaining chapter notebooks into subdirectories fix
broken link to zmp a little progress on stochastic control notes
---
book/BUILD.bazel | 18 -
book/humanoids.html | 2 +-
book/humanoids/BUILD.bazel | 2 +-
book/index.html | 5 +-
book/limit_cycles/BUILD.bazel | 14 +
book/{ => limit_cycles}/limit_cycles.ipynb | 0
book/multibody/BUILD.bazel | 16 +
book/{ => multibody}/multibody.ipynb | 0
book/robust.html | 65 +-
book/simple_legs.ipynb | 727 ---------------------
book/stochastic.html | 17 +-
book/stochastic/BUILD.bazel | 14 +
book/{ => stochastic}/stochastic.ipynb | 0
pyproject.toml | 2 +-
14 files changed, 116 insertions(+), 766 deletions(-)
create mode 100644 book/limit_cycles/BUILD.bazel
rename book/{ => limit_cycles}/limit_cycles.ipynb (100%)
create mode 100644 book/multibody/BUILD.bazel
rename book/{ => multibody}/multibody.ipynb (100%)
delete mode 100644 book/simple_legs.ipynb
create mode 100644 book/stochastic/BUILD.bazel
rename book/{ => stochastic}/stochastic.ipynb (100%)
diff --git a/book/BUILD.bazel b/book/BUILD.bazel
index 14ba4876..6b0eebe7 100644
--- a/book/BUILD.bazel
+++ b/book/BUILD.bazel
@@ -53,11 +53,6 @@ rt_html_test(
rt_html_test(
srcs = ["stochastic.html"],
)
-rt_ipynb_test(
- name = "stochastic",
- srcs = ["stochastic.ipynb"],
- deps = ["//underactuated"],
-)
rt_html_test(
srcs = ["dp.html"],
@@ -110,11 +105,6 @@ rt_html_test(
rt_html_test(
srcs = ["limit_cycles.html"],
)
-rt_ipynb_test(
- name = "limit_cycles",
- srcs = ["limit_cycles.ipynb"],
- deps = ["//underactuated"],
-)
rt_html_test(
srcs = ["contact.html"],
@@ -140,14 +130,6 @@ rt_html_test(
srcs = ["multibody.html"],
)
-rt_ipynb_test(
- name = "multibody",
- srcs = ["multibody.ipynb"],
- deps = [
- requirement("drake"),
- ],
-)
-
rt_html_test(
srcs = ["optimization.html"],
)
diff --git a/book/humanoids.html b/book/humanoids.html
index f6138b4c..a67857ff 100644
--- a/book/humanoids.html
+++ b/book/humanoids.html
@@ -396,7 +396,7 @@ Underactuated Robotics The implementation that we used in the DARPA Robotics Challenge is described in
Tedrake15 and is available in Drake as ZmpPlanner
.
+ href="https://drake.mit.edu/doxygen_cxx/classdrake_1_1planning_1_1_zmp_planner.html">ZmpPlanner
.
The ZMP Planner
diff --git a/book/humanoids/BUILD.bazel b/book/humanoids/BUILD.bazel
index 4319413e..9b43bb9a 100644
--- a/book/humanoids/BUILD.bazel
+++ b/book/humanoids/BUILD.bazel
@@ -23,7 +23,7 @@ rt_ipynb_test(
# deps = ["//underactuated"],
#)
-# TODO(russt): Waiting for drake PR #21305 to merge.
+# TODO(russt): Waiting for next drake version bump.
#rt_ipynb_test(
# name = "zmp_planner",
# srcs = ["zmp_planner.ipynb"],
diff --git a/book/index.html b/book/index.html
index 02b02f80..71fcd5c5 100644
--- a/book/index.html
+++ b/book/index.html
@@ -251,7 +251,7 @@ Table of Contents
Chapter 6: Model Systems
with Stochasticity
- The Master Equation
+ The Master Equation
Stationary Distributions
Extended Example: The Rimless Wheel on Rough
Terrain
@@ -504,6 +504,9 @@ Table of Contents
Stochastic models
Costs and constraints for stochastic systems
Finite Markov Decision Processes
+
+ Dynamics of a Markov chain
+
Linear optimal control
Stochastic LQR
diff --git a/book/limit_cycles/BUILD.bazel b/book/limit_cycles/BUILD.bazel
new file mode 100644
index 00000000..449d226f
--- /dev/null
+++ b/book/limit_cycles/BUILD.bazel
@@ -0,0 +1,14 @@
+# -*- mode: python -*-
+# vi: set ft=python :
+
+# Copyright 2020-2022 Massachusetts Institute of Technology.
+# Licensed under the BSD 3-Clause License. See LICENSE.TXT for details.
+
+load("@pip_deps//:requirements.bzl", "requirement")
+load("//book/htmlbook/tools/jupyter:defs.bzl", "rt_ipynb_test")
+
+rt_ipynb_test(
+ name = "limit_cycles",
+ srcs = ["limit_cycles.ipynb"],
+ deps = ["//underactuated"],
+)
diff --git a/book/limit_cycles.ipynb b/book/limit_cycles/limit_cycles.ipynb
similarity index 100%
rename from book/limit_cycles.ipynb
rename to book/limit_cycles/limit_cycles.ipynb
diff --git a/book/multibody/BUILD.bazel b/book/multibody/BUILD.bazel
new file mode 100644
index 00000000..fbd1a0d8
--- /dev/null
+++ b/book/multibody/BUILD.bazel
@@ -0,0 +1,16 @@
+# -*- mode: python -*-
+# vi: set ft=python :
+
+# Copyright 2020-2022 Massachusetts Institute of Technology.
+# Licensed under the BSD 3-Clause License. See LICENSE.TXT for details.
+
+load("@pip_deps//:requirements.bzl", "requirement")
+load("//book/htmlbook/tools/jupyter:defs.bzl", "rt_ipynb_test")
+
+rt_ipynb_test(
+ name = "multibody",
+ srcs = ["multibody.ipynb"],
+ deps = [
+ requirement("drake"),
+ ],
+)
diff --git a/book/multibody.ipynb b/book/multibody/multibody.ipynb
similarity index 100%
rename from book/multibody.ipynb
rename to book/multibody/multibody.ipynb
diff --git a/book/robust.html b/book/robust.html
index 60083866..338df135 100644
--- a/book/robust.html
+++ b/book/robust.html
@@ -213,19 +213,68 @@ The Bellman equation.
-
- Discounted cost. Infinite-horizon average cost.
+ Finalize if I want to use p_n(s) or \Pr_n(s) here, and use it throughout the
+ notes.
+
+ We already had quick preview into stochastic optimal control in one of the cases
+ where it is particularly easy: finite Markov Decision
+ Processes (MDPs) .
+
+ Recall that in this setting, where the state space and action spaces are finite,
+ we write the dynamics completely in terms of the transition probabilities
+ \[p(s[n+1] = s' | s[n] = s, a[n] = a). \] Conveniently, we can represent the
+ entire state probability distribution as a vector, $p_n(s[n] = s),$ and write the
+ dynamics as a transition matrix for each action: $$T_{i,j}(a) = p(s_j | s_i, a).$$
+ The stochastic dynamics of the master equation
+ are then given by $$p_{n+1}(s') = {\bf T}(a[n]) p_{n}(s).$$ Take a moment to
+ appreciate this -- if we make a discrete-state / discrete-action approximation of
+ even the most complicated and rich nonlinear dynamical system, then the dynamics of
+ the state probability distribution are defined by an (action-dependent) linear map.
+ Note that ${\bf T}$ is not an arbitrary matrix of real numbers but rather a (left)
+ "stochastic matrix ":
+ we always have the $T_{ij} \in [0, 1]$ and $\sum_{i}T_{ij} = 1.$
+
+ Dynamics of a Markov chain
+
+
+ The dynamics of a closed-loop system (e.g. with no action inputs, or a fixed
+ policy, $\pi$), then the MDP equations reduce back to the simpler form of a Markov
+ chain: $$p_{n+1}(s') = {\bf T} p_{n}(s).$$ To evaluate the long-term dynamics of a
+ Markov chain, we have $$p_{n}(s') = {\bf T}^n p_{0}(s).$$ The eigenvalues of a
+ stochastic matrix are also bounded: $\forall i, \lambda_i \in [0, 1].$ Moreover,
+ it can be shown that every stochastic matrix has at least one eigenvalue of $1$;
+ any eigenvector corresponding to an eigenvalue of 1 is a stationary
+ distribution (a fixed point of the master equation) of the Markov chain. If
+ the Markov chain is "irreducible" and "aperiodic", then the stationary
+ distribution is unique and the Markov chain converges to it from any initial
+ condition.
+
+ Discretized cubic polynomial w/ noise
+
+ I used the discrete-time approximation of cubic polynomial with Gaussian noise as an
+ example when we were first building our intuition about stochastic dynamics.
+ Let's now make a finite-state Markov chain approximation of those dynamics, by
+ discretizing the state space in 100 bins over the domain $x \ in [-2, 2].$
+
+ Let's examine the eigenvalues of this stochastic transition matrix...
+
+ Finish coding up the example...
+
+
+ example: discretized cubic polynomial (bistable version) + additive Gaussian noise
+ Metastability
- We already had quick preview into stochastic optimal control in one of
- the cases where it is particularly easy: finite
- Markov Decision Processes (MDPs) .
+ example: discretized cubic polynomial negated. Metastability. Rimless wheel on rough terrain.
- Perhaps a example of something other than expected-value cost
- (worst case, e.g. Katie's metastability?)
+ Discounted cost. Infinite-horizon average cost.
+
+ Perhaps a example of something other than expected-value cost
+ (worst case, e.g. Katie's metastability?)
+
Linear optimal control
diff --git a/book/simple_legs.ipynb b/book/simple_legs.ipynb
deleted file mode 100644
index d26f4e02..00000000
--- a/book/simple_legs.ipynb
+++ /dev/null
@@ -1,727 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "This notebook provides examples to go along with the [textbook](https://underactuated.csail.mit.edu/simple_legs.html). I recommend having both windows open, side-by-side!\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "import copy\n",
- "\n",
- "import matplotlib.pyplot as plt\n",
- "import mpld3\n",
- "import numpy as np\n",
- "from IPython.display import display\n",
- "from pydrake.all import (\n",
- " BasicVector,\n",
- " ConstantVectorSource,\n",
- " DiagramBuilder,\n",
- " LeafSystem,\n",
- " LogVectorOutput,\n",
- " MeshcatVisualizer,\n",
- " PortDataType,\n",
- " PublishEvent,\n",
- " PyPlotVisualizer,\n",
- " SceneGraph,\n",
- " Simulator,\n",
- " StartMeshcat,\n",
- " UnrestrictedUpdateEvent,\n",
- " WitnessFunctionDirection,\n",
- " namedview,\n",
- ")\n",
- "from pydrake.examples import (\n",
- " CompassGait,\n",
- " CompassGaitGeometry,\n",
- " RimlessWheel,\n",
- " RimlessWheelGeometry,\n",
- " RimlessWheelParams,\n",
- ")\n",
- "\n",
- "from underactuated import running_as_notebook\n",
- "from underactuated.jupyter import AdvanceToAndVisualize\n",
- "\n",
- "if running_as_notebook:\n",
- " mpld3.enable_notebook()"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "# Start the visualizer (run this cell only once, each instance consumes a port)\n",
- "meshcat = StartMeshcat()"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# The Rimless Wheel"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "def rimless_wheel(slope=0.08, initial_angle=0, initial_angular_velocity=5.0):\n",
- " params = RimlessWheelParams()\n",
- " params.set_slope(slope)\n",
- "\n",
- " builder = DiagramBuilder()\n",
- " rimless_wheel = builder.AddSystem(RimlessWheel())\n",
- " scene_graph = builder.AddSystem(SceneGraph())\n",
- " RimlessWheelGeometry.AddToBuilder(\n",
- " builder,\n",
- " rimless_wheel.get_floating_base_state_output_port(),\n",
- " params,\n",
- " scene_graph,\n",
- " )\n",
- " visualizer = MeshcatVisualizer.AddToBuilder(builder, scene_graph, meshcat)\n",
- " meshcat.Set2dRenderMode(xmin=-2, xmax=14, ymin=-2, ymax=3)\n",
- "\n",
- " diagram = builder.Build()\n",
- " simulator = Simulator(diagram)\n",
- "\n",
- " context = simulator.get_mutable_context()\n",
- "\n",
- " diagram.GetMutableSubsystemContext(rimless_wheel, context).get_numeric_parameter(\n",
- " 0\n",
- " ).set_slope(slope)\n",
- " context.SetAccuracy(1e-4)\n",
- " context.SetContinuousState([initial_angle, initial_angular_velocity])\n",
- " simulator.Initialize()\n",
- " simulator.set_target_realtime_rate(1.0 if running_as_notebook else 0.0)\n",
- " visualizer.StartRecording()\n",
- " simulator.AdvanceTo(5.0 if running_as_notebook else 0.1)\n",
- " visualizer.PublishRecording()\n",
- "\n",
- "\n",
- "rimless_wheel()"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "rimless_wheel(initial_angular_velocity=5.0)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "rimless_wheel(initial_angular_velocity=10.0)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "rimless_wheel(initial_angular_velocity=0.95)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "rimless_wheel(initial_angular_velocity=-5.0)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "rimless_wheel(initial_angular_velocity=-4.8)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Here is a little interactive plot to allow you to visualize the trajectories of the rimless wheel as you vary the initial velocity."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "# TODO(russt): Port to meshcat and make it interactive, once again.\n",
- "\n",
- "\n",
- "def rimless_wheel_return_map(initial_angular_velocity=1.5, duration=1.5):\n",
- " rimless_wheel = RimlessWheel()\n",
- " simulator = Simulator(rimless_wheel)\n",
- " context = simulator.get_mutable_context()\n",
- " params = context.get_numeric_parameter(0)\n",
- " qmin = params.slope() - rimless_wheel.calc_alpha(params) - 0.1\n",
- " qmax = params.slope() + rimless_wheel.calc_alpha(params) + 0.1\n",
- " qdmin = -2\n",
- " qdmax = 2\n",
- "\n",
- " context.SetAccuracy(1e-2)\n",
- " integrator = simulator.get_mutable_integrator()\n",
- "\n",
- " fig, ax = plt.subplots(figsize=(10, 6))\n",
- "\n",
- " # TODO(russt): make the slope interactive, too.\n",
- " def simulate(initial_angular_velocity=1.5, duration=1.5):\n",
- " rimless_wheel.SetDefaultContext(context)\n",
- " context.SetTime(0.0)\n",
- " if initial_angular_velocity >= 0:\n",
- " initial_angle = params.slope() - rimless_wheel.calc_alpha(params)\n",
- " else:\n",
- " initial_angle = params.slope() + rimless_wheel.calc_alpha(params)\n",
- " if initial_angular_velocity == 0:\n",
- " # Set double_support = True.\n",
- " context.get_mutable_abstract_state(0).set_value(True)\n",
- "\n",
- " context.SetContinuousState([initial_angle, initial_angular_velocity])\n",
- "\n",
- " integrator.StartDenseIntegration()\n",
- " simulator.Initialize()\n",
- " simulator.AdvanceTo(duration if running_as_notebook else 0.1)\n",
- " pp = integrator.StopDenseIntegration()\n",
- "\n",
- " return pp.vector_values(pp.get_segment_times())\n",
- "\n",
- " if False:\n",
- " data = simulate()\n",
- " (line,) = ax.plot(data[0, :], data[1, :], \"b\")\n",
- " (pt,) = ax.plot(data[0, 0], data[1, 0], \"b*\", markersize=12)\n",
- "\n",
- " def update(initial_angular_velocity):\n",
- " data = simulate(initial_angular_velocity)\n",
- " line.set_xdata(data[0, :])\n",
- " line.set_ydata(data[1, :])\n",
- " pt.set_xdata(data[0, 0])\n",
- " pt.set_ydata(data[1, 0])\n",
- " fig.canvas.draw()\n",
- "\n",
- " interact(\n",
- " update,\n",
- " initial_angular_velocity=widgets.FloatSlider(\n",
- " min=qdmin, max=qdmax, step=0.1, value=1.1\n",
- " ),\n",
- " )\n",
- "\n",
- " else:\n",
- " data = simulate(initial_angular_velocity, duration)\n",
- " ax.plot(data[0, :], data[1, :], \"b\")\n",
- " ax.plot(data[0, 0], data[1, 0], \"b*\", markersize=12)\n",
- "\n",
- " # Plot the energy contours.\n",
- " nq = 151\n",
- " nqd = 151\n",
- " mgl = params.mass() * params.gravity() * params.length()\n",
- " q = np.linspace(qmin, qmax, nq)\n",
- " qd = np.linspace(qdmin, qdmax, nqd)\n",
- " Q, QD = np.meshgrid(q, qd)\n",
- " Energy = 0.5 * params.mass() * params.length() ** 2 * QD**2 + mgl * np.cos(Q)\n",
- " ax.contour(\n",
- " Q,\n",
- " QD,\n",
- " Energy,\n",
- " alpha=0.5,\n",
- " linestyles=\"dashed\",\n",
- " colors=\"black\",\n",
- " linewidths=0.5,\n",
- " )\n",
- "\n",
- " ax.plot(\n",
- " params.slope() - rimless_wheel.calc_alpha(params) * np.array([1, 1]),\n",
- " np.array([0, qdmax]),\n",
- " \"k--\",\n",
- " )\n",
- " ax.plot(\n",
- " params.slope() - rimless_wheel.calc_alpha(params) * np.array([1, 1]),\n",
- " np.array([0, qdmin]),\n",
- " \"k\",\n",
- " linewidth=0.25,\n",
- " )\n",
- " ax.plot(\n",
- " params.slope() + rimless_wheel.calc_alpha(params) * np.array([1, 1]),\n",
- " np.array([0, qdmin]),\n",
- " \"k--\",\n",
- " )\n",
- " ax.plot(\n",
- " params.slope() + rimless_wheel.calc_alpha(params) * np.array([1, 1]),\n",
- " np.array([0, qdmax]),\n",
- " \"k\",\n",
- " linewidth=0.25,\n",
- " )\n",
- " ax.plot([qmin, qmax], [0, 0], \"k\", linewidth=0.25)\n",
- " ax.plot([0, 0], [qdmin, qdmax], \"k\", linewidth=0.25)\n",
- " ax.set_xlabel(\"theta\")\n",
- " ax.set_ylabel(\"thetadot\")\n",
- " ax.axis([qmin, qmax, qdmin, qdmax])\n",
- " ax.set_title(\n",
- " \"Trajectories of the Rimless Wheel (w/ contours of \" \"constant energy)\"\n",
- " )\n",
- " display(mpld3.display())\n",
- "\n",
- "\n",
- "# Interesting angular velocities to try: 5, 10, 0.95, -5, -4.8\n",
- "rimless_wheel_return_map(initial_angular_velocity=-4.7, duration=5)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# The Compass Gait"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "def compass_gait():\n",
- " builder = DiagramBuilder()\n",
- " compass_gait = builder.AddSystem(CompassGait())\n",
- "\n",
- " hip_torque = builder.AddSystem(ConstantVectorSource([0.0]))\n",
- " builder.Connect(hip_torque.get_output_port(0), compass_gait.get_input_port(0))\n",
- "\n",
- " scene_graph = builder.AddSystem(SceneGraph())\n",
- " CompassGaitGeometry.AddToBuilder(\n",
- " builder,\n",
- " compass_gait.get_floating_base_state_output_port(),\n",
- " scene_graph,\n",
- " )\n",
- " visualizer = MeshcatVisualizer.AddToBuilder(builder, scene_graph, meshcat)\n",
- " meshcat.Set2dRenderMode(xmin=-1, xmax=5, ymin=-1, ymax=2)\n",
- "\n",
- " logger = LogVectorOutput(compass_gait.get_output_port(1), builder)\n",
- "\n",
- " diagram = builder.Build()\n",
- " simulator = Simulator(diagram)\n",
- " simulator.set_target_realtime_rate(1.0 if running_as_notebook else 0.0)\n",
- "\n",
- " context = simulator.get_mutable_context()\n",
- " context.SetAccuracy(1e-4)\n",
- " context.SetContinuousState([0.0, 0.0, 0.4, -2.0])\n",
- "\n",
- " visualizer.StartRecording()\n",
- " simulator.AdvanceTo(8.0)\n",
- " visualizer.PublishRecording()\n",
- " log = logger.FindLog(context)\n",
- " plt.figure()\n",
- " plt.plot(log.data()[4, :], log.data()[11, :])\n",
- " plt.xlabel(\"left leg angle\")\n",
- " plt.ylabel(\"left leg angular velocity\")\n",
- " display(mpld3.display())\n",
- "\n",
- "\n",
- "compass_gait()"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# The Spring-Loaded Inverted Pendulum (SLIP)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "SLIPState = namedview(\n",
- " \"SLIPState\", [\"x\", \"z\", \"r\", \"theta\", \"xdot\", \"zdot\", \"rdot\", \"thetadot\"]\n",
- ")\n",
- "\n",
- "\n",
- "class SpringLoadedInvertedPendulum(LeafSystem):\n",
- " def __init__(self):\n",
- " LeafSystem.__init__(self)\n",
- "\n",
- " self.DeclareVectorInputPort(\"touchdown_angle\", 1)\n",
- " state_index = self.DeclareContinuousState(BasicVector(np.zeros(8)), 4, 4, 0)\n",
- " self.DeclareStateOutputPort(\"state\", state_index)\n",
- "\n",
- " # Parameters from Geyer05, p.23\n",
- " self.mass = 80.0 # kg\n",
- " self.r0 = 1.0 # m\n",
- " self.gravity = 9.81 # m/s^2\n",
- " # Define spring constant in terms of the dimensionless number.\n",
- " # Definition in section 2.4.3, values in figure 2.4.\n",
- " # Note: Geyer05 says 10.8 (which doesn't work? -- I get no fixed pts).\n",
- " dimensionless_spring_constant = 10.7\n",
- " self.stiffness = (\n",
- " dimensionless_spring_constant * self.mass * self.gravity / self.r0\n",
- " )\n",
- "\n",
- " self.last_apex = None # placeholder for writing return map result.\n",
- "\n",
- " self.touchdown_witness = self.MakeWitnessFunction(\n",
- " \"touchdown\",\n",
- " WitnessFunctionDirection.kPositiveThenNonPositive,\n",
- " self.foot_height,\n",
- " UnrestrictedUpdateEvent(self.touchdown),\n",
- " )\n",
- " self.takeoff_witness = self.MakeWitnessFunction(\n",
- " \"takeoff\",\n",
- " WitnessFunctionDirection.kPositiveThenNonPositive,\n",
- " self.leg_compression,\n",
- " UnrestrictedUpdateEvent(self.takeoff),\n",
- " )\n",
- " self.apex_witness = self.MakeWitnessFunction(\n",
- " \"apex\",\n",
- " WitnessFunctionDirection.kPositiveThenNonPositive,\n",
- " self.apex,\n",
- " PublishEvent(self.publish_apex),\n",
- " )\n",
- "\n",
- " def foot_height(self, context):\n",
- " s = SLIPState(context.get_continuous_state_vector().CopyToVector())\n",
- " return s.z - self.r0 * np.cos(s.theta)\n",
- "\n",
- " def touchdown(self, context, event, state):\n",
- " s = SLIPState(context.get_mutable_continuous_state_vector().CopyToVector())\n",
- "\n",
- " # print(\"touchdown\")\n",
- "\n",
- " # Update rdot and thetadot to match xdot and ydot, using\n",
- " # x = -r*sin(theta), z = r*cos(theta)\n",
- " # => xdot = -rdot*s - r*c*thetadot, zdot = rdot*c - r*s*thetadot\n",
- " # => xdot*c + zdot*s = -r*thetadot\n",
- " # r^2 = x^2 + z^2\n",
- " # => 2r*rdot = 2x*xdot + 2z*zdot\n",
- " # => rdot = -xdot*sin(theta) + zdot*cos(theta)\n",
- " # (matches Geyer05 Eq. 2.24 up to the symbol changes)\n",
- " s.r = self.r0\n",
- " s.rdot = -s.xdot * np.sin(s.theta) + s.zdot * np.cos(s.theta)\n",
- " s.thetadot = -(s.xdot * np.cos(s.theta) + s.zdot * np.sin(s.theta)) / self.r0\n",
- " state.get_mutable_continuous_state().get_mutable_vector().SetFromVector(s[:])\n",
- "\n",
- " def leg_compression(self, context):\n",
- " s = SLIPState(context.get_continuous_state_vector().CopyToVector())\n",
- " return self.r0 - s.r\n",
- "\n",
- " def takeoff(self, context, event, state):\n",
- " s = SLIPState(context.get_mutable_continuous_state_vector().CopyToVector())\n",
- "\n",
- " # print(\"takeoff\")\n",
- "\n",
- " # Setup flight state (these lines aren't strictly required, since we\n",
- " # choose to also integrate x and z in stance below).\n",
- " s.z = self.r0 * np.cos(s.theta)\n",
- " s.xdot = -s.rdot * np.sin(s.theta) - self.r0 * s.thetadot * np.cos(s.theta)\n",
- " s.zdot = s.rdot * np.cos(s.theta) - self.r0 * s.thetadot * np.sin(s.theta)\n",
- "\n",
- " # Update theta to commanded leg angle.\n",
- " s.theta = self.EvalVectorInput(context, 0).GetAtIndex(0)\n",
- " s.thetadot = 0\n",
- " s.r = self.r0\n",
- " s.rdot = 0\n",
- "\n",
- " state.get_mutable_continuous_state().get_mutable_vector().SetFromVector(s[:])\n",
- "\n",
- " def apex(self, context):\n",
- " return context.get_continuous_state_vector().GetAtIndex(5) # zdot\n",
- "\n",
- " def publish_apex(self, context, event):\n",
- " # TODO(russt): provide an option to terminate here instead, pending\n",
- " # resolution of #4447.\n",
- " # print(\"apex\")\n",
- " if self.last_apex is None:\n",
- " s = SLIPState(context.get_mutable_continuous_state_vector().CopyToVector())\n",
- " self.last_apex = s.z\n",
- "\n",
- " def apex_velocity_from_dimensionless_system_energy(self, Etilde, z):\n",
- " E = Etilde * self.mass * self.gravity * self.r0\n",
- " # E = 0.5*m*v^2 + m*g*z\n",
- " xdot = np.sqrt(2.0 / self.mass * (E - self.mass * self.gravity * z))\n",
- " return xdot\n",
- "\n",
- " def energy_flight(self, context):\n",
- " s = SLIPState(context.get_mutable_continuous_state_vector().CopyToVector())\n",
- " return (\n",
- " 0.5 * self.mass * (s.xdot**2 + s.zdot**2) + self.mass * self.gravity * s.z\n",
- " )\n",
- "\n",
- " def energy_stance(self, context):\n",
- " s = SLIPState(context.get_mutable_continuous_state_vector().CopyToVector())\n",
- " return (\n",
- " 0.5 * self.mass * (s.rdot**2 + s.r**2 * s.thetadot**2)\n",
- " + self.mass * self.gravity * s.r * np.cos(s.theta)\n",
- " + 0.5 * self.stiffness * (self.r0 - s.r) ** 2\n",
- " )\n",
- "\n",
- " def DoGetWitnessFunctions(self, context):\n",
- " return [\n",
- " self.touchdown_witness,\n",
- " self.takeoff_witness,\n",
- " self.apex_witness,\n",
- " ]\n",
- "\n",
- " def DoCalcTimeDerivatives(self, context, derivatives):\n",
- " s = SLIPState(context.get_continuous_state_vector().CopyToVector())\n",
- " sdot = SLIPState(np.zeros(8))\n",
- " sdot[0:4] = s[4:8]\n",
- "\n",
- " if self.foot_height(context) < 0:\n",
- " # then we're in \"stance\"\n",
- " sdot.rdot = (\n",
- " self.stiffness / self.mass * (self.r0 - s.r)\n",
- " + s.r * s.thetadot**2\n",
- " - self.gravity * np.cos(s.theta)\n",
- " )\n",
- " sdot.thetadot = (\n",
- " self.gravity / s.r * np.sin(s.theta) - 2 * s.rdot * s.thetadot / s.r\n",
- " )\n",
- "\n",
- " # Integrate x and z also, just for the sake of visualization (all\n",
- " # the integrated values except x will be overwritten in the\n",
- " # take-off reset).\n",
- " # x = -r*sin(theta), y = r*cos(theta) =>\n",
- " sdot.xdot = (\n",
- " -sdot.rdot * np.sin(s.theta)\n",
- " - 2 * s.rdot * s.thetadot * np.cos(s.theta)\n",
- " + s.r * s.thetadot**2 * np.sin(s.theta)\n",
- " - s.r * sdot.thetadot * np.cos(s.theta)\n",
- " )\n",
- " sdot.zdot = (\n",
- " sdot.rdot * np.cos(s.theta)\n",
- " - 2 * s.rdot * s.thetadot * np.sin(s.theta)\n",
- " - s.r * sdot.thetadot * np.sin(s.theta)\n",
- " - s.r * s.thetadot**2 * np.cos(s.theta)\n",
- " )\n",
- "\n",
- " else:\n",
- " sdot.xdot = 0\n",
- " sdot.zdot = -self.gravity\n",
- " sdot.rdot = 0\n",
- " sdot.thetadot = 0\n",
- "\n",
- " derivatives.get_mutable_vector().SetFromVector(sdot[:])\n",
- "\n",
- "\n",
- "class SLIPVisualizer(PyPlotVisualizer):\n",
- " def __init__(self, ax=None, show=True):\n",
- " PyPlotVisualizer.__init__(self, ax=ax, show=show, figsize=(12, 4))\n",
- " self.DeclareInputPort(\"slip_state\", PortDataType.kVectorValued, 8)\n",
- " self.ax.set_aspect(\"equal\")\n",
- " self.ax.set_xlim(0, 2)\n",
- " self.ax.set_ylim(-0.1, 1.5)\n",
- "\n",
- " # Draw the ground.\n",
- " self.ax.plot([-50, 50], [0, 0], \"k\")\n",
- "\n",
- " a = np.linspace(0, 2 * np.pi, 50)\n",
- " radius = 0.1\n",
- " self.hip_fill = self.ax.fill(\n",
- " radius * np.sin(a),\n",
- " radius * np.cos(a),\n",
- " zorder=1,\n",
- " edgecolor=\"k\",\n",
- " facecolor=[0.6, 0.6, 0.6],\n",
- " )\n",
- " self.hip = copy.copy(self.hip_fill[0].get_path().vertices)\n",
- "\n",
- " self.leg_line = [self.ax.plot([0, 0], [0, -1], \"k\")[0]]\n",
- " self.leg_data = [self.leg_line[0].get_xydata().T]\n",
- " for i in range(1, 13):\n",
- " self.leg_line.append(\n",
- " self.ax.plot(\n",
- " 0.1\n",
- " * np.array(\n",
- " [\n",
- " np.sin((i - 1) * np.pi / 2.0),\n",
- " np.sin(i * np.pi / 2.0),\n",
- " ]\n",
- " ),\n",
- " -0.2 - 0.7 / 13 * np.array([i - 1, i]),\n",
- " \"k\",\n",
- " )[0]\n",
- " )\n",
- " self.leg_data.append(self.leg_line[i].get_xydata().T)\n",
- "\n",
- " def draw(self, context):\n",
- " state = SLIPState(self.EvalVectorInput(context, 0).CopyToVector())\n",
- "\n",
- " self.hip_fill[0].get_path().vertices[:, 0] = state.x + self.hip[:, 0]\n",
- " self.hip_fill[0].get_path().vertices[:, 1] = state.z + self.hip[:, 1]\n",
- "\n",
- " R = np.array(\n",
- " [\n",
- " [np.cos(state.theta), -np.sin(state.theta)],\n",
- " [np.sin(state.theta), np.cos(state.theta)],\n",
- " ]\n",
- " )\n",
- " for i in range(len(self.leg_line)):\n",
- " self.leg_line[i].set_xdata(\n",
- " state.x + state.r * R[0, :].dot(self.leg_data[i])\n",
- " )\n",
- " self.leg_line[i].set_ydata(\n",
- " state.z + state.r * R[1, :].dot(self.leg_data[i])\n",
- " )\n",
- "\n",
- " self.ax.set_title(\"t = {:.1f}\".format(context.get_time()))"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "def slip_demo():\n",
- " builder = DiagramBuilder()\n",
- " plant = builder.AddSystem(SpringLoadedInvertedPendulum())\n",
- "\n",
- " # Parameters from Geyer05, Figure 2.4\n",
- " # Note: Geyer uses angle of attack = 90 - touchdown_angle\n",
- " touchdown_angle = np.deg2rad(30)\n",
- " Etilde = 1.61\n",
- "\n",
- " s = SLIPState(np.zeros(8))\n",
- " s.z = 0.9\n",
- " s.theta = touchdown_angle\n",
- " s.r = 1\n",
- " s.xdot = plant.apex_velocity_from_dimensionless_system_energy(Etilde, s.z)\n",
- "\n",
- " visualizer = builder.AddSystem(SLIPVisualizer(show=False))\n",
- " builder.Connect(plant.get_output_port(0), visualizer.get_input_port(0))\n",
- "\n",
- " LogVectorOutput(plant.get_output_port(0), builder)\n",
- "\n",
- " command = builder.AddSystem(ConstantVectorSource([touchdown_angle]))\n",
- " builder.Connect(command.get_output_port(0), plant.get_input_port(0))\n",
- "\n",
- " diagram = builder.Build()\n",
- " simulator = Simulator(diagram)\n",
- " context = simulator.get_mutable_context()\n",
- " context.SetAccuracy(1e-10)\n",
- "\n",
- " context.get_mutable_continuous_state_vector().SetFromVector(s[:])\n",
- "\n",
- " AdvanceToAndVisualize(simulator, visualizer, 2.0)\n",
- "\n",
- "\n",
- "slip_demo()"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Apex-to-apex map"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "def slip_map():\n",
- " plant = SpringLoadedInvertedPendulum()\n",
- "\n",
- " # Parameters from Geyer05, Figure 2.4\n",
- " # Note: Geyer uses angle of attack = 90 - touchdown_angle\n",
- " touchdown_angle = np.deg2rad(30)\n",
- " Etilde = 1.61\n",
- "\n",
- " s = SLIPState(np.zeros(8))\n",
- " s.theta = touchdown_angle\n",
- " s.r = 1\n",
- "\n",
- " simulator = Simulator(plant)\n",
- " context = simulator.get_mutable_context()\n",
- " plant.get_input_port(0).FixValue(context, [touchdown_angle])\n",
- " context.SetAccuracy(1e-5)\n",
- "\n",
- " zs = np.linspace(np.cos(touchdown_angle) + 0.001, 0.95, 25)\n",
- " zns = 0 * zs\n",
- " for i in range(len(zs)):\n",
- " s.z = zs[i]\n",
- " s.xdot = plant.apex_velocity_from_dimensionless_system_energy(Etilde, s.z)\n",
- " context.SetTime(0.0)\n",
- " context.get_mutable_continuous_state_vector().SetFromVector(s[:])\n",
- " simulator.Initialize()\n",
- " # Note: With this duration, I sometimes get an extra \"touchdown\" after the\n",
- " # apex, which results in apex-touchdown; touchdown-takeoff-apex on the\n",
- " # console. It's not a double reset, the consecutive touchdowns are two\n",
- " # different sims.\n",
- " simulator.AdvanceTo(0.6)\n",
- " zns[i] = plant.last_apex\n",
- " plant.last_apex = None\n",
- "\n",
- " fig, ax = plt.subplots()\n",
- " ax.plot(zs, zns)\n",
- " ax.plot(zs, zs)\n",
- " ax.axis(\"equal\")\n",
- " ax.set_xlim([zs[0], zs[-1]])\n",
- " ax.set_ylim([zs[0], zs[-1]])\n",
- " ax.set_xlabel(\"apex height z[n]\")\n",
- " ax.set_ylabel(\"apex height z[n+1]\")\n",
- "\n",
- " display(mpld3.display())\n",
- "\n",
- "\n",
- "slip_map()"
- ]
- },
- {
- "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.10.9 (main, Dec 15 2022, 10:44:50) [Clang 14.0.0 (clang-1400.0.29.202)]"
- },
- "vscode": {
- "interpreter": {
- "hash": "b0fa6594d8f4cbf19f97940f81e996739fb7646882a419484c72d19e05852a7e"
- }
- }
- },
- "nbformat": 4,
- "nbformat_minor": 4
-}
diff --git a/book/stochastic.html b/book/stochastic.html
index ae427d4a..93c58d03 100644
--- a/book/stochastic.html
+++ b/book/stochastic.html
@@ -100,7 +100,7 @@ Underactuated Robotics
- The Master Equation
+ The Master Equation
Let's start by looking at some simple examples.
@@ -320,15 +320,14 @@ Underactuated Robotics
- The Cubic Example + Noise
+ The Cubic Polynomial + Noise
- Now let's consider the discrete-time approximation of \[ \dot{x} = -x +
- x^3, \] again with additive noise: \[ x[n+1] = x[n] + h (-x[n] + x[n]^3 +
- w[n]). \] With $w[n]=0$, the system has only a single stable fixed point
- (at the origin), whose deterministic region of attraction is given by $x
- \in (-1,1)$. If we again simulate the system from a set of random initial
- conditions and plot the histogram, we will see something like the figure
- below.
+ Now let's consider the discrete-time approximation of \[ \dot{x} = -x + x^3, \]
+ again with additive noise: \[ x[n+1] = x[n] + h (-x[n] + x[n]^3 + w[n]). \] With
+ $w[n]=0$, the system has only a single stable fixed point (at the origin), whose
+ deterministic region of attraction is given by $x \in (-1,1)$. If we again
+ simulate the system from a set of random initial conditions and plot the
+ histogram, we will see something like the figure below.
Histogram of the states of the bistable system with noise
diff --git a/book/stochastic/BUILD.bazel b/book/stochastic/BUILD.bazel
new file mode 100644
index 00000000..80e5dda4
--- /dev/null
+++ b/book/stochastic/BUILD.bazel
@@ -0,0 +1,14 @@
+# -*- mode: python -*-
+# vi: set ft=python :
+
+# Copyright 2020-2022 Massachusetts Institute of Technology.
+# Licensed under the BSD 3-Clause License. See LICENSE.TXT for details.
+
+load("@pip_deps//:requirements.bzl", "requirement")
+load("//book/htmlbook/tools/jupyter:defs.bzl", "rt_ipynb_test")
+
+rt_ipynb_test(
+ name = "stochastic",
+ srcs = ["stochastic.ipynb"],
+ deps = ["//underactuated"],
+)
diff --git a/book/stochastic.ipynb b/book/stochastic/stochastic.ipynb
similarity index 100%
rename from book/stochastic.ipynb
rename to book/stochastic/stochastic.ipynb
diff --git a/pyproject.toml b/pyproject.toml
index 9e7be70c..8bdaf1d3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -2,7 +2,7 @@
name = "underactuated"
# Use e.g. 2023.10.4.rc0 if I need to release a release candidate.
# Use e.g. 2023.10.4.post1 if I need to rerelease on the same day.
-version = "2024.04.12"
+version = "2024.04.25"
description = "MIT 6.821 - Underactuated Robotics"
authors = ["Russ Tedrake "]
license = "BSD License"