Move external wrench frame selection into WrenchComposer - #7536
Move external wrench frame selection into WrenchComposer#7536AntoineRichard wants to merge 8 commits into
Conversation
Strengthen the resolve_submission composer tests to assert composed values instead of just the frame, tautology-proof the Newton frame pin, cover the OvPhysX ordered-world articulation path under a nonidentity body ordering, align the PhysX benchmark fixture with production composer construction, and drop a dead field setup left over from a deleted RigidObject attribute.
|
@NeoZng could you see if this works for you. The bulk of the changes are tests. |
|
Thanks @AntoineRichard — I tested the current patch cleanly on the latest Our reset-mode MDP event writes a persistent On Isaac Sim 6.0.1 with an RTX 5090, 388 WrenchComposer tests and 14 focused PhysX/Newton interface tests passed. A learner-free profile of the actual MDP term and full
The patch is correct and ready for our use case. |
Description
WrenchComposer.compose_to_body_frame()was called unconditionally by every backend writer on every physics step. It readsbody_com_pos_wandbody_link_quat_w, which on PhysX chains to_ensure_fk_fresh()→update_articulations_kinematic()(a scene-global articulation FK pass) plusroot_view.get_link_transforms(), then launches the composition kernel.That work is unnecessary whenever the buffered wrench is already in a frame the consumer accepts. This PR moves the frame decision into the composer, which hands each writer the cheapest valid representation:
Writers declare what they can consume once at construction (
supports_world_at_com) and callresolve_submission()per step. All frame logic stays inside the composer; no writer touches a raw buffer or knows a frame rule.New public API
Additive only.
compose_to_body_frame(),out_force_b, andout_torque_bremain public and behaviorally unchanged, so no deprecation cycle is needed.supports_world_at_comdefaults toFalse, which is exactly the previous behavior.Eligibility is tracked with a private mask of which input buffer families hold contributions (
LOCAL,GLOBAL_AT_COM,GLOBAL_POSITIONED), classified from the same routing theadd_forces_to_dual_buffers_*kernels use. The mask is sticky across partial resets and cleared only by a fullreset()— proving a partial reset removed every contribution of a family would require scanning the buffers. That can forgo the fast path but never changes the resulting wrench.Equivalence
All three branches are exact, from
compose_wrench_to_body_frame:quat_rotate_invterms vanish and the output is identically(local_force_b, local_torque_b). Note a positioned local force also qualifies — the kernel foldscross(P_b, F_b)straight intolocal_torque_b.global_force_wand the local buffers are zero, socorrected_torque_w == global_torque_w. Submitting the world buffers is the same wrench, provided the consumer applies force at the CoM — verified against the vendoredomni.physics.tensorsAPI, whereposition_data=Nonemeans "at the link transform" for bothis_globalvalues, so the flag only reinterprets the vectors' frame and never moves the application point.Backend coverage
supports_world_at_comTrueis_globalTruewrench_is_worldkernel flag that skips the inline rotateFalse(default)resolve_submission(); no constructor changeOvPhysX is the largest win. Its wrench binding wants a world-frame wrench, so a global wrench previously round-tripped: the composer rotated world→body, then the packing kernel rotated body→world. Measured on this branch with a body rotated 90° about +z, a local force
(1,0,0)emits(0,1,0)while a global force(1,0,0)emits(1,0,0)— an exact round trip, so skipping both rotations is output-preserving. The packed[6:9]link position is still written unconditionally on both paths.Newton keeps the body frame because it binds a body-frame array to the solver. Worth noting for a follow-up: its writer kernel already performs the same body→world rotation OvPhysX's does, so Newton could plausibly take the same flag and win the global-at-CoM path too. That is deliberately out of scope here.
Validation
Environment:
OMNI_KIT_ACCEPT_EULA=YES uv run --extra isaacsim --extra test --extra ovphysx, which resolvesBACKENDS == ['physx', 'newton', 'ovphysx']with CUDA. NoteEXP_PATHmust be exported or_iface_test_boot.pytakes its kitless path andphysxsilently drops out, making backend tests skip green.test_wrench_composer.pyplustest/assets/: 6760 passed, 65 skipped, 68 xfailed.test_articulation_ordering.pyduring full-suite runs are pre-existing and unrelated: the identical 5 fail at the branch base with none of this branch present, and pass in isolation. Test counts reconcile — 6350 passing at base, 6370 at head for the same selection, and the +20 is exactly this branch's new tests.uv run isaaclab -fclean.New coverage: composer unit tests for every content case and the mask lifecycle; per-backend submission tests asserting the chosen frame and that no body-pose read occurs on a fast path; an OvPhysX articulation test driving the ordered kernel's world path under a non-identity body ordering; and a cross-backend equivalence test over all three backends × four content combinations, comparing the submitted wrench against the composed one.
Two regressions were confirmed to be caught rather than assumed: injecting the eligibility bug the design guards against (
_content & LOCALinstead of==) fails the mixed-content test, and perturbing the expected world-frame torque fails exactly the twoWORLD_AT_COMcases and no others.Performance
Real PhysX, RTX 5090, ANYMAL-C (17 bodies) × 4096 envs = 69,632 links. Same-process paired A/B with the arm order alternated per iteration, 400–500 pairs, 95% CI on paired differences. Baseline is
compose_to_body_frame()— what the writers previously called unconditionally.write_data_to_simwrite_data_to_simRead the end-to-end row as the honest headline: ~0.08 ms per asset per physics step. Roughly half the isolated saving does not reach the caller, because other work in
write_data_to_simtouches body poses anyway and pays part of that cost regardless. A cuboid rigid-object scene at the same env count saved only ~6.5 µs, as expected — there is no articulation FK pass to skip.Scope caveat: no in-tree task sets
is_global=Truetoday (both shippedapply_external_force_torqueevents use the local default), so the local path is what delivers value now, and it is the one that reaches every backend.Supersedes
This replaces two open PRs, both of which found one half of this problem:
Type of change
Checklist
pre-commitchecks with./isaaclab.sh --formatsource/<pkg>/changelog.d/for every touched packageCONTRIBUTORS.mdor my name already exists there🤖 Generated with Claude Code