Mujoco KDL Wrapper  0.1.0
MuJoCo + KDL bridge for robot kinematics and dynamics
Loading...
Searching...
No Matches
test_mjcf_pick.cpp
Go to the documentation of this file.
1/* test_mjcf_pick.cpp
2 * Kinova GEN3 (MJCF) + Robotiq 2F-85: scripted pick-and-place of an orange cube.
3 *
4 * Tests:
5 * 1. KDL chain has 7 joints.
6 * 2. IK converges for pre-grasp, grasp, and lift waypoints (pos error < 2 mm each).
7 * 3. Full pick sequence lifts the cube above 0.20 m. */
8
10
11#include <gtest/gtest.h>
12
13#include <kdl/chaindynparam.hpp>
14#include <kdl/chainfksolverpos_recursive.hpp>
15#include <kdl/chainiksolverpos_nr_jl.hpp>
16#include <kdl/chainiksolvervel_pinv.hpp>
17#include <kdl/chainiksolvervel_wdls.hpp>
18
19#include <algorithm>
20#include <cmath>
21#include <filesystem>
22#include <memory>
23#include <string>
24#include <vector>
25
26namespace fs = std::filesystem;
27
28static constexpr double kHomePose[7] = { 0.0, 0.2618, 3.1416, -2.2689, 0.0, 0.9599, 1.5708 };
29static constexpr double kCubeX = 0.4;
30static constexpr double kCubeY = 0.0;
31static constexpr double kCubeHS = 0.02; // cube half-size [m]
32static constexpr double kCubeZ = kCubeHS;
33static constexpr double kIkTol = 2e-3;
34
35static constexpr double kKp[7] = { 100, 200, 100, 200, 100, 200, 100 };
36static constexpr double kKd[7] = { 10, 20, 10, 20, 10, 20, 10 };
37
38static fs::path repo_root() { return fs::path(__FILE__).parent_path().parent_path(); }
39
40static double clamp01(double v) { return std::max(0.0, std::min(1.0, v)); }
41
42static void lerp_q(const KDL::JntArray &a, const KDL::JntArray &b, double t, KDL::JntArray &out)
43{
44 for (unsigned i = 0; i < a.rows(); ++i) out(i) = a(i) + t * (b(i) - a(i));
45}
46
47static double max_abs_joint_delta(const KDL::JntArray &a, const KDL::JntArray &b)
48{
49 double max_delta = 0.0;
50 for (unsigned i = 0; i < a.rows(); ++i) max_delta = std::max(max_delta, std::abs(a(i) - b(i)));
51 return max_delta;
52}
53
54static bool solve_near_seed(
55 KDL::ChainIkSolverVel_wdls &ik_vel,
56 KDL::ChainFkSolverPos_recursive &fk,
57 const KDL::JntArray &seed,
58 const KDL::Frame &target,
59 const std::vector<bool> &joint_limited,
60 const KDL::JntArray &q_min,
61 const KDL::JntArray &q_max,
62 KDL::JntArray &out
63)
64{
65 out = seed;
66 KDL::JntArray dq(out.rows());
67 for (int iter = 0; iter < 300; ++iter) {
68 KDL::Frame fk_out;
69 fk.JntToCart(out, fk_out);
70 KDL::Twist dx = KDL::diff(fk_out, target);
71 if (dx.vel.Norm() <= kIkTol && dx.rot.Norm() <= 2e-2) return true;
72
73 double vel_norm = dx.vel.Norm();
74 if (vel_norm > 0.05) dx.vel = dx.vel * (0.05 / vel_norm);
75 double rot_norm = dx.rot.Norm();
76 if (rot_norm > 0.20) dx.rot = dx.rot * (0.20 / rot_norm);
77
78 if (ik_vel.CartToJnt(out, dx, dq) < 0) return false;
79 for (unsigned i = 0; i < out.rows(); ++i) {
80 out(i) += dq(i);
81 if (joint_limited[i]) out(i) = std::max(q_min(i), std::min(q_max(i), out(i)));
82 }
83 }
84
85 KDL::Frame fk_out;
86 fk.JntToCart(out, fk_out);
87 KDL::Twist dx = KDL::diff(fk_out, target);
88 return dx.vel.Norm() <= kIkTol && dx.rot.Norm() <= 2e-2;
89}
90
91static void
92 impedance_ctrl(mj_kdl::Robot &s, const KDL::JntArray &q_des, unsigned n, KDL::ChainDynParam &dyn)
93{
94 KDL::JntArray q(n), g(n);
95 for (unsigned i = 0; i < n; ++i) q(i) = s.jnt_pos_msr[i];
96 dyn.JntToGravity(q, g);
97 for (unsigned i = 0; i < n; ++i) {
98 s.jnt_trq_cmd[i] =
99 g(i) + kKp[i] * (q_des(i) - s.jnt_pos_msr[i]) - kKd[i] * s.jnt_vel_msr[i];
100 }
101}
102
103class MjcfPickTest : public testing::Test
104{
105 protected:
106 fs::path root_;
107 mjModel *model_ = nullptr;
108 mjData *data_ = nullptr;
109
111 int fingers_act_ = -1;
112 int cube_jnt_ = -1;
113 unsigned n_ = 0;
114
115 std::unique_ptr<KDL::ChainFkSolverPos_recursive> fk_;
116 std::unique_ptr<KDL::ChainDynParam> dyn_;
117 std::unique_ptr<KDL::ChainIkSolverVel_wdls> ik_vel_;
118
120
121 void SetUp() override
122 {
123 root_ = repo_root();
124 const std::string arm_mjcf =
125 (root_ / "third_party/menagerie/kinova_gen3/gen3.xml").string();
126 const std::string grp_mjcf =
127 (root_ / "third_party/menagerie/robotiq_2f85/2f85.xml").string();
128 if (!fs::exists(arm_mjcf)) {
129 GTEST_SKIP() << arm_mjcf << " not found";
130 return;
131 }
132 if (!fs::exists(grp_mjcf)) {
133 GTEST_SKIP() << grp_mjcf << " not found";
134 return;
135 }
136
138 .mjcf_path = grp_mjcf.c_str(),
139 .attach_to = "bracelet_link",
140 .prefix = "g_",
141 .pos = { 0.0, 0.0, -0.061525 },
142 .euler = { 180.0, 0.0, 0.0 },
143 .contact_exclusions = {},
144 };
146 rs.path = arm_mjcf.c_str();
147 rs.attachments.push_back(gs);
148
150 .name = "cube",
151 .mjcf_path = "",
152 .shape = mj_kdl::Shape::BOX,
153 .size = { kCubeHS, kCubeHS, kCubeHS },
154 .pos = { kCubeX, kCubeY, kCubeZ },
155 .rgba = { 1.0f, 0.5f, 0.0f, 1.0f },
156 .mass = 0.1,
157 .condim = 4,
158 .friction = { 0.8, 0.02, 0.001 },
159 };
160
162 sc.robots.push_back(rs);
163 sc.objects.push_back(cube);
164
165 ASSERT_TRUE(mj_kdl::build_scene(&model_, &data_, &sc));
166 const mj_kdl::ToolFrameSpec tool{ .tool_body = "g_base", .tcp_site = "g_pinch" };
167 ASSERT_TRUE(
168 mj_kdl::init_robot_from_mjcf(&s_, model_, data_, "base_link", "bracelet_link", "", &tool)
169 );
170
171 n_ = s_.chain.getNrOfJoints();
172 ASSERT_EQ(n_, 7u);
173
174 fingers_act_ = mj_name2id(model_, mjOBJ_ACTUATOR, "g_fingers_actuator");
175 ASSERT_GE(fingers_act_, 0) << "g_fingers_actuator not found";
176 cube_jnt_ = mj_name2id(model_, mjOBJ_JOINT, "cube_joint");
177 ASSERT_GE(cube_jnt_, 0) << "cube_joint not found";
178 ASSERT_GE(mj_name2id(model_, mjOBJ_SITE, "g_pinch"), 0) << "g_pinch site not found";
179
180 // IK setup.
181 KDL::JntArray q_min(n_), q_max(n_);
182 std::vector<bool> joint_limited(n_, false);
183 for (unsigned i = 0; i < n_; ++i) {
184 int jid = model_->dof_jntid[s_.kdl_to_mj_dof[i]];
185 if (model_->jnt_limited[jid]) {
186 joint_limited[i] = true;
187 q_min(i) = model_->jnt_range[2 * jid];
188 q_max(i) = model_->jnt_range[2 * jid + 1];
189 } else {
190 q_min(i) = -2 * M_PI;
191 q_max(i) = 2 * M_PI;
192 }
193 }
194 fk_ = std::make_unique<KDL::ChainFkSolverPos_recursive>(s_.chain);
195 dyn_ = std::make_unique<KDL::ChainDynParam>(s_.chain, KDL::Vector(0, 0, -9.81));
196 ik_vel_ = std::make_unique<KDL::ChainIkSolverVel_wdls>(s_.chain, 1e-5, 150);
197 ik_vel_->setLambda(0.05);
198
199 q_home_.resize(n_);
200 for (unsigned i = 0; i < n_; ++i) q_home_(i) = kHomePose[i];
201
202 const double kGraspZ = kCubeZ;
203 const double kPreGraspZ = kGraspZ + 0.20;
204 const double kLiftZ = kGraspZ + 0.30;
205 const KDL::Rotation kGraspRot = s_.tip_T_tcp.M;
206
207 q_pregrasp_.resize(n_);
208 q_grasp_.resize(n_);
209 q_lift_.resize(n_);
210
211 struct WP
212 {
213 double z;
214 KDL::JntArray *out;
215 const KDL::JntArray *seed;
216 };
217 WP wps[] = {
218 { kPreGraspZ, &q_pregrasp_, &q_home_ },
219 { kGraspZ, &q_grasp_, &q_pregrasp_ },
220 { kLiftZ, &q_lift_, &q_grasp_ },
221 };
222 for (auto &wp : wps) {
223 KDL::Frame target(kGraspRot, KDL::Vector(kCubeX, kCubeY, wp.z));
224 ASSERT_TRUE(solve_near_seed(
225 *ik_vel_, *fk_, *wp.seed, target, joint_limited, q_min, q_max, *wp.out
226 )) << "IK failed for z="
227 << wp.z;
228 }
229 }
230
231 void TearDown() override
232 {
233 if (model_) {
236 }
237 }
238
239 // Reset arm to home and cube to initial position.
241 {
242 mj_resetData(model_, data_);
244 int qadr = model_->jnt_qposadr[cube_jnt_];
245 data_->qpos[qadr] = kCubeX;
246 data_->qpos[qadr + 1] = kCubeY;
247 data_->qpos[qadr + 2] = kCubeZ;
248 data_->qpos[qadr + 3] = 1.0;
249 data_->qpos[qadr + 4] = data_->qpos[qadr + 5] = data_->qpos[qadr + 6] = 0.0;
250 mj_forward(model_, data_);
252 for (unsigned i = 0; i < n_; ++i) {
253 s_.jnt_pos_cmd[i] = data_->qpos[s_.kdl_to_mj_qpos[i]];
254 s_.jnt_trq_cmd[i] = 0.0;
255 data_->qfrc_applied[s_.kdl_to_mj_dof[i]] = 0.0;
256 }
258 data_->ctrl[fingers_act_] = 0.0;
259 }
260
261 /*
262 * Run an impedance trajectory from q_enter to q_target over duration seconds.
263 * Returns when duration elapses or settle_tol is met (negative = skip pose check).
264 */
266 const KDL::JntArray &q_enter,
267 const KDL::JntArray &q_target,
268 double duration,
269 double timeout,
270 double settle_tol,
271 double gripper_cmd
272 )
273 {
274 double t_enter = data_->time;
275 KDL::JntArray q_des(n_);
276
277 while (true) {
278 mj_kdl::update(&s_); // read current sensors
279 double alpha = clamp01((data_->time - t_enter) / duration);
280 lerp_q(q_enter, q_target, alpha, q_des);
281 impedance_ctrl(s_, q_des, n_, *dyn_); // writes jnt_trq_cmd
282 data_->ctrl[fingers_act_] = gripper_cmd;
283 mj_kdl::update(&s_); // apply current command through the wrapper
285
286 double t_rel = data_->time - t_enter;
287 bool done_time = t_rel >= duration;
288 bool done_pose = (settle_tol < 0.0);
289 if (!done_pose) {
290 double max_err = 0.0;
291 for (unsigned i = 0; i < n_; ++i)
292 max_err = std::max(max_err, std::abs(q_target(i) - s_.jnt_pos_msr[i]));
293 done_pose = (max_err <= settle_tol);
294 }
295 if ((done_time && done_pose) || t_rel >= timeout) break;
296 }
297 }
298};
299
301{
302 EXPECT_EQ(n_, 7u);
303
304 mj_kdl::Robot wrist;
305 const mj_kdl::ToolFrameSpec wrist_tool{ .tool_body = "g_base" };
306 ASSERT_TRUE(
308 &wrist, model_, data_, "base_link", "bracelet_link", "", &wrist_tool
309 )
310 );
311 EXPECT_EQ(wrist.chain.getNrOfJoints(), s_.chain.getNrOfJoints());
312
313 KDL::ChainFkSolverPos_recursive wrist_fk(wrist.chain);
314 KDL::Frame wrist_frame, tcp_frame;
315 ASSERT_GE(wrist_fk.JntToCart(q_home_, wrist_frame), 0);
316 ASSERT_GE(fk_->JntToCart(q_home_, tcp_frame), 0);
317 EXPECT_GT((tcp_frame.p - wrist_frame.p).Norm(), 0.05);
318 mj_kdl::cleanup(&wrist);
319}
320
321TEST_F(MjcfPickTest, IKConvergence)
322{
323 const KDL::Rotation kGraspRot = s_.tip_T_tcp.M;
324
325 const double kGraspZ = kCubeZ;
326 const double kPreGraspZ = kGraspZ + 0.20;
327 const double kLiftZ = kGraspZ + 0.30;
328
329 struct WP
330 {
331 const char *label;
332 double z;
333 const KDL::JntArray *q;
334 const KDL::JntArray *seed;
335 };
336 WP wps[] = {
337 { "pre-grasp", kPreGraspZ, &q_pregrasp_, &q_home_ },
338 { "grasp", kGraspZ, &q_grasp_, &q_pregrasp_ },
339 { "lift", kLiftZ, &q_lift_, &q_grasp_ },
340 };
341 for (auto &wp : wps) {
342 KDL::Frame tgt(kGraspRot, KDL::Vector(kCubeX, kCubeY, wp.z));
343 KDL::Frame fk_out;
344 fk_->JntToCart(*wp.q, fk_out);
345 double pos_err = (tgt.p - fk_out.p).Norm();
346 double joint_delta = max_abs_joint_delta(*wp.seed, *wp.q);
347 EXPECT_LE(pos_err, kIkTol) << wp.label << " IK error " << pos_err * 1000.0 << " mm";
348 EXPECT_LT(joint_delta, 4.0) << wp.label << " IK jumped to a distant joint branch";
349 }
350}
351
353{
354 reset_scene(); // sets ctrl_mode = TORQUE
355
356 KDL::JntArray q_enter(n_);
357
358 auto snap = [&]() {
359 for (unsigned i = 0; i < n_; ++i) q_enter(i) = s_.jnt_pos_msr[i];
360 };
361
362 snap();
363 run_phase(q_enter, q_home_, 1.0, 2.5, 0.08, 0.0); // HOME
364 snap();
365 run_phase(q_enter, q_pregrasp_, 5.0, 7.0, 0.08, 0.0); // PREGRASP
366 snap();
367 run_phase(q_enter, q_grasp_, 5.0, 8.0, 0.03, 0.0); // GRASP
368 snap();
369 run_phase(q_enter, q_grasp_, 1.5, 2.5, -1.0, 255.0); // CLOSE
370 snap();
371 run_phase(q_enter, q_lift_, 3.0, 5.0, 0.08, 255.0); // LIFT
372 run_phase(q_lift_, q_lift_, 1.0, 1.0, -1.0, 255.0); // HOLD
373
374 int qadr = model_->jnt_qposadr[cube_jnt_];
375 double cube_z = data_->qpos[qadr + 2];
376 EXPECT_GT(cube_z, 0.20) << "cube was not lifted (z=" << cube_z << " m)";
377}
378
379int main(int argc, char *argv[])
380{
381 testing::InitGoogleTest(&argc, argv);
382 return RUN_ALL_TESTS();
383}
std::unique_ptr< KDL::ChainFkSolverPos_recursive > fk_
KDL::JntArray q_grasp_
void TearDown() override
std::unique_ptr< KDL::ChainIkSolverVel_wdls > ik_vel_
std::unique_ptr< KDL::ChainDynParam > dyn_
KDL::JntArray q_home_
void SetUp() override
KDL::JntArray q_lift_
KDL::JntArray q_pregrasp_
void run_phase(const KDL::JntArray &q_enter, const KDL::JntArray &q_target, double duration, double timeout, double settle_tol, double gripper_cmd)
mj_kdl::Robot s_
bool step(Robot *s)
void cleanup(Robot *r)
bool init_robot_from_mjcf(Robot *r, mjModel *model, mjData *data, const char *base_body, const char *tip_body, const char *prefix="", const ToolFrameSpec *tool=nullptr)
void set_joint_pos(Robot *r, const KDL::JntArray &q, bool call_forward=true)
void update(Robot *r)
bool build_scene(mjModel **out_model, mjData **out_data, const SceneSpec *spec)
void destroy_scene(mjModel *model, mjData *data)
std::vector< AttachmentSpec > attachments
std::vector< int > kdl_to_mj_qpos
std::vector< int > kdl_to_mj_dof
std::vector< double > jnt_pos_msr
std::vector< double > jnt_pos_cmd
std::vector< double > jnt_vel_msr
std::vector< double > jnt_trq_cmd
KDL::Frame tip_T_tcp
std::vector< RobotSpec > robots
std::vector< SceneObject > objects
int main(int argc, char *argv[])
TEST_F(MjcfPickTest, KDLChain)