Migrate OVPhysX RigidObjectCollection onto OvPhysxView (view series, part 4)#6227
Conversation
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.
627c298 to
0d4d0c0
Compare
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.
0d4d0c0 to
039f8c3
Compare
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).
039f8c3 to
68f35b8
Compare
Greptile SummaryPart 4 of the OVPhysX view-migration series: migrates
Confidence Score: 4/5The 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
|
| 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 |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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.
Description
Part 4 of the OVPhysX view-migration series. It migrates the OVPhysX
RigidObjectCollectiononto
OvPhysxView(introduced in #6224), following Parts 2 (#6225) and 3 (#6226).This is the part that exercises the view's fused multi-prim path —
prim_paths=[...]pluskey_aliases— which the earlier parts did not touch, so it dogfoods that slice of the API.What changed
RigidObjectCollection._initialize_implbuilds a singleOvPhysxView(prim_paths=[g0, …], key_aliases={LINK_POSE: RIGID_BODY_POSE, …}). The viewcreates each fused binding from its underlying
RIGID_BODY_*type but stores it under thecollection's
LINK_*/BODY_*data-class key viakey_aliases(every alias stays within itsCPU/GPU residency class, which the view requires). Eager creation is fail-loud.
RigidObjectCollectionDatais built from the view; both_get_bindinghelpers delegate toOvPhysxView.try_binding_for, and the dead_binding_gettercallback is removed.root_view.read_into(...)on the native fused path (cachedreinterpret); the articulation-mode mock read path (iface tests) keeps its raw
binding.readbecause its buffer device intentionally bypasses the view's strict device check.
root_view.set_attribute(...)— the wrench plus the body-majorreshape write helper, including CPU-only mass/COM/inertia (pre-staged to pinned CPU).
RigidObjectCollection.root_viewnow returns theOvPhysxViewinstead of a rawdict[TensorType, TensorBinding](breaking).Breaking change / migration
RigidObjectCollection.root_viewreturns anOvPhysxView, not a dict:root_view[tensor_type]→root_view.try_binding_for(tensor_type)root_view.get_attribute(tensor_type)Type of change
Checklist
pre-commitchecks with./isaaclab.sh --formatsource/isaaclab_ovphysx/changelog.d/)changelog.dfragment(s)Testing
Run kitless; cpu and cuda must be separate invocations (process-global device lock):