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