Skip to content

[pull] develop from isaac-sim:develop - #60

Merged
pull[bot] merged 11 commits into
mataylor-nvidia:developfrom
isaac-sim:develop
Sep 24, 2026
Merged

pull[bot] merged 11 commits into
mataylor-nvidia:developfrom
isaac-sim:develop

Conversation

@pull

@pull pull Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

pbarejko and others added 11 commits September 23, 2026 17:25
(cherry picked from commit e5f1bdb)

# Description

Remove confusing field from Camera spec

Fixes # (issue)

<!-- As a practice, it is recommended to open an issue to have
discussions on the proposed pull request.
This makes it easier for the community to keep track of what is being
developed or added, and if a given feature
is demanded by more than one party. -->

## Type of change

<!-- As you go through the list, delete the ones that are not
applicable. -->

- Bug fix (non-breaking change which fixes an issue)
- New feature (non-breaking change which adds functionality)
- Breaking change (existing functionality will not work without user
modification)
- Documentation update

## Release backport

- [ ] <!-- backport-active-release --> Backport this pull request to the
active release branch after it merges into `develop`

## Screenshots

Please attach before and after screenshots of the change if applicable.

<!--
Example:

| Before | After |
| ------ | ----- |
| _gif/png before_ | _gif/png after_ |

To upload images to a PR -- simply drag and drop an image while in edit
mode and it should upload the image directly. You can then paste that
source into the above before/after sections.
-->

## Checklist

Docker and GPU tests run on demand. Push the commits you want tested,
then
comment `run-ci` on the pull request.

- [ ] I have read and understood the [contribution
guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html)
- [ ] I have run the [`pre-commit` checks](https://pre-commit.com/) with
`./isaaclab.sh --format`
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] I have added a changelog fragment under
`source/<pkg>/changelog.d/` for every touched package (do **not** edit
`CHANGELOG.rst` or bump `extension.toml` — CI handles that)
- [ ] I have added my name to the `CONTRIBUTORS.md` or my name already
exists there

<!--
As you go through the checklist above, you can mark something as done by
putting an x character in it

For example,
- [x] I have done this task
- [ ] I have not done this task
-->
# Description

Third PR in a series that splits the core cleanup in #7949 into small,
reviewable pieces. This one fixes five small bugs in `isaaclab.utils`
and the camera utilities. Each fix has a regression test that fails
without it.

**Fixes**
- `utils.math.unproject_depth`: homogeneous pixel coordinates were built
as `(1, u, v)` instead of `(u, v, 1)` (`pad(..., (0, 0, 1, 0))` instead
of `(0, 0, 0, 1)`), so every unprojected point was wrong and the first
image row became `inf`/`nan`. This regressed in #4437 (quaternion
convention change), where the padding was flipped together with the
quaternion code. It also affects `create_pointcloud_from_depth` /
`create_pointcloud_from_rgbd` and the `run_usd_camera.py` /
`run_ray_caster_camera.py` tutorials. This fix is in its own commit.
- `sensors.camera.utils.create_pointcloud_from_rgbd`: a color tuple or
`rgb=None` called `torch.Tensor(data, device=..., dtype=...)`, which
always raises `TypeError`. The uniform color is now built with
`torch.tensor(...).repeat(...)` on the point cloud's device. The comment
and docstring claiming the default color is white are corrected (it is
black).
- `utils.math.quat_slerp`: `q2 *= -1.0` negated the caller's tensor in
place when taking the shorter arc; it is now `q2 = -q2`.
- `utils.sensors.convert_camera_intrinsics_to_usd`: `abs()` was applied
to the boolean `(c_x - w/2) > 1e-4 or ...`, so a principal point left of
or above the image center never triggered the aperture-offset warning.
- `utils.datasets.HDF5DatasetFileHandler.create`: a bare file name gave
`os.path.dirname(...) == ""` and `os.makedirs("")` raised. The directory
is now only created when there is one.

**Tests** (focused checks, no new test files; together they run in about
2.5 s). The aperture-offset warning fix is a one-line `abs()` correction
and is not covered by a dedicated test.
- `test/utils/test_math.py`: new `test_unproject_depth` (compares
against the pinhole model); the existing `test_quat_slerp` now also
asserts that `q2` is left unchanged.
- `test/utils/test_hdf5_dataset_file_handler.py`:
`test_create_dataset_file` also covers a bare file name.
- `test/sensors/test_opencv_distortion.py` (kit-less):
`test_pointcloud_from_rgbd_uniform_color` (color tuple and `None`).

All of these fail against `develop` sources and pass with the fixes; the
three touched test files pass in full.

## Release backport

- [x] <!-- backport-active-release --> Backport this pull request to the
active release branch after it merges into `develop`

## Type of change

- Bug fix (non-breaking change which fixes an issue)

## Checklist

- [x] I have read and understood the [contribution
guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html)
- [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with
`./isaaclab.sh --format`
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] I have added a changelog fragment under
`source/<pkg>/changelog.d/` for every touched package (do **not** edit
`CHANGELOG.rst` or bump `extension.toml` — CI handles that)
- [x] I have added my name to the `CONTRIBUTORS.md` or my name already
exists there
…7982)

Sum each rank's success samples across the process group at every
iteration boundary so all ranks record the same global history and stop
at the same iteration. Remove the validation that rejected
--check_success for distributed runs.

# Description

<!--
Thank you for your interest in sending a pull request. Please make sure
to check the contribution guidelines.

Link:
https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html

💡 Please try to keep PRs small and focused. Large PRs are harder to
review and merge.
-->

Please include a summary of the change and which issue is fixed. Please
also include relevant motivation and context.
List any dependencies that are required for this change.

Fixes # (issue)

<!-- As a practice, it is recommended to open an issue to have
discussions on the proposed pull request.
This makes it easier for the community to keep track of what is being
developed or added, and if a given feature
is demanded by more than one party. -->

## Type of change

<!-- As you go through the list, delete the ones that are not
applicable. -->

- Bug fix (non-breaking change which fixes an issue)
- New feature (non-breaking change which adds functionality)
- Breaking change (existing functionality will not work without user
modification)
- Documentation update

## Release backport

- [x] <!-- backport-active-release --> Backport this pull request to the
active release branch after it merges into `develop`

## Screenshots

Please attach before and after screenshots of the change if applicable.

<!--
Example:

| Before | After |
| ------ | ----- |
| _gif/png before_ | _gif/png after_ |

To upload images to a PR -- simply drag and drop an image while in edit
mode and it should upload the image directly. You can then paste that
source into the above before/after sections.
-->

## Checklist

Docker and GPU tests run on demand. Push the commits you want tested,
then
comment `run-ci` on the pull request.

- [ ] I have read and understood the [contribution
guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html)
- [ ] I have run the [`pre-commit` checks](https://pre-commit.com/) with
`./isaaclab.sh --format`
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] I have added a changelog fragment under
`source/<pkg>/changelog.d/` for every touched package (do **not** edit
`CHANGELOG.rst` or bump `extension.toml` — CI handles that)
- [ ] I have added my name to the `CONTRIBUTORS.md` or my name already
exists there

<!--
As you go through the checklist above, you can mark something as done by
putting an x character in it

For example,
- [x] I have done this task
- [ ] I have not done this task
-->
# Description

Fourth PR in a series that splits the core cleanup in #7949 into small,
reviewable pieces. This one fixes four small bugs in `isaaclab.terrains`
and `isaaclab.sim`, and adds a spawner for meshes (e.g. terrain from an
`.obj` file) ported from #5407 by @ooctipus (closed as stale). The PhysX
`useEnvIds` and clone-spawner changes from that PR are not included.
Each fix has a regression test that fails without it.

## Fixes
- `terrains.trimesh.mesh_terrains.repeated_objects_terrain`: the
built-in `MeshRepeatedBoxesTerrainCfg`,
`MeshRepeatedCylindersTerrainCfg`, and `MeshRepeatedPyramidsTerrainCfg`
default `object_type` to strings such as `"{DIR}.utils:make_box"`, which
configclass turns into a `ResolvableString` (a callable `str` subclass).
The function checked `isinstance(str)` before `callable`, looked up
`make_{DIR}.utils:make_box` in the module, got `None`, and raised
`ValueError`. Callables are now checked first; plain names such as
`"box"` still resolve to `make_box`. The error message now shows the
configured value instead of `None`.
- `sim.schemas.modify_articulation_root_properties`: with
`fix_root_link` set, the existing fixed joint was searched for on the
current stage instead of the `stage` argument.
- `sim.utils.queries.find_global_fixed_joint_prim`: the signature
accepts `Sdf.Path`, but `prim_path.startswith(...)` raised
`AttributeError` for one. The path is converted to `str` first, like the
sibling query functions.
- `sim.converters.MeshConverter`: the prim name was taken from
`basename.split(".")`, which raised `ValueError` for file names with
more than one dot (e.g. `duck.v2.obj`). It now uses `os.path.splitext`;
the existing invalid-identifier fallback turns `duck.v2` into `duck_v2`.
The unused `mesh_file_format` variable is removed.

## Mesh spawner

**Added**
- `sim_utils.MeshFileCfg` / `sim_utils.spawn_from_mesh` spawn a mesh
from:
- a mesh file path (`.obj`, `.stl`, `.fbx`, ...), converted with
`MeshConverter` and referenced, or
- in-memory triangle data: `MeshFileCfg.TriangleMeshCfg(vertices, faces,
vertex_colors)` or a `trimesh.Trimesh` through
`MeshFileCfg.TrimeshObjectCfg`, authored as an Xform with a USD mesh at
`{prim_path}/mesh`.
- The mesh is visual-only by default. `collision_props`,
`mesh_collision_props` (e.g. triangle mesh for terrain, convex hull for
objects), `rigid_props`, `mass_props`, `visual_material`, and
`physics_material` are optional and accept both the legacy cfgs and
schema fragments, like the other spawners.

```python
terrain = sim_utils.MeshFileCfg(
    mesh="/path/to/terrain.obj",
    collision_props=[sim_utils.UsdPhysicsCollisionCfg(collision_enabled=True)],
    mesh_collision_props=[sim_utils.UsdPhysicsMeshCollisionCfg(mesh_approximation_name="none")],
)
terrain.func("/World/ground", terrain)
```

**Changed**
- `isaaclab.terrains.utils.create_prim_from_mesh` (used by
`TerrainImporter.import_mesh`) now spawns through `MeshFileCfg`. I
checked that the authored USD is byte-for-byte identical to `develop`
for a colored terrain mesh with visual and physics materials. The only
difference: its `translation`/`orientation` kwargs now apply to the root
prim instead of the `mesh` child (same world pose).

**Differences from #5407**
- Mesh file paths reuse `MeshConverter` for collision, mass, and rigid
body properties, so both legacy cfgs and fragments work. The in-memory
path follows the same legacy-or-fragment routing as the mesh-primitive
spawners.
- Dropped the `.usd` branch (use `UsdFileCfg` for USD files), the
duplicate "prim already exists" check (`create_prim` already raises),
and the new public `utils.mesh` helpers; mesh-data validation is a
private helper in the spawner.

## Tests

- `test/sim/test_spawn_from_files.py`:
`test_spawn_mesh_from_triangle_data` (colored rigid body with a
convex-hull collider) and `test_spawn_mesh_from_obj_file`.
- The trimesh terrain path is covered by the existing terrain importer,
ray caster, and material fragment tests; `test_terrain_importer.py`
passes in full.

**Fix tests** (one focused check per fix, extending existing tests where
possible)
- `test/terrains/test_terrain_generator.py`: new
`test_repeated_objects_default_object_type` (the three repeated-object
configs share the code path, so one config is covered).
- `test/sim/test_schemas.py`: new
`test_modify_articulation_root_fix_root_link_uses_given_stage`
(in-memory stage that is not the current stage).
- `test/sim/test_utils_queries.py`: `test_find_global_fixed_joint_prim`
also passes an `Sdf.Path`.
- `test/sim/test_mesh_converter.py`: `test_convert_obj` now converts the
OBJ from a dotted file name (`duck.v2.obj`) and checks the prim name,
instead of adding a separate conversion.

All of these fail against `develop` sources and pass with the fixes.

## Type of change

- Bug fix (non-breaking change which fixes an issue)
- New feature (non-breaking change which adds functionality)

## Release backport

- [x] <!-- backport-active-release --> Backport this pull request to the
active release branch after it merges into `develop`

## Checklist

- [x] I have read and understood the [contribution
guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html)
- [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with
`./isaaclab.sh --format`
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] I have added a changelog fragment under
`source/<pkg>/changelog.d/` for every touched package (do **not** edit
`CHANGELOG.rst` or bump `extension.toml` — CI handles that)
- [x] I have added my name to the `CONTRIBUTORS.md` or my name already
exists there

---------

Co-authored-by: Octi Zhang <zhengyuz@nvidia.com>
# Description

`WrenchComposer.compose_to_body_frame()` was called unconditionally by
every backend writer on every physics step. It reads `body_com_pos_w`
and `body_link_quat_w`, which on PhysX chains to `_ensure_fk_fresh()` →
`update_articulations_kinematic()` (a scene-global articulation FK pass)
plus `root_view.get_link_transforms()`, then launches the composition
kernel.

That work is unnecessary whenever the buffered wrench is already in a
frame the consumer accepts. This PR moves the frame decision into the
composer, which hands each writer the cheapest valid representation:

- **all-local content** → the local buffers, untouched. An all-local
wrench composes to exactly itself, because the pose data is multiplied
by zero.
- **all-global-at-CoM content** → the global buffers, untouched,
submitted in the world frame by backends that accept one.
- **anything else** → composed as before.

Writers declare what they can consume once at construction
(`supports_world_at_com`) and call `get_forces_and_torques()` per step.
All frame logic stays inside the composer; no writer touches a raw
buffer or knows a frame rule.

## New public API

```python
class WrenchComposer:
    def __init__(self, asset, *, supports_world_at_com: bool = False) -> None: ...
    def get_forces_and_torques(self) -> tuple[wp.array, wp.array, bool]: ...

forces, torques, is_global = composer.get_forces_and_torques()
```

Additive only. `compose_to_body_frame()`, `out_force_b`, and
`out_torque_b` remain public and behaviorally unchanged, so no
deprecation cycle is needed. `supports_world_at_com` defaults to
`False`, which is exactly the previous behavior.

The composer tracks local contributions, global contributions, and
positioned global forces with plain booleans. These flags stay
conservative across partial resets and clear on a full `reset()`;
checking whether a partial reset removed every contribution would
require scanning the buffers. The returned `is_global` boolean follows
the existing asset API and is passed directly to the backend. No frame
enum, content bitmask, or classification helper is needed.

## Equivalence

All three branches are exact, from `compose_wrench_to_body_frame`:

- **local only**: the three global buffers are zero, so both
`quat_rotate_inv` terms vanish and the output is identically
`(local_force_b, local_torque_b)`. Note a *positioned* local force also
qualifies — the kernel folds `cross(P_b, F_b)` straight into
`local_torque_b`.
- **global-at-CoM only**: `global_force_w` and the local buffers are
zero, so `corrected_torque_w == global_torque_w`. Submitting the world
buffers is the same wrench, provided the consumer applies force at the
CoM — verified against the vendored `omni.physics.tensors` API, where
`position_data=None` means "at the link transform" for **both**
`is_global` values, so the flag only reinterprets the vectors' frame and
never moves the application point.
- **anything else**: unchanged.

## Backend coverage

| backend | `supports_world_at_com` | change |
|---|---|---|
| PhysX | `True` | writer passes the returned `is_global` directly |
| OvPhysX | `True` | writer passes `is_global` to the packing kernel to
skip the inline rotate |
| Newton | `False` (default) | writer swaps to
`get_forces_and_torques()`; no constructor change |

OvPhysX is the largest win. Its wrench binding wants a world-frame
wrench, so a global wrench previously round-tripped: the composer
rotated world→body, then the packing kernel rotated body→world. Measured
on this branch with a body rotated 90° about +z, a local force `(1,0,0)`
emits `(0,1,0)` while a global force `(1,0,0)` emits `(1,0,0)` — an
exact round trip, so skipping both rotations is output-preserving. The
packed `[6:9]` link position is still written unconditionally on both
paths.

Newton keeps the body frame because it binds a body-frame array to the
solver. Worth noting for a follow-up: its writer kernel already performs
the same body→world rotation OvPhysX's does, so Newton could plausibly
take the same flag and win the global-at-CoM path too. That is
deliberately out of scope here.

## Validation

The existing composer rotation, positioned-force, merge, reset, and
overwrite tests now exercise `get_forces_and_torques()` directly.
Existing frame/position cases cover the four add/set index/mask APIs.
The existing articulation body-ordering test checks local/world
forwarding, composition avoidance, and packed link positions. One shared
rigid-object writer check replaces the separate backend/equivalence
suites and their duplicate rotation kernel.

Local results:

- `test_wrench_composer.py`: **375 passed** on CPU/CUDA.
- Articulation ordering and rigid-object wrench writer checks: **24
passed** across PhysX, Newton, and OvPhysX.
- `uv run isaaclab -f`: all pre-commit checks passed.
- Structural audit: no new test classes or kernels; no production
changes in the test consolidation. The composer architecture regression
still rejects frame/content enums, the classification layer, and the old
submission alias.

Regression sensitivity was checked by temporarily restoring
unconditional composition and stale-pose caching: the updated existing
tests fail on unnecessary pose reads and on a second read after the body
rotates, respectively.

The previously recorded paired Python dispatch microbenchmark changed
local buffer selection from about 99 ns to 46 ns per call, and
global-at-CoM selection from 127 ns to 60 ns. This measures buffer
selection only, not end-to-end simulation; production code is unchanged
by the test consolidation.

## Performance

The end-to-end measurements below were recorded for the original
optimization; they were not rerun for the boolean/API simplification.

Real PhysX, RTX 5090, ANYMAL-C (17 bodies) × 4096 envs = 69,632 links.
Same-process paired A/B with the arm order alternated per iteration,
400–500 pairs, 95% CI on paired differences. Baseline is
`compose_to_body_frame()` — what the writers previously called
unconditionally.

| scope | content | baseline | patched | paired delta | 95% CI |
|---|---|---:|---:|---:|---|
| composition path only | global-at-CoM | 0.2194 ms | 0.0191 ms |
**−0.2003 ms** (−91.3%) | [0.1915, 0.2091] |
| composition path only | local | 0.1926 ms | 0.0159 ms | **−0.1767 ms**
(−91.8%) | [0.1712, 0.1822] |
| full `write_data_to_sim` | global-at-CoM | 1.0799 ms | 0.9897 ms |
**−0.0902 ms** (−8.4%) | [0.0797, 0.1007] |
| full `write_data_to_sim` | local | 1.0072 ms | 0.9305 ms | **−0.0767
ms** (−7.6%) | [0.0719, 0.0815] |

Read the end-to-end row as the honest headline: **~0.08 ms per asset per
physics step**. Roughly half the isolated saving does not reach the
caller, because other work in `write_data_to_sim` touches body poses
anyway and pays part of that cost regardless. A cuboid rigid-object
scene at the same env count saved only ~6.5 µs, as expected — there is
no articulation FK pass to skip.

Scope caveat: no in-tree task sets `is_global=True` today (both shipped
`apply_external_force_torque` events use the local default), so the
local path is what delivers value now, and it is the one that reaches
every backend.

## Supersedes

This replaces two open PRs, both of which found one half of this
problem:

- **#7431** (@NeoZng) identified the global-at-CoM case and the cost of
the pose read. Its mechanism is correct and its writer-level measurement
is corroborated here. It is superseded on placement rather than
substance: it put the frame decision in three PhysX writers behind a
public eligibility flag, leaving Newton and OvPhysX unserved. The credit
for the world-at-CoM half of this mechanism is theirs.
- **#7362** cached composition for unchanged local wrenches. Superseded
because not composing at all is strictly stronger — the cache does
nothing when the wrench is rewritten every step, which is the common
case for a per-step randomized push.

## Type of change

- New feature (non-breaking change which adds functionality)

## Checklist

- [x] I have read and understood the [contribution
guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html)
- [x] I have run the `pre-commit` checks with `./isaaclab.sh --format`
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] I have added a changelog fragment under
`source/<pkg>/changelog.d/` for every touched package
- [x] I have added my name to the `CONTRIBUTORS.md` or my name already
exists there

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Octi Zhang <zhengyuz@nvidia.com>
# Description

Migrates the remaining in-repo users of the legacy physics schema cfgs
to schema fragments. These classes are deprecated in 3.1 and removed in
3.2 (#7839), so every site here currently emits `DeprecationWarning`s
and would break at removal.

The earlier migration (#7838) converted `source/` but never covered
`scripts/`, and its grep missed the `*BaseCfg` spellings. On top of
that, some new legacy usage has landed since. An AST scan (comments and
docstrings excluded) finds **144 live legacy constructions or writer
calls in 37 files on `develop`. This PR brings that to 0**, outside the
deliberate exclusions listed below.

**Scope**
- **`scripts/` (29 files):** all tutorials, demos (including the MPM
demos), `convert_mesh` / `convert_instanceable`, and three benchmarks.
- **Task configs:** `core/reach/reach_env_cfg.py`,
`contrib/ur10_particle_push`, and one of the five `contrib/nist` sites.
- **Tests:** the camera-pose, first-frame-rendering and
OpenCV-distortion sensor tests; the rigid/mass/collision parts of the
cable and MPM tests; `test_ovphysx_gravity`.

## Mapping rules

Every one of these has caused a real regression in an earlier migration:
- **A bare fragment when one covers the fields.** This keeps in-place
tuning such as `cfg.rigid_props.disable_gravity = True` working
downstream.
- **Empty legacy cfgs map only to the core `UsdPhysics*` fragment.** A
fragment applies its schema even when all of its fields are `None`.
`RigidBodyPropertiesCfg()` therefore becomes `UsdPhysicsRigidBodyCfg()`
alone. Adding `PhysxRigidBodyCfg()` would add a `PhysxRigidBodyAPI` the
legacy class never applied.
- **`enabled_self_collisions` keeps its Newton mirror.** The legacy
writer also authors `newton:selfCollisionEnabled`, so these sites emit
`NewtonArticulationCfg` next to `PhysxArticulationCfg`.
- **`disable_gravity` goes to `PhysxRigidBodyCfg`**, including on
Newton-flavoured cfgs.
- **`define_*` writer calls become `apply_*(...,
create_if_missing=True)`**, which preserves creation.

## Verification

**USD-attribute parity at every site, with negative controls.** Legacy
and fragment forms are authored through the real spawners on sibling
prims, and the harness diffs `GetPrimTypeInfo().GetAppliedAPISchemas()`,
every authored attribute, and relationships.
- `scripts/`: 64 groups. 60 are identical. The other 4 differ only in
that legacy authored `physics:approximation="none"` explicitly, while
the fragment leaves it unset and it resolves to the same `"none"`. 8 of
8 negative controls are flagged.
- `source/`: every site is identical, and every negative control is
flagged.

**Task configs** are also checked end to end. Each task is loaded
through its gym `env_cfg_entry_point` on both `develop` and this branch,
across presets. All 22 affected spawned entities author identical USD,
including the nist Franka on the real asset. A task-level negative
control produces a diff.

**Execution:** 13 scripts ran headless for 100–300 steps with no schema
deprecations: the tutorials, bin_packing, multi_asset, the sensor demos,
and tacsl. `convert_mesh` was run end to end for 6 approximations. All
29 scripts compile. The migrated test files pass.

**Not runnable in the dev environment, so covered by parity only:** the
Newton-physics script paths, including the MPM demos, which need a newer
`newton` than was installed locally. Also the `omni.replicator`, ovrtx,
and Haply scripts. CI covers the rest.

## Deliberately left on the legacy API

- **Spawner, converter, and schema routing internals.** They *are* the
legacy path, which must keep working until 3.2.
- **Tests that exercise or compare the legacy path**, such as
`test_schemas*`, the parity tests, and the legacy arms of the fragment
tests.
- **`integration_scene_cfgs.py`.** It is a backend-neutral core module,
and its `disable_gravity` has no non-PhysX fragment home.
- **Everything deformable.** The deformable deprecation PR, stacked on
#6673, handles it.
- **4 of the 5 `contrib/nist` sites.** They nest a mesh-collision cfg
inside `collision_props`. The deprecation messages point users to a
spawner `mesh_collision_props` slot, but that slot exists only on
`MeshConverterCfg`, not on `UsdFileCfg` or the shape and mesh spawners.
There is no supported fragment path for these sites yet. That gap needs
its own fix before 3.2.

## Found along the way, not changed here

- `RigidBodyMaterialCfg` is deprecated too, and 8 scripts still use it.
- `convert_instanceable.py`'s mesh branch is already broken on
`develop`: it passes `collision_approximation=` to `MeshConverterCfg`,
which raises `TypeError`.
- `docs/source/how-to/write_articulation_cfg.rst` still describes the
legacy names.

## Type of change

- Bug fix (non-breaking change which fixes an issue)

## Checklist

- [x] I have read and understood the contribution guidelines
- [x] I have run the `pre-commit` checks with `./isaaclab.sh --format`
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] I have added a changelog fragment under
`source/<pkg>/changelog.d/` for every touched package
- [x] I have added my name to the `CONTRIBUTORS.md` or my name already
exists there
# Description

Route rigid-body transforms from physics to renderers through
`SceneDataProvider` (SDP). Physics publishes its native buffer and
increments its version after changes. SDP binds that buffer when format
and ordering match, or converts it once per requested layout and reuses
the result until the next change. Readers never reset the producer's
version.

## What changes

- **OVRTX:** converts physics poses directly into its matrix format,
without first copying them into a Newton render state.
- **Newton rendering:** borrows SDP's transform buffer instead of
copying into a second buffer. Newton physics and rendering continue
sharing their existing model/state.
- **Isaac RTX:** shares one registry-owned Fabric binding with Kit,
preserves authored scale, and updates the transform hierarchy on GPU.
PhysX publishes its native Fabric matrices without fetching packed
poses.
- **Ownership:** physics refreshes native data; SDP borrows or converts
arrays. One `FabricBackend` in `isaaclab_physx` owns the stage/hierarchy
handles and shared transform bindings, identified by stage/device.
Consumers pass SDP explicitly to updates; it is not part of backend
identity. Core `RenderContext` has no Fabric methods or state.
`FabricMatrix44` contains only matrix storage.
- **Rendering updates:** removes repeated renderer-driven physics
refreshes. Writes made between physics steps remain visible on the next
render.

For Newton → Isaac RTX, cached bindings and GPU hierarchy updates
replace repeated binding setup and the CPU hierarchy fallback. This is
the main runtime saving measured below.

Particle/deformable transport is outside this change. OVRTX retains its
existing Newton geometry bridge.

## Transform movement

Counts are SDP output-writing passes, not total SDK-internal copies.
Conversion, reordering, and scale are combined in one pass; unchanged
data reuses the result.

| Physics | Renderer | Published → requested format | SDP passes |
| --- | --- | --- | ---: |
| Newton | Newton Warp | `Transform` → `Transform` | 0 |
| Newton | OVRTX | `Transform` → `TransposedMatrix44d` | 1 |
| Newton | Isaac RTX | `Transform` → `FabricMatrix44` | 1 |
| OVPhysX | Newton Warp | `Transform` → `Transform` | 0 if ordering
matches; otherwise 1 |
| OVPhysX | OVRTX | `Transform` → `TransposedMatrix44d` | 1 |
| Isaac PhysX | Newton Warp | `Transform` → `Transform` | 0 if ordering
matches; otherwise 1 |
| Isaac PhysX | Isaac RTX | Native Fabric → borrowed Fabric | 0 with
`use_fabric=True` |

OVPhysX and OVRTX cannot run with Kit. Fabric hierarchy propagation and
OVRTX's native attribute write happen after the SDP pass and are not
included in these counts.

## Performance

Kuka Allegro Camera, 4096 environments, 64×64 RGB, Newton MJWarp → Isaac
RTX, RTX 5090, no interactive visualizer. Two warm-cache runs per
revision; runtime excludes 25 warmup steps and measures 200 synchronized
full environment steps, including rendering and observations.

| Metric | PR | Develop |
| --- | ---: | ---: |
| Warm startup | 135.47 s | 133.19 s |
| Runtime step | 172.32 ms | 554.49 ms |
| Environment frames/s | 23,770 | 7,387 |

The measured step time was **68.9% lower (3.22× throughput)**. No
startup improvement was measured. All 4096 camera images were finite and
nonconstant.

Measured revisions: PR `257e54d5f` and develop `53a7f1c0a`, with
identical dependencies. Subsequent cleanup has not been rebenchmarked.

## Migration

Custom scene-data backends must initialize `transforms_version=0` and
increment it after native pose writes or buffer swaps. The
consumer-facing `get_transforms(output)` API binds shared, read-only
arrays, including converted outputs. Pass `allow_passthrough=False` for
caller-owned writable or preallocated arrays; conversion writes directly
into them. Single-format backends keep their existing `transforms`
property; the base `get_transforms(output_format)` delegates to it.
Multi-format backends may override that method and
`native_transform_formats`.

## Validation

- Focused CPU tests and GPU 0 Newton/PhysX Fabric tests cover pointer
sharing, publication versions, transform formats, ordering, authored
scale, buffer reallocation, same-step writes, and resets. The existing
cache regression now also checks that independent SDP readers cannot
hide producer changes from one another.
- All 11 native Newton/Fabric tests and the native PhysX Fabric test
passed after moving bindings out of core. They also check one shared
RTX/Kit resource and explicit Newton-to-Fabric updates without an RTX
camera or Kit viewer.
- Removed obsolete synchronization tests and duplicate mock-only checks;
native and numerical regressions remain.
- Native OVPhysX → OVRTX rendered scale, pose changes, and camera
calibration passed for both legacy and ovstage APIs. Matched OVRTX
timings have not been collected.

## Type of change

- Refactor and bug fixes
- Breaking custom scene-data backend interface change, with migration
above

## Release backport

- [x] <!-- backport-active-release --> Backport to the active release
branch

## Checklist

- [x] Contribution guidelines reviewed
- [x] Changelog fragments and migration documentation updated
- [x] Retained focused tests and formatting checks passed after test
cleanup
- [ ] Full GPU CI passed for the latest revision
- [x] Native OVPhysX → OVRTX rendering validated
## Description

Observation terms lacked a built-in delay tied to recorded samples.
Added `delay_min_lag`, `delay_max_lag`, and `delay_hold_prob` directly
to `ObservationTermCfg`, using the same `DelayBuffer` as
`DelayedPDActuator`:

```python
joint_pos = ObservationTermCfg(func=mdp.joint_pos_rel, delay_min_lag=1, delay_max_lag=3, delay_hold_prob=0.8)
```

Each environment samples its lag uniformly at initialization/reset. On
each recorded sample, `delay_hold_prob=0.8` retains the current lag with
probability 0.8 and otherwise draws a new lag, which may equal the
previous one. The default of 1.0 holds the lag for the episode,
preserving Isaac Lab's delayed actuator policy; 0.0 redraws every
recorded sample. Holding a lag keeps latency constant as frames advance.
Equal bounds give a fixed delay; both zero disable it.

The field names, lag-retention semantics, and placement follow mjlab:
delay runs after modifiers, noise, clipping, and scaling, before
history. `compute(update_history=True)` records a sample; extra
`compute()` or `compute_group()` reads leave delay/history and lag
sampling unchanged. Before the first recorded sample after reset, delay
returns the current input; during warm-up it returns the oldest
available sample. Observation lag counts recorded samples, while
actuator lag retains its physics-step clock.

`DelayBuffer` owns ring storage and optional lag sampling. Its default
`hold_prob=None` preserves externally selected lags and `compute(data)`
for existing actuator callers. Actuator configuration, execution, and
reset behavior are preserved. Tests extend existing suites; the diff
adds no runtime module or test file.

Related: #3471.

## Validation

- 145 core tests passed; one CUDA-graph case skipped on CPU. Covered
delay/history recording, extra reads without RNG advancement, processing
order, config validation, partial resets, output isolation, hold
probabilities 0/0.5/1, and CUDA graph replay with lag sampling.
- Four actuator reset integration tests passed, running Newton and PhysX
separately.
- Confirmed regression tests fail when extra reads advance delay or the
lag-retention mask is removed.
- Eight-file architecture audit and `uv run --no-sync isaaclab -f`
passed.
- CPU/CUDA buffer benchmarks at batch 1024, width 48, and history
lengths 4/32/128 showed lower runtime than the base implementation's
history shifting. Adding optional sampling kept the existing manually
selected-lag path within timing variation and introduced no RNG draws.
These are local microbenchmarks, not end-to-end simulation measurements.

## Type of change

- New feature
- Bug fix
- Documentation update

## Release backport

- [x] <!-- backport-active-release --> Backport this pull request to the
active release branch after it merges into `develop`

## Checklist

- [x] Ran pre-commit checks.
- [x] Updated public documentation and changelog.
- [x] Extended existing tests.
- [ ] Passed PR CI.
# Description

Some CLI unit tests still assume that they run from an Isaac Lab source
checkout. They read the root `pyproject.toml`, copy the repository
launcher, or execute scripts that are not shipped in the installed
package. This makes the tests fail when the suite is run against an
installed wheel or conda package.

Use the existing `source_checkout_root` fixture for these remaining
tests, following the pattern introduced in #7389. The launcher and
teleoperation tests also resolve checkout-only files from the path
returned by the fixture.

## Type of change

- Bug fix (non-breaking change which fixes an issue)

## Release backport

- [x] <!-- backport-active-release --> Backport this pull request to the
active release branch after it merges into `develop`

## Checklist

- [x] I have read and understood the [contribution
guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html)
- [ ] I have run the [`pre-commit` checks](https://pre-commit.com/) with
`./isaaclab.sh --format`
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] I have added a changelog fragment under
`source/<pkg>/changelog.d/` for every touched package
- [x] I have added my name to the `CONTRIBUTORS.md` or my name already
exists there

Local validation:

```text
uvx pre-commit run --files <changed files>: passed
56 passed in 9.22s
```
## Description

Newton's joint wrench sensor omitted welded tool and wrist-sensor bodies
because it inherited the articulation's control-joint filter. A fixed
connection still transmits force and torque.

Select reportable tree joints from the Newton model while reusing the
articulation's cached body bindings. This includes fixed joints between
bodies and continues to exclude free joints, world-fixed roots, and
loop-closing constraints. The articulation's control-joint selection and
the wrench conversion kernel remain unchanged. Callers should select
sensor entries by body name.

Extended the existing cartpole gravity-wrench test with a fixed pole
joint. Three selection cases cover distinct root types, loop exclusion,
reordered bodies, and per-environment anchors. The PR adds no USD
fixture or test file, and only the existing Newton sensor test file
changes.

Fixes #7969.

## Validation

- All 13 Newton sensor cases passed after merging current develop, using
Newton 1.6.0 and Warp 1.17.0.
- The fixed-pole physics case and all three selection cases failed with
the original implementation; the movable-pole case still passed.
- Verified cached body bindings are reused, no second sensing view is
created, and control-joint selection stays unchanged.
- Six-file architecture audit and `uv run --no-sync isaaclab -f` passed.

## Type of change

- Bug fix
- Documentation update

## Release backport

- [ ] <!-- backport-active-release --> Backport this pull request to the
active release branch after it merges into `develop`

## Checklist

- [x] I have read and understood the contribution guidelines
- [x] I have run the pre-commit checks
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective
- [x] I have added a changelog fragment for every touched package
- [x] My name already exists in `CONTRIBUTORS.md`
## Description

Fixes #3513 and the PhysX joint-wrench frame discrepancy discussed in
#7978.

`forge_utils.change_FT_frame` used the inverse rotation and the wrong
lever-arm sign when expressing a wrench in another frame. It now
computes the source pose in the target frame and applies:

- `f_t = R_ts f_s`
- `tau_t = R_ts tau_s + p_ts × f_t`

The existing independent point-force test covers translated frames with
identity and arbitrary rotations. FORGE's current force-only observation
remains unchanged because its call uses identity rotations.

The PhysX sensor had a separate error: `get_link_incoming_joint_force()`
already returns forces and torques in the child-side joint frame, about
its anchor. The sensor applied `localPos1` and `localRot1` again. It now
exposes the native components directly, removing the redundant USD
traversal, frame buffers, and GPU transformations. This follows the
[PhysX tensor API
contract](https://docs.omniverse.nvidia.com/kit/docs/omni_physics/108.0/extensions/runtime/source/omni.physics.tensors/docs/api/python.html#omni.physics.tensors.impl.api.ArticulationView.get_link_incoming_joint_force).

Replaced the circular PhysX frame test with one shared integration
fixture used by both Newton and PhysX. It uses the same timestep,
geometry, loads, expected values, and tolerances: a passive vertical
hinge supports a 2 kg body through an offset child frame rotated 90
degrees relative to the body. The articulation is also rotated about
world Z. Both backends must report `F=(0, 0, 19.62) N` and `tau=(-1.962,
4.905, 0) N·m`, calculated from gravity and the known lever arm. The
test checks that the body stays at rest. No raw wrench or production
conversion is used to calculate the reference.

The fixture reuses the existing simple articulation asset with temporary
USD overrides; no asset is added to the repository. The existing backend
test files invoke the shared check. Removed the old circular reference
helper.

## Validation

- Newton sensor suite and FORGE tests: 12 passed.
- PhysX sensor suite: 16 passed, including eager/recorded updates and
reset behavior.
- The shared physical test failed with the original PhysX
implementation, with a 19.62 N force-component error, and passed after
removing the duplicate transform. Newton passes the same analytic
contract.
- Architecture audit and `uv run --no-sync isaaclab -f` passed.
- Recorded GPU update microbenchmark at 1024 environments × 8 links:
approximately 3 microseconds per launch before and after. Initialization
no longer scans USD joint frames or allocates their buffers.

## Type of change

- Bug fix
- Documentation update

## Checklist

- [x] I have read and understood the contribution guidelines
- [x] I have run the pre-commit checks
- [x] I have made corresponding changes to the documentation
- [x] I have added tests that prove my fix is effective
- [x] I have added a changelog fragment for every touched package
- [x] My name already exists in `CONTRIBUTORS.md`

## Follow-up

FORGE names its raw incoming-joint wrench `force_sensor_world`, although
the API reports the child joint frame. Moving FORGE's observation to an
explicitly selected frame is separate work because it can change policy
inputs. This PR preserves its current force observation.

---------

Signed-off-by: Lynn <lynnhe02@gmail.com>
Co-authored-by: Octi Zhang <zhengyuz@nvidia.com>
@pull pull Bot locked and limited conversation to collaborators Sep 24, 2026
@pull pull Bot added the ⤵️ pull label Sep 24, 2026
@pull
pull Bot merged commit 37cfd63 into mataylor-nvidia:develop Sep 24, 2026
12 of 13 checks passed
@github-actions github-actions Bot added documentation Improvements or additions to documentation isaac-lab labels Sep 24, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

⤵️ pull documentation Improvements or additions to documentation isaac-lab

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants