{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Robotic arm control with MuJoCo\n", "\n", "In this tutorial we will demonstrate how to control a robotic arm using the Collimator library and the MuJoCo physics engine. We will use the Franka Emika Panda robotic arm to pick up a block and set it on a table. The multibody dynamics are handled with MuJoCo, but the controller is implemented in Collimator. This can be done either in the UI or directly with the Python library. This notebook is a standalone implementation with the Python library, but at the end we will also note how to do the same thing in the UI." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "from scipy.spatial import transform\n", "import matplotlib.pyplot as plt\n", "\n", "import jax\n", "import jax.numpy as jnp\n", "\n", "import collimator\n", "from collimator import library\n", "from collimator.backend import io_callback\n", "\n", "# If needed, enable offscreen rendering (eg. for CI)\n", "# import os\n", "# os.environ['MUJOCO_GL'] = 'egl'\n", "\n", "import mujoco\n", "import mediapy" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To get started, we will need to download and extract the [MuJoCo scene definition](assets/franka_emika_panda.zip): a collection of XML and STL files that define the robot and surrounding environment. This .zip file should be extracted to a directory called `assets` in the same directory as the notebook." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In order to render the scene and videos, you will also need to have ffmpeg installed. If not found automatically, the path can be set as follows:" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "# Uncomment and change to your ffmpeg path, if needed:\n", "# mediapy.set_ffmpeg(\"/opt/homebrew/bin/ffmpeg\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's start by checking that we can directly load and render the scene. We should see the robotic arm in its \"home\" position about halfway between the orange \"target\" block and the grey \"table\" block. Our goal will be to pick up the block and set it on the table." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "collimator:INFO Initialized MuJoCo from model file 'assets/franka_emika_panda/scene.xml' with dt=0.01, 16 generalized positions, 15 generalized velocities, and 0 actuators. The default control input vector (nu=8) is:\n", "np.array([0.0, 0.0, 0.0, -1.57079, 0.0, 1.57079, -0.7853, 255.0])\n", "libdecor-gtk-WARNING: Failed to initialize GTK\n", "Failed to load plugin 'libdecor-gtk.so': failed to init\n" ] }, { "data": { "text/html": [ "
" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "mjmodel = library.mujoco.MuJoCo(\n", " file_name=\"assets/franka_emika_panda/scene.xml\",\n", " key_frame_0=\"home\",\n", " enable_video_output=True,\n", ")\n", "\n", "frame = mjmodel.render()\n", "\n", "mediapy.show_image(frame)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Pick and place with differential inverse kinematics\n", "\n", "The basic \"pick-and-place\" problem is to use a robotic arm to pick up an object and then put it somewhere else. The main technical difficulty in this problem is that we control the degrees of freedom of the arm (seven for the Panda, plus one for the gripper), but we would most naturally specify the desired trajectory in Cartesian coordinates of the end-effector \"hand\".\n", "\n", "A standard solution to this problem is inverse kinematics, where we use the Jacobian that relates the joint velocities $\\dot{q}$ to the spatial velocity $v_h$ of the hand. Specifically,\n", "\n", "$$\n", "J_h(q) \\dot{q} = v_h.\n", "$$\n", "\n", "For a small time step $\\Delta t$, we could also use this to relate a deviation from a specified command position $\\Delta x_h$ to a small change in joint angles $\\Delta q$:\n", "\n", "$$\n", "J_h(q) \\Delta q \\approx \\Delta x_h,\n", "$$\n", "\n", "where $\\Delta x_h = v_h \\Delta t$ defines the error in both the Cartesian position and the orientation (here the \"orientation error\" is angular velocity times the small time step). For the inverse problem we can compute $\\Delta x_h$ easily and want to determine $\\Delta q$. This linear system is overdetermined, so we can solve with the pseudoinverse to get a minimum-norm solution:\n", "\n", "$$\n", "\\Delta q = J_h^\\dagger(q) \\Delta x_h.\n", "$$\n", "\n", "Defining the command trajectory of the hand as $\\hat{x}_h(t)$ and the current hand position as $x_h(t)$, we can define the error $\\Delta x_h = \\hat{x}_h - x_h$ and use the inverse kinematics to compute the joint position error $\\Delta q$.\n", "\n", "The joints are torque-controlled, so given the \"joint position error\" $\\Delta q$ we can apply PID feedback control on the joint torques to attempt to drive $\\Delta q$ to zero.\n", "\n", "From there, the pick-and-place problem is largely a matter of bookkeeping in defining the desired trajectory so that the arm can safely pick up the object and doesn't run into anything en route to the target location. In this notebook we will use a simple piecewise linear trajectory with a fixed orientation of the end-effector." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Preliminary setup\n", "\n", "First we need to extract some key entity IDs from the MuJoCo model defined in the XML file. The easiest way to do this is to just load the model using MuJoCo directly and then get the IDs. Under the hood the Collimator block will do the exact same thing, but doing this up front lets us do a little analysis and use the IDs to define output port functions." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "model = mujoco.MjModel.from_xml_path(\"assets/franka_emika_panda/scene.xml\")\n", "\n", "hand_id = model.body(\"hand\").id # The end effector ID\n", "target_id = model.site(\"target\").id # The block (\"target\") ID\n", "\n", "# Actuators for controlled joints\n", "arm_actuator_names = [\n", " \"actuator1\",\n", " \"actuator2\",\n", " \"actuator3\",\n", " \"actuator4\",\n", " \"actuator5\",\n", " \"actuator6\",\n", " \"actuator7\",\n", "]\n", "arm_actuator_ids = np.array([model.actuator(name).id for name in arm_actuator_names])\n", "grip_actuator_id = model.actuator(\"actuator8\").id # Tendon for the end-effector gripper" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Next we can create the main MuJoCo block. Currently the default output ports are the joint position (`qpos`), joint velocity (`qvel`), and actuator state (`act`), but additional ports can be defined by creating short \"scripts\" that accept the MuJoCo model and data struct as arguments. Here we will use the Cartesian position (`xpos`) and orientation (`xquat`) of the end-effector along with the Jacobian for this body in the inverse kinematics calculation, and the Cartesian position and orientation (`xmat`) of the target block for post-processing and creating the video later on." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "collimator:INFO Initialized MuJoCo from model file 'assets/franka_emika_panda/scene.xml' with dt=0.001, 16 generalized positions, 15 generalized velocities, and 0 actuators. The default control input vector (nu=8) is:\n", "np.array([0.0, 0.0, 0.0, -1.57079, 0.0, 1.57079, -0.7853, 255.0])\n" ] } ], "source": [ "dt = 0.001\n", "\n", "jac_buffer = np.zeros((6, model.nv))\n", "def jac_script(model, data):\n", " mujoco.mj_jacBody(model, data, jac_buffer[:3], jac_buffer[3:], hand_id)\n", " return jac_buffer\n", "\n", "custom_outputs = {\n", " \"hand_xpos\": lambda model, data: data.body(hand_id).xpos,\n", " \"hand_xquat\": lambda model, data: data.body(hand_id).xquat,\n", " \"target_xpos\": lambda model, data: data.site(target_id).xpos,\n", " \"target_xmat\": lambda model, data: data.site(target_id).xmat,\n", " \"jac\": jac_script,\n", "}\n", "\n", "mjmodel = library.mujoco.MuJoCo(\n", " file_name=\"assets/franka_emika_panda/scene.xml\",\n", " dt=dt,\n", " key_frame_0=\"home\",\n", " enable_video_output=True,\n", " custom_output_scripts=custom_outputs,\n", ")\n", "data = mjmodel._data" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The trajectory for this \"pick-and-place\" task will be a piecewise linear interpolation between a series of waypoints. The arm should move to a \"pre-grasp\" position that's clear of the block, into grasping position, close the gripper, return to pregrasp position, safely move to a \"pre-place\" position, the place position, open the gripper, and finally return home.\n", "\n", "For this simple example there is no need to change the orientation of the object, so we can simply use the \"home\" orientation of the end-effector as a target orientation throughout the entire task." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "# Define key locations in the scene\n", "home_pos = data.body(hand_id).xpos.copy()\n", "u0 = data.ctrl[arm_actuator_ids].copy()\n", "target_pos = data.site(target_id).xpos.copy()\n", "pregrasp_pos = target_pos + np.array([-0.035, 0.0, 0.15])\n", "grasp_pos = target_pos + np.array([-0.035, 0.0, 0.05])\n", "waypoint_pos = home_pos + np.array([0.0, 0.0, -0.1])\n", "preplace_pos = np.array([0.6, 0.35, 0.5])\n", "place_pos = preplace_pos + np.array([0.0, 0.0, -0.12])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can use a simple linear interpolant to get the command position as a function of time, and also define a block to output the gripper command." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "# Custom block to output piecewise linear target locations\n", "OPEN = np.array(0)\n", "CLOSE = np.array(1)\n", "\n", "# Motion stages (times are cumulative in seconds)\n", "scale = 2.0\n", "t0 = 0.0\n", "time_array = np.array([\n", " 0.0, # Home\n", " 2.0, # Pre-grasp\n", " 3.0, # Grasp\n", " 3.5, # Close gripper (grasp)\n", " 4.5, # Pre-grasp\n", " 7.5, # Waypoint\n", " 9.0, # Pre-place\n", " 10.0, # Place\n", " 10.5, # Open gripper (place)\n", " 11.5, # Pre-place\n", " 13.0, # Home\n", "]) / scale\n", "tf = time_array[-1]\n", "\n", "xpos_array = np.array([\n", " home_pos,\n", " pregrasp_pos,\n", " grasp_pos,\n", " grasp_pos,\n", " pregrasp_pos,\n", " waypoint_pos,\n", " preplace_pos,\n", " place_pos,\n", " place_pos,\n", " preplace_pos,\n", " home_pos,\n", "])\n", "\n", "# vmap interpolation to allow for 2d array input\n", "interp_fun = jax.vmap(jnp.interp, (None, None, 1))\n", "\n", "def pos_command_cb(time):\n", " t = time - t0\n", " return jnp.where((t < 0.0) | (t > tf), home_pos, interp_fun(t, time_array, xpos_array))\n", "\n", "def grip_command_cb(time):\n", " t = time - t0\n", " return jnp.where((t > time_array[2]) & (t < time_array[7]), 0.0, 255.0)\n", "\n", "pos_source = library.SourceBlock(pos_command_cb, name=\"pos_command\")\n", "grip_source = library.SourceBlock(grip_command_cb, name=\"grip_command\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The last bit of custom machinery we have to implement is the inverse kinematics. Here we'll use a standard pseudoinverse calculation to get the tracking error in position and orientation (we will not approach any singularities). Since the arm is torque-controlled, we will then be able to apply parallel PID controllers to each joint to apply torque such that the tracking error is driven to zero." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "# Custom class for inverse kinematics\n", "\n", "class InverseKinematics(collimator.LeafSystem):\n", " def __init__(self, nv, name=\"InverseKinematics\"):\n", " super().__init__(name=name)\n", "\n", " # Preallocate some arrays for computing inverse kinematics\n", " self.error = np.zeros(6)\n", " self.hand_quat = np.zeros(4)\n", " self.hand_quat_conj = np.zeros(4)\n", " self.error_quat = np.zeros(4)\n", "\n", " # Create input ports\n", " self.declare_input_port(\"target_pos\")\n", " self.declare_input_port(\"target_quat\")\n", " self.declare_input_port(\"pos\")\n", " self.declare_input_port(\"quat\")\n", " self.declare_input_port(\"jac\")\n", "\n", " # Output port: joint velocities\n", " def _output_cb(time, state, *inputs, **parameters):\n", " return io_callback(self._diffik, np.zeros(nv), *inputs)\n", "\n", " self.declare_output_port(_output_cb, period=dt)\n", "\n", " def _diffik(self, target_pos, target_quat, pos, quat, jac):\n", " # Compute the error in position and orientation\n", " self.error[:3] = target_pos - pos\n", " mujoco.mju_negQuat(self.hand_quat_conj, quat)\n", " mujoco.mju_mulQuat(self.error_quat, target_quat, self.hand_quat_conj)\n", " mujoco.mju_quat2Vel(self.error[3:], self.error_quat, 1.0)\n", "\n", " # Solve system of equations: J @ dq = dx\n", " return np.linalg.pinv(jac) @ self.error" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We're finally ready to construct the full block diagram. In the app, the model would look like this:\n", "\n", "\"diagram\"\n", "\n", "Constructing the system in code there are some minor differences, but the overall controller structure is largely the same. The following code uses the Collimator `DiagramBuilder` interface to programmatically construct this model. Evidently, this approach quickly becomes unwieldy in code and is not recommended for larger models; it is much easier and less error-prone to construct larger block diagrams in the [Collimator web UI](app.collimator.ai) (see notes on UI implementation at the end of the tutorial)." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "│── \u001b[1m\u001b[32mroot\u001b[0m <\u001b[1mDiagram\u001b[0m>\n", " │── \u001b[1m\u001b[32mMuJoCo_2_\u001b[0m <\u001b[1mMuJoCo\u001b[0m> [\u001b[36mhand_xpos\u001b[0m → \u001b[32mInverseKinematics.\u001b[34mpos\u001b[0m, \u001b[36mhand_xquat\u001b[0m → \u001b[32mInverseKinematics.\u001b[34mquat\u001b[0m, \u001b[36mjac\u001b[0m → \u001b[32mInverseKinematics.\u001b[34mjac\u001b[0m]\n", " │── \u001b[1m\u001b[32mgrip_command\u001b[0m <\u001b[1mSourceBlock\u001b[0m> [\u001b[36mout_0\u001b[0m → \u001b[32mu.\u001b[34min_1\u001b[0m]\n", " │── \u001b[1m\u001b[32mpos_command\u001b[0m <\u001b[1mSourceBlock\u001b[0m> [\u001b[36mout_0\u001b[0m → \u001b[32mInverseKinematics.\u001b[34mtarget_pos\u001b[0m]\n", " │── \u001b[1m\u001b[32mq0\u001b[0m <\u001b[1mConstant\u001b[0m> [\u001b[36mout_0\u001b[0m → \u001b[32mInverseKinematics.\u001b[34mtarget_quat\u001b[0m]\n", " │── \u001b[1m\u001b[32mInverseKinematics\u001b[0m <\u001b[1mInverseKinematics\u001b[0m> [\u001b[36mout_0\u001b[0m → \u001b[32mdq_arm.\u001b[34min_0\u001b[0m]\n", " │── \u001b[1m\u001b[32mdq_arm\u001b[0m <\u001b[1mFeedthroughBlock\u001b[0m> [\u001b[36mout_0\u001b[0m → \u001b[32mdemux_dq.\u001b[34min_0\u001b[0m]\n", " │── \u001b[1m\u001b[32mdemux_dq\u001b[0m <\u001b[1mDemultiplexer\u001b[0m> [\u001b[36mout_0\u001b[0m → \u001b[32mPIDDiscrete_10_.\u001b[34min_0\u001b[0m, \u001b[36mout_1\u001b[0m → \u001b[32mPIDDiscrete_11_.\u001b[34min_0\u001b[0m, \u001b[36mout_2\u001b[0m → \u001b[32mPIDDiscrete_12_.\u001b[34min_0\u001b[0m, \u001b[36mout_3\u001b[0m → \u001b[32mPIDDiscrete_13_.\u001b[34min_0\u001b[0m, \u001b[36mout_4\u001b[0m → \u001b[32mPIDDiscrete_14_.\u001b[34min_0\u001b[0m, \u001b[36mout_5\u001b[0m → \u001b[32mPIDDiscrete_15_.\u001b[34min_0\u001b[0m, \u001b[36mout_6\u001b[0m → \u001b[32mPIDDiscrete_16_.\u001b[34min_0\u001b[0m]\n", " │── \u001b[1m\u001b[32mpid\u001b[0m <\u001b[1mMultiplexer\u001b[0m> [\u001b[36mout_0\u001b[0m → \u001b[32muq.\u001b[34min_1\u001b[0m]\n", " │── \u001b[1m\u001b[32mPIDDiscrete_10_\u001b[0m <\u001b[1mPIDDiscrete\u001b[0m> [\u001b[36mout_0\u001b[0m → \u001b[32mpid.\u001b[34min_0\u001b[0m]\n", " │── \u001b[1m\u001b[32mPIDDiscrete_11_\u001b[0m <\u001b[1mPIDDiscrete\u001b[0m> [\u001b[36mout_0\u001b[0m → \u001b[32mpid.\u001b[34min_1\u001b[0m]\n", " │── \u001b[1m\u001b[32mPIDDiscrete_12_\u001b[0m <\u001b[1mPIDDiscrete\u001b[0m> [\u001b[36mout_0\u001b[0m → \u001b[32mpid.\u001b[34min_2\u001b[0m]\n", " │── \u001b[1m\u001b[32mPIDDiscrete_13_\u001b[0m <\u001b[1mPIDDiscrete\u001b[0m> [\u001b[36mout_0\u001b[0m → \u001b[32mpid.\u001b[34min_3\u001b[0m]\n", " │── \u001b[1m\u001b[32mPIDDiscrete_14_\u001b[0m <\u001b[1mPIDDiscrete\u001b[0m> [\u001b[36mout_0\u001b[0m → \u001b[32mpid.\u001b[34min_4\u001b[0m]\n", " │── \u001b[1m\u001b[32mPIDDiscrete_15_\u001b[0m <\u001b[1mPIDDiscrete\u001b[0m> [\u001b[36mout_0\u001b[0m → \u001b[32mpid.\u001b[34min_5\u001b[0m]\n", " │── \u001b[1m\u001b[32mPIDDiscrete_16_\u001b[0m <\u001b[1mPIDDiscrete\u001b[0m> [\u001b[36mout_0\u001b[0m → \u001b[32mpid.\u001b[34min_6\u001b[0m]\n", " │── \u001b[1m\u001b[32muq\u001b[0m <\u001b[1mAdder\u001b[0m> [\u001b[36mout_0\u001b[0m → \u001b[32mu.\u001b[34min_0\u001b[0m]\n", " │── \u001b[1m\u001b[32muq0\u001b[0m <\u001b[1mConstant\u001b[0m> [\u001b[36mout_0\u001b[0m → \u001b[32muq.\u001b[34min_0\u001b[0m]\n", " │── \u001b[1m\u001b[32mu\u001b[0m <\u001b[1mMultiplexer\u001b[0m> [\u001b[36mout_0\u001b[0m → \u001b[32mMuJoCo_2_.\u001b[34mcontrol\u001b[0m]\n" ] } ], "source": [ "\n", "builder = collimator.DiagramBuilder()\n", "builder.add(mjmodel, grip_source, pos_source)\n", "\n", "# Constant orientation for the end-effector\n", "rot = transform.Rotation.from_euler(\"xyz\", [90, 0, 180], degrees=True)\n", "const_q0 = builder.add(library.Constant(rot.as_quat(), name=\"q0\"))\n", "\n", "# Inverse kinematics\n", "ik = builder.add(InverseKinematics(nv=mjmodel.nv))\n", "builder.connect(pos_source.output_ports[0], ik.input_ports[0]) # Target position\n", "builder.connect(const_q0.output_ports[0], ik.input_ports[1]) # Target orientation\n", "builder.connect(mjmodel.get_output_port(\"hand_xpos\"), ik.input_ports[2]) # Current position\n", "builder.connect(mjmodel.get_output_port(\"hand_xquat\"), ik.input_ports[3]) # Current orientation\n", "builder.connect(mjmodel.get_output_port(\"jac\"), ik.input_ports[4]) # Jacobian\n", "\n", "# Extract only the controlled joints from the inverse kinematics\n", "dq_arm = builder.add(library.FeedthroughBlock(lambda dq: dq[arm_actuator_ids], name=\"dq_arm\"))\n", "builder.connect(ik.output_ports[0], dq_arm.input_ports[0])\n", "demux_dq = builder.add(library.Demultiplexer(len(arm_actuator_ids), name=\"demux_dq\"))\n", "builder.connect(dq_arm.output_ports[0], demux_dq.input_ports[0])\n", "\n", "# Replicate PID controllers for each joint\n", "pid = builder.add(library.Multiplexer(len(arm_actuator_ids), name=\"pid\"))\n", "for i in range(len(arm_actuator_ids)):\n", " pid_i = builder.add(library.PIDDiscrete(kp=10.0, ki=1.0, kd=0.01, dt=dt))\n", " builder.connect(demux_dq.output_ports[i], pid_i.input_ports[0])\n", " builder.connect(pid_i.output_ports[0], pid.input_ports[i])\n", "\n", "# Add the base control signal to the PID output\n", "adder_uq = builder.add(library.Adder(2, name=\"uq\")) # Joint commands\n", "const_uq0 = builder.add(library.Constant(mjmodel.ctrl_0[arm_actuator_ids], name=\"uq0\"))\n", "builder.connect(const_uq0.output_ports[0], adder_uq.input_ports[0])\n", "builder.connect(pid.output_ports[0], adder_uq.input_ports[1])\n", "\n", "# Append the gripper command to the joint commands\n", "mux_u = builder.add(library.Multiplexer(2, name=\"u\"))\n", "builder.connect(adder_uq.output_ports[0], mux_u.input_ports[0])\n", "builder.connect(grip_source.output_ports[0], mux_u.input_ports[1])\n", "\n", "builder.connect(mux_u.output_ports[0], mjmodel.input_ports[0])\n", "\n", "system = builder.build()\n", "system.pprint()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Simulation\n", "\n", "Now we can run the full simulation including the control loop as usual in Collimator. We'll record everything we need to re-create a video of the arm performing the placement task in postprocessing." ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "collimator:INFO max_major_steps=12998 based on smallest discrete period=0.001\n", "collimator:INFO Simulator ready to start: SimulatorOptions(math_backend=jax, enable_tracing=True, max_major_step_length=None, max_major_steps=12998, ode_solver_method=auto, rtol=1e-06, atol=1e-08, min_minor_step_size=None, max_minor_step_size=None, zc_bisection_loop_count=40, save_time_series=True, recorded_signals=5, return_context=True), Dopri5Solver(system=Diagram(root, 18 nodes), rtol=1e-06, atol=1e-08, max_step_size=None, min_step_size=None, method='auto', enable_autodiff=False, supports_mass_matrix=False)\n" ] } ], "source": [ "context = system.create_context()\n", "\n", "recorded_signals = {\n", " \"qpos\": mjmodel.output_ports[0],\n", " \"pos_cmd\": pos_source.output_ports[0],\n", " \"hand_xpos\": mjmodel.get_output_port(\"hand_xpos\"),\n", " \"target_xpos\": mjmodel.get_output_port(\"target_xpos\"),\n", " \"target_xmat\": mjmodel.get_output_port(\"target_xmat\"),\n", "}\n", "\n", "results = collimator.simulate(\n", " system,\n", " context,\n", " (0.0, tf),\n", " recorded_signals=recorded_signals,\n", ") " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we can plot the planned versus actual position of the end-effector to make sure the intended trajectory is being followed. On its own this is not the most intuitive way to assess the performance of the controller; next we will render the scene and create a video of the arm performing the task." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "image/png": "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", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "fig, ax = plt.subplots(1, 1, figsize=(7, 2), sharex=True)\n", "\n", "for i in range(3):\n", " ax.plot(results.time, results.outputs[\"pos_cmd\"][:, i], c='k')\n", " ax.plot(results.time, results.outputs[\"hand_xpos\"][:, i], '--', c='tab:red')\n", "ax.set_xlabel(\"Time [s]\")\n", "ax.set_ylabel(\"Position [m]\")\n", "ax.legend([\"Commanded\", \"Actual\"])\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Post-processing\n", "\n", "Finally, since the \"scene\" can be fully reconstructed with the joint positions and the position and orientation of the target block, we can create a video as a post-processing step by interpolating the recorded data. The control loop ran at 1kHz, which is an unnecessarily high frame rate, so in this step we downsample to 60 frames per second and use the MuJoCo renderer to create an RGB array for each frame." ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# Time stamps for scene\n", "fps = 60.0\n", "t = np.arange(0, tf, 1/fps)\n", "\n", "# Linearly interpolate the joint positions\n", "qpos = np.zeros((len(t), mjmodel.nq))\n", "for i in range(mjmodel.nq):\n", " qpos[:, i] = np.interp(t, results.time, results.outputs[\"qpos\"][:, i])\n", "\n", "block_xpos = np.zeros((len(t), 3))\n", "for i in range(3):\n", " block_xpos[:, i] = np.interp(t, results.time, results.outputs[\"target_xpos\"][:, i])\n", "\n", "block_xmat = np.zeros((len(t), 9))\n", "for i in range(9):\n", " block_xmat[:, i] = np.interp(t, results.time, results.outputs[\"target_xmat\"][:, i])\n", "\n", "frames = np.zeros((len(t), *mjmodel._video_default.shape), dtype=np.uint8)\n", "for i, q in enumerate(qpos):\n", " data.qpos[:] = q\n", " data.site_xpos[target_id] = block_xpos[i]\n", " data.site_xmat[target_id] = block_xmat[i]\n", " mujoco.mj_kinematics(model, data)\n", " frames[i] = mjmodel.render(data)\n", "\n", "\n", "mediapy.show_video(frames, fps=fps, loop=False)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Implementing in the UI\n", "\n", "If you want to implement this in the [Collimator web app](app.collimator.ai), you can follow essentially the same steps, but with a few key differences.\n", "\n", "\n", "### Preliminary setup\n", "\n", "First, all the \"assets\" should be uploaded to the project directory. Since the UI doesn't support directory structures, you can just upload all the files to the main folder and remove the \"assets/franka_emika_panda\" prefix from the XML file.\n", "\n", "Now the preliminary configuration (defining waypoints, etc.) can be done in an \"init script\":\n", "\n", "```python\n", "\n", "import numpy as np\n", "from scipy.spatial import transform\n", "import mujoco\n", "\n", "import jax\n", "import jax.numpy as jnp\n", "\n", "model = mujoco.MjModel.from_xml_path(\"scene.xml\")\n", "\n", "hand_id = model.body(\"hand\").id # The end effector ID\n", "target_id = model.site(\"target\").id # The block (\"target\") ID\n", "\n", "# Actuators for controlled joints\n", "arm_actuator_names = [\n", " \"actuator1\",\n", " \"actuator2\",\n", " \"actuator3\",\n", " \"actuator4\",\n", " \"actuator5\",\n", " \"actuator6\",\n", " \"actuator7\",\n", "]\n", "arm_actuator_ids = np.array([model.actuator(name).id for name in arm_actuator_names])\n", "grip_actuator_id = model.actuator(\"actuator8\").id # Tendon for the end-effector gripper\n", "\n", "# Define key locations in the scene\n", "home_pos = np.array([5.54499478e-01, 0.0, 6.24502429e-01])\n", "u0 = np.array([ 0. , 0. , 0. , -1.57079, 0. , 1.57079, -0.7853 ])\n", "target_pos = np.array([0.4, -0.2, 0.1])\n", "pregrasp_pos = target_pos + np.array([-0.035, 0.0, 0.15])\n", "grasp_pos = target_pos + np.array([-0.035, 0.0, 0.05])\n", "waypoint_pos = home_pos + np.array([0.0, 0.0, -0.1])\n", "preplace_pos = np.array([0.6, 0.35, 0.5])\n", "place_pos = preplace_pos + np.array([0.0, 0.0, -0.12])\n", "\n", "# Motion stages (times are cumulative in seconds)\n", "t0 = 0.0\n", "time_array = np.array([\n", " 0.0, # Home\n", " 1.0, # Pre-grasp\n", " 1.5, # Grasp\n", " 1.75, # Close gripper (grasp)\n", " 2.25, # Pre-grasp\n", " 3.75, # Waypoint\n", " 4.5, # Pre-place\n", " 5.0, # Place\n", " 5.25, # Open gripper (place)\n", " 5.75, # Pre-place\n", " 6.5, # Home\n", "])\n", "tf = time_array[-1]\n", "\n", "xpos_array = np.array([\n", " home_pos,\n", " pregrasp_pos,\n", " grasp_pos,\n", " grasp_pos,\n", " pregrasp_pos,\n", " waypoint_pos,\n", " preplace_pos,\n", " place_pos,\n", " place_pos,\n", " preplace_pos,\n", " home_pos,\n", "])\n", "```\n", "\n", "### Defining the MuJoCo plant\n", "\n", "The MuJoCo multibody plant can be created by simply selecting `scene.xml` from the dropdown menu in the MuJoCo block. Be sure to set the timestep to the same value used as the \"discrete clock tick\" in the simulation settings (here we used $\\Delta t = 0.001$). The initial key frame should also be set to `\"home\"` so that the model is initialized in the correct position (otherwise by default all joint angles are zero).\n", "\n", "The custom output ports can be defined as above. For instance, you can create an output port named `hand_xpos` with the script:\n", "\n", "```python\n", "fn = lambda model, data: data.body(9).xpos\n", "```\n", "\n", "and likewise for the `hand_xquat` port. Note that the function _must_ be named `fn`. The `target_xpos` and `target_xmat` ports can be defined similarly, e.g. for `target_xpos`:\n", "\n", "```python\n", "fn = lambda model, data: data.site(0).xpos\n", "```\n", "\n", "For the Jacobian calculation, we will need to explicitly import NumPy and MuJoCo in order to define the function and allocate the buffer array:\n", "\n", "```python\n", "import numpy as np\n", "import mujoco\n", "jac_buffer = np.zeros((6, 15))\n", "\n", "def fn(model, data):\n", " mujoco.mj_jacBody(model, data, jac_buffer[:3], jac_buffer[3:], 9)\n", " return jac_buffer\n", "```\n", "\n", "However, only the `fn` function will be evaluated online, so this will use the preallocated buffer and will not need to re-import NumPy and MuJoCo.\n", "\n", "__NOTE:__ Currently the JAX implementation of MuJoCo (called [MJX](https://mujoco.readthedocs.io/en/stable/mjx.html)) does not support some of the features used in this demo (such as the Jacobian calculation and the tendon joint for the Panda end-effector), so the \"Use mjx\" option should be unchecked in the MuJoCo block settings.\n", "\n", "### Command trajectory\n", "\n", "The trajectory generation is also similar to what we did in code, but we will use PythonScript blocks with a single input port (time). For instance, the `PositionCommand` block can be implemented with the following code:\n", "\n", "__Init__:\n", "```python\n", "import jax\n", "import jax.numpy as jnp\n", "interp = jax.vmap(jnp.interp, (None, None, 1))\n", "```\n", "\n", "__Step__:\n", "```python\n", "t = time - t0\n", "xpos_command = interp(t, time_array, xpos_array)\n", "xpos_command = jnp.where((t < 0.0) | (t > tf), home_pos, xpos_command)\n", "```\n", "\n", "Likewise, the `GripCommand` block can be implemented as follows:\n", "\n", "__Init__:\n", "```python\n", "import jax.numpy as jnp\n", "```\n", "\n", "__Step__:\n", "```python\n", "t = time - t0\n", "grip_command = jnp.where((t > time_array[2]) & (t < time_array[7]), 0.0, 255.0)\n", "```\n", "\n", "Be sure to create block parameters for `t0`, `tf`, `time_array`, etc. in the block configuration and check the \"Accelerate with JAX\" option. Even though the MuJoCo plant will not use JAX, there is still a significant speedup over NumPy when using JAX wherever possible.\n", "\n", "### Inverse kinematics\n", "\n", "This is implemented as another PythonScript block. However, in contrast to the trajectory sources, here we will call the MuJoCo functions to compute the position and orientation errors, so we cannot use JAX. Instead, uncheck \"Accelerate with JAX\" and select the \"Discrete\" time mode so that the control loop runs at the same rate as the MuJoCo plant. We will have five inputs: `target_pos`, `target_quat`, `pos`, `quat`, and `jac`. The output will be the joint tracking error `dq`.\n", "\n", "__Init__:\n", "```python\n", "import numpy as np\n", "import mujoco\n", "\n", "# Preallocate buffer arrays\n", "error = np.zeros(6)\n", "hand_quat_conj = np.zeros(4)\n", "error_quat = np.zeros(4)\n", "dq = np.zeros(15) # Get from model.nv\n", "```\n", "\n", "__Step__:\n", "```python\n", "# Compute the error in position and orientation\n", "error[:3] = target_pos - pos\n", "mujoco.mju_negQuat(hand_quat_conj, quat)\n", "mujoco.mju_mulQuat(error_quat, target_quat, hand_quat_conj)\n", "mujoco.mju_quat2Vel(error[3:], error_quat, 1.0)\n", "\n", "# Solve system of equations: J @ dq = dx.\n", "dq = np.linalg.pinv(jac) @ error\n", "```\n", "\n", "Otherwise, the UI implementation is largely the same as what we did above using the Python library.\n" ] } ], "metadata": { "kernelspec": { "display_name": "collimator", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.9" } }, "nbformat": 4, "nbformat_minor": 2 }