Skip to content

Commit fec2751

Browse files
authored
MapQDDotToAcceleration() and MapAccelerationToQDDot() for rpy mobilizer. (#23092)
1 parent ea93527 commit fec2751

File tree

3 files changed

+175
-35
lines changed

3 files changed

+175
-35
lines changed

multibody/tree/rpy_ball_mobilizer.cc

Lines changed: 110 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -132,17 +132,20 @@ void RpyBallMobilizer<T>::ProjectSpatialForce(
132132
}
133133

134134
template <typename T>
135-
void RpyBallMobilizer<T>::ThrowSinceCosPitchIsNearZero(
136-
const T& pitch, const char* function_name) const {
137-
throw std::runtime_error(fmt::format(
138-
"{}(): The RpyBallMobilizer (implementing a BallRpyJoint) between "
139-
"body {} and body {} has reached a singularity. This occurs when the "
140-
"pitch angle takes values near π/2 + kπ, ∀ k ∈ ℤ. At the current "
141-
"configuration, we have pitch = {} radians. Drake does not yet support "
142-
"a comparable joint using quaternions, but the feature request is "
143-
"tracked in https://github.com/RobotLocomotion/drake/issues/12404.",
144-
function_name, this->inboard_body().name(), this->outboard_body().name(),
145-
pitch));
135+
void RpyBallMobilizer<T>::ThrowIfCosPitchNearZero(
136+
const T& cos_pitch, const T& pitch_angle, const char* function_name) const {
137+
using std::abs;
138+
if (abs(cos_pitch) < 1.0e-3) {
139+
throw std::runtime_error(fmt::format(
140+
"{}(): The RpyBallMobilizer (implementing a BallRpyJoint) between "
141+
"body {} and body {} has reached a singularity. This occurs when the "
142+
"pitch angle takes values near π/2 + kπ, ∀ k ∈ ℤ. At the current "
143+
"configuration, we have pitch = {} radians. Drake does not yet support "
144+
"a comparable joint using quaternions, but the feature request is "
145+
"tracked in https://github.com/RobotLocomotion/drake/issues/12404.",
146+
function_name, this->inboard_body().name(),
147+
this->outboard_body().name(), pitch_angle));
148+
}
146149
}
147150

148151
template <typename T>
@@ -157,17 +160,14 @@ void RpyBallMobilizer<T>::DoCalcNMatrix(const systems::Context<T>& context,
157160
// ⌊ ẏ ⌋ ⌊ sin(p) * cos(y) / cos(p), sin(p) * sin(y) / cos(p), 1 ⌋ ⌊ ω2 ⌋
158161
//
159162
// Note: N(q) is singular for p = π/2 + kπ, for k = ±1, ±2, ...
160-
// See related code and comments in MapVelocityToQdot().
163+
// See related code and comments in DoMapVelocityToQdot().
161164

162-
using std::abs;
163165
using std::cos;
164166
using std::sin;
165167
const Vector3<T> angles = get_angles(context);
166168
const T cp = cos(angles[1]);
167-
if (abs(cp) < 1.0e-3) {
168-
const char* function_name_less_Do = __func__ + 2;
169-
ThrowSinceCosPitchIsNearZero(angles[1], function_name_less_Do);
170-
}
169+
ThrowIfCosPitchNearZero(cp, angles[1], "CalcNMatrix");
170+
171171
const T sp = sin(angles[1]);
172172
const T sy = sin(angles[2]);
173173
const T cy = cos(angles[2]);
@@ -196,7 +196,7 @@ void RpyBallMobilizer<T>::DoCalcNplusMatrix(const systems::Context<T>& context,
196196
// | ω1 | = | sin(y) * cos(p), cos(y), 0 | | ṗ |
197197
// ⌊ ω2 ⌋ ⌊ -sin(p), 0, 1 ⌋ ⌊ ẏ ⌋
198198
//
199-
// See related code and comments in MapQDotToVelocity().
199+
// See related code and comments in DoMapQDotToVelocity().
200200
const Vector3<T> angles = get_angles(context);
201201
const T sp = sin(angles[1]);
202202
const T cp = cos(angles[1]);
@@ -239,10 +239,7 @@ void RpyBallMobilizer<T>::DoCalcNDotMatrix(const systems::Context<T>& context,
239239
const T sp = sin(angles[1]);
240240
const T sy = sin(angles[2]);
241241
const T cy = cos(angles[2]);
242-
if (abs(cp) < 1.0e-3) {
243-
const char* function_name_less_Do = __func__ + 2;
244-
ThrowSinceCosPitchIsNearZero(angles[1], function_name_less_Do);
245-
}
242+
ThrowIfCosPitchNearZero(cp, angles[1], "CalcNDotMatrix");
246243
const T cpi = 1.0 / cp;
247244
const T cpiSqr = cpi * cpi;
248245

@@ -298,10 +295,7 @@ void RpyBallMobilizer<T>::DoCalcNplusDotMatrix(
298295

299296
// Throw an exception with the proper function name if a singularity would be
300297
// encountered in DoMapVelocityToQDot().
301-
if (abs(cp) < 1.0e-3) {
302-
const char* function_name_less_Do = __func__ + 2;
303-
ThrowSinceCosPitchIsNearZero(angles[1], function_name_less_Do);
304-
}
298+
ThrowIfCosPitchNearZero(cp, angles[1], "CalcNplusDotMatrix");
305299

306300
// Calculate time-derivative of roll, pitch, and yaw angles.
307301
const Vector3<T> v = get_angular_velocity(context);
@@ -353,18 +347,14 @@ void RpyBallMobilizer<T>::DoMapVelocityToQDot(
353347
// [Mitiguy August 2019] Mitiguy, P., 2019. Advanced Dynamics & Motion
354348
// Simulation.
355349

356-
using std::abs;
357350
using std::cos;
358351
using std::sin;
359352
const Vector3<T> angles = get_angles(context);
360353
const T sp = sin(angles[1]);
361354
const T cp = cos(angles[1]);
362355
const T sy = sin(angles[2]);
363356
const T cy = cos(angles[2]);
364-
if (abs(cp) < 1.0e-3) {
365-
const char* function_name_less_Do = __func__ + 2;
366-
ThrowSinceCosPitchIsNearZero(angles[1], function_name_less_Do);
367-
}
357+
ThrowIfCosPitchNearZero(cp, angles[1], "MapVelocityToQDot");
368358
const T cpi = 1.0 / cp;
369359

370360
// Although we can calculate q̇ = N(q) * v, it is more efficient to implicitly
@@ -446,6 +436,95 @@ RpyBallMobilizer<T>::TemplatedDoCloneToScalar(
446436
outboard_frame_clone);
447437
}
448438

439+
template <typename T>
440+
Vector3<T> RpyBallMobilizer<T>::CalcAccelerationBiasForQDDot(
441+
const systems::Context<T>& context, const char* function_name) const {
442+
using std::cos;
443+
using std::sin;
444+
const Vector3<T> angles = get_angles(context);
445+
const T cp = cos(angles[1]);
446+
const T sp = sin(angles[1]);
447+
const T sy = sin(angles[2]);
448+
const T cy = cos(angles[2]);
449+
ThrowIfCosPitchNearZero(cp, angles[1], function_name);
450+
451+
// The algorithm below calculates Ṅ⁺(q,q̇)⋅q̇. The algorithm was verified with
452+
// MotionGenesis. It can also be verified by-hand, e.g., with documentation
453+
// in DoCalcNplusDotMatrix which directly differentiates N⁺(q) to form
454+
// Ṅ⁺(q,q̇). Thereafter, multiply by q̇ to form Ṅ⁺(q,q̇)⋅q̇ (and simplify).
455+
const Vector3<T> v = get_angular_velocity(context);
456+
Vector3<T> qdot;
457+
DoMapVelocityToQDot(context, v, &qdot);
458+
const T& rdot = qdot[0];
459+
const T& pdot = qdot[1];
460+
const T& ydot = qdot[2];
461+
const T pdot_ydot = pdot * ydot;
462+
const T rdot_pdot = rdot * pdot;
463+
const T rdot_ydot = rdot * ydot;
464+
const T sp_rdot_pdot = sp * rdot_pdot;
465+
const T cp_rdot_ydot = cp * rdot_ydot;
466+
return Vector3<T>(-cy * pdot_ydot - cy * sp_rdot_pdot - sy * cp_rdot_ydot,
467+
-sy * pdot_ydot - sy * sp_rdot_pdot + cy * cp_rdot_ydot,
468+
-cp * rdot_pdot);
469+
}
470+
471+
template <typename T>
472+
void RpyBallMobilizer<T>::DoMapAccelerationToQDDot(
473+
const systems::Context<T>& context,
474+
const Eigen::Ref<const VectorX<T>>& vdot,
475+
EigenPtr<VectorX<T>> qddot) const {
476+
// As shown in DoMapVelocityToQDot(), the time-derivatives of generalized
477+
// positions q̇ = [ṙ, ṗ, ẏ]ᵀ are related to the generalized velocities
478+
// v = [ωx, ωy, ωz]ᵀ in the matrix form q̇ = N(q)⋅v, where N is a 3x3 matrix.
479+
//
480+
// For this mobilizer v = w_FM_F = [ωx, ωy, ωz]ᵀ is the mobilizer M frame's
481+
// angular velocity in the mobilizer F frame, expressed in frame F and
482+
// q = [r, p, y]ᵀ denote roll, pitch, yaw angles (generalized positions).
483+
//
484+
// There are various ways to get q̈ = [r̈, p̈, ÿ]ᵀ in terms of v̇ = [ω̇x, ω̇y, ω̇z]ᵀ.
485+
// One way to is differentiate q̇ = N(q)⋅v to form q̈ = Ṅ(q,q̇)⋅v + N(q)⋅v̇.
486+
// However, N⁺(q) is simpler than N(q) so Ṅ⁺(q,q̇) is much simpler than Ṅ(q,q̇).
487+
// A calculation of q̈ that leverages the simplicity of Ṅ⁺(q,q̇) over Ṅ(q,q̇)
488+
// and the available function CalcAccelerationBiasForQDDot() starts with
489+
// v̇ = Ṅ⁺(q,q̇)⋅q̇ + N⁺(q)⋅q̈ and then solves as q̈ = N(q) {v̇ - Ṅ⁺(q,q̇)⋅q̇}.
490+
const Vector3<T> vdot_minus_NplusDotTimesQDot =
491+
vdot - CalcAccelerationBiasForQDDot(context, __func__);
492+
493+
// Note: Although the function below was designed to calculate q̇ = N(q)⋅v,
494+
// it can also be used to calculate q̈ = N(q) {v̇ - Ṅ⁺(q,q̇)⋅q̇}.
495+
DoMapVelocityToQDot(context, vdot_minus_NplusDotTimesQDot, qddot);
496+
}
497+
498+
template <typename T>
499+
void RpyBallMobilizer<T>::DoMapQDDotToAcceleration(
500+
const systems::Context<T>& context,
501+
const Eigen::Ref<const VectorX<T>>& qddot,
502+
EigenPtr<VectorX<T>> vdot) const {
503+
// As seen in DoMapQDotToVelocity(), generalized velocities v = [ωx, ωy, ωz]ᵀ
504+
// are related to q̇ = [ṙ, ṗ, ẏ]ᵀ (time-derivatives of generalized positions)
505+
// in the matrix form v = N⁺(q)⋅q̇, where N⁺ is a 3x3 matrix.
506+
//
507+
// For this mobilizer v = w_FM_F = [ωx, ωy, ωz]ᵀ is the mobilizer M frame's
508+
// angular velocity in the mobilizer F frame, expressed in frame F and
509+
// q = [r, p, y]ᵀ denote roll, pitch, yaw angles (generalized positions).
510+
//
511+
// There are various ways to calculate v̇ = [ω̇x, ω̇y, ω̇z]ᵀ (the time-derivatives
512+
// of the generalized velocities). The calculation below is straightforward in
513+
// that it simply differentiates v = N⁺(q)⋅q̇ to form v̇ = Ṅ⁺(q,q̇)⋅q̇ + N⁺(q)⋅q̈.
514+
515+
// Form the Ṅ⁺(q,q̇)⋅q̇ term of the result now (start of this function) so any
516+
// singularity (if one exists) throws an exception referencing this function.
517+
const Vector3<T> NplusDot_times_Qdot =
518+
CalcAccelerationBiasForQDDot(context, __func__);
519+
520+
// Although the function below was designed to calculate v = N⁺(q)⋅q̇, it can
521+
// also be used to calculate N⁺(q)⋅q̈.
522+
DoMapQDotToVelocity(context, qddot, vdot); // On return, vdot = N⁺(q)⋅q̈.
523+
524+
// Sum the previous terms to form v̇ = Ṅ⁺(q,q̇)⋅q̇ + N⁺(q)⋅q̈.
525+
*vdot += NplusDot_times_Qdot;
526+
}
527+
449528
template <typename T>
450529
std::unique_ptr<Mobilizer<double>> RpyBallMobilizer<T>::DoCloneToScalar(
451530
const MultibodyTree<double>& tree_clone) const {

multibody/tree/rpy_ball_mobilizer.h

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ class RpyBallMobilizer final : public MobilizerImpl<T, 3, 3> {
246246

247247
bool is_velocity_equal_to_qdot() const override { return false; }
248248

249-
protected:
249+
private:
250250
void DoCalcNMatrix(const systems::Context<T>& context,
251251
EigenPtr<MatrixX<T>> N) const final;
252252

@@ -299,6 +299,22 @@ class RpyBallMobilizer final : public MobilizerImpl<T, 3, 3> {
299299
const Eigen::Ref<const VectorX<T>>& qdot,
300300
EigenPtr<VectorX<T>> v) const final;
301301

302+
// Maps vdot to qddot, which for this mobilizer is q̈ = Ṅ(q,q̇)⋅v + N(q)⋅v̇.
303+
// For simple mobilizers q̈ = v̇. This mobilizer's N and Ṅ are more elaborate.
304+
void DoMapAccelerationToQDDot(const systems::Context<T>& context,
305+
const Eigen::Ref<const VectorX<T>>& vdot,
306+
EigenPtr<VectorX<T>> qddot) const final;
307+
308+
// Maps qddot to vdot, which for this mobilizer is v̇ = Ṅ⁺(q,q̇)⋅q̇ + N⁺(q)⋅q̈.
309+
// For simple mobilizers v̇ = q̈. This mobilizer's N and Ṅ⁺ are more elaborate.
310+
void DoMapQDDotToAcceleration(const systems::Context<T>& context,
311+
const Eigen::Ref<const VectorX<T>>& qddot,
312+
EigenPtr<VectorX<T>> vdot) const final;
313+
314+
// Calculate the term Ṅ⁺(q,q̇)⋅q̇ which appears in v̇ = Ṅ⁺(q,q̇)⋅q̇ + N⁺(q)⋅q̈.
315+
Vector3<T> CalcAccelerationBiasForQDDot(const systems::Context<T>& context,
316+
const char* function_name) const;
317+
302318
std::unique_ptr<Mobilizer<double>> DoCloneToScalar(
303319
const MultibodyTree<double>& tree_clone) const override;
304320

@@ -310,10 +326,12 @@ class RpyBallMobilizer final : public MobilizerImpl<T, 3, 3> {
310326

311327
// Certain roll pitch yaw calculations (e.g., calculating the N(q) matrix)
312328
// have a singularity (divide-by-zero error) when cos(pitch) ≈ 0.
313-
void ThrowSinceCosPitchIsNearZero(const T& pitch,
314-
const char* function_name) const;
329+
// The tolerance 1.0e-3 is used to test whether the cosine of the pitch angle
330+
// is near zero, which occurs when the pitch angle ≈ π/2 ± n π (n=0, 1 2, …).
331+
// Throw an exception if a pitch angle is within ≈ 0.057° of a singularity.
332+
void ThrowIfCosPitchNearZero(const T& cos_pitch, const T& pitch_angle,
333+
const char* function_name) const;
315334

316-
private:
317335
// Helper method to make a clone templated on ToScalar.
318336
template <typename ToScalar>
319337
std::unique_ptr<Mobilizer<ToScalar>> TemplatedDoCloneToScalar(

multibody/tree/test/rpy_ball_mobilizer_test.cc

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,49 @@ TEST_F(RpyBallMobilizerTest, MapUsesNplus) {
223223
MatrixCompareType::relative));
224224
}
225225

226+
TEST_F(RpyBallMobilizerTest, MapAccelerationToQDDotAndViceVersa) {
227+
// Set an arbitrary non-zero state.
228+
const Vector3<double> rpy(M_PI / 3, -M_PI / 4, M_PI / 5);
229+
const Vector3<double> wxyz(5.4, -9.8, 3.2);
230+
mobilizer_->SetAngles(context_.get(), rpy);
231+
mobilizer_->SetAngularVelocity(context_.get(), wxyz);
232+
233+
// Set an arbitrary v̇ and use MapAccelerationToQDDot() to calculate q̈.
234+
const Vector3<double> vdot(0.3, -0.2, 0.9); // v̇ = [ẇx, ẇy, ẇz].
235+
Vector3<double> qddot;
236+
mobilizer_->MapAccelerationToQDDot(*context_, vdot, &qddot);
237+
238+
// Compute the 3x3 N(q) matrix and its time-derivative Ṅ(q,q̇).
239+
MatrixX<double> N(3, 3), Ndot(3, 3);
240+
mobilizer_->CalcNMatrix(*context_, &N);
241+
mobilizer_->CalcNDotMatrix(*context_, &Ndot);
242+
243+
// Verify equivalence of q̈ = Ṅ(q,q̇)⋅v + N(q)⋅v̇ and MapAccelerationToQDDot().
244+
const Vector3<double> qddot_expected = Ndot * wxyz + N * vdot;
245+
EXPECT_TRUE(CompareMatrices(qddot, qddot_expected, kTolerance,
246+
MatrixCompareType::relative));
247+
248+
// Compute the 3x3 N⁺(q) matrix and its time-derivative Ṅ⁺(q,q̇).
249+
MatrixX<double> Nplus(3, 3), Nplusdot(3, 3);
250+
mobilizer_->CalcNplusMatrix(*context_, &Nplus);
251+
mobilizer_->CalcNplusDotMatrix(*context_, &Nplusdot);
252+
253+
// Starting with the previous q̈, use MapQDDotToAcceleration() to calculate v̇.
254+
Vector3<double> wdot;
255+
mobilizer_->MapQDDotToAcceleration(*context_, qddot, &wdot);
256+
257+
// Verify equivalence of v̇ = Ṅ⁺(q,q̇)⋅q̇ + N⁺(q)⋅q̈ and MapQDDotToAcceleration().
258+
Vector3<double> qdot;
259+
mobilizer_->MapVelocityToQDot(*context_, wxyz, &qdot);
260+
const Vector3<double> vdot_expected = Nplusdot * qdot + Nplus * qddot;
261+
EXPECT_TRUE(CompareMatrices(vdot_expected, wdot, 16 * kTolerance,
262+
MatrixCompareType::relative));
263+
264+
// Verify MapQDDotToAcceleration() is the inverse of MapAccelerationToQDDot().
265+
EXPECT_TRUE(CompareMatrices(vdot, wdot, 16 * kTolerance,
266+
MatrixCompareType::relative));
267+
}
268+
226269
TEST_F(RpyBallMobilizerTest, SingularityError) {
227270
// Set state in singularity
228271
const Vector3d rpy_value(M_PI / 3, M_PI / 2, M_PI / 5);

0 commit comments

Comments
 (0)