Use solver-reported accelerations and track runtime gravity in PhysX and OvPhysX IMU/PVA sensors - #7416
Conversation
Greptile SummaryThe PR replaces finite-difference IMU acceleration with solver-reported COM acceleration and transports it to the configured sensor frame, while retaining an OVPhysX fallback for older wheels.
Confidence Score: 4/5The OVPhysX fallback should be corrected before merging because a wheel with an exposed but unusable acceleration binding can prevent the IMU from initializing. The solver-acceleration transport is consistent with repository spatial-vector and COM conventions, but optional OVPhysX capability handling calls a raising binding API before the documented finite-difference fallback can be selected. Files Needing Attention: source/isaaclab_ov/isaaclab_ov/sensors/imu/imu.py Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
Step[Physics step] --> Solver[Solver-reported COM acceleration]
Solver --> PhysX[PhysX IMU kernel]
Solver --> Capability{OV acceleration binding usable?}
Capability -->|Yes| OVKernel[OV solver-acceleration kernel]
Capability -->|No| Fallback[OV finite-difference kernel]
PhysX --> Transport[Transport COM acceleration to sensor offset]
OVKernel --> Transport
Transport --> Output[Body-frame IMU acceleration plus gravity bias]
Reviews (1): Last reviewed commit: "Use solver-reported accelerations in Phy..." | Re-trigger Greptile |
There was a problem hiding this comment.
Isaac Lab Review Bot
Reviewed the PhysX and OvPhysX IMU transition from velocity finite-differencing to solver-reported accelerations, including center-of-mass transport, gravity bias, buffer caching, recorded launches, fallback behavior, tests, and release notes. No candidate findings were supplied or supported for posting.
- Design and architecture: The backend split is coherent: PhysX uses
RigidBodyView.get_accelerations(), while OvPhysX selects the solver path only whenRIGID_BODY_ACCELERATIONis exposed and otherwise retains the finite-difference path. PVA behavior remains separate and unchanged. - API: The existing
ImuData.lin_acc_bshape and body-frame convention are preserved, while its PhysX semantics intentionally become independent of sensor update period and direct velocity writes. Both affected packages include changelog fragments, and the OvPhysX documentation describes its version-dependent behavior. - Implementation: The new acceleration buffers are connected consistently through acquisition, typed views, pointer-stability checks, kernel launches, and invalidation. Both kernels apply the documented COM-to-sensor transport terms and gravity bias. PhysX regression and recorded-launch coverage were updated; the residual validation gap is that the new OvPhysX solver-binding path was wheel-skipped locally.
No blocking issues. No inline issue met the actionable-evidence threshold; the assessment above records the review feedback.
Automated review; human maintainers own approval decisions.
set_gravity() authored the new gravity into OvStage and applied it to the running scene, but get_gravity() read SimulationCfg.gravity, which the setter never touched. Every call after a live update returned the construction-time value. Track the applied gravity on the manager instead of writing back to cfg.gravity: randomization terms resample from cfg.gravity as the nominal base, so mutating it would compound successive 'add'/'scale' randomizations.
Imu and Pva read scene gravity once in _initialize_impl and baked it into a static buffer, so gravity randomized at runtime never reached the accelerometer bias or the projected gravity direction. Both backends were affected. Refresh the buffer on every update, skipping the refill when the scene gravity is unchanged. The buffer is filled in place rather than reallocated: the PhysX sensors consume it through a recorded wp.Launch that holds the array pointer, and PVA additionally exposes it as the public GRAVITY_VEC_W proxy. Newton's Pva already passes model.gravity into its kernel each update, so it needed no change.
…y buffer PhysX and OvPhysX gravity is scene-wide, so broadcasting one vector into a num_bodies buffer and re-filling it on change was the wrong shape for the data. The kernels now take gravity_bias_w by value. This removes the allocation and the device fill entirely; a gravity change is just a Python-level reassignment. The PhysX recorded launch bakes the value in at record time, so _update_buffers_impl re-binds the parameter with set_param_by_name when it changes, mirroring how env_mask is already handled. If per-env gravity ever reaches these backends the parameter goes back to an array, as Newton already does.
Combines the OvPhysxManager.get_gravity fix and the IMU/PVA runtime gravity tracking with the solver-reported acceleration work so both ship as one PR. Conflict resolutions: - imu kernels: keep the solver-acceleration transport math, with the gravity bias taken by value rather than indexed from a per-body array. - recorded-launch test harness: keep the accelerations-based fixture and thread the gravity sink through it. - the replayed-bias test no longer zeroes finite-difference state, which the solver-acceleration path removed.
The scalar conversion predated the solver-acceleration kernel, so the merge left imu_update_solver_acc_kernel still declaring gravity_bias_w as an array while the sensor passed a vec3f. Every OV IMU test that exercised the solver path failed to launch.
Mirrors the IMU change: PhysX and OvPhysX gravity is scene-wide, so the unit gravity direction no longer needs a per-instance buffer. The kernels take gravity_vec_w by value and the PhysX recorded launch re-binds it on change. The PhysX sensor's public-looking GRAVITY_VEC_W proxy array becomes the internal _gravity_vec_w, matching what the OvPhysX sensor already called it. It was absent from the data class, the type stubs and the docs; consumers wanting this quantity should read data.projected_gravity_b. The identically-named attribute on the asset data classes is untouched.
The IMU and PVA sensors selected between a solver-acceleration kernel and a finite-difference kernel on hasattr(TT, "RIGID_BODY_ACCELERATION"). That guard was written before the binding shipped; the project now pins ovphysx==0.5.11 exactly, and 0.5.11 exposes the binding, so the fallback branch is unreachable on every supported install. Removes both duplicate kernels, the prev_lin_vel_w / prev_ang_vel_w state, the dt-caching update() overrides (PhysX has no equivalent), and the reset-kernel arguments that cleared the dropped buffers. The surviving kernels take the plain imu_update_kernel / pva_update_kernel names.
Dropping the finite-difference fallback left _acc_binding assigned in three places and read in none; read_into creates the binding lazily, so the binding_for call was redundant too. Also corrects documentation that still described the removed behavior: the OvPhysX Imu and Pva class notes advertised a numerical-differentiation fallback and a 200 Hz timestep recommendation, and BasePva told users accelerations may be differentiated from velocities depending on the backend. No backend does that any more -- Newton already read body_qdd, and PhysX and OvPhysX now read the solver.
|
run-ci |
|
run-ci |
|
run-ci |
|
Backported to |
…and OvPhysX IMU/PVA sensors (#7416) # Description Three related defects in the IMU and PVA sensors, across PhysX and OvPhysX. They were originally split across this PR and #7538 (now closed as superseded); they are combined here because they touch the same kernels and could not be reviewed or merged independently. ## 1. Accelerations were finite-differenced (#1294) Both sensors computed linear acceleration by finite-differencing body velocity between updates. This made the reading depend on the sensor update period, produced a zero or stale reading on the first update, and generated large spurious spikes whenever velocities were written directly (resets, teleports). **PhysX**: the sensors now read `RigidBodyView.get_accelerations()` and transport it to the sensor frame as `a_sensor = a_com + α×r + ω×(ω×r)`. The IMU adds the gravity bias; PVA does not, since PVA reports kinematic acceleration. **OvPhysX**: the sensors read accelerations through the `RIGID_BODY_ACCELERATION` tensor binding with the same transport math. An earlier revision of this PR guarded that behind `hasattr(TT, "RIGID_BODY_ACCELERATION")` and fell back to finite differencing, because the binding had not shipped yet. It has since shipped and the project pins `ovphysx==0.5.11` exactly, so the fallback was unreachable on every supported install and has been removed along with its duplicate kernels and previous-velocity state. This also aligns PhysX and OvPhysX with Newton, whose PVA already computed acceleration from `body_qdd` with exactly this transport. ### `get_accelerations()` accuracy Verified on Isaac Sim 6.0.1 with a constant applied force (expected `a = F/m`): | mass | solver acc | expected | rel. err | |---|---|---|---| | 1 kg | 0.00199992 | 0.002 | 0.004% | | 1 g | 2.00004 | 2.0 | 0.002% | | 1 µg | 2000.0 | 2000.0 | <0.001% | The historical small-mass inaccuracy that motivated the finite-difference implementation is no longer present. ## 2. `OvPhysxManager.get_gravity()` returned a stale value `set_gravity()` authored the new gravity into OvStage and applied it to the running simulation, but `get_gravity()` read `SimulationCfg.gravity`, which the setter never touched. Every call after a live update returned the construction-time value, silently and permanently: ``` get_gravity before set : (0.0, 0.0, -9.81) set_gravity called with: (0.0, 0.0, -3.72) get_gravity after set : (0.0, 0.0, -9.81) ``` The manager now tracks the applied gravity rather than writing back to `cfg.gravity`. Writing back would have been wrong: both `_call_physx` and `_call_ovphysx` in `randomize_physics_scene_gravity` resample from `env.sim.cfg.gravity` as the pristine base, so mutating it would make successive `"add"`/`"scale"` randomizations compound. ## 3. Sensors snapshotted gravity at initialization IMU and PVA read gravity once in `_initialize_impl` and baked it into a static buffer, so gravity randomized at runtime never reached the accelerometer bias or the projected gravity direction. This affected **both** backends and was independent of defect 2 — fixing `get_gravity()` alone would not have changed sensor behavior. Both sensors now re-read scene gravity on every update, skipping the work when it is unchanged. This matters more after change 1, not less: the IMU reading is now literally `solver_a + (−g)`, so a stale `g` enters as a clean additive bias. Randomizing to Mars gravity made a resting body read +9.81 instead of +3.72 — a systematic 6.09 m/s² offset on every sample. Gravity is scene-wide on both backends, so the bias and the gravity direction are passed to the kernels **by value** rather than as per-body buffers. The PhysX recorded launches re-bind the parameter with `set_param_by_name` when it changes, mirroring how `env_mask` is already handled. If per-env gravity ever reaches these backends these go back to arrays, as Newton already does. ## Breaking change The PhysX PVA sensor's `GRAVITY_VEC_W` proxy array becomes the internal `_gravity_vec_w` scene-wide vector, matching what the OvPhysX PVA sensor already called it. It was absent from the data class, the type stubs and the docs. Consumers wanting this quantity should read `pva_sensor.data.projected_gravity_b`. **The identically-named attribute on the asset data classes is untouched.** ## Known remaining gaps (not addressed here) - `GRAVITY_VEC_W` is still snapshotted at construction in the **asset** data classes (`rigid_object_data.py`, `rigid_object_collection_data.py`, `articulation_data.py`) for both backends. Newton fixed its three by binding `ProxyArray(model.gravity[:world_count])` zero-copy, which is not possible here: OvPhysX exposes no scene-gravity tensor binding, and PhysX exposes gravity only as a host-side `Float3`. A fix must use the same pull-based refresh used here, and first needs a decision on whether to converge PhysX/OV onto Newton's m/s² convention (they currently store a normalized direction), which would touch consumers in `isaaclab_experimental` and `isaaclab_tasks_experimental`. - Newton's `Imu` applies no gravity bias at all, unlike the PhysX and OvPhysX IMUs. This looks like a cross-backend behavioral gap rather than a staleness bug. Fixes #1294 ## Type of change - Bug fix (non-breaking change which fixes an issue) - 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 Not applicable — no visual change. ## Checklist Docker and GPU tests run on demand. Push the commits you want tested, then comment `run-ci` on the pull request. - [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 ## Testing All suites run locally, one file at a time: | Suite | Result | |---|---| | `isaaclab_physx` IMU | 11 passed | | `isaaclab_physx` PVA | 11 passed | | `isaaclab_physx` recorded-launch | 13 passed | | `isaaclab_ov` IMU (default / `-k cuda`) | 12 passed, 14 skipped / 12 passed | | `isaaclab_ov` PVA (default / `-k cuda`) | 13 passed, 15 skipped / 13 passed | | `isaaclab_ov` physics | 53 passed | Regression coverage, each verified to fail without its fix: - `test_set_gravity_writes_and_releases_ovstage_control_resources` — asserts `get_gravity()` round-trips after a successful `set_gravity()`, still reports the previously applied value when the OvStage write fails, and leaves `cfg.gravity` untouched as the randomization base. Without the fix: `assert (0.0, 0.0, -9.81) == approx((0.0, 0.0, -3.72))`, reproducing the reported symptom. - `test_sensor_tracks_runtime_gravity_changes` and `test_imu_replayed_launch_applies_new_gravity_bias` — a mid-run gravity change must reach the *replayed* recorded launch, with an explicit guard that the test is on the recorded path rather than the eager fallback. - `test_velocity_writes_do_not_produce_spurious_acceleration` (IMU and PVA) — velocity teleports must not produce an acceleration spike. Without the fix the PVA case fails with a `99.9998` difference, exactly the `0.1 / dt = 100 m/s²` finite-difference artifact. The OvPhysX IMU tests were updated where they encoded finite-difference artifacts as expected values — most notably `test_gravity_at_rest`, whose old scenario never had the ball at rest (it fell continuously while zero velocity was rewritten each step). It now applies a real support force so the body is genuinely at rest and the accelerometer reads the bias alone, as a real IMU on a table does. --------- Co-authored-by: Kelly Guo <kellyg@nvidia.com> (cherry picked from commit 2d88d27)
Description
Three related defects in the IMU and PVA sensors, across PhysX and OvPhysX. They were originally split across this PR and #7538 (now closed as superseded); they are combined here because they touch the same kernels and could not be reviewed or merged independently.
1. Accelerations were finite-differenced (#1294)
Both sensors computed linear acceleration by finite-differencing body velocity between updates. This made the reading depend on the sensor update period, produced a zero or stale reading on the first update, and generated large spurious spikes whenever velocities were written directly (resets, teleports).
PhysX: the sensors now read
RigidBodyView.get_accelerations()and transport it to the sensor frame asa_sensor = a_com + α×r + ω×(ω×r). The IMU adds the gravity bias; PVA does not, since PVA reports kinematic acceleration.OvPhysX: the sensors read accelerations through the
RIGID_BODY_ACCELERATIONtensor binding with the same transport math. An earlier revision of this PR guarded that behindhasattr(TT, "RIGID_BODY_ACCELERATION")and fell back to finite differencing, because the binding had not shipped yet. It has since shipped and the project pinsovphysx==0.5.11exactly, so the fallback was unreachable on every supported install and has been removed along with its duplicate kernels and previous-velocity state.This also aligns PhysX and OvPhysX with Newton, whose PVA already computed acceleration from
body_qddwith exactly this transport.get_accelerations()accuracyVerified on Isaac Sim 6.0.1 with a constant applied force (expected
a = F/m):The historical small-mass inaccuracy that motivated the finite-difference implementation is no longer present.
2.
OvPhysxManager.get_gravity()returned a stale valueset_gravity()authored the new gravity into OvStage and applied it to the running simulation, butget_gravity()readSimulationCfg.gravity, which the setter never touched. Every call after a live update returned the construction-time value, silently and permanently:The manager now tracks the applied gravity rather than writing back to
cfg.gravity. Writing back would have been wrong: both_call_physxand_call_ovphysxinrandomize_physics_scene_gravityresample fromenv.sim.cfg.gravityas the pristine base, so mutating it would make successive"add"/"scale"randomizations compound.3. Sensors snapshotted gravity at initialization
IMU and PVA read gravity once in
_initialize_impland baked it into a static buffer, so gravity randomized at runtime never reached the accelerometer bias or the projected gravity direction. This affected both backends and was independent of defect 2 — fixingget_gravity()alone would not have changed sensor behavior.Both sensors now re-read scene gravity on every update, skipping the work when it is unchanged. This matters more after change 1, not less: the IMU reading is now literally
solver_a + (−g), so a stalegenters as a clean additive bias. Randomizing to Mars gravity made a resting body read +9.81 instead of +3.72 — a systematic 6.09 m/s² offset on every sample.Gravity is scene-wide on both backends, so the bias and the gravity direction are passed to the kernels by value rather than as per-body buffers. The PhysX recorded launches re-bind the parameter with
set_param_by_namewhen it changes, mirroring howenv_maskis already handled. If per-env gravity ever reaches these backends these go back to arrays, as Newton already does.Breaking change
The PhysX PVA sensor's
GRAVITY_VEC_Wproxy array becomes the internal_gravity_vec_wscene-wide vector, matching what the OvPhysX PVA sensor already called it. It was absent from the data class, the type stubs and the docs. Consumers wanting this quantity should readpva_sensor.data.projected_gravity_b. The identically-named attribute on the asset data classes is untouched.Known remaining gaps (not addressed here)
GRAVITY_VEC_Wis still snapshotted at construction in the asset data classes (rigid_object_data.py,rigid_object_collection_data.py,articulation_data.py) for both backends. Newton fixed its three by bindingProxyArray(model.gravity[:world_count])zero-copy, which is not possible here: OvPhysX exposes no scene-gravity tensor binding, and PhysX exposes gravity only as a host-sideFloat3. A fix must use the same pull-based refresh used here, and first needs a decision on whether to converge PhysX/OV onto Newton's m/s² convention (they currently store a normalized direction), which would touch consumers inisaaclab_experimentalandisaaclab_tasks_experimental.Imuapplies no gravity bias at all, unlike the PhysX and OvPhysX IMUs. This looks like a cross-backend behavioral gap rather than a staleness bug.Fixes #1294
Type of change
Release backport
developScreenshots
Not applicable — no visual change.
Checklist
Docker and GPU tests run on demand. Push the commits you want tested, then
comment
run-cion the pull request.pre-commitchecks with./isaaclab.sh --formatsource/<pkg>/changelog.d/for every touched package (do not editCHANGELOG.rstor bumpextension.toml— CI handles that)CONTRIBUTORS.mdor my name already exists thereTesting
All suites run locally, one file at a time:
isaaclab_physxIMUisaaclab_physxPVAisaaclab_physxrecorded-launchisaaclab_ovIMU (default /-k cuda)isaaclab_ovPVA (default /-k cuda)isaaclab_ovphysicsRegression coverage, each verified to fail without its fix:
test_set_gravity_writes_and_releases_ovstage_control_resources— assertsget_gravity()round-trips after a successfulset_gravity(), still reports the previously applied value when the OvStage write fails, and leavescfg.gravityuntouched as the randomization base. Without the fix:assert (0.0, 0.0, -9.81) == approx((0.0, 0.0, -3.72)), reproducing the reported symptom.test_sensor_tracks_runtime_gravity_changesandtest_imu_replayed_launch_applies_new_gravity_bias— a mid-run gravity change must reach the replayed recorded launch, with an explicit guard that the test is on the recorded path rather than the eager fallback.test_velocity_writes_do_not_produce_spurious_acceleration(IMU and PVA) — velocity teleports must not produce an acceleration spike. Without the fix the PVA case fails with a99.9998difference, exactly the0.1 / dt = 100 m/s²finite-difference artifact.The OvPhysX IMU tests were updated where they encoded finite-difference artifacts as expected values — most notably
test_gravity_at_rest, whose old scenario never had the ball at rest (it fell continuously while zero velocity was rewritten each step). It now applies a real support force so the body is genuinely at rest and the accelerometer reads the bias alone, as a real IMU on a table does.