Skip to content

Kinematic trajectory recording + post-hoc video replay (#296) - #297

Open
sibocw wants to merge 11 commits into
dev-v2.1.1from
pseudo-rendering
Open

Kinematic trajectory recording + post-hoc video replay (#296)#297
sibocw wants to merge 11 commits into
dev-v2.1.1from
pseudo-rendering

Conversation

@sibocw

@sibocw sibocw commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Closes #296.

Decouples how many worlds are simulated from how many are rendered. Instead of rasterizing frames during simulation, a recorder stores only generalized coordinates (qpos, plus mocap poses) at the render cadence. The trajectory is replayed to video later — on CPU or GPU. This sidesteps buffering full (n_worlds, n_cams, H, W, 3) RGB tensors, which is hopeless at thousands of worlds: simulate many in parallel, then sub-select and render a few.

Architecture

Recordingrecord_trajectory_only=True on set_renderer swaps in a recorder (a recorder is just a renderer whose per-frame output is a qpos slice, not pixels):

  • RecordedTrajectory — backend-agnostic dataclass (qpos + optional mocap + replay metadata). .save()/.load() are one self-describing .npz each.
  • TrajectoryRecorder(Renderer) — CPU single-world (Simulation).
  • WarpTrajectoryRecorder(_BaseWarpRenderer) — GPU multi-world (GPUSimulation), one RecordedTrajectory per selected world, identical format to the CPU recorder.

Replay — set qpos, run position-only kinematics (mj_kinematics + mj_camlight, not a full forward step), rasterize:

  • flygym.rendering.render_trajectories(mj_model, trajectories, ...) — CPU.
  • flygym.warp.rendering.render_trajectories_gpu(mj_model, trajectories, ...) — GPU batch replay; bin-packs (trajectory, frame) work into mjw.Data batches of worlds_per_batch.

A trajectory recorded on either backend replays on either backend (all four combinations), because it carries no trace of how it was produced.

Serializationsave_trajectories/load_trajectories write/read a folder of individual traj_XXXX.npz files.

Deviations from the design notes in #296

The implementation intentionally diverges from the serialization design sketched in the issue thread; see the issue comment for the full rationale. In short:

  • The model is not saved with the trajectory. Trajectories are pure kinematic state; the caller persists the model separately (world.save_xml_with_assets(...)) and passes a compiled MjModel into the replay functions. This is the headline signature change vs. the issue's save_trajectories(trajectories, mj_model, dir) / render_trajectories(source, ...).
  • No model hash / no MJB pathway / no manifest. A compiled-model hash can't survive the XML round-trip or the GPU batch-render edits, so it had no working consumer once the model was decoupled; an nq/nmocap shape check guards mismatches instead (robust across both).
  • CPU and GPU are separate functions (render_trajectories vs render_trajectories_gpu) rather than one function with a use_gpu flag — matching the flygym / flygym.warp split elsewhere.
  • GPU batch-render prep is the caller's responsibility: render_trajectories_gpu expects a batch-ready model (modify_world_for_batch_rendering, now exported from flygym.warp); a use_gpu_batch_rendering=True GPUSimulation already provides one.
  • n_workers CPU multiprocessing was dropped (untested, and re-introduced a model-transport pathway); easy to restore if wanted.

Refactor

rendering.py and warp/rendering.py become packages, each split into live_rendering and recorded_trajectory submodules, with backward-compatible re-exports (from flygym.rendering import Renderer etc. unchanged). docs/SUMMARY.md updated for the new module layout.

Tests

tests/core/test_trajectory.py and tests/warp/test_trajectory.py: CPU/GPU recording, npz round-trip, CPU and GPU replay, incompatible-model guard, and a cross-backend kinematics-identity check (geom_xpos from CPU mj_kinematics vs GPU mjw.kinematics). The CPU replay test asserts a bit-exact match against a qpos-consistent inline render. Full suite: 647 passed, 1 skipped; ruff clean.

🤖 Generated with Claude Code

Decouple how many worlds are *simulated* from how many are *rendered*: a
recorder stores only qpos (plus mocap poses) at the render cadence instead
of rasterizing frames, and the trajectory is replayed to video later on CPU
or GPU. This lets e.g. thousands of GPU-simulated worlds be sub-selected and
rendered with a small batch, sidestepping the full RGB-tensor buffering that
is hopeless at scale.

Recording:
- RecordedTrajectory: backend-agnostic dataclass (qpos + mocap + replay
  metadata); RecordedTrajectory.save/.load are one self-describing .npz each.
- TrajectoryRecorder(Renderer): CPU single-world recorder.
- WarpTrajectoryRecorder(_BaseWarpRenderer): GPU multi-world recorder, one
  RecordedTrajectory per selected world; same format as the CPU recorder.
- Simulation/GPUSimulation.set_renderer(..., record_trajectory_only=True)
  swaps in the recorder (a recorder is a kind of renderer).

Replay (model supplied by the caller, persisted separately via
save_xml_with_assets):
- flygym.rendering.render_trajectories(mj_model, trajectories, ...) — CPU.
- flygym.warp.rendering.render_trajectories_gpu(mj_model, trajectories, ...)
  — GPU batch replay; bin-packs (trajectory, frame) work into mjw.Data
  batches of worlds_per_batch.
- Both set qpos and run position-only kinematics (mj_kinematics + mj_camlight)
  rather than a full forward step.

Serialization: save_trajectories/load_trajectories read/write a folder of
individual traj_XXXX.npz files; the model is not stored with the trajectory.

Refactor rendering.py and warp/rendering.py into packages, each split into
live_rendering and recorded_trajectory submodules, with backward-compatible
re-exports. Tests cover CPU/GPU recording, npz round-trip, CPU and GPU
replay, and cross-backend kinematics identity.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sibocw and others added 6 commits June 29, 2026 21:49
modify_world_for_batch_rendering warns as it adds overhead lights / strips
textures. The GPU replay test calls it deliberately, so wrap it in
warnings.catch_warnings() like the existing test_rendering.py does.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
scripts/record_replay_trajectories_gpu.py mirrors replay_behavior_gpu.py's
GPU-resident captured loop but swaps the live batch renderer for a
WarpTrajectoryRecorder: it simulates many worlds, records qpos for a small
subset, saves the trajectories and model to disk as independent artifacts,
then reloads and renders them post-hoc on GPU (and optionally CPU). This
exercises the full decoupled record -> save -> replay pipeline end to end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Record a trajectory for every simulated world (drop the recorded-subset
flag / `worlds=` selection) and render all of them post-hoc in GPU batches
of --worlds-per-batch (default 10) -- highlighting that the render batch
size is decoupled from the simulated world count. Lower the default
--n-worlds to 50 since every world is now rendered.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ethods

Now that the model is no longer saved alongside trajectories, the free
functions earned their keep only as thin folder loops. Replace them with
RecordedTrajectory.save() and RecordedTrajectory.from_file() (classmethod);
each trajectory is one self-describing .npz. Callers that want a folder of
many just loop. Updates the package re-exports, the GPU recorder error
messages/docstrings, the demo script, and the tests accordingly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sibocw sibocw added this to the v2.1.1 milestone Jun 29, 2026
@sibocw sibocw added the bug Something isn't working label Jun 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant