Skip to content

Commit 0c9e80d

Browse files
authored
Optimize F & M frames for prismatic joint's mobilizer. (#22925)
1 parent 30d0e5b commit 0c9e80d

20 files changed

+652
-315
lines changed

math/rigid_transform.h

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,8 @@ class RigidTransform {
478478
/// `X_AC = X_AB * atX_BC`. This requires only 6 floating point operations.
479479
/// @param[in] atX_BC An axial translation transform about the indicated
480480
/// `axis`.
481-
/// @param[out] X_AC Preallocated space for the result.
481+
/// @param[out] X_AC Preallocated space for the result, which will be a
482+
/// general transform.
482483
/// @tparam axis 0, 1, or 2 corresponding to +x, +y, or +z rotation axis.
483484
/// @pre atX_BC is an @ref special_xform_def "Axial translation transform".
484485
template <int axis>
@@ -494,6 +495,51 @@ class RigidTransform {
494495
X_AC->set_translation(p_AoBo_A_ + p_BC[axis] * R_AB_.col(axis));
495496
}
496497

498+
/// (Internal use only) With `this` a general transform X_BC, and given an
499+
/// axial translation transform atX_AB, efficiently calculates
500+
/// `X_AC = atX_AB * X_BC`. This requires just 1 floating point operation.
501+
/// @param[in] atX_AB An axial translation transform along the indicated
502+
/// `axis`.
503+
/// @param[out] X_AC Preallocated space for the result, which will be a
504+
/// general transform.
505+
/// @tparam axis 0, 1, or 2 corresponding to +x, +y, or +z translation axis.
506+
/// @pre atX_AB is an @ref special_xform_def "Axial translation transform"
507+
template <int axis>
508+
#ifndef DRAKE_DOXYGEN_CXX
509+
requires(0 <= axis && axis <= 2)
510+
#endif
511+
void PreMultiplyByAxialTranslation(const RigidTransform<T>& atX_AB,
512+
RigidTransform<T>* X_AC) const {
513+
DRAKE_ASSERT(X_AC != nullptr);
514+
DRAKE_ASSERT_VOID(atX_AB.IsAxialTranslationOnlyOrThrow<axis>());
515+
// Note that R_AB = I.
516+
const RigidTransform<T>& X_BC = *this; // Rename to keep frames straight.
517+
X_AC->set_rotation(X_BC.rotation()); // R_AC = R_AB * R_BC = R_BC.
518+
X_AC->p_AoBo_A_ = X_BC.translation(); // Initialize to p_BC_B.
519+
X_AC->p_AoBo_A_[axis] += atX_AB.translation()[axis]; // The only flop.
520+
}
521+
522+
/// (Internal use only) Given a new `distance`, updates the axial translation
523+
/// transform atX_BC to represent the new translation by that amount. We
524+
/// expect that atX_BC was already such a transform (about the given x, y, or
525+
/// z axis). Only the 1 active element is modified; the other 11 remain
526+
/// unchanged. No floating point operations are needed.
527+
/// @param[in] distance the component of p_BC along the `axis` direction.
528+
/// @param[in,out] atX_BC the axial translation transform matrix to be
529+
/// updated.
530+
/// @tparam axis 0, 1, or 2 corresponding to +x, +y, or +z translation axis.
531+
/// @pre atX_BC is an @ref special_xform_def "Axial translation transform".
532+
template <int axis>
533+
#ifndef DRAKE_DOXYGEN_CXX
534+
requires(0 <= axis && axis <= 2)
535+
#endif
536+
static void UpdateAxialTranslation(const T& distance,
537+
RigidTransform<T>* atX_BC) {
538+
DRAKE_ASSERT(atX_BC != nullptr);
539+
DRAKE_ASSERT_VOID(atX_BC->IsAxialTranslationOnlyOrThrow<axis>());
540+
atX_BC->p_AoBo_A_[axis] = distance;
541+
}
542+
497543
/// (Internal use only) Composes `this` general transform X_AB with a given
498544
/// rotation-only transform rX_BC to efficiently calculate
499545
/// `X_AC = X_AB * rX_BC`. This requires 45 floating point operations.

math/test/rigid_transform_test.cc

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -874,12 +874,19 @@ GTEST_TEST(RigidTransform, SpecializedTransformOperators) {
874874
EXPECT_TRUE(X_BC_update.IsNearlyEqualTo(
875875
Xform::MakeAxialRotation<2>(4 * theta), kTol));
876876

877+
// Make an axial translation transform (this one is y-axial).
878+
const Xform ytX_BC(Vector3d(0, -2, 0)); // d = -2
879+
880+
// Test UpdateAxialTranslation().
881+
Xform ytX_BC_update = ytX_BC;
882+
Xform::UpdateAxialTranslation<1>(5, &ytX_BC_update); // d = 5
883+
EXPECT_TRUE(ytX_BC_update.IsExactlyEqualTo(Xform(Vector3d(0, 5, 0))));
884+
877885
// Make general, rotation-only, and translation-only transforms.
878886
const Xform X_AB(RollPitchYaw(1.0, 2.0, 3.0), Vector3d(1.5, 2.5, 3.5));
879887
const Xform rX_BC(RollPitchYaw(0.5, 1.5, -2.5), Vector3d::Zero());
880888
const Xform tX_BC(Vector3d(-1.0, 2.0, -3.0));
881-
const Xform ytX_BC(Vector3d(0.0, -2.0, 0.0)); // y-axial translation
882-
Xform X_AC; // reusable result
889+
Xform X_AC; // reusable result
883890

884891
// Test PostMultiplyByRotation() and PostMultiplyBy[Axial]Translation().
885892
X_AB.PostMultiplyByRotation(rX_BC, &X_AC);
@@ -899,7 +906,7 @@ GTEST_TEST(RigidTransform, SpecializedTransformOperators) {
899906
X_AB.PostMultiplyByAxialRotation<2>(zrX_BC, &X_AC);
900907
EXPECT_TRUE(X_AC.IsNearlyEqualTo(X_AB * zrX_BC, kTol));
901908

902-
// Test PreMultiplyByAxialRotation().
909+
// Test PreMultiplyByAxialRotation(), PreMultiplyByAxialTranslation().
903910
const Xform X_CD(RollPitchYaw(1.0, 2.0, 3.0), Vector3d(1.5, 2.5, 3.5));
904911
Xform X_BD; // reusable result
905912
X_CD.PreMultiplyByAxialRotation<0>(xrX_BC, &X_BD);
@@ -908,6 +915,8 @@ GTEST_TEST(RigidTransform, SpecializedTransformOperators) {
908915
EXPECT_TRUE(X_BD.IsNearlyEqualTo(yrX_BC * X_CD, kTol));
909916
X_CD.PreMultiplyByAxialRotation<2>(zrX_BC, &X_BD);
910917
EXPECT_TRUE(X_BD.IsNearlyEqualTo(zrX_BC * X_CD, kTol));
918+
X_CD.PreMultiplyByAxialTranslation<1>(ytX_BC, &X_BD);
919+
EXPECT_TRUE(X_BD.IsNearlyEqualTo(ytX_BC * X_CD, kTol));
911920
}
912921

913922
GTEST_TEST(RigidTransform, TestMemoryLayoutOfRigidTransformDouble) {

multibody/tree/body_node_impl.cc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -935,7 +935,9 @@ void BodyNodeImpl<T, ConcreteMobilizer>::CalcSpatialAccelerationBias(
935935
#define EXPLICITLY_INSTANTIATE_IMPLS(T) \
936936
template class BodyNodeImpl<T, CurvilinearMobilizer<T>>; \
937937
template class BodyNodeImpl<T, PlanarMobilizer<T>>; \
938-
template class BodyNodeImpl<T, PrismaticMobilizer<T>>; \
938+
template class BodyNodeImpl<T, PrismaticMobilizerAxial<T, 0>>; \
939+
template class BodyNodeImpl<T, PrismaticMobilizerAxial<T, 1>>; \
940+
template class BodyNodeImpl<T, PrismaticMobilizerAxial<T, 2>>; \
939941
template class BodyNodeImpl<T, QuaternionFloatingMobilizer<T>>; \
940942
template class BodyNodeImpl<T, RevoluteMobilizerAxial<T, 0>>; \
941943
template class BodyNodeImpl<T, RevoluteMobilizerAxial<T, 1>>; \

multibody/tree/body_node_impl_mass_matrix.cc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,9 @@ DEFINE_MASS_MATRIX_OFF_DIAGONAL_BLOCK(6)
142142
#define EXPLICITLY_INSTANTIATE_IMPLS(T) \
143143
template class BodyNodeImpl<T, CurvilinearMobilizer<T>>; \
144144
template class BodyNodeImpl<T, PlanarMobilizer<T>>; \
145-
template class BodyNodeImpl<T, PrismaticMobilizer<T>>; \
145+
template class BodyNodeImpl<T, PrismaticMobilizerAxial<T, 0>>; \
146+
template class BodyNodeImpl<T, PrismaticMobilizerAxial<T, 1>>; \
147+
template class BodyNodeImpl<T, PrismaticMobilizerAxial<T, 2>>; \
146148
template class BodyNodeImpl<T, QuaternionFloatingMobilizer<T>>; \
147149
template class BodyNodeImpl<T, RevoluteMobilizerAxial<T, 0>>; \
148150
template class BodyNodeImpl<T, RevoluteMobilizerAxial<T, 1>>; \

multibody/tree/joint.cc

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,18 @@ std::unique_ptr<Joint<T>> Joint<T>::DoShallowClone() const {
115115
"The {} joint failed to override DoShallowClone()", type_name()));
116116
}
117117

118+
template <typename T>
119+
std::string Joint<T>::MakeUniqueOffsetFrameName(
120+
const Frame<T>& parent_frame, const std::string& suffix) const {
121+
const internal::MultibodyTree<T>& tree = this->get_parent_tree();
122+
std::string new_name =
123+
fmt::format("{}_{}_{}", this->name(), parent_frame.name(), suffix);
124+
while (tree.HasFrameNamed(new_name, this->model_instance())) {
125+
new_name = "_" + new_name;
126+
}
127+
return new_name;
128+
}
129+
118130
template <typename T>
119131
void Joint<T>::SetSpatialVelocityImpl(systems::Context<T>* context,
120132
const SpatialVelocity<T>& V_FM,

multibody/tree/joint.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,16 @@ class Joint : public MultibodyElement<T> {
779779
DRAKE_DEMAND(has_mobilizer());
780780
return *mobilizer_;
781781
}
782+
783+
// (Internal use only) This utility generates a unique name for an offset
784+
// frame of the form jointname_parentframename_suffix. This is intended for
785+
// creating F and M mobilizer frames that are offset from joint frames Jp and
786+
// Jc. The name is guaranteed to be unique within this Joint's model instance.
787+
// If necessary, leading underscores are prepended until uniqueness is
788+
// achieved. Be sure to create the new frame in the _Joint's_ model instance
789+
// to avoid name clashes.
790+
std::string MakeUniqueOffsetFrameName(const Frame<T>& parent_frame,
791+
const std::string& suffix) const;
782792
#endif
783793
// End of hidden Doxygen section.
784794

multibody/tree/mobilizer_impl.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ Every concrete Mobilizer derived from MobilizerImpl must implement the
2929
following (ideally inline) methods (some have defaults; see below). Note that
3030
these are not virtual methods so we have to document them here in a comment
3131
rather than as declarations in the header file. The code won't compile if
32-
any mobilizer fails to implement the non-defaulted methods.
32+
any mobilizer fails to implement the non-defaulted methods. These are const
33+
member functions (rather than static members) so are permitted to depend on
34+
mobilizer-specific data members, though in many cases they don't require any
35+
such data.
3336
3437
@note The coordinate pointers q and v are guaranteed to point to the kNq or kNv
3538
state variables for the particular mobilizer. They are only 8-byte aligned so be

multibody/tree/prismatic_joint.cc

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
#include <memory>
44
#include <stdexcept>
55

6-
#include "drake/multibody/tree/multibody_tree.h"
6+
#include "drake/multibody/tree/multibody_tree-inl.h"
77

88
namespace drake {
99
namespace multibody {
@@ -30,8 +30,7 @@ PrismaticJoint<T>::PrismaticJoint(const std::string& name,
3030
const double kEpsilon = std::sqrt(std::numeric_limits<double>::epsilon());
3131
if (axis.isZero(kEpsilon)) {
3232
throw std::logic_error(
33-
"Prismatic joint axis vector must have nonzero "
34-
"length.");
33+
"Prismatic joint axis vector must have nonzero length.");
3534
}
3635
if (damping < 0) {
3736
throw std::logic_error("Prismatic joint damping must be nonnegative.");
@@ -97,6 +96,94 @@ std::unique_ptr<Joint<T>> PrismaticJoint<T>::DoShallowClone() const {
9796
this->position_upper_limit(), this->default_damping());
9897
}
9998

99+
/* For a prismatic joint, we are given Jp on parent P, Jc on child C, and a
100+
translation unit vector â whose components are identical in Jp and Jc. At q=0,
101+
Jp and Jc are coincident. At all times, the coordinate axes of Jp and Jc remain
102+
aligned, and vector â has constant and equal components when expressed in these
103+
frames. Jc translates with respect to Jp by a translation distance q meters
104+
along the translation vector â. The origins Jpo and Jco are thus separated by
105+
a vector q⋅â so that Jco = Jpo + q⋅â.
106+
107+
We need to implement this joint with one of three available prismatic
108+
mobilizers. Every mobilizer has an inboard frame F, and an outboard frame M. The
109+
available prismatic mobilizers translate along one of the coordinate axes x, y,
110+
or z. If â happens already to be a coordinate axis of Jp (and Jc), we are golden
111+
and can use Jp and Jc as F and M. Otherwise we are going to have to create new
112+
frames F and M such that one of their coordinate axes is aligned with â.
113+
114+
To create new frames, we use the MakeFromOneVector() function to create a
115+
RotationMatrix R_JpF (R_JcM) such that F(M)'s z axis is aligned with â (or -â if
116+
the mobilizer is reversed from the joint). That rotation matrix is what we need
117+
to create FixedOffsetFrame F from Jp (or Jc if reversed) and M from Jc (or Jp
118+
if reversed). Then we use the z-axial prismatic mobilizer to implement the
119+
joint. */
120+
template <typename T>
121+
std::unique_ptr<internal::Mobilizer<T>>
122+
PrismaticJoint<T>::MakeMobilizerForJoint(
123+
const internal::SpanningForest::Mobod& mobod,
124+
internal::MultibodyTree<T>* tree) const {
125+
DRAKE_DEMAND(tree != nullptr);
126+
const bool reverse = mobod.is_reversed();
127+
// These are the joint's parent and child frames, but adjusted for
128+
// reversal to locate them on the inboard and outboard bodies. We may also
129+
// need to reverse the axis so that q will retain its expected sign.
130+
const Frame<T>& Jin =
131+
reverse ? this->frame_on_child() : this->frame_on_parent();
132+
const Frame<T>& Jout =
133+
reverse ? this->frame_on_parent() : this->frame_on_child();
134+
const Eigen::Vector3d axis = reverse ? -axis_ : axis_; // a unit vector
135+
136+
// Determine whether the axis is one of +x, +y, +z, or something else.
137+
// In the latter case we'll change this to +z below.
138+
std::optional<int> which_axis = [&axis]() -> std::optional<int> {
139+
for (int i = 0; i < 3; ++i) {
140+
if (axis[i] == 1.0) return i; // exact match only
141+
}
142+
return {};
143+
}();
144+
145+
const Frame<T>* F{};
146+
const Frame<T>* M{};
147+
if (!which_axis) {
148+
which_axis = 2; // Arbitrarily using the z axis mobilizer.
149+
const math::RotationMatrixd R_JinF = // Also R_JoutM, since Jp=Jc at q=0.
150+
math::RotationMatrixd::MakeFromOneUnitVector(axis, *which_axis);
151+
F = &tree->AddEphemeralFrame(std::make_unique<FixedOffsetFrame<T>>(
152+
this->MakeUniqueOffsetFrameName(Jin, "F"), Jin,
153+
math::RigidTransformd(R_JinF), this->model_instance()));
154+
M = &tree->AddEphemeralFrame(std::make_unique<FixedOffsetFrame<T>>(
155+
this->MakeUniqueOffsetFrameName(Jout, "M"), Jout,
156+
math::RigidTransformd(R_JinF), this->model_instance()));
157+
} else {
158+
F = &Jin;
159+
M = &Jout;
160+
}
161+
162+
std::unique_ptr<internal::PrismaticMobilizer<T>> prismatic_mobilizer;
163+
164+
switch (*which_axis) {
165+
case 0:
166+
prismatic_mobilizer =
167+
std::make_unique<internal::PrismaticMobilizerAxial<T, 0>>(mobod, *F,
168+
*M);
169+
break;
170+
case 1:
171+
prismatic_mobilizer =
172+
std::make_unique<internal::PrismaticMobilizerAxial<T, 1>>(mobod, *F,
173+
*M);
174+
break;
175+
case 2:
176+
prismatic_mobilizer =
177+
std::make_unique<internal::PrismaticMobilizerAxial<T, 2>>(mobod, *F,
178+
*M);
179+
break;
180+
}
181+
182+
DRAKE_DEMAND(prismatic_mobilizer != nullptr);
183+
prismatic_mobilizer->set_default_position(this->default_positions());
184+
return prismatic_mobilizer;
185+
}
186+
100187
} // namespace multibody
101188
} // namespace drake
102189

multibody/tree/prismatic_joint.h

Lines changed: 16 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,12 @@ namespace multibody {
1616

1717
/// This Joint allows two bodies to translate relative to one another along a
1818
/// common axis.
19-
/// That is, given a frame F attached to the parent body P and a frame M
20-
/// attached to the child body B (see the Joint class's documentation),
21-
/// this Joint allows frames F and M to translate with respect to each other
19+
/// That is, given a frame Jp attached to the parent body P and a frame Jc
20+
/// attached to the child body C (see the Joint class's documentation),
21+
/// this Joint allows frames Jp and Jc to translate with respect to each other
2222
/// along an axis â. The translation distance is defined positive when child
23-
/// body B translates along the direction of â.
24-
/// Axis â is constant and has the same measures in both frames F and M, that
25-
/// is, `â_F = â_M`.
23+
/// body C translates along the direction of â. Axis vector â is constant and
24+
/// has the same components in both frames Jp and Jc, that is, `â_Jp = â_Jc`.
2625
///
2726
/// @tparam_default_scalar
2827
template <typename T>
@@ -36,19 +35,19 @@ class PrismaticJoint final : public Joint<T> {
3635
static const char kTypeName[];
3736

3837
/// Constructor to create a prismatic joint between two bodies so that
39-
/// frame F attached to the parent body P and frame M attached to the child
40-
/// body B, translate relatively to one another along a common axis. See this
38+
/// frame Jp attached to the parent body P and frame Jc attached to the child
39+
/// body C, translate relatively to one another along a common axis. See this
4140
/// class's documentation for further details on the definition of these
4241
/// frames and translation distance.
4342
/// The first three arguments to this constructor are those of the Joint class
4443
/// constructor. See the Joint class's documentation for details.
4544
/// The additional parameter `axis` is:
4645
/// @param[in] axis
4746
/// A vector in ℝ³ specifying the translation axis for this joint. Given
48-
/// that frame M only translates with respect to F and there is no relative
49-
/// rotation, the measures of `axis` in either frame F or M
50-
/// are exactly the same, that is, `axis_F = axis_M`.
51-
/// This vector can have any length, only the direction is used.
47+
/// that frame Jc only translates with respect to Jp and there is no
48+
/// relative rotation, the components of `axis` in either frame Jp or Jc
49+
/// are exactly the same, that is, `axis_Jp = axis_Jc`. This vector can have
50+
/// any length, only the direction is used.
5251
/// @param[in] pos_lower_limit
5352
/// Lower position limit, in meters, for the translation coordinate
5453
/// (see get_translation()).
@@ -76,9 +75,9 @@ class PrismaticJoint final : public Joint<T> {
7675
const std::string& type_name() const final;
7776

7877
/// Returns the axis of translation for `this` joint as a unit vector.
79-
/// Since the measures of this axis in either frame F or M are the same (see
80-
/// this class's documentation for frame definitions) then,
81-
/// `axis = axis_F = axis_M`.
78+
/// Since the components of this axis in either frame Jp or Jc are the same
79+
/// (see this class's documentation for frame definitions) then,
80+
/// `axis = axis_Jp = axis_Jc`.
8281
const Vector3<double>& translation_axis() const { return axis_; }
8382

8483
/// Returns `this` joint's default damping constant in N⋅s/m.
@@ -243,6 +242,7 @@ class PrismaticJoint final : public Joint<T> {
243242
MultibodyForces<T>* forces) const final {
244243
// Right now we assume all the forces in joint_tau go into a single
245244
// mobilizer.
245+
DRAKE_DEMAND(joint_dof == 0);
246246
Eigen::Ref<VectorX<T>> tau_mob =
247247
get_mobilizer().get_mutable_generalized_forces_from_array(
248248
&forces->mutable_generalized_forces());
@@ -300,16 +300,7 @@ class PrismaticJoint final : public Joint<T> {
300300
// Joint<T> finals:
301301
std::unique_ptr<internal::Mobilizer<T>> MakeMobilizerForJoint(
302302
const internal::SpanningForest::Mobod& mobod,
303-
internal::MultibodyTree<T>*) const final {
304-
const auto [inboard_frame, outboard_frame] =
305-
this->tree_frames(mobod.is_reversed());
306-
// TODO(sherm1) The mobilizer needs to be reversed, not just the frames.
307-
auto prismatic_mobilizer =
308-
std::make_unique<internal::PrismaticMobilizer<T>>(
309-
mobod, *inboard_frame, *outboard_frame, axis_);
310-
prismatic_mobilizer->set_default_position(this->default_positions());
311-
return prismatic_mobilizer;
312-
}
303+
internal::MultibodyTree<T>*) const final;
313304

314305
std::unique_ptr<Joint<double>> DoCloneToScalar(
315306
const internal::MultibodyTree<double>& tree_clone) const final;

0 commit comments

Comments
 (0)