Skip to content

Commit 18f1d8b

Browse files
authored
Fix translated floating-base IK Jacobians (#4088)
1 parent d89463b commit 18f1d8b

5 files changed

Lines changed: 99 additions & 39 deletions

File tree

changelog/3866.fixed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix distance-dependent drift in analytic IK Jacobians for FREE and DISTANCE joints.

newton/_src/sim/articulation.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -934,25 +934,23 @@ def eval_ik(
934934
@wp.func
935935
def write_free_distance_motion_subspace(
936936
X_pa_world: wp.transform,
937-
x_child_com_world: wp.vec3,
937+
pivot_world: wp.vec3,
938938
qd_start: int,
939939
# outputs
940940
joint_S_s: wp.array[wp.spatial_vector],
941941
):
942942
"""Write the 6 motion-subspace columns for a FREE/DISTANCE joint.
943943
944-
Used by both the Featherstone inverse-dynamics path (``jcalc_motion``) and
945-
the IK/Jacobian path (``jcalc_motion_subspace``) so they agree on the exact
946-
column layout. Linear DOFs act at the child body's COM; angular DOFs are
947-
world-aligned axes expressed through ``X_pa_world``.
944+
Linear DOFs follow the parent-anchor axes. Angular columns describe
945+
rotations about ``pivot_world`` as world-origin spatial twists.
948946
949947
Args:
950948
X_pa_world: Parent-anchor world transform (``X_wp * joint_X_p``) used
951949
to rotate the joint's parent-anchor axes into the world frame.
952950
This is *not* the classical Featherstone ``X_sc`` (spatial-to-
953951
child); Newton's FREE/DISTANCE joint coordinates live in the
954952
parent-anchor basis.
955-
x_child_com_world: World-space position of the child body's COM.
953+
pivot_world: World-space point about which angular columns rotate.
956954
qd_start: Starting velocity-DOF index for this joint.
957955
joint_S_s: Output spatial-vector subspace array; six slots starting at
958956
``qd_start`` are overwritten.
@@ -964,9 +962,9 @@ def write_free_distance_motion_subspace(
964962
joint_S_s[qd_start + 0] = wp.spatial_vector(axis_world_x, wp.vec3())
965963
joint_S_s[qd_start + 1] = wp.spatial_vector(axis_world_y, wp.vec3())
966964
joint_S_s[qd_start + 2] = wp.spatial_vector(axis_world_z, wp.vec3())
967-
joint_S_s[qd_start + 3] = wp.spatial_vector(-wp.cross(axis_world_x, x_child_com_world), axis_world_x)
968-
joint_S_s[qd_start + 4] = wp.spatial_vector(-wp.cross(axis_world_y, x_child_com_world), axis_world_y)
969-
joint_S_s[qd_start + 5] = wp.spatial_vector(-wp.cross(axis_world_z, x_child_com_world), axis_world_z)
965+
joint_S_s[qd_start + 3] = wp.spatial_vector(-wp.cross(axis_world_x, pivot_world), axis_world_x)
966+
joint_S_s[qd_start + 4] = wp.spatial_vector(-wp.cross(axis_world_y, pivot_world), axis_world_y)
967+
joint_S_s[qd_start + 5] = wp.spatial_vector(-wp.cross(axis_world_z, pivot_world), axis_world_z)
970968

971969

972970
@wp.func

newton/_src/sim/ik/ik_lbfgs_optimizer.py

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -854,14 +854,12 @@ def _compute_motion_subspace(
854854
inputs=[
855855
self.model.joint_type,
856856
self.model.joint_parent,
857-
self.model.joint_child,
858857
self.model.joint_q_start,
859858
self.model.joint_qd_start,
860859
joint_q_in,
861860
self.model.joint_axis,
862861
self.model.joint_dof_dim,
863862
body_q,
864-
self.model.body_com,
865863
self.model.joint_X_p,
866864
],
867865
outputs=[
@@ -1348,7 +1346,10 @@ def _select_best_step_template(
13481346
_select_best_step_tiled = wp.kernel(enable_backward=False, module="unique")(_select_best_step_template)
13491347

13501348
# late-import jcalc_* helpers to avoid circular import error
1351-
from ...sim.articulation import jcalc_motion_subspace # noqa: PLC0415
1349+
from ...sim.articulation import ( # noqa: PLC0415
1350+
jcalc_motion_subspace,
1351+
write_free_distance_motion_subspace,
1352+
)
13521353
from ...solvers.featherstone.kernels import ( # noqa: PLC0415
13531354
jcalc_integrate,
13541355
jcalc_transform,
@@ -1427,14 +1428,12 @@ def _integrate_dq_dof(
14271428
def _compute_motion_subspace_2d(
14281429
joint_type: wp.array[wp.int32], # (n_joints)
14291430
joint_parent: wp.array[wp.int32], # (n_joints)
1430-
joint_child: wp.array[wp.int32], # (n_joints)
14311431
joint_q_start: wp.array[wp.int32], # (n_joints + 1)
14321432
joint_qd_start: wp.array[wp.int32], # (n_joints + 1)
14331433
joint_q: wp.array2d[wp.float32], # (n_batch, n_coords)
14341434
joint_axis: wp.array[wp.vec3], # (n_joint_dof_count)
14351435
joint_dof_dim: wp.array2d[wp.int32], # (n_joints, 2)
14361436
body_q: wp.array2d[wp.transform], # (n_batch, n_bodies)
1437-
body_com: wp.array[wp.vec3], # (n_bodies)
14381437
joint_X_p: wp.array[wp.transform], # (n_joints)
14391438
# outputs
14401439
joint_S_s: wp.array2d[wp.spatial_vector], # (n_batch, n_joint_dof_count)
@@ -1443,7 +1442,6 @@ def _compute_motion_subspace_2d(
14431442

14441443
type = joint_type[joint_idx]
14451444
parent = joint_parent[joint_idx]
1446-
child = joint_child[joint_idx]
14471445
q_start = joint_q_start[joint_idx]
14481446
qd_start = joint_qd_start[joint_idx]
14491447

@@ -1459,16 +1457,10 @@ def _compute_motion_subspace_2d(
14591457
S_s_out = joint_S_s[row]
14601458

14611459
if type == JointType.FREE or type == JointType.DISTANCE:
1462-
jcalc_motion_subspace(
1463-
type,
1464-
joint_axis,
1465-
joint_q_1d,
1466-
lin_axis_count,
1467-
ang_axis_count,
1460+
# IK integration applies angular increments about the fixed parent anchor.
1461+
write_free_distance_motion_subspace(
14681462
X_wpj,
1469-
body_q[row, child],
1470-
body_com[child],
1471-
q_start,
1463+
wp.transform_get_translation(X_wpj),
14721464
qd_start,
14731465
S_s_out,
14741466
)

newton/_src/sim/ik/ik_lm_optimizer.py

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -568,14 +568,12 @@ def _compute_motion_subspace(
568568
inputs=[
569569
self.model.joint_type,
570570
self.model.joint_parent,
571-
self.model.joint_child,
572571
self.model.joint_q_start,
573572
self.model.joint_qd_start,
574573
joint_q_in,
575574
self.model.joint_axis,
576575
self.model.joint_dof_dim,
577576
body_q,
578-
self.model.body_com,
579577
self.model.joint_X_p,
580578
],
581579
outputs=[
@@ -790,7 +788,10 @@ def _template(
790788
_lm_solve_tiled = wp.kernel(enable_backward=False, module="unique")(_template)
791789

792790
# late-import jcalc_* helpers to avoid circular import error
793-
from ...sim.articulation import jcalc_motion_subspace # noqa: PLC0415
791+
from ...sim.articulation import ( # noqa: PLC0415
792+
jcalc_motion_subspace,
793+
write_free_distance_motion_subspace,
794+
)
794795
from ...solvers.featherstone.kernels import ( # noqa: PLC0415
795796
jcalc_integrate,
796797
jcalc_transform,
@@ -869,14 +870,12 @@ def _integrate_dq_dof(
869870
def _compute_motion_subspace_2d(
870871
joint_type: wp.array[wp.int32], # (n_joints)
871872
joint_parent: wp.array[wp.int32], # (n_joints)
872-
joint_child: wp.array[wp.int32], # (n_joints)
873873
joint_q_start: wp.array[wp.int32], # (n_joints + 1)
874874
joint_qd_start: wp.array[wp.int32], # (n_joints + 1)
875875
joint_q: wp.array2d[wp.float32], # (n_batch, n_coords)
876876
joint_axis: wp.array[wp.vec3], # (n_joint_dof_count)
877877
joint_dof_dim: wp.array2d[wp.int32], # (n_joints, 2)
878878
body_q: wp.array2d[wp.transform], # (n_batch, n_bodies)
879-
body_com: wp.array[wp.vec3], # (n_bodies)
880879
joint_X_p: wp.array[wp.transform], # (n_joints)
881880
# outputs
882881
joint_S_s: wp.array2d[wp.spatial_vector], # (n_batch, n_joint_dof_count)
@@ -885,7 +884,6 @@ def _compute_motion_subspace_2d(
885884

886885
type = joint_type[joint_idx]
887886
parent = joint_parent[joint_idx]
888-
child = joint_child[joint_idx]
889887
q_start = joint_q_start[joint_idx]
890888
qd_start = joint_qd_start[joint_idx]
891889

@@ -901,16 +899,10 @@ def _compute_motion_subspace_2d(
901899
S_s_out = joint_S_s[row]
902900

903901
if type == JointType.FREE or type == JointType.DISTANCE:
904-
jcalc_motion_subspace(
905-
type,
906-
joint_axis,
907-
joint_q_1d,
908-
lin_axis_count,
909-
ang_axis_count,
902+
# IK integration applies angular increments about the fixed parent anchor.
903+
write_free_distance_motion_subspace(
910904
X_wpj,
911-
body_q[row, child],
912-
body_com[child],
913-
q_start,
905+
wp.transform_get_translation(X_wpj),
914906
qd_start,
915907
S_s_out,
916908
)

newton/tests/test_ik.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,25 @@ def _build_descendant_free_distance(device, joint_type) -> tuple[newton.Model, i
163163
return builder.finalize(device=device, requires_grad=True), child
164164

165165

166+
def _build_root_free_distance(device, joint_type) -> tuple[newton.Model, int]:
167+
builder = newton.ModelBuilder()
168+
body = builder.add_link(mass=1.0, inertia=wp.mat33(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0))
169+
builder.body_com[body] = wp.vec3(0.2, -0.1, 0.3)
170+
joint = _add_free_distance_joint(
171+
builder=builder,
172+
joint_type=joint_type,
173+
parent=-1,
174+
child=body,
175+
parent_xform=wp.transform(
176+
wp.vec3(0.3, -0.4, 0.6),
177+
wp.quat_from_axis_angle(wp.normalize(wp.vec3(0.2, 1.0, -0.3)), 0.7),
178+
),
179+
child_xform=wp.transform_identity(),
180+
)
181+
builder.add_articulation([joint])
182+
return builder.finalize(device=device, requires_grad=True), body
183+
184+
166185
# ----------------------------------------------------------------------------
167186
# helpers - D6
168187
# ----------------------------------------------------------------------------
@@ -711,6 +730,54 @@ def test_d6_jacobian_compare(test, device):
711730
_jacobian_compare(test, device, _d6_objective_builder)
712731

713732

733+
def test_free_distance_translated_jacobian_compare(test, device, joint_type, optimizer):
734+
"""Match analytic and autodiff Jacobians for a translated floating body."""
735+
with wp.ScopedDevice(device):
736+
translations = (0.0, 1.0, 5.0, 20.0)
737+
model, body = _build_root_free_distance(device, joint_type)
738+
joint_q = np.zeros((len(translations), model.joint_coord_count), dtype=np.float32)
739+
joint_q[:, 0] = translations
740+
joint_q[:, 6] = 1.0
741+
target_positions = wp.zeros(len(translations), dtype=wp.vec3, device=device)
742+
743+
solver_auto = ik.IKSolver(
744+
model,
745+
len(translations),
746+
[ik.IKObjectivePosition(body, wp.vec3(), target_positions)],
747+
optimizer=optimizer,
748+
jacobian_mode=ik.IKJacobianType.AUTODIFF,
749+
)
750+
solver_ana = ik.IKSolver(
751+
model,
752+
len(translations),
753+
[ik.IKObjectivePosition(body, wp.vec3(), target_positions)],
754+
optimizer=optimizer,
755+
jacobian_mode=ik.IKJacobianType.ANALYTIC,
756+
)
757+
q_auto = wp.array(joint_q, device=device, requires_grad=True)
758+
q_ana = wp.array(joint_q, device=device)
759+
760+
solver_auto._impl._compute_residuals(q_auto)
761+
solver_ana._impl._compute_residuals(q_ana)
762+
if optimizer == ik.IKOptimizer.LM:
763+
jacobian_auto = solver_auto._impl._jacobian_at(solver_auto._impl._ctx_solver(q_auto)).numpy()
764+
jacobian_ana = solver_ana._impl._jacobian_at(solver_ana._impl._ctx_solver(q_ana)).numpy()
765+
for problem_idx, translation in enumerate(translations):
766+
with test.subTest(translation=translation):
767+
assert_np_equal(jacobian_auto[problem_idx], jacobian_ana[problem_idx], tol=1e-4)
768+
else:
769+
gradient_auto = wp.zeros((len(translations), model.joint_dof_count), dtype=wp.float32, device=device)
770+
gradient_ana = wp.zeros_like(gradient_auto)
771+
solver_auto._impl._gradient_at(solver_auto._impl._ctx_solver(q_auto), gradient_auto)
772+
solver_ana._impl._gradient_at(solver_ana._impl._ctx_solver(q_ana), gradient_ana)
773+
assert_np_equal(gradient_auto.numpy(), gradient_ana.numpy(), tol=1e-4)
774+
775+
motion_subspace = solver_ana._impl.joint_S_s.numpy()
776+
for problem_idx, translation in enumerate(translations[1:], start=1):
777+
with test.subTest(motion_subspace_translation=translation):
778+
assert_np_equal(motion_subspace[0], motion_subspace[problem_idx], tol=1e-6)
779+
780+
714781
# ----------------------------------------------------------------------------
715782
# 3. Test-class registration per device
716783
# ----------------------------------------------------------------------------
@@ -761,6 +828,16 @@ class TestIKModes(unittest.TestCase):
761828
add_function_test(TestIKModes, "test_rotation_jacobian_compare", test_rotation_jacobian_compare, cuda_devices)
762829
add_function_test(TestIKModes, "test_joint_limit_jacobian_compare", test_joint_limit_jacobian_compare, devices)
763830
add_function_test(TestIKModes, "test_d6_jacobian_compare", test_d6_jacobian_compare, cuda_devices)
831+
for optimizer, optimizer_name in ((ik.IKOptimizer.LM, "lm"), (ik.IKOptimizer.LBFGS, "lbfgs")):
832+
for joint_type in (newton.JointType.FREE, newton.JointType.DISTANCE):
833+
add_function_test(
834+
TestIKModes,
835+
f"test_translated_{_joint_type_name(joint_type)}_{optimizer_name}_jacobian_compare",
836+
test_free_distance_translated_jacobian_compare,
837+
devices,
838+
joint_type=joint_type,
839+
optimizer=optimizer,
840+
)
764841

765842

766843
if __name__ == "__main__":

0 commit comments

Comments
 (0)