diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 088fe4c30..0eb2f7b19 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -95,15 +95,14 @@ jobs: # it benchmarks numeric IK solver performance and uses the # websocket-based Swift visualizer, neither of which is meaningful # or functional inside the Pyodide sandbox -- it stays Colab-only. - # ik-symbolic.ipynb is excluded until it has been tried in Pyodide: it - # needs SymPy, and deriving the equations without python-flint may be - # very slow in the browser. + # ik-symbolic.ipynb is included: SymPy is one of Pyodide's own packages + # and the derivation takes 1 to 10 s without python-flint (timed in + # docs/lite/wasm_check, which checks it on every build). run: | mkdir -p docs/lite/files rsync -a --exclude '.ipynb_checkpoints' --exclude '__pycache__' \ --exclude 'Untitled*.ipynb' --exclude '*.py' --exclude 'Makefile' --exclude 'README.md' \ --exclude 'ik_benchmark.ipynb' \ - --exclude 'ik-symbolic.ipynb' \ docs/notebooks/ docs/lite/files/ - name: Adapt staged notebooks for JupyterLite diff --git a/docs/lite/wasm_check/check_pyodide_install.mjs b/docs/lite/wasm_check/check_pyodide_install.mjs index 4f734879d..df7b335a2 100644 --- a/docs/lite/wasm_check/check_pyodide_install.mjs +++ b/docs/lite/wasm_check/check_pyodide_install.mjs @@ -72,10 +72,22 @@ async def _redirect_wheel_install(requirements, **kwargs): return await _real_install(requirements, **kwargs) micropip.install = _redirect_wheel_install -await bootstrap.ensure_installed() +await bootstrap.ensure_installed(extras=['ik']) import roboticstoolbox as rtb # noqa: F401 rtb.models.DH.Panda().fkine([0, 0, 0, 0, 0, 0, 0]) + +# the notebook ik-symbolic.ipynb: SymPy only, since python-flint is not usable in +# Pyodide 0.27. A closed-form arm and a general 6R arm (the slow one, about 10 s). +import time, warnings +warnings.simplefilter("ignore") +for name in ("Puma560", "Jaco"): + robot = getattr(rtb.models.DH, name)() + t = time.time() + ik = robot.analytic_ik() + sol = ik.solve(robot.fkine([0.1] * robot.n)) + assert len(sol.q) > 0, name + print(f"analytic_ik {name}: {type(ik).__name__}, {time.time() - t:.1f}s") print("import roboticstoolbox: OK") `; diff --git a/docs/notebooks/README.md b/docs/notebooks/README.md index 104b7f336..b18d8fc2a 100644 --- a/docs/notebooks/README.md +++ b/docs/notebooks/README.md @@ -32,5 +32,17 @@ run in the `docs.yml` workflow, and `tests/test_notebook_bootstrap.py` tests the `docs/lite/wasm_check/` runs the bootstrap in Pyodide under Node.js against the wheel just built, in CI. See its README. -`ik-symbolic.ipynb` has no bootstrap cell yet: it needs the `ik` extra (SymPy and -python-flint), and is not part of the JupyterLite site. +## Extras + +A notebook that needs a toolbox extra says so in its own metadata, not in the cell: + +```json +"metadata": {"rtb_bootstrap": {"extras": ["ik"]}} +``` + +`sync_bootstrap.py` then generates `ensure_installed(extras=['ik'])`. For `ik`, Colab +installs `roboticstoolbox-python[ik]`, a local install is told what to install if SymPy +is missing, and in the browser SymPy comes from Pyodide's own packages. python-flint is +not installed in the browser: the one bundled with Pyodide 0.27 is too old, and the +toolbox then uses SymPy alone (about 2x slower; 10 s for a general 6R arm). +`ik-symbolic.ipynb` is the one notebook that does this. diff --git a/docs/notebooks/_rtb_nb_bootstrap.py b/docs/notebooks/_rtb_nb_bootstrap.py index edb02e58c..914bdc412 100644 --- a/docs/notebooks/_rtb_nb_bootstrap.py +++ b/docs/notebooks/_rtb_nb_bootstrap.py @@ -14,9 +14,12 @@ from pathlib import Path -async def ensure_installed() -> bool: +async def ensure_installed(extras: tuple[str, ...] | list[str] = ()) -> bool: """Install roboticstoolbox-python if needed, and report the environment. + :param extras: toolbox extras the notebook needs, for example ``["ik"]``. They + are set per notebook in its metadata (``rtb_bootstrap.extras``) and + passed here by the generated cell. :returns: True if running on Google Colab, False otherwise. """ if sys.platform == "emscripten": @@ -31,6 +34,12 @@ async def ensure_installed() -> bool: # (rai-opensource/spatialmath-python#250). await micropip.install("spatialmath-python", deps=False) await micropip.install("ipympl") + if "ik" in extras: + # SymPy is one of Pyodide's own packages. python-flint is not installed: + # the one bundled with Pyodide 0.27 is too old for the toolbox, which + # then uses SymPy for the polynomial steps, about 2x slower for a + # general 6R arm (10 s to derive a Kinova Jaco). + await micropip.install("sympy") wheels = sorted(Path("/pypi").glob("roboticstoolbox_python-*.whl")) if wheels: @@ -47,17 +56,21 @@ async def ensure_installed() -> bool: import google.colab # noqa: F401 except ImportError: where, colab = "locally", False + if "ik" in extras: + try: + import sympy # noqa: F401 + except ImportError: + print( + "This notebook needs SymPy: " + "pip install 'roboticstoolbox-python[ik]'" + ) else: print("Installing roboticstoolbox-python...") + requirement = "roboticstoolbox-python" + if extras: + requirement += "[" + ",".join(extras) + "]" subprocess.run( - [ - sys.executable, - "-m", - "pip", - "install", - "-q", - "roboticstoolbox-python", - ], + [sys.executable, "-m", "pip", "install", "-q", requirement], check=True, ) where, colab = "on Colab", True diff --git a/docs/notebooks/branched-robot.ipynb b/docs/notebooks/branched-robot.ipynb index 38709bfbe..b9ff236fa 100644 --- a/docs/notebooks/branched-robot.ipynb +++ b/docs/notebooks/branched-robot.ipynb @@ -28,9 +28,12 @@ "from pathlib import Path\n", "\n", "\n", - "async def ensure_installed() -> bool:\n", + "async def ensure_installed(extras: tuple[str, ...] | list[str] = ()) -> bool:\n", " \"\"\"Install roboticstoolbox-python if needed, and report the environment.\n", "\n", + " :param extras: toolbox extras the notebook needs, for example ``[\"ik\"]``. They\n", + " are set per notebook in its metadata (``rtb_bootstrap.extras``) and\n", + " passed here by the generated cell.\n", " :returns: True if running on Google Colab, False otherwise.\n", " \"\"\"\n", " if sys.platform == \"emscripten\":\n", @@ -45,6 +48,12 @@ " # (rai-opensource/spatialmath-python#250).\n", " await micropip.install(\"spatialmath-python\", deps=False)\n", " await micropip.install(\"ipympl\")\n", + " if \"ik\" in extras:\n", + " # SymPy is one of Pyodide's own packages. python-flint is not installed:\n", + " # the one bundled with Pyodide 0.27 is too old for the toolbox, which\n", + " # then uses SymPy for the polynomial steps, about 2x slower for a\n", + " # general 6R arm (10 s to derive a Kinova Jaco).\n", + " await micropip.install(\"sympy\")\n", "\n", " wheels = sorted(Path(\"/pypi\").glob(\"roboticstoolbox_python-*.whl\"))\n", " if wheels:\n", @@ -61,17 +70,21 @@ " import google.colab # noqa: F401\n", " except ImportError:\n", " where, colab = \"locally\", False\n", + " if \"ik\" in extras:\n", + " try:\n", + " import sympy # noqa: F401\n", + " except ImportError:\n", + " print(\n", + " \"This notebook needs SymPy: \"\n", + " \"pip install 'roboticstoolbox-python[ik]'\"\n", + " )\n", " else:\n", " print(\"Installing roboticstoolbox-python...\")\n", + " requirement = \"roboticstoolbox-python\"\n", + " if extras:\n", + " requirement += \"[\" + \",\".join(extras) + \"]\"\n", " subprocess.run(\n", - " [\n", - " sys.executable,\n", - " \"-m\",\n", - " \"pip\",\n", - " \"install\",\n", - " \"-q\",\n", - " \"roboticstoolbox-python\",\n", - " ],\n", + " [sys.executable, \"-m\", \"pip\", \"install\", \"-q\", requirement],\n", " check=True,\n", " )\n", " where, colab = \"on Colab\", True\n", diff --git a/docs/notebooks/dynamics.ipynb b/docs/notebooks/dynamics.ipynb index cd20b4a61..d35ab819a 100644 --- a/docs/notebooks/dynamics.ipynb +++ b/docs/notebooks/dynamics.ipynb @@ -28,9 +28,12 @@ "from pathlib import Path\n", "\n", "\n", - "async def ensure_installed() -> bool:\n", + "async def ensure_installed(extras: tuple[str, ...] | list[str] = ()) -> bool:\n", " \"\"\"Install roboticstoolbox-python if needed, and report the environment.\n", "\n", + " :param extras: toolbox extras the notebook needs, for example ``[\"ik\"]``. They\n", + " are set per notebook in its metadata (``rtb_bootstrap.extras``) and\n", + " passed here by the generated cell.\n", " :returns: True if running on Google Colab, False otherwise.\n", " \"\"\"\n", " if sys.platform == \"emscripten\":\n", @@ -45,6 +48,12 @@ " # (rai-opensource/spatialmath-python#250).\n", " await micropip.install(\"spatialmath-python\", deps=False)\n", " await micropip.install(\"ipympl\")\n", + " if \"ik\" in extras:\n", + " # SymPy is one of Pyodide's own packages. python-flint is not installed:\n", + " # the one bundled with Pyodide 0.27 is too old for the toolbox, which\n", + " # then uses SymPy for the polynomial steps, about 2x slower for a\n", + " # general 6R arm (10 s to derive a Kinova Jaco).\n", + " await micropip.install(\"sympy\")\n", "\n", " wheels = sorted(Path(\"/pypi\").glob(\"roboticstoolbox_python-*.whl\"))\n", " if wheels:\n", @@ -61,17 +70,21 @@ " import google.colab # noqa: F401\n", " except ImportError:\n", " where, colab = \"locally\", False\n", + " if \"ik\" in extras:\n", + " try:\n", + " import sympy # noqa: F401\n", + " except ImportError:\n", + " print(\n", + " \"This notebook needs SymPy: \"\n", + " \"pip install 'roboticstoolbox-python[ik]'\"\n", + " )\n", " else:\n", " print(\"Installing roboticstoolbox-python...\")\n", + " requirement = \"roboticstoolbox-python\"\n", + " if extras:\n", + " requirement += \"[\" + \",\".join(extras) + \"]\"\n", " subprocess.run(\n", - " [\n", - " sys.executable,\n", - " \"-m\",\n", - " \"pip\",\n", - " \"install\",\n", - " \"-q\",\n", - " \"roboticstoolbox-python\",\n", - " ],\n", + " [sys.executable, \"-m\", \"pip\", \"install\", \"-q\", requirement],\n", " check=True,\n", " )\n", " where, colab = \"on Colab\", True\n", diff --git a/docs/notebooks/ets.ipynb b/docs/notebooks/ets.ipynb index fa24f2836..38c8e76e2 100644 --- a/docs/notebooks/ets.ipynb +++ b/docs/notebooks/ets.ipynb @@ -36,9 +36,12 @@ "from pathlib import Path\n", "\n", "\n", - "async def ensure_installed() -> bool:\n", + "async def ensure_installed(extras: tuple[str, ...] | list[str] = ()) -> bool:\n", " \"\"\"Install roboticstoolbox-python if needed, and report the environment.\n", "\n", + " :param extras: toolbox extras the notebook needs, for example ``[\"ik\"]``. They\n", + " are set per notebook in its metadata (``rtb_bootstrap.extras``) and\n", + " passed here by the generated cell.\n", " :returns: True if running on Google Colab, False otherwise.\n", " \"\"\"\n", " if sys.platform == \"emscripten\":\n", @@ -53,6 +56,12 @@ " # (rai-opensource/spatialmath-python#250).\n", " await micropip.install(\"spatialmath-python\", deps=False)\n", " await micropip.install(\"ipympl\")\n", + " if \"ik\" in extras:\n", + " # SymPy is one of Pyodide's own packages. python-flint is not installed:\n", + " # the one bundled with Pyodide 0.27 is too old for the toolbox, which\n", + " # then uses SymPy for the polynomial steps, about 2x slower for a\n", + " # general 6R arm (10 s to derive a Kinova Jaco).\n", + " await micropip.install(\"sympy\")\n", "\n", " wheels = sorted(Path(\"/pypi\").glob(\"roboticstoolbox_python-*.whl\"))\n", " if wheels:\n", @@ -69,17 +78,21 @@ " import google.colab # noqa: F401\n", " except ImportError:\n", " where, colab = \"locally\", False\n", + " if \"ik\" in extras:\n", + " try:\n", + " import sympy # noqa: F401\n", + " except ImportError:\n", + " print(\n", + " \"This notebook needs SymPy: \"\n", + " \"pip install 'roboticstoolbox-python[ik]'\"\n", + " )\n", " else:\n", " print(\"Installing roboticstoolbox-python...\")\n", + " requirement = \"roboticstoolbox-python\"\n", + " if extras:\n", + " requirement += \"[\" + \",\".join(extras) + \"]\"\n", " subprocess.run(\n", - " [\n", - " sys.executable,\n", - " \"-m\",\n", - " \"pip\",\n", - " \"install\",\n", - " \"-q\",\n", - " \"roboticstoolbox-python\",\n", - " ],\n", + " [sys.executable, \"-m\", \"pip\", \"install\", \"-q\", requirement],\n", " check=True,\n", " )\n", " where, colab = \"on Colab\", True\n", diff --git a/docs/notebooks/graphics-test.ipynb b/docs/notebooks/graphics-test.ipynb index 320ed7604..dd35402a0 100644 --- a/docs/notebooks/graphics-test.ipynb +++ b/docs/notebooks/graphics-test.ipynb @@ -28,9 +28,12 @@ "from pathlib import Path\n", "\n", "\n", - "async def ensure_installed() -> bool:\n", + "async def ensure_installed(extras: tuple[str, ...] | list[str] = ()) -> bool:\n", " \"\"\"Install roboticstoolbox-python if needed, and report the environment.\n", "\n", + " :param extras: toolbox extras the notebook needs, for example ``[\"ik\"]``. They\n", + " are set per notebook in its metadata (``rtb_bootstrap.extras``) and\n", + " passed here by the generated cell.\n", " :returns: True if running on Google Colab, False otherwise.\n", " \"\"\"\n", " if sys.platform == \"emscripten\":\n", @@ -45,6 +48,12 @@ " # (rai-opensource/spatialmath-python#250).\n", " await micropip.install(\"spatialmath-python\", deps=False)\n", " await micropip.install(\"ipympl\")\n", + " if \"ik\" in extras:\n", + " # SymPy is one of Pyodide's own packages. python-flint is not installed:\n", + " # the one bundled with Pyodide 0.27 is too old for the toolbox, which\n", + " # then uses SymPy for the polynomial steps, about 2x slower for a\n", + " # general 6R arm (10 s to derive a Kinova Jaco).\n", + " await micropip.install(\"sympy\")\n", "\n", " wheels = sorted(Path(\"/pypi\").glob(\"roboticstoolbox_python-*.whl\"))\n", " if wheels:\n", @@ -61,17 +70,21 @@ " import google.colab # noqa: F401\n", " except ImportError:\n", " where, colab = \"locally\", False\n", + " if \"ik\" in extras:\n", + " try:\n", + " import sympy # noqa: F401\n", + " except ImportError:\n", + " print(\n", + " \"This notebook needs SymPy: \"\n", + " \"pip install 'roboticstoolbox-python[ik]'\"\n", + " )\n", " else:\n", " print(\"Installing roboticstoolbox-python...\")\n", + " requirement = \"roboticstoolbox-python\"\n", + " if extras:\n", + " requirement += \"[\" + \",\".join(extras) + \"]\"\n", " subprocess.run(\n", - " [\n", - " sys.executable,\n", - " \"-m\",\n", - " \"pip\",\n", - " \"install\",\n", - " \"-q\",\n", - " \"roboticstoolbox-python\",\n", - " ],\n", + " [sys.executable, \"-m\", \"pip\", \"install\", \"-q\", requirement],\n", " check=True,\n", " )\n", " where, colab = \"on Colab\", True\n", diff --git a/docs/notebooks/icra2021.ipynb b/docs/notebooks/icra2021.ipynb index 8fd306235..6507a3714 100644 --- a/docs/notebooks/icra2021.ipynb +++ b/docs/notebooks/icra2021.ipynb @@ -38,9 +38,12 @@ "from pathlib import Path\n", "\n", "\n", - "async def ensure_installed() -> bool:\n", + "async def ensure_installed(extras: tuple[str, ...] | list[str] = ()) -> bool:\n", " \"\"\"Install roboticstoolbox-python if needed, and report the environment.\n", "\n", + " :param extras: toolbox extras the notebook needs, for example ``[\"ik\"]``. They\n", + " are set per notebook in its metadata (``rtb_bootstrap.extras``) and\n", + " passed here by the generated cell.\n", " :returns: True if running on Google Colab, False otherwise.\n", " \"\"\"\n", " if sys.platform == \"emscripten\":\n", @@ -55,6 +58,12 @@ " # (rai-opensource/spatialmath-python#250).\n", " await micropip.install(\"spatialmath-python\", deps=False)\n", " await micropip.install(\"ipympl\")\n", + " if \"ik\" in extras:\n", + " # SymPy is one of Pyodide's own packages. python-flint is not installed:\n", + " # the one bundled with Pyodide 0.27 is too old for the toolbox, which\n", + " # then uses SymPy for the polynomial steps, about 2x slower for a\n", + " # general 6R arm (10 s to derive a Kinova Jaco).\n", + " await micropip.install(\"sympy\")\n", "\n", " wheels = sorted(Path(\"/pypi\").glob(\"roboticstoolbox_python-*.whl\"))\n", " if wheels:\n", @@ -71,17 +80,21 @@ " import google.colab # noqa: F401\n", " except ImportError:\n", " where, colab = \"locally\", False\n", + " if \"ik\" in extras:\n", + " try:\n", + " import sympy # noqa: F401\n", + " except ImportError:\n", + " print(\n", + " \"This notebook needs SymPy: \"\n", + " \"pip install 'roboticstoolbox-python[ik]'\"\n", + " )\n", " else:\n", " print(\"Installing roboticstoolbox-python...\")\n", + " requirement = \"roboticstoolbox-python\"\n", + " if extras:\n", + " requirement += \"[\" + \",\".join(extras) + \"]\"\n", " subprocess.run(\n", - " [\n", - " sys.executable,\n", - " \"-m\",\n", - " \"pip\",\n", - " \"install\",\n", - " \"-q\",\n", - " \"roboticstoolbox-python\",\n", - " ],\n", + " [sys.executable, \"-m\", \"pip\", \"install\", \"-q\", requirement],\n", " check=True,\n", " )\n", " where, colab = \"on Colab\", True\n", diff --git a/docs/notebooks/icra2026.ipynb b/docs/notebooks/icra2026.ipynb index 52297afee..3534ad73c 100644 --- a/docs/notebooks/icra2026.ipynb +++ b/docs/notebooks/icra2026.ipynb @@ -50,9 +50,12 @@ "from pathlib import Path\n", "\n", "\n", - "async def ensure_installed() -> bool:\n", + "async def ensure_installed(extras: tuple[str, ...] | list[str] = ()) -> bool:\n", " \"\"\"Install roboticstoolbox-python if needed, and report the environment.\n", "\n", + " :param extras: toolbox extras the notebook needs, for example ``[\"ik\"]``. They\n", + " are set per notebook in its metadata (``rtb_bootstrap.extras``) and\n", + " passed here by the generated cell.\n", " :returns: True if running on Google Colab, False otherwise.\n", " \"\"\"\n", " if sys.platform == \"emscripten\":\n", @@ -67,6 +70,12 @@ " # (rai-opensource/spatialmath-python#250).\n", " await micropip.install(\"spatialmath-python\", deps=False)\n", " await micropip.install(\"ipympl\")\n", + " if \"ik\" in extras:\n", + " # SymPy is one of Pyodide's own packages. python-flint is not installed:\n", + " # the one bundled with Pyodide 0.27 is too old for the toolbox, which\n", + " # then uses SymPy for the polynomial steps, about 2x slower for a\n", + " # general 6R arm (10 s to derive a Kinova Jaco).\n", + " await micropip.install(\"sympy\")\n", "\n", " wheels = sorted(Path(\"/pypi\").glob(\"roboticstoolbox_python-*.whl\"))\n", " if wheels:\n", @@ -83,17 +92,21 @@ " import google.colab # noqa: F401\n", " except ImportError:\n", " where, colab = \"locally\", False\n", + " if \"ik\" in extras:\n", + " try:\n", + " import sympy # noqa: F401\n", + " except ImportError:\n", + " print(\n", + " \"This notebook needs SymPy: \"\n", + " \"pip install 'roboticstoolbox-python[ik]'\"\n", + " )\n", " else:\n", " print(\"Installing roboticstoolbox-python...\")\n", + " requirement = \"roboticstoolbox-python\"\n", + " if extras:\n", + " requirement += \"[\" + \",\".join(extras) + \"]\"\n", " subprocess.run(\n", - " [\n", - " sys.executable,\n", - " \"-m\",\n", - " \"pip\",\n", - " \"install\",\n", - " \"-q\",\n", - " \"roboticstoolbox-python\",\n", - " ],\n", + " [sys.executable, \"-m\", \"pip\", \"install\", \"-q\", requirement],\n", " check=True,\n", " )\n", " where, colab = \"on Colab\", True\n", diff --git a/docs/notebooks/ik-symbolic.ipynb b/docs/notebooks/ik-symbolic.ipynb index 516fdd387..fc5900fdd 100644 --- a/docs/notebooks/ik-symbolic.ipynb +++ b/docs/notebooks/ik-symbolic.ipynb @@ -24,6 +24,104 @@ "This needs SymPy, which is not installed with the Toolbox by default: `pip install roboticstoolbox-python[ik]`. The extra also installs `python-flint`, which makes the derivation much faster." ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "jupyter": { + "source_hidden": true + } + }, + "outputs": [], + "source": [ + "# RTB_BOOTSTRAP_CELL -- sets up the environment (Colab / JupyterLite / local install); click to expand. Generated from docs/notebooks/_rtb_nb_bootstrap.py by sync_bootstrap.py -- do not hand-edit.\n", + "\n", + "\"\"\"Environment bootstrap for roboticstoolbox-python's Jupyter notebooks.\n", + "\n", + "Installs the toolbox (and reports how) across the three environments a notebook in\n", + "this folder might run in: a local Jupyter/VS Code install, Google Colab, and\n", + "JupyterLite (Pyodide/WASM, in-browser).\n", + "\n", + "This file is the single source of truth for that logic. Every notebook's own\n", + "bootstrap cell is a generated copy of this file's content, produced by\n", + "sync_bootstrap.py -- see docs/notebooks/README.md for the full explanation.\n", + "\"\"\"\n", + "\n", + "import subprocess\n", + "import sys\n", + "from pathlib import Path\n", + "\n", + "\n", + "async def ensure_installed(extras: tuple[str, ...] | list[str] = ()) -> bool:\n", + " \"\"\"Install roboticstoolbox-python if needed, and report the environment.\n", + "\n", + " :param extras: toolbox extras the notebook needs, for example ``[\"ik\"]``. They\n", + " are set per notebook in its metadata (``rtb_bootstrap.extras``) and\n", + " passed here by the generated cell.\n", + " :returns: True if running on Google Colab, False otherwise.\n", + " \"\"\"\n", + " if sys.platform == \"emscripten\":\n", + " import micropip\n", + "\n", + " # spatialmath-python <= 1.1.18 lists pre-commit (a dev tool) as a runtime\n", + " # dependency; its virtualenv requires packaging>=26.3, which conflicts with\n", + " # the packaging 24.2 built into Pyodide, so a normal resolve fails. Install\n", + " # it without dependencies first: it is then already satisfied when the\n", + " # toolbox's own dependencies are resolved below. Remove this line once a\n", + " # spatialmath release without that dependency is out\n", + " # (rai-opensource/spatialmath-python#250).\n", + " await micropip.install(\"spatialmath-python\", deps=False)\n", + " await micropip.install(\"ipympl\")\n", + " if \"ik\" in extras:\n", + " # SymPy is one of Pyodide's own packages. python-flint is not installed:\n", + " # the one bundled with Pyodide 0.27 is too old for the toolbox, which\n", + " # then uses SymPy for the polynomial steps, about 2x slower for a\n", + " # general 6R arm (10 s to derive a Kinova Jaco).\n", + " await micropip.install(\"sympy\")\n", + "\n", + " wheels = sorted(Path(\"/pypi\").glob(\"roboticstoolbox_python-*.whl\"))\n", + " if wheels:\n", + " # Prefer the wheel bundled with this JupyterLite site. Relative,\n", + " # not absolute: a leading slash resolves against the origin, not\n", + " # this site's own base URL (a GitHub Pages project subpath).\n", + " await micropip.install(f\"pypi/{wheels[-1].name}\")\n", + " else:\n", + " # Fall back to PyPI when running outside the published site layout.\n", + " await micropip.install(\"roboticstoolbox-python\")\n", + " where, colab = \"in browser\", False\n", + " else:\n", + " try:\n", + " import google.colab # noqa: F401\n", + " except ImportError:\n", + " where, colab = \"locally\", False\n", + " if \"ik\" in extras:\n", + " try:\n", + " import sympy # noqa: F401\n", + " except ImportError:\n", + " print(\n", + " \"This notebook needs SymPy: \"\n", + " \"pip install 'roboticstoolbox-python[ik]'\"\n", + " )\n", + " else:\n", + " print(\"Installing roboticstoolbox-python...\")\n", + " requirement = \"roboticstoolbox-python\"\n", + " if extras:\n", + " requirement += \"[\" + \",\".join(extras) + \"]\"\n", + " subprocess.run(\n", + " [sys.executable, \"-m\", \"pip\", \"install\", \"-q\", requirement],\n", + " check=True,\n", + " )\n", + " where, colab = \"on Colab\", True\n", + "\n", + " import roboticstoolbox\n", + "\n", + " version = getattr(roboticstoolbox, \"__version__\", \"unknown\")\n", + " print(f\"Running {where} using RTB v{version}\")\n", + " return colab\n", + "\n", + "COLAB = await ensure_installed(extras=['ik'])\n" + ] + }, { "cell_type": "code", "execution_count": null, @@ -31,15 +129,6 @@ "metadata": {}, "outputs": [], "source": [ - "try:\n", - " import piplite\n", - " await piplite.install([\"ipympl\", \"sympy\", \"roboticstoolbox-python\"])\n", - "except ImportError:\n", - " pass # not running in Pyodide (e.g. local Jupyter) -- assume it's already pip-installed\n", - "\n", - "import roboticstoolbox\n", - "print(\"roboticstoolbox version:\", roboticstoolbox.__version__)\n", - "\n", "import numpy as np\n", "import roboticstoolbox as rtb\n", "from spatialmath import SE3\n", @@ -743,6 +832,11 @@ "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.14" + }, + "rtb_bootstrap": { + "extras": [ + "ik" + ] } }, "nbformat": 4, diff --git a/docs/notebooks/ik_benchmark.ipynb b/docs/notebooks/ik_benchmark.ipynb index dd3657b05..f6ae80636 100644 --- a/docs/notebooks/ik_benchmark.ipynb +++ b/docs/notebooks/ik_benchmark.ipynb @@ -28,9 +28,12 @@ "from pathlib import Path\n", "\n", "\n", - "async def ensure_installed() -> bool:\n", + "async def ensure_installed(extras: tuple[str, ...] | list[str] = ()) -> bool:\n", " \"\"\"Install roboticstoolbox-python if needed, and report the environment.\n", "\n", + " :param extras: toolbox extras the notebook needs, for example ``[\"ik\"]``. They\n", + " are set per notebook in its metadata (``rtb_bootstrap.extras``) and\n", + " passed here by the generated cell.\n", " :returns: True if running on Google Colab, False otherwise.\n", " \"\"\"\n", " if sys.platform == \"emscripten\":\n", @@ -45,6 +48,12 @@ " # (rai-opensource/spatialmath-python#250).\n", " await micropip.install(\"spatialmath-python\", deps=False)\n", " await micropip.install(\"ipympl\")\n", + " if \"ik\" in extras:\n", + " # SymPy is one of Pyodide's own packages. python-flint is not installed:\n", + " # the one bundled with Pyodide 0.27 is too old for the toolbox, which\n", + " # then uses SymPy for the polynomial steps, about 2x slower for a\n", + " # general 6R arm (10 s to derive a Kinova Jaco).\n", + " await micropip.install(\"sympy\")\n", "\n", " wheels = sorted(Path(\"/pypi\").glob(\"roboticstoolbox_python-*.whl\"))\n", " if wheels:\n", @@ -61,17 +70,21 @@ " import google.colab # noqa: F401\n", " except ImportError:\n", " where, colab = \"locally\", False\n", + " if \"ik\" in extras:\n", + " try:\n", + " import sympy # noqa: F401\n", + " except ImportError:\n", + " print(\n", + " \"This notebook needs SymPy: \"\n", + " \"pip install 'roboticstoolbox-python[ik]'\"\n", + " )\n", " else:\n", " print(\"Installing roboticstoolbox-python...\")\n", + " requirement = \"roboticstoolbox-python\"\n", + " if extras:\n", + " requirement += \"[\" + \",\".join(extras) + \"]\"\n", " subprocess.run(\n", - " [\n", - " sys.executable,\n", - " \"-m\",\n", - " \"pip\",\n", - " \"install\",\n", - " \"-q\",\n", - " \"roboticstoolbox-python\",\n", - " ],\n", + " [sys.executable, \"-m\", \"pip\", \"install\", \"-q\", requirement],\n", " check=True,\n", " )\n", " where, colab = \"on Colab\", True\n", diff --git a/docs/notebooks/iros2020-workshop.ipynb b/docs/notebooks/iros2020-workshop.ipynb index 7013cd3ee..6bff90519 100644 --- a/docs/notebooks/iros2020-workshop.ipynb +++ b/docs/notebooks/iros2020-workshop.ipynb @@ -44,9 +44,12 @@ "from pathlib import Path\n", "\n", "\n", - "async def ensure_installed() -> bool:\n", + "async def ensure_installed(extras: tuple[str, ...] | list[str] = ()) -> bool:\n", " \"\"\"Install roboticstoolbox-python if needed, and report the environment.\n", "\n", + " :param extras: toolbox extras the notebook needs, for example ``[\"ik\"]``. They\n", + " are set per notebook in its metadata (``rtb_bootstrap.extras``) and\n", + " passed here by the generated cell.\n", " :returns: True if running on Google Colab, False otherwise.\n", " \"\"\"\n", " if sys.platform == \"emscripten\":\n", @@ -61,6 +64,12 @@ " # (rai-opensource/spatialmath-python#250).\n", " await micropip.install(\"spatialmath-python\", deps=False)\n", " await micropip.install(\"ipympl\")\n", + " if \"ik\" in extras:\n", + " # SymPy is one of Pyodide's own packages. python-flint is not installed:\n", + " # the one bundled with Pyodide 0.27 is too old for the toolbox, which\n", + " # then uses SymPy for the polynomial steps, about 2x slower for a\n", + " # general 6R arm (10 s to derive a Kinova Jaco).\n", + " await micropip.install(\"sympy\")\n", "\n", " wheels = sorted(Path(\"/pypi\").glob(\"roboticstoolbox_python-*.whl\"))\n", " if wheels:\n", @@ -77,17 +86,21 @@ " import google.colab # noqa: F401\n", " except ImportError:\n", " where, colab = \"locally\", False\n", + " if \"ik\" in extras:\n", + " try:\n", + " import sympy # noqa: F401\n", + " except ImportError:\n", + " print(\n", + " \"This notebook needs SymPy: \"\n", + " \"pip install 'roboticstoolbox-python[ik]'\"\n", + " )\n", " else:\n", " print(\"Installing roboticstoolbox-python...\")\n", + " requirement = \"roboticstoolbox-python\"\n", + " if extras:\n", + " requirement += \"[\" + \",\".join(extras) + \"]\"\n", " subprocess.run(\n", - " [\n", - " sys.executable,\n", - " \"-m\",\n", - " \"pip\",\n", - " \"install\",\n", - " \"-q\",\n", - " \"roboticstoolbox-python\",\n", - " ],\n", + " [sys.executable, \"-m\", \"pip\", \"install\", \"-q\", requirement],\n", " check=True,\n", " )\n", " where, colab = \"on Colab\", True\n", diff --git a/docs/notebooks/kinematics.ipynb b/docs/notebooks/kinematics.ipynb index 7d7e44288..470ceb3f5 100644 --- a/docs/notebooks/kinematics.ipynb +++ b/docs/notebooks/kinematics.ipynb @@ -28,9 +28,12 @@ "from pathlib import Path\n", "\n", "\n", - "async def ensure_installed() -> bool:\n", + "async def ensure_installed(extras: tuple[str, ...] | list[str] = ()) -> bool:\n", " \"\"\"Install roboticstoolbox-python if needed, and report the environment.\n", "\n", + " :param extras: toolbox extras the notebook needs, for example ``[\"ik\"]``. They\n", + " are set per notebook in its metadata (``rtb_bootstrap.extras``) and\n", + " passed here by the generated cell.\n", " :returns: True if running on Google Colab, False otherwise.\n", " \"\"\"\n", " if sys.platform == \"emscripten\":\n", @@ -45,6 +48,12 @@ " # (rai-opensource/spatialmath-python#250).\n", " await micropip.install(\"spatialmath-python\", deps=False)\n", " await micropip.install(\"ipympl\")\n", + " if \"ik\" in extras:\n", + " # SymPy is one of Pyodide's own packages. python-flint is not installed:\n", + " # the one bundled with Pyodide 0.27 is too old for the toolbox, which\n", + " # then uses SymPy for the polynomial steps, about 2x slower for a\n", + " # general 6R arm (10 s to derive a Kinova Jaco).\n", + " await micropip.install(\"sympy\")\n", "\n", " wheels = sorted(Path(\"/pypi\").glob(\"roboticstoolbox_python-*.whl\"))\n", " if wheels:\n", @@ -61,17 +70,21 @@ " import google.colab # noqa: F401\n", " except ImportError:\n", " where, colab = \"locally\", False\n", + " if \"ik\" in extras:\n", + " try:\n", + " import sympy # noqa: F401\n", + " except ImportError:\n", + " print(\n", + " \"This notebook needs SymPy: \"\n", + " \"pip install 'roboticstoolbox-python[ik]'\"\n", + " )\n", " else:\n", " print(\"Installing roboticstoolbox-python...\")\n", + " requirement = \"roboticstoolbox-python\"\n", + " if extras:\n", + " requirement += \"[\" + \",\".join(extras) + \"]\"\n", " subprocess.run(\n", - " [\n", - " sys.executable,\n", - " \"-m\",\n", - " \"pip\",\n", - " \"install\",\n", - " \"-q\",\n", - " \"roboticstoolbox-python\",\n", - " ],\n", + " [sys.executable, \"-m\", \"pip\", \"install\", \"-q\", requirement],\n", " check=True,\n", " )\n", " where, colab = \"on Colab\", True\n", diff --git a/docs/notebooks/symbolic.ipynb b/docs/notebooks/symbolic.ipynb index 0c2d08c96..5bdab2254 100644 --- a/docs/notebooks/symbolic.ipynb +++ b/docs/notebooks/symbolic.ipynb @@ -28,9 +28,12 @@ "from pathlib import Path\n", "\n", "\n", - "async def ensure_installed() -> bool:\n", + "async def ensure_installed(extras: tuple[str, ...] | list[str] = ()) -> bool:\n", " \"\"\"Install roboticstoolbox-python if needed, and report the environment.\n", "\n", + " :param extras: toolbox extras the notebook needs, for example ``[\"ik\"]``. They\n", + " are set per notebook in its metadata (``rtb_bootstrap.extras``) and\n", + " passed here by the generated cell.\n", " :returns: True if running on Google Colab, False otherwise.\n", " \"\"\"\n", " if sys.platform == \"emscripten\":\n", @@ -45,6 +48,12 @@ " # (rai-opensource/spatialmath-python#250).\n", " await micropip.install(\"spatialmath-python\", deps=False)\n", " await micropip.install(\"ipympl\")\n", + " if \"ik\" in extras:\n", + " # SymPy is one of Pyodide's own packages. python-flint is not installed:\n", + " # the one bundled with Pyodide 0.27 is too old for the toolbox, which\n", + " # then uses SymPy for the polynomial steps, about 2x slower for a\n", + " # general 6R arm (10 s to derive a Kinova Jaco).\n", + " await micropip.install(\"sympy\")\n", "\n", " wheels = sorted(Path(\"/pypi\").glob(\"roboticstoolbox_python-*.whl\"))\n", " if wheels:\n", @@ -61,17 +70,21 @@ " import google.colab # noqa: F401\n", " except ImportError:\n", " where, colab = \"locally\", False\n", + " if \"ik\" in extras:\n", + " try:\n", + " import sympy # noqa: F401\n", + " except ImportError:\n", + " print(\n", + " \"This notebook needs SymPy: \"\n", + " \"pip install 'roboticstoolbox-python[ik]'\"\n", + " )\n", " else:\n", " print(\"Installing roboticstoolbox-python...\")\n", + " requirement = \"roboticstoolbox-python\"\n", + " if extras:\n", + " requirement += \"[\" + \",\".join(extras) + \"]\"\n", " subprocess.run(\n", - " [\n", - " sys.executable,\n", - " \"-m\",\n", - " \"pip\",\n", - " \"install\",\n", - " \"-q\",\n", - " \"roboticstoolbox-python\",\n", - " ],\n", + " [sys.executable, \"-m\", \"pip\", \"install\", \"-q\", requirement],\n", " check=True,\n", " )\n", " where, colab = \"on Colab\", True\n", diff --git a/docs/notebooks/sync_bootstrap.py b/docs/notebooks/sync_bootstrap.py index 6889e57ec..b1ece0995 100644 --- a/docs/notebooks/sync_bootstrap.py +++ b/docs/notebooks/sync_bootstrap.py @@ -34,13 +34,23 @@ TEMPLATE_PATH = HERE / "_rtb_nb_bootstrap.py" -def generated_source(template: str) -> list[str]: - """Build the full source (as nbformat-style lines) for the bootstrap cell.""" - body = f"{GENERATED_HEADER}\n{template.rstrip()}\n\nCOLAB = await ensure_installed()\n" +def generated_source(template: str, extras: list[str] | tuple[str, ...] = ()) -> list[str]: + """Build the full source (as nbformat-style lines) for the bootstrap cell. + + :param extras: names of the toolbox extras the notebook needs, for example + ``["ik"]``; passed on to ``ensure_installed``. + """ + call = f"ensure_installed(extras={list(extras)!r})" if extras else "ensure_installed()" + body = f"{GENERATED_HEADER}\n{template.rstrip()}\n\nCOLAB = await {call}\n" lines = body.splitlines(keepends=True) return lines +def notebook_extras(nb: dict) -> list[str]: + """The extras a notebook asks for, from ``metadata["rtb_bootstrap"]["extras"]``.""" + return list(nb.get("metadata", {}).get("rtb_bootstrap", {}).get("extras", [])) + + def cell_source_text(cell: dict) -> str: source = cell.get("source", []) return "".join(source) if isinstance(source, list) else source @@ -67,7 +77,7 @@ def process_notebook(path: Path, template: str, fix: bool) -> bool: if not is_bootstrap_cell(cell): continue - desired_source = generated_source(template) + desired_source = generated_source(template, notebook_extras(nb)) current_source = cell.get("source", []) if not isinstance(current_source, list): current_source = [current_source] diff --git a/docs/notebooks/two-link-dynamics.ipynb b/docs/notebooks/two-link-dynamics.ipynb index a1aca56be..0a08cbfec 100644 --- a/docs/notebooks/two-link-dynamics.ipynb +++ b/docs/notebooks/two-link-dynamics.ipynb @@ -43,9 +43,12 @@ "from pathlib import Path\n", "\n", "\n", - "async def ensure_installed() -> bool:\n", + "async def ensure_installed(extras: tuple[str, ...] | list[str] = ()) -> bool:\n", " \"\"\"Install roboticstoolbox-python if needed, and report the environment.\n", "\n", + " :param extras: toolbox extras the notebook needs, for example ``[\"ik\"]``. They\n", + " are set per notebook in its metadata (``rtb_bootstrap.extras``) and\n", + " passed here by the generated cell.\n", " :returns: True if running on Google Colab, False otherwise.\n", " \"\"\"\n", " if sys.platform == \"emscripten\":\n", @@ -60,6 +63,12 @@ " # (rai-opensource/spatialmath-python#250).\n", " await micropip.install(\"spatialmath-python\", deps=False)\n", " await micropip.install(\"ipympl\")\n", + " if \"ik\" in extras:\n", + " # SymPy is one of Pyodide's own packages. python-flint is not installed:\n", + " # the one bundled with Pyodide 0.27 is too old for the toolbox, which\n", + " # then uses SymPy for the polynomial steps, about 2x slower for a\n", + " # general 6R arm (10 s to derive a Kinova Jaco).\n", + " await micropip.install(\"sympy\")\n", "\n", " wheels = sorted(Path(\"/pypi\").glob(\"roboticstoolbox_python-*.whl\"))\n", " if wheels:\n", @@ -76,17 +85,21 @@ " import google.colab # noqa: F401\n", " except ImportError:\n", " where, colab = \"locally\", False\n", + " if \"ik\" in extras:\n", + " try:\n", + " import sympy # noqa: F401\n", + " except ImportError:\n", + " print(\n", + " \"This notebook needs SymPy: \"\n", + " \"pip install 'roboticstoolbox-python[ik]'\"\n", + " )\n", " else:\n", " print(\"Installing roboticstoolbox-python...\")\n", + " requirement = \"roboticstoolbox-python\"\n", + " if extras:\n", + " requirement += \"[\" + \",\".join(extras) + \"]\"\n", " subprocess.run(\n", - " [\n", - " sys.executable,\n", - " \"-m\",\n", - " \"pip\",\n", - " \"install\",\n", - " \"-q\",\n", - " \"roboticstoolbox-python\",\n", - " ],\n", + " [sys.executable, \"-m\", \"pip\", \"install\", \"-q\", requirement],\n", " check=True,\n", " )\n", " where, colab = \"on Colab\", True\n", diff --git a/tests/test_notebook_bootstrap.py b/tests/test_notebook_bootstrap.py index 185c9e6d8..2577be531 100644 --- a/tests/test_notebook_bootstrap.py +++ b/tests/test_notebook_bootstrap.py @@ -195,3 +195,31 @@ def test_bootstrap_cell_is_found_wherever_it_is(tmp_path): assert "async def ensure_installed():" in text # the other cells are untouched assert sum(sync_bootstrap.is_bootstrap_cell(c) for c in nb["cells"]) == 1 + + +def test_extras_come_from_the_notebook_metadata(tmp_path): + template = "async def ensure_installed(extras=()):\n return False\n" + nb_path = tmp_path / "demo.ipynb" + nb = { + "cells": [_code_cell(f"{sync_bootstrap.MARKER}\n# stale\n")], + "metadata": {"rtb_bootstrap": {"extras": ["ik"]}}, + "nbformat": 4, + "nbformat_minor": 5, + } + nb_path.write_text(json.dumps(nb), encoding="utf-8") + + assert sync_bootstrap.process_notebook(nb_path, template=template, fix=True) + + text = "".join(json.loads(nb_path.read_text(encoding="utf-8"))["cells"][0]["source"]) + assert text.rstrip().endswith("COLAB = await ensure_installed(extras=['ik'])") + # a second pass finds nothing to do, and removing the metadata is noticed + assert not sync_bootstrap.process_notebook(nb_path, template=template, fix=True) + nb = json.loads(nb_path.read_text(encoding="utf-8")) + del nb["metadata"]["rtb_bootstrap"] + nb_path.write_text(json.dumps(nb), encoding="utf-8") + assert sync_bootstrap.process_notebook(nb_path, template=template, fix=False) + + +def test_no_extras_gives_the_plain_call(): + lines = sync_bootstrap.generated_source("async def ensure_installed(): ...\n") + assert "".join(lines).rstrip().endswith("COLAB = await ensure_installed()")