Skip to content

Commit 535b626

Browse files
authored
[Kamino] Fix passive universal joint FK constraint (#3720)
1 parent ad38ac8 commit 535b626

3 files changed

Lines changed: 89 additions & 6 deletions

File tree

newton/_src/solvers/kamino/_src/solvers/fk/kernels.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -193,11 +193,15 @@ def _eval_rotation_jacobian_blocks(
193193

194194
@wp.func
195195
def _eval_passive_universal_jacobian_blocks(
196-
X_T: wp.mat33f, q_base: wp.quatf, q_follower: wp.quatf, has_base: wp.bool
196+
X_B_T: wp.mat33f,
197+
X_F_T: wp.mat33f,
198+
q_base: wp.quatf,
199+
q_follower: wp.quatf,
200+
has_base: wp.bool,
197201
) -> tuple[wp.vec4f, wp.vec4f]:
198202
"""Evaluate the base and follower blocks of a passive universal constraint Jacobian."""
199-
a_x = X_T[0]
200-
a_y = X_T[1]
203+
a_x = X_B_T[0] # x-axis on base
204+
a_y = X_F_T[1] # y-axis on follower
201205
jac_q_base = wp.vec4f(0.0)
202206
if has_base:
203207
a_y_follower = unit_quat_apply(q_follower, a_y)
@@ -970,6 +974,7 @@ def _eval_joint_constraints(
970974
joints_bid_B: wp.array[wp.int32],
971975
joints_bid_F: wp.array[wp.int32],
972976
joints_X_Bj: wp.array[wp.mat33f],
977+
joints_X_Fj: wp.array[wp.mat33f],
973978
joints_B_r_B: wp.array[wp.vec3f],
974979
joints_F_r_F: wp.array[wp.vec3f],
975980
bodies_q: wp.array[wp.transformf],
@@ -996,6 +1001,7 @@ def _eval_joint_constraints(
9961001
joints_bid_B: Joint base body id
9971002
joints_bid_F: Joint follower body id
9981003
joints_X_Bj: Joint local frame on base body
1004+
joints_X_Fj: Joint local frame on follower body
9991005
joints_B_r_B: Joint local position on base body
10001006
joints_F_r_F: Joint local position on follower body
10011007
bodies_q: Body poses
@@ -1059,7 +1065,7 @@ def _eval_joint_constraints(
10591065

10601066
# Compute constraint (dot product between x axis on base and y axis on follower)
10611067
a_x = X_T[0]
1062-
a_y = X_T[1]
1068+
a_y = wp.transpose(joints_X_Fj[jt_id_tot])[1]
10631069
a_x_base = unit_quat_apply(q_base, a_x)
10641070
a_y_follower = unit_quat_apply(q_follower, a_y)
10651071
ct = -wp.dot(a_x_base, a_y_follower)
@@ -1168,6 +1174,7 @@ def _eval_joint_constraints_jacobian(
11681174
joints_bid_B: wp.array[wp.int32],
11691175
joints_bid_F: wp.array[wp.int32],
11701176
joints_X_Bj: wp.array[wp.mat33f],
1177+
joints_X_Fj: wp.array[wp.mat33f],
11711178
joints_B_r_B: wp.array[wp.vec3f],
11721179
joints_F_r_F: wp.array[wp.vec3f],
11731180
bodies_q: wp.array[wp.transformf],
@@ -1191,6 +1198,7 @@ def _eval_joint_constraints_jacobian(
11911198
joints_bid_B: Joint base body id
11921199
joints_bid_F: Joint follower body id
11931200
joints_X_Bj: Joint local frame on base body
1201+
joints_X_Fj: Joint local frame on follower body
11941202
joints_B_r_B: Joint local position on base body
11951203
joints_F_r_F: Joint local position on follower body
11961204
bodies_q: Body poses
@@ -1272,7 +1280,7 @@ def _eval_joint_constraints_jacobian(
12721280

12731281
# Compute constraint Jacobian (cross product between x axis on base and y axis on follower)
12741282
jac_q_base, jac_q_follower = _eval_passive_universal_jacobian_blocks(
1275-
X_T, q_base, q_follower, base_id_tot >= 0
1283+
X_T, wp.transpose(joints_X_Fj[jt_id_tot]), q_base, q_follower, base_id_tot >= 0
12761284
)
12771285

12781286
# Write out Jacobian
@@ -1304,6 +1312,7 @@ def _eval_joint_constraints_sparse_jacobian(
13041312
joints_bid_B: wp.array[wp.int32],
13051313
joints_bid_F: wp.array[wp.int32],
13061314
joints_X_Bj: wp.array[wp.mat33f],
1315+
joints_X_Fj: wp.array[wp.mat33f],
13071316
joints_B_r_B: wp.array[wp.vec3f],
13081317
joints_F_r_F: wp.array[wp.vec3f],
13091318
bodies_q: wp.array[wp.transformf],
@@ -1328,6 +1337,7 @@ def _eval_joint_constraints_sparse_jacobian(
13281337
joints_bid_B: Joint base body id
13291338
joints_bid_F: Joint follower body id
13301339
joints_X_Bj: Joint local frame on base body
1340+
joints_X_Fj: Joint local frame on follower body
13311341
joints_B_r_B: Joint local position on base body
13321342
joints_F_r_F: Joint local position on follower body
13331343
bodies_q: Body poses
@@ -1413,7 +1423,7 @@ def _eval_joint_constraints_sparse_jacobian(
14131423

14141424
# Compute constraint Jacobian (cross product between x axis on base and y axis on follower)
14151425
jac_q_base, jac_q_follower = _eval_passive_universal_jacobian_blocks(
1416-
X_T, q_base, q_follower, base_id >= 0
1426+
X_T, wp.transpose(joints_X_Fj[jt_id_tot]), q_base, q_follower, base_id >= 0
14171427
)
14181428

14191429
# Write out Jacobian

newton/_src/solvers/kamino/_src/solvers/fk/solver.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1269,6 +1269,7 @@ def _eval_kinematic_constraints(
12691269
self.joints_bid_B,
12701270
self.joints_bid_F,
12711271
self.joints_X_Bj,
1272+
self.joints_X_Fj,
12721273
self.joints_B_r_Bj,
12731274
self.joints_F_r_Fj,
12741275
bodies_q,
@@ -1344,6 +1345,7 @@ def _eval_kinematic_constraints_jacobian(
13441345
self.joints_bid_B,
13451346
self.joints_bid_F,
13461347
self.joints_X_Bj,
1348+
self.joints_X_Fj,
13471349
self.joints_B_r_Bj,
13481350
self.joints_F_r_Fj,
13491351
bodies_q,
@@ -1396,6 +1398,7 @@ def _assemble_sparse_jacobian(
13961398
self.joints_bid_B,
13971399
self.joints_bid_F,
13981400
self.joints_X_Bj,
1401+
self.joints_X_Fj,
13991402
self.joints_B_r_Bj,
14001403
self.joints_F_r_Fj,
14011404
bodies_q,

newton/_src/solvers/kamino/tests/test_solvers_forward_kinematics.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
sample_body_poses,
3636
)
3737
from newton.tests.utils.basics import build_cartpole
38+
from newton.tests.utils.testing import build_unary_universal_joint_test
3839

3940
###
4041
# Module configs
@@ -130,6 +131,75 @@ def eval_constraints(bodies_q_stepped_np):
130131
self.assertTrue(success)
131132

132133

134+
class PassiveUniversalJointFrameForwardKinematics(unittest.TestCase):
135+
def setUp(self):
136+
if not test_context.setup_done:
137+
setup_tests(clear_cache=False)
138+
self.default_device = wp.get_device(test_context.device)
139+
140+
def tearDown(self):
141+
self.default_device = None
142+
143+
def test_follower_joint_frame(self):
144+
"""Test kinematic constraints and jacobian for a passive universal joint.
145+
146+
The follower joint frame is rotated relative to the base joint frame,
147+
while the follower body is counter-rotated so both joint frames coincide
148+
in world coordinates. The base-frame X axis and follower-frame Y axis
149+
are therefore orthogonal, as required by a universal joint.
150+
"""
151+
# Build a single body attached to the world and make both rotational
152+
# degrees of freedom passive.
153+
builder = build_unary_universal_joint_test(limits=True, ground=False)
154+
builder.joint_target_mode[0] = newton.JointTargetMode.NONE
155+
builder.joint_target_mode[1] = newton.JointTargetMode.NONE
156+
model = ModelKamino.from_newton(builder.finalize(device=self.default_device))
157+
158+
# Rotate the follower-local joint frame by 90 degrees about Z without
159+
# changing the base-local joint frame.
160+
q_X_F = wp.quat_from_axis_angle(wp.vec3f(0.0, 0.0, 1.0), 0.5 * wp.pi)
161+
X_Fj = model.joints.X_Fj.numpy()
162+
X_Fj[0] = np.asarray(wp.quat_to_matrix(q_X_F), dtype=np.float32).reshape(3, 3)
163+
model.joints.X_Fj.assign(X_Fj)
164+
165+
# Counter-rotate and translate the follower body so its joint frame has
166+
# the same world-space pose as the base joint frame.
167+
q_F = wp.quat_inverse(q_X_F)
168+
B_r_Bj = wp.vec3f(model.joints.B_r_Bj.numpy()[0])
169+
F_r_Fj = wp.vec3f(model.joints.F_r_Fj.numpy()[0])
170+
r_F = B_r_Bj - wp.quat_rotate(q_F, F_r_Fj)
171+
bodies_q = wp.array([wp.transformf(r_F, q_F)], dtype=wp.transformf, device=self.default_device)
172+
173+
# Coincident joint frames satisfy all three anchor constraints and the
174+
# universal joint's rotational orthogonality constraint.
175+
solver = ForwardKinematicsSolver(model)
176+
actuators_q = wp.empty(0, dtype=wp.float32, device=self.default_device)
177+
target_transforms = solver.eval_position_control_transformations(actuators_q, None)
178+
constraints = solver.eval_kinematic_constraints(bodies_q, target_transforms).numpy()[0]
179+
np.testing.assert_allclose(constraints, 0.0, atol=1.0e-6)
180+
181+
# Validate jacobian with finite differences
182+
bodies_q_np = bodies_q.numpy().reshape(-1)
183+
jacobian = solver.eval_kinematic_constraints_jacobian(bodies_q, target_transforms).numpy()[0]
184+
185+
def eval_constraints(bodies_q_stepped_np):
186+
bodies_q.assign(bodies_q_stepped_np)
187+
stepped_constraints = solver.eval_kinematic_constraints(bodies_q, target_transforms).numpy()[0]
188+
bodies_q.assign(bodies_q_np)
189+
return stepped_constraints
190+
191+
self.assertTrue(
192+
diff_check(
193+
eval_constraints,
194+
jacobian,
195+
bodies_q_np,
196+
epsilon=1.0e-4,
197+
tolerance_abs=5.0e-3,
198+
tolerance_rel=5.0e-3,
199+
)
200+
)
201+
202+
133203
class SparseJacobianSingleJointCheckForwardKinematics(unittest.TestCase):
134204
def setUp(self):
135205
if not test_context.setup_done:

0 commit comments

Comments
 (0)