Skip to content

Migrate OVPhysX RigidObjectCollection onto OvPhysxView (view series, part 4)#6227

Open
AntoineRichard wants to merge 13 commits into
isaac-sim:developfrom
AntoineRichard:antoiner/feat/ovphysx_rigidobjectcollection_view
Open

Migrate OVPhysX RigidObjectCollection onto OvPhysxView (view series, part 4)#6227
AntoineRichard wants to merge 13 commits into
isaac-sim:developfrom
AntoineRichard:antoiner/feat/ovphysx_rigidobjectcollection_view

Conversation

@AntoineRichard

Copy link
Copy Markdown
Collaborator

🔗 Stacked on top of #6224 (Part 1 — OvPhysxView)

This PR depends on #6224 and is branched off it (a sibling of the Articulation #6225
and RigidObject #6226 migrations, not stacked on them). It targets develop, but because
#6224 has not merged yet, the diff below includes the Part 1 OvPhysxView commits as well
as
the Part 4 migration. Please review #6224 first. Once #6224 merges this branch will
be rebased onto develop and the diff will shrink to the collection migration only.

Description

Part 4 of the OVPhysX view-migration series. It migrates the OVPhysX RigidObjectCollection
onto OvPhysxView (introduced in #6224), following Parts 2 (#6225) and 3 (#6226).

This is the part that exercises the view's fused multi-prim pathprim_paths=[...] plus
key_aliases — which the earlier parts did not touch, so it dogfoods that slice of the API.

What changed

  • RigidObjectCollection._initialize_impl builds a single
    OvPhysxView(prim_paths=[g0, …], key_aliases={LINK_POSE: RIGID_BODY_POSE, …}). The view
    creates each fused binding from its underlying RIGID_BODY_* type but stores it under the
    collection's LINK_*/BODY_* data-class key via key_aliases (every alias stays within its
    CPU/GPU residency class, which the view requires). Eager creation is fail-loud.
  • RigidObjectCollectionData is built from the view; both _get_binding helpers delegate to
    OvPhysxView.try_binding_for, and the dead _binding_getter callback is removed.
  • Reads route through root_view.read_into(...) on the native fused path (cached
    reinterpret); the articulation-mode mock read path (iface tests) keeps its raw binding.read
    because its buffer device intentionally bypasses the view's strict device check.
  • Writes route through root_view.set_attribute(...) — the wrench plus the body-major
    reshape write helper, including CPU-only mass/COM/inertia (pre-staged to pinned CPU).
  • RigidObjectCollection.root_view now returns the OvPhysxView instead of a raw
    dict[TensorType, TensorBinding] (breaking).

Breaking change / migration

RigidObjectCollection.root_view returns an OvPhysxView, not a dict:

  • root_view[tensor_type]root_view.try_binding_for(tensor_type)
  • full-tensor read → root_view.get_attribute(tensor_type)

Type of change

  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Bug fix (non-breaking change which fixes an issue)
  • This change requires a documentation update

Checklist

  • I have run the pre-commit checks with ./isaaclab.sh --format
  • I have added tests that prove my fix is effective or that my feature works (collection tests exercise the migrated read/write paths)
  • I have updated the changelog (source/isaaclab_ovphysx/changelog.d/)
  • I have updated the documentation accordingly
  • I have added my changes to the right changelog.d fragment(s)

Testing

Run kitless; cpu and cuda must be separate invocations (process-global device lock):

72 passed, 4 xfailed   # test_rigid_object_collection.py -k cpu
72 passed, 4 xfailed   # test_rigid_object_collection.py -k cuda

@github-actions github-actions Bot added the isaac-lab Related to Isaac Lab team label Jun 22, 2026
OVPhysX exposes physics attributes as a loose dict of TensorType ->
TensorBinding with no view object, unlike Newton's
selection.ArticulationView and PhysX's typed tensor views. OvPhysxView
wraps the bindings for one prim pattern behind a string-keyed
get_attribute / set_attribute surface, addressing attributes by the
lowercased TensorType enum name (e.g. "articulation_dof_stiffness").
It needs no Model/State/Control source object because the TensorType
already implies where the data lives.

Prototype per docs/superpowers/specs/2026-06-17-ovphysx-view-design.md.
Adds unit tests covering name<->enum resolution, the read-only guard,
discoverability, and get/set dispatch against a fake binding (no native
simulation required).
Reworks the view from a convenience wrapper into a layer that can back the
OVPhysX asset/data classes, per the PR review of isaac-sim#6224:

- read_into(name, dst): zero-copy fill of a caller-owned, possibly
  structured-dtype buffer (e.g. wp.transformf) via a float32 reinterpret
  view -- the mechanism the data containers use today.
- set_attribute: accepts structured-dtype sources via the same reinterpret;
  non-float32-width buffers are rejected rather than silently bit-cast.
- prim_paths + key_aliases: support the fused multi-prim binding form
  (create_tensor_binding(prim_paths=[...])) and storing a binding under a
  different TensorType key, as RigidObjectCollection needs.
- binding_for(): raw TensorBinding accessor for adoption.
- _CPU_ONLY_NAMES is now derived from tensor_types._CPU_ONLY_TYPES (no drift).
- Added joint/tendon/is_fixed_base metadata passthrough; eager construction
  raises if it creates zero bindings; get_attribute allocates a fresh buffer
  (no aliasing); nested error hierarchy; PhysX/binding Protocols.

Device policy: no implicit CPU<->GPU conversion. CPU-resident property types
are read/written on CPU; a buffer on the wrong device raises DeviceMismatch
instead of being staged. Device-less host data (numpy/list) is materialized
on the binding's native device.
The OvPhysxView addition is a significant new public surface for the OVPhysX
backend, so promote the changelog fragment from a minor to a major bump.
Reword the entry to describe the binding-manager surface (read_into, the
no-device-conversion policy) and drop the internal design-note path from the
user-facing changelog.
From the second PR review of isaac-sim#6224:

- Critical: _as_binding_view now requires a float32 scalar dtype before the
  zero-copy reinterpret. A same-byte-width wrong dtype (int32) previously passed
  the count-only guard and was bit-reinterpreted into garbage on the write path;
  sub-4-byte dtypes (float16) produced a misleading "0 elements" error. Both are
  now rejected with a clear message. Regression tests added (verified failing
  without the guard).
- _resolve enforces the str | TensorType union and raises UnknownAttribute on
  anything else, instead of letting a bogus key reach the wheel.
- _binding accesses binding.count directly (a malformed binding surfaces instead
  of being masked as a phantom no-match) and surfaces the underlying
  create_tensor_binding exception in the AttributeUnavailable message.
- Added docstrings to the six metadata properties; dropped the unused
  runtime_checkable decorator.
- Tests: same-byte/sub-4-byte dtype rejection, get_attribute(out=) wrong device,
  both indices+mask forwarded, read/write through a prim_paths+key_aliases view,
  non-str/non-TensorType key, and a read-only-names-are-valid-vocabulary check.
From the API-hardening review of isaac-sim#6224. Validate at the boundary and fail loud
instead of silently corrupting, mis-binding, or no-op'ing:

- Reject non-contiguous buffers in _as_binding_view (a strided/sliced source would
  be reinterpreted as contiguous and read/write the wrong memory).
- Canonicalize the device (wp.get_device) so a "cuda" view accepts a "cuda:0"
  buffer instead of raising a spurious, unsatisfiable DeviceMismatch; falls back
  to the raw string when the device can't be resolved locally.
- Reject TensorType.INVALID via the member path too (string path already did).
- Normalize key_aliases to TensorType members so string keys are honored rather
  than silently dropped, and reject aliases that cross the CPU/GPU residency or
  read-only boundary (the device/read-only guards key on the requested type).
- Reject empty pattern/prim_paths and tensor_types-without-eager at construction.
- Eager construction with an explicit tensor_types list now surfaces a failing
  type instead of swallowing it at debug level (default sweep still skips
  inapplicable types).
- Document binding_for as an unguarded escape hatch, get_attribute's native-device
  return, and the has_attribute name-validity-vs-availability split.

Adds regression tests for each (contiguity and INVALID verified failing without
the guard).
Surfaced by dogfooding the view in the articulation migration: the assets branch
on a "binding or None" pattern for optional/absent bindings (tendon types on a
tendon-less articulation, not-yet-created bindings), which the raising binding_for
can't express. try_binding_for returns None when the attribute is valid but not
available for the view's prims, while still raising UnknownAttribute for an invalid
name (a programming error, not an availability question).
…xView

read_into now reuses the float32 reinterpret of a destination buffer
across calls (keyed by buffer id, with a pointer-staleness guard), so the
wheel's object-identity read cache stays warm even when callers hand a
structured buffer each step -- they no longer need to maintain their own
reinterpret cache. get_attribute returns a typed array for attributes with
a known structured layout (transformf for poses, spatial_vectorf for
velocities, via a hand-maintained _ATTR_DTYPE map) and flat float32
otherwise.

This lets the asset data containers drop their bespoke _get_read_view
caching and read structured buffers straight through the view.
OvPhysxView (and OvPhysxFrameView) live in isaaclab_ovphysx.sim.views,
which had no API-docs page. Add the automodule stub and wire it into the
isaaclab_ovphysx autosummary so the new binding-manager view shows up in
the rendered API reference alongside assets / cloner / physics.
Three fixes from the Part 1 review:

- get_attribute (no out): route the freshly allocated buffer through
  _as_binding_view directly instead of the id()-keyed read cache. A fresh
  buffer can never hit the cache and would leak one entry (keeping the
  buffer alive) per call in a step loop; the cache only pays off for a
  reused out/dst buffer. Add a regression test asserting the no-out path
  leaves _read_views empty.

- Raise a dedicated DtypeMismatch instead of ShapeMismatch when a buffer's
  scalar element type is not float32, so a dtype error no longer reads as a
  dimensions error. Update the affected tests and the Raises docstrings.

- Make the view Warp-native: drop the fragile __module__ string-match that
  auto-converted Torch tensors on writes. Callers bridge a Torch tensor
  with wp.from_torch(t), keeping the device policy explicit and avoiding an
  optional Torch dependency.
@AntoineRichard AntoineRichard force-pushed the antoiner/feat/ovphysx_rigidobjectcollection_view branch from 0d4d0c0 to 039f8c3 Compare June 23, 2026 08:16
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Jun 23, 2026
Documentation/comment clarifications from the isaac-sim#6224 review (no behavior change):

- Narrow the documented contract to float32-only: attribute_names/has_attribute
  and the module docstring now state that a listed name is name-validity, not a
  dtype-support promise; non-float dtype handling awaits wheel dtype metadata.
- Mark _READ_ONLY_NAMES explicitly temporary; name the three access modes
  (read/write, read-only, write-only) and the wheel access_mode enum that should
  replace the table.
- Document key_aliases as an internal collection adapter, not general public API,
  pending descriptor metadata.
- Make the view test scope explicit: mock API mechanics here; live
  CPU-only-on-GPU / read-only+write-only / structured read_into coverage lives in
  the asset-integration tests.
Migrate the OVPhysX RigidObjectCollection onto OvPhysxView (view series,
part 4; rebased onto develop). One OvPhysxView(prim_paths=[...],
key_aliases=...) creates each fused binding from its RIGID_BODY_* type and
stores it under the collection's LINK_*/BODY_* data-class key. The data
container is built from the view; both _get_binding helpers delegate to it;
the dead _binding_getter callback is removed. Native fused reads route
through read_into (cached); the iface-test mock read path keeps raw
binding.read (its buffer device bypasses the view's strict device check).
Writes route through set_attribute. root_view now returns the OvPhysxView
(breaking).

Verified on both devices (cpu and cuda:0: 72 passed, 4 xfailed each).
@AntoineRichard AntoineRichard force-pushed the antoiner/feat/ovphysx_rigidobjectcollection_view branch from 039f8c3 to 68f35b8 Compare June 23, 2026 10:01
@AntoineRichard AntoineRichard marked this pull request as ready for review June 25, 2026 11:17
@greptile-apps

greptile-apps Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Part 4 of the OVPhysX view-migration series: migrates RigidObjectCollection and its data container from a raw dict[TensorType, TensorBinding] to an OvPhysxView, following the same pattern established in Parts 2-3 for articulations and rigid objects.

  • _initialize_impl now constructs a single OvPhysxView(prim_paths=[...], key_aliases={LINK_*: RIGID_BODY_*}) instead of populating a plain bindings dict; all six fused bindings are eagerly created and the view's device/dtype/read-only guards apply automatically.
  • All reads route through root_view.read_into(...) and all writes through root_view.set_attribute(...); the mock/native dispatch in _read_binding_into_instance_major is preserved.
  • RigidObjectCollection.root_view now returns OvPhysxView instead of the dict (breaking change, documented in changelog).

Confidence Score: 4/5

The migration is structurally sound and consistent with Parts 2-3; all read/write dispatch paths are plausible given the 72-pass test suite.

All changed lines route correctly through the view's device/dtype guards, the mock-vs-native dispatch is explicitly preserved, and the tests cover CPU and CUDA paths for all major operations. The three findings are about pre-initialization robustness and type annotation completeness, not incorrect runtime behavior.

rigid_object_collection.py deserves a second look for the root_view return annotation and the broad except Exception in the binding-creation loop.

Important Files Changed

Filename Overview
source/isaaclab_ovphysx/isaaclab_ovphysx/assets/rigid_object_collection/rigid_object_collection.py Core migration: replaces the raw dict with an OvPhysxView; minor issues with root_view return annotation omitting None and _get_binding crashing before initialization.
source/isaaclab_ovphysx/isaaclab_ovphysx/assets/rigid_object_collection/rigid_object_collection_data.py Data container migrated cleanly: _bindings dict and _binding_getter callback replaced by a single _view (OvPhysxView); mock/native dispatch preserved.
source/isaaclab_ovphysx/test/assets/test_rigid_object_collection.py Minimal correct update: _MATERIAL_GAP_REASON string updated to reflect root_view is now an OvPhysxView rather than a dict.
source/isaaclab_ovphysx/changelog.d/antoiner-feat-ovphysx_rigidobjectcollection_view.major.rst Changelog accurately documents the breaking root_view return-type change and migration path.

Comments Outside Diff (1)

  1. source/isaaclab_ovphysx/isaaclab_ovphysx/assets/rigid_object_collection/rigid_object_collection.py, line 1210-1224 (link)

    P2 _get_binding crashes before initialization instead of returning None

    Before this PR, _get_binding used self._bindings.get(tensor_type) which safely returned None when _bindings was still an empty dict (before _initialize_impl ran). The new implementation calls self._root_view.try_binding_for(tensor_type), but _root_view is None until _initialize_impl completes, so any early call will raise AttributeError: 'NoneType' object has no attribute 'try_binding_for' rather than returning None silently. A simple guard if self._root_view is None: return None would restore the prior safety contract.

Reviews (1): Last reviewed commit: "Merge branch 'develop' into antoiner/fea..." | Re-trigger Greptile

Comment on lines +125 to +137
def root_view(self) -> OvPhysxView:
"""Root view for the rigid object collection.

Dictionary keyed by TensorType constant, each value a single fused
:class:`~isaaclab_ovphysx.TensorBinding` spanning all bodies in the collection.
On OVPhysX this is an :class:`~isaaclab_ovphysx.sim.views.OvPhysxView` over a single
**fused** multi-prim binding per tensor type (created with ``prim_paths=[...]``),
spanning all bodies in the collection. The collection's ``LINK_*``/``BODY_*``
data-class keys are mapped to the underlying ``RIGID_BODY_*`` types via the view's
``key_aliases``.

.. note::
Use this view with caution. It requires handling of tensors in a specific way.
"""
return self._bindings
return self._root_view

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 root_view return-type annotation omits None

_root_view is initialized to None in __init__ and is only assigned a real OvPhysxView during _initialize_impl. The property's return annotation is OvPhysxView (not OvPhysxView | None), so any caller who accesses root_view before sim.reset() will get None while the type system promises a live view. This becomes especially visible because the PR description explicitly calls out root_view as part of the public breaking-change migration guide.

Comment on lines +1115 to 1124
for store_key in key_aliases:
try:
self._bindings[store_key] = self._ovphysx.create_tensor_binding(
prim_paths=self._prim_paths, tensor_type=rb_tt
)
self._root_view.binding_for(store_key)
except Exception as e:
raise RuntimeError(
f"OVPhysX could not create fused RIGID_BODY binding {rb_tt!r} for"
f"OVPhysX could not create the fused RIGID_BODY binding for {store_key!r} with"
f" prim_paths={self._prim_paths!r}."
f" Check that each prim path matches at least one"
f" UsdPhysics.RigidBodyAPI prim."
) from e

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 Binding creation loop uses overly broad except Exception

The loop calls self._root_view.binding_for(store_key) and wraps it in except Exception. This swallows unexpected errors from the wheel's create_tensor_binding and re-raises them as a generic RuntimeError, discarding the original exception type. Catching OvPhysxView.AttributeUnavailable specifically (or at least OvPhysxView.OvPhysxViewError) would preserve unexpected failures while still providing the helpful user-facing message for the expected "no matching prim" case.

Clear self._root_view in _invalidate_initialize_callback so a stale/destroyed
binding cached by the fused view is not held across a simulation stop;
_initialize_impl rebuilds a fresh OvPhysxView on the next play.
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

None yet

Development

Successfully merging this pull request may close these issues.

1 participant