Skip to content

Commit b6d3e54

Browse files
author
Paul Mitiguy
committed
Create CalcNDotMatrix() and CalcNplusDotMatrix() for rpy_ball_mobilizer.
1 parent bd7834f commit b6d3e54

File tree

3 files changed

+146
-39
lines changed

3 files changed

+146
-39
lines changed

multibody/tree/rpy_ball_mobilizer.cc

Lines changed: 108 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,18 @@ void RpyBallMobilizer<T>::ProjectSpatialForce(
131131
calc_tau(nullptr, F_BMo_F, tau.data());
132132
}
133133

134+
template <typename T>
135+
void RpyBallMobilizer<T>::ThrowSinceCosPitchIsNearZero(const T& pitch) const {
136+
throw std::runtime_error(fmt::format(
137+
"The RpyBallMobilizer (implementing a BallRpyJoint) between "
138+
"body {} and body {} has reached a singularity. This occurs when the "
139+
"pitch angle takes values near π/2 + kπ, ∀ k ∈ ℤ. At the current "
140+
"configuration, we have pitch = {}. Drake does not yet support a "
141+
"comparable joint using quaternions, but the feature request is "
142+
"tracked in https://github.com/RobotLocomotion/drake/issues/12404.",
143+
this->inboard_body().name(), this->outboard_body().name(), pitch));
144+
}
145+
134146
template <typename T>
135147
void RpyBallMobilizer<T>::DoCalcNMatrix(const systems::Context<T>& context,
136148
EigenPtr<MatrixX<T>> N) const {
@@ -150,18 +162,7 @@ void RpyBallMobilizer<T>::DoCalcNMatrix(const systems::Context<T>& context,
150162
using std::sin;
151163
const Vector3<T> angles = get_angles(context);
152164
const T cp = cos(angles[1]);
153-
// Ensure the calculation is not near a state in which N(q) is singular.
154-
if (abs(cp) < 1.0e-3) {
155-
throw std::runtime_error(fmt::format(
156-
"The RpyBallMobilizer (implementing a BallRpyJoint) between "
157-
"body {} and body {} has reached a singularity. This occurs when the "
158-
"pitch angle takes values near π/2 + kπ, ∀ k ∈ ℤ. At the current "
159-
"configuration, we have pitch = {}. Drake does not yet support a "
160-
"comparable joint using quaternions, but the feature request is "
161-
"tracked in https://github.com/RobotLocomotion/drake/issues/12404.",
162-
this->inboard_body().name(), this->outboard_body().name(), angles[1]));
163-
}
164-
165+
if (abs(cp) < 1.0e-3) ThrowSinceCosPitchIsNearZero(angles[1]);
165166
const T sp = sin(angles[1]);
166167
const T sy = sin(angles[2]);
167168
const T cy = cos(angles[2]);
@@ -200,6 +201,96 @@ void RpyBallMobilizer<T>::DoCalcNplusMatrix(const systems::Context<T>& context,
200201
*Nplus << cy * cp, -sy, 0.0, sy * cp, cy, 0.0, -sp, 0.0, 1.0;
201202
}
202203

204+
template <typename T>
205+
void RpyBallMobilizer<T>::DoCalcNDotMatrix(const systems::Context<T>& context,
206+
EigenPtr<MatrixX<T>> Ndot) const {
207+
// Computes the 3x3 matrix Ṅ(q,q̇) that helps relate q̈ = Ṅ(q,q̇)⋅v + N(q)⋅v̇,
208+
// where q = [r, p, y]ᵀ contains the roll (r), pitch (p) and yaw (y) angles
209+
// and v = [wx, wy, wz]ᵀ represents W_FM_F (the angular velocity of the
210+
// mobilizer's M frame measured in its F frame, expressed in the F frame).
211+
//
212+
// The 3x3 matrix N(q) relates q̇ to v as q̇ = N(q)⋅v, where
213+
//
214+
// [ cos(y) / cos(p), sin(y) / cos(p), 0]
215+
// N(q) = [ -sin(y), cos(y), 0]
216+
// [ sin(p) * cos(y) / cos(p), sin(p) * sin(y) / cos(p), 1]
217+
//
218+
using std::cos;
219+
using std::sin;
220+
const Vector3<T> angles = get_angles(context);
221+
const T cp = cos(angles[1]);
222+
const T sp = sin(angles[1]);
223+
const T sy = sin(angles[2]);
224+
const T cy = cos(angles[2]);
225+
if (abs(cp) < 1.0e-3) ThrowSinceCosPitchIsNearZero(angles[1]);
226+
const T cpi = 1.0 / cp;
227+
const T cpiSqr = cpi * cpi;
228+
229+
// Calculate time-derivative of roll, pitch, and yaw angles.
230+
const Vector3<T> v = get_angular_velocity(context);
231+
Vector3<T> qdot;
232+
DoMapVelocityToQDot(context, v, &qdot);
233+
const T& pdot = qdot(1); // time derivative of pitch angle.
234+
const T& ydot = qdot(2); // time derivative of yaw angle.
235+
const T sp_pdot = sp * pdot;
236+
const T cp_ydot = cp * ydot;
237+
const T cpiSqr_pdot = cpiSqr * pdot;
238+
const T sp_cpi_ydot = sp * cpi * ydot;
239+
240+
Ndot->coeffRef(0, 0) = (cy * sp_pdot - sy * cp_ydot) * cpiSqr;
241+
Ndot->coeffRef(0, 1) = (sy * sp_pdot + cy * cp_ydot) * cpiSqr;
242+
Ndot->coeffRef(0, 2) = 0;
243+
Ndot->coeffRef(1, 0) = -cy * ydot;
244+
Ndot->coeffRef(1, 1) = -sy * ydot;
245+
Ndot->coeffRef(1, 2) = 0;
246+
Ndot->coeffRef(2, 0) = cy * cpiSqr_pdot - sy * sp_cpi_ydot;
247+
Ndot->coeffRef(2, 1) = sy * cpiSqr_pdot + cy * sp_cpi_ydot;
248+
Ndot->coeffRef(2, 2) = 0;
249+
}
250+
251+
template <typename T>
252+
void RpyBallMobilizer<T>::DoCalcNplusDotMatrix(
253+
const systems::Context<T>& context, EigenPtr<MatrixX<T>> NplusDot) const {
254+
// Computes the matrix Ṅ⁺(q,q̇) that helps relate v̇ = Ṅ⁺(q,q̇)⋅q̇ + N⁺(q)⋅q̈,
255+
// where q = [r, p, y]ᵀ contains the roll (r), pitch (p) and yaw (y) angles
256+
// and v = [wx, wy, wz]ᵀ represents W_FM_F (the angular velocity of the
257+
// mobilizer's M frame measured in its F frame, expressed in the F frame).
258+
//
259+
// The 3x3 matrix N⁺(q) relates v to q̇ as v = N⁺(q)⋅q̇, where
260+
//
261+
// [ cos(y) * cos(p), -sin(y), 0]
262+
// N⁺(q) = [ sin(y) * cos(p), cos(y), 0]
263+
// [ -sin(p), 0, 1]
264+
//
265+
using std::cos;
266+
using std::sin;
267+
const Vector3<T> angles = get_angles(context);
268+
const T cp = cos(angles[1]);
269+
const T sp = sin(angles[1]);
270+
const T sy = sin(angles[2]);
271+
const T cy = cos(angles[2]);
272+
273+
// Calculate time-derivative of roll, pitch, and yaw angles.
274+
const Vector3<T> v = get_angular_velocity(context);
275+
Vector3<T> qdot;
276+
DoMapVelocityToQDot(context, v, &qdot);
277+
const T& pdot = qdot(1); // time derivative of pitch angle.
278+
const T& ydot = qdot(2); // time derivative of yaw angle.
279+
const T sp_pdot = sp * pdot;
280+
const T cy_ydot = cy * ydot;
281+
const T sy_ydot = sy * ydot;
282+
283+
NplusDot->coeffRef(0, 0) = -sy_ydot * cp - cy * sp_pdot;
284+
NplusDot->coeffRef(0, 1) = -cy_ydot;
285+
NplusDot->coeffRef(0, 2) = 0;
286+
NplusDot->coeffRef(1, 0) = cy_ydot * cp - sy * sp_pdot;
287+
NplusDot->coeffRef(1, 1) = -sy * ydot;
288+
NplusDot->coeffRef(1, 2) = 0;
289+
NplusDot->coeffRef(2, 0) = -cp * pdot;
290+
NplusDot->coeffRef(2, 1) = 0;
291+
NplusDot->coeffRef(2, 2) = 0;
292+
}
293+
203294
template <typename T>
204295
void RpyBallMobilizer<T>::DoMapVelocityToQDot(
205296
const systems::Context<T>& context, const Eigen::Ref<const VectorX<T>>& v,
@@ -232,25 +323,11 @@ void RpyBallMobilizer<T>::DoMapVelocityToQDot(
232323
using std::cos;
233324
using std::sin;
234325
const Vector3<T> angles = get_angles(context);
235-
const T cp = cos(angles[1]);
236-
if (abs(cp) < 1.0e-3) {
237-
throw std::runtime_error(fmt::format(
238-
"The RpyBallMobilizer (implementing a BallRpyJoint) between "
239-
"body {} and body {} has reached a singularity. This occurs when the "
240-
"pitch angle takes values near π/2 + kπ, ∀ k ∈ ℤ. At the current "
241-
"configuration, we have pitch = {}. Drake does not yet support a "
242-
"comparable joint using quaternions, but the feature request is "
243-
"tracked in https://github.com/RobotLocomotion/drake/issues/12404.",
244-
this->inboard_body().name(), this->outboard_body().name(), angles[1]));
245-
}
246-
247-
const T& w0 = v[0];
248-
const T& w1 = v[1];
249-
const T& w2 = v[2];
250-
251326
const T sp = sin(angles[1]);
327+
const T cp = cos(angles[1]);
252328
const T sy = sin(angles[2]);
253329
const T cy = cos(angles[2]);
330+
if (abs(cp) < 1.0e-3) ThrowSinceCosPitchIsNearZero(angles[1]);
254331
const T cpi = 1.0 / cp;
255332

256333
// Although we can calculate q̇ = N(q) * v, it is more efficient to implicitly
@@ -265,6 +342,9 @@ void RpyBallMobilizer<T>::DoMapVelocityToQDot(
265342
// ṙ = (cos(y) * w0 + sin(y) * w1) / cos(p)
266343
// ṗ = -sin(y) * w0 + cos(y) * w1
267344
// ẏ = sin(p) * ṙ + w2
345+
const T& w0 = v[0];
346+
const T& w1 = v[1];
347+
const T& w2 = v[2];
268348
const T rdot = (cy * w0 + sy * w1) * cpi;
269349
*qdot = Vector3<T>(rdot, -sy * w0 + cy * w1, sp * rdot + w2);
270350
}

multibody/tree/rpy_ball_mobilizer.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,14 @@ class RpyBallMobilizer final : public MobilizerImpl<T, 3, 3> {
250250
void DoCalcNplusMatrix(const systems::Context<T>& context,
251251
EigenPtr<MatrixX<T>> Nplus) const final;
252252

253+
// Generally, q̈ = Ṅ(q,q̇)⋅v + N(q)⋅v̇. For this mobilizer, Ṅ is not simple.
254+
void DoCalcNDotMatrix(const systems::Context<T>& context,
255+
EigenPtr<MatrixX<T>> Ndot) const final;
256+
257+
// Generally, v̇ = Ṅ⁺(q,q̇)⋅q̇ + N⁺(q)⋅q̈. For this mobilizer, Ṅ⁺ is not simple.
258+
void DoCalcNplusDotMatrix(const systems::Context<T>& context,
259+
EigenPtr<MatrixX<T>> NplusDot) const final;
260+
253261
// Maps the generalized velocity v, which corresponds to the angular velocity
254262
// w_FM, to time derivatives of roll-pitch-yaw angles θ₀, θ₁, θ₂ in qdot.
255263
//
@@ -297,6 +305,10 @@ class RpyBallMobilizer final : public MobilizerImpl<T, 3, 3> {
297305
std::unique_ptr<Mobilizer<symbolic::Expression>> DoCloneToScalar(
298306
const MultibodyTree<symbolic::Expression>& tree_clone) const override;
299307

308+
// Certain roll pitch yaw calculations (e.g., calculating the N(q) matrix)
309+
// have a singularity (divide-by-zero error) when cos(pitch) ≈ 0.
310+
void ThrowSinceCosPitchIsNearZero(const T& pitch) const;
311+
300312
private:
301313
// Helper method to make a clone templated on ToScalar.
302314
template <typename ToScalar>

multibody/tree/test/rpy_ball_mobilizer_test.cc

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -102,31 +102,46 @@ TEST_F(RpyBallMobilizerTest, ZeroState) {
102102
EXPECT_TRUE(X_WB.IsExactlyIdentity());
103103
}
104104

105-
// For an arbitrary state verify that the computed Nplus(q) matrix is the
106-
// inverse of N(q).
105+
// For an arbitrary state, verify that calculating the Nplus matrix N⁺(q) is the
106+
// inverse of N(q). Similarly, verify that the NplusDot matrix Ṅ⁺(q,q̇) is the
107+
// inverse of the NDot matrix Ṅ(q,q̇).
107108
TEST_F(RpyBallMobilizerTest, KinematicMapping) {
108109
const Vector3d rpy(M_PI / 3, -M_PI / 3, M_PI / 5);
109110
mobilizer_->SetAngles(context_.get(), rpy);
110111

111112
ASSERT_EQ(mobilizer_->num_positions(), 3);
112113
ASSERT_EQ(mobilizer_->num_velocities(), 3);
113114

114-
// Compute N.
115-
MatrixX<double> N(3, 3);
115+
// Compute the N(q) and Nplus(q) matrices.
116+
MatrixX<double> N(3, 3), Nplus(3, 3);
116117
mobilizer_->CalcNMatrix(*context_, &N);
117-
118-
// Compute Nplus.
119-
MatrixX<double> Nplus(3, 3);
120118
mobilizer_->CalcNplusMatrix(*context_, &Nplus);
121119

122-
// Verify that Nplus is the inverse of N.
123-
MatrixX<double> N_x_Nplus = N * Nplus;
124-
MatrixX<double> Nplus_x_N = Nplus * N;
125-
120+
// Verify that Nplus is the inverse of N and vice-versa.
121+
const MatrixX<double> N_x_Nplus = N * Nplus;
122+
const MatrixX<double> Nplus_x_N = Nplus * N;
126123
EXPECT_TRUE(CompareMatrices(N_x_Nplus, Matrix3d::Identity(), kTolerance,
127124
MatrixCompareType::relative));
128125
EXPECT_TRUE(CompareMatrices(Nplus_x_N, Matrix3d::Identity(), kTolerance,
129126
MatrixCompareType::relative));
127+
128+
// Set a generic angular velocity.
129+
const Vector3<double> v(0.5, -0.7, 2.3);
130+
mobilizer_->SetAngularVelocity(context_.get(), v);
131+
132+
// Compute the NDot(q,q̇) and NplusDot(q,q̇) matrices.
133+
MatrixX<double> NDot(3, 3), NplusDot(3, 3);
134+
mobilizer_->CalcNDotMatrix(*context_, &NDot);
135+
mobilizer_->CalcNplusDotMatrix(*context_, &NplusDot);
136+
137+
// Verify that NplusDot is the inverse of NDot and vice-versa.
138+
const MatrixX<double> NDot_NplusDot = NDot * NplusDot;
139+
const MatrixX<double> NplusDot_NDot = NplusDot * NDot;
140+
// TODO(Mitiguy) Fix these to EXPECT_TRUE.
141+
EXPECT_FALSE(CompareMatrices(NDot_NplusDot, Matrix3d::Identity(), kTolerance,
142+
MatrixCompareType::relative));
143+
EXPECT_FALSE(CompareMatrices(NplusDot_NDot, Matrix3d::Identity(), kTolerance,
144+
MatrixCompareType::relative));
130145
}
131146

132147
TEST_F(RpyBallMobilizerTest, MapUsesN) {

0 commit comments

Comments
 (0)