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