Logo Xingxin on Bug

Master DROID Action Spaces to Use Pre-Trained Policies

October 7, 2026
26 min read
No tags available

If you are a researcher using a Franka robot, you are not looking at the DROID just for fun. As DROID has become more popular, more versions and conversions have appeared, and they do not all use the data in the same way.

For example, do you know the differences among these🤔?

Many crucial details are hidden beneath the surface. To make sense of them, I will explain the dataset from the ground up while keeping the big picture in mind. Once the foundation is clear, your coding agent can handle the rest.

© DROID team

🧾 Cheat Sheet

This section is a TL;DR for quick reference. Once you understand the mechanics of DROID detailed below, this cheat sheet will be self-explanatory.

FieldObserved range in the sampleUsed by pi05_droid?
observation/gripper_position[0,1]Yes, state index 7 (the eighth value)
action_dict/gripper_position[0,1]Yes, action index 7 (the eighth value)
action_dict/gripper_velocity[-1,1]No

What is DROID and why should we care?

DROID is a massive real-world dataset collected on the Franka Panda robot via teleoperation. You can refer to the original DROID paper or gs://gresearch/robotics/droid for the exact number of episodes.

Because of its scale, many state-of-the-art VLA models offer fine-tuned checkpoints based entirely on this dataset. A few examples include:

If you believe in pretraining robotics foundation models similarly to LLMs, you should seriously consider leveraging these checkpoints.

Different Sources of DROID

📌Official DROID

The official DROID release is hosted in a Google Cloud bucket. This post focuses on the 📄RLDS: an Ecosystem to Generate, Share and Use Datasets in Reinforcement Learning release at gs://gresearch/robotics/droid/1.0.1.

Remark

DROID also provides raw recordings with higher-resolution and stereo camera data. RLDS is not its only official format. See the dataset documentation.


📌 LeRobot port

LeRobot is a robot learning framework with its own data format, LeRobotDataset. The lerobot/droid_1.0.1 dataset is a conversion of the RLDS release. Its keys and storage layout differ from the original.


📌 Isaac GR00T demo data

The original RLDS data represents orientations in SO(3) using Euler angles. GR00T’s DROID example uses Rot6D (first 2 rows of the rotation matrix) to represent rotation.

Tip

It turns out Rot6D is easier for neural network to learn. See more at 📄On the Continuity of Rotation Representations in Neural Networks.

Remark

One more tiny detail about the frame representation in GR00T: by the release of N1.7, the rotation stored in the demo data is obtained by right-multiplying the DROID rotation by the change-of-basis matrix CC:

Rmodel=RDROIDCR_{\text{model}} = R_{\text{DROID}} C

where CC is

C=[00−1−100010].C =\begin{bmatrix}0 & 0 & -1 \\-1 & 0 & 0 \\0 & 1 & 0\end{bmatrix}.

In short, GR00T N 1.7 was pre-trained on top of a lot egocentric data, which is why they apply this transformation. See more at sample conversion script and orientation conversion script.

The Hardware and Its Setup

First, let’s look at the DROID hardware setup.

DROID setup

© DROID team

Can my Franka use the DROID dataset?

There are several Franka models in franka_description. If mine is an fr3, can I still use DROID?

DROID can be useful across these robots, but similar arm geometry does not guarantee identical behavior.

  • 😁Good news: all the Franka robots share the same DH parameters.
  • 😕Bad news: fr3 has significantly different joint limits compared to the fer used in DROID.

Intuitively, it seems that joint space is harder to generalize across models, while Cartesian space might be easier. I will leave you to ponder that!

Overview of the Observation and Action Spaces

For robot learning, this is one of the most important⭐️ parts to understand. The released dataset includes a features.json file that defines its fields.

Remark

The collection loop targets 15 Hz. The RLDS sample contains 3 RGB views with shape (180, 320, 3): height, width, and channels. These are the released image dimensions, not the cameras’ native resolution.


📌 Observation space: observation/*

KeyShapeDtypeDescription
wrist_image_left(180, 320, 3)uint8Wrist camera’s left stereo view
exterior_image_1_left(180, 320, 3)uint8First external camera’s left stereo view
exterior_image_2_left(180, 320, 3)uint8Second external camera’s left stereo view
cartesian_position(6,)float64Flange pose: [x, y, z, roll, pitch, yaw], in meters and radians
joint_position(7,)float647 Franka joint angles, in radians
gripper_position(1,)float64Gripper closedness: 0 = open, 1 = closed

📌 Action space: action_dict/*

KeyShapeDtypeRange or unitsDescription
cartesian_velocity(6,)float64Normalized; nominally [−1,1]6[-1,1]^6Translation and rotation commands
cartesian_position(6,)float64Meters and radiansCartesian pose target
joint_velocity(7,)float64Normalized; nominally [−1,1]7[-1,1]^7Joint commands
joint_position(7,)float64RadiansJoint position targets
gripper_position(1,)float64[0,1]Gripper closedness target
gripper_velocity(1,)float64[-1,1] in the teleoperation pathNormalized gripper command
Tip

Feel overwhelmed😵‍💫? No worries, we will break down exactly how these are generated later.

Observation versus state

LeRobot separates proprioceptive values under observation.state from camera inputs under observation.images. These names are useful, but observation.state does not necessarily contain the full dynamical state. For example, joint positions alone omit joint velocities.

See more at the Section 17.3 of Reinforcement Learning: An Introduction about the discussion between state and observation.

The observation.images Fields

Let’s look at the camera signals first, as they provide the most intuitive understanding of the system. There are 3 cameras in total: 2 third-person views and 1 wrist view.

Defining the “Left”

We must be clear about what “left” means in the dataset schema. The schema includes:

  • exterior_image_1_left
  • exterior_image_2_left
  • wrist_image_left

It is a common pitfall to assume “left” means the camera is positioned on the left side of the robot’s base frame. Instead, “left” refers exclusively to the left lens of the Zed camera, which serves as the canonical camera feed.

A ZED camera has two lenses

A ZED camera has two lenses.

How Policies Use the Camera Views

Because DROID provides 3 camera views, you might assume all VLA models use all of them. The reality is a bit more complex😵‍💫.

Remark

Below, I refer to exterior_1 as the left-hand side camera and exterior_2 as the right-hand side camera relative to the robot base frame.

In the robot base frame, the left side has y>0y>0 and the right side has y<0y<0.

  • xx axis is red
  • yy axis is green
  • zz axis is blue

franka_robot_base_frame.webp


📌 OpenPI π0.5\pi_{0.5}-DROID

In its fine-tuning config, π0.5\pi_{0.5} only allocates slots for 2 camera views (one wrist, one exterior). However, it does not permanently drop either the left or right third-person camera.

During training, OpenPI randomly chooses between the left and right exterior cameras and feeds it into the policy’s single third-person input slot. The policy learns from both viewpoints but only processes one at a time. During inference, the operator must explicitly choose to roll out with either the left or right camera. See droid_rlds_dataset.py.


📌 NVIDIA GR00T N1.7-DROID

Conversely, nvidia/GR00T-N1.7-DROID explicitly drops the right-hand side exterior_2_left camera and only includes the left-hand side camera in its fine-tuning script.


📌 MolmoAct2-DROID

The final approach uses everything. The observation space for allenai/MolmoAct2-DROID includes all 3 cameras during both training and inference.


These examples show why we need to check a checkpoint’s actual inputs🔎, even when all 3 checkpoints are described as “DROID models”.

Observation: Proprioception

Within the LeRobot context, proprioceptive data is stored under observation.state. It consists of:

KeyShapeDtypeDescription
cartesian_position(6,)float64Flange frame: [x, y, z, roll, pitch, yaw]
joint_position(7,)float647 Franka joint angles
gripper_position(1,)float64Gripper closedness: 0 = open, 1 = closed

📌 End-effector pose

In DROID, the end-effector frame is the Franka flange frame, panda_link8. The Polymetis robot configuration names this link explicitly, and DROID’s robot interface computes its pose using forward kinematics.

In robotics, we often set the tool frame {T}\{T\} directly between the two gripper fingers as the end-effector frame. DROID, however, uses the flange frame, even with a Robotiq gripper attached.

Warning

⚠️ This is a critical detail! If your embodiment uses a different gripper, such as the Franka Hand, the UMI setup, or a generalist-style gripper, you must remember that DROID data is tracked at the flange frame. Understanding this ensures smoother transfer to your specific hardware.

Given this context, cartesian_position records the flange frame’s SE(3) pose measured in the robot’s base frame, with the rotation component represented as Euler angles.

Remark

More precisely, the Euler angles refer to X-Y-Z fixed angles(extrinsic Euler). See the conversion helpers, which uses SciPy’s lowercase xyz convention: extrinsic rotations about fixed axes.


📌 Joint position

This value simply records the joint states of the 7 joints of the Franka robot. ⚠️ Be aware that the joint space limits of the Franka Panda and the Franka Research 3 differ slightly, which is reflected in the dataset’s statistical bounds.


📌 Gripper position

This is a normalized value in [0,1].

If ww is the measured opening width and wmax⁡w_{\max} is the configured maximum width, the robot interface computes

g=1−wwmax⁡.g=1-\frac{w}{w_{\max}}.

Thus, g=0g=0 means fully open (max width: 85mm for Robotiq 2F-85, 80mm for Franka Hand), and g=1g=1 means fully closed.

Teleoperation

We have now covered the main observation fields. Before discussing the action fields in detail, let’s walk through DROID’s teleoperation process.

The big picture is: As the operator moves an Oculus Quest 2 controller, the DROID continuously controls the robot’s flange frame. It compares the controller’s motion (since a reference snapshot) with the robot’s motion (since its own snapshot), then commands the robot to reduce the gap.

Remark

The equations below describe the default Cartesian velocity teleoperation path. The gain and step-size constants are implementation settings, not universal properties of every DROID recording. The repository’s version records show that earlier collection code used different values.

Notation

Inspired by Russ Tedrake and Introduction to Robotics: Mechanics and Control, I will define the notation first, then apply it to the DROID pipeline.


📌 Position vector: pp

pp denotes a position vector. A position requires a reference point and axes. We write:

ApFC,{}^A p^C_F,

which reads as “the position of point CC, measured from point AA, with components expressed in frame FF‘s basis.”


📌 Omitting the subscript

FpA≡FpFA{}^F p^A \equiv {}^F p^A_F

When the “measured from” and “expressed in” frames are identical, we drop the subscript. Every position in this pipeline is expressed in the same frame it is measured from.


📌 Orientation: RR; pose: XX

RR denotes rotation: BRA{}^B R^A is the orientation of frame AA measured from frame BB. XX denotes a rigid transformation (a pose), which packages both:

BXA=(BRA,BpA).{}^B X^A = \left({}^B R^A, {}^B p^A\right).

Remark

Concretely, BXA{}^B X^A is a 4×44\times4 homogeneous transformation matrix: BRA{}^B R^A is the top-left 3×33\times3 block, and BpA{}^B p^A is the first 3 entries of the 4th column. BXA=[BRABpA0⊤1]∈SE(3).{}^B X^A=\begin{bmatrix}{}^B R^A & {}^B p^A\\\mathbf 0^\top & 1\end{bmatrix}\in SE(3).


📌 Symbols in DROID teleoperation pipeline

Let:

  • kk denote the control-step index.
  • {S}\{S\} denote the stationary Franka robot base frame (the world frame).
  • {E}\{E\} denote the end-effector (flange) frame.
  • {C}\{C\} denote the Oculus controller frame.
  • q[k],q˙[k]∈R7\mathbf q[k],\dot{\mathbf q}[k]\in\mathbb R^7 denote the measured joint positions and velocities.
  • g[k]∈[0,1]g[k]\in[0,1] denote measured gripper closedness.
  • SXE[k]{}^S X^E[k] denote the pose of {E}\{E\} measured from {S}\{S\}.

Applying the X=(R,p)X = (R, p) decomposition to the end-effector gives:

SXE[k]=(SRE[k],SpE[k]).{}^S X^E[k]=\left({}^S R^E[k],{}^S p^E[k]\right).

For an ideal rigid arm with a fixed base, its mechanical state can be written as

x[k]=[q[k]q˙[k]].\mathbf x[k]= \begin{bmatrix} \mathbf q[k]\\ \dot{\mathbf q}[k] \end{bmatrix}.
Remark

To connect this to hardware, you can read these directly from libfranka: C++ library for Franka Robotics research robots:

The flange pose is determined by forward kinematics:

SXE[k]=FK⁡(q[k]).{}^S X^E[k]=\operatorname{FK}(\mathbf q[k]).
Check the end-effector frame in libfranka

RobotState::O_T_EE is the pose of configured end-effector frame in Desk, which need not be the flange. Its F_T_EE field describes the transform from flange to end effector. In matrix form, the flange pose is O_T_EE multiplied by the inverse of F_T_EE. DROID instead computes the pose of its configured model link from joint angles. See the libfranka field definitions.

  • O_T_EE reads as “origin to end-effector”.
  • F_T_EE reads as “flange to end-effector”

TODO: add a diagram of Franka Desk

The proprioceptive part of the released RLDS observation

KeyShape
joint_position(7,)
cartesian_position(6,)
gripper_position(1,)

can therefore be written as

(q[k],SXE[k],g[k]).\left(\mathbf q[k],{}^S X^E[k],g[k]\right).

Defining the Control Boundaries

In control theory, we often write u[k]\mathbf u[k] for “the” control input. In robot learning, what we call an “action” depends entirely on where we draw the system boundary. Consider 3 distinct levels of control in this pipeline from top to bottom:

  1. The DROID conversion layer: The input is a processed teleoperation command.
  2. The low-level controllers: The input consists of absolute joint and gripper position targets (qd[k],gd[k])(\mathbf q^d[k], g^d[k]).
  3. The physical arm: The input is joint torques τ(t)\boldsymbol\tau(t).

The Journey of a Teleoperation Signal

One human motion, 4 recorded action spaces. After reading this section, you will understand how a single teleoperation signal cascades into all 4 kinds of action space that DROID records, in the order they are derived:

  1. cartesian_velocity
  2. cartesian_position
  3. joint_velocity
  4. joint_position

To spark your interest🥳, here are a few fun facts before we dive in.

  1. the cartesian_velocity is the “root” of other actions since Oculus Quest 2 is a 6DoF controller.
  2. the cartesian_position is a byproduct recorded alongside it and it is one of the two targets NVIDIA’s GR00T N1.7-DROID learns directly.
  3. the joint_velocity is the default finetuned action space by the famous π0.5\pi_{0.5}.
  4. the joint_position is featured in Physical Intelligence’s follow up work 📄PolaRiS: Scalable Real-to-Sim Evaluations for Generalist Robot Policies.

Sounds fun? Let’s trace the signal step by step!

Let me introduce some symbols that might be used in following section.

StageSymbol
Mapped controller poseSXC[k]{}^S X^C[k]
Normalized arm and gripper commandsacart[k],ag[k]\mathbf a^{\mathrm{cart}}[k],a_g[k]
Scaled Cartesian commandd[k]\mathbf d[k]
Recorded Cartesian targetSXEd[k]{}^S X^{E_d}[k]
Differential IK output, normalizedaq[k]\mathbf a^q[k]
Joint and gripper targetsqd[k],gd[k]\mathbf q^d[k],g^d[k]
Low-level arm controlτ(t)\boldsymbol\tau(t)
Next observed flange poseSXE[k+1]{}^S X^E[k+1]

1. The Human-VR Outer Loop: cartesian_velocity

The cartesian_velocity action can be defined as:

a[k]=[ap[k]aθ[k]ag[k]]∈[−1,1]7.\mathbf a[k]= \begin{bmatrix} \mathbf a_p[k]\\ \mathbf a_\theta[k]\\ a_g[k] \end{bmatrix}\in[-1,1]^7.

The fun fact is that the cartesian_velocity has nothing to do with standard physical velocity (like m/s or rad/s). What?🤯 Yes, you read that right. The literal “velocity” label tripped me up at first, too.

So what it is?🧐 Let’s break it down.

Tip

The big picture is: the recorded action action_dict/cartesian_velocity has two parts:

  • Indices 0-2 control translation.
  • Indices 3-5 control rotation.

The gripper has a separate scalar command.


📌 Tracking error

DROID’s VR controller uses proportional feedback. When we start teleoperation, we reset the motion reference and store the controller pose and robot pose. Call this reference step k0k_0📸.

For translation, positions are measured in meters, so each displacement below is also in meters:

ΔpC[k]=SpC[k]−SpC[k0],ΔpE[k]=SpE[k]−SpE[k0].\Delta p_C[k]={}^S p^C[k]-{}^S p^C[k_0],\qquad \Delta p_E[k]={}^S p^E[k]-{}^S p^E[k_0].
Remark

Intuitively,

  • ΔpC[k]\Delta p_C[k] measures the displacement(translation) of the Oculus Quest 2 controller from the latched pose.
  • ΔpE[k]\Delta p_E[k] measures the translation between the current end-effector frame and when it was at the time k0k_0.

The translation error, also in meters, is

ep[k]=ΔpC[k]−ΔpE[k]∈R3.\mathbf e_p[k]=\Delta p_C[k]-\Delta p_E[k]\in\mathbb R^3.
Example

If your hand has moved forward by 0.10 m and the robot has moved forward by 0.06 m, the translation error is 0.04 m.

For rotation, we have a similar concept. However, rather than simple subtraction, we need relative rotations:

ΔRC[k]=SRC[k](SRC[k0])⊤,ΔRE[k]=SRE[k](SRE[k0])⊤.\Delta R_C[k]={}^S R^C[k]\left({}^S R^C[k_0]\right)^\top,\qquad \Delta R_E[k]={}^S R^E[k]\left({}^S R^E[k_0]\right)^\top.

DROID forms the rotation error and converts it to Euler coordinates in radians:

Rerr[k]=ΔRC[k]ΔRE[k]⊤,eθ[k]=Euler⁡xyz ⁣(Rerr[k]).R_{\mathrm{err}}[k]=\Delta R_C[k]\Delta R_E[k]^\top,\qquad \mathbf e_\theta[k]=\operatorname{Euler}_{xyz}\!\left(R_{\mathrm{err}}[k]\right).
Remark

The code computes these products with quaternions, which is equivalent to the rotation-matrix calculation above. Here, Euler⁡xyz\operatorname{Euler}_{xyz} refers to X-Y-Z fixed angles (SciPy’s lowercase xyz convention). This is an Euler representation of the relative rotation, not a rotation-vector logarithm, therefore it still has Euler-angle branch cuts and singularities.


📌 Gains

Recall that we usually need to set KpK_p and KdK_d for a PD controller. The same logic applies to DROID error tracking. With the current defaults, DROID scales the errors as follows:

up[k]=Kpep[k],uθ[k]=Kθeθ[k],ug[k]=3eg[k].\mathbf u_p[k]=K_p\mathbf e_p[k],\qquad \mathbf u_\theta[k]=K_\theta\mathbf e_\theta[k],\qquad u_g[k]=3e_g[k].
Remark

Here, the equations follow the code’s numerical convention: ep\mathbf e_p contains position errors expressed in meters, and eθ\mathbf e_\theta contains Euler-angle errors expressed in radians.


📌 The L2L_2 norm

Translation and rotation are independently rescaled so that each Euclidean norm is at most 1:

ap[k]=up[k]max⁡(1,∥up[k]∥2),aθ[k]=uθ[k]max⁡(1,∥uθ[k]∥2).\mathbf a_p[k]=\frac{\mathbf u_p[k]}{\max(1,\lVert\mathbf u_p[k]\rVert_2)},\qquad \mathbf a_\theta[k]=\frac{\mathbf u_\theta[k]}{\max(1,\lVert\mathbf u_\theta[k]\rVert_2)}.
Example

Suppose up[k]=[1.2,0.9,0]⊤\mathbf u_p[k]=[1.2,0.9,0]^\top. Its norm is 1.22+0.92+02=1.5\sqrt{1.2^2+0.9^2+0^2}=1.5, so DROID divides the vector by 1.5. The result is ap[k]=[0.8,0.6,0]⊤\mathbf a_p[k]=[0.8,0.6,0]^\top: the direction stays the same, but the magnitude is capped.


📌 Scalar clipping

The gripper command is clipped to the interval [−1,1][-1,1]:

ag[k]=clip⁡(ug[k],−1,1).a_g[k]=\operatorname{clip}(u_g[k],-1,1).

The arm command is

acart[k]=[ap[k]aθ[k]]∈[−1,1]6.\mathbf a^{\mathrm{cart}}[k]= \begin{bmatrix} \mathbf a_p[k]\\ \mathbf a_\theta[k] \end{bmatrix}\in[-1,1]^6.

This is action_dict/cartesian_velocity, while ag[k]a_g[k] is action_dict/gripper_velocity.

cartesian_velocity is a normalized command

As I reminded you before, the field name can be misleading. These values are gain-scaled, saturated tracking errors. They do not directly measure how fast the Oculus controller or robot is moving. (i.e., it is NOT a physical velocity in standard SI).

2. Scaling and Recording the Target: cartesian_position

The outer loop gave us a normalized cartesian_velocity. Before this signal ever reaches a joint, DROID scales it back into physical units and uses it to construct an absolute pose which is recorded alongside everything else as action_dict/cartesian_position.

Remark

This construction, and the differential IK that follows it, is split between the IK wrapper and FrankaRobot.create_action_dict.


0️⃣ The cartesian_velocity

Recall that the cartesian_velocity is

acart[k]=[ap[k]aθ[k]]∈[−1,1]6.\mathbf a^{\mathrm{cart}}[k]= \begin{bmatrix} \mathbf a_p[k]\\ \mathbf a_\theta[k] \end{bmatrix}\in[-1,1]^6.

1️⃣ Scale the Cartesian command

The wrapper repeats the separate L2L_2 limits on translation and rotation, then applies the default scales

Lp=0.075 m,Lθ=0.15 rad.L_p=0.075\ \mathrm m,\qquad L_\theta=0.15\ \mathrm{rad}.

For the already bounded commands from Section 1, this gives

dp[k]=Lpap[k],dθ[k]=Lθaθ[k],d[k]=[dp[k]dθ[k]].\mathbf d_p[k]=L_p\mathbf a_p[k],\qquad \mathbf d_\theta[k]=L_\theta\mathbf a_\theta[k],\qquad \mathbf d[k]=\begin{bmatrix}\mathbf d_p[k]\\\mathbf d_\theta[k]\end{bmatrix}.
Tip

Now we finally feel a connection to the physical world again. The dimensionless “maximum” value of 1 in translation now becomes “0.075m / control step”.

Remark

These scales define the increments used to construct a Cartesian target on each update. They do not guarantee that the robot will move to the exact target during the next 15 Hz interval.


2️⃣ Construct the recorded Cartesian target

Let the subscript rr mark the robot state read when the command is resolved. DROID constructs

SpEd[k]=SprE[k]+dp[k],{}^S p^{E_d}[k]={}^S p_r^E[k]+\mathbf d_p[k], SREd[k]=Rxyz ⁣(dθ[k])SRrE[k].{}^S R^{E_d}[k]=R_{xyz}\!\left(\mathbf d_\theta[k]\right){}^S R_r^E[k].

Here, Rxyz(α)=Rz(αz)Ry(αy)Rx(αx)R_{xyz}(\boldsymbol\alpha)=R_z(\alpha_z)R_y(\alpha_y)R_x(\alpha_x) converts extrinsic xyz Euler angles to a rotation matrix. Translation is added in the base frame, and the incremental rotation is composed on the left.

The translation SpEd[k]{}^S p^{E_d}[k] and rotation SREd[k]{}^S R^{E_d}[k] together define the action_dict/cartesian_position.

The reference state is read at command time

A single control cycle performs two separate robot state reads:

  • p[k]\mathbf p[k]: the saved observation (read early, before computing the action).
  • pr[k]\mathbf p_r[k]: the fresh reference (read later by create_action_dict).

DROID constructs the target as pd[k]=pr[k]+dp[k]\mathbf p^d[k] = \mathbf p_r[k] + \mathbf d_p[k]. Why use the later read? 🤔 My guess is to ensure the intended motion is “fully expressed.”

Because the arm moves between reads, anchoring the command to the outdated p[k]\mathbf p[k] would shortchange the displacement. For example: if p[k]=0.50p[k]=0.50 m, the robot drifts to pr[k]=0.51p_r[k]=0.51 m by command time, and the desired delta dp[k]=0.02d_p[k]=0.02 m. A target of 0.520.52 m (based on p[k]p[k]) only yields 0.010.01 m of actual physical movement. Adding the delta directly to the fresh pr[k]p_r[k] ensures the full 0.020.02 m offset executes.

3. Differential IK: joint_velocity

Alright alright alright, we see the word “velocity” again in joint_velocity. Does it also lack a standard physical meaning? Is it not in SI units?

Correct, joint_velocity is also normalized! :- )

DROID uses dm_robotics’s Cartesian6dVelocityEffector for differential IK, where the input and output can be viewed as

(q,q˙,ξd)  ⟶  q˙d,(\mathbf q,\dot{\mathbf q}, \boldsymbol\xi^d) \;\longrightarrow\; \dot{\mathbf q}^d,

where

  • input includes
    • q\mathbf q: the current joint position
    • q˙\dot{\mathbf q}: the current joint velocity
    • ξd\boldsymbol\xi^d: the desired twist
  • output includes
    • q˙d\dot{\mathbf q}^d: the desired joint velocity

Let FF denote the DROID wrapper’s calculation. It treats the measured joint positions qr[k]\mathbf q_r[k], measured velocities q˙r[k]\dot{\mathbf q}_r[k], and the scaled 6-vector d[k]\mathbf d[k] as input. The wrapper computes

bq[k]=F ⁣(qr[k],q˙r[k],d[k]),aq[k]=bq[k]Lq,\mathbf b^q[k]=F\!\left(\mathbf q_r[k],\dot{\mathbf q}_r[k],\mathbf d[k]\right),\qquad \mathbf a^q[k]=\frac{\mathbf b^q[k]}{L_q},

where Lq=0.2 radL_q=0.2\ \mathrm{rad} is the default joint-step scale.

Remark

A few things are worth mentioned⭐️:

  • The scaled 6-vector d[k]\mathbf d[k](cartesian_velocity) is not rigorously the twist we know from robotics textbook like Modern Robotics: Mechanics, Planning, and Control. It’s more of a “Cartesian increment”.
  • Vice versa, the dm_robotics solver’s numerical output is treated as a joint increment bq[k]\mathbf b^q[k] in radians.

And ladies and gentlemen, the normalized result aq[k]\mathbf a^q[k] is saved as action_dict/joint_velocity.


📌Where does the “normalized” come from?

The joint_velocity aq[k]\mathbf a^q[k] is normalized such that

−1≤aq[k]≤1.-1 \leq \mathbf a^q[k] \leq 1.

Looking at the equation aq[k]=bq[k]Lq\displaystyle \mathbf a^q[k]=\frac{\mathbf b^q[k]}{L_q}, this bounded range might not be obvious. It works because DROID sets the same numerical limit for all 7 joints at IK configuration:

self.relative_max_joint_delta = np.array([0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2])

This array is passed to Cartesian6dVelocityEffector. Because the effector intrinsically bounds its own output. The joint increment bq[k]\mathbf b^q[k] produced by FF is already constrained to [-0.2, 0.2]rad. Therefore

∣biq[k]∣≤Lq⟹∣aiq[k]∣=∣biq[k]∣Lq≤1.|b_i^q[k]|\le L_q \quad\Longrightarrow\quad |a_i^q[k]|=\frac{|b_i^q[k]|}{L_q}\le1.

4. Resolving the Joint Target: joint_position

DROID converts the joint_velocity command into an increment using Lq=0.2 radL_q=0.2\,\mathrm{rad} for every joint, applies infinity norm saturation if needed, and adds it to the reference configuration:

Δq[k]=Lqaq[k]max⁡(1,∥aq[k]∥∞),qd[k]=qr[k]+Δq[k].\Delta\mathbf q[k]=L_q\frac{\mathbf a^q[k]}{\max(1,\lVert\mathbf a^q[k]\rVert_\infty)},\qquad \mathbf q^d[k]=\mathbf q_r[k]+\Delta\mathbf q[k].

From last section, we know that aq[k]\mathbf a^q[k] is already normalized such that ∥aq[k]∥∞≤1\lVert\mathbf a^q[k]\rVert_\infty\le1. Therefore, the preceding equation could be simplified as:

Δq[k]=Lqaq[k]=bq[k],qd[k]=qr[k]+bq[k].\Delta\mathbf q[k]=L_q\mathbf a^q[k]=\mathbf b^q[k],\qquad \mathbf q^d[k]=\mathbf q_r[k]+\mathbf b^q[k].
Tip

Intuitively, it caps each joint increment Δq[k]\Delta\mathbf q[k] is at most 0.2 rad0.2\,\mathrm{rad} in magnitude. Note that this operation alone does not enforce the physical joint limits qmin⁡,qmax⁡q_{\min}, q_{\max} of the robot.

IK(cartesian_position) does NOT guarantee joint_position

As you might suspect, running pose-based IK on the recorded cartesian_position, even when seeded with the observed joint configuration, is not guaranteed to reproduce the recorded joint_position. Reproduction requires following DROID’s differential-IK and joint target construction path.

5. The Common Pitfall

We now have the joint target qd[k]\mathbf q^d[k]. They are sent to their low-level controllers, but the robot does not reach them instantly.

The arm controller produces motor torques τ(t)\boldsymbol\tau(t), while the physical response depends on inertia, gravity, friction, contact, latency, etc. The DROID outer loop targets 15 Hz, while the Franka hardware client is configured for 1 kHz.

We therefore cannot assume

SXE[k+1]=SXEd[k].{}^S X^E[k+1]={}^S X^{E_d}[k].
Commanded action versus observed motion

You generally cannot recover the commanded action by subtracting two consecutive observations. In particular, the expression

SpE[k+1]−SpE[k]Lp\frac{{}^S p^E[k+1]-{}^S p^E[k]}{L_p}

measures a scaled observed displacement. It is NOT generally equal to ap[k]\mathbf a_p[k]. They may coincide in a special case, but this is not a reliable way to reconstruct actions.

Where Do VLA Models Plug In?

A learned policy takes observations o[k]\mathbf o[k] and a language instruction ℓ\ell, then predicts an action chunk. The training target determines which part of the control pipeline it learns to imitate.

Remark

A hat marks a policy prediction. I write a^[k+j∣k]\hat{\mathbf a}[k+j\mid k] for the action intended for step k+jk+j, predicted using information available at step kk. See also Understanding Action Chunking with Flow Matching for how action chunk is used in robot learning.


📌 OpenPI π0.5\pi_{0.5}-DROID: joint velocity

The original pi05_droid checkpoint’s deployment path uses normalized joint velocity commands and absolute gripper position targets:

a^[k+j∣k]=[a^q[k+j∣k]g^d[k+j∣k]]∈R8.\hat{\mathbf a}[k+j\mid k]= \begin{bmatrix} \hat{\mathbf a}^{q}[k+j\mid k]\\ \hat g^d[k+j\mid k] \end{bmatrix}\in\mathbb R^8.
  • In training: the arm prediction corresponds to the output of differential IK, before the joint target resolver.
  • In deployment, DROID converts that normalized joint command into a desired absolute joint position using a fresh robot state.

📌 OpenPI π0.5\pi_{0.5}-DROID: joint position

There are 2 pi05_droid checkpoints, not 1

It’s easy to miss this🙃: Physical Intelligence ships pi05_droid (joint velocity) and a later pi05_droid_jointpos (joint position), introduced alongside 📄PolaRiS: Scalable Real-to-Sim Evaluations for Generalist Robot Policies. The velocity checkpoint is the widely-cited default while the position checkpoint is the one the training guide recommends for simulation-friendly training. This is also why the RLDS loader defaults to joint position targets.

For the joint position variant, the policy output transform converts predicted joint deltas into absolute targets, so the deployed action is

a^[k+j∣k]=[q^d[k+j∣k]g^d[k+j∣k]]∈R8.\hat{\mathbf a}[k+j\mid k]= \begin{bmatrix} \hat{\mathbf q}^d[k+j\mid k]\\ \hat g^d[k+j\mid k] \end{bmatrix}\in\mathbb R^8.

Its arm output corresponds to the joint position target after DROID’s joint target resolver and can be executed with action_space="joint_position", without differential IK or the normalized joint command rescaling. The PolaRiS checkpoint list lists this base checkpoint separately from pi05_droid_jointpos_polaris, which adds further training on a mixture of simulated and DROID data.


📌 NVIDIA GR00T N1.7-DROID

Unlike OpenPI, GR00T’s DROID configuration does something challenging: it learns to predict both the end-effector pose(cartesian_position) AND the joint angles (joint_position) simultaneously, alongside an absolute gripper target.

The Rollout Trap: Cartesian and Joint targets are NOT interchangeable

It is incredibly easy to misunderstand this during deployment. You might assume that GR00T’s predicted cartesian_position and joint_position are geometrically identical. Meaning, you could just run inverse kinematics on the predicted Cartesian pose to get the exact predicted joint angles.

They are not. As explained earlier, DROID processes these two spaces through entirely distinct clipping and differential IK paths. Because GR00T predicts both of these separately derived targets, you must be extremely careful during rollout. You have to explicitly decide which action space drives your low-level controller, and you cannot assume the two predicted outputs will perfectly mirror each other.

Data formatting notes
  • Relative actions: GR00T learns these as relative targets (relative to the latest observation in the action chunk). See Action Representations.
  • Dataset source: GR00T uses the LeRobot format, not raw RLDS. The DROID-to-LeRobot converter explicitly copies action_dict/cartesian_position and action_dict/joint_position into separate fields to allow this dual-prediction setup.

Which Settings Must Match Your Action Space?

💬Question

I want to collect DROID-like data with your own controller. Do I have to copy KpK_p, KθK_\theta, LpL_p, LθL_\theta, and LqL_q exactly🧐?

🗣Answer

It depends entirely on which field your model learns to predict.


DROID-trained checkpoints are the “free lunch”. In order to use them, we should align the generation and meaning of our chosen action labels with DROID. Here we focus on data collection through DROID-style Cartesian teleoperation and only the 5 parameters Kp,Kθ,Lp,Lθ,LqK_p,K_\theta,L_p,L_\theta,L_q.

Warning

Using exact Kp,Kθ,Lp,Lθ,LqK_p,K_\theta,L_p,L_\theta,L_q parameters does not guarantee you will get 100% out of the pre-trained checkpoint, but ignoring them almost guarantees you will struggle to use them effectively.

Recorded action fieldKpK_pKθK_\thetaLpL_pLθL_\thetaLqL_q
cartesian_velocity✅✅✅✅❌
cartesian_position✅✅✅✅❌
joint_velocity✅✅✅✅✅
joint_position✅✅✅✅✅

See also...