diff --git a/README.md b/README.md index bb505fe..9a53930 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,8 @@ to embodied intelligence and deployment: Physical Simulation Genesis Simulation - Load and control a Franka Panda, tune PD controllers, solve inverse kinematics, execute pick-and-place, and scale to parallel GPU environments. - Genesis parallel robot simulation + Progress from Franka control, inverse kinematics, and parallel GPU simulation to ROCm vision and tactile perception, then build a guarded language-guided agent with an interactive live HUD and reproducible scene layouts. + Genesis language-guided Physical AI agent MuJoCo + PyTorch @@ -88,9 +88,12 @@ teaching content was made possible through the joint efforts of these partners. | Nanjing University | [Prof. Jingwei Xu](https://njudeepengine.github.io/jingweixu/), [NJUDeepEngine](https://github.com/NJUDeepEngine) | LLM | | National Yang Ming Chiao Tung University | [Prof. Ping-Chun Hsieh](https://pinghsieh.github.io/), [Reinforcement Learning and Bandits Lab](https://pinghsieh.github.io/group.html) | Physical AI, Reinforcement Learning on MuJoCo | -We also thank the open-source projects that make these labs possible, -including [Genesis](https://github.com/Genesis-Embodied-AI/Genesis) and -[MuJoCo](https://github.com/google-deepmind/mujoco). +We also thank the AMD AECG team for contributing portions of the Physical AI +teaching materials, along with the open-source projects that make these labs +possible, including [Genesis](https://github.com/Genesis-Embodied-AI/Genesis) +and [MuJoCo](https://github.com/google-deepmind/mujoco). Detailed source +attributions and links to the original repositories are provided in each +relevant notebook. ## License diff --git a/README.zh-TW.md b/README.zh-TW.md index a1d90a4..7c78034 100644 --- a/README.zh-TW.md +++ b/README.zh-TW.md @@ -23,8 +23,8 @@ 物理模擬 Genesis Simulation - 載入並控制 Franka Panda、調整 PD 控制器、求解逆向運動學、執行夾取與放置,並擴展至 GPU 平行模擬環境。 - Genesis 平行機器人模擬 + 從 Franka 控制、逆向運動學與 GPU 平行模擬,進階至 ROCm 視覺與觸覺感知,最後建立具備互動式即時 HUD、可重現場景配置與安全驗證流程的語言引導代理。 + Genesis 語言引導實體 AI 代理 MuJoCo + PyTorch @@ -77,7 +77,7 @@ AUP 感謝以下大學、教授與實驗室的共同投入,讓這些教學內 | 南京大學 | [徐经纬教授](https://njudeepengine.github.io/jingweixu/)、[NJUDeepEngine](https://github.com/NJUDeepEngine) | LLM | | 國立陽明交通大學 | [謝秉均教授](https://pinghsieh.github.io/)、[Reinforcement Learning and Bandits Lab](https://pinghsieh.github.io/group.html) | 實體 AI、MuJoCo 強化學習 | -我們也感謝支援這些課程的開源專案,包括 [Genesis](https://github.com/Genesis-Embodied-AI/Genesis) 與 [MuJoCo](https://github.com/google-deepmind/mujoco)。 +我們也感謝 AMD AECG 團隊提供部分實體 AI 教學素材,以及支援這些課程的開源專案,包括 [Genesis](https://github.com/Genesis-Embodied-AI/Genesis) 與 [MuJoCo](https://github.com/google-deepmind/mujoco)。各項素材的詳細來源說明與原始 repository 連結,皆附於相關 notebook 中。 ## 授權 diff --git a/assets/readme/gs06.gif b/assets/readme/gs06.gif new file mode 100644 index 0000000..765ec9b Binary files /dev/null and b/assets/readme/gs06.gif differ diff --git a/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/.dockerignore b/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/.dockerignore new file mode 100644 index 0000000..450c8c9 --- /dev/null +++ b/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/.dockerignore @@ -0,0 +1,8 @@ +.git +.ipynb_checkpoints +Artifacts +Videos +models +**/__pycache__ +*.pyc +*.gguf diff --git a/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/.gitignore b/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/.gitignore new file mode 100644 index 0000000..270d342 --- /dev/null +++ b/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/.gitignore @@ -0,0 +1,7 @@ +.ipynb_checkpoints/ +Artifacts/ +Videos/ +models/ +helpers/__pycache__/ +*.pyc +*.gguf diff --git a/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/Dockerfile b/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/Dockerfile index 0836552..c435ddf 100644 --- a/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/Dockerfile +++ b/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/Dockerfile @@ -18,12 +18,31 @@ # SOFTWARE. ARG BASE_IMAGE=ghcr.io/amdresearch/auplc-base:latest +ARG LLAMA_CPP_REF=8e7f22b67ef4667b4ddd50230771287f328cfb3f +FROM ubuntu:24.04 AS llama-builder + +ARG LLAMA_CPP_REF +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential ca-certificates cmake git glslc libvulkan-dev spirv-headers \ + && rm -rf /var/lib/apt/lists/* +RUN git clone --filter=blob:none https://github.com/ggml-org/llama.cpp.git /src/llama.cpp && \ + git -C /src/llama.cpp checkout "${LLAMA_CPP_REF}" && \ + cmake -S /src/llama.cpp -B /src/llama.cpp/build \ + -DCMAKE_BUILD_TYPE=Release \ + -DGGML_VULKAN=ON \ + -DGGML_NATIVE=OFF \ + -DLLAMA_CURL=OFF \ + -DBUILD_SHARED_LIBS=OFF && \ + cmake --build /src/llama.cpp/build --target llama-server --parallel + FROM ${BASE_IMAGE} ARG GENESIS_WORLD_VERSION=1.3.1 ENV ROCM_PYTHON_LIB=/opt/rocm-python/lib ENV LD_LIBRARY_PATH="${ROCM_PYTHON_LIB}:${LD_LIBRARY_PATH}" +ENV LLAMA_SERVER_BIN=/opt/llama/bin/llama-server +ENV LLAMA_MODEL_PATH=/opt/workspace/PhySim/models/Llama-3.2-3B-Instruct-Q4_K_M.gguf USER root RUN SDK_LIB=$(python3 -c "import _rocm_sdk_core, os; print(os.path.join(os.path.dirname(_rocm_sdk_core.__file__), 'lib'))") && \ @@ -35,13 +54,21 @@ RUN SDK_LIB=$(python3 -c "import _rocm_sdk_core, os; print(os.path.join(os.path. RUN apt-get update && apt-get install -y --no-install-recommends \ ffmpeg \ libgl1 libglx-mesa0 libgl1-mesa-dri libegl1 libgbm1 libglib2.0-0 \ + libvulkan1 mesa-vulkan-drivers \ && rm -rf /var/lib/apt/lists/* RUN pip3 install --no-cache-dir \ + "huggingface-hub>=0.34,<2" \ + "ipywidgets>=8,<9" \ "numpy>=1.26.4,<2.4" \ + "opencv-python>=4.10,<6" \ + "requests>=2.31" \ + "tqdm>=4.66" \ "genesis-world==${GENESIS_WORLD_VERSION}" -COPY --chown=jovyan:1000 PhySim*.ipynb /opt/workspace/PhySim/ +COPY --from=llama-builder /src/llama.cpp/build/bin/llama-server /opt/llama/bin/llama-server +COPY --chown=jovyan:1000 GS*.ipynb /opt/workspace/PhySim/ +COPY --chown=jovyan:1000 helpers /opt/workspace/PhySim/helpers COPY --chown=jovyan:1000 xml /opt/workspace/PhySim/xml USER jovyan diff --git a/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/PhySim01_hello_genesis.ipynb b/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/GS01_hello_genesis.ipynb similarity index 71% rename from projects/Physical-AI/Physical-Simulation/Genesis-Simulation/PhySim01_hello_genesis.ipynb rename to projects/Physical-AI/Physical-Simulation/Genesis-Simulation/GS01_hello_genesis.ipynb index 8b90bf4..7e15385 100644 --- a/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/PhySim01_hello_genesis.ipynb +++ b/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/GS01_hello_genesis.ipynb @@ -2,55 +2,53 @@ "cells": [ { "cell_type": "markdown", - "id": "c9583f2f-64b5-4e5e-9a2c-bd82573d6196", "metadata": {}, "source": [ - "# Load a Robot into the Genesis Scene\n", + "# GS01 — Hello Genesis: Load a Robot Scene\n", "\n", - "In this tutorial, we introduce how to set up a basic Genesis environment, create a simple scene, and load a robot. We will go step by step through initialization, scene construction, simulator options, and visualizer settings.\n", + "### Lab Description\n", "\n", + "This introductory lab builds a complete Genesis simulation from initialization to recorded output. You will create a scene, add a plane and a Franka Emika Panda arm, configure a camera, and advance the physics simulation in headless mode.\n", "\n", - "## What You Will Learn\n", + "Genesis combines a physics simulator and visualizer inside each `Scene`. On AMD hardware, this course uses the `gs.amdgpu` backend so simulation kernels execute through ROCm.\n", "\n", - "1. How to Initialize Genesis\n", + "#### Recommended Hardware\n", "\n", - "* Understand the basic startup procedure using `gs.init()`.\n", - "* Select `vulkan` backend recommended for AMD GPUs.\n", + "An AMD GPU supported by ROCm, such as an AMD Radeon™ GPU or AMD Ryzen™ AI processor with integrated Radeon graphics.\n", "\n", - "2. How to Build a Scene\n", + "#### Software Environment\n", "\n", - "* Every simulation in Genesis occurs within a `Scene`, which integrates a **Simulator** (physics engine) and a **Visualizer** (rendering engine).\n", - "* Show the difference between GUI (`show_viewer=True`) and headless (`False`) modes.\n", + "OS: Ubuntu 24.04 LTS \n", + "Install [AUP Learning Cloud](https://amdresearch.github.io/aup-learning-cloud/installation/quick-start.html). The Genesis Simulation image provides ROCm, PyTorch, and `genesis-world==1.3.1`.\n", "\n", - "3. How to Configure the Visualizer\n", + "## Goals\n", "\n", - "* Learn to control virtual camera behavior using `ViewerOptions` (position, look-at point, and field of view).\n", - "* Adjust viewer resolution, frame rate, and threading for optimized rendering performance.\n", - "\n", - "4. How to Add Entities and Robots\n", - "\n", - "* Understand that all physical elements are represented as `Entity` objects, managed via `scene.add_entity()`.\n", - "* See a full example importing a **Franka Emika Panda** robotic arm and a plane into the scene, preparing for motion control." - ] + "- Initialize Genesis with the AMD GPU backend.\n", + "- Create and configure a headless Genesis scene.\n", + "- Add a plane, Franka Panda robot, and fixed camera.\n", + "- Understand the `build()` and `step()` simulation lifecycle.\n", + "- Render camera data and record a simulation video." + ], + "id": "c9583f2f-64b5-4e5e-9a2c-bd82573d6196" }, { "cell_type": "code", - "execution_count": null, - "id": "102fe2cb-39b4-47dc-83a2-c7f9e75be984", "metadata": {}, - "outputs": [], "source": [ "# Suppress warning messages for clearer output\n", "import os\n", "import warnings\n", "\n", "os.environ[\"TI_LOG_LEVEL\"] = \"error\"\n", - "warnings.filterwarnings(\"ignore\")" - ] + "warnings.filterwarnings(\"ignore\")\n", + "os.makedirs(\"Videos\", exist_ok=True)" + ], + "execution_count": null, + "outputs": [], + "id": "102fe2cb-39b4-47dc-83a2-c7f9e75be984" }, { "cell_type": "markdown", - "id": "bdde9503-dc15-48dc-989f-fe859118c459", "metadata": {}, "source": [ "## Backend Initialization\n", @@ -71,24 +69,25 @@ "```\n", "\n", "Here, we use the default init settings and set backend to amdgpu." - ] + ], + "id": "bdde9503-dc15-48dc-989f-fe859118c459" }, { "cell_type": "code", - "execution_count": null, - "id": "b3a7428b-2c74-4199-a975-7dbf6312f6ce", "metadata": {}, - "outputs": [], "source": [ "import genesis as gs\n", "import numpy as np\n", "\n", + "assert \"scene\" not in globals(), \"Scene already exists. Restart the kernel before rerunning this lab.\"\n", "gs.init(backend=gs.amdgpu, theme=\"light\")" - ] + ], + "execution_count": null, + "outputs": [], + "id": "b3a7428b-2c74-4199-a975-7dbf6312f6ce" }, { "cell_type": "markdown", - "id": "5aa289ea-6a36-4847-b992-d4904b74f76e", "metadata": {}, "source": [ "## Create a Scene\n", @@ -107,21 +106,21 @@ "Later, instead of rendering the scene directly in this notebook, we will save the rendering output as a video for visualization.\n", "\n", "Note that you can only create the scene ONCE. Recreating it will cause an ERROR. \n" - ] + ], + "id": "5aa289ea-6a36-4847-b992-d4904b74f76e" }, { "cell_type": "code", - "execution_count": null, - "id": "08be920a-fcc8-424c-a200-cb9ec4119d48", "metadata": {}, - "outputs": [], "source": [ "scene = gs.Scene(show_viewer=False)" - ] + ], + "execution_count": null, + "outputs": [], + "id": "08be920a-fcc8-424c-a200-cb9ec4119d48" }, { "cell_type": "markdown", - "id": "57a4876e-e592-413e-af26-eb5e240fee1b", "metadata": {}, "source": [ "## Load a Robot\n", @@ -142,14 +141,12 @@ "* **Soft robot descriptions**\n", "\n", "This flexibility means that most commonly used robotics models—such as those described in URDF or MJCF—can be seamlessly integrated into Genesis." - ] + ], + "id": "57a4876e-e592-413e-af26-eb5e240fee1b" }, { "cell_type": "code", - "execution_count": null, - "id": "79547b30", "metadata": {}, - "outputs": [], "source": [ "# Load Entity\n", "\n", @@ -159,24 +156,24 @@ "franka = scene.add_entity(\n", " gs.morphs.MJCF(file=\"xml/franka_emika_panda/panda.xml\"),\n", ")" - ] + ], + "execution_count": null, + "outputs": [], + "id": "79547b30" }, { "cell_type": "markdown", - "id": "43fd365b-f502-4364-bd24-85cb60c9ae9c", "metadata": {}, "source": [ "## Add a Camera\n", "\n", "Next, we add a camera to record what happens in the scene." - ] + ], + "id": "43fd365b-f502-4364-bd24-85cb60c9ae9c" }, { "cell_type": "code", - "execution_count": null, - "id": "184eeb6d-184d-428a-950b-a2be6ff9f3e9", "metadata": {}, - "outputs": [], "source": [ "cam = scene.add_camera(\n", " res=(640, 480),\n", @@ -187,46 +184,46 @@ ")\n", "\n", "print(\"Successfully loaded a camera.\")" - ] + ], + "execution_count": null, + "outputs": [], + "id": "184eeb6d-184d-428a-950b-a2be6ff9f3e9" }, { "cell_type": "markdown", - "id": "8601f73c-aed2-4b8d-a151-fc51262fe2eb", "metadata": {}, "source": [ "## Build the Scene\n", "\n", "After creating a scene, it must be built explicitly by calling `scene.build()`. This step is required because Genesis uses just-in-time (JIT) compilation to generate GPU kernels on the fly. Building the scene initializes this process, allocates device memory, and sets up the underlying data structures required for simulation." - ] + ], + "id": "8601f73c-aed2-4b8d-a151-fc51262fe2eb" }, { "cell_type": "code", - "execution_count": null, - "id": "86735786-f847-4221-9a20-51edecaae014", "metadata": {}, - "outputs": [], "source": [ "scene.build()\n", "\n", "print(\"Successfully built the scene.\")" - ] + ], + "execution_count": null, + "outputs": [], + "id": "86735786-f847-4221-9a20-51edecaae014" }, { "cell_type": "markdown", - "id": "a392fbb8-df0d-4ef3-b9e1-123f4b1307a5", "metadata": {}, "source": [ "## Start simulating\n", "\n", "Then we start simulating and save it in the `Video` folder." - ] + ], + "id": "a392fbb8-df0d-4ef3-b9e1-123f4b1307a5" }, { "cell_type": "code", - "execution_count": null, - "id": "76791170", "metadata": {}, - "outputs": [], "source": [ "# render rgb, depth, segmentation, normal\n", "rgb, depth, segmentation, normal = cam.render(rgb=True, depth=True, segmentation=True, normal=True)\n", @@ -237,37 +234,55 @@ " cam.render()\n", "\n", "cam.stop_recording()" - ] + ], + "execution_count": null, + "outputs": [], + "id": "76791170" }, { "cell_type": "markdown", - "id": "364bfa53-0131-43c4-b224-bdbc1d3a6b5f", "metadata": {}, "source": [ "## Show the video\n", "\n", "If everything works correctly, you will see a robotic arm appear on the screen and naturally fall due to gravity." - ] + ], + "id": "364bfa53-0131-43c4-b224-bdbc1d3a6b5f" }, { "cell_type": "code", - "execution_count": null, - "id": "8468b766", "metadata": {}, - "outputs": [], "source": [ "from IPython.display import Video\n", "\n", "Video(url=\"Videos/video_01.mp4\")" - ] + ], + "execution_count": null, + "outputs": [], + "id": "8468b766" }, { "cell_type": "code", - "execution_count": null, - "id": "ce238e20-8b2d-45e0-a59d-812d57d5add9", "metadata": {}, + "source": [], + "execution_count": null, "outputs": [], - "source": [] + "id": "ce238e20-8b2d-45e0-a59d-812d57d5add9" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Conclusions\n", + "\n", + "You initialized Genesis on an AMD GPU, built a headless scene, loaded a Franka Panda robot, rendered camera data, and recorded a simulation. In GS02, you will replace passive motion with explicit joint and PD control.\n", + "\n", + "---\n", + "\n", + "Copyright (C) 2026 Advanced Micro Devices, Inc. All rights reserved. Portions of this file consist of AI-generated content. \n", + "SPDX-License-Identifier: MIT" + ], + "id": "a550709c" } ], "metadata": { diff --git a/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/GS02_control_your_robot.ipynb b/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/GS02_control_your_robot.ipynb new file mode 100644 index 0000000..9277e08 --- /dev/null +++ b/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/GS02_control_your_robot.ipynb @@ -0,0 +1,402 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# GS02 — PD Control and Robot DOFs\n", + "\n", + "### Lab Description\n", + "\n", + "A robot arm collapses under gravity when no controller supports it. This lab introduces Genesis joint-level control and shows how proportional–derivative (PD) gains produce stable, physically consistent motion.\n", + "\n", + "Using the Franka Emika Panda, you will inspect its seven arm joints and two gripper joints, then compare direct state setting with position, velocity, and force control.\n", + "\n", + "#### Recommended Hardware\n", + "\n", + "An AMD GPU supported by ROCm, such as an AMD Radeon™ GPU or AMD Ryzen™ AI processor with integrated Radeon graphics.\n", + "\n", + "#### Software Environment\n", + "\n", + "OS: Ubuntu 24.04 LTS \n", + "Install [AUP Learning Cloud](https://amdresearch.github.io/aup-learning-cloud/installation/quick-start.html). The Genesis Simulation image provides ROCm, PyTorch, and `genesis-world==1.3.1`.\n", + "\n", + "## Goals\n", + "\n", + "- Identify the Franka Panda joints and nine degrees of freedom.\n", + "- Configure proportional, derivative, and force-limit parameters.\n", + "- Compare direct state updates with physically consistent control commands.\n", + "- Apply position, velocity, and force control to selected DOFs.\n", + "- Record and compare the resulting robot motion." + ], + "id": "c367b6a3-310e-4133-8b79-693d422f95f6" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Suppress warning messages for clearer output\n", + "import os\n", + "import warnings\n", + "\n", + "os.environ[\"TI_LOG_LEVEL\"] = \"error\"\n", + "warnings.filterwarnings(\"ignore\")\n", + "os.makedirs(\"Videos\", exist_ok=True)" + ], + "execution_count": null, + "outputs": [], + "id": "77699ce4-db80-4b47-bfa2-bbbda26ab3f0" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Init and Create a Scene\n", + "\n", + "In GS01, we used mostly default settings when creating the scene. Here we use more advanced simulator and viewer options.\n", + "\n", + "You can customize the **Simulator** and **Visualizer** through detailed configuration options when creating a scene.\n", + "\n", + "**Simulator options** define the physical simulation behavior. Common parameters include:\n", + "\n", + "* **dt** – Duration of each simulation step (in seconds)\n", + "* **gravity** – Gravity force vector (N/kg)\n", + "* **floor_height** – Ground plane height\n", + "\n", + "**Visualizer options** control the virtual camera and rendering behavior. Key parameters:\n", + "\n", + "* **camera_pos** – Initial position of the camera\n", + "* **camera_lookat** – Target point the camera focuses on\n", + "* **camera_fov** – Field of view (in degrees)\n", + "* **max_FPS** – Set max FPS.\n", + "" + ], + "id": "5c6fa926-026c-4ab3-8e01-af1b6a7d009d" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "import genesis as gs\n", + "import numpy as np\n", + "\n", + "assert \"scene\" not in globals(), \"Scene already exists. Restart the kernel before rerunning this lab.\"\n", + "\n", + "########################## init ##########################\n", + "gs.init(backend=gs.amdgpu, theme=\"light\")\n", + "\n", + "########################## create a scene ##########################\n", + "scene = gs.Scene(\n", + " viewer_options=gs.options.ViewerOptions(\n", + " camera_pos=(0, -3.5, 2.5),\n", + " camera_lookat=(0.0, 0.0, 0.5),\n", + " camera_fov=30,\n", + " max_FPS=60,\n", + " ),\n", + " sim_options=gs.options.SimOptions(\n", + " dt=0.01,\n", + " ),\n", + " show_viewer=False,\n", + ")" + ], + "execution_count": null, + "outputs": [], + "id": "f16475de" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Add Entities and Build the Scene\n", + "\n", + "Just like what we did in Lab 1, we add a **plane**, an **arm**, and a **camera** to the scene, and then build the scene." + ], + "id": "a2f702fe-eb80-4f76-9694-c2fed7e1408c" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "########################## entities ##########################\n", + "plane = scene.add_entity(\n", + " gs.morphs.Plane(),\n", + ")\n", + "franka = scene.add_entity(\n", + " gs.morphs.MJCF(\n", + " file=\"xml/franka_emika_panda/panda.xml\",\n", + " ),\n", + ")\n", + "cam = scene.add_camera(\n", + " res=(640, 480),\n", + " pos=(3.5, 0.0, 2.5),\n", + " lookat=(0, 0, 0.5),\n", + " fov=30,\n", + " GUI=True,\n", + ")\n", + "\n", + "########################## build ##########################\n", + "scene.build()\n", + "print(\"Successfully built the scene.\")" + ], + "execution_count": null, + "outputs": [], + "id": "0eaa9cca" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Control Joints and DOFs\n", + "\n", + "In robotics, the terms **joint** and **degree of freedom (DOF)** are related but not quite the same. \n", + "\n", + "A joint is the physical connection between two parts (or links) of a robot that allows relative motion. For example, a hinge, a slider, or a ball-and-socket connection. Each joint enables certain types of movement.\n", + "\n", + "A degree of freedom (DOF), on the other hand, refers to the number of independent ways a joint (or the entire robot) can move. For instance, a revolute (rotational) joint has one DOF because it can rotate around a single axis, while a spherical joint has three DOFs, it can rotate around three perpendicular axes.\n", + "\n", + "![image.png](attachment:267b2468-7d14-45fd-a20d-3eb514f2c857.png)\n", + "\n", + "Take Franka Panda arm for example, it has 7 revolute joints in the arm and 2 prismatic joints in its gripper. Since each joint has only 1 DOF, the robot ends up with 9 DOFs in total." + ], + "id": "d11a3c68-fccb-4e09-916e-ca3a9a048861" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "jnt_names = [\n", + " \"joint1\",\n", + " \"joint2\",\n", + " \"joint3\",\n", + " \"joint4\",\n", + " \"joint5\",\n", + " \"joint6\",\n", + " \"joint7\",\n", + " \"finger_joint1\",\n", + " \"finger_joint2\",\n", + "]\n", + "\n", + "dofs_idx_temp = [franka.get_joint(name).dofs_idx_local for name in jnt_names]\n", + "dofs_idx = [idx for sublist in dofs_idx_temp for idx in sublist]\n", + "\n", + "print(dofs_idx)" + ], + "execution_count": null, + "outputs": [], + "id": "19902729" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Control Gains\n", + "\n", + "Control gains decide how much torque the controller applies to reduce errors in position or velocity. URDF/MJCF files usually provide default values, but manual tuning is often necessary for stable, realistic control.\n", + "\n", + "Genesis exposes three functions:\n", + "\n", + "* `.set_dofs_kp` — proportional gains\n", + "* `.set_dofs_kv` — derivative gains\n", + "* `.set_dofs_force_range` — safety limits on torque/force\n", + "\n", + "Together, `kp` and `kv` form the **PD controller**:\n", + "\n", + "For Franka, the arm joints (joint1–joint7) use higher gains for stiffness and precision, and the finger joints use lower gains so they feel softer and safer when grasping objects. \n", + "\n", + "A typical setup looks like this:" + ], + "id": "c7ff8a1b-a745-4f58-87b0-36f3d7255ef8" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "############ Optional: set control gains ############\n", + "\n", + "# set positional gains\n", + "franka.set_dofs_kp(\n", + " kp=np.array([4500, 4500, 3500, 3500, 2000, 2000, 2000, 100, 100]),\n", + " dofs_idx_local=dofs_idx,\n", + ")\n", + "# set velocity gains\n", + "franka.set_dofs_kv(\n", + " kv=np.array([450, 450, 350, 350, 200, 200, 200, 10, 10]),\n", + " dofs_idx_local=dofs_idx,\n", + ")\n", + "# set force range for safety\n", + "franka.set_dofs_force_range(\n", + " lower=np.array([-87, -87, -87, -87, -12, -12, -12, -100, -100]),\n", + " upper=np.array([87, 87, 87, 87, 12, 12, 12, 100, 100]),\n", + " dofs_idx_local=dofs_idx,\n", + ")" + ], + "execution_count": null, + "outputs": [], + "id": "6151d602-0c5d-4e20-a41b-1b01864f0151" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Directly Setting DOF Positions\n", + "\n", + "It’s also possible to set DOF positions directly with `.set_dofs_position`. This instantly changes the robot state and bypasses physics. This can be useful for resets or demonstrations, but it may create unrealistic motion that violates physical laws.\n", + "\n", + "To avoid overly lengthy log information, we will not print the detailed logs here, we’ll use a progress bar to indicate the progress.\n" + ], + "id": "02417769-e654-438e-a721-a044d29282d7" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "import logging\n", + "\n", + "from tqdm import tqdm\n", + "\n", + "# Set logger to warning to avoid log info.\n", + "gs.logger._logger.setLevel(logging.WARNING)\n", + "\n", + "# Camera recording\n", + "rgb, depth, segmentation, normal = cam.render(rgb=True, depth=True, segmentation=True, normal=True)\n", + "cam.start_recording(save_to_filename=\"Videos/video_02.mp4\", fps=60)\n", + "\n", + "# Hard reset\n", + "for i in tqdm(range(150), ncols=100):\n", + " if i < 50:\n", + " franka.set_dofs_position(np.array([1, 1, 0, 0, 0, 0, 0, 0.04, 0.04]), dofs_idx)\n", + " elif i < 100:\n", + " franka.set_dofs_position(np.array([-1, 0.8, 1, -2, 1, 0.5, -0.5, 0.04, 0.04]), dofs_idx)\n", + " else:\n", + " franka.set_dofs_position(np.array([0, 0, 0, 0, 0, 0, 0, 0, 0]), dofs_idx)\n", + " cam.render()\n", + " scene.step()" + ], + "execution_count": null, + "outputs": [], + "id": "c89dd04d" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Using the PD Controller\n", + "\n", + "To respect physics, use the `control_*` APIs instead. These send commands to the PD controller rather than overwriting the state. We can save it as a complete video and check the result." + ], + "id": "3ae6b658-988e-4d06-a529-237fe6e2d3b1" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# PD control\n", + "for i in tqdm(range(1250), ncols=100):\n", + " if i == 0:\n", + " franka.control_dofs_position(\n", + " np.array([1, 1, 0, 0, 0, 0, 0, 0.04, 0.04]),\n", + " dofs_idx,\n", + " )\n", + " elif i == 250:\n", + " franka.control_dofs_position(\n", + " np.array([-1, 0.8, 1, -2, 1, 0.5, -0.5, 0.04, 0.04]),\n", + " dofs_idx,\n", + " )\n", + " elif i == 500:\n", + " franka.control_dofs_position(\n", + " np.array([0, 0, 0, 0, 0, 0, 0, 0, 0]),\n", + " dofs_idx,\n", + " )\n", + " elif i == 750:\n", + " # control first dof with velocity, and the rest with position\n", + " franka.control_dofs_position(\n", + " np.array([0, 0, 0, 0, 0, 0, 0, 0, 0])[1:],\n", + " dofs_idx[1:],\n", + " )\n", + " franka.control_dofs_velocity(\n", + " np.array([1.0, 0, 0, 0, 0, 0, 0, 0, 0])[:1],\n", + " dofs_idx[:1],\n", + " )\n", + " elif i == 1000:\n", + " franka.control_dofs_force(\n", + " np.array([0, 0, 0, 0, 0, 0, 0, 0, 0]),\n", + " dofs_idx,\n", + " )\n", + "\n", + " cam.render()\n", + " scene.step()\n", + "\n", + "cam.stop_recording()" + ], + "execution_count": null, + "outputs": [], + "id": "bc94198b-ffb3-4e5c-822f-4f42dbff1eb5" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Show the video\n", + "\n", + "In the video, you’ll first see three relatively rigid movements, these are discontinuous actions created using set_dofs_position. The following smoother, continuous motions are produced by the control_* APIs. Therefore, when creating robots in a virtual environment that behave according to physical laws, we usually use the control_* APIs to write the program." + ], + "id": "d9a2ef33-c6ef-48d1-8173-832b24a260f2" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from IPython.display import Video\n", + "\n", + "Video(url=\"Videos/video_02.mp4\")" + ], + "execution_count": null, + "outputs": [], + "id": "b1a79fdc-c447-41ed-8319-0a5f19608b58" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [], + "execution_count": null, + "outputs": [], + "id": "3ac5f258-35b3-499c-9293-f06cd1384464" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Conclusions\n", + "\n", + "You identified the Franka Panda DOFs, configured PD gains and force limits, and compared direct state updates with position, velocity, and force control. In GS03, these control primitives become a complete IK-guided grasping sequence.\n", + "\n", + "---\n", + "\n", + "Copyright (C) 2026 Advanced Micro Devices, Inc. All rights reserved. Portions of this file consist of AI-generated content. \n", + "SPDX-License-Identifier: MIT" + ], + "id": "37b9f9be" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/PhySim03_motion_planning.ipynb b/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/GS03_motion_planning.ipynb similarity index 69% rename from projects/Physical-AI/Physical-Simulation/Genesis-Simulation/PhySim03_motion_planning.ipynb rename to projects/Physical-AI/Physical-Simulation/Genesis-Simulation/GS03_motion_planning.ipynb index 037bc2b..b7556dd 100644 --- a/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/PhySim03_motion_planning.ipynb +++ b/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/GS03_motion_planning.ipynb @@ -2,57 +2,68 @@ "cells": [ { "cell_type": "markdown", - "id": "e5adb4ed-96d0-49e9-95b9-fdad50a75751", "metadata": {}, "source": [ - "# Grasping with IK and Motion Planning\n", + "# GS03 — IK and Motion Planning for Grasping\n", "\n", - "In this section, we’ll walk through a simple grasping task, showing how to use **inverse kinematics (IK)** together with **motion planning** to pick up a cube.\n", + "### Lab Description\n", "\n", - "### What You Will Learn\n", + "This lab combines inverse kinematics (IK), joint-space motion planning, and gripper control to perform a complete simulated grasp. The Franka arm approaches a cube, lowers its gripper, applies grasping force, and lifts the object.\n", "\n", - "1. Applying Inverse Kinematics (IK)\n", - "Use `franka.inverse_kinematics()` to automatically determine the robot’s joint angles for specific poses, such as hovering above or grasping the cube.\n", + "The lesson builds directly on the PD control APIs introduced in GS02 and turns fixed end-effector targets into smooth robot trajectories.\n", "\n", - "2. Using Motion Planning for Smooth Trajectories\n", - "Learn how `plan_path()` interpolates joint-space waypoints between the current and goal poses.\n", + "#### Recommended Hardware\n", "\n", - "3. Performing a Full Grasping Sequence" - ] + "An AMD GPU supported by ROCm, such as an AMD Radeon™ GPU or AMD Ryzen™ AI processor with integrated Radeon graphics.\n", + "\n", + "#### Software Environment\n", + "\n", + "OS: Ubuntu 24.04 LTS \n", + "Install [AUP Learning Cloud](https://amdresearch.github.io/aup-learning-cloud/installation/quick-start.html). The Genesis Simulation image provides ROCm, PyTorch, and `genesis-world==1.3.1`.\n", + "\n", + "## Goals\n", + "\n", + "- Compute Franka joint targets with `inverse_kinematics()`.\n", + "- Generate smooth joint-space trajectories with `plan_path()`.\n", + "- Separate arm and gripper DOF control.\n", + "- Execute approach, grasp, and lift phases.\n", + "- Record and inspect the complete grasping sequence." + ], + "id": "e5adb4ed-96d0-49e9-95b9-fdad50a75751" }, { "cell_type": "code", - "execution_count": null, - "id": "0b774e89-ff67-4285-a5da-f47170945e31", "metadata": {}, - "outputs": [], "source": [ "# Suppress warning messages for clearer output\n", "import os\n", "import warnings\n", "\n", "os.environ[\"TI_LOG_LEVEL\"] = \"error\"\n", - "warnings.filterwarnings(\"ignore\")" - ] + "warnings.filterwarnings(\"ignore\")\n", + "os.makedirs(\"Videos\", exist_ok=True)" + ], + "execution_count": null, + "outputs": [], + "id": "0b774e89-ff67-4285-a5da-f47170945e31" }, { "cell_type": "markdown", - "id": "204d0dbb-69c6-42d1-8c57-cec4b082ba27", "metadata": {}, "source": [ "## Init and Create a Scene" - ] + ], + "id": "204d0dbb-69c6-42d1-8c57-cec4b082ba27" }, { "cell_type": "code", - "execution_count": null, - "id": "b7a9c1c3-7661-40d7-b895-79de1d0337e0", "metadata": {}, - "outputs": [], "source": [ "import genesis as gs\n", "import numpy as np\n", "\n", + "assert \"scene\" not in globals(), \"Scene already exists. Restart the kernel before rerunning this lab.\"\n", + "\n", "########################## init ##########################\n", "gs.init(backend=gs.amdgpu, theme=\"light\")\n", "\n", @@ -69,24 +80,24 @@ " ),\n", " show_viewer=False,\n", ")" - ] + ], + "execution_count": null, + "outputs": [], + "id": "b7a9c1c3-7661-40d7-b895-79de1d0337e0" }, { "cell_type": "markdown", - "id": "28a9816b-1905-47ff-acb1-1b65e78cb62e", "metadata": {}, "source": [ "## Add Entities and Build the Scene\n", "\n", "Just like what we did in Lab 1, we add a plane, an arm, and a camera to the scene, and then build the scene." - ] + ], + "id": "28a9816b-1905-47ff-acb1-1b65e78cb62e" }, { "cell_type": "code", - "execution_count": null, - "id": "85fca6cf-23ec-4925-b691-8208ae7489af", "metadata": {}, - "outputs": [], "source": [ "########################## entities ##########################\n", "plane = scene.add_entity(\n", @@ -110,24 +121,24 @@ ")\n", "########################## build ##########################\n", "scene.build()" - ] + ], + "execution_count": null, + "outputs": [], + "id": "85fca6cf-23ec-4925-b691-8208ae7489af" }, { "cell_type": "markdown", - "id": "7ae79515-10b9-4853-bafc-ade2a851640f", "metadata": {}, "source": [ "## Set Control Gains\n", "\n", "Just like Lab2, we set control gains for the arm." - ] + ], + "id": "7ae79515-10b9-4853-bafc-ade2a851640f" }, { "cell_type": "code", - "execution_count": null, - "id": "cac12a53-5b5b-4533-b1e8-a5b901444dff", "metadata": {}, - "outputs": [], "source": [ "rgb, depth, segmentation, normal = cam.render(rgb=True, depth=True, segmentation=True, normal=True)\n", "cam.start_recording(save_to_filename=\"Videos/video_03.mp4\", fps=60)\n", @@ -146,11 +157,13 @@ " np.array([-87, -87, -87, -87, -12, -12, -12, -100, -100]),\n", " np.array([87, 87, 87, 87, 12, 12, 12, 100, 100]),\n", ")" - ] + ], + "execution_count": null, + "outputs": [], + "id": "cac12a53-5b5b-4533-b1e8-a5b901444dff" }, { "cell_type": "markdown", - "id": "d4c9877a-3fcb-4782-910a-55414d83778d", "metadata": {}, "source": [ "## Inverse Kinematics (IK)\n", @@ -158,42 +171,40 @@ "Inverse Kinematics lets us compute the robot’s joint positions for a given end-effector pose (position + orientation). This is essential for telling the robot where the hand should go, without manually specifying every joint angle.\n", "\n", "Let’s go step by step through the grasping process." - ] + ], + "id": "d4c9877a-3fcb-4782-910a-55414d83778d" }, { "cell_type": "markdown", - "id": "84c70acb-449f-4446-9f31-8b04e69ce8b0", "metadata": {}, "source": [ "### 1. Define the End-Effector" - ] + ], + "id": "84c70acb-449f-4446-9f31-8b04e69ce8b0" }, { "cell_type": "code", - "execution_count": null, - "id": "361bd79d-3710-4fb8-9c82-967c536d7dc2", "metadata": {}, - "outputs": [], "source": [ "end_effector = franka.get_link(\"hand\")" - ] + ], + "execution_count": null, + "outputs": [], + "id": "361bd79d-3710-4fb8-9c82-967c536d7dc2" }, { "cell_type": "markdown", - "id": "d6218a59-73be-4ad0-a75b-e498973e4215", "metadata": {}, "source": [ "### 2. Move to a Pre-Grasp Pose\n", "\n", "We’ll solve IK for a position directly above the cube, and open the gripper." - ] + ], + "id": "d6218a59-73be-4ad0-a75b-e498973e4215" }, { "cell_type": "code", - "execution_count": null, - "id": "bedf4dfe-d51c-484b-baaf-b351bc1c8406", "metadata": {}, - "outputs": [], "source": [ "# move to pre-grasp pose\n", "qpos = franka.inverse_kinematics(\n", @@ -203,22 +214,22 @@ ")\n", "# gripper open pos\n", "qpos[-2:] = 0.04" - ] + ], + "execution_count": null, + "outputs": [], + "id": "bedf4dfe-d51c-484b-baaf-b351bc1c8406" }, { "cell_type": "markdown", - "id": "dc1b99f1-1209-47dc-aa6d-9212037d0f5c", "metadata": {}, "source": [ - "To reach this smoothly, we use the motion planner. `plan_path` interpolates between the current joint angles and the target, generating a sequence of waypoints. Executing them one by one produces a natural trajectory." - ] + "To reach this smoothly, we use the motion planner. `plan_path()` searches for a valid joint-space route, performs collision and joint-limit checks, and interpolates the accepted route into waypoints. We verify its validity before executing the waypoints." + ], + "id": "dc1b99f1-1209-47dc-aa6d-9212037d0f5c" }, { "cell_type": "code", - "execution_count": null, - "id": "b3ef0fd2-1b2e-408c-adb3-d16862a09112", "metadata": {}, - "outputs": [], "source": [ "import logging\n", "\n", @@ -227,10 +238,14 @@ "# Set logger to warning to avoid log info.\n", "gs.logger._logger.setLevel(logging.WARNING)\n", "\n", - "path = franka.plan_path(\n", + "path, path_valid = franka.plan_path(\n", " qpos_goal=qpos,\n", " num_waypoints=200, # 2s duration\n", + " return_valid_mask=True,\n", ")\n", + "if not bool(np.asarray(path_valid.cpu() if hasattr(path_valid, \"cpu\") else path_valid).reshape(-1)[0]):\n", + " raise RuntimeError(\"Motion planning failed; adjust the target pose before execution.\")\n", + "\n", "# draw the planned path\n", "path_debug = scene.draw_debug_path(path, franka)\n", "\n", @@ -247,24 +262,24 @@ "for i in tqdm(range(100), desc=\"Reach the last waypoint\", ncols=100):\n", " cam.render()\n", " scene.step()" - ] + ], + "execution_count": null, + "outputs": [], + "id": "b3ef0fd2-1b2e-408c-adb3-d16862a09112" }, { "cell_type": "markdown", - "id": "88c71e74-0e0d-4505-9009-1410d3099666", "metadata": {}, "source": [ "### 3. Lower the Gripper\n", "\n", "Now we solve IK again, this time just above the cube’s top surface." - ] + ], + "id": "88c71e74-0e0d-4505-9009-1410d3099666" }, { "cell_type": "code", - "execution_count": null, - "id": "3c9b2e21-6f85-4220-8d59-d05990823ca9", "metadata": {}, - "outputs": [], "source": [ "# reach\n", "qpos = franka.inverse_kinematics(\n", @@ -277,24 +292,24 @@ "for i in tqdm(range(100), desc=\"Lower the gripper\", ncols=100):\n", " cam.render()\n", " scene.step()" - ] + ], + "execution_count": null, + "outputs": [], + "id": "3c9b2e21-6f85-4220-8d59-d05990823ca9" }, { "cell_type": "markdown", - "id": "a003333e-6bdd-462e-ab01-a2a695f13976", "metadata": {}, "source": [ "### 4. Close the Fingers (Grasp)\n", "\n", "We command the arm to hold its joint position, then apply closing force to the gripper fingers." - ] + ], + "id": "a003333e-6bdd-462e-ab01-a2a695f13976" }, { "cell_type": "code", - "execution_count": null, - "id": "583befbf-0a92-4540-840a-64204dbeedc7", "metadata": {}, - "outputs": [], "source": [ "# grasp\n", "franka.control_dofs_position(qpos[:-2], motors_dof)\n", @@ -303,24 +318,24 @@ "for i in tqdm(range(100), desc=\"Close the finger\", ncols=100):\n", " cam.render()\n", " scene.step()" - ] + ], + "execution_count": null, + "outputs": [], + "id": "583befbf-0a92-4540-840a-64204dbeedc7" }, { "cell_type": "markdown", - "id": "862fcd0f-c35a-4d42-8ef1-7f9688ead63a", "metadata": {}, "source": [ "### 5. Lift the Cube\n", "\n", "Finally, we solve IK for a higher position and move the arm upwards." - ] + ], + "id": "862fcd0f-c35a-4d42-8ef1-7f9688ead63a" }, { "cell_type": "code", - "execution_count": null, - "id": "45581280-45c5-44a7-afcb-33cbf44a8ec3", "metadata": {}, - "outputs": [], "source": [ "# lift\n", "qpos = franka.inverse_kinematics(\n", @@ -335,37 +350,55 @@ " scene.step()\n", "\n", "cam.stop_recording()" - ] + ], + "execution_count": null, + "outputs": [], + "id": "45581280-45c5-44a7-afcb-33cbf44a8ec3" }, { "cell_type": "markdown", - "id": "71bbc69c-4d27-42b7-a3d7-28b3e25b22e9", "metadata": {}, "source": [ "## Show the video\n", "\n", "In the video, you will see the robotic arm plan a path to pick up the block. It opens its gripper and then moves downward to grasp the block." - ] + ], + "id": "71bbc69c-4d27-42b7-a3d7-28b3e25b22e9" }, { "cell_type": "code", - "execution_count": null, - "id": "eda4709b-4663-41ac-850e-6be12eb52311", "metadata": {}, - "outputs": [], "source": [ "from IPython.display import Video\n", "\n", "Video(url=\"Videos/video_03.mp4\")" - ] + ], + "execution_count": null, + "outputs": [], + "id": "eda4709b-4663-41ac-850e-6be12eb52311" }, { "cell_type": "code", - "execution_count": null, - "id": "df08073f-1121-4d36-8ba4-0f13c1cf1853", "metadata": {}, + "source": [], + "execution_count": null, "outputs": [], - "source": [] + "id": "df08073f-1121-4d36-8ba4-0f13c1cf1853" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Conclusions\n", + "\n", + "You translated end-effector targets into joint configurations, planned a smooth approach, and executed a grasp-and-lift sequence with separate arm and gripper control. In GS04, you will extend these ideas to batched robot environments.\n", + "\n", + "---\n", + "\n", + "Copyright (C) 2026 Advanced Micro Devices, Inc. All rights reserved. Portions of this file consist of AI-generated content. \n", + "SPDX-License-Identifier: MIT" + ], + "id": "ed858301" } ], "metadata": { diff --git a/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/PhySim04_parallel_simulation.ipynb b/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/GS04_parallel_simulation.ipynb similarity index 63% rename from projects/Physical-AI/Physical-Simulation/Genesis-Simulation/PhySim04_parallel_simulation.ipynb rename to projects/Physical-AI/Physical-Simulation/Genesis-Simulation/GS04_parallel_simulation.ipynb index 077b86b..acc7af3 100644 --- a/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/PhySim04_parallel_simulation.ipynb +++ b/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/GS04_parallel_simulation.ipynb @@ -2,53 +2,70 @@ "cells": [ { "cell_type": "markdown", - "id": "6ef99da4-7a61-4efd-bb3f-5ebe45d611c7", "metadata": {}, "source": [ - "# Parallel Simulation\n", + "# GS04 — Parallel GPU Simulation\n", "\n", - "Parallel simulation is a powerful technique to accelerate the training process by leveraging GPU capabilities. Instead of simulating only one robot or environment at a time, we can run multiple environments in parallel within the same scene.\n", + "### Lab Description\n", "\n", - "## What You Will Learn\n", + "Genesis can simulate multiple environments inside one scene, allowing the GPU to process batched robot states efficiently. This lab creates nine Franka environments and drives each end effector along a related trajectory with a different angular speed.\n", "\n", - "Hands-on with parallel simulation code and visualize parallel execution." - ] + "The focus is batched simulation and vectorized inverse kinematics. Training and reinforcement learning are possible extensions, but they are not performed in this notebook.\n", + "\n", + "#### Recommended Hardware\n", + "\n", + "An AMD GPU supported by ROCm, such as an AMD Radeon™ GPU or AMD Ryzen™ AI processor with integrated Radeon graphics.\n", + "\n", + "#### Software Environment\n", + "\n", + "OS: Ubuntu 24.04 LTS \n", + "Install [AUP Learning Cloud](https://amdresearch.github.io/aup-learning-cloud/installation/quick-start.html). The Genesis Simulation image provides ROCm, PyTorch, and `genesis-world==1.3.1`.\n", + "\n", + "## Goals\n", + "\n", + "- Build multiple Genesis environments with `n_envs` and `env_spacing`.\n", + "- Represent batched target positions and orientations.\n", + "- Solve inverse kinematics for multiple robots in parallel.\n", + "- Apply vectorized robot state updates.\n", + "- Record and visualize parallel execution." + ], + "id": "6ef99da4-7a61-4efd-bb3f-5ebe45d611c7" }, { "cell_type": "code", - "execution_count": null, - "id": "3caab361-5625-4df3-bbac-024295980c0b", "metadata": {}, - "outputs": [], "source": [ "# Suppress warning messages for clearer output\n", "import os\n", "import warnings\n", "\n", "os.environ[\"TI_LOG_LEVEL\"] = \"error\"\n", - "warnings.filterwarnings(\"ignore\")" - ] + "warnings.filterwarnings(\"ignore\")\n", + "os.makedirs(\"Videos\", exist_ok=True)" + ], + "execution_count": null, + "outputs": [], + "id": "3caab361-5625-4df3-bbac-024295980c0b" }, { "cell_type": "markdown", - "id": "12fe4746-bd21-4856-9e4f-641716e95df1", "metadata": {}, "source": [ "## Init and Create a Scene\n", "\n", "Just like previous labs, we create and init a scene in the beginning." - ] + ], + "id": "12fe4746-bd21-4856-9e4f-641716e95df1" }, { "cell_type": "code", - "execution_count": null, - "id": "bc4e3ed3", "metadata": {}, - "outputs": [], "source": [ "import genesis as gs\n", "import numpy as np\n", "\n", + "assert \"scene\" not in globals(), \"Scene already exists. Restart the kernel before rerunning this lab.\"\n", + "\n", "########################## init ##########################\n", "gs.init(backend=gs.amdgpu, theme=\"light\")\n", "\n", @@ -65,24 +82,24 @@ " ),\n", " show_viewer=False,\n", ")" - ] + ], + "execution_count": null, + "outputs": [], + "id": "bc4e3ed3" }, { "cell_type": "markdown", - "id": "fc0555c9-db1a-4965-8444-a6a08dfbf1cb", "metadata": {}, "source": [ "## Add Entities\n", "\n", "Just like what we did in previous labs, we add a **plane**, an **arm**, and a **camera** to the scene, and then build the scene." - ] + ], + "id": "fc0555c9-db1a-4965-8444-a6a08dfbf1cb" }, { "cell_type": "code", - "execution_count": null, - "id": "67347a5e", "metadata": {}, - "outputs": [], "source": [ "########################## entities ##########################\n", "plane = scene.add_entity(\n", @@ -98,47 +115,48 @@ " lookat=(0, 0, 0.2),\n", " fov=30,\n", ")" - ] + ], + "execution_count": null, + "outputs": [], + "id": "67347a5e" }, { "cell_type": "markdown", - "id": "ac4ee28c-b200-468f-be8a-057efc04dd25", "metadata": {}, "source": [ "## Build the Scene\n", "\n", "In Genesis, enabling scene-level parallelism is straightforward. When building your scene, you simply add the parameter `n_envs` to specify how many environments you want. To align with terminology commonly used in machine learning literature, we will also use the term `batching` to refer to parallelization." - ] + ], + "id": "ac4ee28c-b200-468f-be8a-057efc04dd25" }, { "cell_type": "code", - "execution_count": null, - "id": "a9f5096b-1131-47ec-aec0-8ff95eab9855", "metadata": {}, - "outputs": [], "source": [ "########################## build ##########################\n", "n_envs = 9\n", "scene.build(n_envs=n_envs, env_spacing=(1.0, 1.0))" - ] + ], + "execution_count": null, + "outputs": [], + "id": "a9f5096b-1131-47ec-aec0-8ff95eab9855" }, { "cell_type": "markdown", - "id": "fda33a5a-2894-49db-aa28-dfd5f604e479", "metadata": {}, "source": [ "## Controlling Robots in Batched Environments\n", "\n", - "In earlier tutorials, we used APIs such as franka.control_dofs_position() to control a single robot.\n", - "With parallel simulation, the same API can be extended naturally: simply add a batch dimension to control multiple robots at once." - ] + "In earlier tutorials, we used APIs such as `control_dofs_position()` to control a single robot. Batched control uses the same leading environment dimension.\n", + "\n", + "For this visualization-only IK demo, the next cell uses `set_qpos()` to place all nine robots directly at each solved configuration. This bypasses PD dynamics and should not be treated as a deployment controller; use batched `control_dofs_position()` when physical response matters." + ], + "id": "fda33a5a-2894-49db-aa28-dfd5f604e479" }, { "cell_type": "code", - "execution_count": null, - "id": "d6c14a54", "metadata": {}, - "outputs": [], "source": [ "import logging\n", "\n", @@ -162,7 +180,6 @@ " target_pos[:, 0] = center[:, 0] + np.cos(i / 360 * np.pi * angular_speed) * r\n", " target_pos[:, 1] = center[:, 1] + np.sin(i / 360 * np.pi * angular_speed) * r\n", " target_pos[:, 2] = center[:, 2]\n", - " target_q = np.hstack([target_pos, target_quat])\n", "\n", " q = robot.inverse_kinematics(\n", " link=ee_link,\n", @@ -176,38 +193,40 @@ " cam.render()\n", "\n", "cam.stop_recording()" - ] + ], + "execution_count": null, + "outputs": [], + "id": "d6c14a54" }, { "cell_type": "markdown", - "id": "5b2fb085-d8b0-4999-ab54-2243db074ed5", "metadata": {}, "source": [ "## Show the video\n", "\n", "You will see nine robotic arms moving at different speeds and angles. This is because we set `n_envs` to nine and assigned each of them a random `angular_speed`." - ] + ], + "id": "5b2fb085-d8b0-4999-ab54-2243db074ed5" }, { "cell_type": "code", - "execution_count": null, - "id": "a236c16e", "metadata": {}, - "outputs": [], "source": [ "from IPython.display import Video\n", "\n", "Video(url=\"Videos/video_04.mp4\")" - ] + ], + "execution_count": null, + "outputs": [], + "id": "a236c16e" }, { "cell_type": "markdown", - "id": "dc8da334-4c15-48d3-a2f5-4dbe7b229d9b", "metadata": {}, "source": [ - "# Congratulations! \n", + "## GS01–04 Stage Complete\n", "\n", - "After completing these four labs, you now have a basic understanding of how to create and control a virtual robotic arm in Genesis. You’ve also learned how to plan a pick-and-place path for a block and how to perform parallel simulations within a scene.\n", + "After completing the first four labs, you now understand how to create and control a virtual robotic arm in Genesis, plan a grasping path for a block, and run parallel environments within one scene. GS05 and GS06 continue with multimodal perception and language-guided interaction.\n", "\n", "If you’re interested in running more Genesis on AMD machines, here are some useful references:\n", "\n", @@ -216,14 +235,25 @@ "If you find aup learning cloud useful, please give us a star!\n", "\n", "* **AUP Learning Cloud:** [https://github.com/AMDResearch/aup-learning-cloud](https://github.com/AMDResearch/aup-learning-cloud) \n", - "\n" - ] + "\n", + "" + ], + "id": "dc8da334-4c15-48d3-a2f5-4dbe7b229d9b" }, { "cell_type": "markdown", - "id": "eed45bc1", "metadata": {}, - "source": [] + "source": [ + "## Conclusions\n", + "\n", + "You created multiple Genesis environments, generated batched end-effector targets, solved IK in parallel, and visualized nine robot trajectories. In GS05, the course moves from scripted targets to perception-guided target selection with ROCm.\n", + "\n", + "---\n", + "\n", + "Copyright (C) 2026 Advanced Micro Devices, Inc. All rights reserved. Portions of this file consist of AI-generated content. \n", + "SPDX-License-Identifier: MIT" + ], + "id": "89c29d56" } ], "metadata": { @@ -242,7 +272,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.11" + "version": "3.12.3" } }, "nbformat": 4, diff --git a/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/GS05_perception_with_rocm.ipynb b/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/GS05_perception_with_rocm.ipynb new file mode 100644 index 0000000..74c759b --- /dev/null +++ b/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/GS05_perception_with_rocm.ipynb @@ -0,0 +1,1411 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "placeholder", + "metadata": {}, + "source": [ + "# GS05 — Perception with ROCm in Genesis\n", + "\n", + "### Lab Description\n", + "\n", + "This lab extends the Genesis sequence from scripted motion to perception-guided decisions. Genesis camera buffers are processed with editable ROCm/PyTorch kernels and converted into a structured target record for the language-guided agent in GS06.\n", + "\n", + "The processing pipeline is:\n", + "\n", + "`RGB / depth / segmentation / normal → ROCm kernels → visual target selection → target record`\n", + "\n", + "> **Important:** vision selects the target identity. The exact 3D world position is read from Genesis entity state and labeled as simulator ground truth; it is not presented as monocular 3D reconstruction.\n", + "\n", + "#### Recommended Hardware\n", + "\n", + "An AMD GPU supported by ROCm, such as an AMD Radeon™ GPU or AMD Ryzen™ AI processor with integrated Radeon graphics.\n", + "\n", + "#### Software Environment\n", + "\n", + "OS: Ubuntu 24.04 LTS \n", + "Install [AUP Learning Cloud](https://amdresearch.github.io/aup-learning-cloud/installation/quick-start.html). The Genesis Simulation image provides ROCm, PyTorch, and `genesis-world==1.3.1`.\n", + "\n", + "## Goals\n", + "\n", + "- Capture RGB, depth, segmentation, and normal buffers from Genesis.\n", + "- Verify that PyTorch kernels execute on the ROCm/HIP device.\n", + "- Implement depth, normal, segmentation, color-lock, grayscale, blur, and Sobel processing.\n", + "- Reduce two 8×8 fingertip tactile fields into contact, force, and secure-grasp signals.\n", + "- Build an eight-panel perception dashboard.\n", + "- Produce visual and tactile contracts for GS06." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "03d996ac", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import json\n", + "import logging\n", + "import warnings\n", + "import collections\n", + "\n", + "os.environ.setdefault(\"TI_LOG_LEVEL\", \"error\")\n", + "warnings.filterwarnings(\"ignore\")\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import matplotlib.patches as mpatches\n", + "import numpy as np\n", + "import torch\n", + "import torch.nn.functional as F\n", + "import genesis as gs\n", + "import genesis.utils.geom as gu\n", + "from genesis.utils.misc import tensor_to_array\n", + "\n", + "from helpers.physisim_hud import FFmpegHUDWriter, GPUMonitor, compose_hud_frame\n", + "from helpers.physisim_widget import LiveHUDController\n", + "\n", + "os.makedirs(\"Videos\", exist_ok=True)\n", + "os.makedirs(\"Artifacts\", exist_ok=True)\n", + "\n", + "DEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n", + "\n", + "print(\"=== ROCm runtime receipt ===\")\n", + "print(\"PyTorch :\", torch.__version__)\n", + "print(\"HIP :\", getattr(torch.version, \"hip\", None))\n", + "print(\"Device :\", DEVICE)\n", + "if DEVICE.type == \"cuda\":\n", + " print(\"GPU :\", torch.cuda.get_device_name(0))\n", + "else:\n", + " print(\"WARNING : ROCm/HIP is unavailable; kernels will run on CPU.\")" + ] + }, + { + "cell_type": "markdown", + "id": "2c487866", + "metadata": {}, + "source": [ + "## 1. Build the perception scene\n", + "\n", + "The scene follows the same Genesis 1.3.1 pattern as GS03: a headless scene, Franka Panda, fixed camera, and colored cubes.\n", + "\n", + "> Genesis initialization and scene build should run only once per kernel. Restart the kernel before rerunning this section." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8c30ee75", + "metadata": {}, + "outputs": [], + "source": [ + "assert \"scene\" not in globals(), \"Scene already exists. Restart the kernel before rebuilding it.\"\n", + "\n", + "gs.init(backend=gs.amdgpu, theme=\"light\", seed=0)\n", + "gs.logger._logger.setLevel(logging.WARNING)\n", + "\n", + "CUBE_SIZE = 0.04\n", + "CUBE_Z = CUBE_SIZE / 2.0\n", + "\n", + "scene = gs.Scene(\n", + " viewer_options=gs.options.ViewerOptions(\n", + " camera_pos=(3, -1, 1.5),\n", + " camera_lookat=(0.0, 0.0, 0.5),\n", + " camera_fov=30,\n", + " max_FPS=60,\n", + " ),\n", + " sim_options=gs.options.SimOptions(dt=0.01, substeps=4),\n", + " rigid_options=gs.options.RigidOptions(\n", + " box_box_detection=True,\n", + " constraint_timeconst=0.01,\n", + " ),\n", + " show_viewer=False,\n", + ")\n", + "\n", + "plane = scene.add_entity(gs.morphs.Plane())\n", + "franka = scene.add_entity(\n", + " gs.morphs.MJCF(file=\"xml/franka_emika_panda/panda.xml\"),\n", + ")\n", + "\n", + "cube_specs = {\n", + " \"red\": {\"pos\": (0.55, -0.15, CUBE_Z), \"color\": (1.0, 0.0, 0.0, 1.0)},\n", + " \"green\": {\"pos\": (0.55, 0.00, CUBE_Z), \"color\": (0.0, 1.0, 0.0, 1.0)},\n", + " \"blue\": {\"pos\": (0.55, 0.15, CUBE_Z), \"color\": (0.0, 0.0, 1.0, 1.0)},\n", + "}\n", + "\n", + "cube_entities = {}\n", + "for name, spec in cube_specs.items():\n", + " cube_entities[name] = scene.add_entity(\n", + " gs.morphs.Box(size=(CUBE_SIZE,) * 3, pos=spec[\"pos\"]),\n", + " surface=gs.surfaces.Default(color=spec[\"color\"]),\n", + " )\n", + "\n", + "cam = scene.add_camera(\n", + " res=(640, 480),\n", + " pos=(3, -1, 1.5),\n", + " lookat=(0, 0, 0.5),\n", + " fov=30,\n", + " GUI=True,\n", + ")\n", + "\n", + "# Official Genesis 1.3.1 Franka tactile layout: one 8×8 pad per fingertip.\n", + "CONTACT_THRESH_M = 5e-4\n", + "CONTACT_SECURE_TAXELS = 12\n", + "GRIP_STIFFNESS_N_PER_M = 5000.0\n", + "\n", + "probe_normal = (0.0, -1.0, 0.0)\n", + "probe_local_pos = gu.generate_grid_points_on_plane(\n", + " lo=(-0.006, 0.0, 0.04),\n", + " hi=(0.008, 0.0, 0.05),\n", + " normal=probe_normal,\n", + " nx=8,\n", + " ny=8,\n", + ")\n", + "tracked_cube_links = tuple(int(entity.base_link_idx) for entity in cube_entities.values())\n", + "tactile_options = dict(\n", + " probe_local_pos=probe_local_pos,\n", + " probe_local_normal=probe_normal,\n", + " probe_radius=0.002,\n", + " track_link_idx=tracked_cube_links,\n", + " n_sample_points=1000,\n", + " lambda_d=5000.0,\n", + " lambda_s=4000.0,\n", + " dilate_scale=1.0,\n", + " shear_scale=1.0,\n", + " normal_exponent=1.0,\n", + " compressibility=0.8,\n", + " draw_debug=False,\n", + ")\n", + "left_tactile = scene.add_sensor(\n", + " gs.sensors.ElastomerTaxel(\n", + " entity_idx=franka.idx,\n", + " link_idx_local=franka.get_link(\"left_finger\").idx_local,\n", + " **tactile_options,\n", + " )\n", + ")\n", + "right_tactile = scene.add_sensor(\n", + " gs.sensors.ElastomerTaxel(\n", + " entity_idx=franka.idx,\n", + " link_idx_local=franka.get_link(\"right_finger\").idx_local,\n", + " **tactile_options,\n", + " )\n", + ")\n", + "\n", + "scene.build()\n", + "for _ in range(20):\n", + " scene.step()\n", + "\n", + "# Preserve the settled, upright Franka pose before later teaching cells move it.\n", + "UPRIGHT_QPOS = tensor_to_array(franka.get_qpos()).reshape(-1).astype(np.float64)\n", + "UPRIGHT_QPOS[-2:] = 0.04\n", + "\n", + "print(\"Scene built with Genesis 1.3.1 APIs\")\n", + "print(\"Semantic objects:\", \", \".join(cube_entities))\n", + "print(\"Tactile pads: left 8×8 + right 8×8\")\n", + "print(\"Saved upright reset pose:\", UPRIGHT_QPOS.round(3).tolist())" + ] + }, + { + "cell_type": "markdown", + "id": "f7180657", + "metadata": {}, + "source": [ + "## 2. Capture and inspect camera buffers\n", + "\n", + "A Genesis camera can return several aligned modalities in one render call:\n", + "\n", + "- **RGB** provides appearance and color.\n", + "- **Depth** stores the distance from the camera to visible surfaces.\n", + "- **Segmentation** assigns an integer ID to each rendered entity.\n", + "- **Normals** describe surface orientation at each pixel.\n", + "\n", + "Before sending these arrays to PyTorch, we normalize possible batch and singleton-channel dimensions. Printing shape, dtype, and range is an important debugging step: it prevents a depth map or segmentation map from being interpreted as an ordinary RGB image." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "55ffbec9", + "metadata": {}, + "outputs": [], + "source": [ + "def to_numpy(value):\n", + " \"\"\"Convert Genesis/PyTorch output to a NumPy array.\"\"\"\n", + " if isinstance(value, torch.Tensor):\n", + " return tensor_to_array(value)\n", + " return np.asarray(value)\n", + "\n", + "\n", + "def squeeze_render_buffer(value):\n", + " \"\"\"Remove optional environment and singleton-channel dimensions.\"\"\"\n", + " arr = to_numpy(value)\n", + " if arr.ndim == 4 and arr.shape[0] == 1:\n", + " arr = arr[0]\n", + " if arr.ndim == 3 and arr.shape[-1] == 1:\n", + " arr = arr[..., 0]\n", + " return arr\n", + "\n", + "\n", + "def as_rgb_uint8(value):\n", + " \"\"\"Normalize an RGB render buffer to H×W×3 uint8.\"\"\"\n", + " arr = squeeze_render_buffer(value)[..., :3]\n", + " if arr.dtype != np.uint8:\n", + " arr = arr.astype(np.float32)\n", + " if arr.size and float(np.nanmax(arr)) <= 1.5:\n", + " arr = arr * 255.0\n", + " arr = np.clip(arr, 0, 255).astype(np.uint8)\n", + " return arr\n", + "\n", + "\n", + "rgb_raw, depth_raw, seg_raw, normal_raw = cam.render(\n", + " rgb=True,\n", + " depth=True,\n", + " segmentation=True,\n", + " normal=True,\n", + " colorize_seg=False,\n", + ")\n", + "\n", + "rgb_u8 = as_rgb_uint8(rgb_raw)\n", + "depth_hw = squeeze_render_buffer(depth_raw).astype(np.float32)\n", + "seg_ids = squeeze_render_buffer(seg_raw).astype(np.int32)\n", + "normal_hwc = squeeze_render_buffer(normal_raw).astype(np.float32)\n", + "\n", + "for label, arr in {\n", + " \"RGB\": rgb_u8,\n", + " \"depth\": depth_hw,\n", + " \"segmentation\": seg_ids,\n", + " \"normal\": normal_hwc,\n", + "}.items():\n", + " print(f\"{label:13s}: shape={arr.shape}, dtype={arr.dtype}, min={arr.min():.4g}, max={arr.max():.4g}\")\n", + "\n", + "plt.figure(figsize=(8, 6))\n", + "plt.imshow(rgb_u8)\n", + "plt.title(\"Genesis perception scene\")\n", + "plt.axis(\"off\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "6c3e3d08", + "metadata": {}, + "source": [ + "## 3. Move camera data to ROCm\n", + "\n", + "Each perception function follows the same boundary:\n", + "\n", + "1. receive a NumPy camera buffer;\n", + "2. make its host memory contiguous;\n", + "3. create a PyTorch tensor and move it to `DEVICE`;\n", + "4. perform the expensive operation on the GPU;\n", + "5. copy only the final display result back to the CPU.\n", + "\n", + "The source arrays stay alive until GPU work completes. We use `torch.from_numpy(...).to(DEVICE)` instead of DLPack so ownership and synchronization remain explicit and easy to inspect." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "07d98fdc", + "metadata": {}, + "outputs": [], + "source": [ + "_HOST_KEEPALIVE = collections.deque(maxlen=16)\n", + "\n", + "\n", + "def to_device(array, dtype=None):\n", + " host = np.ascontiguousarray(array if dtype is None else np.asarray(array, dtype=dtype))\n", + " _HOST_KEEPALIVE.append(host)\n", + " return torch.from_numpy(host).to(DEVICE)\n", + "\n", + "\n", + "def depth_band(depth_np):\n", + " depth = to_device(depth_np, np.float32)\n", + " valid = torch.isfinite(depth) & (depth > 0)\n", + " if not bool(valid.any()):\n", + " return torch.zeros_like(depth, dtype=torch.uint8).cpu().numpy()\n", + " values = depth[valid]\n", + " near = torch.quantile(values, 0.02)\n", + " far = torch.quantile(values, 0.98)\n", + " scaled = ((depth - near) / (far - near + 1e-6)).clamp(0, 1)\n", + " scaled = torch.where(valid, scaled, torch.zeros_like(scaled))\n", + " return (scaled * 255).byte().cpu().numpy()\n", + "\n", + "\n", + "def normal_map(normal_np, segmentation_np=None):\n", + " normal = to_device(normal_np, np.float32)\n", + " if float(normal.max()) > 1.5:\n", + " mapped = normal / 255.0 # Genesis rasterizer already encodes [-1, 1] as uint8 RGB.\n", + " elif float(normal.min()) < -0.05:\n", + " mapped = (normal + 1.0) / 2.0\n", + " else:\n", + " mapped = normal\n", + " image = (mapped.clamp(0, 1) * 255).byte().cpu().numpy()\n", + " if segmentation_np is not None:\n", + " image[np.asarray(segmentation_np) == 0] = 0\n", + " return image\n", + "\n", + "\n", + "def segmentation_map(segmentation_np):\n", + " ids = to_device(segmentation_np, np.int32)\n", + " r = ((ids * 37 + 11) % 256).byte()\n", + " g = ((ids * 79 + 43) % 256).byte()\n", + " b = ((ids * 131 + 97) % 256).byte()\n", + " colored = torch.stack([r, g, b], dim=-1)\n", + " colored[ids == 0] = 0\n", + " return colored.cpu().numpy()\n", + "\n", + "\n", + "print(f\"Kernel device: {DEVICE}\")" + ] + }, + { + "cell_type": "markdown", + "id": "7da791f6", + "metadata": {}, + "source": [ + "### 3.1 Depth, normals, and segmentation\n", + "\n", + "These first kernels transform geometry-oriented camera data:\n", + "\n", + "- `depth_band()` maps the useful depth range to an 8-bit image. Percentiles reduce the effect of invalid pixels and distant outliers.\n", + "- `normal_map()` converts surface vectors into displayable RGB values.\n", + "- `segmentation_map()` hashes integer entity IDs into stable colors while keeping background ID `0` black.\n", + "\n", + "They are visualization kernels: they help us inspect what the simulator sees before using the data to make a decision." + ] + }, + { + "cell_type": "markdown", + "id": "a3e69f83", + "metadata": {}, + "source": [ + "### 3.2 Locate a requested color\n", + "\n", + "`color_lock()` turns RGB similarity into a small detection result. It computes a GPU mask, counts matching pixels, and returns a bounding box plus centroid only when enough evidence is present.\n", + "\n", + "Returning a structured no-match result instead of raising an exception is important: GS06 can stop safely when an object is not visible." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f1012719", + "metadata": {}, + "outputs": [], + "source": [ + "def color_lock(rgb_np, target_rgb, tolerance=0.30, min_pixels=8):\n", + " \"\"\"Find pixels close to target_rgb and summarize their image location.\"\"\"\n", + " rgb = to_device(np.asarray(rgb_np)[..., :3], np.float32) / 255.0\n", + " target = torch.tensor(target_rgb, dtype=torch.float32, device=DEVICE)\n", + " mask = torch.linalg.norm(rgb - target, dim=-1) < tolerance\n", + " count = int(mask.sum().item())\n", + "\n", + " if count < min_pixels:\n", + " return {\n", + " \"visible\": False,\n", + " \"pixel_count\": count,\n", + " \"bbox\": None,\n", + " \"centroid_px\": None,\n", + " \"mask\": mask.cpu().numpy(),\n", + " }\n", + "\n", + " ys, xs = torch.where(mask)\n", + " return {\n", + " \"visible\": True,\n", + " \"pixel_count\": count,\n", + " \"bbox\": [int(xs.min()), int(ys.min()), int(xs.max()), int(ys.max())],\n", + " \"centroid_px\": [float(xs.float().mean()), float(ys.float().mean())],\n", + " \"mask\": mask.cpu().numpy(),\n", + " }" + ] + }, + { + "cell_type": "markdown", + "id": "63bded29", + "metadata": {}, + "source": [ + "### 3.3 Build a classical image-processing chain\n", + "\n", + "The remaining kernels demonstrate a common vision pipeline:\n", + "\n", + "`RGB → grayscale → Gaussian blur → Sobel edges`\n", + "\n", + "Grayscale reduces three color channels to one intensity value. Gaussian blur suppresses small variations, and Sobel filters measure horizontal and vertical intensity changes. Running the convolution operations through PyTorch keeps the expensive work on the ROCm device." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5e3c493d", + "metadata": {}, + "outputs": [], + "source": [ + "def rgb_to_gray(rgb_np):\n", + " rgb = to_device(np.asarray(rgb_np)[..., :3], np.float32)\n", + " weights = torch.tensor([0.299, 0.587, 0.114], device=DEVICE)\n", + " return (rgb * weights).sum(-1).clamp(0, 255).byte().cpu().numpy()\n", + "\n", + "\n", + "def gaussian_blur(gray_np, kernel_size=5, sigma=1.4):\n", + " gray = to_device(gray_np, np.float32)[None, None]\n", + " axis = torch.arange(kernel_size, device=DEVICE, dtype=torch.float32)\n", + " axis = axis - (kernel_size - 1) / 2\n", + " kernel = torch.exp(-(axis * axis) / (2 * sigma * sigma))\n", + " kernel = kernel / kernel.sum()\n", + " pad = kernel_size // 2\n", + " out = F.conv2d(gray, kernel.view(1, 1, 1, -1), padding=(0, pad))\n", + " out = F.conv2d(out, kernel.view(1, 1, -1, 1), padding=(pad, 0))\n", + " return out[0, 0].clamp(0, 255).byte().cpu().numpy()\n", + "\n", + "\n", + "def sobel_edges(gray_np):\n", + " gray = to_device(gray_np, np.float32)[None, None]\n", + " gx = torch.tensor(\n", + " [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]],\n", + " dtype=torch.float32,\n", + " device=DEVICE,\n", + " ).view(1, 1, 3, 3)\n", + " gy = gx.transpose(-1, -2)\n", + " edge_x = F.conv2d(gray, gx, padding=1)\n", + " edge_y = F.conv2d(gray, gy, padding=1)\n", + " magnitude = torch.sqrt(edge_x.square() + edge_y.square())\n", + " magnitude = magnitude / (magnitude.max() + 1e-6) * 255\n", + " return magnitude[0, 0].byte().cpu().numpy()" + ] + }, + { + "cell_type": "markdown", + "id": "14a0dcac", + "metadata": {}, + "source": [ + "### 3.4 Reduce two 8×8 tactile pads on ROCm\n", + "\n", + "Each `ElastomerTaxel` returns an 8×8×3 marker-displacement field in meters. The final dimension describes local 3D displacement at one taxel.\n", + "\n", + "`tactile_reduce()` flattens both fingertip grids and computes:\n", + "\n", + "- the number of taxels above a contact threshold;\n", + "- total taxel count;\n", + "- a heuristic grip-force estimate;\n", + "- peak displacement in millimeters;\n", + "- a `secure` gate that becomes true only when enough taxels report contact.\n", + "\n", + "The reduction runs on the same ROCm device as the vision kernels. The force scale is a teaching heuristic, while `secure` is the signal GS06 uses to decide whether lifting is allowed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1a825c19", + "metadata": {}, + "outputs": [], + "source": [ + "def read_tactile_displacement():\n", + " \"\"\"Return left/right 8×8×3 ground-truth marker displacement in meters.\"\"\"\n", + " left = to_numpy(left_tactile.read_ground_truth()).astype(np.float32)\n", + " right = to_numpy(right_tactile.read_ground_truth()).astype(np.float32)\n", + " return left, right\n", + "\n", + "\n", + "def tactile_reduce(\n", + " left_disp,\n", + " right_disp,\n", + " contact_thresh_m=CONTACT_THRESH_M,\n", + " secure_taxels=CONTACT_SECURE_TAXELS,\n", + " grip_stiffness_N_per_m=GRIP_STIFFNESS_N_PER_M,\n", + "):\n", + " left = to_device(left_disp, np.float32).reshape(-1, 3)\n", + " right = to_device(right_disp, np.float32).reshape(-1, 3)\n", + " left_magnitude = torch.linalg.norm(left, dim=-1)\n", + " right_magnitude = torch.linalg.norm(right, dim=-1)\n", + "\n", + " n_contact = int(\n", + " (left_magnitude > contact_thresh_m).sum().item()\n", + " + (right_magnitude > contact_thresh_m).sum().item()\n", + " )\n", + " peak_m = torch.maximum(left_magnitude.max(), right_magnitude.max())\n", + " grip_force_N = (left_magnitude.sum() + right_magnitude.sum()) * grip_stiffness_N_per_m\n", + "\n", + " return {\n", + " \"n_contact\": n_contact,\n", + " \"n_taxels\": int(left_magnitude.numel() + right_magnitude.numel()),\n", + " \"grip_force_N\": float(grip_force_N.item()),\n", + " \"peak_mm\": float(peak_m.item() * 1000.0),\n", + " \"secure\": bool(n_contact >= secure_taxels),\n", + " }\n", + "\n", + "\n", + "scene.step()\n", + "air_tactile = tactile_reduce(*read_tactile_displacement())\n", + "print(\"Open-air tactile receipt:\", air_tactile)\n", + "assert air_tactile[\"n_taxels\"] == 128\n", + "assert air_tactile[\"secure\"] is False" + ] + }, + { + "cell_type": "markdown", + "id": "6480e6da", + "metadata": {}, + "source": [ + "## 4. Run the perception pipeline\n", + "\n", + "Rendered colors differ from ideal RGB values because lighting and materials affect pixel intensity. We therefore use entity-level segmentation once to estimate the rendered mean color of each cube. The runtime detector then relies only on RGB similarity.\n", + "\n", + "The deliberately absent yellow target checks the failure path: it should return `visible=False` without interrupting the notebook." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "65531e65", + "metadata": {}, + "outputs": [], + "source": [ + "# Calibrate the rendered target colors from entity-level segmentation.\n", + "# Genesis reserves segmentation ID 0 for the background; entity IDs are idx + 1.\n", + "target_colors = {}\n", + "for name, entity in cube_entities.items():\n", + " segmentation_id = int(entity.idx + 1)\n", + " entity_mask = seg_ids == segmentation_id\n", + " if entity_mask.any():\n", + " target_colors[name] = (rgb_u8[entity_mask].mean(axis=0) / 255.0).tolist()\n", + " source = \"rendered pixels selected by segmentation\"\n", + " else:\n", + " target_colors[name] = list(cube_specs[name][\"color\"][:3])\n", + " source = \"nominal surface color fallback\"\n", + " print(f\"{name:5s}: seg_id={segmentation_id}, pixels={int(entity_mask.sum())}, source={source}\")\n", + "\n", + "processed = {\n", + " \"depth\": depth_band(depth_hw),\n", + " \"normal\": normal_map(normal_hwc, seg_ids),\n", + " \"segmentation\": segmentation_map(seg_ids),\n", + "}\n", + "processed[\"gray\"] = rgb_to_gray(rgb_u8)\n", + "processed[\"blur\"] = gaussian_blur(processed[\"gray\"])\n", + "processed[\"sobel\"] = sobel_edges(processed[\"blur\"])\n", + "\n", + "lock_results = {\n", + " name: color_lock(rgb_u8, target_rgb)\n", + " for name, target_rgb in target_colors.items()\n", + "}\n", + "lock_results[\"yellow\"] = color_lock(rgb_u8, [1.0, 1.0, 0.0])\n", + "\n", + "for name, result in lock_results.items():\n", + " printable = {key: value for key, value in result.items() if key != \"mask\"}\n", + " print(f\"{name:6s} -> {printable}\")" + ] + }, + { + "cell_type": "markdown", + "id": "62f9673d", + "metadata": {}, + "source": [ + "## 5. Compare the perception outputs\n", + "\n", + "The eight-panel dashboard places raw sensing and processed results side by side. This makes it easier to answer three questions:\n", + "\n", + "1. Is the object visible in the original RGB frame?\n", + "2. Do geometry buffers and edge filters contain the expected structure?\n", + "3. Does the selected color mask cover only the requested cube?\n", + "\n", + "The bounding box and centroid are overlaid on the original RGB frame so the detection can be checked visually." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a6a5e71a", + "metadata": {}, + "outputs": [], + "source": [ + "REQUESTED_TARGET = \"green\"\n", + "selected_lock = lock_results[REQUESTED_TARGET]\n", + "lock_panel = np.zeros_like(rgb_u8)\n", + "lock_panel[selected_lock[\"mask\"]] = rgb_u8[selected_lock[\"mask\"]]\n", + "\n", + "fig, axes = plt.subplots(2, 4, figsize=(16, 8))\n", + "panels = [\n", + " (rgb_u8, \"RGB camera\", None),\n", + " (processed[\"depth\"], \"Depth band\", \"inferno\"),\n", + " (processed[\"normal\"], \"Normal map\", None),\n", + " (processed[\"segmentation\"], \"Segmentation\", None),\n", + " (lock_panel, f\"Color lock: {REQUESTED_TARGET}\", None),\n", + " (processed[\"gray\"], \"Grayscale\", \"gray\"),\n", + " (processed[\"blur\"], \"Gaussian blur\", \"gray\"),\n", + " (processed[\"sobel\"], \"Sobel edges\", \"magma\"),\n", + "]\n", + "\n", + "for axis, (image, title, cmap) in zip(axes.ravel(), panels):\n", + " axis.imshow(image, cmap=cmap)\n", + " axis.set_title(title)\n", + " axis.axis(\"off\")\n", + "\n", + "if selected_lock[\"visible\"]:\n", + " x0, y0, x1, y1 = selected_lock[\"bbox\"]\n", + " cx, cy = selected_lock[\"centroid_px\"]\n", + " axes[0, 0].add_patch(\n", + " mpatches.Rectangle(\n", + " (x0, y0), x1 - x0 + 1, y1 - y0 + 1,\n", + " fill=False, edgecolor=\"red\", linewidth=2,\n", + " )\n", + " )\n", + " axes[0, 0].plot(cx, cy, \"+\", color=\"yellow\", markersize=14, markeredgewidth=2)\n", + "\n", + "artifact_path = \"Artifacts/gs05_perception.png\"\n", + "plt.tight_layout()\n", + "plt.savefig(artifact_path, dpi=150)\n", + "plt.show()\n", + "print(\"Saved:\", artifact_path)" + ] + }, + { + "cell_type": "markdown", + "id": "d87fe0c2", + "metadata": {}, + "source": [ + "## 6. Create a target record for the action layer\n", + "\n", + "A detector result contains image-space evidence, but the robot action layer needs a 3D target. For this teaching bridge:\n", + "\n", + "- visibility, pixel count, bounding box, and centroid come from the RGB detector;\n", + "- world position comes from `entity.get_pos(relative=False)`;\n", + "- `position_source` explicitly labels that position as simulator state.\n", + "\n", + "This provenance field prevents downstream code from presenting perfect simulator coordinates as if they had been reconstructed from a monocular camera." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "823db87e", + "metadata": {}, + "outputs": [], + "source": [ + "def entity_world_position(entity):\n", + " position = to_numpy(entity.get_pos(relative=False)).reshape(-1)[:3]\n", + " return position.astype(float).tolist()\n", + "\n", + "\n", + "def build_target_record(name, lock):\n", + " visible = bool(lock.get(\"visible\", False)) and name in cube_entities\n", + " return {\n", + " \"name\": name,\n", + " \"visible\": visible,\n", + " \"pixel_count\": int(lock.get(\"pixel_count\", 0)),\n", + " \"bbox\": lock.get(\"bbox\"),\n", + " \"centroid_px\": lock.get(\"centroid_px\"),\n", + " \"world_position\": entity_world_position(cube_entities[name]) if visible else None,\n", + " \"position_source\": \"genesis_entity_state\" if visible else None,\n", + " }\n", + "\n", + "\n", + "target_record = build_target_record(REQUESTED_TARGET, selected_lock)\n", + "missing_record = build_target_record(\"yellow\", lock_results[\"yellow\"])\n", + "\n", + "print(\"Selected target record:\")\n", + "print(json.dumps(target_record, indent=2))\n", + "print(\"\\nMissing target record:\")\n", + "print(json.dumps(missing_record, indent=2))\n", + "\n", + "assert target_record[\"visible\"], \"the selected cube must be visible before handoff\"\n", + "assert target_record[\"world_position\"] is not None\n", + "assert target_record[\"position_source\"] == \"genesis_entity_state\"\n", + "assert missing_record[\"visible\"] is False\n", + "assert missing_record[\"world_position\"] is None" + ] + }, + { + "cell_type": "markdown", + "id": "0aa0df60", + "metadata": {}, + "source": [ + "## 7. Demonstrate a tactile-secure grasp\n", + "\n", + "The open-air receipt above should contain no secure contact. We now move the gripper to the centered green cube and close it while reading both pads after every simulation step. The centered target uses the same stable workspace demonstrated in GS03.\n", + "\n", + "The loop stops when `secure=True` or when the timeout is reached. The two heatmaps show displacement magnitude across the left and right 8×8 pads. This is the tactile equivalent of visually inspecting a segmentation mask." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "72b3b537", + "metadata": {}, + "outputs": [], + "source": [ + "motors_dof = np.arange(7)\n", + "fingers_dof = np.arange(7, 9)\n", + "end_effector = franka.get_link(\"hand\")\n", + "\n", + "franka.set_dofs_kp(\n", + " np.array([4500, 4500, 3500, 3500, 2000, 2000, 2000, 100, 100])\n", + ")\n", + "franka.set_dofs_kv(\n", + " np.array([450, 450, 350, 350, 200, 200, 200, 10, 10])\n", + ")\n", + "franka.set_dofs_force_range(\n", + " np.array([-87, -87, -87, -87, -12, -12, -12, -100, -100]),\n", + " np.array([87, 87, 87, 87, 12, 12, 12, 100, 100]),\n", + ")\n", + "\n", + "# Match the official Genesis tactile_franka.py workspace exactly.\n", + "cube_entities[\"green\"].set_pos((0.5, 0.1, CUBE_Z))\n", + "for _ in range(10):\n", + " scene.step()\n", + "\n", + "grasp_center = np.asarray(entity_world_position(cube_entities[\"green\"]))\n", + "pre_grasp = np.array([grasp_center[0], grasp_center[1], 0.18])\n", + "grasp = np.array([grasp_center[0], grasp_center[1], 0.125])\n", + "lift = np.array([grasp_center[0], grasp_center[1], 0.30])\n", + "\n", + "pre_qpos = to_numpy(\n", + " franka.inverse_kinematics(\n", + " link=end_effector,\n", + " pos=pre_grasp,\n", + " quat=np.array([0.0, 1.0, 0.0, 0.0]),\n", + " dofs_idx_local=motors_dof,\n", + " )\n", + ").reshape(-1)\n", + "\n", + "grasp_qpos = to_numpy(\n", + " franka.inverse_kinematics(\n", + " link=end_effector,\n", + " pos=grasp,\n", + " quat=np.array([0.0, 1.0, 0.0, 0.0]),\n", + " init_qpos=pre_qpos,\n", + " dofs_idx_local=motors_dof,\n", + " )\n", + ").reshape(-1)\n", + "lift_qpos = to_numpy(\n", + " franka.inverse_kinematics(\n", + " link=end_effector,\n", + " pos=lift,\n", + " quat=np.array([0.0, 1.0, 0.0, 0.0]),\n", + " init_qpos=grasp_qpos,\n", + " dofs_idx_local=motors_dof,\n", + " )\n", + ").reshape(-1)\n", + "retreat_qpos = to_numpy(\n", + " franka.inverse_kinematics(\n", + " link=end_effector,\n", + " pos=pre_grasp,\n", + " quat=np.array([0.0, 1.0, 0.0, 0.0]),\n", + " init_qpos=grasp_qpos,\n", + " dofs_idx_local=motors_dof,\n", + " )\n", + ").reshape(-1)\n", + "\n", + "\n", + "def require_nearby_joint_goal(current, goal, label, max_delta=0.5):\n", + " delta = np.abs(np.asarray(goal)[motors_dof] - np.asarray(current)[motors_dof])\n", + " if float(delta.max()) > max_delta:\n", + " raise RuntimeError(\n", + " f\"{label} crosses IK branches: max joint delta={float(delta.max()):.3f} rad\"\n", + " )\n", + "\n", + "\n", + "require_nearby_joint_goal(pre_qpos, grasp_qpos, \"pre→grasp\")\n", + "require_nearby_joint_goal(grasp_qpos, lift_qpos, \"grasp→lift\")\n", + "require_nearby_joint_goal(grasp_qpos, retreat_qpos, \"grasp→retreat\")\n", + "\n", + "preview_path = \"Videos/video_05.mp4\"\n", + "hud_path = \"Videos/video_05_hud.mp4\"\n", + "contact_tactile = None\n", + "best_tactile = {\"n_contact\": -1}\n", + "contact_snapshot = None\n", + "\n", + "# Establish tactile-secure contact before starting the camera and HUD recordings.\n", + "franka.set_qpos(pre_qpos[motors_dof], motors_dof)\n", + "franka.control_dofs_position(np.array([0.04, 0.04]), fingers_dof)\n", + "for _ in range(30):\n", + " scene.step()\n", + "\n", + "franka.control_dofs_position(grasp_qpos[motors_dof], motors_dof)\n", + "for _ in range(100):\n", + " scene.step()\n", + "\n", + "for step in range(150):\n", + " franka.control_dofs_position(grasp_qpos[motors_dof], motors_dof)\n", + " franka.control_dofs_position(np.array([-0.03, -0.03]), fingers_dof)\n", + " scene.step()\n", + " displacement = read_tactile_displacement()\n", + " contact_tactile = tactile_reduce(*displacement)\n", + " if contact_tactile[\"n_contact\"] > best_tactile[\"n_contact\"]:\n", + " best_tactile = contact_tactile.copy()\n", + " contact_snapshot = displacement\n", + " if contact_tactile[\"secure\"]:\n", + " break\n", + "\n", + "assert contact_tactile[\"secure\"], (\n", + " \"expected the fingertip taxels to confirm a secure grasp; \"\n", + " f\"best receipt was {best_tactile}\"\n", + ")\n", + "\n", + "gpu_monitor = GPUMonitor().start()\n", + "hud_writer = FFmpegHUDWriter(hud_path, fps=25).open()\n", + "hud_frame_index = 0\n", + "cam.start_recording(save_to_filename=preview_path, fps=25)\n", + "\n", + "\n", + "def record_frame(status):\n", + " global hud_frame_index\n", + " hud_frame_index += 1\n", + " if hud_frame_index % 4 != 0:\n", + " cam.render(rgb=True, depth=False, segmentation=False, normal=False)\n", + " return\n", + "\n", + " rgb_frame, depth_frame, seg_frame, normal_frame = cam.render(\n", + " rgb=True,\n", + " depth=True,\n", + " segmentation=True,\n", + " normal=True,\n", + " colorize_seg=False,\n", + " )\n", + " rgb_now = as_rgb_uint8(rgb_frame)\n", + " depth_now = squeeze_render_buffer(depth_frame).astype(np.float32)\n", + " seg_now = squeeze_render_buffer(seg_frame).astype(np.int32)\n", + " normal_now = squeeze_render_buffer(normal_frame).astype(np.float32)\n", + " thumbnails_now = {\n", + " \"depth\": depth_band(depth_now),\n", + " \"normal\": normal_map(normal_now, seg_now),\n", + " \"segmentation\": segmentation_map(seg_now),\n", + " }\n", + " thumbnails_now[\"gray\"] = rgb_to_gray(rgb_now)\n", + " thumbnails_now[\"blur\"] = gaussian_blur(thumbnails_now[\"gray\"])\n", + " thumbnails_now[\"sobel\"] = sobel_edges(thumbnails_now[\"blur\"])\n", + "\n", + " left_now, right_now = read_tactile_displacement()\n", + " tactile_now = tactile_reduce(left_now, right_now)\n", + " hud_writer.write(\n", + " compose_hud_frame(\n", + " rgb_now,\n", + " title=\"GS05 · Multimodal Perception\",\n", + " status=status,\n", + " user_input=\"Inspect the centered cube and verify tactile contact\",\n", + " plan={\"lesson\": \"vision + tactile\", \"target\": \"green\"},\n", + " stages=[{\"stage\": \"perception\", \"success\": True}],\n", + " thumbnails=thumbnails_now,\n", + " tactile=tactile_now,\n", + " left_tactile=left_now,\n", + " right_tactile=right_now,\n", + " gpu=gpu_monitor.snapshot(),\n", + " )\n", + " )\n", + "\n", + "\n", + "try:\n", + " # Hold the secure grasp for one second, then lift, lower, release, and retreat.\n", + " for _ in range(100):\n", + " scene.step()\n", + " record_frame(\"Tactile secure · holding before lift\")\n", + "\n", + " # Lift, hold for inspection, lower, release, and retreat.\n", + " franka.control_dofs_position(lift_qpos[motors_dof], motors_dof)\n", + " for _ in range(150):\n", + " scene.step()\n", + " record_frame(\"Lifting only after K4 secure contact\")\n", + " for _ in range(100):\n", + " scene.step()\n", + " record_frame(\"Holding the cube · monitor both tactile pads\")\n", + "\n", + " franka.control_dofs_position(grasp_qpos[motors_dof], motors_dof)\n", + " for _ in range(150):\n", + " scene.step()\n", + " record_frame(\"Lowering the cube while maintaining contact\")\n", + "\n", + " franka.control_dofs_position(np.array([0.04, 0.04]), fingers_dof)\n", + " for _ in range(100):\n", + " scene.step()\n", + " record_frame(\"Opening gripper · contact should return to zero\")\n", + "\n", + " franka.control_dofs_position(retreat_qpos[motors_dof], motors_dof)\n", + " for _ in range(120):\n", + " scene.step()\n", + " record_frame(\"Retreating on the same IK branch\")\n", + "finally:\n", + " cam.stop_recording()\n", + " hud_writer.close()\n", + " gpu_monitor.stop()\n", + "\n", + "print(f\"Tactile gate after {step + 1} close steps:\", contact_tactile)\n", + "print(\"Saved complete grasp sequence:\", preview_path)\n", + "print(f\"Saved {hud_writer.frames_written} HUD frames:\", hud_path)\n", + "\n", + "left_disp, right_disp = contact_snapshot\n", + "fig, axes = plt.subplots(1, 2, figsize=(8, 3))\n", + "for axis, displacement, title in zip(\n", + " axes,\n", + " (left_disp, right_disp),\n", + " (\"Left fingertip |displacement|\", \"Right fingertip |displacement|\"),\n", + "):\n", + " heatmap = np.linalg.norm(displacement, axis=-1) * 1000.0\n", + " image = axis.imshow(heatmap, cmap=\"magma\", vmin=0)\n", + " axis.set_title(title)\n", + " axis.set_xlabel(\"taxel x\")\n", + " axis.set_ylabel(\"taxel y\")\n", + " fig.colorbar(image, ax=axis, label=\"mm\")\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "262c94dc", + "metadata": {}, + "source": [ + "## 8. Review the complete grasp sequence\n", + "\n", + "The recording now covers 7.2 simulated seconds at 25 FPS:\n", + "\n", + "`tactile-secure hold → lift → hold → lower → release → retreat`\n", + "\n", + "This complete sequence makes the relationship between tactile confirmation and robot motion visible. The gripper does not lift until the secure-contact assertion passes.\n", + "\n", + "Two videos are produced: a raw Genesis camera view and a 1440×816 HUD that adds GPU telemetry, six ROCm vision outputs, live left/right tactile heatmaps, and the current K4 secure state." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "159c903d", + "metadata": {}, + "outputs": [], + "source": [ + "assert os.path.exists(preview_path), \"Run the tactile-grasp demonstration before displaying the video.\"\n", + "assert os.path.exists(hud_path), \"The HUD recording was not created.\"\n", + "print(\"Saved complete tactile-grasp video:\", preview_path)\n", + "print(\"Saved multimodal HUD video:\", hud_path)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "439c8f35", + "metadata": {}, + "outputs": [], + "source": [ + "from IPython.display import Video, display\n", + "\n", + "print(\"Raw Genesis camera recording\")\n", + "display(Video(\"Videos/video_05.mp4\", embed=True, width=720))\n", + "\n", + "print(\"Multimodal HUD recording\")\n", + "display(Video(\"Videos/video_05_hud.mp4\", embed=True, width=960))" + ] + }, + { + "cell_type": "markdown", + "id": "8734fb7a", + "metadata": {}, + "source": [ + "## 9. Live simulation interface\n", + "\n", + "The widget below controls the existing Genesis scene directly; it is not a replay of the MP4. Each button advances the simulation, reads the current camera and both 8×8 tactile pads, and refreshes the same multimodal HUD used by `video_05_hud.mp4`.\n", + "\n", + "Recommended sequence:\n", + "\n", + "1. Select a target and press **Reset**.\n", + "2. Press **Approach**.\n", + "3. Press **Close to Secure** and watch the K4 banner plus tactile heatmaps.\n", + "4. Press **Lift** only after secure contact.\n", + "5. Press **Lower**, then **Release**.\n", + "6. Press **Export HUD MP4** to save the captured interaction to `Videos/video_05_live_hud.mp4`.\n", + "7. Press **Shutdown HUD** when finished to stop the GPU telemetry thread.\n", + "\n", + "The contact threshold and required secure-taxel count are live controls. If a grasp does not become secure, adjust them deliberately and compare the heatmaps rather than bypassing the gate." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "214f1b5a", + "metadata": {}, + "outputs": [], + "source": [ + "from IPython.display import display\n", + "\n", + "live_monitor = GPUMonitor() # starts on the first button action\n", + "live_state = {\n", + " \"target\": None,\n", + " \"layout\": None,\n", + " \"layout_seed\": None,\n", + " \"layout_positions\": None,\n", + " \"pre_qpos\": None,\n", + " \"grasp_qpos\": None,\n", + " \"lift_qpos\": None,\n", + " \"secure\": False,\n", + " \"tactile\": air_tactile,\n", + "}\n", + "\n", + "RANDOM_LAYOUT_X = (0.46, 0.62)\n", + "RANDOM_LAYOUT_Y = (-0.20, 0.20)\n", + "RANDOM_LAYOUT_MIN_DISTANCE = 0.09\n", + "\n", + "\n", + "def live_cube_layout_positions(controller):\n", + " \"\"\"Return deterministic default or seeded-random cube positions.\"\"\"\n", + " if controller.scene_layout.value == \"default\":\n", + " return {\n", + " name: np.asarray(spec[\"pos\"], dtype=np.float64).copy()\n", + " for name, spec in cube_specs.items()\n", + " }\n", + "\n", + " rng = np.random.default_rng(int(controller.layout_seed.value))\n", + " positions = {}\n", + " for name in cube_entities:\n", + " for _ in range(200):\n", + " candidate = np.array(\n", + " [\n", + " rng.uniform(*RANDOM_LAYOUT_X),\n", + " rng.uniform(*RANDOM_LAYOUT_Y),\n", + " CUBE_Z,\n", + " ],\n", + " dtype=np.float64,\n", + " )\n", + " if all(\n", + " np.linalg.norm(candidate[:2] - other[:2]) >= RANDOM_LAYOUT_MIN_DISTANCE\n", + " for other in positions.values()\n", + " ):\n", + " positions[name] = candidate\n", + " break\n", + " else:\n", + " raise RuntimeError(\"Could not sample a collision-free seeded cube layout\")\n", + " return positions\n", + "\n", + "\n", + "def live_prepare_target(controller):\n", + " \"\"\"Apply the selected layout and compute Franka tactile-demo poses.\"\"\"\n", + " selected = controller.target.value\n", + " layout_positions = live_cube_layout_positions(controller)\n", + " for name, entity in cube_entities.items():\n", + " entity.set_pos(layout_positions[name])\n", + " for _ in range(10):\n", + " scene.step()\n", + "\n", + " center = np.asarray(entity_world_position(cube_entities[selected]))\n", + " positions = {\n", + " \"pre\": np.array([center[0], center[1], 0.18]),\n", + " \"grasp\": np.array([center[0], center[1], 0.125]),\n", + " \"lift\": np.array([center[0], center[1], 0.30]),\n", + " }\n", + " pre_qpos = to_numpy(\n", + " franka.inverse_kinematics(\n", + " link=end_effector,\n", + " pos=positions[\"pre\"],\n", + " quat=np.array([0.0, 1.0, 0.0, 0.0]),\n", + " init_qpos=UPRIGHT_QPOS,\n", + " dofs_idx_local=motors_dof,\n", + " )\n", + " ).reshape(-1)\n", + " grasp_qpos = to_numpy(\n", + " franka.inverse_kinematics(\n", + " link=end_effector,\n", + " pos=positions[\"grasp\"],\n", + " quat=np.array([0.0, 1.0, 0.0, 0.0]),\n", + " init_qpos=pre_qpos,\n", + " dofs_idx_local=motors_dof,\n", + " )\n", + " ).reshape(-1)\n", + " lift_qpos = to_numpy(\n", + " franka.inverse_kinematics(\n", + " link=end_effector,\n", + " pos=positions[\"lift\"],\n", + " quat=np.array([0.0, 1.0, 0.0, 0.0]),\n", + " init_qpos=grasp_qpos,\n", + " dofs_idx_local=motors_dof,\n", + " )\n", + " ).reshape(-1)\n", + " retreat_qpos = to_numpy(\n", + " franka.inverse_kinematics(\n", + " link=end_effector,\n", + " pos=positions[\"pre\"],\n", + " quat=np.array([0.0, 1.0, 0.0, 0.0]),\n", + " init_qpos=grasp_qpos,\n", + " dofs_idx_local=motors_dof,\n", + " )\n", + " ).reshape(-1)\n", + " require_nearby_joint_goal(pre_qpos, grasp_qpos, \"live pre→grasp\")\n", + " require_nearby_joint_goal(grasp_qpos, lift_qpos, \"live grasp→lift\")\n", + " require_nearby_joint_goal(grasp_qpos, retreat_qpos, \"live grasp→retreat\")\n", + "\n", + " live_state.update(\n", + " {\n", + " \"target\": selected,\n", + " \"layout\": controller.scene_layout.value,\n", + " \"layout_seed\": int(controller.layout_seed.value),\n", + " \"layout_positions\": {\n", + " name: position.copy() for name, position in layout_positions.items()\n", + " },\n", + " \"pre_qpos\": pre_qpos,\n", + " \"grasp_qpos\": grasp_qpos,\n", + " \"lift_qpos\": lift_qpos,\n", + " \"retreat_qpos\": retreat_qpos,\n", + " \"secure\": False,\n", + " }\n", + " )\n", + "\n", + "\n", + "def live_update(controller, status, *, capture=True):\n", + " live_monitor.start()\n", + " rgb_frame, depth_frame, seg_frame, normal_frame = cam.render(\n", + " rgb=True,\n", + " depth=True,\n", + " segmentation=True,\n", + " normal=True,\n", + " colorize_seg=False,\n", + " )\n", + " rgb_now = as_rgb_uint8(rgb_frame)\n", + " depth_now = squeeze_render_buffer(depth_frame).astype(np.float32)\n", + " seg_now = squeeze_render_buffer(seg_frame).astype(np.int32)\n", + " normal_now = squeeze_render_buffer(normal_frame).astype(np.float32)\n", + " thumbnails_now = {\n", + " \"depth\": depth_band(depth_now),\n", + " \"normal\": normal_map(normal_now, seg_now),\n", + " \"segmentation\": segmentation_map(seg_now),\n", + " }\n", + " thumbnails_now[\"gray\"] = rgb_to_gray(rgb_now)\n", + " thumbnails_now[\"blur\"] = gaussian_blur(thumbnails_now[\"gray\"])\n", + " thumbnails_now[\"sobel\"] = sobel_edges(thumbnails_now[\"blur\"])\n", + "\n", + " left_now, right_now = read_tactile_displacement()\n", + " tactile_now = tactile_reduce(\n", + " left_now,\n", + " right_now,\n", + " contact_thresh_m=controller.contact_threshold.value,\n", + " secure_taxels=controller.secure_taxels.value,\n", + " )\n", + " live_state[\"tactile\"] = tactile_now\n", + " frame = compose_hud_frame(\n", + " rgb_now,\n", + " title=\"GS05 · Live Multimodal Perception\",\n", + " status=status,\n", + " user_input=f\"Interactive target: {controller.target.value}\",\n", + " plan={\"action\": \"tactile grasp lab\", \"target\": controller.target.value},\n", + " stages=[{\"stage\": \"live simulation\", \"success\": tactile_now[\"secure\"]}],\n", + " thumbnails=thumbnails_now,\n", + " tactile=tactile_now,\n", + " left_tactile=left_now,\n", + " right_tactile=right_now,\n", + " gpu=live_monitor.snapshot(),\n", + " )\n", + " controller.update(frame, status, capture=capture)\n", + " return tactile_now\n", + "\n", + "\n", + "def live_steps(controller, count, status, *, every=4):\n", + " for index in range(count):\n", + " scene.step()\n", + " if index % every == 0:\n", + " live_update(controller, status)\n", + " return live_update(controller, status)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5a39369e", + "metadata": {}, + "outputs": [], + "source": [ + "def live_follow_planned_path(controller, qpos_goal, status, *, waypoints):\n", + " \"\"\"Plan and execute a smooth, joint-limit-checked path for the live HUD.\"\"\"\n", + " goal = np.asarray(qpos_goal, dtype=np.float64).copy()\n", + " goal[-2:] = 0.04\n", + " path, path_valid = franka.plan_path(\n", + " qpos_goal=goal,\n", + " num_waypoints=waypoints,\n", + " return_valid_mask=True,\n", + " )\n", + " if not bool(to_numpy(path_valid).reshape(-1)[0]):\n", + " raise RuntimeError(f\"Motion planning failed while {status.lower()}\")\n", + " for index, waypoint in enumerate(path):\n", + " franka.control_dofs_position(waypoint)\n", + " scene.step()\n", + " if index % 4 == 0:\n", + " live_update(controller, status)\n", + " return live_update(controller, status)\n", + "\n", + "\n", + "def live_reset(controller):\n", + " franka.set_qpos(UPRIGHT_QPOS)\n", + " franka.control_dofs_position(UPRIGHT_QPOS[motors_dof], motors_dof)\n", + " franka.control_dofs_position(np.array([0.04, 0.04]), fingers_dof)\n", + " live_prepare_target(controller)\n", + " layout_status = (\n", + " \"Default layout\"\n", + " if controller.scene_layout.value == \"default\"\n", + " else f\"Random layout · seed {controller.layout_seed.value}\"\n", + " )\n", + " live_steps(controller, 30, f\"Reset · upright home · {layout_status}\")\n", + "\n", + "\n", + "def live_approach(controller):\n", + " layout_changed = (\n", + " live_state[\"layout\"] != controller.scene_layout.value\n", + " or (\n", + " controller.scene_layout.value == \"random\"\n", + " and live_state[\"layout_seed\"] != int(controller.layout_seed.value)\n", + " )\n", + " )\n", + " if live_state[\"target\"] != controller.target.value or layout_changed:\n", + " live_prepare_target(controller)\n", + " live_follow_planned_path(\n", + " controller,\n", + " live_state[\"pre_qpos\"],\n", + " f\"Moving to pre-grasp for {controller.target.value}\",\n", + " waypoints=160,\n", + " )\n", + " live_follow_planned_path(\n", + " controller,\n", + " live_state[\"grasp_qpos\"],\n", + " f\"Approaching {controller.target.value}\",\n", + " waypoints=100,\n", + " )\n", + " live_state[\"secure\"] = False\n", + "\n", + "\n", + "def live_close(controller):\n", + " if live_state[\"grasp_qpos\"] is None:\n", + " live_prepare_target(controller)\n", + " receipt = None\n", + " for step in range(150):\n", + " franka.control_dofs_position(live_state[\"grasp_qpos\"][motors_dof], motors_dof)\n", + " franka.control_dofs_position(np.array([-0.03, -0.03]), fingers_dof)\n", + " scene.step()\n", + " if step % 4 == 0:\n", + " receipt = live_update(\n", + " controller,\n", + " f\"Closing · contact {live_state['tactile']['n_contact']}/{128}\",\n", + " )\n", + " if receipt is not None and receipt[\"secure\"]:\n", + " break\n", + " receipt = live_update(controller, \"K4 secure\" if receipt and receipt[\"secure\"] else \"K4 timeout\")\n", + " live_state[\"secure\"] = bool(receipt[\"secure\"])\n", + " if not live_state[\"secure\"]:\n", + " raise RuntimeError(f\"Tactile gate did not become secure: {receipt}\")\n", + "\n", + "\n", + "def live_lift(controller):\n", + " if not live_state[\"secure\"]:\n", + " raise RuntimeError(\"Lift blocked: press Close to Secure first\")\n", + " franka.control_dofs_position(live_state[\"lift_qpos\"][motors_dof], motors_dof)\n", + " live_steps(controller, 150, \"Lifting after tactile secure\")\n", + "\n", + "\n", + "def live_lower(controller):\n", + " franka.control_dofs_position(live_state[\"grasp_qpos\"][motors_dof], motors_dof)\n", + " live_steps(controller, 150, \"Lowering while monitoring tactile contact\")\n", + "\n", + "\n", + "def live_release(controller):\n", + " franka.control_dofs_position(np.array([0.04, 0.04]), fingers_dof)\n", + " live_steps(controller, 100, \"Released · tactile contact returns to zero\")\n", + " live_state[\"secure\"] = False\n", + " franka.control_dofs_position(live_state[\"retreat_qpos\"][motors_dof], motors_dof)\n", + " live_steps(controller, 120, \"Retreating on the same IK branch\")\n", + "\n", + "\n", + "def live_shutdown(controller):\n", + " live_monitor.stop()\n", + " controller.set_status(\"Live HUD monitor stopped. Any action button will restart it.\")\n", + "\n", + "\n", + "live_hud = LiveHUDController(\n", + " targets=tuple(cube_entities),\n", + " contact_threshold=CONTACT_THRESH_M,\n", + " secure_taxels=CONTACT_SECURE_TAXELS,\n", + " export_path=\"Videos/video_05_live_hud.mp4\",\n", + " export_fps=25,\n", + ")\n", + "live_hud.bind(\"reset\", live_reset)\n", + "live_hud.bind(\"approach\", live_approach)\n", + "live_hud.bind(\"close\", live_close)\n", + "live_hud.bind(\"lift\", live_lift)\n", + "live_hud.bind(\"lower\", live_lower)\n", + "live_hud.bind(\"release\", live_release)\n", + "live_hud.bind(\"shutdown\", live_shutdown)\n", + "\n", + "live_update(\n", + " live_hud,\n", + " \"Live preview · press Reset to initialize the selected target.\",\n", + " capture=False,\n", + ")\n", + "display(live_hud.widget)" + ] + }, + { + "cell_type": "markdown", + "id": "2176fe74", + "metadata": {}, + "source": [ + "## 10. Experiment further\n", + "\n", + "1. **Detection sensitivity:** change the color-lock tolerance and record false positives and false negatives.\n", + "2. **Spatial response:** move one cube, render again, and compare its bounding box and centroid.\n", + "3. **Noise reduction:** add image noise and compare Sobel output before and after Gaussian blur.\n", + "4. **Tactile sensitivity:** change `CONTACT_THRESH_M` or `CONTACT_SECURE_TAXELS` and compare open-air and grasp receipts.\n", + "5. **3D extension:** use camera intrinsics, extrinsics, and depth to estimate a world position; compare it with `entity.get_pos(relative=False)`.\n", + "\n", + "### Handoff to GS06\n", + "\n", + "GS06 reuses the visual target record and the tactile receipt. It adds constrained language planning and allows lifting only after the same tactile reduction reports `secure=True`." + ] + }, + { + "cell_type": "markdown", + "id": "4fb4da76", + "metadata": {}, + "source": [ + "## Conclusions\n", + "\n", + "You combined four Genesis camera modalities with two 8×8 fingertip tactile pads, processed both through editable ROCm/PyTorch reductions, and produced explicit visual and tactile contracts. The live widget lets you control each grasp phase, tune contact thresholds, inspect synchronized heatmaps, and export the interaction as `video_05_live_hud.mp4`. GS06 consumes the same contracts to build a language-guided agent that lifts only after contact is secure." + ] + }, + { + "cell_type": "markdown", + "id": "0c897e4b", + "metadata": {}, + "source": [ + "## Acknowledgements\n", + "\n", + "This notebook adapts the ROCm perception-kernel teaching approach from `AI_LABS/vision_kernels_rocm/ROCm_Physical_AI_Agent_Workshop.ipynb` in the original [ssw-mktg/igpu-training-env](https://gitenterprise.xilinx.com/ssw-mktg/igpu-training-env) repository.\n", + "\n", + "An earlier version of this material was presented as a demonstration and used for teaching at Advancing AI 2026. It was subsequently refined and adapted into the current notebook and Genesis 1.3.1 course environment.\n", + "\n", + "We thank the original repository contributors for the Physical AI vision workshop material that provided the foundation for this Genesis 1.3.1 course adaptation." + ] + }, + { + "cell_type": "markdown", + "id": "580d0923", + "metadata": {}, + "source": [ + "---\n", + "\n", + "Copyright (C) 2026 Advanced Micro Devices, Inc. All rights reserved. Portions of this file consist of AI-generated content. \n", + "SPDX-License-Identifier: MIT" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/GS06_language_guided_agent.ipynb b/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/GS06_language_guided_agent.ipynb new file mode 100644 index 0000000..1e0c615 --- /dev/null +++ b/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/GS06_language_guided_agent.ipynb @@ -0,0 +1,2067 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "placeholder", + "metadata": {}, + "source": [ + "# GS06 — Language-Guided Physical AI Agent\n", + "\n", + "### Lab Description\n", + "\n", + "This capstone combines the previous Genesis labs into one guarded physical AI loop:\n", + "\n", + "`natural language → validated JSON plan → visual confirmation → IK/action → verification`\n", + "\n", + "The agent supports picking a colored cube, stacking one cube on another, returning the arm home, and acknowledging stop commands locally. It includes a reproducible offline parser and can optionally connect to an OpenAI-compatible local LLM endpoint.\n", + "\n", + "Model output never drives the robot directly: every plan must pass schema validation and perception checks before action dispatch.\n", + "\n", + "> This synchronous teaching notebook prevents new actions after `stop`. A production deployment would also require asynchronous cancellation of an action already in progress.\n", + "\n", + "#### Recommended Hardware\n", + "\n", + "An AMD GPU supported by ROCm, such as an AMD Radeon™ GPU or AMD Ryzen™ AI processor with integrated Radeon graphics.\n", + "\n", + "#### Software Environment\n", + "\n", + "OS: Ubuntu 24.04 LTS \n", + "Install [AUP Learning Cloud](https://amdresearch.github.io/aup-learning-cloud/installation/quick-start.html). The Genesis Simulation image provides ROCm, PyTorch, and `genesis-world==1.3.1`.\n", + "\n", + "## Goals\n", + "\n", + "- Convert natural-language commands into constrained JSON plans.\n", + "- Reject invalid or unsafe plans before robot control.\n", + "- Confirm requested objects with ROCm visual perception.\n", + "- Gate every lift on a secure 8×8 fingertip tactile reading.\n", + "- Execute Genesis pick, stack, home, and stop behaviors.\n", + "- Verify visual, tactile, and motion outcomes in structured results." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41364e61", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import re\n", + "import json\n", + "import time\n", + "import uuid\n", + "import logging\n", + "import warnings\n", + "from pathlib import Path\n", + "\n", + "os.environ.setdefault(\"TI_LOG_LEVEL\", \"error\")\n", + "warnings.filterwarnings(\"ignore\")\n", + "\n", + "import numpy as np\n", + "import requests\n", + "import torch\n", + "import genesis as gs\n", + "import genesis.utils.geom as gu\n", + "from tqdm.auto import tqdm\n", + "from genesis.utils.misc import tensor_to_array\n", + "\n", + "from helpers.physisim_hud import (\n", + " FFmpegHUDWriter,\n", + " GPUMonitor,\n", + " build_vision_thumbnails,\n", + " compose_hud_frame,\n", + ")\n", + "from helpers.physisim_widget import LiveAgentController\n", + "\n", + "os.makedirs(\"Videos\", exist_ok=True)\n", + "\n", + "DEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n", + "PLANNER_MODE = os.getenv(\"PLANNER_MODE\", \"offline\").lower() # offline | llm\n", + "LLM_BASE_URL = os.getenv(\"LLM_BASE_URL\", \"http://127.0.0.1:8081\").rstrip(\"/\")\n", + "LLM_MODEL = os.getenv(\"LLM_MODEL\", \"local-model\")\n", + "LLM_TIMEOUT_SECONDS = float(os.getenv(\"LLM_TIMEOUT_SECONDS\", \"30\"))\n", + "\n", + "print(\"=== Runtime receipt ===\")\n", + "print(\"PyTorch device :\", DEVICE)\n", + "print(\"HIP :\", getattr(torch.version, \"hip\", None))\n", + "if DEVICE.type == \"cuda\":\n", + " print(\"GPU :\", torch.cuda.get_device_name(0))\n", + "print(\"Planner mode :\", PLANNER_MODE)\n", + "print(\"LLM endpoint :\", LLM_BASE_URL)\n", + "print(\"LLM model :\", LLM_MODEL)" + ] + }, + { + "cell_type": "markdown", + "id": "1ce38026", + "metadata": {}, + "source": [ + "## 1. Build the Genesis capstone scene\n", + "\n", + "The action layer reuses the Franka DOF groups, PD gains, IK orientation, and camera conventions from GS02–03. Three colored cubes provide deterministic targets for perception and language planning.\n", + "\n", + "> Run this section once per kernel. Restart the kernel before rebuilding the scene." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f3678e63", + "metadata": {}, + "outputs": [], + "source": [ + "assert \"scene\" not in globals(), \"Scene already exists. Restart the kernel before rebuilding it.\"\n", + "\n", + "gs.init(backend=gs.amdgpu, theme=\"light\", seed=0)\n", + "gs.logger._logger.setLevel(logging.WARNING)\n", + "\n", + "CUBE_SIZE = 0.04\n", + "CUBE_HALF = CUBE_SIZE / 2.0\n", + "EE_QUAT = np.array([0.0, 1.0, 0.0, 0.0])\n", + "GRIPPER_OPEN = 0.04\n", + "\n", + "cube_specs = {\n", + " \"red\": {\"pos\": (0.55, -0.15, CUBE_HALF), \"color\": (1.0, 0.0, 0.0, 1.0)},\n", + " \"green\": {\"pos\": (0.55, 0.00, CUBE_HALF), \"color\": (0.0, 1.0, 0.0, 1.0)},\n", + " \"blue\": {\"pos\": (0.55, 0.15, CUBE_HALF), \"color\": (0.0, 0.0, 1.0, 1.0)},\n", + "}\n", + "\n", + "scene = gs.Scene(\n", + " viewer_options=gs.options.ViewerOptions(\n", + " camera_pos=(3, -1, 1.5),\n", + " camera_lookat=(0.0, 0.0, 0.5),\n", + " camera_fov=30,\n", + " max_FPS=60,\n", + " ),\n", + " sim_options=gs.options.SimOptions(dt=0.01, substeps=4),\n", + " rigid_options=gs.options.RigidOptions(\n", + " box_box_detection=True,\n", + " constraint_timeconst=0.01,\n", + " ),\n", + " show_viewer=False,\n", + ")\n", + "\n", + "plane = scene.add_entity(gs.morphs.Plane())\n", + "cube_entities = {}\n", + "for name, spec in cube_specs.items():\n", + " cube_entities[name] = scene.add_entity(\n", + " gs.morphs.Box(size=(CUBE_SIZE,) * 3, pos=spec[\"pos\"]),\n", + " surface=gs.surfaces.Default(color=spec[\"color\"]),\n", + " )\n", + "\n", + "franka = scene.add_entity(\n", + " gs.morphs.MJCF(file=\"xml/franka_emika_panda/panda.xml\"),\n", + ")\n", + "cam = scene.add_camera(\n", + " res=(640, 480),\n", + " pos=(3, -1, 1.5),\n", + " lookat=(0, 0, 0.5),\n", + " fov=30,\n", + " GUI=True,\n", + ")\n", + "\n", + "# Same tactile contract taught in GS05.\n", + "CONTACT_THRESH_M = 5e-4\n", + "CONTACT_SECURE_TAXELS = 12\n", + "GRIP_STIFFNESS_N_PER_M = 5000.0\n", + "GRIP_CLOSE_TIMEOUT = 150\n", + "\n", + "probe_normal = (0.0, -1.0, 0.0)\n", + "probe_local_pos = gu.generate_grid_points_on_plane(\n", + " lo=(-0.006, 0.0, 0.04),\n", + " hi=(0.008, 0.0, 0.05),\n", + " normal=probe_normal,\n", + " nx=8,\n", + " ny=8,\n", + ")\n", + "tracked_cube_links = tuple(int(entity.base_link_idx) for entity in cube_entities.values())\n", + "tactile_options = dict(\n", + " probe_local_pos=probe_local_pos,\n", + " probe_local_normal=probe_normal,\n", + " probe_radius=0.002,\n", + " track_link_idx=tracked_cube_links,\n", + " n_sample_points=1000,\n", + " lambda_d=5000.0,\n", + " lambda_s=4000.0,\n", + " dilate_scale=1.0,\n", + " shear_scale=1.0,\n", + " normal_exponent=1.0,\n", + " compressibility=0.8,\n", + " draw_debug=False,\n", + ")\n", + "left_tactile = scene.add_sensor(\n", + " gs.sensors.ElastomerTaxel(\n", + " entity_idx=franka.idx,\n", + " link_idx_local=franka.get_link(\"left_finger\").idx_local,\n", + " **tactile_options,\n", + " )\n", + ")\n", + "right_tactile = scene.add_sensor(\n", + " gs.sensors.ElastomerTaxel(\n", + " entity_idx=franka.idx,\n", + " link_idx_local=franka.get_link(\"right_finger\").idx_local,\n", + " **tactile_options,\n", + " )\n", + ")\n", + "\n", + "scene.build()\n", + "print(\"Genesis scene built with cubes, Franka Panda, camera, and two 8×8 tactile pads\")" + ] + }, + { + "cell_type": "markdown", + "id": "8add8294", + "metadata": {}, + "source": [ + "## 2. Configure the Franka controller\n", + "\n", + "The agent reuses the control model from GS02–03. The seven arm joints and two gripper joints are controlled separately, while shared PD gains stabilize the complete nine-DOF system.\n", + "\n", + "We also save the initial joint configuration. The `home` behavior later plans a path back to this known pose instead of relying on a hard-coded posture." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c76e11be", + "metadata": {}, + "outputs": [], + "source": [ + "motors_dof = np.arange(7)\n", + "fingers_dof = np.arange(7, 9)\n", + "end_effector = franka.get_link(\"hand\")\n", + "\n", + "franka.set_dofs_kp(\n", + " np.array([4500, 4500, 3500, 3500, 2000, 2000, 2000, 100, 100])\n", + ")\n", + "franka.set_dofs_kv(\n", + " np.array([450, 450, 350, 350, 200, 200, 200, 10, 10])\n", + ")\n", + "franka.set_dofs_force_range(\n", + " np.array([-87, -87, -87, -87, -12, -12, -12, -100, -100]),\n", + " np.array([87, 87, 87, 87, 12, 12, 12, 100, 100]),\n", + ")\n", + "\n", + "\n", + "def to_numpy(value):\n", + " if isinstance(value, torch.Tensor):\n", + " return tensor_to_array(value)\n", + " return np.asarray(value)\n", + "\n", + "\n", + "INITIAL_QPOS = to_numpy(franka.get_qpos()).reshape(-1).astype(np.float64)\n", + "INITIAL_QPOS[-2:] = GRIPPER_OPEN\n", + "franka.set_qpos(INITIAL_QPOS)\n", + "for _ in range(20):\n", + " scene.step()\n", + "\n", + "print(\"Initial joint configuration:\", INITIAL_QPOS.round(3).tolist())" + ] + }, + { + "cell_type": "markdown", + "id": "c3a33f68", + "metadata": {}, + "source": [ + "## 3. Prepare the perception bridge\n", + "\n", + "GS06 needs only the target-record contract from GS05, so the full dashboard is not repeated here. We render RGB plus entity segmentation, normalize the buffer layout, and use segmentation once to calibrate each cube's rendered color.\n", + "\n", + "This calibration handles lighting and material differences between ideal colors such as `(1, 0, 0)` and the actual pixels seen by the camera." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e5b8777f", + "metadata": {}, + "outputs": [], + "source": [ + "def squeeze_buffer(value):\n", + " arr = to_numpy(value)\n", + " if arr.ndim == 4 and arr.shape[0] == 1:\n", + " arr = arr[0]\n", + " if arr.ndim == 3 and arr.shape[-1] == 1:\n", + " arr = arr[..., 0]\n", + " return arr\n", + "\n", + "\n", + "def as_rgb_uint8(value):\n", + " arr = squeeze_buffer(value)[..., :3]\n", + " if arr.dtype != np.uint8:\n", + " arr = arr.astype(np.float32)\n", + " if arr.size and float(np.nanmax(arr)) <= 1.5:\n", + " arr = arr * 255.0\n", + " arr = np.clip(arr, 0, 255).astype(np.uint8)\n", + " return arr\n", + "\n", + "\n", + "def render_perception_buffers():\n", + " rgb, depth, segmentation, normal = cam.render(\n", + " rgb=True,\n", + " depth=True,\n", + " segmentation=True,\n", + " normal=True,\n", + " colorize_seg=False,\n", + " )\n", + " return as_rgb_uint8(rgb), squeeze_buffer(segmentation).astype(np.int32)\n", + "\n", + "\n", + "initial_rgb, initial_seg = render_perception_buffers()\n", + "target_colors = {}\n", + "for name, entity in cube_entities.items():\n", + " mask = initial_seg == int(entity.idx + 1)\n", + " target_colors[name] = (\n", + " (initial_rgb[mask].mean(axis=0) / 255.0).tolist()\n", + " if mask.any()\n", + " else list(cube_specs[name][\"color\"][:3])\n", + " )\n", + "\n", + "print(\"Calibrated target colors:\", target_colors)" + ] + }, + { + "cell_type": "markdown", + "id": "4aca1f29", + "metadata": {}, + "source": [ + "### 3.1 Convert visual evidence into target records\n", + "\n", + "`color_lock()` produces image-space evidence: visibility, matching-pixel count, bounding box, and centroid. Only after an object is visually confirmed do we attach its exact Genesis world position.\n", + "\n", + "As in GS05, `position_source=\"genesis_entity_state\"` makes the simulator shortcut explicit. A physical robot would replace this field with calibrated depth or pose-estimation output." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e7a09a18", + "metadata": {}, + "outputs": [], + "source": [ + "def color_lock(rgb, target_rgb, tolerance=0.30, min_pixels=8):\n", + " # Genesis render buffers may use negative strides; PyTorch requires contiguous host memory.\n", + " contiguous_rgb = np.ascontiguousarray(np.asarray(rgb)[..., :3])\n", + " image = torch.from_numpy(contiguous_rgb).to(DEVICE, dtype=torch.float32) / 255.0\n", + " target = torch.tensor(target_rgb, dtype=torch.float32, device=DEVICE)\n", + " mask = torch.linalg.norm(image - target, dim=-1) < tolerance\n", + " count = int(mask.sum().item())\n", + "\n", + " if count < min_pixels:\n", + " return {\"visible\": False, \"pixel_count\": count, \"bbox\": None, \"centroid_px\": None}\n", + "\n", + " ys, xs = torch.where(mask)\n", + " return {\n", + " \"visible\": True,\n", + " \"pixel_count\": count,\n", + " \"bbox\": [int(xs.min()), int(ys.min()), int(xs.max()), int(ys.max())],\n", + " \"centroid_px\": [float(xs.float().mean()), float(ys.float().mean())],\n", + " }\n", + "\n", + "\n", + "def entity_world_position(name):\n", + " pos = to_numpy(cube_entities[name].get_pos(relative=False)).reshape(-1)[:3]\n", + " return pos.astype(float).tolist()\n", + "\n", + "\n", + "def build_target_record(name, rgb=None):\n", + " rgb = render_perception_buffers()[0] if rgb is None else rgb\n", + " lock = color_lock(rgb, target_colors[name])\n", + " return {\n", + " \"name\": name,\n", + " **lock,\n", + " \"world_position\": entity_world_position(name) if lock[\"visible\"] else None,\n", + " \"position_source\": \"genesis_entity_state\" if lock[\"visible\"] else None,\n", + " }\n", + "\n", + "\n", + "for color in cube_entities:\n", + " print(color, build_target_record(color, initial_rgb))" + ] + }, + { + "cell_type": "markdown", + "id": "2e4dc237", + "metadata": {}, + "source": [ + "### 3.2 Reuse the tactile contract from GS05\n", + "\n", + "The agent reads both 8×8 fingertip pads after each close-gripper step. `tactile_reduce()` returns the same five fields introduced in GS05: contact count, total taxels, force estimate, peak displacement, and `secure`.\n", + "\n", + "Keeping this interface identical lets the action layer consume tactile evidence without knowing how the individual taxels are arranged." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8bb967f9", + "metadata": {}, + "outputs": [], + "source": [ + "def read_tactile_displacement():\n", + " left = to_numpy(left_tactile.read_ground_truth()).astype(np.float32)\n", + " right = to_numpy(right_tactile.read_ground_truth()).astype(np.float32)\n", + " return left, right\n", + "\n", + "\n", + "def tactile_reduce(\n", + " left_disp,\n", + " right_disp,\n", + " contact_thresh_m=CONTACT_THRESH_M,\n", + " secure_taxels=CONTACT_SECURE_TAXELS,\n", + " grip_stiffness_N_per_m=GRIP_STIFFNESS_N_PER_M,\n", + "):\n", + " left_host = np.ascontiguousarray(left_disp, dtype=np.float32)\n", + " right_host = np.ascontiguousarray(right_disp, dtype=np.float32)\n", + " left = torch.from_numpy(left_host).to(DEVICE).reshape(-1, 3)\n", + " right = torch.from_numpy(right_host).to(DEVICE).reshape(-1, 3)\n", + " left_magnitude = torch.linalg.norm(left, dim=-1)\n", + " right_magnitude = torch.linalg.norm(right, dim=-1)\n", + "\n", + " n_contact = int(\n", + " (left_magnitude > contact_thresh_m).sum().item()\n", + " + (right_magnitude > contact_thresh_m).sum().item()\n", + " )\n", + " peak_m = torch.maximum(left_magnitude.max(), right_magnitude.max())\n", + " grip_force_N = (left_magnitude.sum() + right_magnitude.sum()) * grip_stiffness_N_per_m\n", + " return {\n", + " \"n_contact\": n_contact,\n", + " \"n_taxels\": int(left_magnitude.numel() + right_magnitude.numel()),\n", + " \"grip_force_N\": float(grip_force_N.item()),\n", + " \"peak_mm\": float(peak_m.item() * 1000.0),\n", + " \"secure\": bool(n_contact >= secure_taxels),\n", + " }\n", + "\n", + "\n", + "scene.step()\n", + "air_tactile = tactile_reduce(*read_tactile_displacement())\n", + "print(\"Open-air tactile receipt:\", air_tactile)\n", + "assert air_tactile[\"n_taxels\"] == 128\n", + "assert air_tactile[\"secure\"] is False" + ] + }, + { + "cell_type": "markdown", + "id": "e96d6ce7", + "metadata": {}, + "source": [ + "## 4. Define a constrained plan language\n", + "\n", + "Natural language is flexible, but robot handlers need predictable inputs. The planner therefore emits one of four small JSON shapes:\n", + "\n", + "```json\n", + "{\"intent\": \"pick\", \"object\": \"blue\"}\n", + "{\"intent\": \"stack\", \"object\": \"blue\", \"target\": \"red\"}\n", + "{\"intent\": \"home\"}\n", + "{\"intent\": \"stop\"}\n", + "```\n", + "\n", + "`validate_plan()` is the trust boundary between language and control. It rejects unknown keys, unsupported colors, missing fields, and attempts to stack an object on itself before any perception or motion code runs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f86ac84b", + "metadata": {}, + "outputs": [], + "source": [ + "ALLOWED_INTENTS = {\"pick\", \"stack\", \"home\", \"stop\"}\n", + "ALLOWED_KEYS = {\"intent\", \"object\", \"target\"}\n", + "COLORS = set(cube_entities)\n", + "COLOR_SYNONYMS = {\n", + " \"r\": \"red\", \"red\": \"red\", \"crimson\": \"red\",\n", + " \"g\": \"green\", \"green\": \"green\",\n", + " \"b\": \"blue\", \"blue\": \"blue\",\n", + "}\n", + "STOP_RE = re.compile(r\"\\b(stop|halt|freeze|abort|emergency)\\b\", re.IGNORECASE)\n", + "NOISE_TEXT = {\"\", \"uh\", \"um\", \"hmm\", \"hello\", \"hi\", \"thanks\", \"thank you\", \"okay\", \"ok\"}\n", + "\n", + "\n", + "def normalize_text(text):\n", + " return re.sub(r\"[\\s\\.,!?]+$\", \"\", (text or \"\").strip().lower()).strip()\n", + "\n", + "\n", + "def normalize_color(value):\n", + " return COLOR_SYNONYMS.get(str(value).strip().lower()) if value is not None else None\n", + "\n", + "\n", + "def validate_plan(raw):\n", + " if not isinstance(raw, dict):\n", + " return None, [\"plan must be a JSON object\"]\n", + "\n", + " errors = []\n", + " extra = set(raw) - ALLOWED_KEYS\n", + " if extra:\n", + " errors.append(f\"unknown keys: {sorted(extra)}\")\n", + "\n", + " intent = str(raw.get(\"intent\", \"\")).strip().lower()\n", + " if intent not in ALLOWED_INTENTS:\n", + " errors.append(f\"unsupported intent: {intent!r}\")\n", + "\n", + " obj = normalize_color(raw.get(\"object\")) if \"object\" in raw else None\n", + " target = normalize_color(raw.get(\"target\")) if \"target\" in raw else None\n", + "\n", + " if intent == \"pick\" and obj not in COLORS:\n", + " errors.append(f\"pick requires a supported object color, got {raw.get('object')!r}\")\n", + " if intent == \"stack\":\n", + " if obj not in COLORS:\n", + " errors.append(f\"stack requires a supported object color, got {raw.get('object')!r}\")\n", + " if target not in COLORS:\n", + " errors.append(f\"stack requires a supported target color, got {raw.get('target')!r}\")\n", + " if obj is not None and obj == target:\n", + " errors.append(\"stack object and target must differ\")\n", + " if intent in {\"home\", \"stop\"} and (\"object\" in raw or \"target\" in raw):\n", + " errors.append(f\"{intent} must not include object or target\")\n", + "\n", + " if errors:\n", + " return None, errors\n", + "\n", + " plan = {\"intent\": intent}\n", + " if obj is not None:\n", + " plan[\"object\"] = obj\n", + " if target is not None:\n", + " plan[\"target\"] = target\n", + " return plan, []\n", + "\n", + "\n", + "for example in [\n", + " {\"intent\": \"pick\", \"object\": \"blue\"},\n", + " {\"intent\": \"stack\", \"object\": \"red\", \"target\": \"red\"},\n", + " {\"intent\": \"home\", \"object\": \"green\"},\n", + "]:\n", + " plan, errors = validate_plan(example)\n", + " print(example, \"->\", plan or errors)" + ] + }, + { + "cell_type": "markdown", + "id": "09a01cb6", + "metadata": {}, + "source": [ + "### 4.1 Start with a deterministic offline parser\n", + "\n", + "The offline parser keeps the lab reproducible and makes intent routing easy to inspect. Regular expressions recognize the supported verbs, colors, and relations, then return the same dictionary shape expected from an LLM.\n", + "\n", + "It is intentionally limited: unsupported language produces `{\"intent\": \"unknown\"}` and is rejected by the same validator used for model output." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c74bb6d0", + "metadata": {}, + "outputs": [], + "source": [ + "PICK_RE = re.compile(r\"\\b(?:pick|grab|get)\\b.*?\\b(red|green|blue|r|g|b)\\b\", re.I)\n", + "STACK_RE = re.compile(\n", + " r\"\\b(?:stack|place|put)\\b.*?\\b(red|green|blue|r|g|b)\\b.*?\"\n", + " r\"\\b(?:on|onto|above)\\b.*?\\b(red|green|blue|r|g|b)\\b\",\n", + " re.I,\n", + ")\n", + "HOME_RE = re.compile(r\"\\b(?:go\\s+home|home|rest)\\b\", re.I)\n", + "\n", + "\n", + "def offline_rule_parser(text):\n", + " normalized = normalize_text(text)\n", + " match = STACK_RE.search(normalized)\n", + " if match:\n", + " return {\n", + " \"intent\": \"stack\",\n", + " \"object\": normalize_color(match.group(1)),\n", + " \"target\": normalize_color(match.group(2)),\n", + " }\n", + "\n", + " match = PICK_RE.search(normalized)\n", + " if match:\n", + " return {\"intent\": \"pick\", \"object\": normalize_color(match.group(1))}\n", + "\n", + " if HOME_RE.search(normalized):\n", + " return {\"intent\": \"home\"}\n", + " return {\"intent\": \"unknown\"}\n", + "\n", + "\n", + "for command in [\"pick green\", \"stack blue on red\", \"go home\", \"pick purple\"]:\n", + " print(command, \"->\", offline_rule_parser(command))" + ] + }, + { + "cell_type": "markdown", + "id": "91261e5d", + "metadata": {}, + "source": [ + "### 4.2 Optionally connect an OpenAI-compatible LLM\n", + "\n", + "`llm_rule_parser()` sends the same task to a local `/v1/chat/completions` endpoint. A short system prompt limits the response vocabulary, but prompt instructions alone are not a safety boundary—returned JSON still passes through `validate_plan()`.\n", + "\n", + "The lab does not download or start a model automatically. Configure `LLM_BASE_URL`, `LLM_MODEL`, and `PLANNER_MODE=llm` when a compatible local server is available." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7deed1eb", + "metadata": {}, + "outputs": [], + "source": [ + "LLM_SYSTEM_PROMPT = \"\"\"You route commands for a simulated Franka arm.\n", + "Return exactly one JSON object and no other text.\n", + "Allowed forms:\n", + "{\"intent\":\"pick\",\"object\":\"red|green|blue\"}\n", + "{\"intent\":\"stack\",\"object\":\"red|green|blue\",\"target\":\"red|green|blue\"}\n", + "{\"intent\":\"home\"}\n", + "{\"intent\":\"stop\"}\n", + "The stack object and target must differ.\n", + "\"\"\"\n", + "\n", + "\n", + "def llm_rule_parser(text):\n", + " payload = {\n", + " \"model\": LLM_MODEL,\n", + " \"messages\": [\n", + " {\"role\": \"system\", \"content\": LLM_SYSTEM_PROMPT},\n", + " {\"role\": \"user\", \"content\": text},\n", + " ],\n", + " \"temperature\": 0.0,\n", + " \"max_tokens\": 64,\n", + " }\n", + " response = requests.post(\n", + " f\"{LLM_BASE_URL}/v1/chat/completions\",\n", + " json=payload,\n", + " timeout=LLM_TIMEOUT_SECONDS,\n", + " )\n", + " response.raise_for_status()\n", + " content = response.json()[\"choices\"][0][\"message\"][\"content\"].strip()\n", + " start, end = content.find(\"{\"), content.rfind(\"}\")\n", + " if start < 0 or end < start:\n", + " raise ValueError(f\"model did not return a JSON object: {content!r}\")\n", + " return json.loads(content[start : end + 1])" + ] + }, + { + "cell_type": "markdown", + "id": "39ed942a", + "metadata": {}, + "source": [ + "### 4.3 Download the GGUF model on demand\n", + "\n", + "The Docker image contains `llama-server`, but deliberately does not embed the 2.02 GB model. Use the button below once per workspace to download:\n", + "\n", + "- Repository: `bartowski/Llama-3.2-3B-Instruct-GGUF`\n", + "- File: `Llama-3.2-3B-Instruct-Q4_K_M.gguf`\n", + "- Destination: `/opt/workspace/PhySim/models/`\n", + "- Expected SHA256: `6c1a2b41161032677be168d354123594c0e6e67d2b9227c84f296ad037c728ff`\n", + "\n", + "The download is opt-in, so Offline mode and automated notebook execution remain network-independent. The `models/` directory is ignored by Git and Docker build context. When the course directory is mounted from the host, the model persists after the container stops.\n", + "\n", + "Llama 3.2 weights are governed by the [Meta Llama 3.2 Community License](https://www.llama.com/llama3_2/license/). Review and accept the applicable terms before downloading or using the model. The quantized file is redistributed by the referenced Hugging Face repository; it is not covered by this course's MIT license.\n", + "\n", + "You may also call `download_llm_model()` directly to see normal Python progress output." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "908f5dc8", + "metadata": {}, + "outputs": [], + "source": [ + "import hashlib\n", + "import importlib\n", + "import site\n", + "import subprocess\n", + "import sys\n", + "\n", + "import ipywidgets as widgets\n", + "from IPython.display import display\n", + "\n", + "try:\n", + " from huggingface_hub import hf_hub_download\n", + "except ModuleNotFoundError:\n", + " print(\"huggingface_hub is missing; installing it into the current Jupyter kernel…\")\n", + " subprocess.check_call(\n", + " [\n", + " sys.executable,\n", + " \"-m\",\n", + " \"pip\",\n", + " \"install\",\n", + " \"--user\",\n", + " \"huggingface-hub>=0.34,<2\",\n", + " ]\n", + " )\n", + " user_site = site.getusersitepackages()\n", + " if user_site not in sys.path:\n", + " sys.path.insert(0, user_site)\n", + " importlib.invalidate_caches()\n", + " from huggingface_hub import hf_hub_download\n", + "\n", + "GGUF_REPO_ID = \"bartowski/Llama-3.2-3B-Instruct-GGUF\"\n", + "GGUF_REVISION = \"c346bfc2029e79ba6d7edf026cf01fe44242db0d\"\n", + "GGUF_FILENAME = \"Llama-3.2-3B-Instruct-Q4_K_M.gguf\"\n", + "GGUF_SHA256 = \"6c1a2b41161032677be168d354123594c0e6e67d2b9227c84f296ad037c728ff\"\n", + "GGUF_DIR = Path(\"/opt/workspace/PhySim/models\")\n", + "\n", + "\n", + "def file_sha256(path, chunk_size=8 * 1024 * 1024):\n", + " digest = hashlib.sha256()\n", + " with Path(path).open(\"rb\") as stream:\n", + " while chunk := stream.read(chunk_size):\n", + " digest.update(chunk)\n", + " return digest.hexdigest()\n", + "\n", + "\n", + "def download_llm_model():\n", + " \"\"\"Download and verify the pinned Q4_K_M GGUF, then update notebook globals.\"\"\"\n", + " global LLAMA_MODEL_PATH\n", + " GGUF_DIR.mkdir(parents=True, exist_ok=True)\n", + " destination = GGUF_DIR / GGUF_FILENAME\n", + "\n", + " if destination.is_file() and file_sha256(destination) == GGUF_SHA256:\n", + " print(\"Model already present and checksum verified:\", destination)\n", + " else:\n", + " if destination.exists():\n", + " destination.unlink()\n", + " print(\"Downloading approximately 2.02 GB from Hugging Face…\")\n", + " downloaded = Path(\n", + " hf_hub_download(\n", + " repo_id=GGUF_REPO_ID,\n", + " filename=GGUF_FILENAME,\n", + " revision=GGUF_REVISION,\n", + " local_dir=GGUF_DIR,\n", + " )\n", + " )\n", + " actual_sha256 = file_sha256(downloaded)\n", + " if actual_sha256 != GGUF_SHA256:\n", + " raise RuntimeError(\n", + " f\"GGUF checksum mismatch: expected {GGUF_SHA256}, got {actual_sha256}\"\n", + " )\n", + " destination = downloaded\n", + " print(\"Download and checksum verification complete:\", destination)\n", + "\n", + " LLAMA_MODEL_PATH = destination\n", + " os.environ[\"LLAMA_MODEL_PATH\"] = str(destination)\n", + " return destination\n", + "\n", + "\n", + "download_model_button = widgets.Button(\n", + " description=\"Download Llama 3.2 3B GGUF (2.02 GB)\",\n", + " button_style=\"warning\",\n", + " icon=\"download\",\n", + ")\n", + "download_model_output = widgets.Output()\n", + "\n", + "\n", + "def _download_model_clicked(_button):\n", + " download_model_button.disabled = True\n", + " try:\n", + " with download_model_output:\n", + " download_model_output.clear_output()\n", + " download_llm_model()\n", + " except Exception as error:\n", + " with download_model_output:\n", + " print(f\"{type(error).__name__}: {error}\")\n", + " finally:\n", + " download_model_button.disabled = False\n", + "\n", + "\n", + "download_model_button.on_click(_download_model_clicked)\n", + "display(widgets.VBox([download_model_button, download_model_output]))" + ] + }, + { + "cell_type": "markdown", + "id": "7449342c", + "metadata": {}, + "source": [ + "### 4.4 Start the bundled LLM endpoint from a JupyterHub Terminal\n", + "\n", + "The image contains a pinned Vulkan build of `llama-server` at:\n", + "\n", + "```text\n", + "/opt/llama/bin/llama-server\n", + "```\n", + "\n", + "After the download button reports a verified model, open **JupyterLab → File → New → Terminal** and run:\n", + "\n", + "```bash\n", + "bash helpers/start_llama_server.sh\n", + "```\n", + "\n", + "The helper automatically uses:\n", + "\n", + "```text\n", + "/opt/llama/bin/llama-server\n", + "/opt/workspace/PhySim/models/Llama-3.2-3B-Instruct-Q4_K_M.gguf\n", + "```\n", + "\n", + "Keep that Terminal open. A successful server reports that it is listening on `127.0.0.1:8081`. In a second Terminal, verify:\n", + "\n", + "```bash\n", + "curl http://127.0.0.1:8081/health\n", + "```\n", + "\n", + "Then rerun the health-check cell below and select **LLM endpoint** in the live agent interface. Press `Ctrl+C` in the server Terminal when finished.\n", + "\n", + "Advanced users can override `LLAMA_SERVER_BIN`, `LLAMA_MODEL_PATH`, `LLAMA_HOST`, or `LLAMA_PORT` before running the helper. Because the Terminal and notebook kernel share one JupyterHub container, the default loopback address is correct." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28f38e19", + "metadata": {}, + "outputs": [], + "source": [ + "LLAMA_SERVER_BIN = Path(\n", + " os.getenv(\"LLAMA_SERVER_BIN\", \"/opt/llama/bin/llama-server\")\n", + ").expanduser()\n", + "LLAMA_MODEL_PATH = Path(\n", + " os.getenv(\n", + " \"LLAMA_MODEL_PATH\",\n", + " \"/opt/workspace/PhySim/models/Llama-3.2-3B-Instruct-Q4_K_M.gguf\",\n", + " )\n", + ").expanduser()\n", + "\n", + "\n", + "def llm_endpoint_status(base_url=LLM_BASE_URL):\n", + " status = {\n", + " \"base_url\": base_url,\n", + " \"server_binary\": str(LLAMA_SERVER_BIN),\n", + " \"server_binary_ready\": LLAMA_SERVER_BIN.is_file() and os.access(LLAMA_SERVER_BIN, os.X_OK),\n", + " \"model\": str(LLAMA_MODEL_PATH),\n", + " \"model_ready\": LLAMA_MODEL_PATH.is_file(),\n", + " \"endpoint_ready\": False,\n", + " \"error\": None,\n", + " }\n", + " try:\n", + " response = requests.get(f\"{base_url}/health\", timeout=2)\n", + " response.raise_for_status()\n", + " status[\"endpoint_ready\"] = True\n", + " status[\"health\"] = response.json() if response.content else {\"status\": \"ok\"}\n", + " except (requests.RequestException, ValueError) as error:\n", + " status[\"error\"] = f\"{type(error).__name__}: {error}\"\n", + " return status\n", + "\n", + "\n", + "llm_status = llm_endpoint_status()\n", + "print(json.dumps(llm_status, indent=2))\n", + "if not llm_status[\"endpoint_ready\"]:\n", + " if not llm_status[\"model_ready\"]:\n", + " print(\"\\nDownload the GGUF with the button in the previous section first.\")\n", + " print(\"\\nThen open a JupyterHub Terminal and run:\")\n", + " print(\" bash helpers/start_llama_server.sh\")\n", + "else:\n", + " print(\"\\nLLM endpoint is ready. Select 'LLM endpoint' in the live interface.\")" + ] + }, + { + "cell_type": "markdown", + "id": "bb5ab7e9", + "metadata": {}, + "source": [ + "### 4.5 Apply local safety gates before either planner\n", + "\n", + "The order below is adapted from the VVLA pipeline and is part of the safety design:\n", + "\n", + "1. normalize the transcript;\n", + "2. recognize `stop` locally without waiting for a model;\n", + "3. reject empty or noise-like text;\n", + "4. call the selected parser;\n", + "5. validate the returned plan.\n", + "\n", + "Connection failures, malformed JSON, and schema errors produce a receipt with `plan=None`. They never fall back silently to a robot action." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d9d9538f", + "metadata": {}, + "outputs": [], + "source": [ + "def plan_command(text, mode=PLANNER_MODE):\n", + " started = time.perf_counter()\n", + " normalized = normalize_text(text)\n", + "\n", + " if STOP_RE.search(normalized):\n", + " return {\n", + " \"planner\": \"local-stop-fast-path\",\n", + " \"latency_ms\": 0.0,\n", + " \"plan\": {\"intent\": \"stop\"},\n", + " \"errors\": [],\n", + " }\n", + " if normalized in NOISE_TEXT or not any(char.isalpha() for char in normalized):\n", + " return {\n", + " \"planner\": \"noise-gate\",\n", + " \"latency_ms\": 0.0,\n", + " \"plan\": None,\n", + " \"errors\": [\"noise-like input ignored\"],\n", + " }\n", + "\n", + " if mode not in {\"offline\", \"llm\"}:\n", + " raise ValueError(f\"unsupported planner mode: {mode}\")\n", + " parser = offline_rule_parser if mode == \"offline\" else llm_rule_parser\n", + " planner_name = \"offline-rule-parser\" if mode == \"offline\" else \"openai-compatible-llm\"\n", + "\n", + " try:\n", + " raw = parser(normalized)\n", + " plan, errors = validate_plan(raw)\n", + " return {\n", + " \"planner\": planner_name,\n", + " \"latency_ms\": (time.perf_counter() - started) * 1000,\n", + " \"raw\": raw,\n", + " \"plan\": plan,\n", + " \"errors\": errors,\n", + " }\n", + " except (requests.RequestException, KeyError, ValueError, json.JSONDecodeError) as error:\n", + " return {\n", + " \"planner\": planner_name,\n", + " \"latency_ms\": (time.perf_counter() - started) * 1000,\n", + " \"plan\": None,\n", + " \"errors\": [f\"{type(error).__name__}: {error}\"],\n", + " }\n", + "\n", + "\n", + "for command in [\n", + " \"pick the green cube\",\n", + " \"stack the blue cube on the red cube\",\n", + " \"go home\",\n", + " \"stop immediately\",\n", + " \"stack red on red\",\n", + " \"pick purple\",\n", + "]:\n", + " receipt = plan_command(command, mode=\"offline\")\n", + " print(command, \"->\", receipt[\"plan\"] or receipt[\"errors\"])\n", + "\n", + "assert plan_command(\"stop\", mode=\"llm\")[\"planner\"] == \"local-stop-fast-path\"" + ] + }, + { + "cell_type": "markdown", + "id": "7adf5a88", + "metadata": {}, + "source": [ + "## 5. Build reusable Genesis action primitives\n", + "\n", + "The action layer turns validated plans into small, testable motion functions. It reuses the GS03 control pattern:\n", + "\n", + "- `inverse_kinematics()` computes a Franka joint target;\n", + "- `plan_path()` handles longer collision-aware approaches;\n", + "- arm-only position control handles short vertical motions;\n", + "- force control closes the gripper;\n", + "- every behavior returns structured data or raises an error before later phases run.\n", + "\n", + "Separating these primitives from language parsing makes the same motion code usable with the offline parser, an LLM, or future ROS 2 input." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0cc6742a", + "metadata": {}, + "outputs": [], + "source": [ + "PATH_WAYPOINTS = 150\n", + "SETTLE_STEPS = 80\n", + "REACH_STEPS = 100\n", + "_recording = False\n", + "_hud_writer = None\n", + "_hud_monitor = None\n", + "_live_agent_widget = None\n", + "_hud_frame_index = 0\n", + "_seg_ids = initial_seg.astype(np.int32)\n", + "HUD_THUMBNAILS = {\n", + " \"gray\": (initial_rgb[..., :3].astype(np.float32) @ np.array([0.299, 0.587, 0.114])).astype(np.uint8),\n", + " \"segmentation\": np.stack(\n", + " [(_seg_ids * 37 + 11) % 256, (_seg_ids * 79 + 43) % 256, (_seg_ids * 131 + 97) % 256],\n", + " axis=-1,\n", + " ).astype(np.uint8),\n", + "}\n", + "HUD_THUMBNAILS[\"segmentation\"][_seg_ids == 0] = 0\n", + "HUD_CONTEXT = {\n", + " \"user_input\": \"\",\n", + " \"planner_raw\": None,\n", + " \"plan\": None,\n", + " \"stages\": [],\n", + " \"status\": \"Ready\",\n", + " \"contact_threshold\": CONTACT_THRESH_M,\n", + " \"secure_taxels\": CONTACT_SECURE_TAXELS,\n", + "}\n", + "\n", + "\n", + "def _render_step(status=None, *, force=False, capture=True):\n", + " \"\"\"Render Genesis and optionally update MP4 and notebook live HUD sinks.\"\"\"\n", + " global _hud_frame_index, HUD_THUMBNAILS\n", + " _hud_frame_index += 1\n", + " hud_due = (\n", + " (_hud_writer is not None or _live_agent_widget is not None)\n", + " and (force or _hud_frame_index % 4 == 0)\n", + " )\n", + " if hud_due:\n", + " rgb_frame, depth_frame, seg_frame, normal_frame = cam.render(\n", + " rgb=True,\n", + " depth=True,\n", + " segmentation=True,\n", + " normal=True,\n", + " colorize_seg=False,\n", + " )\n", + " HUD_THUMBNAILS = build_vision_thumbnails(\n", + " to_numpy(rgb_frame),\n", + " to_numpy(depth_frame),\n", + " to_numpy(seg_frame),\n", + " to_numpy(normal_frame),\n", + " )\n", + " else:\n", + " rgb_frame, *_ = cam.render(\n", + " rgb=True,\n", + " depth=False,\n", + " segmentation=False,\n", + " normal=False,\n", + " )\n", + "\n", + " if hud_due:\n", + " left_now, right_now = read_tactile_displacement()\n", + " tactile_now = tactile_reduce(\n", + " left_now,\n", + " right_now,\n", + " contact_thresh_m=HUD_CONTEXT[\"contact_threshold\"],\n", + " secure_taxels=HUD_CONTEXT[\"secure_taxels\"],\n", + " )\n", + " if status:\n", + " HUD_CONTEXT[\"status\"] = status\n", + " frame = compose_hud_frame(\n", + " to_numpy(rgb_frame),\n", + " title=\"GS06 · Language-Guided Agent\",\n", + " status=HUD_CONTEXT[\"status\"],\n", + " user_input=HUD_CONTEXT[\"user_input\"],\n", + " planner_raw=HUD_CONTEXT[\"planner_raw\"],\n", + " plan=HUD_CONTEXT[\"plan\"],\n", + " stages=HUD_CONTEXT[\"stages\"],\n", + " thumbnails=HUD_THUMBNAILS,\n", + " tactile=tactile_now,\n", + " left_tactile=left_now,\n", + " right_tactile=right_now,\n", + " gpu=_hud_monitor.snapshot() if _hud_monitor is not None else None,\n", + " )\n", + " if _hud_writer is not None:\n", + " _hud_writer.write(frame)\n", + " if _live_agent_widget is not None:\n", + " _live_agent_widget.update(\n", + " frame,\n", + " HUD_CONTEXT[\"status\"],\n", + " capture=capture,\n", + " )\n", + " return rgb_frame\n", + "\n", + "\n", + "def simulate_steps(count, render=True, status=\"Settling simulation\"):\n", + " for _ in range(count):\n", + " scene.step()\n", + " if render:\n", + " _render_step(status)\n", + "\n", + "\n", + "def solve_ik(position, init_qpos=None):\n", + " \"\"\"Solve IK on the branch nearest the current or explicitly supplied pose.\"\"\"\n", + " seed = (\n", + " to_numpy(franka.get_qpos()).reshape(-1).astype(np.float64)\n", + " if init_qpos is None\n", + " else np.asarray(init_qpos, dtype=np.float64).reshape(-1)\n", + " )\n", + " qpos = franka.inverse_kinematics(\n", + " link=end_effector,\n", + " pos=np.asarray(position, dtype=np.float64),\n", + " quat=EE_QUAT,\n", + " init_qpos=seed,\n", + " )\n", + " qpos = to_numpy(qpos).reshape(-1).astype(np.float64)\n", + " if qpos.size < 9 or not np.isfinite(qpos).all():\n", + " raise RuntimeError(f\"IK failed for position {np.asarray(position).tolist()}\")\n", + " return qpos\n", + "\n", + "\n", + "def execute_planned_path(qpos, description):\n", + " path, valid = franka.plan_path(\n", + " qpos_goal=np.asarray(qpos, dtype=np.float64),\n", + " num_waypoints=PATH_WAYPOINTS,\n", + " return_valid_mask=True,\n", + " )\n", + " is_valid = bool(to_numpy(valid).reshape(-1)[0])\n", + " if not is_valid:\n", + " raise RuntimeError(f\"path planning failed: {description}\")\n", + " for waypoint in tqdm(path, desc=description, leave=False, ncols=90):\n", + " franka.control_dofs_position(waypoint)\n", + " scene.step()\n", + " _render_step(description)\n", + " simulate_steps(SETTLE_STEPS, status=f\"Settling after {description}\")\n", + "\n", + "\n", + "def move_arm_direct(position, description):\n", + " qpos = solve_ik(position)\n", + " franka.control_dofs_position(qpos[:-2], motors_dof)\n", + " for _ in tqdm(range(REACH_STEPS), desc=description, leave=False, ncols=90):\n", + " scene.step()\n", + " _render_step(description)\n", + " return qpos\n", + "\n", + "\n", + "def open_gripper():\n", + " franka.control_dofs_position(\n", + " np.array([GRIPPER_OPEN, GRIPPER_OPEN]),\n", + " fingers_dof,\n", + " )\n", + " simulate_steps(REACH_STEPS, status=\"Opening gripper\")\n", + "\n", + "\n", + "def close_gripper(\n", + " arm_qpos,\n", + " timeout_steps=GRIP_CLOSE_TIMEOUT,\n", + " contact_threshold=CONTACT_THRESH_M,\n", + " secure_taxels=CONTACT_SECURE_TAXELS,\n", + "):\n", + " \"\"\"Close until both tactile pads report a secure grasp or timeout.\"\"\"\n", + " arm_qpos = np.asarray(arm_qpos)\n", + " tactile = tactile_reduce(\n", + " *read_tactile_displacement(),\n", + " contact_thresh_m=contact_threshold,\n", + " secure_taxels=secure_taxels,\n", + " )\n", + "\n", + " for step in range(timeout_steps):\n", + " franka.control_dofs_position(arm_qpos[:-2], motors_dof)\n", + " franka.control_dofs_position(np.array([-0.03, -0.03]), fingers_dof)\n", + " scene.step()\n", + " tactile = tactile_reduce(\n", + " *read_tactile_displacement(),\n", + " contact_thresh_m=contact_threshold,\n", + " secure_taxels=secure_taxels,\n", + " )\n", + " _render_step(\n", + " f\"Closing gripper · K4 contact {tactile['n_contact']}/{tactile['n_taxels']}\"\n", + " )\n", + " if tactile[\"secure\"]:\n", + " return {\n", + " \"success\": True,\n", + " \"secure\": True,\n", + " \"steps\": step + 1,\n", + " \"tactile\": tactile,\n", + " }\n", + "\n", + " return {\n", + " \"success\": False,\n", + " \"secure\": False,\n", + " \"steps\": timeout_steps,\n", + " \"reason\": \"grasp timeout without secure tactile contact\",\n", + " \"tactile\": tactile,\n", + " }\n", + "\n", + "\n", + "print(\"Motion primitives ready; lift is tactile-gated\")" + ] + }, + { + "cell_type": "markdown", + "id": "e691ef3d", + "metadata": {}, + "source": [ + "### 5.1 Compose primitives into pick and stack behaviors\n", + "\n", + "A pick uses three end-effector waypoints relative to the cube center:\n", + "\n", + "1. **pre-grasp** approaches from above with an open gripper;\n", + "2. **grasp** lowers to the object and applies finger force;\n", + "3. **lift** raises the held object.\n", + "\n", + "Stacking first performs a pick, then computes the destination cube center one cube-height above the target. These offsets are teaching parameters for the fixed cube and Franka geometry, not general-purpose grasp planning." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "47c484cc", + "metadata": {}, + "outputs": [], + "source": [ + "def grasp_poses(object_center):\n", + " center = np.asarray(object_center, dtype=np.float64)\n", + " return {\n", + " \"pre_grasp\": center + np.array([0.0, 0.0, 0.23]),\n", + " \"grasp\": center + np.array([0.0, 0.0, 0.11]),\n", + " \"lift\": center + np.array([0.0, 0.0, 0.26]),\n", + " }\n", + "\n", + "\n", + "def pick_object(\n", + " record,\n", + " contact_threshold=CONTACT_THRESH_M,\n", + " secure_taxels=CONTACT_SECURE_TAXELS,\n", + "):\n", + " if not record[\"visible\"] or record[\"world_position\"] is None:\n", + " raise RuntimeError(f\"cannot pick invisible object: {record['name']}\")\n", + " poses = grasp_poses(record[\"world_position\"])\n", + "\n", + " pre_qpos = solve_ik(poses[\"pre_grasp\"])\n", + " pre_qpos[-2:] = GRIPPER_OPEN\n", + " execute_planned_path(pre_qpos, f\"approach {record['name']}\")\n", + "\n", + " grasp_qpos = move_arm_direct(poses[\"grasp\"], f\"lower to {record['name']}\")\n", + " grasp_receipt = close_gripper(\n", + " grasp_qpos,\n", + " contact_threshold=contact_threshold,\n", + " secure_taxels=secure_taxels,\n", + " )\n", + " if not grasp_receipt[\"success\"]:\n", + " raise RuntimeError(\n", + " \"tactile grasp gate failed: \"\n", + " f\"{grasp_receipt['reason']} \"\n", + " f\"(contacts={grasp_receipt['tactile']['n_contact']})\"\n", + " )\n", + "\n", + " move_arm_direct(poses[\"lift\"], f\"lift {record['name']}\")\n", + " return {\n", + " \"success\": True,\n", + " \"object\": record[\"name\"],\n", + " \"phases\": [\"approach\", \"tactile-secure grasp\", \"lift\"],\n", + " \"grasp\": grasp_receipt,\n", + " }\n", + "\n", + "\n", + "def place_object(source_name, desired_center):\n", + " poses = grasp_poses(desired_center)\n", + " move_arm_direct(poses[\"pre_grasp\"], f\"carry {source_name}\")\n", + " move_arm_direct(poses[\"grasp\"], f\"lower {source_name}\")\n", + " open_gripper()\n", + " move_arm_direct(poses[\"lift\"], f\"retreat from {source_name}\")\n", + " return {\"success\": True, \"placed_center\": np.asarray(desired_center).tolist()}\n", + "\n", + "\n", + "def stack_objects(\n", + " source_record,\n", + " target_record,\n", + " contact_threshold=CONTACT_THRESH_M,\n", + " secure_taxels=CONTACT_SECURE_TAXELS,\n", + "):\n", + " pick_result = pick_object(\n", + " source_record,\n", + " contact_threshold=contact_threshold,\n", + " secure_taxels=secure_taxels,\n", + " )\n", + " target = np.asarray(target_record[\"world_position\"], dtype=np.float64)\n", + " desired_center = target + np.array([0.0, 0.0, CUBE_SIZE])\n", + " place_result = place_object(source_record[\"name\"], desired_center)\n", + " return {\n", + " \"success\": True,\n", + " \"source\": source_record[\"name\"],\n", + " \"target\": target_record[\"name\"],\n", + " \"pick\": pick_result,\n", + " \"place\": place_result,\n", + " }\n", + "\n", + "\n", + "def go_home():\n", + " home = INITIAL_QPOS.copy()\n", + " home[-2:] = GRIPPER_OPEN\n", + " execute_planned_path(home, \"return home\")\n", + " return {\"success\": True}" + ] + }, + { + "cell_type": "markdown", + "id": "6cdfe262", + "metadata": {}, + "source": [ + "### 5.2 Guard the recording lifecycle\n", + "\n", + "Genesis cameras allow only one active recording at a time. The `_recording` flag prevents nested starts and ensures `stop_recording()` is safe to call from a `finally` block even when an action raises an exception.\n", + "\n", + "`start_recording()` now opens two synchronized outputs: the raw Genesis camera MP4 and a `_hud.mp4` stream composed by `helpers/physisim_hud.py`. Separate `start_hud_session()` and `stop_hud_session()` helpers support the optional typed-command demo without starting a second Genesis recorder." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "643b1d9b", + "metadata": {}, + "outputs": [], + "source": [ + "def start_hud_session(path=\"Videos/interactive_session.mp4\", fps=25):\n", + " global _hud_writer, _hud_monitor, _hud_frame_index\n", + " if _hud_writer is not None:\n", + " raise RuntimeError(\"HUD recording is already active\")\n", + " _hud_frame_index = 0\n", + " if _hud_monitor is None:\n", + " _hud_monitor = GPUMonitor().start()\n", + " _hud_writer = FFmpegHUDWriter(path, fps=fps).open()\n", + " return path\n", + "\n", + "\n", + "def stop_hud_session():\n", + " global _hud_writer, _hud_monitor\n", + " if _hud_writer is not None:\n", + " _hud_writer.close()\n", + " _hud_writer = None\n", + " if _hud_monitor is not None:\n", + " _hud_monitor.stop()\n", + " _hud_monitor = None\n", + "\n", + "\n", + "def start_recording(path=\"Videos/video_06.mp4\", fps=25):\n", + " global _recording\n", + " if _recording:\n", + " raise RuntimeError(\"camera recording is already active\")\n", + " cam.start_recording(save_to_filename=path, fps=fps)\n", + " hud_path = str(Path(path).with_name(Path(path).stem + \"_hud.mp4\"))\n", + " start_hud_session(hud_path, fps=fps)\n", + " _recording = True\n", + " return path, hud_path\n", + "\n", + "\n", + "def stop_recording():\n", + " global _recording\n", + " if _recording:\n", + " cam.stop_recording()\n", + " _recording = False\n", + " stop_hud_session()" + ] + }, + { + "cell_type": "markdown", + "id": "2a90bdc9", + "metadata": {}, + "source": [ + "## 6. Observe before acting and verify afterward\n", + "\n", + "Before dispatch, `observe_targets()` renders one frame and creates records for every object named in the plan. Any invisible object blocks the command.\n", + "\n", + "After motion, `verify_outcome()` checks simulator state rather than trusting the handler's return value. Pick requires measurable lift; stack requires both vertical separation and horizontal alignment; home checks joint error relative to the saved initial configuration." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a0ce2a6b", + "metadata": {}, + "outputs": [], + "source": [ + "def observe_targets(plan):\n", + " rgb, _ = render_perception_buffers()\n", + " names = []\n", + " if plan[\"intent\"] in {\"pick\", \"stack\"}:\n", + " names.append(plan[\"object\"])\n", + " if plan[\"intent\"] == \"stack\":\n", + " names.append(plan[\"target\"])\n", + "\n", + " records = {name: build_target_record(name, rgb) for name in names}\n", + " errors = [f\"{name} is not visible\" for name, record in records.items() if not record[\"visible\"]]\n", + " return records, errors\n", + "\n", + "\n", + "def extract_grasp_receipt(action_result):\n", + " if not isinstance(action_result, dict):\n", + " return None\n", + " if isinstance(action_result.get(\"grasp\"), dict):\n", + " return action_result[\"grasp\"]\n", + " pick_result = action_result.get(\"pick\")\n", + " if isinstance(pick_result, dict):\n", + " return pick_result.get(\"grasp\")\n", + " return None\n", + "\n", + "\n", + "def verify_outcome(plan, before_positions, action_result=None):\n", + " intent = plan[\"intent\"]\n", + " if intent == \"stop\":\n", + " return {\"success\": True, \"checks\": {\"stop\": \"no action dispatched\"}}\n", + " if intent == \"home\":\n", + " current = to_numpy(franka.get_qpos()).reshape(-1)\n", + " error = float(np.max(np.abs(current[:7] - INITIAL_QPOS[:7])))\n", + " return {\"success\": error < 0.15, \"checks\": {\"max_joint_error\": error}}\n", + "\n", + " source = plan[\"object\"]\n", + " source_before = np.asarray(before_positions[source])\n", + " source_after = np.asarray(entity_world_position(source))\n", + " moved = float(np.linalg.norm(source_after - source_before))\n", + " checks = {\"object_moved_m\": moved}\n", + " success = moved > 0.015\n", + "\n", + " grasp = extract_grasp_receipt(action_result)\n", + " tactile = grasp.get(\"tactile\", {}) if grasp else {}\n", + " checks[\"tactile_secure\"] = bool(grasp and grasp.get(\"secure\"))\n", + " checks[\"tactile_n_contact\"] = int(tactile.get(\"n_contact\", -1))\n", + " checks[\"tactile_peak_mm\"] = float(tactile.get(\"peak_mm\", -1.0))\n", + " success = success and checks[\"tactile_secure\"]\n", + "\n", + " if intent == \"pick\":\n", + " checks[\"object_lift_m\"] = float(source_after[2] - source_before[2])\n", + " success = success and checks[\"object_lift_m\"] > 0.02\n", + "\n", + " if intent == \"stack\":\n", + " target_after = np.asarray(entity_world_position(plan[\"target\"]))\n", + " xy_error = float(np.linalg.norm(source_after[:2] - target_after[:2]))\n", + " height_difference = float(source_after[2] - target_after[2])\n", + " checks.update({\"stack_xy_error_m\": xy_error, \"stack_height_difference_m\": height_difference})\n", + " success = success and xy_error < 0.06 and height_difference > CUBE_SIZE * 0.7\n", + "\n", + " return {\"success\": bool(success), \"checks\": checks}\n", + "\n", + "\n", + "print(\"Observation and multimodal verification functions ready\")" + ] + }, + { + "cell_type": "markdown", + "id": "25c6d7ce", + "metadata": {}, + "source": [ + "## 7. Register intent handlers\n", + "\n", + "`CommandRegistry` decouples plan names from implementation functions. Each supported intent has one handler, and unregistered intents fail closed.\n", + "\n", + "The `stop` handler intentionally issues no joint command. In this synchronous notebook it acknowledges the request and prevents a new behavior from starting; a production controller would also need asynchronous interruption for an action already in progress." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e086d70b", + "metadata": {}, + "outputs": [], + "source": [ + "class CommandRegistry:\n", + " def __init__(self):\n", + " self.handlers = {}\n", + "\n", + " def register(self, intent):\n", + " def decorator(function):\n", + " self.handlers[intent] = function\n", + " return function\n", + " return decorator\n", + "\n", + " def dispatch(self, plan, targets, tactile_config=None):\n", + " if plan[\"intent\"] not in self.handlers:\n", + " raise RuntimeError(f\"no handler registered for {plan['intent']}\")\n", + " return self.handlers[plan[\"intent\"]](plan, targets, tactile_config or {})\n", + "\n", + "\n", + "registry = CommandRegistry()\n", + "\n", + "\n", + "@registry.register(\"pick\")\n", + "def handle_pick(plan, targets, tactile_config):\n", + " return pick_object(targets[plan[\"object\"]], **tactile_config)\n", + "\n", + "\n", + "@registry.register(\"stack\")\n", + "def handle_stack(plan, targets, tactile_config):\n", + " return stack_objects(\n", + " targets[plan[\"object\"]],\n", + " targets[plan[\"target\"]],\n", + " **tactile_config,\n", + " )\n", + "\n", + "\n", + "@registry.register(\"home\")\n", + "def handle_home(plan, targets, tactile_config):\n", + " return go_home()\n", + "\n", + "\n", + "@registry.register(\"stop\")\n", + "def handle_stop(plan, targets, tactile_config):\n", + " return {\"success\": True, \"reason\": \"stop acknowledged; no joint command issued\"}\n", + "\n", + "\n", + "print(\"Registered intents:\", sorted(registry.handlers))" + ] + }, + { + "cell_type": "markdown", + "id": "b7878ed4", + "metadata": {}, + "source": [ + "## 8. Connect the complete guarded pipeline\n", + "\n", + "`handle_command()` is the orchestrator. It records each stage in order:\n", + "\n", + "`planner → perception → action → verification`\n", + "\n", + "Every result carries a trace ID and latency. Early failures return immediately without adding an action stage. Action exceptions are converted into structured failure receipts, and recording is always closed in `finally`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7a12841f", + "metadata": {}, + "outputs": [], + "source": [ + "def handle_command(\n", + " text,\n", + " mode=PLANNER_MODE,\n", + " record=False,\n", + " video_path=\"Videos/video_06.mp4\",\n", + " tactile_overrides=None,\n", + "):\n", + " trace_id = uuid.uuid4().hex[:8]\n", + " started = time.perf_counter()\n", + " stages = []\n", + " tactile_config = {\n", + " \"contact_threshold\": CONTACT_THRESH_M,\n", + " \"secure_taxels\": CONTACT_SECURE_TAXELS,\n", + " }\n", + " if tactile_overrides:\n", + " tactile_config.update(tactile_overrides)\n", + " HUD_CONTEXT.update(\n", + " {\n", + " \"user_input\": text,\n", + " \"planner_raw\": None,\n", + " \"plan\": None,\n", + " \"stages\": [],\n", + " \"status\": \"Planning command\",\n", + " \"contact_threshold\": tactile_config[\"contact_threshold\"],\n", + " \"secure_taxels\": tactile_config[\"secure_taxels\"],\n", + " }\n", + " )\n", + "\n", + " planner_receipt = plan_command(text, mode=mode)\n", + " stages.append({\"stage\": \"planner\", **planner_receipt})\n", + " plan = planner_receipt.get(\"plan\")\n", + " HUD_CONTEXT.update(\n", + " {\n", + " \"planner_raw\": planner_receipt.get(\"raw\") or planner_receipt.get(\"errors\"),\n", + " \"plan\": plan,\n", + " \"stages\": list(stages),\n", + " \"status\": \"Plan validated\" if plan is not None else \"Planner rejected command\",\n", + " }\n", + " )\n", + " if plan is None:\n", + " return {\n", + " \"trace_id\": trace_id,\n", + " \"success\": False,\n", + " \"stages\": stages,\n", + " \"total_latency_ms\": (time.perf_counter() - started) * 1000,\n", + " }\n", + "\n", + " targets, perception_errors = observe_targets(plan)\n", + " stages.append({\"stage\": \"perception\", \"targets\": targets, \"errors\": perception_errors})\n", + " HUD_CONTEXT.update(\n", + " {\n", + " \"stages\": list(stages),\n", + " \"status\": \"Targets confirmed\" if not perception_errors else \"Perception blocked action\",\n", + " }\n", + " )\n", + " if perception_errors:\n", + " return {\n", + " \"trace_id\": trace_id,\n", + " \"success\": False,\n", + " \"plan\": plan,\n", + " \"stages\": stages,\n", + " \"total_latency_ms\": (time.perf_counter() - started) * 1000,\n", + " }\n", + "\n", + " before_positions = {\n", + " name: record_data[\"world_position\"]\n", + " for name, record_data in targets.items()\n", + " if record_data[\"world_position\"] is not None\n", + " }\n", + "\n", + " action_result = None\n", + " HUD_CONTEXT[\"status\"] = f\"Executing {plan['intent']}\"\n", + " if record:\n", + " start_recording(video_path)\n", + " try:\n", + " action_result = registry.dispatch(plan, targets, tactile_config=tactile_config)\n", + " except Exception as error:\n", + " action_result = {\n", + " \"success\": False,\n", + " \"error\": f\"{type(error).__name__}: {error}\",\n", + " }\n", + " finally:\n", + " if record:\n", + " stop_recording()\n", + " stages.append({\"stage\": \"action\", \"result\": action_result})\n", + " HUD_CONTEXT.update(\n", + " {\n", + " \"stages\": list(stages),\n", + " \"status\": \"Action completed\" if action_result.get(\"success\") else \"Action failed\",\n", + " }\n", + " )\n", + "\n", + " if action_result.get(\"success\"):\n", + " verification = verify_outcome(plan, before_positions, action_result=action_result)\n", + " else:\n", + " verification = {\"success\": False, \"checks\": {\"skipped\": \"action failed\"}}\n", + " stages.append({\"stage\": \"verification\", **verification})\n", + "\n", + " return {\n", + " \"trace_id\": trace_id,\n", + " \"success\": bool(action_result.get(\"success\") and verification[\"success\"]),\n", + " \"plan\": plan,\n", + " \"stages\": stages,\n", + " \"total_latency_ms\": (time.perf_counter() - started) * 1000,\n", + " }\n", + "\n", + "\n", + "# These checks exercise planning and the local stop path without moving the robot.\n", + "for text in [\"stop immediately\", \"unsupported request\"]:\n", + " result = handle_command(text, mode=\"offline\", record=False)\n", + " print(text, \"->\", result[\"success\"], [stage[\"stage\"] for stage in result[\"stages\"]])\n", + "\n", + "stop_result = handle_command(\"stop\", mode=\"llm\", record=False)\n", + "assert stop_result[\"plan\"] == {\"intent\": \"stop\"}\n", + "assert stop_result[\"stages\"][0][\"planner\"] == \"local-stop-fast-path\"" + ] + }, + { + "cell_type": "markdown", + "id": "1f9184ec", + "metadata": {}, + "source": [ + "## 9. Run an end-to-end mission\n", + "\n", + "The mission asks the agent to stack the blue cube on the red cube, records the manipulation, verifies the final geometry, and then returns the arm home.\n", + "\n", + "This Run All mission explicitly uses the deterministic Offline planner so it remains reproducible even when an LLM endpoint is configured. Use the live language-agent interface later in the notebook to select and test the local LLM endpoint." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "06cd4bda", + "metadata": {}, + "outputs": [], + "source": [ + "from IPython.display import Video, display\n", + "\n", + "video_path = \"Videos/video_06.mp4\"\n", + "hud_video_path = \"Videos/video_06_hud.mp4\"\n", + "mission = handle_command(\n", + " \"stack the blue cube on the red cube\",\n", + " mode=\"offline\", # deterministic Run All path; use the live widget to test the LLM endpoint\n", + " record=True,\n", + " video_path=video_path,\n", + ")\n", + "\n", + "print(\"Mission summary:\")\n", + "print(json.dumps({\n", + " \"trace_id\": mission[\"trace_id\"],\n", + " \"success\": mission[\"success\"],\n", + " \"plan\": mission.get(\"plan\"),\n", + " \"total_latency_ms\": mission[\"total_latency_ms\"],\n", + "}, indent=2))\n", + "\n", + "for stage in mission[\"stages\"]:\n", + " if stage[\"stage\"] in {\"action\", \"verification\"}:\n", + " print(stage)\n", + "\n", + "home_result = handle_command(\"go home\", mode=\"offline\", record=False)\n", + "print(\"Home result:\", home_result[\"success\"])\n", + "\n", + "action_stage = next(stage for stage in mission[\"stages\"] if stage[\"stage\"] == \"action\")\n", + "verification_stage = next(stage for stage in mission[\"stages\"] if stage[\"stage\"] == \"verification\")\n", + "grasp_receipt = action_stage[\"result\"][\"pick\"][\"grasp\"]\n", + "\n", + "assert grasp_receipt[\"secure\"] is True\n", + "assert grasp_receipt[\"tactile\"][\"n_contact\"] >= CONTACT_SECURE_TAXELS\n", + "assert verification_stage[\"checks\"][\"tactile_secure\"] is True\n", + "assert mission[\"success\"], f\"stack mission failed: {mission['stages']}\"\n", + "assert home_result[\"success\"], f\"home behavior failed: {home_result['stages']}\"\n", + "\n", + "if os.path.exists(video_path):\n", + " print(\"Raw Genesis mission\")\n", + " display(Video(video_path, embed=True, width=720))\n", + "else:\n", + " print(\"No raw video was written because the mission stopped before action dispatch.\")\n", + "\n", + "if os.path.exists(hud_video_path):\n", + " print(\"AI BRAIN mission HUD\")\n", + " display(Video(hud_video_path, embed=True, width=960))\n", + "else:\n", + " print(\"No HUD video was written.\")" + ] + }, + { + "cell_type": "markdown", + "id": "32aca0ee", + "metadata": {}, + "source": [ + "## 10. Live language-agent interface\n", + "\n", + "This widget sends commands through the same guarded pipeline used by the tested mission:\n", + "\n", + "`text → planner → validate → visual confirmation → tactile-gated action → verification`\n", + "\n", + "Use **Offline** for the self-contained parser or select **LLM endpoint** after configuring `LLM_BASE_URL`. The live HUD updates during robot motion and shows the command, validated plan, execution stage, GPU telemetry, vision thumbnails, and both tactile pads.\n", + "\n", + "- **Run Command** executes the text field.\n", + "- **Home** and **Stop** use the same local safety paths as ordinary commands.\n", + "- **Reset Scene** restores all cubes and the initial robot pose.\n", + "- Tactile sliders change the actual close-gripper secure gate, not only the display.\n", + "- **Export HUD MP4** writes the captured interaction to `Videos/video_06_live_hud.mp4`.\n", + "- **Shutdown HUD** stops the GPU telemetry thread when the interaction is finished." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f66bd5f6", + "metadata": {}, + "outputs": [], + "source": [ + "from IPython.display import display\n", + "\n", + "# Reset returns to the settled scene-start pose, before any mission moves the arm.\n", + "UPRIGHT_QPOS = INITIAL_QPOS.copy()\n", + "UPRIGHT_QPOS[-2:] = GRIPPER_OPEN\n", + "\n", + "RANDOM_LAYOUT_X = (0.46, 0.62)\n", + "RANDOM_LAYOUT_Y = (-0.20, 0.20)\n", + "RANDOM_LAYOUT_MIN_DISTANCE = 0.09\n", + "\n", + "\n", + "def agent_cube_layout_positions(controller):\n", + " \"\"\"Return deterministic default or seeded-random cube positions.\"\"\"\n", + " if controller.scene_layout.value == \"default\":\n", + " return {\n", + " name: np.asarray(spec[\"pos\"], dtype=np.float64).copy()\n", + " for name, spec in cube_specs.items()\n", + " }\n", + "\n", + " rng = np.random.default_rng(int(controller.layout_seed.value))\n", + " positions = {}\n", + " for name in cube_entities:\n", + " for _ in range(200):\n", + " candidate = np.array(\n", + " [\n", + " rng.uniform(*RANDOM_LAYOUT_X),\n", + " rng.uniform(*RANDOM_LAYOUT_Y),\n", + " CUBE_HALF,\n", + " ],\n", + " dtype=np.float64,\n", + " )\n", + " if all(\n", + " np.linalg.norm(candidate[:2] - other[:2]) >= RANDOM_LAYOUT_MIN_DISTANCE\n", + " for other in positions.values()\n", + " ):\n", + " positions[name] = candidate\n", + " break\n", + " else:\n", + " raise RuntimeError(\"Could not sample a collision-free seeded cube layout\")\n", + " return positions\n", + "\n", + "\n", + "def activate_agent_widget(controller):\n", + " global _live_agent_widget, _hud_monitor, _hud_frame_index\n", + " _live_agent_widget = controller\n", + " _hud_frame_index = 0\n", + " if _hud_monitor is None:\n", + " _hud_monitor = GPUMonitor().start()\n", + "\n", + "\n", + "def agent_live_result_frame(controller, result):\n", + " controller.last_result = result\n", + " HUD_CONTEXT.update(\n", + " {\n", + " \"stages\": result.get(\"stages\", []),\n", + " \"status\": \"Command succeeded\" if result[\"success\"] else \"Command failed safely\",\n", + " }\n", + " )\n", + " for _ in range(40):\n", + " scene.step()\n", + " _render_step(HUD_CONTEXT[\"status\"])\n", + "\n", + "\n", + "def agent_live_run(controller):\n", + " activate_agent_widget(controller)\n", + " result = handle_command(\n", + " controller.command.value,\n", + " mode=controller.planner_mode.value,\n", + " record=False,\n", + " tactile_overrides={\n", + " \"contact_threshold\": controller.contact_threshold.value,\n", + " \"secure_taxels\": controller.secure_taxels.value,\n", + " },\n", + " )\n", + " agent_live_result_frame(controller, result)\n", + " print(json.dumps({\n", + " \"trace_id\": result[\"trace_id\"],\n", + " \"success\": result[\"success\"],\n", + " \"plan\": result.get(\"plan\"),\n", + " }, indent=2))\n", + "\n", + "\n", + "def agent_live_home(controller):\n", + " controller.command.value = \"home\"\n", + " agent_live_run(controller)\n", + "\n", + "\n", + "def agent_live_stop(controller):\n", + " controller.command.value = \"stop\"\n", + " agent_live_run(controller)\n", + "\n", + "\n", + "def agent_live_shutdown(controller):\n", + " global _live_agent_widget, _hud_monitor\n", + " if _hud_monitor is not None:\n", + " _hud_monitor.stop()\n", + " _hud_monitor = None\n", + " _live_agent_widget = None\n", + " controller.set_status(\"Live agent HUD stopped. Run, Home, Stop, or Reset will restart it.\")\n", + "\n", + "\n", + "def agent_live_reset(controller):\n", + " activate_agent_widget(controller)\n", + " layout_positions = agent_cube_layout_positions(controller)\n", + " for name, entity in cube_entities.items():\n", + " entity.set_pos(layout_positions[name])\n", + " franka.set_qpos(UPRIGHT_QPOS)\n", + " franka.control_dofs_position(UPRIGHT_QPOS[:7], motors_dof)\n", + " franka.control_dofs_position(\n", + " np.array([GRIPPER_OPEN, GRIPPER_OPEN]),\n", + " fingers_dof,\n", + " )\n", + " layout_status = (\n", + " \"Default layout\"\n", + " if controller.scene_layout.value == \"default\"\n", + " else f\"Random layout · seed {controller.layout_seed.value}\"\n", + " )\n", + " HUD_CONTEXT.update(\n", + " {\n", + " \"user_input\": f\"Reset Scene · {layout_status}\",\n", + " \"planner_raw\": None,\n", + " \"plan\": {\n", + " \"intent\": \"reset\",\n", + " \"layout\": controller.scene_layout.value,\n", + " \"seed\": int(controller.layout_seed.value),\n", + " },\n", + " \"stages\": [{\"stage\": \"reset\", \"success\": True}],\n", + " \"status\": f\"Scene reset · upright home · {layout_status}\",\n", + " }\n", + " )\n", + " for _ in range(50):\n", + " scene.step()\n", + " _render_step(HUD_CONTEXT[\"status\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bfb72217", + "metadata": {}, + "outputs": [], + "source": [ + "agent_live = LiveAgentController(\n", + " contact_threshold=CONTACT_THRESH_M,\n", + " secure_taxels=CONTACT_SECURE_TAXELS,\n", + " export_path=\"Videos/video_06_live_hud.mp4\",\n", + " export_fps=25,\n", + ")\n", + "agent_live.bind(\"run\", agent_live_run)\n", + "agent_live.bind(\"home\", agent_live_home)\n", + "agent_live.bind(\"stop\", agent_live_stop)\n", + "agent_live.bind(\"reset\", agent_live_reset)\n", + "agent_live.bind(\"shutdown\", agent_live_shutdown)\n", + "\n", + "activate_agent_widget(agent_live)\n", + "_render_step(\n", + " \"Live preview · enter a command or press Reset Scene.\",\n", + " force=True,\n", + " capture=False,\n", + ")\n", + "display(agent_live.widget)" + ] + }, + { + "cell_type": "markdown", + "id": "ef5fb309", + "metadata": {}, + "source": [ + "## 11. Optional terminal-style typed-command demo\n", + "\n", + "The function below turns the notebook into a small REPL without changing the tested top-to-bottom path. It keeps one HUD recording open across multiple commands and saves `Videos/interactive_session.mp4` when you type `quit`.\n", + "\n", + "Try `pick green`, `stack blue on red`, `home`, or `stop`. The function is defined but not called automatically, so `nbconvert` and **Run All** do not block on keyboard input.\n", + "\n", + "```python\n", + "run_typed_demo(mode=\"offline\")\n", + "# Or connect a local OpenAI-compatible endpoint first:\n", + "# run_typed_demo(mode=\"llm\")\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "254fbbeb", + "metadata": {}, + "outputs": [], + "source": [ + "def run_typed_demo(mode=\"offline\", path=\"Videos/interactive_session.mp4\"):\n", + " \"\"\"Run optional typed commands and record one continuous AI BRAIN HUD session.\"\"\"\n", + " command_count = 0\n", + " start_hud_session(path, fps=25)\n", + " try:\n", + " while True:\n", + " text = input(\"[YOU] > \").strip()\n", + " if not text or text.lower() in {\"quit\", \"exit\", \"q\"}:\n", + " break\n", + "\n", + " result = handle_command(text, mode=mode, record=False)\n", + " command_count += 1\n", + " print(\n", + " json.dumps(\n", + " {\n", + " \"trace_id\": result[\"trace_id\"],\n", + " \"success\": result[\"success\"],\n", + " \"plan\": result.get(\"plan\"),\n", + " },\n", + " indent=2,\n", + " )\n", + " )\n", + "\n", + " HUD_CONTEXT.update(\n", + " {\n", + " \"stages\": result.get(\"stages\", []),\n", + " \"status\": \"Command succeeded\" if result[\"success\"] else \"Command failed safely\",\n", + " }\n", + " )\n", + " for _ in range(40):\n", + " scene.step()\n", + " _render_step(HUD_CONTEXT[\"status\"])\n", + " finally:\n", + " stop_hud_session()\n", + "\n", + " print(f\"Saved {command_count} typed commands to {path}\")\n", + " if os.path.exists(path):\n", + " display(Video(path, embed=True, width=960))\n", + " return path" + ] + }, + { + "cell_type": "markdown", + "id": "1a46bf60", + "metadata": {}, + "source": [ + "## 12. Test failure paths\n", + "\n", + "A safe agent must demonstrate what it refuses to do. The following cases check unsupported colors, self-stacking, unknown intents, extra fields, and unsupported language.\n", + "\n", + "Each invalid request must fail before action dispatch. The final assertion verifies this by confirming that the stage trace contains no `action` entry." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1c62d891", + "metadata": {}, + "outputs": [], + "source": [ + "# Failure-path exercises: all are blocked before robot action dispatch.\n", + "invalid_cases = [\n", + " {\"intent\": \"pick\", \"object\": \"purple\"},\n", + " {\"intent\": \"stack\", \"object\": \"red\", \"target\": \"red\"},\n", + " {\"intent\": \"dance\"},\n", + " {\"intent\": \"home\", \"object\": \"blue\"},\n", + "]\n", + "\n", + "for raw in invalid_cases:\n", + " plan, errors = validate_plan(raw)\n", + " assert plan is None\n", + " print(\"Blocked:\", raw, \"->\", errors)\n", + "\n", + "unsupported = handle_command(\"please fly away\", mode=\"offline\", record=False)\n", + "assert unsupported[\"success\"] is False\n", + "assert all(stage[\"stage\"] != \"action\" for stage in unsupported[\"stages\"])\n", + "print(\"PASS: unsupported language produced no action stage\")" + ] + }, + { + "cell_type": "markdown", + "id": "b7c64739", + "metadata": {}, + "source": [ + "### 12.1 Verify tactile timeout behavior\n", + "\n", + "A syntactically valid command is still unsafe when the gripper cannot confirm contact. With the arm at home, closing in open air must reach the timeout with `secure=False`.\n", + "\n", + "This demonstrates the difference between plan validation and execution-time safety: both are required before an agent can report success." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "79c64ce5", + "metadata": {}, + "outputs": [], + "source": [ + "franka.set_qpos(INITIAL_QPOS)\n", + "franka.control_dofs_position(np.array([GRIPPER_OPEN, GRIPPER_OPEN]), fingers_dof)\n", + "simulate_steps(20, render=False)\n", + "home_qpos = to_numpy(franka.get_qpos()).reshape(-1)\n", + "air_grasp = close_gripper(home_qpos, timeout_steps=30)\n", + "print(\"Open-air grasp receipt:\", air_grasp)\n", + "\n", + "assert air_grasp[\"success\"] is False\n", + "assert air_grasp[\"secure\"] is False\n", + "assert \"timeout\" in air_grasp[\"reason\"]\n", + "\n", + "open_gripper()" + ] + }, + { + "cell_type": "markdown", + "id": "fb6185d2", + "metadata": {}, + "source": [ + "## Conclusions\n", + "\n", + "You built a simulation-native physical AI agent with local language safety gates, ROCm visual perception, two 8×8 fingertip tactile pads, tactile-gated grasping, Genesis IK and manipulation behaviors, and structured multimodal verification. A lift occurs only after the taxel reduction confirms secure contact; open-air closure fails with a timeout receipt. The AI BRAIN HUD synchronizes scene motion with plans, stage receipts, GPU telemetry, vision thumbnails, and live tactile heatmaps. The notebook-native agent widget lets you submit commands, switch planner modes, reset/home/stop the scene, tune the real tactile gate, and export captured frames. A terminal-style typed-command function remains available as an optional alternative without blocking automated notebook execution." + ] + }, + { + "cell_type": "markdown", + "id": "ac6ed0f2", + "metadata": {}, + "source": [ + "## Acknowledgements\n", + "\n", + "This notebook adapts material and design patterns from two original projects:\n", + "\n", + "- The ROCm perception and Genesis agent workshop in [`AI_LABS/vision_kernels_rocm/ROCm_Physical_AI_Agent_Workshop.ipynb`](https://gitenterprise.xilinx.com/ssw-mktg/igpu-training-env), from the [ssw-mktg/igpu-training-env](https://gitenterprise.xilinx.com/ssw-mktg/igpu-training-env) repository.\n", + "- The intent validation, local stop path, command dispatch, and behavior architecture from the [ahaidous/aai-vvla-pipeline](https://gitenterprise.xilinx.com/ahaidous/aai-vvla-pipeline) repository.\n", + "\n", + "We thank the contributors to both repositories for the original Physical AI workshop and VVLA pipeline material used as the foundation for this Genesis 1.3.1 simulation adaptation." + ] + }, + { + "cell_type": "markdown", + "id": "b23225f5", + "metadata": {}, + "source": [ + "---\n", + "\n", + "Copyright (C) 2026 Advanced Micro Devices, Inc. All rights reserved. Portions of this file consist of AI-generated content. \n", + "SPDX-License-Identifier: MIT" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/PhySim02_control_your_robot.ipynb b/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/PhySim02_control_your_robot.ipynb deleted file mode 100644 index 059b4ae..0000000 --- a/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/PhySim02_control_your_robot.ipynb +++ /dev/null @@ -1,378 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "c367b6a3-310e-4133-8b79-693d422f95f6", - "metadata": {}, - "source": [ - "# Control a Robot\n", - "\n", - "When gravity is enabled in the simulator, a robot arm will naturally collapse if no control is applied. This is not a bug, it's just physics. In this notebook, we’ll look at how to bring the robot arm under control.\n", - "\n", - "### What You Will Learn\n", - "\n", - "1. PD Control and Commanding Robots \\\n", - "Experience how Genesis provides built-in Proportional–Derivative (PD) controllers for applying stable joint-level control.\n", - "\n", - "2. Control Joints and DOFs \\\n", - "Use the **Franka Emika Panda** as an example: 7 revolute joints + 2 prismatic joints = 9 DOFs.\n", - "\n", - "3. Tuning Control Gains \\\n", - "Learn to configure proportional (`kp`), derivative (`kv`), and force-limit parameters for each DOF." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "77699ce4-db80-4b47-bfa2-bbbda26ab3f0", - "metadata": {}, - "outputs": [], - "source": [ - "# Suppress warning messages for clearer output\n", - "import os\n", - "import warnings\n", - "\n", - "os.environ[\"TI_LOG_LEVEL\"] = \"error\"\n", - "warnings.filterwarnings(\"ignore\")" - ] - }, - { - "cell_type": "markdown", - "id": "5c6fa926-026c-4ab3-8e01-af1b6a7d009d", - "metadata": {}, - "source": [ - "## Init and Create a Scene\n", - "\n", - "In Lab 1, we used default setting when creating thhe scene. Here we use more advanced settings.\n", - "\n", - "You can customize the **Simulator** and **Visualizer** through detailed configuration options when creating a scene.\n", - "\n", - "**Simulator options** define the physical simulation behavior. Common parameters include:\n", - "\n", - "* **dt** – Duration of each simulation step (in seconds)\n", - "* **gravity** – Gravity force vector (N/kg)\n", - "* **floor_height** – Ground plane height\n", - "\n", - "**Visualizer options** control the virtual camera and rendering behavior. Key parameters:\n", - "\n", - "* **camera_pos** – Initial position of the camera\n", - "* **camera_lookat** – Target point the camera focuses on\n", - "* **camera_fov** – Field of view (in degrees)\n", - "* **max_FPS** – Set max FPS.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f16475de", - "metadata": {}, - "outputs": [], - "source": [ - "import genesis as gs\n", - "import numpy as np\n", - "\n", - "########################## init ##########################\n", - "gs.init(backend=gs.amdgpu, theme=\"light\")\n", - "\n", - "########################## create a scene ##########################\n", - "scene = gs.Scene(\n", - " viewer_options=gs.options.ViewerOptions(\n", - " camera_pos=(0, -3.5, 2.5),\n", - " camera_lookat=(0.0, 0.0, 0.5),\n", - " camera_fov=30,\n", - " max_FPS=60,\n", - " ),\n", - " sim_options=gs.options.SimOptions(\n", - " dt=0.01,\n", - " ),\n", - " show_viewer=False,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "a2f702fe-eb80-4f76-9694-c2fed7e1408c", - "metadata": {}, - "source": [ - "## Add Entities and Build the Scene\n", - "\n", - "Just like what we did in Lab 1, we add a **plane**, an **arm**, and a **camera** to the scene, and then build the scene." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0eaa9cca", - "metadata": {}, - "outputs": [], - "source": [ - "########################## entities ##########################\n", - "plane = scene.add_entity(\n", - " gs.morphs.Plane(),\n", - ")\n", - "franka = scene.add_entity(\n", - " gs.morphs.MJCF(\n", - " file=\"xml/franka_emika_panda/panda.xml\",\n", - " ),\n", - ")\n", - "cam = scene.add_camera(\n", - " res=(640, 480),\n", - " pos=(3.5, 0.0, 2.5),\n", - " lookat=(0, 0, 0.5),\n", - " fov=30,\n", - " GUI=True,\n", - ")\n", - "\n", - "########################## build ##########################\n", - "scene.build()\n", - "print(\"Successfully built the scene.\")" - ] - }, - { - "cell_type": "markdown", - "id": "d11a3c68-fccb-4e09-916e-ca3a9a048861", - "metadata": {}, - "source": [ - "## Control Joints and DOFs\n", - "\n", - "In robotics, the terms **joint** and **degree of freedom (DOF)** are related but not quite the same. \n", - "\n", - "A joint is the physical connection between two parts (or links) of a robot that allows relative motion. For example, a hinge, a slider, or a ball-and-socket connection. Each joint enables certain types of movement.\n", - "\n", - "A degree of freedom (DOF), on the other hand, refers to the number of independent ways a joint (or the entire robot) can move. For instance, a revolute (rotational) joint has one DOF because it can rotate around a single axis, while a spherical joint has three DOFs, it can rotate around three perpendicular axes.\n", - "\n", - "![image.png](attachment:267b2468-7d14-45fd-a20d-3eb514f2c857.png)\n", - "\n", - "Take Franka Panda arm for example, it has 7 revolute joints in the arm and 2 prismatic joints in its gripper. Since each joint has only 1 DOF, the robot ends up with 9 DOFs in total." - ], - "attachments": { - "267b2468-7d14-45fd-a20d-3eb514f2c857.png": { - "image/png": "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" - } - } - }, - { - "cell_type": "code", - "execution_count": null, - "id": "19902729", - "metadata": {}, - "outputs": [], - "source": [ - "jnt_names = [\n", - " \"joint1\",\n", - " \"joint2\",\n", - " \"joint3\",\n", - " \"joint4\",\n", - " \"joint5\",\n", - " \"joint6\",\n", - " \"joint7\",\n", - " \"finger_joint1\",\n", - " \"finger_joint2\",\n", - "]\n", - "\n", - "dofs_idx_temp = [franka.get_joint(name).dofs_idx_local for name in jnt_names]\n", - "dofs_idx = [idx for sublist in dofs_idx_temp for idx in sublist]\n", - "\n", - "print(dofs_idx)" - ] - }, - { - "cell_type": "markdown", - "id": "c7ff8a1b-a745-4f58-87b0-36f3d7255ef8", - "metadata": {}, - "source": [ - "## Control Gains\n", - "\n", - "Control gains decide how much torque the controller applies to reduce errors in position or velocity. URDF/MJCF files usually provide default values, but manual tuning is often necessary for stable, realistic control.\n", - "\n", - "Genesis exposes three functions:\n", - "\n", - "* `.set_dofs_kp` — proportional gains\n", - "* `.set_dofs_kv` — derivative gains\n", - "* `.set_dofs_force_range` — safety limits on torque/force\n", - "\n", - "Together, `kp` and `kv` form the **PD controller**:\n", - "\n", - "For Franka, the arm joints (joint1–joint7) use higher gains for stiffness and precision, and the finger joints use lower gains so they feel softer and safer when grasping objects. \n", - "\n", - "A typical setup looks like this:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6151d602-0c5d-4e20-a41b-1b01864f0151", - "metadata": {}, - "outputs": [], - "source": [ - "############ Optional: set control gains ############\n", - "\n", - "# set positional gains\n", - "franka.set_dofs_kp(\n", - " kp=np.array([4500, 4500, 3500, 3500, 2000, 2000, 2000, 100, 100]),\n", - " dofs_idx_local=dofs_idx,\n", - ")\n", - "# set velocity gains\n", - "franka.set_dofs_kv(\n", - " kv=np.array([450, 450, 350, 350, 200, 200, 200, 10, 10]),\n", - " dofs_idx_local=dofs_idx,\n", - ")\n", - "# set force range for safety\n", - "franka.set_dofs_force_range(\n", - " lower=np.array([-87, -87, -87, -87, -12, -12, -12, -100, -100]),\n", - " upper=np.array([87, 87, 87, 87, 12, 12, 12, 100, 100]),\n", - " dofs_idx_local=dofs_idx,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "02417769-e654-438e-a721-a044d29282d7", - "metadata": {}, - "source": [ - "## Directly Setting DOF Positions\n", - "\n", - "It’s also possible to set DOF positions directly with `.set_dofs_position`. This instantly changes the robot state and bypasses physics. This can be useful for resets or demonstrations, but it may create unrealistic motion that violates physical laws.\n", - "\n", - "To avoid overly lengthy log information, we will not print the detailed logs here, we’ll use a progress bar to indicate the progress.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c89dd04d", - "metadata": {}, - "outputs": [], - "source": [ - "import logging\n", - "\n", - "from tqdm import tqdm\n", - "\n", - "# Set logger to warning to avoid log info.\n", - "gs.logger._logger.setLevel(logging.WARNING)\n", - "\n", - "# Camera recording\n", - "rgb, depth, segmentation, normal = cam.render(rgb=True, depth=True, segmentation=True, normal=True)\n", - "cam.start_recording(save_to_filename=\"Videos/video_02.mp4\", fps=60)\n", - "\n", - "# Hard reset\n", - "for i in tqdm(range(150), ncols=100):\n", - " if i < 50:\n", - " franka.set_dofs_position(np.array([1, 1, 0, 0, 0, 0, 0, 0.04, 0.04]), dofs_idx)\n", - " elif i < 100:\n", - " franka.set_dofs_position(np.array([-1, 0.8, 1, -2, 1, 0.5, -0.5, 0.04, 0.04]), dofs_idx)\n", - " else:\n", - " franka.set_dofs_position(np.array([0, 0, 0, 0, 0, 0, 0, 0, 0]), dofs_idx)\n", - " cam.render()\n", - " scene.step()" - ] - }, - { - "cell_type": "markdown", - "id": "3ae6b658-988e-4d06-a529-237fe6e2d3b1", - "metadata": {}, - "source": [ - "## Using the PD Controller\n", - "\n", - "To respect physics, use the `control_*` APIs instead. These send commands to the PD controller rather than overwriting the state. We can save it as a complete video and check the result." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "bc94198b-ffb3-4e5c-822f-4f42dbff1eb5", - "metadata": {}, - "outputs": [], - "source": [ - "# PD control\n", - "for i in tqdm(range(1250), ncols=100):\n", - " if i == 0:\n", - " franka.control_dofs_position(\n", - " np.array([1, 1, 0, 0, 0, 0, 0, 0.04, 0.04]),\n", - " dofs_idx,\n", - " )\n", - " elif i == 250:\n", - " franka.control_dofs_position(\n", - " np.array([-1, 0.8, 1, -2, 1, 0.5, -0.5, 0.04, 0.04]),\n", - " dofs_idx,\n", - " )\n", - " elif i == 500:\n", - " franka.control_dofs_position(\n", - " np.array([0, 0, 0, 0, 0, 0, 0, 0, 0]),\n", - " dofs_idx,\n", - " )\n", - " elif i == 750:\n", - " # control first dof with velocity, and the rest with position\n", - " franka.control_dofs_position(\n", - " np.array([0, 0, 0, 0, 0, 0, 0, 0, 0])[1:],\n", - " dofs_idx[1:],\n", - " )\n", - " franka.control_dofs_velocity(\n", - " np.array([1.0, 0, 0, 0, 0, 0, 0, 0, 0])[:1],\n", - " dofs_idx[:1],\n", - " )\n", - " elif i == 1000:\n", - " franka.control_dofs_force(\n", - " np.array([0, 0, 0, 0, 0, 0, 0, 0, 0]),\n", - " dofs_idx,\n", - " )\n", - "\n", - " cam.render()\n", - " scene.step()\n", - "\n", - "cam.stop_recording()" - ] - }, - { - "cell_type": "markdown", - "id": "d9a2ef33-c6ef-48d1-8173-832b24a260f2", - "metadata": {}, - "source": [ - "## Show the video\n", - "\n", - "In the video, you’ll first see three relatively rigid movements, these are discontinuous actions created using set_dofs_position. The following smoother, continuous motions are produced by the control_* APIs. Therefore, when creating robots in a virtual environment that behave according to physical laws, we usually use the control_* APIs to write the program." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b1a79fdc-c447-41ed-8319-0a5f19608b58", - "metadata": {}, - "outputs": [], - "source": [ - "from IPython.display import Video\n", - "\n", - "Video(url=\"Videos/video_02.mp4\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3ac5f258-35b3-499c-9293-f06cd1384464", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "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.11" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/README.md b/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/README.md new file mode 100644 index 0000000..b1111b7 --- /dev/null +++ b/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/README.md @@ -0,0 +1,336 @@ +# Genesis Simulation Labs + +This directory contains six Jupyter labs for learning Genesis physical +simulation on AMD GPUs: + +1. `GS01_hello_genesis.ipynb` — create a scene and load a robot +2. `GS02_control_your_robot.ipynb` — joint and PD control +3. `GS03_motion_planning.ipynb` — IK, motion planning, and grasping +4. `GS04_parallel_simulation.ipynb` — parallel GPU environments +5. `GS05_perception_with_rocm.ipynb` — ROCm vision and tactile perception +6. `GS06_language_guided_agent.ipynb` — tactile-gated, language-guided Physical AI agent + +The Docker image uses `genesis-world==1.3.1` and includes the ROCm runtime, +PyTorch, JupyterLab, the Franka Panda assets, and all notebook dependencies. + +## Requirements + +- Linux with an AMD GPU supported by ROCm +- A working ROCm host driver +- Docker +- The following GPU device files: + +```bash +ls -l /dev/kfd /dev/dri +``` + +The command should show `/dev/kfd` and at least one render device under +`/dev/dri`. + +## 1. Build the image + +Run these commands from this directory: + +```bash +cd /path/to/aup-teaching-labs/projects/Physical-AI/Physical-Simulation/Genesis-Simulation + +docker build -t auplc-physisim:physicalai-genesis . +``` + +The first build may take several minutes and use significant disk space because +Genesis and its rendering dependencies are included. + +## 2. Start JupyterLab + +```bash +docker run --rm -it \ + --name physisim-jupyter \ + --device=/dev/kfd \ + --device=/dev/dri \ + --group-add video \ + --ipc=host \ + --security-opt seccomp=unconfined \ + -p 127.0.0.1:8888:8888 \ + -v "$PWD:/opt/workspace/PhySim" \ + --entrypoint python3 \ + auplc-physisim:physicalai-genesis \ + -m jupyterlab \ + --ip=0.0.0.0 \ + --port=8888 \ + --no-browser \ + --ServerApp.token='' \ + --ServerApp.password='' +``` + +Open the following URL: + + + +The current directory is mounted at `/opt/workspace/PhySim`, so notebook edits +and generated videos remain on the host after the container stops. + +> The image's default `/entrypoint.sh` is intended for AUP Learning Cloud's +> JupyterHub deployment. Standalone Docker use must override it with +> `--entrypoint python3` as shown above. + +## 3. Run the labs + +Open the notebooks in numerical order, starting with GS01. Run cells from +top to bottom. + +Genesis scenes should be built only once in a notebook kernel. If you need to +rerun a scene-creation cell, select **Kernel → Restart Kernel and Run All +Cells**. + +Generated files are written to: + +- `Videos/video_05.mp4` — raw tactile-grasp camera recording +- `Videos/video_05_hud.mp4` — vision, tactile, and GPU telemetry HUD +- `Videos/video_05_live_hud.mp4` — exported GS05 widget interaction +- `Videos/video_06.mp4` — raw language-guided mission recording +- `Videos/video_06_hud.mp4` — AI plan, execution, telemetry, and tactile HUD +- `Videos/video_06_live_hud.mp4` — exported GS06 widget interaction +- `Videos/interactive_session.mp4` — optional GS06 typed-command HUD +- `Artifacts/` — perception figures + +The reusable OpenCV compositor, ffmpeg writer, and AMD GPU telemetry reader live +in `helpers/physisim_hud.py`. They consume notebook-owned NumPy arrays and +structured receipts; they do not build Genesis scenes, control the robot, or +start an LLM server. + +### GS05 live simulation + +After running GS05 through the **Live simulation interface** section, use +the notebook buttons in this order: + +1. **Reset** +2. **Approach** +3. **Close to Secure** +4. **Lift** +5. **Lower** +6. **Release** + +The widget updates the Genesis scene, GPU telemetry, six vision outputs, and +both 8×8 tactile heatmaps after each group of simulation steps. The contact +threshold and secure-taxel count can be adjusted with sliders. **Export HUD +MP4** writes the captured interaction to `Videos/video_05_live_hud.mp4`; the +regular top-to-bottom path keeps its own `video_05_hud.mp4`. Press **Shutdown +HUD** when finished to stop the telemetry thread. + +Before pressing **Reset**, choose either the reproducible **Default** cube +layout or **Random (seeded)**. A seeded layout samples collision-free cube +positions inside the reachable workspace; using the same seed reproduces the +same positions. Reset also returns the Franka to its saved upright pose. + +### GS06 live language-agent interface + +After running GS06 through the **Live language-agent interface** section: + +1. Enter a command such as `pick green` or `stack blue on red`. +2. Select the **Offline** or **LLM endpoint** planner. +3. Press **Run Command**. +4. Watch the validated plan, execution stages, scene, GPU telemetry, and tactile + gate update in one AI BRAIN HUD. + +The interface also provides **Home**, **Stop**, **Reset Scene**, and **Shutdown +HUD** buttons. +Its tactile sliders change the actual secure-grasp decision used by +`close_gripper()`. **Export HUD MP4** writes captured live commands to +`Videos/video_06_live_hud.mp4` without overwriting the Run All mission video. +**Reset Scene** applies the selected Default or seeded-random cube layout and +returns the Franka to its saved upright pose. + +### Start the optional LLM endpoint + +Offline mode works without any model. LLM mode requires a llama.cpp +`llama-server` binary and a GGUF model inside the same JupyterHub container. +The Docker image already contains a pinned Vulkan `llama-server` at +`/opt/llama/bin/llama-server`. + +In GS06, press **Download Llama 3.2 3B GGUF (2.02 GB)**. The opt-in cell +downloads a pinned model from `bartowski/Llama-3.2-3B-Instruct-GGUF`, verifies +its SHA256, and stores it at: + +```text +/opt/workspace/PhySim/models/Llama-3.2-3B-Instruct-Q4_K_M.gguf +``` + +The workspace mount keeps the model after the container stops. `models/` and +`*.gguf` are excluded from Git and Docker build context. + +Llama 3.2 weights are subject to the +[Meta Llama 3.2 Community License](https://www.llama.com/llama3_2/license/), +not this course's MIT license. Review the applicable terms before downloading +or using the model. + +After the download completes, open **JupyterLab → File → New → Terminal**: + +```bash +bash helpers/start_llama_server.sh +``` + +Keep the Terminal open and verify the endpoint from another Terminal: + +```bash +curl http://127.0.0.1:8081/health +``` + +Rerun GS06's LLM health-check cell, then select **LLM endpoint**. Press +`Ctrl+C` in the server Terminal to stop it. Advanced users may override +`LLAMA_SERVER_BIN` or `LLAMA_MODEL_PATH` before running the helper. + +### Optional typed-command demo + +After running GS06 through its function-definition cells, call: + +```python +run_typed_demo(mode="offline") +``` + +Try `pick green`, `stack blue on red`, `home`, or `stop`. Enter `quit` to +finish and save `Videos/interactive_session.mp4`. The function is not called by +default, so automated notebook execution does not block on keyboard input. + +## 4. Stop the container + +Press `Ctrl+C` in the terminal running Docker, or run: + +```bash +docker stop physisim-jupyter +``` + +The container is removed automatically because it was started with `--rm`. + +## Automated notebook test + +The following example executes GS05 non-interactively. A successful run +exits with status code `0`. + +```bash +docker run --rm \ + --device=/dev/kfd \ + --device=/dev/dri \ + --group-add video \ + --ipc=host \ + --security-opt seccomp=unconfined \ + -v "$PWD:/opt/workspace/PhySim" \ + --entrypoint jupyter \ + auplc-physisim:physicalai-genesis \ + nbconvert \ + --to notebook \ + --execute \ + --ExecutePreprocessor.timeout=1200 \ + --output /tmp/GS05-executed.ipynb \ + /opt/workspace/PhySim/GS05_perception_with_rocm.ipynb +``` + +Run the same command for GS06 with a longer timeout: + +```bash +docker run --rm \ + --device=/dev/kfd \ + --device=/dev/dri \ + --group-add video \ + --ipc=host \ + --security-opt seccomp=unconfined \ + -v "$PWD:/opt/workspace/PhySim" \ + --entrypoint jupyter \ + auplc-physisim:physicalai-genesis \ + nbconvert \ + --to notebook \ + --execute \ + --ExecutePreprocessor.timeout=2400 \ + --output /tmp/GS06-executed.ipynb \ + /opt/workspace/PhySim/GS06_language_guided_agent.ipynb +``` + +## Third-party assets and licenses + +- Course notebooks and helper code: MIT +- Franka Emika Panda MJCF/assets: Apache-2.0; see + `xml/franka_emika_panda/LICENSE` +- Llama 3.2 model weights: Meta Llama 3.2 Community License + +## Troubleshooting + +### `ModuleNotFoundError: No module named 'huggingface_hub'` + +The running JupyterHub container predates the latest course image. Rerun the +GS06 download cell: it installs `huggingface-hub` into the current kernel +when missing. Alternatively, use a Terminal: + +```bash +python -m pip install --user "huggingface-hub>=0.34,<2" +``` + +Then rerun the model-download cell. Newly built course images already include +the package. + +### `Missing required environment $JUPYTERHUB_SERVICE_URL` + +The image was started with its JupyterHub entrypoint. Use the standalone +JupyterLab command above, including `--entrypoint python3`. + +### PyTorch reports that no GPU is available + +Confirm the host devices exist and that both are passed to Docker: + +```bash +ls -l /dev/kfd /dev/dri + +docker run --rm \ + --device=/dev/kfd \ + --device=/dev/dri \ + --group-add video \ + --entrypoint python3 \ + auplc-physisim:physicalai-genesis \ + -c "import torch; print(torch.cuda.is_available()); print(torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'No GPU')" +``` + +### Port 8888 is already in use + +Map another host port, for example: + +```bash +-p 127.0.0.1:8899:8888 +``` + +Then open . + +### Accessing a remote machine + +The launch command binds JupyterLab to localhost for safety. Forward the port +over SSH: + +```bash +ssh -L 8888:localhost:8888 user@remote-host +``` + +Then open on your local computer. + +## Security note + +The example disables the Jupyter token and password, but publishes the port +only on `127.0.0.1`. Do not expose this token-free server directly to an +untrusted network. + +`--ipc=host` and `--security-opt seccomp=unconfined` reduce container isolation +and are used here for ROCm/Jupyter compatibility on the lab machine. Do not +copy these settings into a multi-tenant or production deployment without a +separate security review. + +## Workspace permissions and GPU sharing + +The bind mount replaces the notebooks, helpers, XML assets, and model directory +that were copied into the image. The host course directory must be writable by +the container's `jovyan` user (UID 1000), otherwise model downloads and videos +will fail: + +```bash +sudo chown -R 1000:1000 /path/to/Genesis-Simulation +``` + +Genesis uses the AMDGPU/ROCm backend while the bundled llama-server uses +Vulkan/RADV. They share the same physical GPU and VRAM. If either process runs +out of memory, reduce `LLAMA_GPU_LAYERS`, stop other GPU workloads, or run the +LLM and simulation separately. diff --git a/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/helpers/__init__.py b/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/helpers/__init__.py new file mode 100644 index 0000000..fcf8542 --- /dev/null +++ b/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/helpers/__init__.py @@ -0,0 +1,21 @@ +# Copyright (C) 2026 Advanced Micro Devices, Inc. All rights reserved. +# SPDX-License-Identifier: MIT + +"""Reusable display helpers for the Genesis simulation labs.""" + +from .physisim_hud import ( + FFmpegHUDWriter, + GPUMonitor, + build_vision_thumbnails, + compose_hud_frame, +) +from .physisim_widget import LiveAgentController, LiveHUDController + +__all__ = [ + "FFmpegHUDWriter", + "GPUMonitor", + "LiveAgentController", + "LiveHUDController", + "build_vision_thumbnails", + "compose_hud_frame", +] diff --git a/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/helpers/physisim_hud.py b/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/helpers/physisim_hud.py new file mode 100644 index 0000000..c1415e1 --- /dev/null +++ b/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/helpers/physisim_hud.py @@ -0,0 +1,731 @@ +# Copyright (C) 2026 Advanced Micro Devices, Inc. All rights reserved. +# SPDX-License-Identifier: MIT + +"""OpenCV HUD and recording helpers for GS05 and GS06. + +The module is intentionally independent of Genesis and the planner. Notebooks +pass ordinary NumPy images and structured receipts into :func:`compose_hud_frame`. +""" + +from __future__ import annotations + +import collections +import copy +import json +import os +import shutil +import subprocess +import tempfile +import textwrap +import threading +import time +from pathlib import Path +from typing import IO, Any, Mapping, Sequence + +import cv2 +import numpy as np + +COMP_W = 1440 +COMP_H = 816 +MAIN_W = 820 +PANEL_W = COMP_W - MAIN_W + +THEME = { + "background": (18, 15, 16), + "panel": (34, 29, 30), + "panel_alt": (45, 38, 39), + "border": (80, 68, 70), + "text": (235, 235, 235), + "muted": (165, 155, 158), + "amd_red": (34, 34, 220), + "orange": (0, 155, 255), + "green": (105, 220, 80), + "blue": (235, 155, 70), + "danger": (80, 80, 235), +} + + +def _as_uint8_rgb(image: Any) -> np.ndarray: + arr = np.asarray(image) + if arr.ndim == 4 and arr.shape[0] == 1: + arr = arr[0] + if arr.ndim == 2: + arr = np.repeat(arr[..., None], 3, axis=-1) + if arr.shape[-1] > 3: + arr = arr[..., :3] + if arr.dtype != np.uint8: + arr = arr.astype(np.float32) + if arr.size and float(np.nanmax(arr)) <= 1.5: + arr *= 255.0 + arr = np.nan_to_num(arr, nan=0.0, posinf=255.0, neginf=0.0) + arr = np.clip(arr, 0, 255).astype(np.uint8) + return np.ascontiguousarray(arr) + + +def _fit_bgr(image: np.ndarray, width: int, height: int) -> np.ndarray: + """Fit a BGR image without changing its aspect ratio.""" + + source_h, source_w = image.shape[:2] + scale = min(width / source_w, height / source_h) + resized_w = max(1, int(source_w * scale)) + resized_h = max(1, int(source_h * scale)) + interpolation = cv2.INTER_AREA if scale < 1.0 else cv2.INTER_LINEAR + resized = cv2.resize(image, (resized_w, resized_h), interpolation=interpolation) + canvas = np.full((height, width, 3), THEME["background"], dtype=np.uint8) + x = (width - resized_w) // 2 + y = (height - resized_h) // 2 + canvas[y : y + resized_h, x : x + resized_w] = resized + return canvas + + +def _letterbox_bgr(rgb: Any, width: int, height: int) -> np.ndarray: + image = cv2.cvtColor(_as_uint8_rgb(rgb), cv2.COLOR_RGB2BGR) + return _fit_bgr(image, width, height) + + +def _put( + image: np.ndarray, + text: str, + x: int, + y: int, + scale: float = 0.45, + color: tuple[int, int, int] | None = None, + thickness: int = 1, +) -> None: + cv2.putText( + image, + str(text), + (int(x), int(y)), + cv2.FONT_HERSHEY_SIMPLEX, + scale, + color or THEME["text"], + thickness, + cv2.LINE_AA, + ) + + +def _panel( + image: np.ndarray, + x0: int, + y0: int, + x1: int, + y1: int, + label: str | None = None, + accent: tuple[int, int, int] | None = None, +) -> None: + cv2.rectangle(image, (x0, y0), (x1, y1), THEME["panel"], -1) + cv2.rectangle(image, (x0, y0), (x1, y1), THEME["border"], 1) + if label: + cv2.rectangle(image, (x0, y0), (x0 + 4, y1), accent or THEME["orange"], -1) + _put(image, label.upper(), x0 + 12, y0 + 20, 0.42, THEME["muted"]) + + +def _bar( + image: np.ndarray, + x: int, + y: int, + width: int, + pct: float, + label: str, + color: tuple[int, int, int], +) -> None: + pct = float(np.clip(pct, 0, 100)) + _put(image, f"{label} {pct:5.1f}%", x, y - 5, 0.38, THEME["text"]) + cv2.rectangle(image, (x, y), (x + width, y + 9), THEME["panel_alt"], -1) + cv2.rectangle(image, (x, y), (x + int(width * pct / 100.0), y + 9), color, -1) + + +def _sparkline( + image: np.ndarray, + values: Sequence[float], + x: int, + y: int, + width: int, + height: int, + color: tuple[int, int, int], +) -> None: + if len(values) < 2: + return + vals = np.clip(np.asarray(values, dtype=np.float32), 0, 100) + xs = np.linspace(x, x + width, vals.size) + ys = y + height - vals / 100.0 * height + points = np.stack([xs, ys], axis=-1).astype(np.int32) + cv2.polylines(image, [points], False, color, 1, cv2.LINE_AA) + + +def _truncate(value: Any, limit: int) -> str: + if isinstance(value, str): + text = value + else: + try: + text = json.dumps(value, ensure_ascii=False) + except TypeError: + text = str(value) + text = " ".join(text.split()) + return text if len(text) <= limit else text[: limit - 1] + "…" + + +def _put_wrapped( + image: np.ndarray, + value: Any, + x: int, + y: int, + *, + width: int, + max_lines: int, + scale: float = 0.40, + line_height: int = 18, + color: tuple[int, int, int] | None = None, + thickness: int = 1, +) -> None: + """Render compact wrapped text inside a fixed-width HUD region.""" + + text = _truncate(value, max(width * max_lines * 2, width)) + lines = textwrap.wrap( + text, + width=width, + break_long_words=True, + break_on_hyphens=False, + ) or [""] + if len(lines) > max_lines: + lines = lines[:max_lines] + lines[-1] = _truncate(lines[-1], max(1, width - 1)) + for index, line in enumerate(lines): + _put( + image, + line, + x, + y + index * line_height, + scale, + color, + thickness, + ) + + +def _image_thumbnail(key: str, value: Any, width: int, height: int) -> np.ndarray: + arr = np.asarray(value) + if arr.ndim == 4 and arr.shape[0] == 1: + arr = arr[0] + if arr.ndim == 3 and arr.shape[-1] == 1: + arr = arr[..., 0] + if arr.ndim == 2: + gray = arr + if gray.dtype != np.uint8: + gray = np.nan_to_num(gray.astype(np.float32)) + if gray.size and float(gray.max()) <= 1.5: + gray *= 255.0 + gray = np.clip(gray, 0, 255).astype(np.uint8) + cmap = cv2.COLORMAP_INFERNO if key in {"depth", "sobel"} else cv2.COLORMAP_BONE + bgr = cv2.applyColorMap(gray, cmap) + else: + rgb = _as_uint8_rgb(arr) + bgr = cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR) + return _fit_bgr(bgr, width, height) + + +def _tactile_thumbnail(displacement: Any, width: int, height: int) -> np.ndarray: + if displacement is None: + return np.zeros((height, width, 3), dtype=np.uint8) + arr = np.asarray(displacement, dtype=np.float32) + if arr.ndim == 4 and arr.shape[0] == 1: + arr = arr[0] + magnitude = np.linalg.norm(arr, axis=-1) + peak = float(magnitude.max()) if magnitude.size else 0.0 + scaled = magnitude / max(peak, 1e-9) * 255.0 + heatmap = cv2.applyColorMap(scaled.astype(np.uint8), cv2.COLORMAP_MAGMA) + return cv2.resize(heatmap, (width, height), interpolation=cv2.INTER_NEAREST) + + +def build_vision_thumbnails( + rgb: Any, + depth: Any, + segmentation: Any, + normal: Any, +) -> dict[str, np.ndarray]: + """Build current CPU-side HUD thumbnails from one Genesis render.""" + + rgb_u8 = _as_uint8_rgb(rgb) + depth_arr = np.asarray(depth, dtype=np.float32).squeeze() + seg_ids = np.asarray(segmentation, dtype=np.int32).squeeze() + normal_arr = np.asarray(normal).squeeze() + + valid = np.isfinite(depth_arr) & (depth_arr > 0) + depth_u8 = np.zeros(depth_arr.shape, dtype=np.uint8) + if valid.any(): + near, far = np.quantile(depth_arr[valid], (0.02, 0.98)) + scaled = np.clip((depth_arr - near) / max(float(far - near), 1e-6), 0, 1) + depth_u8[valid] = (scaled[valid] * 255).astype(np.uint8) + + if normal_arr.dtype == np.uint8 or (normal_arr.size and float(normal_arr.max()) > 1.5): + normal_u8 = np.clip(normal_arr, 0, 255).astype(np.uint8) + elif normal_arr.size and float(normal_arr.min()) < -0.05: + normal_u8 = np.clip((normal_arr + 1.0) * 127.5, 0, 255).astype(np.uint8) + else: + normal_u8 = np.clip(normal_arr * 255.0, 0, 255).astype(np.uint8) + normal_u8[seg_ids == 0] = 0 + + segmentation_u8 = np.stack( + [ + (seg_ids * 37 + 11) % 256, + (seg_ids * 79 + 43) % 256, + (seg_ids * 131 + 97) % 256, + ], + axis=-1, + ).astype(np.uint8) + segmentation_u8[seg_ids == 0] = 0 + + gray = cv2.cvtColor(rgb_u8, cv2.COLOR_RGB2GRAY) + blur = cv2.GaussianBlur(gray, (5, 5), 1.4) + edge_x = cv2.Sobel(blur, cv2.CV_32F, 1, 0, ksize=3) + edge_y = cv2.Sobel(blur, cv2.CV_32F, 0, 1, ksize=3) + magnitude = cv2.magnitude(edge_x, edge_y) + sobel = np.clip(magnitude / max(float(magnitude.max()), 1e-6) * 255, 0, 255).astype(np.uint8) + + return { + "depth": depth_u8, + "normal": normal_u8, + "segmentation": segmentation_u8, + "gray": gray, + "blur": blur, + "sobel": sobel, + } + + +def _render_main( + scene_rgb: Any, + status: str, + tactile: Mapping[str, Any] | None, + title: str, +) -> np.ndarray: + canvas = np.full((COMP_H, MAIN_W, 3), THEME["background"], dtype=np.uint8) + cv2.rectangle(canvas, (0, 0), (MAIN_W, 64), THEME["panel"], -1) + cv2.rectangle(canvas, (0, 0), (12, 64), THEME["amd_red"], -1) + _put(canvas, title, 28, 31, 0.72, THEME["text"], 2) + _put(canvas, "Genesis 1.3.1 · AMD ROCm", 29, 54, 0.42, THEME["muted"]) + _put(canvas, time.strftime("%H:%M:%S"), MAIN_W - 112, 38, 0.48, THEME["muted"]) + + scene_y = 76 + scene_h = COMP_H - 154 + scene = _letterbox_bgr(scene_rgb, MAIN_W - 32, scene_h) + canvas[scene_y : scene_y + scene_h, 16 : 16 + scene.shape[1]] = scene + cv2.rectangle( + canvas, + (15, scene_y - 1), + (MAIN_W - 16, scene_y + scene_h), + THEME["border"], + 1, + ) + + tactile = dict(tactile or {}) + secure = bool(tactile.get("secure", False)) + contact = int(tactile.get("n_contact", 0)) + total = int(tactile.get("n_taxels", 128)) + force = float(tactile.get("grip_force_N", 0.0)) + banner_color = THEME["green"] if secure else THEME["orange"] + banner = "GRIP SECURE" if secure else "TACTILE MONITORING" + cv2.rectangle(canvas, (28, 90), (390, 134), THEME["panel"], -1) + cv2.rectangle(canvas, (28, 90), (34, 134), banner_color, -1) + _put(canvas, banner, 45, 110, 0.48, banner_color, 2) + _put( + canvas, + f"K4 {force:.2f} N contact {contact}/{total}", + 45, + 128, + 0.40, + THEME["text"], + ) + + cv2.rectangle(canvas, (16, COMP_H - 54), (MAIN_W - 16, COMP_H - 12), THEME["panel"], -1) + _put(canvas, _truncate(status or "Ready", 90), 29, COMP_H - 27, 0.52, THEME["text"], 1) + return canvas + + +def _render_brain( + gpu: Mapping[str, Any] | None, + user_input: str, + planner_raw: Any, + plan: Any, + stages: Sequence[Any] | None, + thumbnails: Mapping[str, Any] | None, + tactile: Mapping[str, Any] | None, + left_tactile: Any, + right_tactile: Any, +) -> np.ndarray: + panel = np.full((COMP_H, PANEL_W, 3), THEME["background"], dtype=np.uint8) + _put(panel, "AI BRAIN", 18, 32, 0.78, THEME["text"], 2) + _put(panel, "PLAN · PERCEPTION · ACTION · VERIFY", 19, 52, 0.38, THEME["muted"]) + + gpu = dict(gpu or {}) + _panel(panel, 14, 62, PANEL_W - 14, 150, "AMD GPU telemetry", THEME["amd_red"]) + gpu_pct = float(gpu.get("gpu_pct", 0.0)) + vram_pct = float(gpu.get("vram_pct", 0.0)) + bar_w = 245 + _bar(panel, 30, 98, bar_w, gpu_pct, "GPU", THEME["amd_red"]) + _bar(panel, 330, 98, bar_w, vram_pct, "VRAM", THEME["blue"]) + _sparkline(panel, gpu.get("gpu_history", []), 30, 116, bar_w, 22, THEME["amd_red"]) + _sparkline(panel, gpu.get("vram_history", []), 330, 116, bar_w, 22, THEME["blue"]) + _put(panel, f"{float(gpu.get('temp_c', 0.0)):.0f} C", PANEL_W - 70, 84, 0.42, THEME["muted"]) + _put(panel, _truncate(gpu.get("source", "not available"), 66), 30, 144, 0.34, THEME["muted"]) + + _panel(panel, 14, 158, PANEL_W - 14, 238, "User input / planner", THEME["orange"]) + _put_wrapped( + panel, + "> " + _truncate(user_input or "(not provided)", 88), + 30, + 190, + width=82, + max_lines=1, + scale=0.46, + color=THEME["text"], + thickness=1, + ) + _put_wrapped( + panel, + planner_raw or "(offline / no raw model output)", + 30, + 216, + width=94, + max_lines=1, + scale=0.38, + color=THEME["muted"], + ) + + _panel(panel, 14, 246, PANEL_W - 14, 334, "Validated plan / execution", THEME["green"]) + _put_wrapped( + panel, + plan or "(no plan)", + 30, + 278, + width=92, + max_lines=2, + scale=0.40, + line_height=18, + color=THEME["text"], + ) + stage_lines = [] + for stage in list(stages or [])[-2:]: + if isinstance(stage, Mapping): + label = stage.get("stage", "stage") + ok = stage.get("success", stage.get("result", {}).get("success", "")) + stage_lines.append(f"{label}: {ok}") + else: + stage_lines.append(str(stage)) + for index, line in enumerate(stage_lines): + _put(panel, _truncate(line, 82), 30, 315 + index * 16, 0.35, THEME["muted"]) + + _panel(panel, 14, 342, PANEL_W - 14, COMP_H - 14, "Live ROCm outputs", THEME["blue"]) + _put(panel, time.strftime("%H:%M:%S"), PANEL_W - 93, 362, 0.36, THEME["muted"]) + keys = ["depth", "normal", "segmentation", "gray", "blur", "sobel"] + labels = ["K1 depth", "K2 normal", "K3 segmentation", "K5 grayscale", "K6 Gaussian blur", "K7 Sobel edges"] + thumbs = dict(thumbnails or {}) + thumb_w = 180 + thumb_h = 135 + start_x = 28 + start_y = 372 + gap_x = 12 + gap_y = 34 + for index, (key, label) in enumerate(zip(keys, labels)): + col = index % 3 + row = index // 3 + x = start_x + col * (thumb_w + gap_x) + y = start_y + row * (thumb_h + gap_y) + if key in thumbs: + image = _image_thumbnail(key, thumbs[key], thumb_w, thumb_h) + else: + image = np.zeros((thumb_h, thumb_w, 3), dtype=np.uint8) + panel[y : y + thumb_h, x : x + thumb_w] = image + cv2.rectangle(panel, (x, y), (x + thumb_w, y + thumb_h), THEME["border"], 1) + _put(panel, label, x, y + thumb_h + 18, 0.42, THEME["text"], 1) + + tactile = dict(tactile or {}) + tactile_y = 712 + tactile_size = 86 + _put(panel, "K4 left tactile", 28, tactile_y - 8, 0.40, THEME["text"]) + _put(panel, "K4 right tactile", 128, tactile_y - 8, 0.40, THEME["text"]) + left_image = _tactile_thumbnail(left_tactile, tactile_size, tactile_size) + right_image = _tactile_thumbnail(right_tactile, tactile_size, tactile_size) + panel[tactile_y : tactile_y + tactile_size, 28 : 28 + tactile_size] = left_image + panel[tactile_y : tactile_y + tactile_size, 128 : 128 + tactile_size] = right_image + cv2.rectangle(panel, (28, tactile_y), (28 + tactile_size, tactile_y + tactile_size), THEME["border"], 1) + cv2.rectangle(panel, (128, tactile_y), (128 + tactile_size, tactile_y + tactile_size), THEME["border"], 1) + tactile_color = THEME["green"] if tactile.get("secure") else THEME["orange"] + _put( + panel, + f"secure {bool(tactile.get('secure', False))}", + 242, + 730, + 0.48, + tactile_color, + 2, + ) + _put( + panel, + f"contact {int(tactile.get('n_contact', 0))}/{int(tactile.get('n_taxels', 128))}", + 242, + 754, + 0.42, + THEME["text"], + ) + _put( + panel, + f"peak {float(tactile.get('peak_mm', 0.0)):.3f} mm " + f"force {float(tactile.get('grip_force_N', 0.0)):.2f} N", + 242, + 779, + 0.42, + THEME["text"], + ) + return panel + + +def compose_hud_frame( + scene_rgb: Any, + *, + status: str = "", + title: str = "PHYSim · Physical AI", + user_input: str = "", + planner_raw: Any = None, + plan: Any = None, + stages: Sequence[Any] | None = None, + thumbnails: Mapping[str, Any] | None = None, + tactile: Mapping[str, Any] | None = None, + left_tactile: Any = None, + right_tactile: Any = None, + gpu: Mapping[str, Any] | None = None, +) -> np.ndarray: + """Compose a 1440×816 BGR frame from notebook-owned state.""" + + left = _render_main(scene_rgb, status, tactile, title) + right = _render_brain( + gpu, + user_input, + planner_raw, + plan, + stages, + thumbnails, + tactile, + left_tactile, + right_tactile, + ) + frame = np.concatenate([left, right], axis=1) + if frame.shape != (COMP_H, COMP_W, 3): + raise ValueError(f"unexpected HUD shape: {frame.shape}") + return np.ascontiguousarray(frame, dtype=np.uint8) + + +class FFmpegHUDWriter: + """Stream BGR frames to an H.264 MP4 using the system ffmpeg.""" + + def __init__( + self, + path: str | os.PathLike[str], + *, + fps: int = 25, + width: int = COMP_W, + height: int = COMP_H, + ) -> None: + self.path = str(path) + self.fps = int(fps) + self.width = int(width) + self.height = int(height) + self._process: subprocess.Popen[bytes] | None = None + self._stderr_log: IO[bytes] | None = None + self.frames_written = 0 + + def open(self) -> "FFmpegHUDWriter": + Path(self.path).parent.mkdir(parents=True, exist_ok=True) + ffmpeg = shutil.which("ffmpeg") + if ffmpeg is None: + raise FileNotFoundError("ffmpeg is not available in PATH") + command = [ + ffmpeg, + "-loglevel", + "error", + "-y", + "-f", + "rawvideo", + "-pix_fmt", + "bgr24", + "-s", + f"{self.width}x{self.height}", + "-r", + str(self.fps), + "-i", + "-", + "-an", + "-c:v", + "libx264", + "-preset", + "veryfast", + "-pix_fmt", + "yuv420p", + self.path, + ] + # A pipe would block ffmpeg once the 64 KiB buffer fills, because stderr + # is only drained on failure or shutdown. A temp file has no such limit. + self._stderr_log = tempfile.TemporaryFile() + self._process = subprocess.Popen( + command, + stdin=subprocess.PIPE, + stderr=self._stderr_log, + ) + return self + + def _read_stderr(self) -> str: + if self._stderr_log is None: + return "" + try: + self._stderr_log.seek(0) + return self._stderr_log.read().decode("utf-8", errors="replace") + except ValueError: + return "" + + def write(self, frame: Any) -> None: + if self._process is None: + self.open() + arr = np.ascontiguousarray(frame, dtype=np.uint8) + if arr.shape != (self.height, self.width, 3): + raise ValueError( + f"frame shape {arr.shape} does not match {(self.height, self.width, 3)}" + ) + if self._process is None or self._process.stdin is None: + raise RuntimeError("ffmpeg writer is not available") + try: + self._process.stdin.write(arr.tobytes()) + except BrokenPipeError as error: + stderr = self._read_stderr() + raise RuntimeError(f"ffmpeg stopped while writing a frame: {stderr}") from error + self.frames_written += 1 + + def close(self) -> None: + if self._process is None: + return + if self._process.stdin is not None: + self._process.stdin.close() + try: + return_code = self._process.wait(timeout=30) + except subprocess.TimeoutExpired: + self._process.kill() + return_code = self._process.wait(timeout=5) + stderr = self._read_stderr() + self._process = None + if self._stderr_log is not None: + self._stderr_log.close() + self._stderr_log = None + if return_code != 0: + raise RuntimeError(f"ffmpeg exited with status {return_code}: {stderr}") + + def __enter__(self) -> "FFmpegHUDWriter": + return self.open() + + def __exit__(self, exc_type, exc, traceback) -> None: + self.close() + + def __del__(self) -> None: + if self._process is not None: + try: + self.close() + except Exception: + pass + + +class GPUMonitor: + """Background AMD GPU telemetry reader with a thread-safe snapshot API.""" + + def __init__(self, interval: float = 0.5) -> None: + self.interval = float(interval) + self._lock = threading.Lock() + self._stop = threading.Event() + self._thread: threading.Thread | None = None + self._card = self._find_card() + self._state: dict[str, Any] = { + "available": False, + "source": "not available", + "gpu_pct": 0.0, + "vram_used_mb": 0.0, + "vram_total_mb": 0.0, + "vram_pct": 0.0, + "temp_c": 0.0, + "gpu_history": collections.deque(maxlen=60), + "vram_history": collections.deque(maxlen=60), + } + + @staticmethod + def _find_card() -> Path | None: + drm = Path("/sys/class/drm") + if not drm.exists(): + return None + for card in sorted(drm.glob("card[0-9]*")): + vendor = card / "device" / "vendor" + try: + if vendor.read_text().strip().lower() == "0x1002": + return card + except OSError: + continue + return None + + @staticmethod + def _read_number(path: Path) -> float: + try: + return float(path.read_text().strip()) + except (OSError, ValueError): + return 0.0 + + def _sample(self) -> None: + if self._card is None: + return + device = self._card / "device" + gpu_pct = self._read_number(device / "gpu_busy_percent") + used = self._read_number(device / "mem_info_vram_used") / (1024**2) + total = self._read_number(device / "mem_info_vram_total") / (1024**2) + vram_pct = used / total * 100.0 if total else 0.0 + temp_c = 0.0 + for temp in sorted(device.glob("hwmon/hwmon*/temp1_input")): + temp_c = self._read_number(temp) / 1000.0 + if temp_c: + break + with self._lock: + self._state.update( + { + "available": True, + "source": str(device), + "gpu_pct": gpu_pct, + "vram_used_mb": used, + "vram_total_mb": total, + "vram_pct": vram_pct, + "temp_c": temp_c, + } + ) + self._state["gpu_history"].append(gpu_pct) + self._state["vram_history"].append(vram_pct) + + def _run(self) -> None: + while not self._stop.is_set(): + self._sample() + self._stop.wait(self.interval) + + def start(self) -> "GPUMonitor": + if self._thread is None or not self._thread.is_alive(): + self._stop.clear() + self._thread = threading.Thread(target=self._run, daemon=True) + self._thread.start() + return self + + def stop(self) -> None: + self._stop.set() + if self._thread is not None: + self._thread.join(timeout=2) + self._thread = None + + def snapshot(self) -> dict[str, Any]: + with self._lock: + state = copy.deepcopy(self._state) + state["gpu_history"] = list(state["gpu_history"]) + state["vram_history"] = list(state["vram_history"]) + return state + + def __enter__(self) -> "GPUMonitor": + return self.start() + + def __exit__(self, exc_type, exc, traceback) -> None: + self.stop() diff --git a/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/helpers/physisim_widget.py b/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/helpers/physisim_widget.py new file mode 100644 index 0000000..44c4dde --- /dev/null +++ b/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/helpers/physisim_widget.py @@ -0,0 +1,341 @@ +# Copyright (C) 2026 Advanced Micro Devices, Inc. All rights reserved. +# SPDX-License-Identifier: MIT + +"""ipywidgets controller for live PhySim HUD interaction.""" + +from __future__ import annotations + +import html +from collections.abc import Callable +from pathlib import Path +from typing import Any + +import cv2 +import ipywidgets as widgets +import numpy as np + +from .physisim_hud import FFmpegHUDWriter + + +class LiveHUDController: + """Display live HUD frames and dispatch notebook-owned simulation actions.""" + + ACTIONS = ( + ("Reset", "reset"), + ("Approach", "approach"), + ("Close to Secure", "close"), + ("Lift", "lift"), + ("Lower", "lower"), + ("Release", "release"), + ("Shutdown HUD", "shutdown"), + ) + + def __init__( + self, + *, + targets: tuple[str, ...] = ("red", "green", "blue"), + contact_threshold: float = 5e-4, + secure_taxels: int = 12, + export_path: str = "Videos/video_05_hud.mp4", + export_fps: int = 25, + max_frames: int = 500, + ) -> None: + self.handlers: dict[str, Callable[["LiveHUDController"], Any]] = {} + self.busy = False + self.export_fps = int(export_fps) + self.max_frames = int(max_frames) + self._captured_jpegs: list[bytes] = [] + + self.image = widgets.Image( + format="jpeg", + layout=widgets.Layout( + width="auto", + height="auto", + max_width="100%", + max_height="calc(100vh - 160px)", + object_fit="contain", + margin="0 auto", + display="none", + ), + ) + self.status = widgets.HTML( + value="Status: Initialize the live simulation.", + layout=widgets.Layout(width="100%"), + ) + self.target = widgets.Dropdown( + options=targets, + value=targets[0], + description="Target", + ) + self.scene_layout = widgets.Dropdown( + options=(("Default", "default"), ("Random (seeded)", "random")), + value="default", + description="Layout", + layout=widgets.Layout(width="240px"), + ) + self.layout_seed = widgets.BoundedIntText( + value=42, + min=0, + max=2_147_483_647, + description="Seed", + layout=widgets.Layout(width="210px"), + ) + self.contact_threshold = widgets.FloatLogSlider( + value=contact_threshold, + base=10, + min=-6, + max=-2, + step=0.1, + description="Contact (m)", + readout_format=".2e", + continuous_update=False, + layout=widgets.Layout(width="360px"), + ) + self.secure_taxels = widgets.IntSlider( + value=secure_taxels, + min=1, + max=128, + step=1, + description="Secure taxels", + continuous_update=False, + layout=widgets.Layout(width="360px"), + ) + self.export_path = widgets.Text( + value=export_path, + description="Export", + layout=widgets.Layout(width="520px"), + ) + self.output = widgets.Output(layout=widgets.Layout(width="100%")) + + self.action_buttons: dict[str, widgets.Button] = {} + for label, action in self.ACTIONS: + button = widgets.Button(description=label, button_style="") + button.on_click(lambda _button, name=action: self._dispatch(name)) + self.action_buttons[action] = button + + self.export_button = widgets.Button( + description="Export MP4", + button_style="success", + icon="save", + ) + self.export_button.on_click(self._on_export) + self.clear_button = widgets.Button( + description="Clear Frames", + icon="trash", + ) + self.clear_button.on_click(self._on_clear) + + if "approach" in self.action_buttons: + controls = widgets.GridspecLayout( + 2, + 12, + layout=widgets.Layout(width="100%", grid_gap="4px"), + ) + for control in ( + self.target, + self.scene_layout, + self.layout_seed, + self.contact_threshold, + self.secure_taxels, + self.export_path, + self.export_button, + self.clear_button, + *self.action_buttons.values(), + ): + control.layout.width = "auto" + controls[0, 0:2] = self.target + controls[0, 2:4] = self.scene_layout + controls[0, 4:6] = self.layout_seed + controls[0, 6:9] = self.contact_threshold + controls[0, 9:12] = self.secure_taxels + controls[1, 0] = self.action_buttons["reset"] + controls[1, 1] = self.action_buttons["approach"] + controls[1, 2:4] = self.action_buttons["close"] + controls[1, 4] = self.action_buttons["lift"] + controls[1, 5] = self.action_buttons["lower"] + controls[1, 6] = self.action_buttons["release"] + controls[1, 7] = self.action_buttons["shutdown"] + controls[1, 8:10] = self.export_path + controls[1, 10] = self.export_button + controls[1, 11] = self.clear_button + else: + # LiveAgentController replaces this temporary shell below. + controls = widgets.VBox() + self.widget = widgets.VBox([self.status, controls, self.image, self.output]) + + @property + def captured_frames(self) -> int: + return len(self._captured_jpegs) + + def bind(self, action: str, handler: Callable[["LiveHUDController"], Any]) -> None: + if action not in self.action_buttons: + raise KeyError(f"unknown action: {action}") + self.handlers[action] = handler + + def set_status(self, message: str, *, error: bool = False) -> None: + # Status text can carry planner output, LLM replies and exception strings. + color = "#d32f2f" if error else "#2e7d32" + self.status.value = ( + f"Status: {html.escape(message)} " + f"· captured {self.captured_frames} frames" + ) + + def update(self, frame: Any, status: str, *, capture: bool = True) -> None: + ok, encoded = cv2.imencode( + ".jpg", + frame, + [int(cv2.IMWRITE_JPEG_QUALITY), 88], + ) + if not ok: + raise RuntimeError("OpenCV failed to encode the HUD frame") + jpeg = encoded.tobytes() + self.image.value = jpeg + self.image.layout.display = "block" + if capture: + if len(self._captured_jpegs) >= self.max_frames: + self._captured_jpegs.pop(0) + self._captured_jpegs.append(jpeg) + self.set_status(status) + + def clear_frames(self) -> None: + self._captured_jpegs.clear() + self.set_status("Captured frame buffer cleared.") + + def export(self, path: str | None = None) -> str: + if not self._captured_jpegs: + raise RuntimeError("No live HUD frames have been captured") + output = path or self.export_path.value + Path(output).parent.mkdir(parents=True, exist_ok=True) + writer = FFmpegHUDWriter(output, fps=self.export_fps).open() + try: + for jpeg in self._captured_jpegs: + frame = cv2.imdecode( + np.frombuffer(jpeg, dtype=np.uint8), + cv2.IMREAD_COLOR, + ) + if frame is None: + raise RuntimeError("Failed to decode a captured HUD frame") + writer.write(frame) + finally: + writer.close() + self.set_status( + f"Exported {writer.frames_written} frames to {output}." + ) + return output + + def _set_buttons_disabled(self, disabled: bool) -> None: + for button in self.action_buttons.values(): + button.disabled = disabled + self.export_button.disabled = disabled + self.clear_button.disabled = disabled + + def _dispatch(self, action: str) -> None: + if self.busy: + return + handler = self.handlers.get(action) + if handler is None: + self.set_status(f"No handler is bound for {action}.", error=True) + return + self.busy = True + self._set_buttons_disabled(True) + self.set_status(f"Running {action}…") + try: + with self.output: + handler(self) + except Exception as error: + self.set_status(f"{action} failed: {error}", error=True) + with self.output: + print(f"{type(error).__name__}: {error}") + finally: + self.busy = False + self._set_buttons_disabled(False) + + def _on_export(self, _button: widgets.Button) -> None: + if self.busy: + return + try: + with self.output: + path = self.export() + print("Exported:", path) + except Exception as error: + self.set_status(f"Export failed: {error}", error=True) + + def _on_clear(self, _button: widgets.Button) -> None: + self.clear_frames() + + +class LiveAgentController(LiveHUDController): + """Live command console for the GS06 language-guided agent.""" + + ACTIONS = ( + ("Run Command", "run"), + ("Home", "home"), + ("Stop", "stop"), + ("Reset Scene", "reset"), + ("Shutdown HUD", "shutdown"), + ) + + def __init__( + self, + *, + contact_threshold: float = 5e-4, + secure_taxels: int = 12, + export_path: str = "Videos/video_06_hud.mp4", + export_fps: int = 25, + max_frames: int = 750, + ) -> None: + super().__init__( + targets=("red", "green", "blue"), + contact_threshold=contact_threshold, + secure_taxels=secure_taxels, + export_path=export_path, + export_fps=export_fps, + max_frames=max_frames, + ) + self.command = widgets.Text( + value="stack the blue cube on the red cube", + placeholder="pick green | stack blue on red | home | stop", + description="Command", + layout=widgets.Layout(width="680px"), + ) + self.planner_mode = widgets.Dropdown( + options=(("Offline", "offline"), ("LLM endpoint", "llm")), + value="offline", + description="Planner", + layout=widgets.Layout(width="220px"), + ) + self.last_result: dict[str, Any] | None = None + + controls = widgets.GridspecLayout( + 3, + 12, + layout=widgets.Layout(width="100%", grid_gap="4px"), + ) + for control in ( + self.command, + self.planner_mode, + self.scene_layout, + self.layout_seed, + self.contact_threshold, + self.secure_taxels, + self.export_path, + self.export_button, + self.clear_button, + *self.action_buttons.values(), + ): + control.layout.width = "auto" + controls[0, 0:6] = self.command + controls[0, 6:8] = self.planner_mode + controls[0, 8:10] = self.scene_layout + controls[0, 10:12] = self.layout_seed + controls[1, 0:4] = self.contact_threshold + controls[1, 4:8] = self.secure_taxels + controls[1, 8:10] = self.action_buttons["run"] + controls[1, 10:12] = self.action_buttons["reset"] + controls[2, 0] = self.action_buttons["home"] + controls[2, 1] = self.action_buttons["stop"] + controls[2, 2:4] = self.action_buttons["shutdown"] + controls[2, 4:9] = self.export_path + controls[2, 9:11] = self.export_button + controls[2, 11] = self.clear_button + self.widget = widgets.VBox([self.status, controls, self.image, self.output]) diff --git a/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/helpers/start_llama_server.sh b/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/helpers/start_llama_server.sh new file mode 100644 index 0000000..3d232d8 --- /dev/null +++ b/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/helpers/start_llama_server.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# Copyright (C) 2026 Advanced Micro Devices, Inc. All rights reserved. +# SPDX-License-Identifier: MIT + +set -euo pipefail + +LLAMA_SERVER_BIN="${LLAMA_SERVER_BIN:-/opt/llama/bin/llama-server}" +LLAMA_MODEL_PATH="${LLAMA_MODEL_PATH:-/opt/workspace/PhySim/models/Llama-3.2-3B-Instruct-Q4_K_M.gguf}" +LLAMA_HOST="${LLAMA_HOST:-127.0.0.1}" +LLAMA_PORT="${LLAMA_PORT:-8081}" +LLAMA_GPU_LAYERS="${LLAMA_GPU_LAYERS:-99}" +LLAMA_CTX_SIZE="${LLAMA_CTX_SIZE:-2048}" +LLAMA_PARALLEL="${LLAMA_PARALLEL:-1}" + +if [[ ! -x "$LLAMA_SERVER_BIN" ]]; then + echo "ERROR: llama-server is not executable: $LLAMA_SERVER_BIN" >&2 + echo "Set LLAMA_SERVER_BIN to a llama.cpp Vulkan or ROCm/HIP build." >&2 + exit 1 +fi + +if [[ ! -f "$LLAMA_MODEL_PATH" ]]; then + echo "ERROR: GGUF model was not found: $LLAMA_MODEL_PATH" >&2 + echo "Set LLAMA_MODEL_PATH to Llama-3.2-3B-Instruct-Q4_K_M.gguf." >&2 + exit 1 +fi + +if python3 -c \ + "import sys, urllib.request; sys.exit(0 if urllib.request.urlopen('http://${LLAMA_HOST}:${LLAMA_PORT}/health', timeout=2).status == 200 else 1)" \ + >/dev/null 2>&1; then + echo "llama-server is already healthy at http://${LLAMA_HOST}:${LLAMA_PORT}" + exit 0 +fi + +if python3 -c \ + "import socket, sys; s=socket.socket(); s.settimeout(1); sys.exit(0 if s.connect_ex(('${LLAMA_HOST}', ${LLAMA_PORT})) == 0 else 1)" \ + >/dev/null 2>&1; then + echo "ERROR: ${LLAMA_HOST}:${LLAMA_PORT} is already occupied by another process." >&2 + exit 1 +fi + +echo "Starting llama-server" +echo " binary : $LLAMA_SERVER_BIN" +echo " model : $LLAMA_MODEL_PATH" +echo " URL : http://${LLAMA_HOST}:${LLAMA_PORT}" +echo "Keep this terminal open. Press Ctrl+C to stop the server." + +exec "$LLAMA_SERVER_BIN" \ + --model "$LLAMA_MODEL_PATH" \ + --host "$LLAMA_HOST" \ + --port "$LLAMA_PORT" \ + --n-gpu-layers "$LLAMA_GPU_LAYERS" \ + --ctx-size "$LLAMA_CTX_SIZE" \ + --parallel "$LLAMA_PARALLEL" \ + --no-warmup diff --git a/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/xml/franka_emika_panda/LICENSE b/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/xml/franka_emika_panda/LICENSE new file mode 100644 index 0000000..d9a10c0 --- /dev/null +++ b/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/xml/franka_emika_panda/LICENSE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/xml/franka_emika_panda/README.md b/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/xml/franka_emika_panda/README.md index 164da2b..311de2c 100644 --- a/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/xml/franka_emika_panda/README.md +++ b/projects/Physical-AI/Physical-Simulation/Genesis-Simulation/xml/franka_emika_panda/README.md @@ -1,22 +1,6 @@ - # Franka Emika Panda Description (MJCF) @@ -31,10 +15,6 @@ Emika](https://www.franka.de/company). It is derived from the [publicly available URDF description](https://github.com/frankaemika/franka_ros/tree/develop/franka_description). -

- -

- ## URDF → MJCF derivation steps 1. Converted the DAE [mesh @@ -59,7 +39,8 @@ description](https://github.com/frankaemika/franka_ros/tree/develop/franka_descr 13. Added an equality constraint so that the left finger mimics the position of the right finger. 14. Added a tendon to split the force equally between both fingers and a position actuator acting on this tendon. -15. Added `scene.xml` which includes the robot, with a textured groundplane, skybox, and haze. +15. The upstream package also provides a separate `scene.xml`; this course uses + `panda.xml` directly inside Genesis. ## License