File size: 21,717 Bytes
700dd75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
# Kinematic Planner ONNX Model Reference

This page provides a detailed specification of the **Kinematic Planner** ONNX model inputs and outputs. The kinematic planner is the core motion generation component of the GEAR-SONIC system: given the robot's current state and high-level navigation commands, it produces a sequence of future whole-body poses (MuJoCo `qpos` frames) that the low-level whole-body controller then tracks.

The ONNX model is part of the **C++ inference stack** and is called by the deployment runtime during operation. The C++ stack manages input construction, timing, and state management β€” certain combinations of inputs are invalid and are handled by the C++ layer to ensure safe operation. This page is intended for developers who want to understand the model interface at a deeper level or build custom integrations beyond the standard deployment pipeline.

```{admonition} Training Code & Technical Report
:class: note
The kinematic planner training code and technical report will be released soon. This page documents the ONNX model interface for deployment integration.
```

---

## Overview

The planner takes **11 input tensors** and produces **2 output tensors**. The 6 primary inputs are listed below; the remaining 5 are <a href="#advanced-inputs">advanced inputs</a> managed by the C++ stack and should not need to be modified in most cases.

**Primary inputs:**

| Tensor Name | Shape | Dtype | Default |
|-------------|-------|-------|---------|
| `context_mujoco_qpos` | `[1, 4, 36]` | `float32` | Required |
| `target_vel` | `[1]` | `float32` | `-1.0` (use mode default velocity) |
| `mode` | `[1]` | `int64` | Required |
| `movement_direction` | `[1, 3]` | `float32` | Required |
| `facing_direction` | `[1, 3]` | `float32` | Required |
| `height` | `[1]` | `float32` | `-1.0` (disable height control) |

**Outputs:**

| Tensor Name | Shape | Dtype |
|-------------|-------|-------|
| `mujoco_qpos` | `[1, N, 36]` | `float32` |
| `num_pred_frames` | scalar | `int64` |

Where:
- **K** = `max_tokens - min_tokens + 1` (model-dependent; the range of allowed prediction horizons)
- **N** = maximum number of output frames (padded); only the first `num_pred_frames` frames are valid

---

## Coordinate System

The model operates in **MuJoCo's Z-up coordinate convention**:

- **X** β€” forward
- **Y** β€” left
- **Z** β€” up

All position and direction vectors in the inputs and outputs follow this convention.

---

## Input Tensors

(context_mujoco_qpos)=
### `context_mujoco_qpos`

| Property | Value |
|----------|-------|
| **Shape** | `[1, 4, 36]` |
| **Dtype** | `float32` |
| **Description** | The planner's context input consisting of 4 consecutive MuJoCo `qpos` frames representing the recent states of the robot |

This is the primary context input. It provides 4 frames of the robot's recent joint configuration at the simulation framerate.
The 36 dimensions of each frame are the standard MuJoCo `qpos` vector for the Unitree G1 (29-DOF) model:

| Index | Field | Description |
|-------|-------|-------------|
| 0–2 | Root position | `(x, y, z)` in meters, Z-up world frame |
| 3–6 | Root quaternion | `(w, x, y, z)` orientation β€” MuJoCo convention |
| 7–35 | DOF positions | 29 joint angles in radians, following MuJoCo body tree order |

```{admonition} Coordinate Frame
:class: note
All inputs β€” including `context_mujoco_qpos`, `movement_direction`, `facing_direction`, `specific_target_positions`, and `specific_target_headings` β€” should be provided in the **world coordinate frame**. The root quaternion uses MuJoCo's `(w, x, y, z)` ordering at indices 3 to 6. The model handles canonicalization internally.
```

### `target_vel`

| Property | Value |
|----------|-------|
| **Shape** | `[1]` |
| **Dtype** | `float32` |
| **Description** | Desired locomotion speed override |

Controls the target movement speed. When set to **zero or below** (e.g., `-1.0`), the model uses the default velocity for the selected mode. When set to a **positive value**, it overrides the mode's default speed (in meters per second). Note that the actual achieved speed may differ from the target due to the critically damped spring model and motion dynamics.

| Value | Behavior |
|-------|----------|
| `<= 0.0` | Use the default velocity for the selected `mode` |
| `> 0.0` | Override with this target velocity (m/s) |


### `mode`

| Property | Value |
|----------|-------|
| **Shape** | `[1]` |
| **Dtype** | `int64` |
| **Description** | Index selecting the motion style/behavior |

Selects the motion style from the pre-loaded clip library. The mode index is clamped to the number of available clips at runtime. The default planner ships with the following modes:

**Locomotion set:**

| Index | Mode | Description |
|-------|------|-------------|
| 0 | `idle` | Standing still |
| 1 | `slowWalk` | Slow forward locomotion |
| 2 | `walk` | Normal walking speed |
| 3 | `run` | Running |

**Squat / ground set:**

| Index | Mode | Description |
|-------|------|-------------|
| 4 | `squat` | Squatting β€” requires `height` input (range ~0.4–0.8m) |
| 5 | `kneelTwoLeg` | Kneeling on both knees β€” requires `height` input (0.2m-0.4m) |
| 6 | `kneelOneLeg` | Kneeling on one knee β€” requires `height` input (0.2m-0.4m) |
| 7 | `lyingFacedown` | Lying face down β€” requires `height` input |
| 8 | `handCrawling` | Crawling on hands and knees |
| 14 | `elbowCrawling` | Crawling on elbows (more likely to overheat) |

**Boxing set:**

| Index | Mode | Description |
|-------|------|-------------|
| 9 | `idleBoxing` | Boxing stance (idle) |
| 10 | `walkBoxing` | Walking with boxing guard |
| 11 | `leftJab` | Left jab |
| 12 | `rightJab` | Right jab |
| 13 | `randomPunches` | Random punch sequence |
| 15 | `leftHook` | Left hook |
| 16 | `rightHook` | Right hook |

**Style walks:**

| Index | Mode | Description |
|-------|------|-------------|
| 17 | `happy` | Happy walking |
| 18 | `stealth` | Stealthy walking |
| 19 | `injured` | Limping walk |
| 20 | `careful` | Cautious walking |
| 21 | `objectCarrying` | Walking with hands reaching out |
| 22 | `crouch` | Crouched walking |
| 23 | `happyDance` | Dancing walk (only walk forward) |
| 24 | `zombie` | Zombie walk |
| 25 | `point` | Walking with hands pointing |
| 26 | `scared` | Scared walk |

### `movement_direction`

| Property | Value |
|----------|-------|
| **Shape** | `[1, 3]` |
| **Dtype** | `float32` |
| **Description** | Desired direction of movement in the MuJoCo world frame |

A 3D direction vector `(x, y, z)` in the Z-up world coordinate system indicating where the robot should move. It is recommended to pass a normalized vector for good practice, though the model normalizes internally. Speed is controlled by `target_vel` and `mode`, not by the magnitude of this vector.

- The planner uses the `(x, y)` components (horizontal plane) for computing the target root trajectory via a critically-damped spring model.
- When the magnitude is near zero (`< 1e-5`), the model falls back to using the `facing_direction` with a small scaling factor for in-place turning.


### `facing_direction`

| Property | Value |
|----------|-------|
| **Shape** | `[1, 3]` |
| **Dtype** | `float32` |
| **Description** | Desired facing (heading) direction in the MuJoCo world frame |

A 3D direction vector `(x, y, z)` indicating which direction the robot's torso should face. The target heading angle is computed as `atan2(y, x)` from this vector. Like `movement_direction`, this does not need to be normalized.

This is independent of `movement_direction` β€” the robot can walk in one direction while facing another (e.g., strafing).


### `height`

| Property | Value |
|----------|-------|
| **Shape** | `[1]` |
| **Dtype** | `float32` |
| **Description** | Desired root height for height-aware behaviors |

Controls the target pelvis height for modes that support variable height (e.g., `squat`, `kneelTwoLeg`, `kneelOneLeg`, `lyingFacedown`). When a positive value is provided, the model searches the reference clip's keyframes and selects the one whose root height is closest to the requested value, using it as the target pose for motion generation.

| Value | Behavior |
|-------|----------|
| `< 0.0` | Height control disabled; use the randomly-selected keyframe from the reference clip |
| `>= 0.0` | Find the closest height keyframe in the reference clip and use it as the target pose (meters) |


<div id="advanced-inputs"></div>

## Advanced Inputs

These inputs are managed internally by the C++ deployment stack and **should not be modified** under normal operation. They are documented here for completeness and for advanced users who need to build custom integrations.

### `random_seed`

| Property | Value |
|----------|-------|
| **Shape** | `[1]` |
| **Dtype** | `int64` |
| **Description** | Seed for controlling network randomness |


### `has_specific_target`

| Property | Value |
|----------|-------|
| **Shape** | `[1, 1]` |
| **Dtype** | `int64` |
| **Description** | Flag indicating whether specific waypoint targets are provided |

| Value | Behavior |
|-------|----------|
| `0` | Ignore `specific_target_positions` and `specific_target_headings`; use `movement_direction` / `facing_direction` |
| `1` | Use the provided specific target positions and headings as waypoint constraints |

When enabled, the spring model's target root position and heading are overridden by the values in `specific_target_positions` and `specific_target_headings`.


### `specific_target_positions`

| Property | Value |
|----------|-------|
| **Shape** | `[1, 4, 3]` |
| **Dtype** | `float32` |
| **Description** | 4 waypoint positions in MuJoCo world coordinates |

Each waypoint is a 3D position `(x, y, z)` in the Z-up world frame. The 4 waypoints correspond to 4 frames (one token's worth) of target root positions. Only used when `has_specific_target = 1`.


### `specific_target_headings`

| Property | Value |
|----------|-------|
| **Shape** | `[1, 4]` |
| **Dtype** | `float32` |
| **Description** | 4 waypoint heading angles in radians |

Target heading (yaw) angles for each of the 4 waypoint frames. These are absolute angles in the Z-up world frame, measured as rotation around the Z-axis. Only used when `has_specific_target = 1`. The last waypoint's heading (`[:, -1]`) is used as the primary target heading for the spring model.


### `allowed_pred_num_tokens`

| Property | Value |
|----------|-------|
| **Shape** | `[1, K]` where `K = max_tokens - min_tokens + 1` |
| **Dtype** | `int64` |
| **Description** | Binary mask controlling the allowed prediction horizon |

A binary mask where each element corresponds to a possible number of predicted tokens. Index `i` maps to `min_tokens + i` tokens. A value of `1` means that prediction length is allowed; `0` means it is disallowed.

Since each token represents 4 frames, the prediction horizon in frames is `num_tokens * 4`. In our default planner we have `min_tokens = 6` and `max_tokens = 16`:

| Index | Tokens | Frames |
|-------|--------|--------|
| 0 | 6 | 24 |
| 1 | 7 | 28 |
| 2 | 8 | 32 |
| 3 | 9 | 36 |
| 4 | 10 | 40 |
| 5 | 11 | 44 |
| 6 | 12 | 48 |
| 7 | 13 | 52 |
| 8 | 14 | 56 |
| 9 | 15 | 60 |
| 10 | 16 | 64 |

---

## Output Tensors

### `mujoco_qpos`

| Property | Value |
|----------|-------|
| **Shape** | `[1, N, 36]` |
| **Dtype** | `float32` |
| **Description** | Predicted motion sequence as MuJoCo `qpos` frames |

The primary output: a sequence of whole-body pose frames in the same 36-dimensional MuJoCo `qpos` format as the input (see {ref}`context_mujoco_qpos <context_mujoco_qpos>` for the dimension layout).

```{admonition} Important: Use num_pred_frames to Truncate
:class: warning
The output tensor `mujoco_qpos` is **not truncated** β€” it contains the full padded buffer. Only the first `num_pred_frames` frames are valid predictions. When consuming this output, always slice:
```

```python
valid_qpos = mujoco_qpos[:, :num_pred_frames, :]
```

The poses are in the **global MuJoCo world frame** (not canonicalized). The model internally handles canonicalization, inference, and coordinate conversion, then transforms the output back to the original world frame. The first 4 predicted frames are blended with the input context for smooth transitions.

The root quaternion in the output uses `(w, x, y, z)` ordering (MuJoCo convention).


### `num_pred_frames`

| Property | Value |
|----------|-------|
| **Shape** | scalar |
| **Dtype** | `int64` |
| **Description** | Number of valid predicted frames in the `mujoco_qpos` output |

This value equals `num_pred_tokens * 4`, where `num_pred_tokens` is the number of motion tokens the model decided to generate (constrained by `allowed_pred_num_tokens`). Use this value to slice the `mujoco_qpos` output.

---

## Internal Pipeline

1. **Canonicalization** β€” The input qpos is transformed to a body-relative frame by removing the first frame's heading rotation and horizontal position. This helps the model generalize across different starting orientations and positions.

2. **Spring Model** β€” A critically-damped spring model generates smooth target root trajectories and heading angles from the high-level commands, using mode-dependent average velocities from the training clips.

3. **Target Pose Selection** β€” Based on the `mode` and `random_seed`, a target pose is fetched from the pre-loaded clip library and aligned (rotated/translated) to match the spring model's predicted target position and heading.

4. **Motion Inference** β€” The core motion model fills in the motion between the context (current state) and target (desired future state), producing a natural transition.

5. **Post-processing** β€” The output is converted back to MuJoCo qpos in the original world frame, and the first 4 frames are blended with the input context for smooth transitions.

---

## Deployment Integration

This section describes how the C++ deployment stack uses the planner at runtime. Understanding this is useful for building custom integrations or modifying the replan behavior.

### Threading Model

The planner runs on a **dedicated thread at 10 Hz** (`planner_dt = 0.1s`), separate from the control loop (50 Hz) and input thread (100 Hz). The planner thread:

1. Reads the latest `MovementState` from a thread-safe buffer (written by the input interface).
2. Decides whether a replan is needed.
3. If so, calls `UpdatePlanning()` which runs TensorRT inference.
4. Stores the result in a shared buffer that the control thread picks up on its next tick.

### Initialization

When the planner is first enabled (e.g., pressing **ENTER** on the keyboard interface), the planner thread:

1. Reads the robot's current base quaternion and joint positions from the latest `LowState`.
2. Calls `Initialize()`, which:
   - Sets up a 4-frame context at the default standing height with zero-yaw orientation.
   - Runs an initial inference with `IDLE` mode and no movement.
   - Resamples the 30 Hz output to 50 Hz.
3. The control thread detects the new planner motion and switches `current_motion_` to the planner output.

### Context Construction

The planner requires a 4-frame context (`context_mujoco_qpos` of shape `[1, 4, 36]`). During operation, this context is sampled from the **current planner motion** (not the robot state):

- The context starts at `gen_frame = current_frame + motion_look_ahead_steps` (default look-ahead = 2 frames at 50 Hz).
- 4 frames are sampled at 30 Hz intervals from this starting point.
- Joint positions, body positions, and quaternions are linearly interpolated (quaternions via slerp) between 50 Hz motion frames to produce the 30 Hz context samples.

### Replan Logic

Not every planner tick triggers a replan. The decision follows this priority:

**1. Always replan when** (regardless of static/non-static mode):
- Locomotion mode changed
- Facing direction changed
- Height changed

**2. For non-static modes only**, also replan when any of the following is true:
- Movement speed changed
- Movement direction changed
- Periodic replan timer expired **and** movement speed is non-zero

Static modes (Idle, Squat, Kneel, Lying, Idle Boxing) **never** trigger replans from the second category β€” they only replan on mode/facing/height changes from the first category.

**Replan intervals** (periodic timer) vary by locomotion type to balance responsiveness and computational cost:

| Locomotion Type | Replan Interval |
|----------------|-----------------|
| Running | 0.1 s (every planner tick) |
| Crawling | 0.2 s |
| Boxing (punches, hooks) | 1.0 s |
| All others (walk, squat, styled, etc.) | 1.0 s |

The periodic timer only triggers a replan if the current movement speed is non-zero β€” a stationary robot in a non-static mode (e.g., Walk mode with speed 0) will not replan on the timer.

### Output Resampling (30 Hz β†’ 50 Hz)

The planner model outputs frames at **30 Hz**. The deployment stack resamples them to **50 Hz** (the control loop rate) using linear interpolation:

- For each 50 Hz frame, compute the corresponding fractional 30 Hz frame index.
- Linearly interpolate joint positions and body positions between the two nearest 30 Hz frames.
- Slerp-interpolate body quaternions.
- Compute joint velocities by finite differencing the resampled positions (`(pos[t+1] - pos[t]) * 50`).

The resampled motion is stored in `planner_motion_50hz_` and has `num_pred_frames * 50/30` frames (rounded down).

### Animation Blending

When a new planner output arrives while the previous one is still playing, the control thread **blends** the old and new animations over an 8-frame cross-fade:

1. The old animation is rebased so `current_frame` maps to frame 0.
2. The new animation is aligned to start at `gen_frame - current_frame` in the rebased timeline.
3. Over 8 frames starting from the blend point, a linearly increasing weight `w_new` (0 β†’ 1) is applied:
   - Joint positions/velocities: `w_old * old + w_new * new`
   - Body positions: `w_old * old + w_new * new`
   - Body quaternions: `slerp(old, new, w_new)`
4. After the blend region, the new animation takes over completely.
5. `current_frame` is reset to 0 on the blended result.

This ensures smooth transitions between successive planner outputs without visible discontinuities.

### TensorRT Acceleration

The planner runs via **TensorRT** with CUDA graph capture for low-latency inference:

1. At startup, the ONNX model is converted to a TensorRT engine (cached on disk).
2. A **CUDA graph** is captured during initialization β€” this records the entire inference pass (input copy β†’ kernel launches β†’ output copy) as a single replayable graph.
3. On each replan, inputs are copied to GPU via pinned memory (`TPinnedVector`), the CUDA graph is launched, and outputs are copied back.
4. FP16 precision is supported via `--planner-precision 16` (default is FP32).

### Planner Model Versions

The deployment stack supports multiple planner model versions, auto-detected from the model filename:

| Version | Inputs | Modes | Description |
|---------|--------|-------|-------------|
| V0 | 6 | 4 (Idle, Slow Walk, Walk, Run) | Basic locomotion only |
| V1 | 11 | 20 | Adds squat/kneel/boxing/styled walks + height control + waypoint targets |
| V2 | 11 | 27 | All V1 modes + additional styled walking modes |

The version is determined by the presence of `V0`, `V1`, or `V2` in the planner model filename. Version determines:
- The number of input tensors (6 vs 11)
- The valid range of `mode` values
- Whether `height`, `has_specific_target`, `specific_target_positions`, `specific_target_headings`, and `allowed_pred_num_tokens` inputs are used

---

## Model Properties

The exported ONNX model has the following properties:

- **ONNX opset version**: 17
- **Batch size**: 1 (fixed)

The model is distributed as a single `.onnx` file, along with a `.pt` file containing reference input/output tensors that can be used for validation.

```{admonition} Coming Soon
:class: note
The training code, export tooling, and a full technical report will be released soon. Stay tuned for updates.
```

---

## Usage Example

```python
import onnxruntime as ort
import numpy as np

# Load the ONNX model
session = ort.InferenceSession("kinematic_planner.onnx")

# Primary inputs
inputs = {
    "context_mujoco_qpos": current_qpos_buffer.astype(np.float32),           # [1, 4, 36]
    "target_vel": np.array([-1.0], dtype=np.float32),                         # -1.0 = use mode default
    "mode": np.array([2], dtype=np.int64),                                    # 2 = walk
    "movement_direction": np.array([[1.0, 0.0, 0.0]], dtype=np.float32),      # forward
    "facing_direction": np.array([[1.0, 0.0, 0.0]], dtype=np.float32),        # face forward
    "height": np.array([-1.0], dtype=np.float32),                             # -1.0 = disabled

    # Advanced inputs (typically managed by the C++ stack)
    "random_seed": np.array([1234], dtype=np.int64),
    "has_specific_target": np.array([[0]], dtype=np.int64),
    "specific_target_positions": np.zeros([1, 4, 3], dtype=np.float32),
    "specific_target_headings": np.zeros([1, 4], dtype=np.float32),
    "allowed_pred_num_tokens": np.ones([1, 11], dtype=np.int64),              # K = 11 for default model
}

# Run inference
mujoco_qpos, num_pred_frames = session.run(None, inputs)

# Extract valid frames only
num_frames = int(num_pred_frames)
predicted_motion = mujoco_qpos[0, :num_frames, :]  # [num_frames, 36]

# Each row: [x, y, z, qw, qx, qy, qz, 29 joint angles in radians]
for frame in predicted_motion:
    root_pos = frame[:3]
    root_quat = frame[3:7]       # (w, x, y, z)
    joint_angles = frame[7:36]   # 29 DOF positions
```