diff --git a/docs/notebooks/README.md b/docs/notebooks/README.md
index 6cc0bfd6a..ce914c8cd 100644
--- a/docs/notebooks/README.md
+++ b/docs/notebooks/README.md
@@ -21,21 +21,30 @@ found by that line, not by its position) and is folded by default.
Then it installs the toolbox with its dependencies, from the wheel bundled with the
JupyterLite site if there is one, or else from PyPI.
-## The standard first cell
+## The first cell
-Every notebook starts with the same markdown cell: the logo and title, the copyright
-line, and a short description. It is generated from the notebook's own metadata by
-`sync_bootstrap.py`, like the bootstrap cell, and the `\n",
"
\n",
" \n",
" Robotics Toolbox for Python | \n",
- "  | \n",
+ "  | \n",
"
\n",
"
\n",
"\n",
@@ -26,6 +26,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "a13e6155",
"metadata": {
"jupyter": {
"source_hidden": true
@@ -80,6 +81,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "9daa4a69",
"metadata": {},
"outputs": [],
"source": [
@@ -89,6 +91,7 @@
},
{
"cell_type": "markdown",
+ "id": "5a617374",
"metadata": {},
"source": [
"We will load a model for a simple Y-shaped planar robot. Check out the source code to see how the model was constructed using `Link` and `Robot` classes."
@@ -97,6 +100,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "d91fc922",
"metadata": {},
"outputs": [],
"source": [
@@ -106,6 +110,7 @@
},
{
"cell_type": "markdown",
+ "id": "ee1f4eb9",
"metadata": {},
"source": [
"From the displayed table we see that this robot has two end-effectors, indicated by the links shown in blue and prefixed with the @ symbol.\n",
@@ -116,6 +121,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "0f9b8925",
"metadata": {},
"outputs": [],
"source": [
@@ -124,6 +130,7 @@
},
{
"cell_type": "markdown",
+ "id": "9be2bf18",
"metadata": {},
"source": [
"which writes a file in the current folder. We then render it to SVG format and [load and display it](https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html?highlight=svg#IPython.display.SVG)"
@@ -132,6 +139,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "36593abb",
"metadata": {},
"outputs": [],
"source": [
@@ -141,6 +149,7 @@
},
{
"cell_type": "markdown",
+ "id": "ee7b73be",
"metadata": {},
"source": [
"The fixed base of the robot is shown as the dark grey node. Link reference frames are shown by circles and the edges are marked with the elementary transform sequences that define the relative poses of the link frames. End-effector nodes are marked by a blue double circle.\n",
@@ -152,6 +161,7 @@
},
{
"cell_type": "markdown",
+ "id": "11846548",
"metadata": {},
"source": [
"The forward kinematics is quite straightforward, but we must specify which end-effector we are interested in"
@@ -160,6 +170,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "fdcdda90",
"metadata": {},
"outputs": [],
"source": [
@@ -168,6 +179,7 @@
},
{
"cell_type": "markdown",
+ "id": "2fec3923",
"metadata": {},
"source": [
"which is exactly the same as the pose of the second branch"
@@ -176,6 +188,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "9506e063",
"metadata": {},
"outputs": [],
"source": [
@@ -184,6 +197,7 @@
},
{
"cell_type": "markdown",
+ "id": "a46a6bcd",
"metadata": {},
"source": [
"In a similar fashion we can also compute the Jacobian, in this case as a $6 \\times 4$ matrix"
@@ -192,6 +206,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "1ea285df",
"metadata": {},
"outputs": [],
"source": [
@@ -200,6 +215,7 @@
},
{
"cell_type": "markdown",
+ "id": "990a8207",
"metadata": {},
"source": [
"We can generate an ETS for the bottom-branch of the robot"
@@ -208,6 +224,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "fac103c3",
"metadata": {},
"outputs": [],
"source": [
@@ -217,6 +234,7 @@
},
{
"cell_type": "markdown",
+ "id": "ec1eafbd",
"metadata": {},
"source": [
"and we could easily turn this into an unbranched robot represented by an `Robot` instance"
@@ -225,6 +243,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "47dd0ff7",
"metadata": {},
"outputs": [],
"source": [
@@ -234,6 +253,7 @@
},
{
"cell_type": "markdown",
+ "id": "216c43d3",
"metadata": {},
"source": [
"This robot has 5 links but only 4 joint angles which are inherited from the original branched robot: `q0`, `q1`, `q4` and `q5`. Its `fkine` method accepts either a vector of the four joint angles, in order, or a vector of six elements indexed by the original joint numbers, in which case `q2` and `q3` are ignored.\n",
@@ -244,6 +264,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "5c4851ce",
"metadata": {},
"outputs": [],
"source": [
@@ -253,6 +274,7 @@
},
{
"cell_type": "markdown",
+ "id": "bfa5dd51",
"metadata": {},
"source": [
"which is expressed as an ETS. Note that it is not in minimal form and the terms in the middle `tx(-1) * tx(1)` cancel out. We can precompile the constants which will take care of this for us"
@@ -261,6 +283,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "8081cc08",
"metadata": {},
"outputs": [],
"source": [
@@ -269,6 +292,7 @@
},
{
"cell_type": "markdown",
+ "id": "fce34f81",
"metadata": {},
"source": [
"# Graph display variations\n",
@@ -279,6 +303,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "733672f5",
"metadata": {},
"outputs": [],
"source": [
@@ -289,6 +314,7 @@
},
{
"cell_type": "markdown",
+ "id": "fb39355d",
"metadata": {},
"source": [
"and the input to a link frame is a black dot if it is preceded by a revolute joint, and a black box if it preceded by a prismatic joint.\n",
@@ -299,6 +325,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "6f999740",
"metadata": {},
"outputs": [],
"source": [
@@ -309,6 +336,7 @@
},
{
"cell_type": "markdown",
+ "id": "173f9240",
"metadata": {},
"source": [
"## PR2 model\n",
@@ -319,6 +347,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "454a5ced",
"metadata": {},
"outputs": [],
"source": [
@@ -331,6 +360,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "644b4e65",
"metadata": {},
"outputs": [],
"source": []
@@ -343,54 +373,9 @@
"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.8.5"
- },
- "varInspector": {
- "cols": {
- "lenName": 16,
- "lenType": 16,
- "lenVar": 40
- },
- "kernels_config": {
- "python": {
- "delete_cmd_postfix": "",
- "delete_cmd_prefix": "del ",
- "library": "var_list.py",
- "varRefreshCmd": "print(var_dic_list())"
- },
- "r": {
- "delete_cmd_postfix": ") ",
- "delete_cmd_prefix": "rm(",
- "library": "var_list.r",
- "varRefreshCmd": "cat(var_dic_list()) "
- }
- },
- "types_to_exclude": [
- "module",
- "function",
- "builtin_function_or_method",
- "instance",
- "_Feature"
- ],
- "window_display": false
- },
- "rtb_header": {
- "title": "A branched robot",
- "paragraphs": [
- "A robot with more than one end-effector: its forward kinematics and Jacobian for each branch, extracting the ETS of a branch, and drawing the robot as a graph. Drawing the graphs needs [Graphviz](https://graphviz.org) (the `dot` command) installed."
- ],
- "modernised": "2026-10-19"
+ "name": "python"
}
},
"nbformat": 4,
- "nbformat_minor": 4
+ "nbformat_minor": 5
}
diff --git a/docs/notebooks/clear_outputs.py b/docs/notebooks/clear_outputs.py
index 8bba1e55a..96a2e04ce 100644
--- a/docs/notebooks/clear_outputs.py
+++ b/docs/notebooks/clear_outputs.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python
-"""Clear cell outputs and execution counts from every notebook in this folder.
+"""Clear cell outputs, execution counts and editor metadata from every notebook here.
Pure stdlib (no dependency on jupyter/nbconvert being installed), so it can run as
a commit hook on any machine with just Python. Equivalent to
@@ -21,6 +21,28 @@
HERE = Path(__file__).resolve().parent
+# What an editor writes into a notebook's metadata depends on who opened it last: the
+# name of their Python environment, its version, interpreter paths, widget state and
+# so on. That is noise in every diff, so notebooks are committed with the same
+# kernel and language, and with only the metadata that belongs to this repository.
+KERNELSPEC = {"display_name": "Python 3", "language": "python", "name": "python3"}
+LANGUAGE_INFO = {"name": "python"}
+OWN_METADATA = ("rtb_bootstrap",)
+
+
+def normalize_metadata(nb: dict) -> bool:
+ """Set a notebook's metadata to its canonical form, in place.
+
+ :returns: True if it was (or would be) changed.
+ """
+ old = nb.get("metadata", {})
+ new = {"kernelspec": dict(KERNELSPEC), "language_info": dict(LANGUAGE_INFO)}
+ new.update({k: old[k] for k in OWN_METADATA if k in old})
+ if old == new:
+ return False
+ nb["metadata"] = new
+ return True
+
def clear_cell(cell: dict) -> bool:
"""Clear one cell's outputs/execution count in place.
@@ -44,22 +66,29 @@ def clear_cell(cell: dict) -> bool:
return changed
+def canonical_text(nb: dict) -> str:
+ """The notebook as nbformat (and so VS Code and Jupyter) writes it."""
+ return json.dumps(nb, indent=1, ensure_ascii=False, sort_keys=True) + "\n"
+
+
def process_notebook(path: Path, fix: bool) -> bool:
- """Check (and optionally clear) one notebook's outputs.
+ """Check (and optionally clear) one notebook's outputs and metadata.
:returns: True if the notebook was (or would be) changed.
"""
- with path.open("r", encoding="utf-8") as f:
- nb = json.load(f)
+ raw = path.read_text(encoding="utf-8")
+ nb = json.loads(raw)
# a list, not a generator: any() would stop at the first dirty cell and leave
# the rest uncleared
changed = any([clear_cell(cell) for cell in nb.get("cells", [])])
+ changed = normalize_metadata(nb) or changed
+ text = canonical_text(nb)
+ changed = changed or text != raw
if changed and fix:
- with path.open("w", encoding="utf-8") as f:
- json.dump(nb, f, indent=1, ensure_ascii=False)
- f.write("\n")
+ with path.open("w", encoding="utf-8", newline="\n") as f:
+ f.write(text)
return changed
diff --git a/docs/notebooks/dynamics.ipynb b/docs/notebooks/dynamics.ipynb
index 76a003333..9b5b46c4c 100644
--- a/docs/notebooks/dynamics.ipynb
+++ b/docs/notebooks/dynamics.ipynb
@@ -2,13 +2,13 @@
"cells": [
{
"cell_type": "markdown",
+ "id": "28f9bef8",
"metadata": {},
"source": [
- "\n",
"\n",
" \n",
" Robotics Toolbox for Python | \n",
- "  | \n",
+ "  | \n",
"
\n",
"
\n",
"\n",
@@ -24,6 +24,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "643c6a7b",
"metadata": {
"jupyter": {
"source_hidden": true
@@ -78,6 +79,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "b51522bd",
"metadata": {
"scrolled": false
},
@@ -102,6 +104,7 @@
},
{
"cell_type": "markdown",
+ "id": "7058a6d2",
"metadata": {},
"source": [
"We will instantiate a model of the Puma 560 robot which has well known inertial parameters"
@@ -110,6 +113,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "05a35b38",
"metadata": {
"scrolled": false
},
@@ -120,6 +124,7 @@
},
{
"cell_type": "markdown",
+ "id": "037b8347",
"metadata": {},
"source": [
"and show its configuration in a typical desktop working pose"
@@ -128,6 +133,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "55c9eafa",
"metadata": {
"scrolled": false
},
@@ -138,6 +144,7 @@
},
{
"cell_type": "markdown",
+ "id": "92192d3b",
"metadata": {},
"source": [
"The rigid-body equations of motion for a robot are a set of coupled differential equations\n",
@@ -159,6 +166,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "4e6cb1bb",
"metadata": {
"scrolled": false
},
@@ -169,6 +177,7 @@
},
{
"cell_type": "markdown",
+ "id": "4f123bc5",
"metadata": {},
"source": [
"and it shows, as expected, that the shoulder is exerting significant torque to hold the arm up and stationary.\n",
@@ -179,6 +188,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "ff4a51b0",
"metadata": {
"scrolled": false
},
@@ -189,6 +199,7 @@
},
{
"cell_type": "markdown",
+ "id": "62f5cd82",
"metadata": {},
"source": [
"The diagonal elements $M_{jj}$ indicate the inertia experienced by the joint $j$, ie. Newton's second law for this joint is $\\tau_j = M_{jj} \\ddot{q}_j$.\n",
@@ -201,6 +212,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "348a5802",
"metadata": {
"scrolled": false
},
@@ -219,6 +231,7 @@
},
{
"cell_type": "markdown",
+ "id": "ef040af1",
"metadata": {},
"source": [
"The inertia \"seen\" by joint 1 varies as a function of $q_2$ and $q_3$ as shown below"
@@ -227,6 +240,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "9507f059",
"metadata": {
"scrolled": false
},
@@ -242,6 +256,7 @@
},
{
"cell_type": "markdown",
+ "id": "7d71f5fa",
"metadata": {},
"source": [
"The ratio of maximum to minimum values is"
@@ -250,6 +265,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "d6326739",
"metadata": {
"scrolled": false
},
@@ -260,6 +276,7 @@
},
{
"cell_type": "markdown",
+ "id": "d45959e5",
"metadata": {},
"source": [
"The coupling inertia between joints 1 and 2 also varies with configuration and we can plot that as well"
@@ -268,6 +285,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "9eaffd99",
"metadata": {
"scrolled": false
},
@@ -283,6 +301,7 @@
},
{
"cell_type": "markdown",
+ "id": "76e6c451",
"metadata": {},
"source": [
"The velocity terms are a bit harder to comprehend but they mean that rotation of one joint (and its link) can exert a torque on other joints. Consider that the should joint is rotating at 1 rad/sec, then the torque will be "
@@ -291,6 +310,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "5f7d4b51",
"metadata": {
"scrolled": false
},
@@ -302,6 +322,7 @@
},
{
"cell_type": "markdown",
+ "id": "1eec4c42",
"metadata": {},
"source": [
"and we see that it exerts a torque on the waist and elbow joints.\n",
@@ -312,6 +333,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "80332c44",
"metadata": {
"scrolled": false
},
@@ -322,6 +344,7 @@
},
{
"cell_type": "markdown",
+ "id": "6db5db6b",
"metadata": {},
"source": [
"which computes $\\tau = \\mbox{rne}(\\mathit{q}, \\mathit{\\dot{q}}, \\mathit{\\dot{q}})$ and can accept additional arguments such as gravity or a wrench applied to the end-effector. In the Toolbox this algorithm is implemented in C for maximum performance.\n",
@@ -331,6 +354,7 @@
},
{
"cell_type": "markdown",
+ "id": "c9546c97",
"metadata": {},
"source": [
"We can rearrange the equations of motion as\n",
@@ -345,6 +369,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "6b90ebc6",
"metadata": {
"scrolled": false
},
@@ -356,6 +381,7 @@
},
{
"cell_type": "markdown",
+ "id": "d3bf77a9",
"metadata": {},
"source": [
"The first line needs some explanation. The Toolbox can model two types of joint friction:\n",
@@ -370,6 +396,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "536f859c",
"metadata": {
"scrolled": true
},
@@ -380,6 +407,7 @@
},
{
"cell_type": "markdown",
+ "id": "7c5477ba",
"metadata": {},
"source": [
"which we can plot using a Toolbox convenience function"
@@ -388,6 +416,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "f7a070a7",
"metadata": {
"scrolled": false
},
@@ -398,6 +427,7 @@
},
{
"cell_type": "markdown",
+ "id": "0400ddbc",
"metadata": {},
"source": [
"or we can animate it, showing the robot collapsing under gravity"
@@ -406,6 +436,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "fdd61a45",
"metadata": {
"scrolled": false
},
@@ -416,6 +447,7 @@
},
{
"cell_type": "markdown",
+ "id": "210b9389",
"metadata": {},
"source": [
"_Note: this animation is not working in Jupyter notebook..._\n",
@@ -431,73 +463,9 @@
"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.8.5"
- },
- "toc": {
- "base_numbering": 1,
- "nav_menu": {},
- "number_sections": true,
- "sideBar": true,
- "skip_h1_title": false,
- "title_cell": "Table of Contents",
- "title_sidebar": "Contents",
- "toc_cell": false,
- "toc_position": {},
- "toc_section_display": true,
- "toc_window_display": false
- },
- "varInspector": {
- "cols": {
- "lenName": 16,
- "lenType": 16,
- "lenVar": 40
- },
- "kernels_config": {
- "python": {
- "delete_cmd_postfix": "",
- "delete_cmd_prefix": "del ",
- "library": "var_list.py",
- "varRefreshCmd": "print(var_dic_list())"
- },
- "r": {
- "delete_cmd_postfix": ") ",
- "delete_cmd_prefix": "rm(",
- "library": "var_list.r",
- "varRefreshCmd": "cat(var_dic_list()) "
- }
- },
- "types_to_exclude": [
- "module",
- "function",
- "builtin_function_or_method",
- "instance",
- "_Feature"
- ],
- "window_display": false
- },
- "widgets": {
- "application/vnd.jupyter.widget-state+json": {
- "state": {},
- "version_major": 2,
- "version_minor": 0
- }
- },
- "rtb_header": {
- "title": "Robot dynamics",
- "paragraphs": [
- "The inertia, velocity terms and gravity load of a Puma 560 robot, and its forward dynamics."
- ]
+ "name": "python"
}
},
"nbformat": 4,
- "nbformat_minor": 4
+ "nbformat_minor": 5
}
diff --git a/docs/notebooks/ets.ipynb b/docs/notebooks/ets.ipynb
index 250df85ed..3e3e66ade 100644
--- a/docs/notebooks/ets.ipynb
+++ b/docs/notebooks/ets.ipynb
@@ -2,13 +2,13 @@
"cells": [
{
"cell_type": "markdown",
+ "id": "10582ade",
"metadata": {},
"source": [
- "\n",
"\n",
" \n",
" Robotics Toolbox for Python | \n",
- "  | \n",
+ "  | \n",
"
\n",
"
\n",
"\n",
@@ -26,6 +26,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "9160be55",
"metadata": {
"jupyter": {
"source_hidden": true
@@ -80,6 +81,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "aa0c8891",
"metadata": {},
"outputs": [],
"source": [
@@ -93,6 +95,7 @@
},
{
"cell_type": "markdown",
+ "id": "3f6052da",
"metadata": {},
"source": [
"The traditional approach to robot kinematics is to use Denavit-Hartenberg notation where each link is deescribed by a very specific sequence of simple (elementary) transformations\n",
@@ -117,6 +120,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "f95ded42",
"metadata": {},
"outputs": [],
"source": [
@@ -128,6 +132,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "450063ee",
"metadata": {},
"outputs": [],
"source": [
@@ -137,6 +142,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "ef1861e6",
"metadata": {},
"outputs": [],
"source": [
@@ -145,6 +151,7 @@
},
{
"cell_type": "markdown",
+ "id": "ec7cbbf3",
"metadata": {},
"source": [
"Consider we want to create a simple 2 link planar robot arranged like\n",
@@ -157,6 +164,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "8f76fadf",
"metadata": {},
"outputs": [],
"source": [
@@ -166,6 +174,7 @@
},
{
"cell_type": "markdown",
+ "id": "25bf41e4",
"metadata": {},
"source": [
"`e` is an ETS object"
@@ -174,6 +183,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "0df1b638",
"metadata": {},
"outputs": [],
"source": [
@@ -182,6 +192,7 @@
},
{
"cell_type": "markdown",
+ "id": "86669081",
"metadata": {},
"source": [
"which has a number of methods including "
@@ -190,6 +201,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "a887d3db",
"metadata": {},
"outputs": [],
"source": [
@@ -198,6 +210,7 @@
},
{
"cell_type": "markdown",
+ "id": "59214ecb",
"metadata": {},
"source": [
"indicating it has four transforms"
@@ -206,6 +219,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "aeed5bd7",
"metadata": {},
"outputs": [],
"source": [
@@ -214,6 +228,7 @@
},
{
"cell_type": "markdown",
+ "id": "9bb00c35",
"metadata": {},
"source": [
"indicating it has three joint variables, and they occur at the indices given by"
@@ -222,6 +237,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "506f0f73",
"metadata": {},
"outputs": [],
"source": [
@@ -230,6 +246,7 @@
},
{
"cell_type": "markdown",
+ "id": "d7959cae",
"metadata": {},
"source": [
"The ETS object acts a lot like a list, and we can slice it"
@@ -238,6 +255,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "43b2e0f6",
"metadata": {},
"outputs": [],
"source": [
@@ -247,6 +265,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "3fa3fa85",
"metadata": {},
"outputs": [],
"source": [
@@ -255,6 +274,7 @@
},
{
"cell_type": "markdown",
+ "id": "0242faeb",
"metadata": {},
"source": [
"There are also a number of predicates"
@@ -263,6 +283,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "2c7a8c92",
"metadata": {},
"outputs": [],
"source": [
@@ -272,6 +293,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "a7c3b2ef",
"metadata": {},
"outputs": [],
"source": [
@@ -281,6 +303,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "6f4aa252",
"metadata": {},
"outputs": [],
"source": [
@@ -290,6 +313,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "837d7307",
"metadata": {},
"outputs": [],
"source": [
@@ -299,6 +323,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "d34b4e70",
"metadata": {},
"outputs": [],
"source": [
@@ -307,6 +332,7 @@
},
{
"cell_type": "markdown",
+ "id": "6377ab22",
"metadata": {},
"source": [
"We can substitute in values"
@@ -315,6 +341,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "b0c00e67",
"metadata": {},
"outputs": [],
"source": [
@@ -323,6 +350,7 @@
},
{
"cell_type": "markdown",
+ "id": "be8e39ec",
"metadata": {},
"source": [
"and the result is an SE(3) matrix encapsulated in an `SE3` instance. As expected is a translation of 2m in the x-direction. \n",
@@ -333,6 +361,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "868b757f",
"metadata": {},
"outputs": [],
"source": [
@@ -341,6 +370,7 @@
},
{
"cell_type": "markdown",
+ "id": "905574d2",
"metadata": {},
"source": [
"Joint angles are in radians, so we converted the angles from degrees with `np.deg2rad`. The prismatic joint value is a length in metres and is left as it is."
@@ -348,6 +378,7 @@
},
{
"cell_type": "markdown",
+ "id": "943c4184",
"metadata": {},
"source": [
"## Joint flip\n",
@@ -358,6 +389,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "f1c519dc",
"metadata": {},
"outputs": [],
"source": [
@@ -367,6 +399,7 @@
},
{
"cell_type": "markdown",
+ "id": "f013f36a",
"metadata": {},
"source": [
"and the flip is indicated by a negative sign `Rz(-q0)` when the ETS is displayed."
@@ -375,6 +408,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "8971de1e",
"metadata": {},
"outputs": [],
"source": [
@@ -383,6 +417,7 @@
},
{
"cell_type": "markdown",
+ "id": "3da01f41",
"metadata": {},
"source": [
"Now if we negate the passed value of q0 we see that we have compute the same answer as above.\n",
@@ -393,6 +428,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "61e98e5b",
"metadata": {},
"outputs": [],
"source": [
@@ -402,6 +438,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "645710e5",
"metadata": {},
"outputs": [],
"source": [
@@ -410,6 +447,7 @@
},
{
"cell_type": "markdown",
+ "id": "492b1d4b",
"metadata": {},
"source": [
"## Symbolics\n",
@@ -419,6 +457,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "24641d96",
"metadata": {},
"outputs": [],
"source": [
@@ -428,6 +467,7 @@
},
{
"cell_type": "markdown",
+ "id": "c2a4d56a",
"metadata": {},
"source": [
"Which creates a vector of three joint angle variables. The advantage of having the underscore in the name is that Jupyter considers this as LaTeX subscript notation when it pretty prints the symbols"
@@ -436,6 +476,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "c0d66855",
"metadata": {},
"outputs": [],
"source": [
@@ -444,6 +485,7 @@
},
{
"cell_type": "markdown",
+ "id": "ee17b8a5",
"metadata": {},
"source": [
"We can substitute in the symbolic values just as easily as we did the numerical values to find the resulting overall transform"
@@ -452,6 +494,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "1f2f6981",
"metadata": {},
"outputs": [],
"source": [
@@ -460,6 +503,7 @@
},
{
"cell_type": "markdown",
+ "id": "d75bb33b",
"metadata": {},
"source": [
"## ETS in 2D\n",
@@ -470,6 +514,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "dbaa7737",
"metadata": {},
"outputs": [],
"source": [
@@ -480,6 +525,7 @@
},
{
"cell_type": "markdown",
+ "id": "dc29781c",
"metadata": {},
"source": [
"The main difference is that in 2D there are three elementary transforms to choose from $\\{r, t_x, t_y\\}$ - rotation and translation in the plane.\n",
@@ -490,6 +536,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "7d6b9f09",
"metadata": {},
"outputs": [],
"source": [
@@ -498,6 +545,7 @@
},
{
"cell_type": "markdown",
+ "id": "4cd9a23f",
"metadata": {},
"source": [
"and the result this time is an SE(2) matrix encapsulated in an `SE2` instance."
@@ -505,6 +553,7 @@
},
{
"cell_type": "markdown",
+ "id": "159d08f4",
"metadata": {},
"source": [
"## A more complex example\n",
@@ -518,6 +567,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "5e622b60",
"metadata": {},
"outputs": [],
"source": [
@@ -531,6 +581,7 @@
},
{
"cell_type": "markdown",
+ "id": "3904e7f8",
"metadata": {},
"source": [
"and we can describe the tip of the robot by a sequence of elementary transforms"
@@ -539,6 +590,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "f944d7c7",
"metadata": {},
"outputs": [],
"source": [
@@ -563,6 +615,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "f3403ac2",
"metadata": {},
"outputs": [],
"source": [
@@ -572,6 +625,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "5cc84d5f",
"metadata": {},
"outputs": [],
"source": [
@@ -581,6 +635,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "4b359ba4",
"metadata": {},
"outputs": [],
"source": [
@@ -589,6 +644,7 @@
},
{
"cell_type": "markdown",
+ "id": "6cd527a2",
"metadata": {},
"source": [
"We can compute not only forward kinematics, but also the differential kinematics"
@@ -597,6 +653,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "cf24b992",
"metadata": {},
"outputs": [],
"source": [
@@ -606,6 +663,7 @@
},
{
"cell_type": "markdown",
+ "id": "519116f1",
"metadata": {},
"source": [
"which relates the rate of change of the end-effector position to the rate of change of joint coordinates.\n",
@@ -616,6 +674,7 @@
},
{
"cell_type": "markdown",
+ "id": "357b6ba5",
"metadata": {},
"source": [
"The Hessian is a tensor (3-dimensional matrix) that relates joint velocity to end-effector acceleration $\\dot{\\nu}$"
@@ -624,6 +683,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "4461a5cc",
"metadata": {},
"outputs": [],
"source": [
@@ -632,6 +692,7 @@
},
{
"cell_type": "markdown",
+ "id": "83559b57",
"metadata": {},
"source": [
"## Compilation\n",
@@ -642,6 +703,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "7288738e",
"metadata": {},
"outputs": [],
"source": [
@@ -650,6 +712,7 @@
},
{
"cell_type": "markdown",
+ "id": "9ffd4455",
"metadata": {},
"source": [
"To make an ETS more efficient to run we can \"compile it\""
@@ -658,6 +721,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "1873953a",
"metadata": {},
"outputs": [],
"source": [
@@ -667,6 +731,7 @@
},
{
"cell_type": "markdown",
+ "id": "15fac11a",
"metadata": {},
"source": [
"and we can see that consecutive constant transforms have been folded into new \"not so elementary\" transforms denoted by `Ci`. The value of the ETS, is of course the same."
@@ -675,6 +740,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "e0288d66",
"metadata": {},
"outputs": [],
"source": [
@@ -683,6 +749,7 @@
},
{
"cell_type": "markdown",
+ "id": "a4004687",
"metadata": {},
"source": [
"## Explicit joint indices\n",
@@ -693,6 +760,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "2dd8c273",
"metadata": {},
"outputs": [],
"source": [
@@ -702,6 +770,7 @@
},
{
"cell_type": "markdown",
+ "id": "d966ca43",
"metadata": {},
"source": [
"we see that by default the wrist joints, joint 3-5 in the robot are associated with joints 0-2 in the wrist. If we had a vector of six joint angles, corresponding to the whole robot "
@@ -710,6 +779,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "ea0ef9d1",
"metadata": {},
"outputs": [],
"source": [
@@ -718,6 +788,7 @@
},
{
"cell_type": "markdown",
+ "id": "a6bbabf7",
"metadata": {},
"source": [
"then to evaluate the wrist we'd have to write"
@@ -726,6 +797,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "67a160ee",
"metadata": {},
"outputs": [],
"source": [
@@ -734,6 +806,7 @@
},
{
"cell_type": "markdown",
+ "id": "03d7473d",
"metadata": {},
"source": [
"which is quite doable, but imagine now that the ETS represents the transforms from the right hand to the left hand of a two armed robot. The joint numbers in that ETS would not be sequential with respect to the overall joint coordinate vector of the whole robot.\n",
@@ -744,6 +817,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "53383d22",
"metadata": {},
"outputs": [],
"source": [
@@ -767,6 +841,7 @@
},
{
"cell_type": "markdown",
+ "id": "acd9ac3f",
"metadata": {},
"source": [
"and when we display the ETS it looks no different, since the joint indices we assigned match the default sequential assignment. However if we now take the wrist part"
@@ -775,6 +850,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "9bd1d80f",
"metadata": {},
"outputs": [],
"source": [
@@ -784,6 +860,7 @@
},
{
"cell_type": "markdown",
+ "id": "8a03ec2c",
"metadata": {},
"source": [
"we see that the explicit joint indices are displayed as arguments of the ETs.\n",
@@ -794,6 +871,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "2856e577",
"metadata": {},
"outputs": [],
"source": [
@@ -802,6 +880,7 @@
},
{
"cell_type": "markdown",
+ "id": "18dec36c",
"metadata": {},
"source": [
"we get the same answer as earlier because it is referencing `q[3]`, `q[4]` and `q[5]`."
@@ -809,6 +888,7 @@
},
{
"cell_type": "markdown",
+ "id": "529d6c3c",
"metadata": {},
"source": [
"## Handling joint offsets\n",
@@ -829,6 +909,7 @@
},
{
"cell_type": "markdown",
+ "id": "eb9d0f73",
"metadata": {},
"source": [
"## Converting an ETS to a robot\n",
@@ -839,6 +920,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "bcebdbc3",
"metadata": {},
"outputs": [],
"source": [
@@ -848,6 +930,7 @@
},
{
"cell_type": "markdown",
+ "id": "28861093",
"metadata": {},
"source": [
"The ETS has been chopped into segments that connect links. Each link is a rigid-body with an attached coordinate frame. Relative to that frame is a short ETS that ends with joint transform that describes the coordinate frame of the next link in the chain.\n",
@@ -857,6 +940,7 @@
},
{
"cell_type": "markdown",
+ "id": "3dfbbb58",
"metadata": {},
"source": [
"## Converting a robot to ETS\n",
@@ -867,6 +951,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "e075473b",
"metadata": {},
"outputs": [],
"source": [
@@ -877,6 +962,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "8e5dcad8",
"metadata": {},
"outputs": [],
"source": [
@@ -885,6 +971,7 @@
},
{
"cell_type": "markdown",
+ "id": "735dfa27",
"metadata": {},
"source": [
"Or for a robot with a prismatic joint"
@@ -893,6 +980,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "ba16fc91",
"metadata": {},
"outputs": [],
"source": [
@@ -903,6 +991,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "97bf38a1",
"metadata": {},
"outputs": [],
"source": [
@@ -912,6 +1001,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "c551fbe0",
"metadata": {},
"outputs": [],
"source": []
@@ -924,54 +1014,9 @@
"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.8.5"
- },
- "varInspector": {
- "cols": {
- "lenName": 16,
- "lenType": 16,
- "lenVar": 40
- },
- "kernels_config": {
- "python": {
- "delete_cmd_postfix": "",
- "delete_cmd_prefix": "del ",
- "library": "var_list.py",
- "varRefreshCmd": "print(var_dic_list())"
- },
- "r": {
- "delete_cmd_postfix": ") ",
- "delete_cmd_prefix": "rm(",
- "library": "var_list.r",
- "varRefreshCmd": "cat(var_dic_list()) "
- }
- },
- "types_to_exclude": [
- "module",
- "function",
- "builtin_function_or_method",
- "instance",
- "_Feature"
- ],
- "window_display": false
- },
- "rtb_header": {
- "title": "Elementary transform sequences",
- "paragraphs": [
- "An introduction to the `ET` and `ETS` classes: building a robot model as a sequence of elementary transforms, evaluating its forward kinematics and Jacobians, and converting between an ETS and a robot."
- ],
- "modernised": "2026-10-19"
+ "name": "python"
}
},
"nbformat": 4,
- "nbformat_minor": 4
+ "nbformat_minor": 5
}
diff --git a/docs/notebooks/graphics-test.ipynb b/docs/notebooks/graphics-test.ipynb
index 94d6112c0..d8c66e31f 100644
--- a/docs/notebooks/graphics-test.ipynb
+++ b/docs/notebooks/graphics-test.ipynb
@@ -2,13 +2,13 @@
"cells": [
{
"cell_type": "markdown",
+ "id": "362a8fec",
"metadata": {},
"source": [
- "\n",
"\n",
" \n",
" Robotics Toolbox for Python | \n",
- "  | \n",
+ "  | \n",
"
\n",
"
\n",
"\n",
@@ -24,6 +24,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "b243eb35",
"metadata": {
"jupyter": {
"source_hidden": true
@@ -78,6 +79,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "bb43571a",
"metadata": {},
"outputs": [],
"source": [
@@ -92,34 +94,15 @@
}
],
"metadata": {
- "interpreter": {
- "hash": "b7d6b0d76025b9176285a6442c3dd6dd39bcfe7241029b7898b7106bd5e9b472"
- },
"kernelspec": {
- "display_name": "Python 3.8.5 64-bit ('dev': conda)",
+ "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.8.5"
- },
- "orig_nbformat": 4,
- "rtb_header": {
- "title": "Graphics test",
- "paragraphs": [
- "A one-cell check that plotting a robot works in your environment."
- ]
+ "name": "python"
}
},
"nbformat": 4,
- "nbformat_minor": 2
+ "nbformat_minor": 5
}
diff --git a/docs/notebooks/icra2021.ipynb b/docs/notebooks/icra2021.ipynb
index 8e6b2d387..2f2e86322 100644
--- a/docs/notebooks/icra2021.ipynb
+++ b/docs/notebooks/icra2021.ipynb
@@ -2,13 +2,13 @@
"cells": [
{
"cell_type": "markdown",
+ "id": "0065faab",
"metadata": {},
"source": [
- "\n",
"\n",
" \n",
" Robotics Toolbox for Python | \n",
- "  | \n",
+ "  | \n",
"
\n",
"
\n",
"\n",
@@ -28,6 +28,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "1a59f089",
"metadata": {
"jupyter": {
"source_hidden": true
@@ -82,6 +83,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "9bdb77dd",
"metadata": {},
"outputs": [],
"source": [
@@ -114,6 +116,7 @@
},
{
"cell_type": "markdown",
+ "id": "fccd7bc3",
"metadata": {},
"source": [
"# III.SPATIAL MATHEMATICS"
@@ -122,6 +125,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "575d2335",
"metadata": {},
"outputs": [],
"source": [
@@ -133,6 +137,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "5eafdde4",
"metadata": {},
"outputs": [],
"source": [
@@ -144,6 +149,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "4765e0c8",
"metadata": {},
"outputs": [],
"source": [
@@ -153,6 +159,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "87f59a29",
"metadata": {},
"outputs": [],
"source": [
@@ -162,6 +169,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "b9ab96bc",
"metadata": {},
"outputs": [],
"source": [
@@ -171,6 +179,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "5887c919",
"metadata": {},
"outputs": [],
"source": [
@@ -181,6 +190,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "a8c9c5b0",
"metadata": {},
"outputs": [],
"source": [
@@ -190,6 +200,7 @@
},
{
"cell_type": "markdown",
+ "id": "73993f11",
"metadata": {},
"source": [
"# IV. ROBOTICS TOOLBOX\n",
@@ -199,6 +210,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "e8ce61e1",
"metadata": {},
"outputs": [],
"source": [
@@ -215,6 +227,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "95745a32",
"metadata": {},
"outputs": [],
"source": [
@@ -231,6 +244,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "a350f331",
"metadata": {},
"outputs": [],
"source": [
@@ -240,6 +254,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "d202e6f5",
"metadata": {},
"outputs": [],
"source": [
@@ -249,6 +264,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "efc6d9ee",
"metadata": {},
"outputs": [],
"source": [
@@ -258,6 +274,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "df8baf59",
"metadata": {},
"outputs": [],
"source": [
@@ -267,6 +284,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "9dc57bf5",
"metadata": {},
"outputs": [],
"source": [
@@ -276,6 +294,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "98410a92",
"metadata": {},
"outputs": [],
"source": [
@@ -285,6 +304,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "e32ae17a",
"metadata": {},
"outputs": [],
"source": [
@@ -300,6 +320,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "ed5d45bc",
"metadata": {},
"outputs": [],
"source": [
@@ -322,6 +343,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "7fedc2ec",
"metadata": {},
"outputs": [],
"source": [
@@ -332,6 +354,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "477c2901",
"metadata": {
"lines_to_next_cell": 2
},
@@ -343,6 +366,7 @@
},
{
"cell_type": "markdown",
+ "id": "796c2401",
"metadata": {},
"source": [
"## B. Trajectories"
@@ -351,6 +375,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "f0c3fa38",
"metadata": {},
"outputs": [],
"source": [
@@ -361,6 +386,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "b275c405",
"metadata": {},
"outputs": [],
"source": [
@@ -371,6 +397,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "6b690343",
"metadata": {},
"outputs": [],
"source": [
@@ -380,6 +407,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "127e2a37",
"metadata": {},
"outputs": [],
"source": [
@@ -390,6 +418,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "b04aa6d3",
"metadata": {},
"outputs": [],
"source": [
@@ -399,6 +428,7 @@
},
{
"cell_type": "markdown",
+ "id": "71fd6549",
"metadata": {},
"source": [
"## C. Symbolic manipulation"
@@ -407,6 +437,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "9dc16ca6",
"metadata": {},
"outputs": [],
"source": [
@@ -419,6 +450,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "a333f26b",
"metadata": {},
"outputs": [],
"source": [
@@ -429,6 +461,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "221ef01e",
"metadata": {},
"outputs": [],
"source": [
@@ -440,6 +473,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "c10327f1",
"metadata": {},
"outputs": [],
"source": [
@@ -450,6 +484,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "d26cf89c",
"metadata": {},
"outputs": [],
"source": [
@@ -458,6 +493,7 @@
},
{
"cell_type": "markdown",
+ "id": "3e1af871",
"metadata": {},
"source": [
"## D. Differential kinematics"
@@ -466,6 +502,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "06a7bfae",
"metadata": {},
"outputs": [],
"source": [
@@ -475,6 +512,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "bfd86d43",
"metadata": {},
"outputs": [],
"source": [
@@ -484,6 +522,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "f19c45e1",
"metadata": {},
"outputs": [],
"source": [
@@ -493,6 +532,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "bf603029",
"metadata": {},
"outputs": [],
"source": [
@@ -503,6 +543,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "645ff100",
"metadata": {},
"outputs": [],
"source": [
@@ -512,6 +553,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "7267b88a",
"metadata": {},
"outputs": [],
"source": [
@@ -521,6 +563,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "bb095c57",
"metadata": {},
"outputs": [],
"source": [
@@ -530,6 +573,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "3ef4e6ec",
"metadata": {
"lines_to_next_cell": 2
},
@@ -540,6 +584,7 @@
},
{
"cell_type": "markdown",
+ "id": "df72eb26",
"metadata": {},
"source": [
"## E. Dynamics"
@@ -548,6 +593,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "dcbd3b3e",
"metadata": {
"lines_to_next_cell": 2
},
@@ -559,6 +605,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "facc4147",
"metadata": {
"lines_to_next_cell": 2
},
@@ -570,6 +617,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "4447e527",
"metadata": {
"lines_to_next_cell": 2
},
@@ -581,6 +629,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "436a1633",
"metadata": {
"lines_to_next_cell": 2
},
@@ -592,6 +641,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "9f1cd0c9",
"metadata": {
"lines_to_next_cell": 2
},
@@ -602,6 +652,7 @@
},
{
"cell_type": "markdown",
+ "id": "931b155c",
"metadata": {},
"source": [
"# V. NEW CAPABILITY\n",
@@ -611,6 +662,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "839ce028",
"metadata": {},
"outputs": [],
"source": [
@@ -624,6 +676,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "4e67c4d9",
"metadata": {},
"outputs": [],
"source": [
@@ -635,6 +688,7 @@
},
{
"cell_type": "markdown",
+ "id": "c1fcd470",
"metadata": {},
"source": [
"## C. Interfaces"
@@ -643,6 +697,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "7d052b79",
"metadata": {},
"outputs": [],
"source": [
@@ -652,6 +707,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "b3d35544",
"metadata": {},
"outputs": [],
"source": [
@@ -666,71 +722,15 @@
}
],
"metadata": {
- "jupytext": {
- "cell_metadata_filter": "-all",
- "encoding": "# -*- coding: utf-8 -*-",
- "notebook_metadata_filter": "-all",
- "text_representation": {
- "extension": ".py",
- "format_name": "light"
- }
- },
"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.8.5"
- },
- "varInspector": {
- "cols": {
- "lenName": 16,
- "lenType": 16,
- "lenVar": 40
- },
- "kernels_config": {
- "python": {
- "delete_cmd_postfix": "",
- "delete_cmd_prefix": "del ",
- "library": "var_list.py",
- "varRefreshCmd": "print(var_dic_list())"
- },
- "r": {
- "delete_cmd_postfix": ") ",
- "delete_cmd_prefix": "rm(",
- "library": "var_list.r",
- "varRefreshCmd": "cat(var_dic_list()) "
- }
- },
- "types_to_exclude": [
- "module",
- "function",
- "builtin_function_or_method",
- "instance",
- "_Feature"
- ],
- "window_display": false
- },
- "rtb_header": {
- "title": "Not your grandmother’s toolbox: the Robotics Toolbox reinvented for Python",
- "paragraphs": [
- "Peter Corke and Jesse Haviland",
- "This is the code for the examples in the paper published at ICRA 2021."
- ],
- "copyright": "Peter Corke and Jesse Haviland 2021, updated 2026",
- "modernised": "2026-10-19"
+ "name": "python"
}
},
"nbformat": 4,
- "nbformat_minor": 4
+ "nbformat_minor": 5
}
diff --git a/docs/notebooks/icra2026.ipynb b/docs/notebooks/icra2026.ipynb
index 77a186e40..7fa18c843 100644
--- a/docs/notebooks/icra2026.ipynb
+++ b/docs/notebooks/icra2026.ipynb
@@ -2,13 +2,13 @@
"cells": [
{
"cell_type": "markdown",
+ "id": "4b9f8521",
"metadata": {},
"source": [
- "\n",
"\n",
" \n",
" Robotics Toolbox for Python | \n",
- "  | \n",
+ "  | \n",
"
\n",
"
\n",
"\n",
@@ -24,6 +24,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "8c2c33ae",
"metadata": {
"jupyter": {
"source_hidden": true
@@ -1117,22 +1118,7 @@
"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.16"
- },
- "rtb_header": {
- "title": "🤖🚀 Robotics without the cruft",
- "paragraphs": [
- "Presented at ICRA 2026 as Keynote Tutorial 3 (the \"lost tutorial\")"
- ]
+ "name": "python"
}
},
"nbformat": 4,
diff --git a/docs/notebooks/ik-symbolic.ipynb b/docs/notebooks/ik-symbolic.ipynb
index 338edf904..83e59f74c 100644
--- a/docs/notebooks/ik-symbolic.ipynb
+++ b/docs/notebooks/ik-symbolic.ipynb
@@ -2,13 +2,13 @@
"cells": [
{
"cell_type": "markdown",
+ "id": "f20c8df0",
"metadata": {},
"source": [
- "\n",
"\n",
" \n",
" Robotics Toolbox for Python | \n",
- "  | \n",
+ "  | \n",
"
\n",
"
\n",
"\n",
@@ -43,6 +43,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "cf4a9e24",
"metadata": {
"jupyter": {
"source_hidden": true
@@ -789,30 +790,17 @@
],
"metadata": {
"kernelspec": {
- "display_name": "dev",
+ "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.12.14"
+ "name": "python"
},
"rtb_bootstrap": {
"extras": [
"ik"
]
- },
- "rtb_header": {
- "title": "Analytic inverse kinematics",
- "paragraphs": []
}
},
"nbformat": 4,
diff --git a/docs/notebooks/ik_benchmark.ipynb b/docs/notebooks/ik_benchmark.ipynb
index 3edc2761f..ffe69585d 100644
--- a/docs/notebooks/ik_benchmark.ipynb
+++ b/docs/notebooks/ik_benchmark.ipynb
@@ -2,13 +2,13 @@
"cells": [
{
"cell_type": "markdown",
+ "id": "d8cda822",
"metadata": {},
"source": [
- "\n",
"\n",
" \n",
" Robotics Toolbox for Python | \n",
- "  | \n",
+ "  | \n",
"
\n",
"
\n",
"\n",
@@ -22,6 +22,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "c937d041",
"metadata": {
"jupyter": {
"source_hidden": true
@@ -76,6 +77,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "81f01e8a",
"metadata": {},
"outputs": [],
"source": [
@@ -111,6 +113,7 @@
{
"attachments": {},
"cell_type": "markdown",
+ "id": "08b20209",
"metadata": {},
"source": [
"In this Notebook we benchmark the performance of each numerical IK solver we provide with the toolbox.\n",
@@ -136,6 +139,7 @@
{
"attachments": {},
"cell_type": "markdown",
+ "id": "ebf33971",
"metadata": {},
"source": [
"
\n",
@@ -149,6 +153,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "c48854fb",
"metadata": {},
"outputs": [],
"source": [
@@ -160,6 +165,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "4e755bd7",
"metadata": {},
"outputs": [],
"source": [
@@ -193,6 +199,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "17b2b8c8",
"metadata": {},
"outputs": [],
"source": [
@@ -262,6 +269,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "6a0ddecb",
"metadata": {},
"outputs": [],
"source": [
@@ -301,32 +309,14 @@
],
"metadata": {
"kernelspec": {
- "display_name": "rtb",
+ "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.11.0"
- },
- "orig_nbformat": 4,
- "vscode": {
- "interpreter": {
- "hash": "4e8f2602a343ae62ff483b4df7805ab0eb447ccc1a56caf0631d5620d36b21ab"
- }
- },
- "rtb_header": {
- "title": "Numerical inverse kinematics benchmark"
+ "name": "python"
}
},
"nbformat": 4,
- "nbformat_minor": 2
+ "nbformat_minor": 5
}
diff --git a/docs/notebooks/index.ipynb b/docs/notebooks/index.ipynb
index 0f37cfddc..f2fce1f46 100644
--- a/docs/notebooks/index.ipynb
+++ b/docs/notebooks/index.ipynb
@@ -2,12 +2,13 @@
"cells": [
{
"cell_type": "markdown",
+ "id": "c63145b0",
"metadata": {},
"source": [
"\n",
" \n",
" Robotics Toolbox for Python | \n",
- "  | \n",
+ "  | \n",
"
\n",
"
\n",
"\n",
diff --git a/docs/notebooks/iros2020.ipynb b/docs/notebooks/iros2020.ipynb
index c2921cd35..6443f17b3 100644
--- a/docs/notebooks/iros2020.ipynb
+++ b/docs/notebooks/iros2020.ipynb
@@ -2,13 +2,13 @@
"cells": [
{
"cell_type": "markdown",
+ "id": "42f1fc26",
"metadata": {},
"source": [
- "\n",
"\n",
" \n",
" Robotics Toolbox for Python | \n",
- "  | \n",
+ "  | \n",
"
\n",
"
\n",
"\n",
@@ -23,6 +23,7 @@
},
{
"cell_type": "markdown",
+ "id": "84252cb7",
"metadata": {},
"source": [
"This notebook corresponds to my virtual tutorial at the IROS 2020 workshop on Screw Theory and Geometric Robot Dynamics.\n",
@@ -33,6 +34,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "c7709dc8",
"metadata": {
"jupyter": {
"source_hidden": true
@@ -87,6 +89,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "caa1b535",
"metadata": {},
"outputs": [],
"source": [
@@ -108,6 +111,7 @@
},
{
"cell_type": "markdown",
+ "id": "d41c6dbc",
"metadata": {},
"source": [
"## Creating and using a simple Denavit-Hartenberg model\n",
@@ -117,6 +121,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "823706e9",
"metadata": {},
"outputs": [],
"source": [
@@ -125,6 +130,7 @@
},
{
"cell_type": "markdown",
+ "id": "0b9b5814",
"metadata": {},
"source": [
"which has created an object in the local _workspace_ which subclasses the `DHRobot` class which subclasses the top-level `Robot` class. If we print this we get some detailed information about it's kinematics as well as the base and tool transforms, and some predefined joint configurations"
@@ -133,6 +139,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "e6532611",
"metadata": {},
"outputs": [],
"source": [
@@ -141,6 +148,7 @@
},
{
"cell_type": "markdown",
+ "id": "c4a979a4",
"metadata": {},
"source": [
"and we can plot the robot in its zero-joint-angle configuration"
@@ -149,6 +157,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "cfe97914",
"metadata": {},
"outputs": [],
"source": [
@@ -157,6 +166,7 @@
},
{
"cell_type": "markdown",
+ "id": "f1349186",
"metadata": {},
"source": [
"For an arbitrary set of joint angles"
@@ -165,6 +175,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "8fededc2",
"metadata": {},
"outputs": [],
"source": [
@@ -173,6 +184,7 @@
},
{
"cell_type": "markdown",
+ "id": "85163e8f",
"metadata": {},
"source": [
"we can compute the end-effector pose using forward kinematics, taking into account the base and tool transforms"
@@ -181,6 +193,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "3b8abe37",
"metadata": {},
"outputs": [],
"source": [
@@ -190,6 +203,7 @@
},
{
"cell_type": "markdown",
+ "id": "19493c65",
"metadata": {},
"source": [
"the result is an SE(3) matrix, an instance of an `SE3` object. It is shown with the rotational part in red, the translational part in blue, and the constant part in grey.\n",
@@ -200,6 +214,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "2a7c48ad",
"metadata": {},
"outputs": [],
"source": [
@@ -209,6 +224,7 @@
},
{
"cell_type": "markdown",
+ "id": "050ef302",
"metadata": {},
"source": [
"_Note: that `ikine_LM` returns an `IKSolution` object, with the joint coordinates in its `q` attribute and the status of the solution in other attributes, which we are ignoring here_\n",
@@ -219,6 +235,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "2eea6dde",
"metadata": {},
"outputs": [],
"source": [
@@ -227,6 +244,7 @@
},
{
"cell_type": "markdown",
+ "id": "043a9acf",
"metadata": {},
"source": [
"and the Jacobian, in the world frame (indicated by the `0` suffix) is"
@@ -235,6 +253,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "7d510083",
"metadata": {
"scrolled": false
},
@@ -246,6 +265,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "07222729",
"metadata": {},
"outputs": [],
"source": [
@@ -254,6 +274,7 @@
},
{
"cell_type": "markdown",
+ "id": "8d952da9",
"metadata": {},
"source": [
"the Jacobian in the end-effector frame is given by `jacobe`."
@@ -261,6 +282,7 @@
},
{
"cell_type": "markdown",
+ "id": "087795dc",
"metadata": {},
"source": [
"## More models\n",
@@ -270,6 +292,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "975e3e96",
"metadata": {},
"outputs": [],
"source": [
@@ -278,6 +301,7 @@
},
{
"cell_type": "markdown",
+ "id": "30d0599f",
"metadata": {},
"source": [
"which highlights the three categories of robot models supported by the toolbox:\n",
@@ -289,6 +313,7 @@
},
{
"cell_type": "markdown",
+ "id": "8cc47510",
"metadata": {},
"source": [
"### Elementary transform sequence (ETS) models\n",
@@ -298,6 +323,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "7a0b425a",
"metadata": {},
"outputs": [],
"source": [
@@ -313,6 +339,7 @@
},
{
"cell_type": "markdown",
+ "id": "f3ef7f27",
"metadata": {},
"source": [
"Consider the robot in a configuration where it is stretched out vertically, this will be our zero-joint-angle configuration. Then starting at the base we define a series of rigid-body motions all the way to the tip. The motion can be:\n",
@@ -324,6 +351,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "75f3a39a",
"metadata": {},
"outputs": [],
"source": [
@@ -345,6 +373,7 @@
},
{
"cell_type": "markdown",
+ "id": "fe827a2e",
"metadata": {},
"source": [
"The overloaded `*` indicates composition of the rigid-body motions. The total number of elementary transforms is"
@@ -353,6 +382,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "adef41af",
"metadata": {},
"outputs": [],
"source": [
@@ -361,6 +391,7 @@
},
{
"cell_type": "markdown",
+ "id": "ed94d528",
"metadata": {},
"source": [
"and the elements which are joints are"
@@ -369,6 +400,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "0d4bc8d2",
"metadata": {},
"outputs": [],
"source": [
@@ -377,6 +409,7 @@
},
{
"cell_type": "markdown",
+ "id": "0472fc55",
"metadata": {},
"source": [
"and we can pretty print the transform sequence by"
@@ -385,6 +418,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "ba3d35dc",
"metadata": {},
"outputs": [],
"source": [
@@ -393,6 +427,7 @@
},
{
"cell_type": "markdown",
+ "id": "49ef48bb",
"metadata": {},
"source": [
"We can also evaluate the transform sequence, by subsituting in values for the joint angles. They are taken from consecutive elemements of the joint-angle vector provide to the `eval` method"
@@ -401,6 +436,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "587acb32",
"metadata": {},
"outputs": [],
"source": [
@@ -410,6 +446,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "7c8d984e",
"metadata": {},
"outputs": [],
"source": [
@@ -418,6 +455,7 @@
},
{
"cell_type": "markdown",
+ "id": "33adf87c",
"metadata": {},
"source": [
"and this is the forward kinematics of a robot described by this elementary transform sequence."
@@ -425,6 +463,7 @@
},
{
"cell_type": "markdown",
+ "id": "5f4f21d4",
"metadata": {},
"source": [
"## Robot dynamics\n",
@@ -434,6 +473,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "23d9a236",
"metadata": {},
"outputs": [],
"source": [
@@ -444,6 +484,7 @@
},
{
"cell_type": "markdown",
+ "id": "0c7272cc",
"metadata": {},
"source": [
"and then compute the inverse dynamics using the recursive Newton-Euler algorithm"
@@ -452,6 +493,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "880a2a2d",
"metadata": {},
"outputs": [],
"source": [
@@ -460,6 +502,7 @@
},
{
"cell_type": "markdown",
+ "id": "c3108943",
"metadata": {},
"source": [
"which is written in C for performance. It's actually the same 20 year C-code used in the MATLAB version of the Toolbox!\n",
@@ -470,6 +513,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "485886bc",
"metadata": {},
"outputs": [],
"source": [
@@ -478,6 +522,7 @@
},
{
"cell_type": "markdown",
+ "id": "780d7706",
"metadata": {},
"source": [
"the velocity terms"
@@ -486,6 +531,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "31a6ab42",
"metadata": {},
"outputs": [],
"source": [
@@ -494,6 +540,7 @@
},
{
"cell_type": "markdown",
+ "id": "78ca8306",
"metadata": {},
"source": [
"where the second argument is the joint velocuty.\n",
@@ -504,6 +551,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "23e4794d",
"metadata": {},
"outputs": [],
"source": [
@@ -512,6 +560,7 @@
},
{
"cell_type": "markdown",
+ "id": "d0a28297",
"metadata": {},
"source": [
"If we set the joint torques to zero, ie. turn the motors off"
@@ -520,6 +569,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "3475f70c",
"metadata": {},
"outputs": [],
"source": [
@@ -528,6 +578,7 @@
},
{
"cell_type": "markdown",
+ "id": "e72fdc09",
"metadata": {},
"source": [
"then the resulting joint angle acceleration will be"
@@ -536,6 +587,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "2ee705dd",
"metadata": {},
"outputs": [],
"source": [
@@ -544,6 +596,7 @@
},
{
"cell_type": "markdown",
+ "id": "fb0f1a31",
"metadata": {},
"source": [
"which we can integrate over time to obtain a joint angle trajectory"
@@ -552,6 +605,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "666f40b2",
"metadata": {},
"outputs": [],
"source": [
@@ -561,6 +615,7 @@
},
{
"cell_type": "markdown",
+ "id": "964e3465",
"metadata": {},
"source": [
"Note: _Coulomb friction is a harsh non-linearity and it causes the numerical integrator to take very small times steps, so the result will take many minutes to compute. To speed things up, at the expense of some modeling fidelity, we set the Coulomb friction to zero, but retain the viscous friction. The `nofriction()` method returns a clone of the robot with its friction parameters modified._"
@@ -568,6 +623,7 @@
},
{
"cell_type": "markdown",
+ "id": "8bb71d63",
"metadata": {},
"source": [
"# Twists and screws\n",
@@ -579,6 +635,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "1af83003",
"metadata": {},
"outputs": [],
"source": [
@@ -589,6 +646,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "95d63981",
"metadata": {},
"outputs": [],
"source": [
@@ -598,6 +656,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "dca10a9e",
"metadata": {},
"outputs": [],
"source": [
@@ -608,6 +667,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "76017aec",
"metadata": {},
"outputs": [],
"source": [
@@ -616,6 +676,7 @@
},
{
"cell_type": "markdown",
+ "id": "264adf0b",
"metadata": {},
"source": [
"## 3D twists\n",
@@ -625,6 +686,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "420331bf",
"metadata": {},
"outputs": [],
"source": [
@@ -633,6 +695,7 @@
},
{
"cell_type": "markdown",
+ "id": "02b7149e",
"metadata": {},
"source": [
" which is a 6-vector encapsulated in an instance of a `Twist3` object. A prismatic twist has just the direction of its line of action"
@@ -641,6 +704,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "455a5bc6",
"metadata": {},
"outputs": [],
"source": [
@@ -649,6 +713,7 @@
},
{
"cell_type": "markdown",
+ "id": "d6302fb0",
"metadata": {},
"source": [
"We can convert an SE(3) matrix, encapsulated in an instance of an `SE3` object to a 3D twist"
@@ -657,6 +722,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "6cca3eb4",
"metadata": {},
"outputs": [],
"source": [
@@ -667,6 +733,7 @@
},
{
"cell_type": "markdown",
+ "id": "d28e730e",
"metadata": {},
"source": [
"Twists have a number of properties and methods. The moment is a 3-vector"
@@ -675,6 +742,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "0d8bf900",
"metadata": {},
"outputs": [],
"source": [
@@ -683,6 +751,7 @@
},
{
"cell_type": "markdown",
+ "id": "e21d9b81",
"metadata": {},
"source": [
"as is the direction part"
@@ -691,6 +760,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "e333a7b5",
"metadata": {},
"outputs": [],
"source": [
@@ -699,6 +769,7 @@
},
{
"cell_type": "markdown",
+ "id": "e6a42e38",
"metadata": {},
"source": [
"A twist also has a pitch"
@@ -707,6 +778,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "47de6e4b",
"metadata": {},
"outputs": [],
"source": [
@@ -715,6 +787,7 @@
},
{
"cell_type": "markdown",
+ "id": "3bfc9a07",
"metadata": {},
"source": [
"and a pole"
@@ -723,6 +796,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "974c6771",
"metadata": {},
"outputs": [],
"source": [
@@ -731,6 +805,7 @@
},
{
"cell_type": "markdown",
+ "id": "08449d6d",
"metadata": {},
"source": [
"The line of action of the twist is a line in 3D space which can be represented by a Toolbox Plucker object"
@@ -739,6 +814,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "39f22934",
"metadata": {},
"outputs": [],
"source": [
@@ -747,6 +823,7 @@
},
{
"cell_type": "markdown",
+ "id": "28b6c645",
"metadata": {},
"source": [
"which we could plot, by first defining a plot volume"
@@ -755,6 +832,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "f3f444b3",
"metadata": {},
"outputs": [],
"source": [
@@ -768,6 +846,7 @@
},
{
"cell_type": "markdown",
+ "id": "e322fbb0",
"metadata": {},
"source": [
"## Exponentiation of twists\n",
@@ -777,6 +856,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "5b728c95",
"metadata": {},
"outputs": [],
"source": [
@@ -785,6 +865,7 @@
},
{
"cell_type": "markdown",
+ "id": "0e73d2eb",
"metadata": {},
"source": [
"which is of course the identity matrix, and"
@@ -793,6 +874,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "1e19870e",
"metadata": {},
"outputs": [],
"source": [
@@ -801,6 +883,7 @@
},
{
"cell_type": "markdown",
+ "id": "cf13c21e",
"metadata": {},
"source": [
"All Toolbox objects can contain multiple values, so in this example, we exponentiate the twist with a sequence of values, resulting in a single `SE3` object that contains a sequence of SE(3) values."
@@ -809,6 +892,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "3b039f93",
"metadata": {},
"outputs": [],
"source": [
@@ -818,6 +902,7 @@
},
{
"cell_type": "markdown",
+ "id": "e24981ab",
"metadata": {},
"source": [
"and we access them in a familiar Pythonic way"
@@ -826,6 +911,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "2465a8a7",
"metadata": {},
"outputs": [],
"source": [
@@ -835,6 +921,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "ef1a9fd2",
"metadata": {},
"outputs": [],
"source": [
@@ -843,6 +930,7 @@
},
{
"cell_type": "markdown",
+ "id": "129bd6bd",
"metadata": {},
"source": [
"## Product of exponentials"
@@ -850,6 +938,7 @@
},
{
"cell_type": "markdown",
+ "id": "4b762a6d",
"metadata": {},
"source": [
"For the robot model we created earlier, we can extract its product of exponentials representation, a set of six twists and a transformation"
@@ -858,6 +947,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "f0806c59",
"metadata": {},
"outputs": [],
"source": [
@@ -866,6 +956,7 @@
},
{
"cell_type": "markdown",
+ "id": "fa4b8a6d",
"metadata": {},
"source": [
"where the transformation for zero-joint-angles is"
@@ -874,6 +965,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "1fe17d94",
"metadata": {},
"outputs": [],
"source": [
@@ -882,6 +974,7 @@
},
{
"cell_type": "markdown",
+ "id": "053f469e",
"metadata": {},
"source": [
"and the twists are"
@@ -890,6 +983,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "4b17d142",
"metadata": {},
"outputs": [],
"source": [
@@ -898,6 +992,7 @@
},
{
"cell_type": "markdown",
+ "id": "46012989",
"metadata": {},
"source": [
"For a nominal joint-angle configuration"
@@ -906,6 +1001,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "f2ff1f05",
"metadata": {},
"outputs": [],
"source": [
@@ -914,6 +1010,7 @@
},
{
"cell_type": "markdown",
+ "id": "00f9a9c8",
"metadata": {},
"source": [
"we can exponentiate the twists"
@@ -922,6 +1019,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "aa15acca",
"metadata": {},
"outputs": [],
"source": [
@@ -930,6 +1028,7 @@
},
{
"cell_type": "markdown",
+ "id": "3e37280e",
"metadata": {},
"source": [
"where the result is again an `SE3` object with multiple values, six in this case.\n",
@@ -940,6 +1039,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "b738bfac",
"metadata": {},
"outputs": [],
"source": [
@@ -948,6 +1048,7 @@
},
{
"cell_type": "markdown",
+ "id": "67a0d5f2",
"metadata": {},
"source": [
"which of course is identical to the classical approach "
@@ -956,6 +1057,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "07a8cca5",
"metadata": {},
"outputs": [],
"source": [
@@ -964,6 +1066,7 @@
},
{
"cell_type": "markdown",
+ "id": "de069bd8",
"metadata": {},
"source": [
"Extending an earlier example, the lines of actions of the twists are"
@@ -972,6 +1075,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "05725261",
"metadata": {},
"outputs": [],
"source": [
@@ -981,6 +1085,7 @@
},
{
"cell_type": "markdown",
+ "id": "39324a5f",
"metadata": {},
"source": [
"Which we can overlay on a plot of the robot in its zero-angle configuration"
@@ -989,6 +1094,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "25fd9ebd",
"metadata": {
"scrolled": true
},
@@ -1000,6 +1106,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "894bd19e",
"metadata": {},
"outputs": [],
"source": [
@@ -1009,67 +1116,15 @@
}
],
"metadata": {
- "anaconda-cloud": {},
"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.8.5"
- },
- "varInspector": {
- "cols": {
- "lenName": 16,
- "lenType": 16,
- "lenVar": 40
- },
- "kernels_config": {
- "python": {
- "delete_cmd_postfix": "",
- "delete_cmd_prefix": "del ",
- "library": "var_list.py",
- "varRefreshCmd": "print(var_dic_list())"
- },
- "r": {
- "delete_cmd_postfix": ") ",
- "delete_cmd_prefix": "rm(",
- "library": "var_list.r",
- "varRefreshCmd": "cat(var_dic_list()) "
- }
- },
- "position": {
- "height": "144px",
- "left": "854px",
- "right": "20px",
- "top": "99px",
- "width": "350px"
- },
- "types_to_exclude": [
- "module",
- "function",
- "builtin_function_or_method",
- "instance",
- "_Feature"
- ],
- "window_display": false
- },
- "rtb_header": {
- "title": "IROS 2020 workshop: review on screw theory and geometric robot dynamics",
- "paragraphs": [],
- "copyright": "Peter Corke 2020, updated 2026",
- "modernised": "2026-10-19"
+ "name": "python"
}
},
"nbformat": 4,
- "nbformat_minor": 4
+ "nbformat_minor": 5
}
diff --git a/docs/notebooks/kinematics.ipynb b/docs/notebooks/kinematics.ipynb
index 955325fa3..7b59c38b1 100644
--- a/docs/notebooks/kinematics.ipynb
+++ b/docs/notebooks/kinematics.ipynb
@@ -2,13 +2,13 @@
"cells": [
{
"cell_type": "markdown",
+ "id": "4eb2352d",
"metadata": {},
"source": [
- "\n",
"\n",
" \n",
" Robotics Toolbox for Python | \n",
- "  | \n",
+ "  | \n",
"
\n",
"
\n",
"\n",
@@ -24,6 +24,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "d7d8472b",
"metadata": {
"jupyter": {
"source_hidden": true
@@ -78,6 +79,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "3f97f1fa",
"metadata": {},
"outputs": [],
"source": [
@@ -99,6 +101,7 @@
},
{
"cell_type": "markdown",
+ "id": "ef971274",
"metadata": {},
"source": [
"The Toolbox supports models defined using a number of different conventions. We will load a very classical model, a Puma560 robot defined in terms of standard Denavit-Hartenberg parameters"
@@ -107,6 +110,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "acacd545",
"metadata": {},
"outputs": [],
"source": [
@@ -115,6 +119,7 @@
},
{
"cell_type": "markdown",
+ "id": "329070d5",
"metadata": {},
"source": [
"Now we can display the simple Denavit-Hartenberg parameter model"
@@ -123,6 +128,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "47e6f7f0",
"metadata": {},
"outputs": [],
"source": [
@@ -131,6 +137,7 @@
},
{
"cell_type": "markdown",
+ "id": "bdf3fbed",
"metadata": {},
"source": [
"The first table shows the kinematic parameters, and from the column titles we can see clearly that this is expressed in terms of standard Denavit-Hartenberg parameters. The first column shows that the joint variables qi are rotations since they are in the θ column. Joint limits are also shown. Joint flip (motion in the opposite sense) would be indicated by the joint variable being shown as for example like `-q3`, and joint offsets by being shown as for example like `q2 + 45°`.\n",
@@ -141,6 +148,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "bfd5ed4b",
"metadata": {},
"outputs": [],
"source": [
@@ -149,6 +157,7 @@
},
{
"cell_type": "markdown",
+ "id": "c021e3fa",
"metadata": {},
"source": [
"If the robot had a base or tool transform they would be listed in this table also.\n",
@@ -159,6 +168,7 @@
},
{
"cell_type": "markdown",
+ "id": "953eb52b",
"metadata": {},
"source": [
"We can easily display the robot graphically"
@@ -167,6 +177,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "5bb698bb",
"metadata": {
"scrolled": false
},
@@ -177,6 +188,7 @@
},
{
"cell_type": "markdown",
+ "id": "dd3a1595",
"metadata": {},
"source": [
"where `qn` is one of the named configurations shown above, and has the robot positioned to work above a table top. You can use the mouse to rotate the plot and view the robot from different directions. The grey line is the _shadow_ which is a projection of the robot onto the xy-plane.\n",
@@ -187,6 +199,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "fccd07f1",
"metadata": {},
"outputs": [],
"source": [
@@ -195,6 +208,7 @@
},
{
"cell_type": "markdown",
+ "id": "64933906",
"metadata": {},
"source": [
"which is a 4x4 SE(3) matrix displayed in a color coded way with rotation matrix in red, translation vector in blue, and constant elements in grey. This is an instance of an `SE3` object safely encapsulates the SE(3) matrix. This class, and related ones, are implemented by the [Spatial Math Toolbox for Python](https://github.com/petercorke/spatialmath-python).\n",
@@ -207,6 +221,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "874cb125",
"metadata": {
"scrolled": false
},
@@ -217,6 +232,7 @@
},
{
"cell_type": "markdown",
+ "id": "2e177824",
"metadata": {},
"source": [
"An important problem in robotics is _inverse kinematics_, determining the joint angles to put the robot's end effector at a particular pose.\n",
@@ -232,6 +248,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "2f2dd38c",
"metadata": {},
"outputs": [],
"source": [
@@ -241,6 +258,7 @@
},
{
"cell_type": "markdown",
+ "id": "9ad7d122",
"metadata": {},
"source": [
"Now we can compute the joint angles that results in this pose"
@@ -249,6 +267,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "44561647",
"metadata": {},
"outputs": [],
"source": [
@@ -257,6 +276,7 @@
},
{
"cell_type": "markdown",
+ "id": "af5040f8",
"metadata": {},
"source": [
"which returns the joint coordinates as well as solution status"
@@ -265,6 +285,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "3439b7da",
"metadata": {},
"outputs": [],
"source": [
@@ -273,6 +294,7 @@
},
{
"cell_type": "markdown",
+ "id": "78f05e6a",
"metadata": {},
"source": [
"indicating, in this case, that there is no failure. The joint coordinates are"
@@ -281,6 +303,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "dcde0ff3",
"metadata": {},
"outputs": [],
"source": [
@@ -289,6 +312,7 @@
},
{
"cell_type": "markdown",
+ "id": "e650dd68",
"metadata": {},
"source": [
"and we can confirm that this is indeed an inverse kinematic solution by computing the forward kinematics"
@@ -297,6 +321,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "ef04ecf0",
"metadata": {},
"outputs": [],
"source": [
@@ -305,6 +330,7 @@
},
{
"cell_type": "markdown",
+ "id": "851b1a98",
"metadata": {},
"source": [
"which matches the original transform."
@@ -312,6 +338,7 @@
},
{
"cell_type": "markdown",
+ "id": "3374002f",
"metadata": {},
"source": [
"A simple trajectory between two joint configuration is"
@@ -320,6 +347,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "b017a430",
"metadata": {},
"outputs": [],
"source": [
@@ -328,6 +356,7 @@
},
{
"cell_type": "markdown",
+ "id": "d503f249",
"metadata": {},
"source": [
"The result is a _namedtuple_ with attributes `q` containing the joint angles, as well as `qd`, `qdd` and `t` which hold the joint velocity, joint accelerations and time respectively. \n",
@@ -338,6 +367,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "8ace240a",
"metadata": {
"scrolled": true
},
@@ -348,6 +378,7 @@
},
{
"cell_type": "markdown",
+ "id": "72b2d59e",
"metadata": {},
"source": [
"We can plot this trajectory as a function of time using the convenience method `qplot`"
@@ -356,6 +387,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "634d1126",
"metadata": {},
"outputs": [],
"source": [
@@ -364,6 +396,7 @@
},
{
"cell_type": "markdown",
+ "id": "3b54aa99",
"metadata": {},
"source": [
"and then we can animate this"
@@ -372,6 +405,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "e18004d7",
"metadata": {},
"outputs": [],
"source": [
@@ -380,6 +414,7 @@
},
{
"cell_type": "markdown",
+ "id": "adc7bfe2",
"metadata": {},
"source": [
"_Note: animation not working in Jupyter..._"
@@ -387,6 +422,7 @@
},
{
"cell_type": "markdown",
+ "id": "a05c484c",
"metadata": {},
"source": [
"The inverse kinematic solution was found using an iterative numerical procedure. It is quite general but it has several drawbacks:\n",
@@ -404,6 +440,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "d7b4b125",
"metadata": {},
"outputs": [],
"source": [
@@ -412,6 +449,7 @@
},
{
"cell_type": "markdown",
+ "id": "cd7b6def",
"metadata": {},
"source": [
"This greatly simplifies things because the last three joints only control orientation and have no effect on the end-effector position. This means that only the first three joints define position $(x_e, y_e, z_e)$. Three joints that control three unknowns is relatively easy to solve for, and analytical solutions (complex trigonmetric equations) can be found, and in fact have been published for most industrial robot manipulators.\n",
@@ -422,6 +460,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "ee3adf9e",
"metadata": {},
"outputs": [],
"source": [
@@ -431,6 +470,7 @@
},
{
"cell_type": "markdown",
+ "id": "dcbb23c1",
"metadata": {},
"source": [
"which is different to the values found earlier, but we can verify it is a valid solution"
@@ -439,6 +479,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "7f989d9c",
"metadata": {},
"outputs": [],
"source": [
@@ -447,6 +488,7 @@
},
{
"cell_type": "markdown",
+ "id": "064fb467",
"metadata": {},
"source": [
"In fact the solution we found earlier, but didn't explicitly specify, is the right-handed elbow-up configuration"
@@ -455,6 +497,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "dc237281",
"metadata": {},
"outputs": [],
"source": [
@@ -464,6 +507,7 @@
},
{
"cell_type": "markdown",
+ "id": "4d65d29d",
"metadata": {},
"source": [
"Other useful functions include the manipulator Jacobian which maps joint velocity to end-effector velocity expressed in the world frame"
@@ -472,6 +516,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "0952d140",
"metadata": {},
"outputs": [],
"source": [
@@ -481,6 +526,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "1d5bea0e",
"metadata": {},
"outputs": [],
"source": []
@@ -493,66 +539,9 @@
"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.8.5"
- },
- "toc": {
- "base_numbering": 1,
- "nav_menu": {},
- "number_sections": true,
- "sideBar": true,
- "skip_h1_title": false,
- "title_cell": "Table of Contents",
- "title_sidebar": "Contents",
- "toc_cell": false,
- "toc_position": {},
- "toc_section_display": true,
- "toc_window_display": false
- },
- "varInspector": {
- "cols": {
- "lenName": 16,
- "lenType": 16,
- "lenVar": 40
- },
- "kernels_config": {
- "python": {
- "delete_cmd_postfix": "",
- "delete_cmd_prefix": "del ",
- "library": "var_list.py",
- "varRefreshCmd": "print(var_dic_list())"
- },
- "r": {
- "delete_cmd_postfix": ") ",
- "delete_cmd_prefix": "rm(",
- "library": "var_list.r",
- "varRefreshCmd": "cat(var_dic_list()) "
- }
- },
- "types_to_exclude": [
- "module",
- "function",
- "builtin_function_or_method",
- "instance",
- "_Feature"
- ],
- "window_display": false
- },
- "rtb_header": {
- "title": "Robot kinematics",
- "paragraphs": [
- "Forward and inverse kinematics, a joint-space trajectory and the Jacobian of a Puma 560 robot."
- ]
+ "name": "python"
}
},
"nbformat": 4,
- "nbformat_minor": 4
+ "nbformat_minor": 5
}
diff --git a/docs/notebooks/symbolic.ipynb b/docs/notebooks/symbolic.ipynb
index dbad257d6..b509a5a47 100644
--- a/docs/notebooks/symbolic.ipynb
+++ b/docs/notebooks/symbolic.ipynb
@@ -2,13 +2,13 @@
"cells": [
{
"cell_type": "markdown",
+ "id": "0e39b490",
"metadata": {},
"source": [
- "\n",
"\n",
" \n",
" Robotics Toolbox for Python | \n",
- "  | \n",
+ "  | \n",
"
\n",
"
\n",
"\n",
@@ -26,6 +26,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "8fb6aeb6",
"metadata": {
"jupyter": {
"source_hidden": true
@@ -80,6 +81,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "3c085bb6",
"metadata": {},
"outputs": [],
"source": [
@@ -95,6 +97,7 @@
},
{
"cell_type": "markdown",
+ "id": "70cda885",
"metadata": {},
"source": [
"We use the Spatial Math Toolbox wrapper of SymPy which is `spatialmath.base.symbolic` which is imported above.\n",
@@ -106,6 +109,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "1a5df959",
"metadata": {},
"outputs": [],
"source": [
@@ -115,6 +119,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "51ddfd57",
"metadata": {},
"outputs": [],
"source": [
@@ -123,6 +128,7 @@
},
{
"cell_type": "markdown",
+ "id": "1c08445e",
"metadata": {},
"source": [
"which displays as a Greek letter, it has the type"
@@ -131,6 +137,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "130a18ae",
"metadata": {},
"outputs": [],
"source": [
@@ -139,6 +146,7 @@
},
{
"cell_type": "markdown",
+ "id": "f46e3050",
"metadata": {},
"source": [
"The function specifies that the symbolic variables are real valued by default, which will simplify subsequent simplification steps"
@@ -147,6 +155,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "c1f363bf",
"metadata": {},
"outputs": [],
"source": [
@@ -155,6 +164,7 @@
},
{
"cell_type": "markdown",
+ "id": "7d03e967",
"metadata": {},
"source": [
"We can test if a variable is symbolic"
@@ -163,6 +173,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "c19eb5ac",
"metadata": {},
"outputs": [],
"source": [
@@ -172,6 +183,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "b4aa94d7",
"metadata": {},
"outputs": [],
"source": [
@@ -180,6 +192,7 @@
},
{
"cell_type": "markdown",
+ "id": "5016dfa9",
"metadata": {},
"source": [
"## Symbolics with the Spatial Math Toolbox\n",
@@ -189,6 +202,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "e612eba7",
"metadata": {},
"outputs": [],
"source": [
@@ -198,6 +212,7 @@
},
{
"cell_type": "markdown",
+ "id": "f34c6ea9",
"metadata": {},
"source": [
"and return a NumPy array with symbolic values. \n",
@@ -208,6 +223,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "f9e71c5f",
"metadata": {},
"outputs": [],
"source": [
@@ -218,6 +234,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "be3f7dff",
"metadata": {},
"outputs": [],
"source": [
@@ -227,6 +244,7 @@
},
{
"cell_type": "markdown",
+ "id": "3e169bc8",
"metadata": {},
"source": [
"The elements of this NumPy array are all objects, as indicated by the `dtype`. However when we index the elements, they will be converted back to numeric types if possible"
@@ -235,6 +253,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "a7caa2fd",
"metadata": {},
"outputs": [],
"source": [
@@ -244,6 +263,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "a749c06d",
"metadata": {},
"outputs": [],
"source": [
@@ -252,6 +272,7 @@
},
{
"cell_type": "markdown",
+ "id": "3df2affa",
"metadata": {},
"source": [
"We can perform arithmetic on such matrices, for example"
@@ -260,6 +281,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "b0ec7923",
"metadata": {},
"outputs": [],
"source": [
@@ -268,6 +290,7 @@
},
{
"cell_type": "markdown",
+ "id": "d2567a90",
"metadata": {},
"source": [
"## Pose classes\n",
@@ -277,6 +300,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "2707cd5c",
"metadata": {},
"outputs": [],
"source": [
@@ -287,6 +311,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "a5ce3099",
"metadata": {},
"outputs": [],
"source": [
@@ -296,6 +321,7 @@
},
{
"cell_type": "markdown",
+ "id": "4a06bc35",
"metadata": {},
"source": [
"but the colored layout is problematic."
@@ -303,6 +329,7 @@
},
{
"cell_type": "markdown",
+ "id": "bb8a15e0",
"metadata": {},
"source": [
"## Robot forward kinematics"
@@ -310,6 +337,7 @@
},
{
"cell_type": "markdown",
+ "id": "e18a7cd2",
"metadata": {},
"source": [
"We will create a symbolic version of the robot model"
@@ -318,6 +346,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "9408a564",
"metadata": {},
"outputs": [],
"source": [
@@ -327,6 +356,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "c9759f40",
"metadata": {},
"outputs": [],
"source": [
@@ -335,6 +365,7 @@
},
{
"cell_type": "markdown",
+ "id": "ffa2df67",
"metadata": {},
"source": [
"We see that the $\\alpha_j$ values are now given in radians and are colored red. This means the value is a symbolic expression, for the first link it is $\\pi/2$ which is a precise value, compared to the non-symbolic case which is a floating point number 1.5707963267948966 that only approximates $\\pi/2$."
@@ -342,6 +373,7 @@
},
{
"cell_type": "markdown",
+ "id": "7cb9952d",
"metadata": {},
"source": [
"The next thing we need to do is create a vector of joint coordinates which are all symbols"
@@ -350,6 +382,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "61352f75",
"metadata": {},
"outputs": [],
"source": [
@@ -359,6 +392,7 @@
},
{
"cell_type": "markdown",
+ "id": "e2349534",
"metadata": {},
"source": [
"We use the underscore, because the value of the symbol is pretty printed by SymPy as a subscript (just as it is with LaTeX math notation)"
@@ -367,6 +401,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "86f820c2",
"metadata": {},
"outputs": [],
"source": [
@@ -375,6 +410,7 @@
},
{
"cell_type": "markdown",
+ "id": "55f7574e",
"metadata": {},
"source": [
"We are now set to compute the forward kinematics which will be a matrix whose elements will be complicated expressions of symbolic joint variables and kinematic parameters"
@@ -383,6 +419,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "d33f041b",
"metadata": {},
"outputs": [],
"source": [
@@ -392,6 +429,7 @@
},
{
"cell_type": "markdown",
+ "id": "2222d8f6",
"metadata": {},
"source": [
"The color coding helps us identify the rotational and translational parts, but the format is not very readable. We can display the underlying NumPy array"
@@ -400,6 +438,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "19ff6ef3",
"metadata": {},
"outputs": [],
"source": [
@@ -408,6 +447,7 @@
},
{
"cell_type": "markdown",
+ "id": "88827405",
"metadata": {},
"source": [
"which is not a lot better.\n",
@@ -418,6 +458,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "5a8f1894",
"metadata": {},
"outputs": [],
"source": [
@@ -427,6 +468,7 @@
},
{
"cell_type": "markdown",
+ "id": "ab43bcd3",
"metadata": {},
"source": [
"which is decently pretty printed by SymPy.\n",
@@ -437,6 +479,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "ca1bad2d",
"metadata": {},
"outputs": [],
"source": [
@@ -448,6 +491,7 @@
},
{
"cell_type": "markdown",
+ "id": "ca0ab88f",
"metadata": {},
"source": [
"which is more compact (it takes a few seconds to compute). We can see that a trigometric _sum of angles_ substition has been performed, there are instances of sine and cosine of $q_1 + q_2$, the shoulder and elbow joints. This is to be expected since these joints are adjacent and have with parallel axes.\n",
@@ -458,6 +502,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "ab687736",
"metadata": {},
"outputs": [],
"source": [
@@ -466,6 +511,7 @@
},
{
"cell_type": "markdown",
+ "id": "a659ed23",
"metadata": {},
"source": [
"The floating point constants here have been inherited from the kinematic model we imported at [this step](#Robot-forward-kinematics). It would be possible to replace the non-zero kinematic constants $a_i$ and $d_i$ with symbols created using `sym.symbol` as shown in [this section](#Creating-a-symbolic-variable)."
@@ -473,6 +519,7 @@
},
{
"cell_type": "markdown",
+ "id": "66ac5b98",
"metadata": {},
"source": [
"## Code generation\n",
@@ -482,6 +529,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "3a0312f9",
"metadata": {},
"outputs": [],
"source": [
@@ -491,6 +539,7 @@
},
{
"cell_type": "markdown",
+ "id": "8228338a",
"metadata": {},
"source": [
"which is pure C code that does not require any linear algebra package to compute. We simply need to define the values of `q_0` to `q_5` in order to determine the end-effector pose. The result is computed using symbolically simplified expressions. The code is not optimized, but we could expect the compiler to perform some additional simplification.\n",
@@ -501,6 +550,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "22aaf482",
"metadata": {},
"outputs": [],
"source": [
@@ -509,6 +559,7 @@
},
{
"cell_type": "markdown",
+ "id": "19de3fd2",
"metadata": {},
"source": [
"We can also output Python code"
@@ -517,6 +568,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "9e03efae",
"metadata": {},
"outputs": [],
"source": [
@@ -525,6 +577,7 @@
},
{
"cell_type": "markdown",
+ "id": "d8a37469",
"metadata": {},
"source": [
"Which constructs an instance of a SymPy `ImmutableDenseMatrix` which we can turn into a function"
@@ -533,6 +586,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "22e110f7",
"metadata": {},
"outputs": [],
"source": [
@@ -542,6 +596,7 @@
},
{
"cell_type": "markdown",
+ "id": "545c42a8",
"metadata": {},
"source": [
"If we pass in the zero joint angles we get the familiar forward kinematic solution result"
@@ -550,6 +605,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "da379113",
"metadata": {},
"outputs": [],
"source": [
@@ -558,6 +614,7 @@
},
{
"cell_type": "markdown",
+ "id": "2fed14fe",
"metadata": {},
"source": [
"SymPy also supports printing code in C++ (`cxxcode`), Fortran (`fcode`), JavaScript (`jscode`), Julia (`julia_code`), Rust (`rust_code`), Mathematica (`mathematica_code`) and Octave/MATLAB (`octave_code`).\n",
@@ -567,6 +624,7 @@
},
{
"cell_type": "markdown",
+ "id": "84ec9a92",
"metadata": {},
"source": [
"## Robot dynamics\n",
@@ -579,6 +637,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "c2c2765d",
"metadata": {},
"outputs": [],
"source": [
@@ -588,6 +647,7 @@
},
{
"cell_type": "markdown",
+ "id": "e1fa5bf1",
"metadata": {},
"source": [
"Next we create symbolic vectors for joint velocity and acceleration, just as we did earlier for joint coordinates"
@@ -596,6 +656,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "7a855919",
"metadata": {},
"outputs": [],
"source": [
@@ -606,6 +667,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "52774649",
"metadata": {},
"outputs": [],
"source": [
@@ -615,6 +677,7 @@
},
{
"cell_type": "markdown",
+ "id": "5c967a12",
"metadata": {},
"source": [
"Now we compute the inverse dynamics as a function of symbolic joint coordinates, velocities, accelerations and gravity as well as a lot of numerical kinematic and dynamic parameters.\n",
@@ -625,6 +688,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "b6db73a3",
"metadata": {
"scrolled": true
},
@@ -636,6 +700,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "051cec6a",
"metadata": {},
"outputs": [],
"source": [
@@ -645,6 +710,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "effe62d5",
"metadata": {},
"outputs": [],
"source": [
@@ -653,6 +719,7 @@
},
{
"cell_type": "markdown",
+ "id": "3d6e6daa",
"metadata": {},
"source": [
"SymPy's `simplify` can reduce these expressions a lot, but it takes longer the more terms there are. It took about a second for the last joint, 15 seconds for $\\tau_3$, 50 seconds for $\\tau_4$ and several minutes for $\\tau_2$, which we will not wait for."
@@ -660,6 +727,7 @@
},
{
"cell_type": "markdown",
+ "id": "eade206a",
"metadata": {},
"source": [
"The result `tau` is a NumPy array, as it is for the numeric case, but with `dtype=object` since each element is a SymPy expression"
@@ -668,6 +736,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "00437d19",
"metadata": {},
"outputs": [],
"source": [
@@ -676,6 +745,7 @@
},
{
"cell_type": "markdown",
+ "id": "43780f20",
"metadata": {},
"source": [
"and it has the expected shape"
@@ -684,6 +754,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "00d293ec",
"metadata": {},
"outputs": [],
"source": [
@@ -692,6 +763,7 @@
},
{
"cell_type": "markdown",
+ "id": "d4594129",
"metadata": {},
"source": [
"The torque on the first joint $\\tau_0$ is"
@@ -700,6 +772,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "db07968d",
"metadata": {},
"outputs": [],
"source": [
@@ -708,6 +781,7 @@
},
{
"cell_type": "markdown",
+ "id": "53b9c432",
"metadata": {},
"source": [
"which is a complicated expression. To make it easier to work with, we will expand it"
@@ -716,6 +790,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "c5248007",
"metadata": {},
"outputs": [],
"source": [
@@ -725,6 +800,7 @@
},
{
"cell_type": "markdown",
+ "id": "6297390b",
"metadata": {},
"source": [
"to form a multinomial, a sum of products of trigonometric functions. The number of product terms is"
@@ -733,6 +809,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "1a7fbeeb",
"metadata": {},
"outputs": [],
"source": [
@@ -741,6 +818,7 @@
},
{
"cell_type": "markdown",
+ "id": "87bf7dec",
"metadata": {},
"source": [
"With the expression in this form we can find all the terms that contain $\\ddot{q}_0$"
@@ -749,6 +827,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "ae97b209",
"metadata": {},
"outputs": [],
"source": [
@@ -758,6 +837,7 @@
},
{
"cell_type": "markdown",
+ "id": "9e27d63d",
"metadata": {},
"source": [
"so we can write our torque expression as $\\tau_0 = m \\ddot{q}_0 + \\cdots$ which means that $m$ must be element $\\textbf{M}_{00}$ of the manipulator inertia matrix. We see it has contributions due to the centre of mass of the arm which depends on the configuration of joints 1 to 5, and motor inertia. \n",
@@ -768,6 +848,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "eaf3b9a3",
"metadata": {},
"outputs": [],
"source": [
@@ -777,6 +858,7 @@
},
{
"cell_type": "markdown",
+ "id": "d7594f8b",
"metadata": {},
"source": [
"must be the element $\\textbf{M}_{01}$, an off-diagonal element of the inertia matrix. In this way we can generate expressions for each element of the manipulator inertia matrix. Remember that this matrix is symmetric so we only need to compute half of the off-diagnonal terms.\n",
@@ -787,6 +869,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "e66008fa",
"metadata": {},
"outputs": [],
"source": [
@@ -796,6 +879,7 @@
},
{
"cell_type": "markdown",
+ "id": "a23fb8be",
"metadata": {},
"source": [
"so we can write our torque expression as $\\tau_0 = c \\dot{q}_0 + \\cdots$\n",
@@ -805,6 +889,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "6d0e77ea",
"metadata": {},
"outputs": [],
"source": [
@@ -814,6 +899,7 @@
},
{
"cell_type": "markdown",
+ "id": "4e89d40c",
"metadata": {},
"source": [
"ie. they originally contained $\\dot{q}_0^2$. The squared velocity terms belong on the diagonal of the Coriolis matrix and represent centripetal torques. This particular expression is the element $\\textbf{C}_{00}$.\n",
@@ -823,16 +909,18 @@
},
{
"cell_type": "code",
+ "execution_count": null,
+ "id": "fbba5177",
"metadata": {},
+ "outputs": [],
"source": [
"c = tau_0.coeff(qd[0]).coeff(qd[1]).args\n",
"c"
- ],
- "execution_count": null,
- "outputs": []
+ ]
},
{
"cell_type": "markdown",
+ "id": "c024f72d",
"metadata": {},
"source": [
"We add $C/2$ to both $C_{01}$ and $C_{10}$ and repeat for all the off-diagonal terms. $C_{ij} = \\Sum_{i=0}^n \\Sum_{j=0}^n c\n",
@@ -842,6 +930,7 @@
},
{
"cell_type": "markdown",
+ "id": "dd52ac06",
"metadata": {},
"source": [
"SymPy provides tools that allow us to pull apart the expression's parse tree. The top node is given by `.func` which in this case is"
@@ -850,6 +939,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "66950865",
"metadata": {},
"outputs": [],
"source": [
@@ -858,6 +948,7 @@
},
{
"cell_type": "markdown",
+ "id": "0dd4dc63",
"metadata": {},
"source": [
"indicating an addition node, and its arguments are"
@@ -866,6 +957,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "34645872",
"metadata": {
"scrolled": true
},
@@ -876,6 +968,7 @@
},
{
"cell_type": "markdown",
+ "id": "8772b5fd",
"metadata": {},
"source": [
"expressed as a tuple. A term, say the first one, can be further decomposed"
@@ -884,6 +977,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "ea341725",
"metadata": {},
"outputs": [],
"source": [
@@ -893,6 +987,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "dd92407d",
"metadata": {},
"outputs": [],
"source": [
@@ -901,6 +996,7 @@
},
{
"cell_type": "markdown",
+ "id": "40e07b83",
"metadata": {},
"source": [
"which is we see here is a product of three terms. There is a standard ordering of the arguments and it looks like constants are always first."
@@ -908,6 +1004,7 @@
},
{
"cell_type": "markdown",
+ "id": "31e6ee9d",
"metadata": {},
"source": [
"We can see that many of the terms have very small coefficients, they literally don't add much to the result, so we can cull them out, or rather, we can select those with significant coefficients"
@@ -916,6 +1013,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "b3276582",
"metadata": {},
"outputs": [],
"source": [
@@ -925,6 +1023,7 @@
},
{
"cell_type": "markdown",
+ "id": "37cb8924",
"metadata": {},
"source": [
"and there are now only"
@@ -933,6 +1032,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "7ddacfaa",
"metadata": {},
"outputs": [],
"source": [
@@ -941,6 +1041,7 @@
},
{
"cell_type": "markdown",
+ "id": "a8286e0d",
"metadata": {},
"source": [
"product terms remaining, just 25% of the number we started with.\n",
@@ -951,6 +1052,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "7180d48f",
"metadata": {},
"outputs": [],
"source": [
@@ -960,6 +1062,7 @@
},
{
"cell_type": "markdown",
+ "id": "e28872d0",
"metadata": {},
"source": [
"and the top-level node function, `tau_0.func` (which is `Add`) acts as a constructor to which we pass the significant terms.\n",
@@ -970,6 +1073,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "5e7f04f5",
"metadata": {},
"outputs": [],
"source": [
@@ -978,6 +1082,7 @@
},
{
"cell_type": "markdown",
+ "id": "6a829be4",
"metadata": {},
"source": [
"We can do this for any of the expressions we generated above, ie. for elements of the inertia matrix or the velocity matrix.\n",
@@ -987,6 +1092,7 @@
},
{
"cell_type": "markdown",
+ "id": "6f6fb8b9",
"metadata": {},
"source": [
"To set these symbolic values to zero we define a substituion dictionary where a key is the symbols to be substituted and the value is its new (substituted) value."
@@ -995,6 +1101,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "18037d42",
"metadata": {},
"outputs": [],
"source": [
@@ -1007,6 +1114,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "84832fe9",
"metadata": {},
"outputs": [],
"source": [
@@ -1017,6 +1125,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "02ee0c13",
"metadata": {},
"outputs": [],
"source": [
@@ -1034,6 +1143,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "0889139e",
"metadata": {},
"outputs": [],
"source": [
@@ -1043,6 +1153,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "9c4271d7",
"metadata": {},
"outputs": [],
"source": [
@@ -1052,6 +1163,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "2742b4c0",
"metadata": {
"scrolled": true
},
@@ -1068,65 +1180,7 @@
"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.8.3"
- },
- "toc": {
- "base_numbering": 1,
- "nav_menu": {},
- "number_sections": true,
- "sideBar": true,
- "skip_h1_title": false,
- "title_cell": "Table of Contents",
- "title_sidebar": "Contents",
- "toc_cell": false,
- "toc_position": {},
- "toc_section_display": true,
- "toc_window_display": false
- },
- "varInspector": {
- "cols": {
- "lenName": 16,
- "lenType": 16,
- "lenVar": 40
- },
- "kernels_config": {
- "python": {
- "delete_cmd_postfix": "",
- "delete_cmd_prefix": "del ",
- "library": "var_list.py",
- "varRefreshCmd": "print(var_dic_list())"
- },
- "r": {
- "delete_cmd_postfix": ") ",
- "delete_cmd_prefix": "rm(",
- "library": "var_list.r",
- "varRefreshCmd": "cat(var_dic_list()) "
- }
- },
- "types_to_exclude": [
- "module",
- "function",
- "builtin_function_or_method",
- "instance",
- "_Feature"
- ],
- "window_display": false
- },
- "rtb_header": {
- "title": "Symbolic computation",
- "paragraphs": [
- "Forward kinematics and inverse dynamics of a Puma 560 robot with symbolic joint variables, and generating code from the results. The dynamics section takes a few minutes to run in full."
- ],
- "modernised": "2026-10-19"
+ "name": "python"
},
"rtb_bootstrap": {
"extras": [
@@ -1135,5 +1189,5 @@
}
},
"nbformat": 4,
- "nbformat_minor": 4
+ "nbformat_minor": 5
}
diff --git a/docs/notebooks/sync_bootstrap.py b/docs/notebooks/sync_bootstrap.py
index b9e29db62..2e31e54ed 100644
--- a/docs/notebooks/sync_bootstrap.py
+++ b/docs/notebooks/sync_bootstrap.py
@@ -18,6 +18,7 @@
import ast
import json
import sys
+import uuid
from pathlib import Path
MARKER = "# RTB_BOOTSTRAP_CELL"
@@ -71,46 +72,28 @@ def notebook_extras(nb: dict) -> list[str]:
return list(nb.get("metadata", {}).get("rtb_bootstrap", {}).get("extras", []))
-HEADER_MARKER = "\n"
- "\n"
- " \n"
- ' Robotics Toolbox for Python | \n'
- '  | \n'
- "
\n"
- "
\n\n"
- "\n"
- f'{header["title"]}
\n'
- "\n\n"
- f'(c) {header.get("copyright", "Peter Corke 2026")}'
- ]
- parts.extend(header.get("paragraphs", []))
- if "modernised" in header:
- parts.append(
- f"> **Modernised {header['modernised']}** to address API drift: this "
- "notebook was written for an earlier release of the Toolbox, and has been "
- "updated so that it runs with the current one."
- )
- return "\n\n".join(parts).splitlines(keepends=True)
-
-
-def is_header_cell(cell: dict) -> bool:
- return (
- cell.get("cell_type") == "markdown"
- and cell_source_text(cell).lstrip().startswith(HEADER_MARKER)
- )
+ cells = nb.get("cells", [])
+ used = {c["id"] for c in cells if "id" in c}
+ changed = False
+ for cell in cells:
+ if "id" not in cell:
+ new = uuid.uuid4().hex[:8]
+ while new in used:
+ new = uuid.uuid4().hex[:8]
+ cell["id"] = new
+ used.add(new)
+ changed = True
+ if changed and nb.get("nbformat_minor", 0) < 5:
+ nb["nbformat_minor"] = 5
+ return changed
def cell_source_text(cell: dict) -> str:
@@ -127,30 +110,14 @@ def is_bootstrap_cell(cell: dict) -> bool:
def process_notebook(path: Path, template: str, fix: bool) -> bool:
- """Check (and optionally fix) one notebook's header and bootstrap cells.
+ """Check (and optionally fix) one notebook's bootstrap cell and cell ids.
- :returns: True if the notebook's header or bootstrap cell was (or would be) changed.
+ :returns: True if the notebook's bootstrap cell or cell ids were (or would be) changed.
"""
with path.open("r", encoding="utf-8") as f:
nb = json.load(f)
- changed = False
- header = nb.get("metadata", {}).get("rtb_header")
- if header is not None:
- desired = header_source(header)
- cells = nb.setdefault("cells", [])
- existing = next((c for c in cells if is_header_cell(c)), None)
- if existing is None:
- changed = True
- if fix:
- cells.insert(
- 0, {"cell_type": "markdown", "metadata": {}, "source": desired}
- )
- elif existing.get("source") != desired:
- changed = True
- if fix:
- existing["source"] = desired
-
+ changed = ensure_cell_ids(nb) if fix else any("id" not in c for c in nb.get("cells", []))
for cell in nb.get("cells", []):
if not is_bootstrap_cell(cell):
continue
@@ -172,7 +139,7 @@ def process_notebook(path: Path, template: str, fix: bool) -> bool:
if changed and fix:
with path.open("w", encoding="utf-8") as f:
- json.dump(nb, f, indent=1, ensure_ascii=False)
+ json.dump(nb, f, indent=1, ensure_ascii=False, sort_keys=True)
f.write("\n")
return changed
@@ -204,9 +171,9 @@ def main() -> int:
any_changed = True
rel = nb_path.relative_to(HERE.parent.parent)
if fix:
- print(f"{rel}: header or bootstrap cell out of date, regenerating")
+ print(f"{rel}: bootstrap cell or cell ids out of date, regenerating")
else:
- print(f"{rel}: header or bootstrap cell out of date")
+ print(f"{rel}: bootstrap cell or cell ids out of date")
if args.check and any_changed:
return 1
diff --git a/docs/notebooks/two-link-dynamics.ipynb b/docs/notebooks/two-link-dynamics.ipynb
index 00a15e496..0a38cd268 100644
--- a/docs/notebooks/two-link-dynamics.ipynb
+++ b/docs/notebooks/two-link-dynamics.ipynb
@@ -2,13 +2,13 @@
"cells": [
{
"cell_type": "markdown",
+ "id": "59e40ae3",
"metadata": {},
"source": [
- "\n",
"\n",
" \n",
" Robotics Toolbox for Python | \n",
- "  | \n",
+ "  | \n",
"
\n",
"
\n",
"\n",
@@ -25,6 +25,7 @@
},
{
"cell_type": "markdown",
+ "id": "1181a25a",
"metadata": {},
"source": [
"\n",
@@ -39,6 +40,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "44ef7c64",
"metadata": {
"jupyter": {
"source_hidden": true
@@ -93,6 +95,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "fff04355",
"metadata": {},
"outputs": [],
"source": [
@@ -127,6 +130,7 @@
},
{
"cell_type": "markdown",
+ "id": "97dca5bc",
"metadata": {},
"source": [
"## Denavit-Hartenberg notation"
@@ -135,6 +139,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "5bc9a4b1",
"metadata": {},
"outputs": [],
"source": [
@@ -145,6 +150,7 @@
},
{
"cell_type": "markdown",
+ "id": "90faa0b7",
"metadata": {},
"source": [
"The DH notation only allows rotation about the z-axis, which is out of the page in the Figure above. The `gravity` argument is the acceleration due to gravity, a vector in the world frame. Gravity acts downward, in the $-y$ direction of the figure, so we set it to `[0, -g, 0]` (the default is `[0, 0, -9.81]`). The gravity load is the torque the joints must exert to hold the arm still against gravity, so it is positive for an arm stretched out horizontally."
@@ -153,6 +159,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "54518096",
"metadata": {},
"outputs": [],
"source": [
@@ -163,6 +170,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "9ac9cf16",
"metadata": {},
"outputs": [],
"source": [
@@ -173,6 +181,7 @@
},
{
"cell_type": "markdown",
+ "id": "23cb99c3",
"metadata": {},
"source": [
"Using equations (7.85) and (7.86) we can write an expression for gravity torque derived analytically using Euler-Lagrange (rather than Newton-Euler) approach"
@@ -181,6 +190,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "7a06438a",
"metadata": {},
"outputs": [],
"source": [
@@ -194,6 +204,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "353ac8f8",
"metadata": {},
"outputs": [],
"source": [
@@ -205,6 +216,7 @@
},
{
"cell_type": "markdown",
+ "id": "df4b8940",
"metadata": {},
"source": [
"In fact we could use symbolic values of the constants above, repeat the above process, and generate the symbolic equations for gravity load."
@@ -212,6 +224,7 @@
},
{
"cell_type": "markdown",
+ "id": "fc0719d4",
"metadata": {},
"source": [
"Now we can explore the velocity coupling terms. To isolate them we will set gravity and joint acceleration to zero"
@@ -220,6 +233,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "934c0ed1",
"metadata": {},
"outputs": [],
"source": [
@@ -229,6 +243,7 @@
},
{
"cell_type": "markdown",
+ "id": "a4f17632",
"metadata": {},
"source": [
"Using equations on page 261 and (7.87) we can write the velocity dependent terms as"
@@ -237,6 +252,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "a8398927",
"metadata": {},
"outputs": [],
"source": [
@@ -257,6 +273,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "a59f1388",
"metadata": {},
"outputs": [],
"source": [
@@ -270,6 +287,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "3a4abe52",
"metadata": {},
"outputs": [],
"source": [
@@ -299,6 +317,7 @@
},
{
"cell_type": "markdown",
+ "id": "c90a3625",
"metadata": {},
"source": [
"## ETS notation"
@@ -306,6 +325,7 @@
},
{
"cell_type": "markdown",
+ "id": "6a72b4cf",
"metadata": {},
"source": [
"We will redefine the parameters of the model to numerical values"
@@ -314,6 +334,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "7ceead6a",
"metadata": {},
"outputs": [],
"source": [
@@ -342,6 +363,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "04fbffa0",
"metadata": {},
"outputs": [],
"source": [
@@ -353,6 +375,7 @@
{
"cell_type": "code",
"execution_count": null,
+ "id": "dbeb857e",
"metadata": {},
"outputs": [],
"source": [
@@ -370,54 +393,9 @@
"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.8.5"
- },
- "varInspector": {
- "cols": {
- "lenName": 16,
- "lenType": 16,
- "lenVar": 40
- },
- "kernels_config": {
- "python": {
- "delete_cmd_postfix": "",
- "delete_cmd_prefix": "del ",
- "library": "var_list.py",
- "varRefreshCmd": "print(var_dic_list())"
- },
- "r": {
- "delete_cmd_postfix": ") ",
- "delete_cmd_prefix": "rm(",
- "library": "var_list.r",
- "varRefreshCmd": "cat(var_dic_list()) "
- }
- },
- "types_to_exclude": [
- "module",
- "function",
- "builtin_function_or_method",
- "instance",
- "_Feature"
- ],
- "window_display": false
- },
- "rtb_header": {
- "title": "Dynamics of a 2-link manipulator",
- "paragraphs": [
- "The gravity and velocity torques of the two-link planar arm in Spong, Hutchinson and Vidyasagar, computed by the Toolbox from a DH model and from an ETS model, and compared with the analytic Euler-Lagrange equations."
- ],
- "modernised": "2026-10-19"
+ "name": "python"
}
},
"nbformat": 4,
- "nbformat_minor": 4
+ "nbformat_minor": 5
}
diff --git a/tests/test_notebook_bootstrap.py b/tests/test_notebook_bootstrap.py
index 8bcab3dd5..8c00d058a 100644
--- a/tests/test_notebook_bootstrap.py
+++ b/tests/test_notebook_bootstrap.py
@@ -12,6 +12,8 @@
import sys
from pathlib import Path
+import pytest
+
NOTEBOOKS_DIR = Path(__file__).resolve().parent.parent / "docs" / "notebooks"
@@ -37,8 +39,12 @@ def _write_notebook(path: Path, cells: list[dict]) -> None:
path.write_text(json.dumps(nb, indent=1) + "\n", encoding="utf-8")
+_ids = iter(f"{n:08x}" for n in range(10**6))
+
+
def _code_cell(source: str, outputs=None, execution_count=None) -> dict:
return {
+ "id": next(_ids),
"cell_type": "code",
"metadata": {},
"execution_count": execution_count,
@@ -225,44 +231,79 @@ def test_no_extras_gives_the_plain_call():
assert "".join(lines).rstrip().endswith("COLAB = await ensure_installed()")
-def test_header_cell_is_generated_from_metadata(tmp_path):
+def test_cells_without_ids_get_unique_ids_and_nbformat_5(tmp_path):
+ nb_path = tmp_path / "demo.ipynb"
+ cells = [_code_cell("x = 1\n"), _code_cell("y = 2\n"), _code_cell("z = 3\n")]
+ for cell in cells[:2]:
+ del cell["id"]
+ nb = {"cells": cells, "metadata": {}, "nbformat": 4, "nbformat_minor": 4}
+ nb_path.write_text(json.dumps(nb), encoding="utf-8")
+ kept = cells[2]["id"]
+
+ # check mode reports the missing ids and does not write
+ assert sync_bootstrap.process_notebook(nb_path, template="", fix=False)
+ assert "id" not in json.loads(nb_path.read_text(encoding="utf-8"))["cells"][0]
+
+ assert sync_bootstrap.process_notebook(nb_path, template="", fix=True)
+ out = json.loads(nb_path.read_text(encoding="utf-8"))
+ ids = [c["id"] for c in out["cells"]]
+ assert len(set(ids)) == 3 and ids[2] == kept # an existing id is kept
+ assert out["nbformat_minor"] == 5
+
+ # nothing more to do, and the check passes
+ assert not sync_bootstrap.process_notebook(nb_path, template="", fix=False)
+
+
+def test_markdown_cells_get_ids_too(tmp_path):
+ nb_path = tmp_path / "demo.ipynb"
+ nb = {
+ "cells": [{"cell_type": "markdown", "metadata": {}, "source": ["# Title\n"]}],
+ "metadata": {},
+ "nbformat": 4,
+ "nbformat_minor": 5,
+ }
+ nb_path.write_text(json.dumps(nb), encoding="utf-8")
+
+ assert sync_bootstrap.process_notebook(nb_path, template="", fix=True)
+
+ assert "id" in json.loads(nb_path.read_text(encoding="utf-8"))["cells"][0]
+
+
+def test_editor_metadata_is_normalised(tmp_path):
nb_path = tmp_path / "demo.ipynb"
nb = {
"cells": [_code_cell("x = 1\n")],
"metadata": {
- "rtb_header": {
- "title": "A title",
- "paragraphs": ["About it."],
- "modernised": "2026-10-19",
- }
+ "kernelspec": {"display_name": "dev", "language": "python", "name": "python3"},
+ "language_info": {"name": "python", "version": "3.12.14"},
+ "vscode": {"interpreter": {"hash": "abc"}},
+ "varInspector": {"window_display": False},
+ "rtb_bootstrap": {"extras": ["ik"]},
},
"nbformat": 4,
"nbformat_minor": 5,
}
nb_path.write_text(json.dumps(nb), encoding="utf-8")
- # check mode reports the missing header and does not write
- assert sync_bootstrap.process_notebook(nb_path, template="", fix=False)
- assert json.loads(nb_path.read_text(encoding="utf-8"))["cells"][0]["cell_type"] == "code"
+ assert clear_outputs.process_notebook(nb_path, fix=False) # reported, not written
+ assert clear_outputs.process_notebook(nb_path, fix=True)
+
+ out = json.loads(nb_path.read_text(encoding="utf-8"))
+ assert out["metadata"] == {
+ "kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"},
+ "language_info": {"name": "python"},
+ "rtb_bootstrap": {"extras": ["ik"]}, # ours, kept
+ }
+ assert not clear_outputs.process_notebook(nb_path, fix=True)
- assert sync_bootstrap.process_notebook(nb_path, template="", fix=True)
- cells = json.loads(nb_path.read_text(encoding="utf-8"))["cells"]
- assert sync_bootstrap.is_header_cell(cells[0])
- text = "".join(cells[0]["source"])
- assert "A title" in text and "About it." in text
- assert "Modernised 2026-10-19" in text
- assert len(cells) == 2 # inserted once, the original cell kept
-
- # idempotent, and a change to the metadata is noticed
- assert not sync_bootstrap.process_notebook(nb_path, template="", fix=True)
- nb = json.loads(nb_path.read_text(encoding="utf-8"))
- nb["metadata"]["rtb_header"]["title"] = "Another"
- nb_path.write_text(json.dumps(nb), encoding="utf-8")
- assert sync_bootstrap.process_notebook(nb_path, template="", fix=True)
- assert "Another" in "".join(json.loads(nb_path.read_text(encoding="utf-8"))["cells"][0]["source"])
+def test_notebooks_are_written_as_nbformat_writes_them(tmp_path):
+ # so that opening and saving a notebook in VS Code or Jupyter changes nothing
+ nbformat = pytest.importorskip("nbformat")
-def test_notebook_without_header_metadata_gets_no_header(tmp_path):
nb_path = tmp_path / "demo.ipynb"
_write_notebook(nb_path, [_code_cell("x = 1\n")])
- assert not sync_bootstrap.process_notebook(nb_path, template="", fix=True)
+ clear_outputs.process_notebook(nb_path, fix=True)
+
+ text = nb_path.read_text(encoding="utf-8")
+ assert text == nbformat.writes(nbformat.reads(text, as_version=4)) + "\n"