mj-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#include "test_utils.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
19#include <algorithm>
20#include <cmath>
21#include <filesystem>
22#include <iomanip>
23#include <iostream>
24#include <memory>
25#include <string>
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 kGripperReach = 0.18422; // bracelet_link -> pad tip [m]
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 fs::path repo_root() { return fs::path(__FILE__).parent_path().parent_path(); }
40
41static double clamp01(double v) { return std::max(0.0, std::min(1.0, v)); }
42
43static void lerp_q(const KDL::JntArray &a, const KDL::JntArray &b, double t, KDL::JntArray &out)
44{
45 for (unsigned i = 0; i < a.rows(); ++i) out(i) = a(i) + t * (b(i) - a(i));
46}
47
48static void
49 impedance_ctrl(mj_kdl::Robot &s, const KDL::JntArray &q_des, unsigned n, KDL::ChainDynParam &dyn)
50{
51 KDL::JntArray q(n), g(n);
52 for (unsigned i = 0; i < n; ++i) q(i) = s.jnt_pos_msr[i];
53 dyn.JntToGravity(q, g);
54 for (unsigned i = 0; i < n; ++i) {
55 s.jnt_trq_cmd[i] =
56 g(i) + kKp[i] * (q_des(i) - s.jnt_pos_msr[i]) - kKd[i] * s.jnt_vel_msr[i];
57 }
58}
59
60class MjcfPickTest : public testing::Test
61{
62 protected:
63 fs::path root_;
64 mjModel *model_ = nullptr;
65 mjData *data_ = nullptr;
66
68 int fingers_act_ = -1;
69 int cube_jnt_ = -1;
70 unsigned n_ = 0;
71
72 std::unique_ptr<KDL::ChainFkSolverPos_recursive> fk_;
73 std::unique_ptr<KDL::ChainDynParam> dyn_;
74 std::unique_ptr<KDL::ChainIkSolverVel_pinv> ik_vel_;
75 std::unique_ptr<KDL::ChainIkSolverPos_NR_JL> ik_;
76
78
79 void SetUp() override
80 {
81 root_ = repo_root();
82 if (!fs::exists(root_ / "third_party/menagerie")) {
83 GTEST_SKIP() << "third_party/menagerie/ not found";
84 return;
85 }
86
87 const std::string arm_mjcf =
88 (root_ / "third_party/menagerie/kinova_gen3/gen3.xml").string();
89 const std::string grp_mjcf =
90 (root_ / "third_party/menagerie/robotiq_2f85/2f85.xml").string();
91
93 gs.mjcf_path = grp_mjcf.c_str();
94 gs.attach_to = "bracelet_link";
95 gs.prefix = "g_";
96 gs.pos[2] = -0.061525;
97 gs.euler[0] = 180.0;
98
100 rs.path = arm_mjcf.c_str();
101 rs.attachments.push_back(gs);
102
104 cube.name = "cube";
106 cube.size[0] = cube.size[1] = cube.size[2] = kCubeHS;
107 cube.pos[0] = kCubeX;
108 cube.pos[1] = kCubeY;
109 cube.pos[2] = kCubeZ;
110 cube.rgba[0] = 1.0f;
111 cube.rgba[1] = 0.5f;
112 cube.rgba[2] = 0.0f;
113 cube.rgba[3] = 1.0f;
114 cube.mass = 0.1;
115 cube.condim = 4;
116 cube.friction[0] = 0.8;
117 cube.friction[1] = 0.02;
118 cube.friction[2] = 0.001;
119
121 sc.robots.push_back(rs);
122 sc.objects.push_back(cube);
123
124 ASSERT_TRUE(mj_kdl::build_scene(&model_, &data_, &sc));
125 ASSERT_TRUE(
127 &s_, model_, data_, "base_link", "bracelet_link", "", "g_base"
128 )
129 );
130
131 n_ = s_.chain.getNrOfJoints();
132 ASSERT_EQ(n_, 7u);
133
134 fingers_act_ = mj_name2id(model_, mjOBJ_ACTUATOR, "g_fingers_actuator");
135 ASSERT_GE(fingers_act_, 0) << "g_fingers_actuator not found";
136 cube_jnt_ = mj_name2id(model_, mjOBJ_JOINT, "cube_joint");
137 ASSERT_GE(cube_jnt_, 0) << "cube_joint not found";
138
139 // IK setup.
140 KDL::JntArray q_min(n_), q_max(n_);
141 for (unsigned i = 0; i < n_; ++i) {
142 int jid = model_->dof_jntid[s_.kdl_to_mj_dof[i]];
143 if (model_->jnt_limited[jid]) {
144 q_min(i) = model_->jnt_range[2 * jid];
145 q_max(i) = model_->jnt_range[2 * jid + 1];
146 } else {
147 q_min(i) = -2 * M_PI;
148 q_max(i) = 2 * M_PI;
149 }
150 }
151 fk_ = std::make_unique<KDL::ChainFkSolverPos_recursive>(s_.chain);
152 dyn_ = std::make_unique<KDL::ChainDynParam>(s_.chain, KDL::Vector(0, 0, -9.81));
153 ik_vel_ = std::make_unique<KDL::ChainIkSolverVel_pinv>(s_.chain);
154 ik_ = std::make_unique<KDL::ChainIkSolverPos_NR_JL>(
155 s_.chain, q_min, q_max, *fk_, *ik_vel_, 500, 1e-5
156 );
157
158 q_home_.resize(n_);
159 for (unsigned i = 0; i < n_; ++i) q_home_(i) = kHomePose[i];
160
161 const double kGraspZ = kCubeZ + kGripperReach + 0.02;
162 const double kPreGraspZ = kGraspZ + 0.20;
163 const double kLiftZ = kGraspZ + 0.30;
164 const KDL::Rotation kDownRot = KDL::Rotation::Identity();
165
166 q_pregrasp_.resize(n_);
167 q_grasp_.resize(n_);
168 q_lift_.resize(n_);
169
170 struct WP
171 {
172 double z;
173 KDL::JntArray *out;
174 const KDL::JntArray *seed;
175 };
176 WP wps[] = {
177 { kPreGraspZ, &q_pregrasp_, &q_home_ },
178 { kGraspZ, &q_grasp_, &q_pregrasp_ },
179 { kLiftZ, &q_lift_, &q_grasp_ },
180 };
181 for (auto &wp : wps) {
182 KDL::Frame tgt(kDownRot, KDL::Vector(kCubeX, kCubeY, wp.z));
183 int ret = ik_->CartToJnt(*wp.seed, tgt, *wp.out);
184 if (ret < 0)
185 TEST_WARN("IK warning: did not converge for z=" << wp.z << " (ret=" << ret << ")");
186 }
187 }
188
189 void TearDown() override
190 {
191 if (model_) {
194 }
195 }
196
197 // Reset arm to home and cube to initial position.
199 {
200 mj_resetData(model_, data_);
202 int qadr = model_->jnt_qposadr[cube_jnt_];
203 data_->qpos[qadr] = kCubeX;
204 data_->qpos[qadr + 1] = kCubeY;
205 data_->qpos[qadr + 2] = kCubeZ;
206 data_->qpos[qadr + 3] = 1.0;
207 data_->qpos[qadr + 4] = data_->qpos[qadr + 5] = data_->qpos[qadr + 6] = 0.0;
208 mj_forward(model_, data_);
210 for (unsigned i = 0; i < n_; ++i) {
211 s_.jnt_pos_cmd[i] = data_->qpos[s_.kdl_to_mj_qpos[i]];
212 s_.jnt_trq_cmd[i] = 0.0;
213 data_->qfrc_applied[s_.kdl_to_mj_dof[i]] = 0.0;
214 }
216 data_->ctrl[fingers_act_] = 0.0;
217 }
218
219 /*
220 * Run an impedance trajectory from q_enter to q_target over duration seconds.
221 * Returns when duration elapses or settle_tol is met (negative = skip pose check).
222 */
224 const KDL::JntArray &q_enter,
225 const KDL::JntArray &q_target,
226 double duration,
227 double timeout,
228 double settle_tol,
229 double gripper_cmd
230 )
231 {
232 double t_enter = data_->time;
233 KDL::JntArray q_des(n_);
234
235 while (true) {
236 mj_kdl::update(&s_); // TORQUE: applies old jnt_trq_cmd, reads sensors
237 double alpha = clamp01((data_->time - t_enter) / duration);
238 lerp_q(q_enter, q_target, alpha, q_des);
239 impedance_ctrl(s_, q_des, n_, *dyn_); // writes jnt_trq_cmd
240 // Apply current torques immediately (same step, no delay).
241 for (unsigned i = 0; i < n_; ++i)
242 data_->qfrc_applied[s_.kdl_to_mj_dof[i]] = s_.jnt_trq_cmd[i];
243 data_->ctrl[fingers_act_] = gripper_cmd;
244 mj_step(model_, data_);
245
246 double t_rel = data_->time - t_enter;
247 bool done_time = t_rel >= duration;
248 bool done_pose = (settle_tol < 0.0);
249 if (!done_pose) {
250 double max_err = 0.0;
251 for (unsigned i = 0; i < n_; ++i)
252 max_err = std::max(max_err, std::abs(q_target(i) - s_.jnt_pos_msr[i]));
253 done_pose = (max_err <= settle_tol);
254 }
255 if ((done_time && done_pose) || t_rel >= timeout) break;
256 }
257 }
258};
259
261{
262 TEST_INFO("arm KDL chain: " << n_ << " joints");
263 EXPECT_EQ(n_, 7u);
264}
265
266TEST_F(MjcfPickTest, IKConvergence)
267{
268 const double kMaxPosErr = 2e-3; // 2 mm
269 const KDL::Rotation kDownRot = KDL::Rotation::Identity();
270
271 const double kGraspZ = kCubeZ + kGripperReach + 0.02;
272 const double kPreGraspZ = kGraspZ + 0.20;
273 const double kLiftZ = kGraspZ + 0.30;
274
275 struct WP
276 {
277 const char *label;
278 double z;
279 const KDL::JntArray *q;
280 };
281 WP wps[] = {
282 { "pre-grasp", kPreGraspZ, &q_pregrasp_ },
283 { "grasp", kGraspZ, &q_grasp_ },
284 { "lift", kLiftZ, &q_lift_ },
285 };
286 for (auto &wp : wps) {
287 KDL::Frame tgt(kDownRot, KDL::Vector(kCubeX, kCubeY, wp.z));
288 KDL::Frame fk_out;
289 fk_->JntToCart(*wp.q, fk_out);
290 double pos_err = (tgt.p - fk_out.p).Norm();
291 TEST_INFO(
292 "IK " << wp.label << " pos error: " << std::fixed << std::setprecision(3)
293 << pos_err * 1000.0 << " mm"
294 );
295 EXPECT_LE(pos_err, kMaxPosErr) << wp.label << " IK error " << pos_err * 1000.0 << " mm";
296 }
297}
298
300{
301 reset_scene(); // sets ctrl_mode = TORQUE
302
303 KDL::JntArray q_enter(n_);
304
305 auto snap = [&]() {
306 for (unsigned i = 0; i < n_; ++i) q_enter(i) = s_.jnt_pos_msr[i];
307 };
308
309 snap();
310 run_phase(q_enter, q_home_, 1.0, 2.5, 0.08, 0.0); // HOME
311 snap();
312 run_phase(q_enter, q_pregrasp_, 2.0, 4.0, 0.08, 0.0); // PREGRASP
313 snap();
314 run_phase(q_enter, q_grasp_, 2.0, 4.0, 0.06, 0.0); // GRASP
315 snap();
316 run_phase(q_enter, q_grasp_, 1.5, 2.5, -1.0, 255.0); // CLOSE
317 snap();
318 run_phase(q_enter, q_lift_, 3.0, 5.0, 0.08, 255.0); // LIFT
319 run_phase(q_lift_, q_lift_, 1.0, 1.0, -1.0, 255.0); // HOLD
320
321 int qadr = model_->jnt_qposadr[cube_jnt_];
322 double cube_z = data_->qpos[qadr + 2];
323 TEST_INFO("cube Z after pick: " << std::fixed << std::setprecision(3) << cube_z << " m");
324 EXPECT_GT(cube_z, 0.20) << "cube was not lifted (z=" << cube_z << " m)";
325}
326
327int main(int argc, char *argv[])
328{
329 testing::InitGoogleTest(&argc, argv);
330 return RUN_ALL_TESTS();
331}
std::unique_ptr< KDL::ChainFkSolverPos_recursive > fk_
KDL::JntArray q_grasp_
std::unique_ptr< KDL::ChainIkSolverVel_pinv > ik_vel_
void TearDown() override
std::unique_ptr< KDL::ChainDynParam > dyn_
KDL::JntArray q_home_
void SetUp() override
KDL::JntArray q_lift_
std::unique_ptr< KDL::ChainIkSolverPos_NR_JL > ik_
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 init_robot_from_mjcf(Robot *r, mjModel *model, mjData *data, const char *base_body, const char *tip_body, const char *prefix="", const char *tool_body=nullptr)
void cleanup(Robot *r)
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
std::vector< RobotSpec > robots
std::vector< SceneObject > objects
int main(int argc, char *argv[])
TEST_F(MjcfPickTest, KDLChain)
#define TEST_WARN(msg)
#define TEST_INFO(msg)