@@ -365,6 +365,68 @@ TEST_F(QuaternionFloatingMobilizerTest, MapUsesNplus) {
365
365
MatrixCompareType::relative));
366
366
}
367
367
368
+ TEST_F (QuaternionFloatingMobilizerTest, MapAccelerationToQDDotAndViceVersa) {
369
+ // Set an arbitrary non-zero state.
370
+ const math::RollPitchYaw<double > rpy (M_PI / 3 , -M_PI / 4 , M_PI / 5 );
371
+ const Quaternion<double > q_FM = rpy.ToQuaternion ();
372
+ const Vector3<double > wxyz (5.4 , -9.8 , 3.2 );
373
+ const Vector3<double > vxyz (0.2 , 0.5 , -0.87 );
374
+ const Vector6<double > v (wxyz[0 ], wxyz[1 ], wxyz[2 ], vxyz[0 ], vxyz[1 ], vxyz[2 ]);
375
+ mobilizer_->SetQuaternion (context_.get (), q_FM);
376
+ mobilizer_->SetAngularVelocity (context_.get (), wxyz);
377
+ mobilizer_->SetTranslationalVelocity (context_.get (), vxyz);
378
+
379
+ // Set an arbitrary v̇ and use MapAccelerationToQDDot() to calculate q̈.
380
+ // Reminder: v̇ = [ẇx, ẇy, ẇz, v̇x, v̇y, v̇z]ᵀ
381
+ const Vector6<double > vdot (0.3 , -0.2 , 0.9 , -1.2 , 3.1 , -2.3 );
382
+ Eigen::Matrix<double , 7 , 1 > qddot;
383
+ mobilizer_->MapAccelerationToQDDot (*context_, vdot, &qddot);
384
+
385
+ // Starting with the previous q̈, use MapQDDotToAcceleration() to calculate v̇.
386
+ // Verify MapQDDotToAcceleration() is the inverse of MapAccelerationToQDDot().
387
+ Vector6<double > vdot_redo;
388
+ mobilizer_->MapQDDotToAcceleration (*context_, qddot, &vdot_redo);
389
+ EXPECT_TRUE (CompareMatrices (vdot_redo, vdot, 16 * kTolerance ,
390
+ MatrixCompareType::relative));
391
+
392
+ // Compute the 7x6 N(q) matrix and its time-derivative Ṅ(q,q̇) that appear in
393
+ // q̈ = Ṅ(q,q̇)⋅v + N(q)⋅v̇
394
+ MatrixX<double > N (7 , 6 ), Ndot (7 , 6 );
395
+ mobilizer_->CalcNMatrix (*context_, &N);
396
+ mobilizer_->CalcNDotMatrix (*context_, &Ndot);
397
+
398
+ // Verify equivalence of q̈ = Ṅ(q,q̇)⋅v + N(q)⋅v̇ and MapAccelerationToQDDot().
399
+ const Eigen::Matrix<double , 7 , 1 > qddot_expected = Ndot * v + N * vdot;
400
+ EXPECT_TRUE (CompareMatrices (qddot, qddot_expected, kTolerance ,
401
+ MatrixCompareType::relative));
402
+
403
+ // Compute the 6x7 N⁺(q) matrix and its time-derivative Ṅ⁺(q,q̇) that appear in
404
+ // v̇ = Ṅ⁺(q,q̇)⋅q̇ + N⁺(q)⋅q̈
405
+ MatrixX<double > Nplus (6 , 7 ), Nplusdot (6 , 7 );
406
+ mobilizer_->CalcNplusMatrix (*context_, &Nplus);
407
+ mobilizer_->CalcNplusDotMatrix (*context_, &Nplusdot);
408
+
409
+ // Verify equivalence of v̇ = Ṅ⁺(q,q̇)⋅q̇ + N⁺(q)⋅q̈ and MapQDDotToAcceleration().
410
+ Eigen::Matrix<double , 7 , 1 > qdot;
411
+ mobilizer_->MapVelocityToQDot (*context_, v, &qdot);
412
+ const Vector6<double > vdot_expected = Nplusdot * qdot + Nplus * qddot;
413
+ EXPECT_TRUE (CompareMatrices (vdot_expected, vdot, 16 * kTolerance ,
414
+ MatrixCompareType::relative));
415
+
416
+ // Ensure the N⁺(q) matrix is the left pseudoinverse of the N(q) matrix.
417
+ // In other words, ensure Nplus * N = [I₆₆] (6x6 identity matrix).
418
+ MatrixX<double > Nplus_x_N = Nplus * N;
419
+ EXPECT_TRUE (CompareMatrices (Nplus_x_N, MatrixX<double >::Identity (6 , 6 ),
420
+ kTolerance , MatrixCompareType::relative));
421
+
422
+ // Ensure the rotation (upper-left block) part of the N(q) matrix is
423
+ // 0.25 times the transpose of the rotation part of the N⁺(q) matrix.
424
+ MatrixX<double > N_rotational = N.block <4 , 3 >(0 , 0 );
425
+ MatrixX<double > Nplus_rotational = Nplus.block <3 , 4 >(0 , 0 );
426
+ EXPECT_TRUE (CompareMatrices (N_rotational.transpose (), 0.25 * Nplus_rotational,
427
+ kTolerance , MatrixCompareType::relative));
428
+ }
429
+
368
430
} // namespace
369
431
} // namespace internal
370
432
} // namespace multibody
0 commit comments