Prerequisites

Install FastSim and the Isaac Lab provider in the same environment. This Run starts Isaac Lab 3.0 / Isaac Sim 6.0 with a visible window.

Configuration, key by key

yaml
schema: fastsim/2                  # FastSim vNext Run schema.
name: demo-frame-transforms        # Stable Run name.
backend: isaaclab                  # Select the arm's Isaac Lab USD resource.

runtime:
  launch_profile: visible          # Open the native simulator window.
  physics_hz: 60                   # Physics integration frequency.
  control_hz: 60                   # Control admission frequency.
  seed: 41                         # Reproducible reset seed.

scenario:
  scene:
    robots:
      arm:                         # Scenario alias; canonical ID is robots.arm.
        use: robot://fastsim/demo-two-joint-arm
        pose:
          xyz_m: [0.25, -0.20, 0.0] # Non-zero world translation, in metres.
          quat_xyzw: [0.0, 0.0, 0.2588190451, 0.9659258263] # 30 deg about Z.
        initial_state:
          joints:
            joint_1: 0.35          # Revolute position in radians.
            joint_2: -0.50

The non-zero root pose and joint values make it easy to see that the returned transform is a real composed result rather than a hard-coded identity.

Python, line by line

  1. The docstring states that this is a portable frame lookup.
  2. asyncio, math, Path, and fastsim provide the async runner, finite-value checks, adjacent Run lookup, and public application API.
  3. RUN_FILE points to run.yaml; VISIBLE_HOLD_SECONDS controls the short visual inspection period.
  4. main() defines the async application flow.
  5. fastsim.app(..., planning_reads=True) explicitly enables the pay-for-play frame publication path.
  6. async with guarantees complete backend cleanup.
  7. start() builds the real scene; pause() preserves it at a stable transform boundary.
  8. frames.catalog() returns the immutable frame graph for this generation.
  9. The first next(...) finds the one descriptor whose kind is world. Code does not assume a provider-specific world-frame spelling.
  10. The first list comprehension selects every frame owned by canonical entity robots.arm; the second keeps only its link frames.
  11. parent_frame_ids collects frames that have a child. The program requires exactly one leaf link, then selects that physical distal link without depending on opaque hashed frame-ID ordering. Production code can instead select an explicitly declared named frame.
  12. frames.transform(source, target) returns target_T_source. Passing the selected link first and world second therefore requests world_T_link.
  13. The validation block requires finite translation/quaternion components and a quaternion norm within 1e-4 of one; otherwise the process exits non-zero.
  14. The print loop shows each portable frame and parent, the selected leaf, explicit transform direction, SI translation, quaternion, and measured norm.
  15. The final sleep holds the paused visible scene for three seconds.
  16. asyncio.run(main()) executes the program when launched directly.

Acceptance gates

  • The arm graph must have exactly one leaf link.
  • Every transform component must be finite and quaternion norm error must be at most 1e-4.
  • The visible Isaac Lab acceptance run on 2026-08-25 selected the distal link at approximately (0.25, -0.20, 0.50) m, measured quaternion norm 1.000000, and exited with code 0.

Run it

From this case directory:

bash
cd demo/fundamentals/09_frame_transforms
fastsim config validate run.yaml --json
python main.py

Expected output lists at least one robots.arm link frame, prints a lookup in the form world_T_<link>, and returns finite translation and quaternion values.

Verification level

REAL VISIBLE ISAAC PASS. The native run selected the actual leaf link, returned finite transform data with quaternion norm 1.000000, exited normally, and left no Isaac worker process behind.

Transform notation

A_T_B maps coordinates expressed in frame B into frame A. Consequently:

python
await simulation.frames.transform(source_frame, target_frame)

returns target_frame_T_source_frame. Keeping that order explicit prevents a common class of inverted-pose bugs.

Common errors

  • SERVICE_ACCESS_DENIED: frame reads require planning_reads=True.
  • FRAME_NOT_FOUND: discover IDs from frames.catalog(); never construct one from a backend path.
  • An empty arm_links list: the provider did not publish the component's declared links; treat this as a component/provider mismatch rather than guessing a frame.