Skip to content

[OVPHYSX] SceneDataProvider#5589

Open
AntoineRichard wants to merge 5 commits into
isaac-sim:developfrom
AntoineRichard:antoiner/feat/ovphysx_scene_data_provider
Open

[OVPHYSX] SceneDataProvider#5589
AntoineRichard wants to merge 5 commits into
isaac-sim:developfrom
AntoineRichard:antoiner/feat/ovphysx_scene_data_provider

Conversation

@AntoineRichard
Copy link
Copy Markdown
Collaborator

Description

Adds OvPhysxSceneDataProvider, the OVPhysX-backend implementation of BaseSceneDataProvider. Enables Newton-based visualizers (Rerun, Viser, native Newton viewport) to render OVPhysX simulations. Also fixes the SceneDataProvider factory dispatch (and adds a regression test) so the substring check "physx" in "ovphysxmanager" no longer routes ovphysx to PhysX.

Fixes #5327

Important

Stacked on #5459 ([OVPHYSX] Articulation rewrite). The base of this PR is develop, so the diff includes every commit from #5459 — review only the 16 SDP-specific commits on top (git log antoiner/feat/ovphysx_articulation..antoiner/feat/ovphysx_scene_data_provider). Once #5459 merges, this PR will rebase cleanly.

Architectural notes

Mirrors PhysxSceneDataProvider end-to-end: build a Newton model from the scene's ClonePlan at init (gated on SceneDataRequirement.requires_newton_model), then on every update() read body poses through ovphysx TensorBindings and write them into the Newton state's body_q via a single Warp kernel.

The interesting OVPhysX-specific surface:

  • One-pattern-per-binding: the ovphysx wheel's PhysX.create_tensor_binding accepts a single glob-style pattern per binding (not a list, unlike PhysX's RigidBodyView). We bucket Newton body paths by their env-wildcard form (/World/envs/env_*/Robot/cart, /World/envs/env_*/Robot/pole, ...), one pose+velocity binding per distinct relative path. Cartpole produces 2 bindings; Allegro hand ~17 — each covering all envs.
  • RIGID_BODY_POSE covers everything: confirmed via standalone probe + wheel docs that the matcher accepts any prim with UsdPhysics.RigidBodyAPI, including articulation links. No separate LINK_POSE plumbing is needed. Paths flow through a RigidBodyAPI filter (mirroring PhysxSceneDataProvider._setup_rigid_body_view line ~281) so joints and articulation-root xforms are excluded from binding patterns.
  • Wheel-0.4 binding.read(dst) API: pre-allocated warp destination buffers, so per-step reads are allocation-free.
  • View-order reorder: each binding's prim_paths is matched against the Newton body_label ordering to build a per-binding reorder tensor with -1 sentinels for rows owned by other bindings. The pose merge respects partial coverage across bindings, with FrameView fallback for anything that lacks RigidBodyAPI (cameras, decorative xforms).
  • Factory fix: SceneDataProvider._get_backend now checks "ovphysx" explicitly before "physx" so the substring overlap doesn't route the wrong backend.

Files changed (16 commits since antoiner/feat/ovphysx_articulation)

  • source/isaaclab/isaaclab/physics/scene_data_provider.py — factory dispatch fix (+9/-1).
  • source/isaaclab/test/sim/test_scene_data_provider_factory.py — new parametrized regression test (44 lines).
  • source/isaaclab_ovphysx/isaaclab_ovphysx/scene_data_providers/__init__.py, __init__.pyi — package scaffolding.
  • source/isaaclab_ovphysx/isaaclab_ovphysx/scene_data_providers/ovphysx_scene_data_provider.py — the provider (~760 lines).
  • source/isaaclab_ovphysx/test/scene_data_providers/test_ovphysx_scene_data_provider.py — 18 stub-based unit tests covering factory dispatch, metadata, env discovery, Newton-model build, binding setup, pose-merge orchestration, transform/velocity getters, view-order reorder, camera-transform stage walk.
  • docs/source/api/index.rst + docs/source/api/lab_ovphysx/isaaclab_ovphysx.scene_data_providers.rst — first isaaclab_ovphysx section in the API reference.
  • source/isaaclab/changelog.d/antoiner-feat-ovphysx-scene-data-provider.rst — patch fragment for the factory fix.
  • source/isaaclab_ovphysx/changelog.d/antoiner-feat-ovphysx-scene-data-provider.minor.rst — minor fragment for the new provider.

Type of change

  • New feature (non-breaking change which adds functionality).
  • Bug fix (factory dispatch).

Validation

  1. Unit tests./isaaclab.sh -p -m pytest source/isaaclab/test/sim/test_scene_data_provider_factory.py source/isaaclab_ovphysx/test/scene_data_providers/ → 23 passed.
  2. End-to-end./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Cartpole-Direct-v0 --visualizer newton --num_envs 4096 presets=ovphysx reaches the simulation loop; visualizer renders the cartpole scene.
  3. Pre-commit./isaaclab.sh -f clean.
  4. Pattern probe — standalone reproduction at /tmp/probe_rigid_body_pose_on_articulation.py confirmed RIGID_BODY_POSE accepts articulation-link patterns. Reproducible against the wheel's bundled minimal articulation USD.

Known follow-ups (not in this PR)

  • Extract _build_newton_model_from_clone_plan into a shared helper used by both PhysX and OVPhysX providers (currently a verbatim copy).
  • FrameView factory could grow an explicit "ovphysx" registration entry (today's dispatch falls through to the "physx" branch, which works because FabricFrameView reads from the shared USD stage).
  • Pattern length cap: if a scene's distinct-body-path count exceeds the wheel's per-pattern matcher limit (unknown), chunk into multiple bindings indexed by buffer-row offset. Hasn't bitten yet.
  • A future test_ovphysx_scene_data_provider_visualizer_contract.py mirroring test_physx_scene_data_provider_visualizer_contract.py.

Checklist

  • I have read and understood the contribution guidelines
  • I have run the pre-commit checks 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 feature works
  • I have added a changelog fragment under source/<pkg>/changelog.d/ for every touched package
  • I have added my name to the CONTRIBUTORS.md or my name already exists there

@github-actions github-actions Bot added documentation Improvements or additions to documentation isaac-lab Related to Isaac Lab team labels May 12, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 12, 2026

Greptile Summary

This PR introduces OvPhysxSceneDataProvider, the OVPhysX-backend implementation of BaseSceneDataProvider, and fixes a factory dispatch bug where \"physx\" in \"ovphysxmanager\" incorrectly routed OVPhysX simulations to the PhysX backend. The provider mirrors PhysxSceneDataProvider's approach: build a Newton model from ClonePlan at init, then on each update() read body poses through OVPhysX TensorBinding objects and write them into Newton body_q via a Warp kernel.

  • Factory fix (scene_data_provider.py): ovphysx substring check now precedes physx, backed by a parametrized regression test covering all three manager names plus the unknown-manager error path.
  • OvPhysxSceneDataProvider: per-pattern TensorBindings bucketed by env-wildcard form, view-order reorder tensor for Newton-body alignment, and FrameView fallback for prims without UsdPhysics.RigidBodyAPI.
  • Tests: 23 stub-based unit tests; the get_camera_transforms global-camera edge case (non-env cameras producing misaligned order/positions arrays) is not covered.

Confidence Score: 3/5

The core pose-sync pipeline is solid, but two edge-case logic bugs in get_camera_transforms and _wildcard_env_paths can silently produce wrong data or a permanent no-op with no WARNING-level signal.

The core pose-sync path (TensorBinding to Warp kernel to Newton body_q) is architecturally sound and well-tested for the primary cartpole and Allegro-hand scenes. The two flagged issues are narrow edge cases unlikely to affect those workloads today, but both fail silently once triggered: get_camera_transforms produces structurally misaligned arrays for global cameras, and _wildcard_env_paths permanently excludes non-env_0 rigid bodies from binding when a mixed scene is used.

source/isaaclab_ovphysx/isaaclab_ovphysx/scene_data_providers/ovphysx_scene_data_provider.py — specifically get_camera_transforms (num_envs initialisation) and _wildcard_env_paths (mixed-path handling)

Important Files Changed

Filename Overview
source/isaaclab_ovphysx/isaaclab_ovphysx/scene_data_providers/ovphysx_scene_data_provider.py New OVPhysX scene data provider (~760 lines); two P1 logic issues: get_camera_transforms misaligns order vs pose arrays for global cameras, and _wildcard_env_paths silently drops non-env_0 paths in mixed scenes; two P2 issues around broad exception suppression and unbounded per-step retry.
source/isaaclab/isaaclab/physics/scene_data_provider.py Factory dispatch fix: ovphysx check inserted before physx check, resolving the substring collision. Clean and well-tested.
source/isaaclab/test/sim/test_scene_data_provider_factory.py New parametrized regression test for the factory dispatch; covers PhysxManager, NewtonManager, OvPhysxManager (all-caps variant included), and unknown-manager error path.
source/isaaclab_ovphysx/test/scene_data_providers/test_ovphysx_scene_data_provider.py 18 stub-based unit tests covering metadata, env discovery, Newton-model build, binding setup, pose-merge orchestration, velocity, view-order reorder, and camera-transform stage walk; global-camera edge case not tested.
source/isaaclab_ovphysx/isaaclab_ovphysx/scene_data_providers/init.py Package scaffolding using lazy_export(), consistent with the rest of isaaclab_ovphysx.

Sequence Diagram

sequenceDiagram
    participant Sim as SimulationContext
    participant Factory as SceneDataProvider (factory)
    participant OvSDP as OvPhysxSceneDataProvider
    participant PhysX as OvPhysX TensorBinding
    participant Warp as Warp kernel
    participant Newton as Newton body_q

    Sim->>Factory: create(stage, sim_ctx)
    Factory->>Factory: "_get_backend() -> ovphysx"
    Factory->>OvSDP: __init__(stage, sim_ctx)
    OvSDP->>OvSDP: _build_newton_model_from_clone_plan()
    OvSDP->>OvSDP: _setup_scene_bindings()
    OvSDP->>PhysX: create_tensor_binding(pattern, RIGID_BODY_POSE)
    PhysX-->>OvSDP: pose_binding
    OvSDP->>PhysX: create_tensor_binding(pattern, RIGID_BODY_VELOCITY)
    PhysX-->>OvSDP: vel_binding

    loop Each simulation step
        Sim->>OvSDP: update()
        OvSDP->>OvSDP: _refresh_newton_model_if_needed()
        OvSDP->>OvSDP: _read_poses_from_best_source()
        OvSDP->>PhysX: pose_binding.read(pose_buf)
        PhysX-->>OvSDP: poses [Nx7]
        OvSDP->>OvSDP: _apply_xform_poses() [FrameView fallback]
        OvSDP->>Warp: launch _set_body_q_kernel(positions, orientations, body_q)
        Warp->>Newton: body_q updated
    end
Loading

Reviews (1): Last reviewed commit: "Refactor OVPhysX SDP to mirror PhysX sin..." | Re-trigger Greptile

Comment on lines +731 to +784
num_envs = -1

# Breadth-first walk so we discover camera prims across the full stage.
stage_prims = deque([self._stage.GetPseudoRoot()])
while stage_prims:
prim = stage_prims.popleft()
prim_path = prim.GetPath().pathString

world_id = 0
template_path = prim_path
if match := env_pattern.match(prim_path):
# Normalize per-env path to a shared template key (env_%d/...) so
# visualizers can query one camera path for all env instances.
world_id = int(match.group("id"))
template_path = match.group("root") + "%d" + match.group("path")
if world_id > num_envs:
num_envs = world_id

imageable = UsdGeom.Imageable(prim)
if imageable and imageable.ComputeVisibility() == UsdGeom.Tokens.invisible:
continue

if prim.IsA(UsdGeom.Camera):
if template_path not in instances:
instances[template_path] = []
instances[template_path].append((world_id, prim_path))
if template_path not in shared_paths:
shared_paths.append(template_path)

if hasattr(UsdGeom, "TraverseInstanceProxies"):
child_prims = prim.GetFilteredChildren(UsdGeom.TraverseInstanceProxies())
else:
child_prims = prim.GetChildren()
if child_prims:
stage_prims.extend(child_prims)

num_envs += 1
positions: list[list[list[float] | None]] = []
orientations: list[list[list[float] | None]] = []

for template_path in shared_paths:
per_world_pos: list[list[float] | None] = [None] * num_envs
per_world_ori: list[list[float] | None] = [None] * num_envs
for world_id, prim_path in instances.get(template_path, []):
if world_id < 0 or world_id >= num_envs:
continue
prim = self._stage.GetPrimAtPath(prim_path)
if not prim.IsValid():
continue
pos, ori = isaaclab_sim.resolve_prim_pose(prim)
per_world_pos[world_id] = [float(pos[0]), float(pos[1]), float(pos[2])]
per_world_ori[world_id] = [float(ori[0]), float(ori[1]), float(ori[2]), float(ori[3])]
positions.append(per_world_pos)
orientations.append(per_world_ori)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Global cameras silently produce misaligned output

num_envs is initialised to -1 and incremented only when a camera prim matches the env pattern (/World/envs/env_<id>/...). When only global cameras exist, num_envs stays 0 after += 1. Any such camera gets world_id = 0 and template_path = prim_path (no env match), is added to shared_paths and instances, but then at line 775 the guard world_id >= num_envs (i.e. 0 >= 0) skips its pose. The result is a return value where order has the camera path but the corresponding positions[i] / orientations[i] are [] — structurally misaligned with callers that zip order with the pose arrays and expect num_envs-length sub-arrays. Even when env cameras exist, a global camera writes its pose at per_world_pos[0], corrupting the env-0 slot for that template path.

Comment on lines +293 to +296
Deduplicated wildcard-form paths, in input order.
"""
wildcard = [p.replace("/World/envs/env_0", "/World/envs/env_*") for p in paths if "/World/envs/env_0" in p]
return list(dict.fromkeys(wildcard)) if wildcard else list(paths)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Mixed env and non-env rigid-body paths: non-env paths silently dropped

_wildcard_env_paths only converts paths containing /World/envs/env_0. When wildcard is non-empty (any env_0 paths present), the return value is the deduplicated wildcard list — which entirely omits any non-env path. A rigid body at /World/Pedestal that passes the UsdPhysics.RigidBodyAPI filter in _setup_scene_bindings is stripped here, receives no binding, and silently falls through to FrameView fallback. If FrameView also fails for that prim, _read_poses_from_best_source returns None and update() is a silent no-op for every subsequent frame.

Comment on lines +578 to +599
def update(self) -> None:
"""Sync OVPhysX body transforms into the full Newton state.

Reads body poses from the rigid + articulation bindings (with FrameView
fallback) and writes them into ``newton_state.body_q`` via a single
Warp kernel launch.
"""
if not self._needs_newton_sync or self._newton_state is None:
return
try:
self._refresh_newton_model_if_needed()
# Bindings may be deferred if the ovphysx runtime was not ready at __init__.
if not self._rigid_bindings:
self._setup_scene_bindings()
result = self._read_poses_from_best_source()
if result is None:
return
positions, orientations, _, _ = result
positions_wp = wp.from_torch(positions.reshape(-1, 3), dtype=wp.vec3)
orientations_wp = wp.from_torch(orientations.reshape(-1, 4), dtype=wp.quatf)
if positions_wp.shape[0] != self._newton_state.body_q.shape[0]:
return
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Silent exception swallow in _apply_xform_poses hides permanent FrameView failures

The except Exception: continue swallows all errors from FrameView(...) construction and from get_world_poses(). Only self._xform_view_failures is updated, and the warn-once log fires at DEBUG level. If the FrameView fails permanently for a body, the covered mask for that body is never set to True, _read_poses_from_best_source returns None on every frame, and update() is a no-op forever with no WARNING-level log after the first attempt.

Comment on lines +670 to +674
return None

def get_velocities(self) -> dict[str, Any] | None:
"""Return linear/angular velocities concatenated across per-pattern bindings.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 _setup_scene_bindings retried every step after permanent failure

In update(), if not self._rigid_bindings: self._setup_scene_bindings() is meant to defer binding creation until the OVPhysX runtime is ready. But if binding creation fails permanently, _rigid_bindings stays empty and _setup_scene_bindings() is called on every simulation step, walking every body path against the stage repeatedly. A flag distinguishing "deferred because physx not ready" from "tried and failed" would allow the hot path to skip the retry.


def update(self) -> None:
"""Sync OVPhysX body transforms into the full Newton state.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 torch.where(~covered) with a single argument returns a tuple of index tensors; [0] is correct for the 1-D case here but torch.nonzero(~covered, as_tuple=True)[0] (already used in _apply_rigid_binding at line 549) is the conventional idiom and makes the 1-D assumption explicit.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@AntoineRichard AntoineRichard force-pushed the antoiner/feat/ovphysx_scene_data_provider branch from 4384bfb to 41f8784 Compare May 13, 2026 06:53
Copy link
Copy Markdown

@isaaclab-review-bot isaaclab-review-bot Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔄 Follow-up Review (post-push update)

Reviewed incremental changes: 4384bfb → 41f8784

✅ Improvements in this push

  • Added replace_newton_shape_colors() for material parity with PhysX/Newton
  • Enhanced docstrings throughout
  • Added warning when no Newton body paths available
  • Fixed get_velocities() to return None on partial read failure (prevents silent misalignment)

📋 Previous review items

The original P1/P2 items from the prior review remain as-is:

  • P1 issues (global cameras, non-env paths) — these are edge cases that may be acceptable for the initial implementation
  • P2 issues (exception handling, retry logic, idiom consistency) — minor improvements that could be addressed in follow-up PRs

Since the PR has already been approved, these can be tracked as potential follow-up work if they cause issues in practice.


Update (1d70296): Reviewed new commits 41f8784 → 1d70296 which add OvPhysxSceneDataBackend for the new SceneDataProvider API (#5128 integration). Changes look solid:

  • ✅ New OvPhysxSceneDataBackend class properly implements the SceneDataBackend interface
  • ✅ Comprehensive unit tests (8 tests) cover transform count/paths, setup, and read logic
  • ✅ Backend constructed eagerly in OvPhysxManager.initialize matching PhysX pattern
  • ✅ Changelog fragment added

No new issues introduced. The implementation correctly mirrors the existing one-pattern-per-binding approach from the original SceneDataProvider.


Update (49a64ed): Reviewed 1d70296 → 49a64ed:

  • ✅ Performance optimization: pose_buf_transformf view now cached at setup time (eliminates per-step Python allocations)
  • ✅ Improved docstrings explaining the zero-copy reinterpretation strategy
  • ✅ Type annotation fix (list[dict[str, Any]])
  • ✅ Two new tests for error handling paths (test_setup_continues_when_create_tensor_binding_raises, test_transforms_logs_warning_when_a_binding_read_fails)

Good refinements, no new issues.

AntoineRichard added a commit to AntoineRichard/IsaacLab that referenced this pull request May 15, 2026
Verification agents flagged five issues against the previous two
commits and one issue-isaac-sim#876 gap that the first pass missed.

1. Newton using-kamino.rst literalinclude path was one level short
   (../../../../../ → ../../../../../../). The previous depth resolved
   to a non-existent /docs/source/isaaclab_tasks/... and would have
   broken the sphinx build.

2. PhysX supported-features.rst listed Ray Caster, Visuo-tactile, and
   Camera as PhysX-specific sensors. Ray Caster and Camera are
   implemented in isaaclab core (backend-agnostic); Visuo-tactile lives
   in isaaclab_contrib. Reframe the section to separate PhysX-implemented
   sensors from backend-agnostic ones. Drop the unverifiable
   "path-traced" qualifier on the RTX renderer claim.

3. OvPhysX stub referenced only PR isaac-sim#5426 and PR isaac-sim#5459. The actual
   in-flight set spans six PRs: isaac-sim#5426 (merged), isaac-sim#5459, isaac-sim#5422, isaac-sim#5421,
   isaac-sim#5570, isaac-sim#5589. List them all with merge state, and reword "primary
   covered surfaces" to reflect that most are still open PRs.

4. backends/index.rst feature matrix said OvPhysX sensor coverage was
   "Partial" — actually only RigidObject is merged. Replace the
   matrix rows with concrete "In-flight (PR #...)" / "Not yet" cells.

5. Issue isaac-sim#876 asked to "review the limitations list and update it." The
   previous pass only reworded the intro. Refresh the task list against
   develop's actual newton_mjwarp coverage, add Shadow Hand / Shadow
   Hand Over / cabinet / dexsuite / rough-terrain locomotion, and
   replace the rigid bullet list with a discovery recipe so the list
   stops bit-rotting.
@kellyguo11 kellyguo11 moved this to In review in Isaac Lab May 17, 2026
Implements the SceneDataBackend interface introduced in develop's PR isaac-sim#5128
("New Scene Data Provider") for the kitless OVPhysX backend. The backend
adapts the wheel's one-pattern-per-binding API by bucketing
UsdPhysics.RigidBodyAPI prims by their env-wildcard form (cartpole -> 2
bindings, Allegro hand -> ~17), pre-allocating per-binding read buffers
plus a single merged wp.transformf buffer, and concatenating reads on
each transforms-property access. OvPhysxManager exposes the backend via
the new get_scene_data_backend() classmethod, initialized at warmup.
Pre-commit reformats dict-literal and SimpleNamespace(...) constructions
in the new test file to multi-line form. Mechanical change with no
semantic effect; tests still pass 9/9.
Bumps isaaclab_ovphysx by a minor tier. Describes the new
OvPhysxSceneDataBackend and OvPhysxManager.get_scene_data_backend
introduced in the previous commit and the per-binding/merged-buffer
strategy used to bridge the wheel's one-pattern-per-binding API to
the central SceneDataProvider.
@AntoineRichard AntoineRichard force-pushed the antoiner/feat/ovphysx_scene_data_provider branch from 41f8784 to d4855e4 Compare May 17, 2026 20:16
SimulationContext.__init__ wires up the central SceneDataProvider with
the result of physics_manager.get_scene_data_backend() — when the
backend was lazy-constructed in _warmup_and_load, that capture happened
before warmup and the SDP held backend=None forever, breaking
NewtonManager.update_visualization_state on the first call to
visualizer.initialize().

Construct the backend instance in initialize() (matches PhysX's
pattern) so SimulationContext gets a real reference. setup() still
runs in _warmup_and_load when the wheel and USD stage are live; the
backend reports empty transform_paths until then, which create_mapping
already handles (returns None).

Also clear the singleton in close() so cached TensorBinding handles
don't survive a physx.reset() across SimulationContext restarts.
Matches Newton's lifecycle.
Folds in the polish items the post-implementation code review surfaced:

* Cache the per-binding wp.transformf view at setup time (entry
  pose_buf_transformf) instead of reconstructing on every transforms
  access. Removes Python allocation churn on hot rendering paths.
* Widen _rigid_bindings annotation from list[dict[str, object]] to
  list[dict[str, Any]]. Adds an inline comment documenting the
  per-entry shape so future readers don't have to dig.
* SI-unit note in the transforms property docstring per AGENTS.md:
  position [m], quaternion (xyzw, unit).
* Two new tests covering the previously unverified defensive paths:
  test_setup_continues_when_create_tensor_binding_raises and
  test_transforms_logs_warning_when_a_binding_read_fails. Both assert
  the surviving bindings still produce correct output, so the
  fault-isolation isn't just decorative.

Tests: 11/11 pass (was 9). Pre-commit clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation isaac-lab Related to Isaac Lab team

Projects

Status: In review

Development

Successfully merging this pull request may close these issues.

2 participants