{ "cells": [ { "cell_type": "markdown", "id": "bbd9b37d", "metadata": {}, "source": [ "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/tayalmanan28/MuJoCo-Tutorial/blob/main/tutorial/06_xml_modeling.ipynb)\n", "\n", "# Tutorial 6: MJCF Modeling Guide\n", "\n", "Build custom robots and environments in MuJoCo's XML format." ] }, { "cell_type": "code", "execution_count": null, "id": "add7be57", "metadata": {}, "outputs": [], "source": [ "!pip install -q mujoco mediapy" ] }, { "cell_type": "code", "execution_count": null, "id": "af6b5e42", "metadata": {}, "outputs": [], "source": [ "import mujoco\n", "import numpy as np\n", "import mediapy as media" ] }, { "cell_type": "markdown", "id": "0b294778", "metadata": {}, "source": [ "## Body-Joint-Geom Hierarchy\n", "\n", "MJCF has a tree structure:\n", "```\n", "mujoco\n", "\u2514\u2500\u2500 worldbody\n", " \u2514\u2500\u2500 body (has joints & geoms)\n", " \u2514\u2500\u2500 body (child body)\n", " \u2514\u2500\u2500 ...\n", "```\n", "\n", "- **Bodies** define frames in the kinematic tree\n", "- **Joints** connect bodies (add degrees of freedom)\n", "- **Geoms** define collision/visual shapes" ] }, { "cell_type": "code", "execution_count": null, "id": "8898b1cb", "metadata": {}, "outputs": [], "source": [ "# Build a 2-link arm step by step\n", "arm_xml = \"\"\"\n", "\n", " \n", "\"\"\"\n", "\n", "model = mujoco.MjModel.from_xml_string(arm_xml)\n", "data = mujoco.MjData(model)\n", "mujoco.mj_forward(model, data)\n", "\n", "renderer = mujoco.Renderer(model, height=400, width=500)\n", "renderer.update_scene(data)\n", "media.show_image(renderer.render())" ] }, { "cell_type": "markdown", "id": "e5293af3", "metadata": {}, "source": [ "## Using Defaults\n", "\n", "The `` element lets you set common properties once.\n", "You can also use **default classes** for different groups:" ] }, { "cell_type": "code", "execution_count": null, "id": "8337a5b8", "metadata": {}, "outputs": [], "source": [ "xml_defaults = \"\"\"\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n", "\"\"\"\n", "m = mujoco.MjModel.from_xml_string(xml_defaults)\n", "print(f\"Geoms: {m.ngeom} (visual + collision separated via classes)\")" ] }, { "cell_type": "markdown", "id": "9f75a056", "metadata": {}, "source": [ "## Composite Objects\n", "\n", "MuJoCo can auto-generate soft bodies, ropes, and grids:" ] }, { "cell_type": "code", "execution_count": null, "id": "1c8ff8c4", "metadata": {}, "outputs": [], "source": [ "rope_xml = \"\"\"\n", "\n", " \n", "\"\"\"\n", "\n", "model_r = mujoco.MjModel.from_xml_string(rope_xml)\n", "data_r = mujoco.MjData(model_r)\n", "renderer_r = mujoco.Renderer(model_r, height=400, width=500)\n", "\n", "frames = []\n", "while data_r.time < 3.0:\n", " mujoco.mj_step(model_r, data_r)\n", " if len(frames) < data_r.time * 30:\n", " renderer_r.update_scene(data_r)\n", " frames.append(renderer_r.render().copy())\n", "\n", "renderer_r.close()\n", "media.show_video(frames, fps=30)" ] }, { "cell_type": "markdown", "id": "31a664db", "metadata": {}, "source": [ "## Loading External Models\n", "\n", "Use `` to compose models, or load from [MuJoCo Menagerie](https://github.com/google-deepmind/mujoco_menagerie):" ] }, { "cell_type": "code", "execution_count": null, "id": "4933bf73", "metadata": {}, "outputs": [], "source": [ "# Example: programmatic model editing (MuJoCo 3.x spec API)\n", "spec = mujoco.MjSpec()\n", "spec.worldbody.add_light(pos=[0, 0, 2])\n", "spec.worldbody.add_geom(type=mujoco.mjtGeom.mjGEOM_PLANE, size=[1, 1, 0.1])\n", "\n", "body = spec.worldbody.add_body(name='dynamic_box', pos=[0, 0, 1])\n", "body.add_joint(type=mujoco.mjtJoint.mjJNT_FREE)\n", "body.add_geom(type=mujoco.mjtGeom.mjGEOM_BOX, size=[0.1, 0.1, 0.1], mass=1.0, rgba=[0, 0.7, 0, 1])\n", "\n", "model_spec = spec.compile()\n", "data_spec = mujoco.MjData(model_spec)\n", "mujoco.mj_forward(model_spec, data_spec)\n", "\n", "renderer_s = mujoco.Renderer(model_spec, height=300, width=400)\n", "renderer_s.update_scene(data_spec)\n", "media.show_image(renderer_s.render())\n", "renderer_s.close()\n", "print(\"Model created programmatically with MjSpec!\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Tendons\n", "\n", "Tendons are length constraints that connect joints or spatial points.\n", "They enable **coupled joints**, **pulleys**, and **muscle routing**.\n", "\n", "Types:\n", "- `` \u2014 weighted sum of joint positions (coupling)\n", "- `` \u2014 routes through via-points and wrapping surfaces" ] }, { "cell_type": "code", "metadata": {}, "source": [ "xml_tendon = \"\"\"\n", "\n", " \n", "\"\"\"\n", "\n", "model_t = mujoco.MjModel.from_xml_string(xml_tendon)\n", "data_t = mujoco.MjData(model_t)\n", "\n", "# Drive left joint \u2014 right joint follows (opposite direction)\n", "times, left_angles, right_angles = [], [], []\n", "while data_t.time < 4.0:\n", " data_t.ctrl[0] = 3.0 * np.sin(2 * data_t.time)\n", " mujoco.mj_step(model_t, data_t)\n", " times.append(data_t.time)\n", " left_angles.append(np.degrees(data_t.qpos[0]))\n", " right_angles.append(np.degrees(data_t.qpos[1]))\n", "\n", "plt.figure(figsize=(10, 3))\n", "plt.plot(times, left_angles, label='Left joint')\n", "plt.plot(times, right_angles, label='Right joint (coupled)')\n", "plt.xlabel('Time (s)'); plt.ylabel('Angle (deg)')\n", "plt.title('Tendon-Coupled Joints (differential)'); plt.legend(); plt.grid(True); plt.show()\n", "print('The tendon forces the joints to move in opposite directions!')" ], "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Equality Constraints\n", "\n", "Equality constraints enforce geometric relationships:\n", "\n", "| Type | Effect |\n", "|------|--------|\n", "| `` | Two bodies share a point (ball joint without a body) |\n", "| `` | Two bodies are rigidly attached |\n", "| `` | One joint follows another (gear ratio) |\n", "| `` | Tendon length is fixed |\n", "\n", "These are soft constraints (solved with impedance), controlled by `solref` and `solimp`." ] }, { "cell_type": "code", "metadata": {}, "source": [ "xml_equality = \"\"\"\n", "\n", " \n", "\"\"\"\n", "\n", "model_eq = mujoco.MjModel.from_xml_string(xml_equality)\n", "data_eq = mujoco.MjData(model_eq)\n", "\n", "times, master_angles, follower_angles = [], [], []\n", "while data_eq.time < 4.0:\n", " data_eq.ctrl[0] = 3.0 * np.sin(data_eq.time)\n", " mujoco.mj_step(model_eq, data_eq)\n", " times.append(data_eq.time)\n", " master_angles.append(np.degrees(data_eq.qpos[0]))\n", " follower_angles.append(np.degrees(data_eq.qpos[1]))\n", "\n", "plt.figure(figsize=(10, 3))\n", "plt.plot(times, master_angles, label='Master')\n", "plt.plot(times, follower_angles, label='Follower (2:1 ratio)')\n", "plt.xlabel('Time (s)'); plt.ylabel('Angle (deg)')\n", "plt.title('Equality Constraint: Gear Ratio'); plt.legend(); plt.grid(True); plt.show()\n", "print('Follower moves at 2x the master angle \u2014 like a gear train!')" ], "outputs": [] } ], "metadata": { "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 5 }