Mujoco KDL Wrapper  0.1.0
MuJoCo + KDL bridge for robot kinematics and dynamics
Loading...
Searching...
No Matches
ex_table_pour.cpp
Go to the documentation of this file.
1/* ex_table_pour.cpp
2 * Kinova GEN3 + Robotiq 2F-85 pours small balls from a small attached bottle into a
3 * transparent tabletop receiver.
4 *
5 * Usage:
6 * ex_table_pour [--headless] [--record output.mp4]
7 *
8 * With --headless runs the full pour sequence and prints how many balls ended
9 * in the receiver. */
10
12
13#include <kdl/chaindynparam.hpp>
14#include <kdl/chainfksolverpos_recursive.hpp>
15#include <kdl/chainiksolverpos_lma.hpp>
16#include <kdl/chainiksolverpos_nr_jl.hpp>
17#include <kdl/chainiksolvervel_pinv.hpp>
18
19#include <algorithm>
20#include <array>
21#include <cmath>
22#include <filesystem>
23#include <iomanip>
24#include <iostream>
25#include <string>
26#include <vector>
27
28namespace fs = std::filesystem;
29
30static constexpr double kHomePose[7] = { 0.0, 0.2618, 3.1416, -2.2689, 0.0, 0.9599, 1.5708 };
31static constexpr double kTableZ = 0.70;
32static constexpr double kRobotBackX = -0.26;
33static constexpr double kJugX = 0.30;
34static constexpr double kJugY = 0.14;
35static constexpr double kRetreatX = kJugX - 0.08;
36static constexpr double kRetreatY = kJugY - 0.08;
37static constexpr double kJugRadius = 0.028;
38static constexpr double kJugHeight = 0.084;
39static constexpr int kNumBallsGui = 36;
40static constexpr int kNumBallsHeadless = kNumBallsGui;
41static constexpr double kBallRadius = 0.007;
42static constexpr double kReceiverFrameZ = kTableZ;
43static constexpr double kIkTol = 3e-3;
44static constexpr double kPourTiltRad = 1.95;
45static constexpr double kTiltOutletZ = kTableZ + 0.18;
46
47static constexpr double kKp[7] = { 120, 220, 120, 220, 110, 190, 90 };
48static constexpr double kKd[7] = { 12, 22, 12, 22, 11, 18, 9 };
49
50static fs::path repo_root() { return fs::path(__FILE__).parent_path().parent_path().parent_path(); }
51static double clamp01(double v) { return std::max(0.0, std::min(1.0, v)); }
52
53static void lerp_q(const KDL::JntArray &a, const KDL::JntArray &b, double t, KDL::JntArray &out)
54{
55 for (unsigned i = 0; i < a.rows(); ++i) out(i) = a(i) + t * (b(i) - a(i));
56}
57
58static void snapshot_q(const mj_kdl::Robot &robot, unsigned n, KDL::JntArray &q)
59{
60 for (unsigned i = 0; i < n; ++i) q(i) = robot.jnt_pos_msr[i];
61}
62
63static double max_abs_joint_err(const mj_kdl::Robot &robot, const KDL::JntArray &q, unsigned n)
64{
65 double max_err = 0.0;
66 for (unsigned i = 0; i < n; ++i)
67 max_err = std::max(max_err, std::abs(q(i) - robot.jnt_pos_msr[i]));
68 return max_err;
69}
70
71static void impedance_ctrl(
72 mj_kdl::Robot &robot,
73 const KDL::JntArray &q_des,
74 unsigned n,
75 KDL::ChainDynParam &dyn
76)
77{
78 KDL::JntArray q(n), g(n);
79 for (unsigned i = 0; i < n; ++i) q(i) = robot.jnt_pos_msr[i];
80 dyn.JntToGravity(q, g);
81 for (unsigned i = 0; i < n; ++i) {
82 robot.jnt_trq_cmd[i] =
83 g(i) + kKp[i] * (q_des(i) - robot.jnt_pos_msr[i]) - kKd[i] * robot.jnt_vel_msr[i];
84 }
85}
86
87static mj_kdl::SceneObject make_ball(int idx)
88{
89 char name[32];
90 std::snprintf(name, sizeof(name), "grain_%02d", idx);
91 return {
92 .name = name,
93 .mjcf_path = "",
94 .shape = mj_kdl::Shape::SPHERE,
95 .size = { kBallRadius, 0.0, 0.0 },
96 .pos = { 0.0, 0.0, kTableZ + 0.40 + idx * 2.0 * kBallRadius },
97 .rgba = { 1.0f, 0.84f, 0.30f, 1.0f },
98 .mass = 0.006,
99 .condim = 4,
100 .friction = { 0.5, 0.02, 0.001 },
101 };
102}
103
104static bool inside_jug(const mjData *data, const mjModel *model, int joint_id)
105{
106 const int qadr = model->jnt_qposadr[joint_id];
107 const double *p = data->qpos + qadr;
108 return std::abs(p[0] - kJugX) < (kJugRadius - 0.012)
109 && std::abs(p[1] - kJugY) < (kJugRadius - 0.012) && p[2] > kTableZ + 0.006
110 && p[2] < kTableZ + kJugHeight + 0.04;
111}
112
113struct Phase
114{
115 const char *name;
116 const KDL::JntArray *target;
117 double duration;
118 double timeout;
119 double settle_tol;
120 double gripper_cmd;
121};
122
123int main(int argc, char *argv[])
124{
125 bool headless = false;
126 bool do_record = false;
127 std::string record_path = "table_pour.mp4";
128 for (int i = 1; i < argc; ++i) {
129 const std::string arg = argv[i];
130 if (arg == "--headless") {
131 headless = true;
132 } else if (arg == "--record") {
133 do_record = true;
134 headless = true;
135 if (i + 1 < argc && argv[i + 1][0] != '-') record_path = argv[++i];
136 }
137 }
138 const int num_balls = headless ? kNumBallsHeadless : kNumBallsGui;
139
140 const fs::path root = repo_root();
141 if (!fs::exists(root / "third_party/menagerie")) {
142 std::cerr << "third_party/menagerie/ not found - run:\n"
143 " cmake -B build -DFETCH_MENAGERIE=ON\n";
144 return 1;
145 }
146
147 const std::string arm_mjcf = (root / "third_party/menagerie/kinova_gen3/gen3.xml").string();
148 const std::string grp_mjcf = (root / "third_party/menagerie/robotiq_2f85/2f85.xml").string();
149 const std::string bottle_mjcf = (root / "src/examples/assets/mug.xml").string();
150 const std::string receiver_mjcf = (root / "src/examples/assets/mug_table.xml").string();
151 const std::string table_mjcf = (root / "src/examples/assets/table.xml").string();
152
154 gripper.mjcf_path = grp_mjcf.c_str();
155 gripper.attach_to = "bracelet_link";
156 gripper.prefix = "g_";
157 gripper.pos[2] = -0.061525;
158 gripper.euler[0] = 180.0;
159
161 bottle.mjcf_path = bottle_mjcf.c_str();
162 bottle.attach_to = "g_base";
163 bottle.prefix = "pour_";
164 bottle.pos[0] = 0.0;
165 bottle.pos[1] = 0.0;
166 bottle.pos[2] = 0.0;
167
168 mj_kdl::RobotSpec robot_spec;
169 robot_spec.path = arm_mjcf.c_str();
170 robot_spec.pos[0] = kRobotBackX;
171 robot_spec.pos[2] = kTableZ;
172 robot_spec.attachments.push_back(gripper);
173 robot_spec.attachments.push_back(bottle);
174
175 mj_kdl::SceneSpec scene_cfg;
177 .name = "table",
178 .mjcf_path = table_mjcf,
179 .pos = { 0.0, 0.0, kTableZ },
180 .fixed = true,
181 };
182 scene_cfg.objects.push_back(table);
183 for (int i = 0; i < num_balls; ++i) scene_cfg.objects.push_back(make_ball(i));
184 scene_cfg.objects.push_back(mj_kdl::SceneObject{
185 .name = "recv",
186 .mjcf_path = receiver_mjcf,
187 .pos = { kJugX, kJugY, kReceiverFrameZ },
188 });
189 scene_cfg.robots.push_back(robot_spec);
190
191 mjModel *model = nullptr;
192 mjData *data = nullptr;
193 if (!mj_kdl::build_scene(&model, &data, &scene_cfg)) {
194 std::cerr << "build_scene() failed\n";
195 return 1;
196 }
197
198 KDL::Frame world_T_table_top;
199 const std::string table_top_site = mj_kdl::scene_object_site_name(table, "table_top");
200 if (!mj_kdl::get_site_frame(model, data, table_top_site.c_str(), &world_T_table_top)) {
201 std::cerr << "table_top site not found\n";
202 mj_kdl::destroy_scene(model, data);
203 return 1;
204 }
205
207 tool.tool_body = "g_base";
208 tool.tcp_site = "g_pinch";
209
210 mj_kdl::Robot robot;
212 &robot, model, data, "base_link", "bracelet_link", "", &tool
213 )) {
214 std::cerr << "init_robot_from_mjcf() failed\n";
215 mj_kdl::destroy_scene(model, data);
216 return 1;
217 }
218
219 const unsigned n = robot.chain.getNrOfJoints();
220 const int fingers_act = mj_name2id(model, mjOBJ_ACTUATOR, "g_fingers_actuator");
221 if (fingers_act < 0) {
222 std::cerr << "g_fingers_actuator not found\n";
223 mj_kdl::cleanup(&robot);
224 mj_kdl::destroy_scene(model, data);
225 return 1;
226 }
227
228 KDL::JntArray q_home(n);
229 for (unsigned i = 0; i < n; ++i) q_home(i) = kHomePose[i];
230
231 KDL::ChainFkSolverPos_recursive fk(robot.chain);
232 KDL::JntArray q_min(n), q_max(n);
233 for (unsigned i = 0; i < n; ++i) {
234 int jid = model->dof_jntid[robot.kdl_to_mj_dof[i]];
235 if (model->jnt_limited[jid]) {
236 q_min(i) = model->jnt_range[2 * jid];
237 q_max(i) = model->jnt_range[2 * jid + 1];
238 } else {
239 q_min(i) = -2 * M_PI;
240 q_max(i) = 2 * M_PI;
241 }
242 }
243 KDL::ChainIkSolverVel_pinv ik_vel(robot.chain);
244 KDL::ChainIkSolverPos_NR_JL ik_nr(robot.chain, q_min, q_max, fk, ik_vel, 2000, 1e-5);
245 KDL::ChainIkSolverPos_LMA ik_lma(robot.chain, 1e-5, 2000);
246 KDL::ChainDynParam dyn(robot.chain, KDL::Vector(0.0, 0.0, scene_cfg.gravity_z));
247
248 KDL::Frame home_fk;
249 fk.JntToCart(q_home, home_fk);
250 const KDL::Rotation carry_tcp = home_fk.M * KDL::Rotation::RotY(-0.05);
251
252 KDL::JntArray q_pre_pour(n), q_pour(n), q_tilt(n), q_retreat(n);
253 struct Waypoint
254 {
255 const char *name;
256 KDL::Frame target;
257 KDL::JntArray *out;
258 const KDL::JntArray *seed;
259 };
260 const KDL::Frame world_T_base(
261 KDL::Rotation::Identity(), KDL::Vector(kRobotBackX, 0.0, kTableZ)
262 );
263 const KDL::Frame base_T_world = world_T_base.Inverse();
264
265 mj_kdl::set_joint_pos(&robot, q_home, false);
266 KDL::Frame world_T_outlet, world_T_tcp;
267 mj_kdl::get_site_frame(model, data, "pour_outlet", &world_T_outlet);
268 mj_kdl::get_site_frame(model, data, "g_pinch", &world_T_tcp);
269 const KDL::Vector tcp_outlet = world_T_tcp.Inverse() * world_T_outlet.p;
270
271 const auto outlet_target_to_tcp_target =
272 [&](const KDL::Rotation &tcp_rot, const KDL::Vector &outlet_pos) {
273 return KDL::Frame(tcp_rot, outlet_pos - tcp_rot * tcp_outlet);
274 };
275
276 std::array<KDL::Vector, 3> waypoint_pos = {
277 KDL::Vector(kJugX, kJugY, kTableZ + 0.27),
278 KDL::Vector(kJugX, kJugY, kTableZ + 0.20),
279 KDL::Vector(kRetreatX, kRetreatY, kTableZ + 0.27),
280 };
281 const auto solve_waypoints = [&](const std::array<KDL::Vector, 3> &pos) {
282 Waypoint waypoints[] = {
283 { "pre-pour",
284 base_T_world * outlet_target_to_tcp_target(carry_tcp, pos[0]),
285 &q_pre_pour,
286 &q_home },
287 { "pour",
288 base_T_world * outlet_target_to_tcp_target(carry_tcp, pos[1]),
289 &q_pour,
290 &q_pre_pour },
291 { "retreat",
292 base_T_world * outlet_target_to_tcp_target(carry_tcp, pos[2]),
293 &q_retreat,
294 &q_pour },
295 };
296 for (const auto &wp : waypoints) {
297 bool ok = ik_nr.CartToJnt(*wp.seed, wp.target, *wp.out) >= 0;
298 if (!ok) ok = ik_lma.CartToJnt(*wp.seed, wp.target, *wp.out) >= 0;
299 if (!ok) {
300 std::cerr << "IK failed for " << wp.name << "\n";
301 return false;
302 }
303 KDL::Frame fk_out;
304 fk.JntToCart(*wp.out, fk_out);
305 if ((wp.target.p - fk_out.p).Norm() > kIkTol) {
306 std::cerr << "IK pose error for " << wp.name << "\n";
307 return false;
308 }
309 }
310 return true;
311 };
312 if (!solve_waypoints(waypoint_pos)) {
313 mj_kdl::cleanup(&robot);
314 mj_kdl::destroy_scene(model, data);
315 return 1;
316 }
317 q_tilt = q_pour;
318 q_tilt(n - 1) += kPourTiltRad;
319 for (int iter = 0; iter < 4; ++iter) {
320 mj_kdl::set_joint_pos(&robot, q_tilt, false);
321 mj_kdl::get_site_frame(model, data, "pour_outlet", &world_T_outlet);
322 const double dx = kJugX - world_T_outlet.p.x();
323 const double dy = kJugY - world_T_outlet.p.y();
324 const double dz = kTiltOutletZ - world_T_outlet.p.z();
325 if (std::sqrt(dx * dx + dy * dy + dz * dz) < 5e-3) break;
326
327 waypoint_pos[1][0] += dx;
328 waypoint_pos[1][1] += dy;
329 waypoint_pos[1][2] += dz;
330 if (!solve_waypoints(waypoint_pos)) {
331 mj_kdl::cleanup(&robot);
332 mj_kdl::destroy_scene(model, data);
333 return 1;
334 }
335 q_tilt = q_pour;
336 q_tilt(n - 1) += kPourTiltRad;
337 }
338
339 std::vector<int> grain_joints;
340 grain_joints.reserve(num_balls);
341 for (int i = 0; i < num_balls; ++i) {
342 char name[32];
343 std::snprintf(name, sizeof(name), "grain_%02d_joint", i);
344 int jid = mj_name2id(model, mjOBJ_JOINT, name);
345 if (jid >= 0) grain_joints.push_back(jid);
346 }
347
349
350 mj_kdl::Env env;
351 env.model = model;
352 env.data = data;
353 mj_kdl::env_add_robot(&env, &robot);
354
355 /* Scene-specific reset: place balls inside bottle and close gripper.
356 * Env::on_reset runs after mj_resetData and before final mj_forward/robot sync. */
357 env.on_reset = [&](mj_kdl::ResetContext *ctx) {
358 mjModel *m = ctx->model;
359 mjData *d = ctx->data;
360 mj_kdl::set_joint_pos(&robot, q_home, false);
361
362 KDL::Frame world_T_center;
363 mj_kdl::get_site_frame(m, d, "pour_center", &world_T_center);
364
365 const double spacing = 2.00 * kBallRadius;
366 for (int i = 0; i < num_balls; ++i) {
367 const int layer = i / 9;
368 const int slot = i % 9;
369 const double ix = static_cast<double>(slot % 3) - 1.0;
370 const double iy = static_cast<double>(slot / 3) - 1.0;
371 KDL::Vector world_v =
372 world_T_center * KDL::Vector(ix * spacing, iy * spacing, -0.026 + layer * spacing);
373 const double world[3] = { world_v.x(), world_v.y(), world_v.z() };
374 char body_name[32];
375 std::snprintf(body_name, sizeof(body_name), "grain_%02d", i);
376 mj_kdl::set_body_pose(m, d, body_name, world);
377 }
378 d->ctrl[fingers_act] = 255.0;
379 };
380
381 double prev_sim_time = 0.0;
382 bool restart = false;
383 bool aborted = false;
384
385 auto reset_scene = [&]() {
386 mj_kdl::reset(&env);
387 prev_sim_time = data->time;
388 restart = true;
389 };
390
391 const std::vector<Phase> phases = {
392 { .name = "HOME",
393 .target = &q_home,
394 .duration = 1.0,
395 .timeout = 2.5,
396 .settle_tol = 0.08,
397 .gripper_cmd = 255.0 },
398 { .name = "PRE_POUR",
399 .target = &q_pre_pour,
400 .duration = 4.0,
401 .timeout = 6.5,
402 .settle_tol = 0.08,
403 .gripper_cmd = 255.0 },
404 { .name = "POUR",
405 .target = &q_pour,
406 .duration = 3.5,
407 .timeout = 5.5,
408 .settle_tol = 0.07,
409 .gripper_cmd = 255.0 },
410 { .name = "TILT",
411 .target = &q_tilt,
412 .duration = 7.0,
413 .timeout = 10.0,
414 .settle_tol = 0.07,
415 .gripper_cmd = 255.0 },
416 { .name = "POUR_HOLD",
417 .target = &q_tilt,
418 .duration = headless ? 9.0 : 10.0,
419 .timeout = headless ? 10.0 : 11.0,
420 .settle_tol = -1.0,
421 .gripper_cmd = 255.0 },
422 { .name = "RETREAT",
423 .target = &q_retreat,
424 .duration = 2.0,
425 .timeout = 4.0,
426 .settle_tol = 0.08,
427 .gripper_cmd = 255.0 },
428 { .name = "HOLD",
429 .target = &q_retreat,
430 .duration = headless ? 1.0 : 1e9,
431 .timeout = headless ? 1.0 : 1e9,
432 .settle_tol = -1.0,
433 .gripper_cmd = 255.0 },
434 };
435
436 mj_kdl::VideoRecorder recorder;
437 bool recorder_ok = false;
438 const int kRecordFps = 60;
439 const int steps_per_frame =
440 std::max(1, static_cast<int>(1.0 / (kRecordFps * model->opt.timestep)));
441 int sim_step = 0;
442 if (do_record) {
444 &recorder, model, record_path.c_str(), mj_kdl::VideoResolution::R1080p, kRecordFps
445 )) {
446 std::cerr << "init_video_recorder() failed -- is EGL available and ffmpeg installed?\n";
447 mj_kdl::cleanup(&robot);
448 mj_kdl::destroy_scene(model, data);
449 return 1;
450 }
451 recorder.cam.azimuth = 145.0;
452 recorder.cam.elevation = -22.0;
453 recorder.cam.distance = 1.35;
454 recorder.cam.lookat[0] = 0.05;
455 recorder.cam.lookat[1] = 0.02;
456 recorder.cam.lookat[2] = 0.88;
457 recorder_ok = true;
458 }
459
460 mj_kdl::Viewer viewer;
461 if (!headless && !mj_kdl::init_window_sim(&viewer, &robot)) {
462 std::cerr << "init_window_sim() failed\n";
463 mj_kdl::cleanup(&robot);
464 mj_kdl::destroy_scene(model, data);
465 return 1;
466 }
467
468 reset_scene();
469
470 KDL::JntArray q_enter(n), q_des(n);
471 do {
472 restart = false;
473 for (const Phase &phase : phases) {
474 if (restart) break;
475 std::cout << "State: " << phase.name << "\n";
476 const double t_enter = data->time;
477 snapshot_q(robot, n, q_enter);
478 while (true) {
479 if (data->time < prev_sim_time - 1e-6) {
480 reset_scene();
481 break;
482 }
483 prev_sim_time = data->time;
484
485 mj_kdl::update(&robot);
486 const double alpha =
487 phase.duration > 0.0 ? clamp01((data->time - t_enter) / phase.duration) : 1.0;
488 lerp_q(q_enter, *phase.target, alpha, q_des);
489 impedance_ctrl(robot, q_des, n, dyn);
490 data->ctrl[fingers_act] = phase.gripper_cmd;
491 mj_kdl::update(&robot);
492
493 const double t_rel = data->time - t_enter;
494 const bool done_time = t_rel >= phase.duration;
495 const bool done_pose =
496 phase.settle_tol < 0.0
497 || max_abs_joint_err(robot, *phase.target, n) <= phase.settle_tol;
498 const bool done_timeout = phase.timeout > 0.0 && t_rel >= phase.timeout;
499 if ((done_time && done_pose) || done_timeout) break;
500
501 if (!mj_kdl::step(&robot)) {
502 aborted = true;
503 break;
504 }
505 ++sim_step;
506 if (recorder_ok && sim_step % steps_per_frame == 0) {
507 if (!mj_kdl::record_frame(&recorder, model, data)) {
508 std::cerr << "record_frame() failed at step " << sim_step << "\n";
509 mj_kdl::cleanup(&recorder);
510 recorder_ok = false;
511 }
512 }
513 }
514 if (aborted) break;
515 }
516 } while (restart);
517
518 if (recorder_ok) {
519 mj_kdl::cleanup(&recorder);
520 std::cout << "Saved recording: " << record_path << "\n";
521 }
522
523 int ret = 0;
524 if (!aborted) {
525 int in_jug = 0;
526 double avg[3] = {};
527 for (int jid : grain_joints)
528 if (inside_jug(data, model, jid)) ++in_jug;
529 for (int jid : grain_joints) {
530 const double *p = data->qpos + model->jnt_qposadr[jid];
531 avg[0] += p[0];
532 avg[1] += p[1];
533 avg[2] += p[2];
534 }
535 if (!grain_joints.empty()) {
536 avg[0] /= static_cast<double>(grain_joints.size());
537 avg[1] /= static_cast<double>(grain_joints.size());
538 avg[2] /= static_cast<double>(grain_joints.size());
539 }
540
541 std::cout << "balls in transparent receiver: " << in_jug << "/" << grain_joints.size()
542 << "\n";
543 std::cout << "grain centroid: [" << std::fixed << std::setprecision(3) << avg[0] << ", "
544 << avg[1] << ", " << avg[2] << "] receiver center=[" << kJugX << ", " << kJugY
545 << "]\n";
546 if (headless && in_jug < 4) {
547 std::cerr << "pour failed: too few balls reached the receiver\n";
548 ret = 1;
549 }
550 }
551
552 if (!headless) mj_kdl::cleanup(&viewer);
553 mj_kdl::cleanup(&robot);
554 mj_kdl::destroy_scene(model, data);
555 return ret;
556}
int main(int argc, char *argv[])
bool init_video_recorder(VideoRecorder *vr, mjModel *model, const char *out_path, int width=1280, int height=720, int fps=60)
bool record_frame(VideoRecorder *vr, mjModel *model, mjData *data)
void set_body_pose(mjModel *model, mjData *data, const char *body_name, const double pos[3], const double *quat=nullptr)
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 get_site_frame(const mjModel *model, mjData *data, const char *site_name, KDL::Frame *out)
bool build_scene(mjModel **out_model, mjData **out_data, const SceneSpec *spec)
void destroy_scene(mjModel *model, mjData *data)
std::string scene_object_site_name(const SceneObject &obj, const char *site_name)
bool init_window_sim(Viewer *v, Robot *r, const char *title="MuJoCo")
void env_add_robot(Env *env, Robot *robot)
ResetInfo reset(Env *env, const ResetOptions *options=nullptr)
KDL::Frame target
const char * name
ResetHook on_reset
std::vector< AttachmentSpec > attachments
std::vector< int > kdl_to_mj_dof
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