Skip to content

Commit 49a64ed

Browse files
Address remaining SDP code-review feedback
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.
1 parent 1d70296 commit 49a64ed

2 files changed

Lines changed: 143 additions & 21 deletions

File tree

source/isaaclab_ovphysx/isaaclab_ovphysx/physics/ovphysx_manager.py

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,14 @@ class OvPhysxSceneDataBackend(SceneDataBackend):
5454

5555
def __init__(self):
5656
self._physx = None
57-
self._rigid_bindings: list[dict[str, object]] = []
57+
# Each entry: ``{"pattern": str, "pose": TensorBinding,
58+
# "pose_buf": wp.array (float32, (N, 7)),
59+
# "pose_buf_transformf": wp.array (transformf, (N,)),
60+
# "row_offset": int, "row_count": int}``.
61+
# The ``pose_buf_transformf`` view aliases ``pose_buf`` via zero-copy
62+
# ``wp.array(ptr=...)``; cached at setup time so per-step reads in
63+
# :attr:`transforms` don't churn Python allocations.
64+
self._rigid_bindings: list[dict[str, Any]] = []
5865
self._merged_transforms: wp.array | None = None
5966
self._scene_data = SceneDataFormat.Transform()
6067
self._device: str = "cpu"
@@ -116,11 +123,23 @@ def setup(self, physx, stage) -> None:
116123
pattern,
117124
)
118125
continue
126+
pose_buf = wp.zeros(pose_binding.shape, dtype=wp.float32, device=self._device)
127+
# Zero-copy reinterpret of the (N, 7) float32 staging buffer as (N,) wp.transformf.
128+
# Same pointer + layout; transformf is 7 float32s (pos.xyz + quat.xyzw). Cached
129+
# so per-step ``transforms`` reads don't reallocate the view object.
130+
pose_buf_transformf = wp.array(
131+
ptr=pose_buf.ptr,
132+
shape=(row_count,),
133+
dtype=wp.transformf,
134+
device=str(pose_buf.device),
135+
copy=False,
136+
)
119137
self._rigid_bindings.append(
120138
{
121139
"pattern": pattern,
122140
"pose": pose_binding,
123-
"pose_buf": wp.zeros(pose_binding.shape, dtype=wp.float32, device=self._device),
141+
"pose_buf": pose_buf,
142+
"pose_buf_transformf": pose_buf_transformf,
124143
"row_offset": total_count,
125144
"row_count": row_count,
126145
}
@@ -135,45 +154,37 @@ def transforms(self) -> SceneDataFormat.Transform:
135154
"""Read all bindings into the merged buffer; return as ``SceneDataFormat.Transform``.
136155
137156
Each binding's float32 ``(N, 7)`` read buffer is reinterpreted as ``(N,)`` of
138-
``wp.transformf`` (zero-copy via ``wp.array(ptr=..., dtype=wp.transformf)``)
139-
and copied into the merged buffer at the binding's ``row_offset``.
157+
``wp.transformf`` (zero-copy via ``wp.array(ptr=..., dtype=wp.transformf)``,
158+
cached on the entry at setup time) and copied into the merged buffer at the
159+
binding's ``row_offset``.
140160
141161
Returns:
142-
``SceneDataFormat.Transform`` with ``transforms`` set to the merged
143-
``wp.array(dtype=wp.transformf)``. ``transforms`` is ``None`` when no
162+
``SceneDataFormat.Transform`` whose ``transforms`` field is a
163+
``wp.array(dtype=wp.transformf)`` of length :attr:`transform_count`.
164+
Each ``wp.transformf`` row carries position [m] followed by
165+
quaternion (xyzw, unit). ``transforms`` is ``None`` when no
144166
bindings are wired.
145167
"""
146168
if self._merged_transforms is None or not self._rigid_bindings:
147169
self._scene_data.transforms = self._merged_transforms
148170
return self._scene_data
149171

150172
for entry in self._rigid_bindings:
151-
buf: wp.array = entry["pose_buf"]
152173
try:
153-
entry["pose"].read(buf)
174+
entry["pose"].read(entry["pose_buf"])
154175
except Exception as exc:
155176
logger.warning(
156177
"[OvPhysxSceneDataBackend] RIGID_BODY_POSE read failed for %s: %s",
157178
entry["pattern"],
158179
exc,
159180
)
160181
continue
161-
# Reinterpret the (N, 7) float32 buffer as (N,) wp.transformf without a copy.
162-
n = int(entry["row_count"])
163-
transformf_view = wp.array(
164-
ptr=buf.ptr,
165-
shape=(n,),
166-
dtype=wp.transformf,
167-
device=str(buf.device),
168-
copy=False,
169-
)
170-
offset = int(entry["row_offset"])
171182
wp.copy(
172183
self._merged_transforms,
173-
transformf_view,
174-
dest_offset=offset,
184+
entry["pose_buf_transformf"],
185+
dest_offset=int(entry["row_offset"]),
175186
src_offset=0,
176-
count=n,
187+
count=int(entry["row_count"]),
177188
)
178189

179190
self._scene_data.transforms = self._merged_transforms

source/isaaclab_ovphysx/test/physics/test_ovphysx_scene_data_backend.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,16 +174,24 @@ def fake_read_b(dst):
174174
host = np.array([[3.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0]], dtype=np.float32)
175175
_wp.copy(dst, _wp.from_numpy(host, dtype=_wp.float32, device="cpu").reshape((1, 7)))
176176

177+
# ``pose_buf_transformf`` is the zero-copy transformf view over the float32 staging
178+
# buffer; production code caches it at setup time. Tests mirror that shape here.
179+
buf_a_tf = _wp.array(ptr=buf_a.ptr, shape=(2,), dtype=_wp.transformf, device="cpu", copy=False)
180+
buf_b_tf = _wp.array(ptr=buf_b.ptr, shape=(1,), dtype=_wp.transformf, device="cpu", copy=False)
177181
b._rigid_bindings = [
178182
{
183+
"pattern": "/World/envs/env_*/Cube",
179184
"pose": SimpleNamespace(read=fake_read_a, prim_paths=["/Cube0", "/Cube1"]),
180185
"pose_buf": buf_a,
186+
"pose_buf_transformf": buf_a_tf,
181187
"row_offset": 0,
182188
"row_count": 2,
183189
},
184190
{
191+
"pattern": "/World/envs/env_*/Pole",
185192
"pose": SimpleNamespace(read=fake_read_b, prim_paths=["/Pole"]),
186193
"pose_buf": buf_b,
194+
"pose_buf_transformf": buf_b_tf,
187195
"row_offset": 2,
188196
"row_count": 1,
189197
},
@@ -236,3 +244,106 @@ def test_manager_returns_none_when_backend_uninitialized():
236244
assert OvPhysxManager.get_scene_data_backend() is None
237245
finally:
238246
OvPhysxManager._scene_data_backend = saved
247+
248+
249+
def test_setup_continues_when_create_tensor_binding_raises(monkeypatch, caplog):
250+
"""A single failed binding-creation logs a warning and skips that pattern; others proceed."""
251+
import logging
252+
253+
from isaaclab_ovphysx.physics.ovphysx_manager import OvPhysxSceneDataBackend
254+
255+
b = OvPhysxSceneDataBackend()
256+
b._device = "cpu"
257+
258+
paths = [
259+
"/World/envs/env_0/Robot/cart",
260+
"/World/envs/env_0/Robot/pole",
261+
]
262+
263+
def fake_traverse():
264+
for p in paths:
265+
yield SimpleNamespace(
266+
HasAPI=lambda api: True,
267+
GetPath=lambda p=p: SimpleNamespace(pathString=p),
268+
)
269+
270+
stage = SimpleNamespace(Traverse=fake_traverse)
271+
272+
class FlakyPhysX:
273+
def create_tensor_binding(self, pattern, tensor_type):
274+
if pattern.endswith("/cart"):
275+
raise RuntimeError("simulated wheel-side failure")
276+
return SimpleNamespace(
277+
pattern=pattern, tensor_type=tensor_type, shape=(1, 7), count=1, prim_paths=[], read=lambda dst: None
278+
)
279+
280+
import isaaclab_ovphysx.physics.ovphysx_manager as om_mod
281+
282+
monkeypatch.setattr(om_mod, "UsdPhysics", SimpleNamespace(RigidBodyAPI=object()))
283+
284+
with caplog.at_level(logging.WARNING, logger=om_mod.logger.name):
285+
b.setup(FlakyPhysX(), stage)
286+
287+
# The pole pattern survived; the cart pattern was logged and skipped.
288+
assert len(b._rigid_bindings) == 1
289+
assert b._rigid_bindings[0]["pattern"].endswith("/pole")
290+
assert any("simulated wheel-side failure" in record.message for record in caplog.records)
291+
292+
293+
def test_transforms_logs_warning_when_a_binding_read_fails(caplog):
294+
"""A failed ``binding.read(dst)`` logs and skips that binding; other bindings still merge."""
295+
import logging
296+
297+
import warp as _wp
298+
299+
_wp.init()
300+
301+
from isaaclab_ovphysx.physics.ovphysx_manager import OvPhysxSceneDataBackend
302+
303+
b = OvPhysxSceneDataBackend()
304+
b._device = "cpu"
305+
b._merged_transforms = _wp.zeros((2,), dtype=_wp.transformf, device="cpu")
306+
307+
buf_good = _wp.zeros((1, 7), dtype=_wp.float32, device="cpu")
308+
buf_bad = _wp.zeros((1, 7), dtype=_wp.float32, device="cpu")
309+
buf_good_tf = _wp.array(ptr=buf_good.ptr, shape=(1,), dtype=_wp.transformf, device="cpu", copy=False)
310+
buf_bad_tf = _wp.array(ptr=buf_bad.ptr, shape=(1,), dtype=_wp.transformf, device="cpu", copy=False)
311+
312+
def good_read(dst):
313+
import numpy as np
314+
315+
host = np.array([[7.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0]], dtype=np.float32)
316+
_wp.copy(dst, _wp.from_numpy(host, dtype=_wp.float32, device="cpu").reshape((1, 7)))
317+
318+
def bad_read(dst):
319+
raise RuntimeError("simulated read failure")
320+
321+
b._rigid_bindings = [
322+
{
323+
"pattern": "/World/envs/env_*/Good",
324+
"pose": SimpleNamespace(read=good_read, prim_paths=["/Good"]),
325+
"pose_buf": buf_good,
326+
"pose_buf_transformf": buf_good_tf,
327+
"row_offset": 0,
328+
"row_count": 1,
329+
},
330+
{
331+
"pattern": "/World/envs/env_*/Bad",
332+
"pose": SimpleNamespace(read=bad_read, prim_paths=["/Bad"]),
333+
"pose_buf": buf_bad,
334+
"pose_buf_transformf": buf_bad_tf,
335+
"row_offset": 1,
336+
"row_count": 1,
337+
},
338+
]
339+
340+
import isaaclab_ovphysx.physics.ovphysx_manager as om_mod
341+
342+
with caplog.at_level(logging.WARNING, logger=om_mod.logger.name):
343+
out = b.transforms
344+
345+
assert out is b._scene_data
346+
assert any("simulated read failure" in record.message for record in caplog.records)
347+
# Good row was still written; bad row is left at the merged buffer's prior contents (zeros).
348+
flat = out.transforms.numpy().view("<f4").reshape((2, 7))
349+
assert flat[0, 0] == 7.0

0 commit comments

Comments
 (0)