Mujoco KDL Wrapper  0.5.1
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, with joint impedance (PD + KDL gravity) in TORQUE mode.
4 *
5 * Usage:
6 * ex_table_pour [--headless] [--record output.mp4]
7 *
8 * Runs the pour sequence once and prints how many balls ended in the receiver; --headless skips
9 * the viewer and exits 1 if fewer than kMinInReceiver did; --record writes an MP4 offscreen
10 * (EGL + ffmpeg) and implies --headless. */
11
12#include "common.hpp"
13#include "example_paths.hpp"
14
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 <cmath>
21#include <cstdio>
22#include <iomanip>
23#include <iostream>
24#include <string>
25#include <vector>
26
27namespace ex = mj_kdl_examples;
28
29static constexpr double kRobotBackX = -0.26;
30static constexpr double kJugX = 0.30;
31static constexpr double kJugY = 0.14;
32static constexpr double kRetreatX = kJugX - 0.08;
33static constexpr double kRetreatY = kJugY - 0.08;
34static constexpr double kJugRadius = 0.028;
35static constexpr double kJugHeight = 0.084;
36static constexpr int kNumBalls = 36;
37static constexpr double kBallRadius = 0.007;
38static constexpr double kIkTol = 3e-3;
39static constexpr double kPourTiltRad = 1.95;
40static constexpr double kTiltOutletZ = ex::kTableZ + 0.18;
41static constexpr int kMinInReceiver = 24;
42static constexpr int kRecordFps = 60;
43
44static constexpr double kKp[7] = { 120, 220, 120, 220, 110, 190, 90 };
45static constexpr double kKd[7] = { 12, 22, 12, 22, 11, 18, 9 };
46
47static mj_kdl::SceneObject make_ball(int idx)
48{
50 char name[32];
51 std::snprintf(name, sizeof(name), "grain_%02d", idx);
52 ball.name = name;
54 ball.size[0] = kBallRadius;
55 ball.size[1] = ball.size[2] = 0.0;
56 ball.pos[2] = ex::kTableZ + 0.40 + idx * 2.0 * kBallRadius;
57 const float rgba[4] = { 1.0f, 0.84f, 0.30f, 1.0f };
58 std::copy(rgba, rgba + 4, ball.rgba);
59 ball.mass = 0.006;
61 ball.friction[0] = 0.5;
62 ball.friction[1] = 0.02;
63 ball.friction[2] = 0.001;
64 return ball;
65}
66
67static bool inside_receiver(const KDL::Vector &p)
68{
69 return std::abs(p.x() - kJugX) < (kJugRadius - 0.012)
70 && std::abs(p.y() - kJugY) < (kJugRadius - 0.012) && p.z() > ex::kTableZ + 0.006
71 && p.z() < ex::kTableZ + kJugHeight + 0.04;
72}
73
74int main(int argc, char *argv[])
75{
76 const ex::Args args = ex::parse_args(argc, argv, "table_pour.mp4");
77 const bool headless = args.headless;
78
80 bottle.mjcf_path = ex::asset("mug.xml");
81 bottle.attach_to = { mj_kdl::AttachKind::Body, "g_base" };
82 bottle.prefix = "pour_";
83
84 mj_kdl::RobotSpec robot_spec;
85 robot_spec.path = ex::asset("kinova_gen3/gen3.xml");
86 robot_spec.pos[0] = kRobotBackX;
87 robot_spec.pos[2] = ex::kTableZ;
88 robot_spec.attachments.push_back(ex::gripper_attachment(ex::asset("robotiq_2f85/2f85.xml")));
89 robot_spec.attachments.push_back(bottle);
90
92 scene_cfg.objects.push_back(ex::table_object(ex::asset("table.xml"), ex::kTableZ));
93 for (int i = 0; i < kNumBalls; ++i) scene_cfg.objects.push_back(make_ball(i));
94 mj_kdl::SceneObject receiver;
95 receiver.name = "recv";
96 receiver.mjcf_path = ex::asset("mug_table.xml");
97 receiver.pos[0] = kJugX;
98 receiver.pos[1] = kJugY;
99 receiver.pos[2] = ex::kTableZ;
100 scene_cfg.objects.push_back(receiver);
101 scene_cfg.robots.push_back(robot_spec);
102
103 mj_kdl::Env env;
104 mj_kdl::Robot robot;
105 if (!mj_kdl::init_env(&env, &scene_cfg)) return 1;
107 if (!mj_kdl::init_robot_from_mjcf(&robot, &env, "base_link", "bracelet_link", "", &tool))
108 return 1;
111 mj_kdl::bind_scene_actuator(&env.scene, "g_fingers_actuator");
112 if (!fingers) return 1;
113 std::vector<mj_kdl::SceneFreeBodySlot *> grains;
114 for (int i = 0; i < kNumBalls; ++i) {
115 char name[32];
116 std::snprintf(name, sizeof(name), "grain_%02d", i);
117 grains.push_back(mj_kdl::bind_scene_free_body(&env.scene, name));
118 if (!grains.back()) return 1;
119 }
120
121 const unsigned n = robot.chain.getNrOfJoints();
122 const KDL::JntArray q_home = ex::home_q(n);
124 KDL::JntArray q_min(n), q_max(n);
125 for (unsigned i = 0; i < n; ++i) {
126 const auto [lo, hi] = robot.joint_limits[i];
127 q_min(i) = std::isfinite(lo) ? lo : -2 * M_PI;
128 q_max(i) = std::isfinite(hi) ? hi : 2 * M_PI;
129 }
130 KDL::ChainIkSolverVel_pinv ik_vel(robot.chain);
131 KDL::ChainIkSolverPos_NR_JL ik_nr(robot.chain, q_min, q_max, fk, ik_vel, 2000, 1e-5);
132 KDL::ChainIkSolverPos_LMA ik_lma(robot.chain, 1e-5, 2000);
133 KDL::ChainDynParam dyn(robot.chain, KDL::Vector(0.0, 0.0, scene_cfg.gravity_z));
134
135 KDL::Frame home_fk;
136 fk.JntToCart(q_home, home_fk);
137 const KDL::Rotation carry_tcp = home_fk.M * KDL::Rotation::RotY(-0.05);
138 const KDL::Frame base_T_world =
139 KDL::Frame(KDL::Vector(kRobotBackX, 0.0, ex::kTableZ)).Inverse();
140
141 // The TCP -> outlet offset, measured at home, turns an outlet target into a TCP target.
142 mj_kdl::set_joint_pos(&robot, q_home);
143 KDL::Frame world_T_outlet, world_T_tcp;
144 mj_kdl::get_site_frame(&env, "pour_outlet", &world_T_outlet);
145 mj_kdl::get_site_frame(&env, "g_pinch", &world_T_tcp);
146 const KDL::Vector tcp_outlet = world_T_tcp.Inverse() * world_T_outlet.p;
147
148 const auto solve =
149 [&](
150 const char *name, const KDL::Vector &outlet, const KDL::JntArray &seed, KDL::JntArray &out
151 ) {
152 const KDL::Frame target =
153 base_T_world * KDL::Frame(carry_tcp, outlet - carry_tcp * tcp_outlet);
154 bool ok = ik_nr.CartToJnt(seed, target, out) >= 0;
155 if (!ok) ok = ik_lma.CartToJnt(seed, target, out) >= 0;
156 KDL::Frame fk_out;
157 fk.JntToCart(out, fk_out);
158 if (!ok || (target.p - fk_out.p).Norm() > kIkTol) {
159 std::cerr << "IK failed for " << name << "\n";
160 return false;
161 }
162 return true;
163 };
164
165 KDL::JntArray q_pre_pour(n), q_pour(n), q_tilt(n), q_retreat(n);
166 KDL::Vector pour_outlet(kJugX, kJugY, ex::kTableZ + 0.20);
167 if (!solve("pre-pour", KDL::Vector(kJugX, kJugY, ex::kTableZ + 0.27), q_home, q_pre_pour))
168 return 1;
169 if (!solve("pour", pour_outlet, q_pre_pour, q_pour)) return 1;
170 // Tilting swings the outlet away; shift the pour pose until the tilted outlet is on target.
171 for (int iter = 0; iter < 4; ++iter) {
172 q_tilt = q_pour;
173 q_tilt(n - 1) += kPourTiltRad;
174 mj_kdl::set_joint_pos(&robot, q_tilt);
175 mj_kdl::get_site_frame(&env, "pour_outlet", &world_T_outlet);
176 const KDL::Vector err = KDL::Vector(kJugX, kJugY, kTiltOutletZ) - world_T_outlet.p;
177 if (err.Norm() < 5e-3) break;
178 pour_outlet += err;
179 if (!solve("pour", pour_outlet, q_pre_pour, q_pour)) return 1;
180 }
181 q_tilt = q_pour;
182 q_tilt(n - 1) += kPourTiltRad;
183 if (!solve("retreat", KDL::Vector(kRetreatX, kRetreatY, ex::kTableZ + 0.27), q_pour, q_retreat))
184 return 1;
185
186 bool restart = false;
187 env.on_reset = [&](mj_kdl::ResetContext *ctx) {
188 mj_kdl::set_joint_pos(&robot, q_home);
189 KDL::Frame world_T_center;
190 mj_kdl::get_site_frame(&env, "pour_center", &world_T_center);
191 const double spacing = 2.0 * kBallRadius;
192 for (int i = 0; i < kNumBalls; ++i) {
193 const int layer = i / 9, slot = i % 9;
194 const KDL::Vector local(
195 (slot % 3 - 1) * spacing, (slot / 3 - 1) * spacing, -0.026 + layer * spacing
196 );
197 const KDL::Vector w = world_T_center * local;
198 const double pos[3] = { w.x(), w.y(), w.z() };
199 mj_kdl::set_body_pose(&env, grains[i]->name.c_str(), pos);
200 }
201 ctx->data->ctrl[fingers->ctrl_id] = ex::kGripperClosed;
202 ex::prime_gravity(robot, dyn, q_home);
203 restart = true;
204 };
205
206 const double close = ex::kGripperClosed;
207 // clang-format off
208 const std::vector<ex::Phase> phases = {
209 { "HOME", &q_home, 1.0, 2.5, 0.08, close },
210 { "PRE_POUR", &q_pre_pour, 4.0, 6.5, 0.08, close },
211 { "POUR", &q_pour, 3.5, 5.5, 0.07, close },
212 { "TILT", &q_tilt, 7.0, 10.0, 0.07, close },
213 { "POUR_HOLD", &q_tilt, 9.0, 9.0, -1.0, close },
214 { "RETREAT", &q_retreat, 2.0, 4.0, 0.08, close },
215 { "HOLD", &q_retreat, 1.0, 1.0, -1.0, close },
216 };
217 // clang-format on
218
219 mj_kdl::VideoRecorder recorder;
220 bool recording = false;
221 const int steps_per_frame =
222 std::max(1, static_cast<int>(std::lround(1.0 / (kRecordFps * env.model->opt.timestep))));
223 if (args.record) {
225 &recorder,
226 env.model,
227 args.record_path.c_str(),
229 kRecordFps
230 );
231 if (!s) {
232 std::cerr << "init_video_recorder() failed: " << s.error << "\n";
233 return 1;
234 }
235 recorder.cam.azimuth = 145.0;
236 recorder.cam.elevation = -22.0;
237 recorder.cam.distance = 1.35;
238 recorder.cam.lookat[0] = 0.05;
239 recorder.cam.lookat[1] = 0.02;
240 recorder.cam.lookat[2] = 0.88;
241 recording = true;
242 }
243
244 mj_kdl::reset(&env);
245 if (!headless && !mj_kdl::open_viewer(&env)) return 1;
246
247 int sim_step = 0;
248 bool record_fail = false;
249 const bool completed = ex::run_phases(
250 env,
251 { { &robot, fingers } },
252 phases,
253 restart,
254 [&](std::size_t, const KDL::JntArray &q_des) { ex::pd_gravity(robot, dyn, q_des, kKp, kKd); },
255 [&](const ex::Phase &, double) {
256 if (recording && ++sim_step % steps_per_frame == 0
257 && !mj_kdl::record_frame(&recorder, &env)) {
258 std::cerr << "record_frame() failed at step " << sim_step << "\n";
259 mj_kdl::cleanup(&recorder);
260 recording = false;
261 record_fail = true;
262 }
263 }
264 );
265 mj_kdl::update(&env);
266 if (recording) {
267 mj_kdl::cleanup(&recorder);
268 std::cout << "Saved recording: " << args.record_path << "\n";
269 }
270
271 int in_receiver = 0;
272 KDL::Vector centroid = KDL::Vector::Zero();
273 for (const auto *g : grains) {
274 if (inside_receiver(g->pose.p)) ++in_receiver;
275 centroid += g->pose.p / kNumBalls;
276 }
277 std::cout << "balls in transparent receiver: " << in_receiver << "/" << kNumBalls
278 << " (at least " << kMinInReceiver << ")\n"
279 << std::fixed << std::setprecision(3) << "grain centroid: [" << centroid.x() << ", "
280 << centroid.y() << ", " << centroid.z() << "] receiver center=[" << kJugX << ", "
281 << kJugY << "]\n";
282 mj_kdl::cleanup(&env);
283 const bool ok = completed && !record_fail && in_receiver >= kMinInReceiver;
284 return headless ? ex::verdict(ok, "the balls were poured into the receiver") : 0;
285}
virtual int JntToCart(const JntArray &q_in, Frame &p_out, int segmentNr=-1)
virtual int CartToJnt(const KDL::JntArray &q_init, const KDL::Frame &T_base_goal, KDL::JntArray &q_out)
virtual int CartToJnt(const JntArray &q_init, const Frame &p_in, JntArray &q_out)
unsigned int getNrOfJoints() const
Frame Inverse() const
Rotation M
static Rotation RotY(double angle)
double z() const
double x() const
static Vector Zero()
double Norm(double eps=epsilon) const
double y() const
int main(int argc, char *argv[])
ResetInfo reset(Env *env, const ResetOptions *options=nullptr)
void set_body_pose(Env *env, const char *body_name, const double pos[3], const double *quat=nullptr)
Status init_env(Env *env, const SceneSpec *spec)
void update(Env *env)
Status 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, Env *env)
Status set_control_mode(Robot *r, CtrlMode mode)
Status init_robot_from_mjcf(Robot *r, Env *env, 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)
void cleanup(Robot *r)
bool get_site_frame(Env *env, const char *site_name, KDL::Frame *out)
SceneActuatorSlot * bind_scene_actuator(SceneState *s, const char *name)
SceneFreeBodySlot * bind_scene_free_body(SceneState *s, const char *body_name)
Status open_viewer(Env *env, const char *title="MuJoCo")
std::string asset(const fs::path &relative)
constexpr double kGripperClosed
Definition common.hpp:19
int verdict(bool ok, const std::string &goal)
Definition common.hpp:58
mj_kdl::SceneSpec scene_spec()
Definition common.hpp:138
mj_kdl::ToolFrameSpec gripper_tool(const std::string &prefix="")
Definition common.hpp:159
constexpr double kTableZ
Definition common.hpp:28
mj_kdl::AttachmentSpec gripper_attachment(const std::string &path, const std::string &site="pinch_site")
Definition common.hpp:149
KDL::JntArray home_q(unsigned n)
Definition common.hpp:68
void pd_gravity(mj_kdl::Robot &robot, KDL::ChainDynParam &dyn, const KDL::JntArray &q_des, const double *kp=nullptr, const double *kd=nullptr)
Definition common.hpp:112
mj_kdl::SceneObject table_object(const std::string &path, double surface_z)
Definition common.hpp:168
void prime_gravity(mj_kdl::Robot &robot, KDL::ChainDynParam &dyn, const KDL::JntArray &q)
Definition common.hpp:131
bool run_phases(mj_kdl::Env &env, const std::vector< PhaseArm > &arms, const std::vector< Phase > &phases, bool &restart, const std::function< void(std::size_t, const KDL::JntArray &)> &control, const std::function< void(const Phase &, double t_rel)> &after_step={})
Definition common.hpp:319
Args parse_args(int argc, char **argv, const std::string &default_record_path="")
Definition common.hpp:41
ResetHook on_reset
SceneState scene
std::vector< AttachmentSpec > attachments
std::vector< std::pair< double, double > > joint_limits
std::vector< RobotSpec > robots
std::vector< SceneObject > objects
std::string error
std::string record_path
Definition common.hpp:37