Mujoco KDL Wrapper  0.5.1
MuJoCo + KDL bridge for robot kinematics and dynamics
Loading...
Searching...
No Matches
ex_achd_table_slide.cpp
Go to the documentation of this file.
1/* ex_achd_table_slide.cpp
2 * Kinova GEN3, no gripper, slides its wrist flange (the TCP, the bracelet's pinch_site) 0.2 m
3 * along +X on a table while pressing down
4 * with kPressForce, under ACHD (KDL ChainHdSolver_Vereshchagin, linear Z left free) + RNEA.
5 * The press goes through ACHD's external-force input with driver weights 1, the pinned KDL
6 * fork's setDriverWeights(); the table's contact normal force is measured against it. Before
7 * the press, a guarded approach (all six directions tracked, nc=6) lowers the TCP at a speed
8 * that falls with its height, and the press ramps up once the table is touched.
9 *
10 * Usage:
11 * ex_achd_table_slide [--headless]
12 *
13 * Runs the slide once and prints the mean table reaction over its second half, the commanded
14 * press and their ratio (not judged), plus a one-shot nc=6 vs nc=5 comparison; --headless
15 * skips the viewer and exits 1 if the table contact was held for less than kContactHeld of
16 * that window, the touchdown was faster than kMaxTouchdown or the elbow went below
17 * kMinElbowHeight. */
18
19#include "common.hpp"
20#include "example_paths.hpp"
21
22#include <kdl/chainhdsolver_vereshchagin.hpp>
23#include <kdl/chainidsolver_recursive_newton_euler.hpp>
24#include <kdl/kinfam_io.hpp>
25
26#include <array>
27#include <cmath>
28#include <iomanip>
29#include <iostream>
30#include <limits>
31#include <string>
32
33namespace ex = mj_kdl_examples;
34
35static constexpr int kApproachSteps = 7500; // no contact within 15 s fails the run
36static constexpr int kPressSteps = 250; // the press ramps up over 0.5 s
37static constexpr int kSlideSteps = 2000;
38static constexpr double kTableZ = 0.447;
39static constexpr double kMoveX = 0.20;
40static constexpr double kVMaxLin = 0.08; // [m/s]
41static constexpr double kVTouch = 0.02; // [m/s] slowest descent, the touchdown speed
42static constexpr double kDescentGain = 0.5; // [1/s] descent speed per metre above the table
43static constexpr double kKpLin = 200.0;
44static constexpr double kKdLin = 30.0;
45static constexpr double kKpRot = 175.0;
46static constexpr double kKdRot = 28.0;
47static constexpr double kBetaMax = 120.0;
48static constexpr double kPressForce = 10.0; // [N] commanded straight down at the TCP
49static constexpr double kContactHeld = 0.6; // the contact chatters while sliding
50static constexpr double kMaxTouchdown = 0.05; // [m/s] TCP vertical speed at first contact
51static constexpr double kMinElbowHeight = 0.30; // [m] above the table
52static constexpr double kNoTouchdown = std::numeric_limits<double>::quiet_NaN();
53
54static constexpr const char *kElbowBody = "forearm_link"; // its origin is the elbow joint
55static constexpr const char *kTcpSite = "pinch_site"; // the flange face, z out of the arm
56
57// [m] TCP start in the base frame, flange down; the base stands on the table, so z is its height.
58static constexpr double kStartTcp[3] = { 0.40, 0.0, 0.12 };
59
60enum class Phase {
61 Approach, // all six TCP directions tracked, the reference descends onto the table
62 Press, // linear Z free, the press ramps up in place
63 Slide // linear Z free under the full press, the reference moves kMoveX along +X
64};
65
66// Sum of contact normal forces on one geom; positive pushes the bodies apart.
67static double contact_normal_force(const mjModel *model, mjData *data, int geom)
68{
69 double total = 0.0;
70 for (int i = 0; i < data->ncon; ++i) {
71 const mjContact &c = data->contact[i];
72 if (c.geom1 != geom && c.geom2 != geom) continue;
73 double f[6];
74 mj_contactForce(model, data, i, f);
75 total += f[0];
76 }
77 return total;
78}
79
80static void set_alpha_no_linear_z(KDL::Jacobian &alpha)
81{
82 alpha.setColumn(0, KDL::Twist(KDL::Vector(1, 0, 0), KDL::Vector(0, 0, 0)));
83 alpha.setColumn(1, KDL::Twist(KDL::Vector(0, 1, 0), KDL::Vector(0, 0, 0)));
84 alpha.setColumn(2, KDL::Twist(KDL::Vector(0, 0, 0), KDL::Vector(1, 0, 0)));
85 alpha.setColumn(3, KDL::Twist(KDL::Vector(0, 0, 0), KDL::Vector(0, 1, 0)));
86 alpha.setColumn(4, KDL::Twist(KDL::Vector(0, 0, 0), KDL::Vector(0, 0, 1)));
87}
88
89static double elbow_height(mj_kdl::Env &env)
90{
91 KDL::Frame elbow;
92 mj_kdl::get_body_frame(&env, kElbowBody, &elbow);
93 return elbow.p.z() - kTableZ;
94}
95
96static bool touches(const mjData *data, int geom)
97{
98 for (int i = 0; i < data->ncon; ++i)
99 if (data->contact[i].geom1 == geom || data->contact[i].geom2 == geom) return true;
100 return false;
101}
102
103// Downward world-frame speed of a site.
104static double site_sink_speed(const mjModel *model, const mjData *data, int site)
105{
106 double vel[6];
107 mj_objectVelocity(model, data, mjOBJ_SITE, site, vel, 0);
108 return -vel[5];
109}
110
111static void print_contact_heights(mj_kdl::Env &env)
112{
113 for (const char *name : { "spherical_wrist_2_link", "bracelet_link" }) {
114 KDL::Frame frame;
115 if (mj_kdl::get_body_frame(&env, name, &frame)) {
116 std::cout << name << "_z_above_table=" << std::fixed << std::setprecision(4)
117 << frame.p.z() - kTableZ << "\n";
118 }
119 }
120 KDL::Frame tcp;
121 if (mj_kdl::get_site_frame(&env, kTcpSite, &tcp)) {
122 std::cout << "tcp_z_above_table=" << std::fixed << std::setprecision(4)
123 << tcp.p.z() - kTableZ << "\n";
124 }
125}
126
127// Prints qdd and the RNEA torque for the current error with nc task constraints (6 or 5).
128static void print_nc_comparison(
129 const mj_kdl::Robot &robot,
130 const KDL::Twist &err,
131 const KDL::JntArray &q,
132 const KDL::JntArray &qd,
133 const KDL::Twist &root_acc,
135 unsigned nc
136)
137{
138 const unsigned n = robot.chain.getNrOfJoints();
139 KDL::ChainHdSolver_Vereshchagin achd(robot.chain, root_acc, nc);
140 KDL::JntArray qdd(n), beta(nc), ff(n), ctau(n), tau(n);
141 KDL::Jacobian alpha(nc);
143 if (nc == 6) {
144 for (unsigned i = 0; i < 6; ++i) alpha(i, i) = 1.0;
145 const double e[6] = { err.vel.x(), err.vel.y(), err.vel.z(),
146 err.rot.x(), err.rot.y(), err.rot.z() };
147 for (unsigned i = 0; i < 6; ++i)
148 beta(i) = ex::clamp_abs((i < 3 ? kKpLin : kKpRot) * e[i], kBetaMax);
149 } else {
150 set_alpha_no_linear_z(alpha);
151 const double e[5] = { err.vel.x(), err.vel.y(), err.rot.x(), err.rot.y(), err.rot.z() };
152 for (unsigned i = 0; i < 5; ++i)
153 beta(i) = ex::clamp_abs((i < 2 ? kKpLin : kKpRot) * e[i], kBetaMax);
154 }
155 achd.CartToJnt(q, qd, qdd, alpha, beta, f_ext, ff, ctau);
156 rnea.CartToJnt(q, qd, qdd, f_ext, tau);
157 std::cout << "\n--- nc=" << nc << (nc == 6 ? " (lin Z constrained)" : " (lin Z free)")
158 << " ---\nbeta=" << beta << "\nqdd=" << qdd << "\nconstraint_tau=" << ctau
159 << "\ntau_cmd=" << tau << "\n";
160}
161
162int main(int argc, char **argv)
163{
164 const bool headless = ex::parse_args(argc, argv).headless;
165
166 mj_kdl::RobotSpec robot_spec;
167 robot_spec.path = ex::asset("kinova_gen3/gen3.xml");
168 robot_spec.pos[2] = kTableZ;
169
171 scene.robots.push_back(robot_spec);
172 scene.objects.push_back(ex::table_object(ex::asset("table.xml"), kTableZ));
173
174 mj_kdl::Env env;
175 if (!mj_kdl::init_env(&env, &scene)) return 1;
177 tool.tcp_site = kTcpSite;
178 mj_kdl::Robot robot;
179 if (!mj_kdl::init_robot_from_mjcf(&robot, &env, "base_link", "bracelet_link", "", &tool))
180 return 1;
181 const int table_geom = mj_name2id(env.model, mjOBJ_GEOM, "top");
182 const int tcp_site = mj_name2id(env.model, mjOBJ_SITE, kTcpSite);
183 if (table_geom < 0 || tcp_site < 0) return 1;
184
185 const unsigned n = robot.chain.getNrOfJoints();
186 const unsigned ns = robot.chain.getNrOfSegments();
187 KDL::JntArray q_start(n);
188 {
190 KDL::ChainIkSolverVel_wdls ik_vel(robot.chain, 1e-5, 150);
191 ik_vel.setLambda(0.05);
192 const KDL::Vector start(kStartTcp[0], kStartTcp[1], kStartTcp[2]);
194 ik_vel,
195 fk_ik,
196 robot,
197 ex::home_q(n),
198 KDL::Frame(KDL::Rotation::RotX(M_PI), start),
199 q_start
200 )) {
201 std::cerr << "IK failed for the start pose\n";
202 return 1;
203 }
204 }
205
206 bool restarted = false;
207 env.on_reset = [&](mj_kdl::ResetContext *) {
208 mj_kdl::set_joint_pos(&robot, q_start);
209 for (unsigned i = 0; i < n; ++i) robot.jnt_pos_cmd[i] = q_start(i);
210 restarted = true;
211 };
212 mj_kdl::reset(&env);
213 print_contact_heights(env);
215
216 const KDL::Twist root_acc(KDL::Vector(0.0, 0.0, -scene.gravity_z), KDL::Vector::Zero());
218 KDL::ChainHdSolver_Vereshchagin achd_approach(robot.chain, root_acc, 6);
219 KDL::ChainHdSolver_Vereshchagin achd(robot.chain, root_acc, 5);
220 KDL::ChainIdSolver_RNE rnea(robot.chain, KDL::Vector(0.0, 0.0, scene.gravity_z));
221 KDL::JntArray q(n), qd(n), qdd(n), beta6(6), beta5(5), ff_tau(n), ctau(n), tau(n);
222 const KDL::JntArray zero(n);
223 KDL::Wrenches f_ext(ns, KDL::Wrench::Zero());
224 const KDL::Wrenches f_zero(ns, KDL::Wrench::Zero());
225 KDL::Jacobian alpha6(6), alpha5(5);
226 for (unsigned i = 0; i < 6; ++i) alpha6(i, i) = 1.0;
227 set_alpha_no_linear_z(alpha5);
228
229 mj_kdl::update(&env);
230 ex::read_q(robot, q, qd);
231 KDL::Frame tracked;
232 fk.JntToCart(q, tracked);
233 KDL::Frame target = tracked;
234 target.p += KDL::Vector(kMoveX, 0.0, 0.0);
235 print_nc_comparison(robot, KDL::diff(tracked, target), q, qd, root_acc, rnea, 6);
236 print_nc_comparison(robot, KDL::diff(tracked, target), q, qd, root_acc, rnea, 5);
237 std::cout << "\n";
238
239 // Driver weights 1 pass the commanded wrench through to the environment.
240 achd.setDriverWeights(Eigen::VectorXd::Ones(5), Eigen::VectorXd::Zero(5));
241
242 std::array<double, 6> err_prev{};
243 Phase phase = Phase::Approach;
244 bool first = true;
245 int phase_steps = 0;
246 double touchdown = kNoTouchdown; // [m/s] TCP sink speed at first contact
247 double elbow_min = elbow_height(env);
248 // Measured over the second half of the slide: the contact chatters while the TCP moves.
249 int contact_steps = 0;
250 double reaction_sum = 0.0;
251 int reaction_count = 0;
252
253 const auto restart_task = [&] {
254 mj_kdl::update(&env);
255 ex::read_q(robot, q, qd);
256 fk.JntToCart(q, tracked);
257 phase = Phase::Approach;
258 first = true;
259 phase_steps = 0;
260 touchdown = kNoTouchdown;
261 elbow_min = elbow_height(env);
262 contact_steps = 0;
263 reaction_sum = 0.0;
264 reaction_count = 0;
265 };
266
267 // One cycle: read the state (and apply the previous command), then command this one.
268 const auto control = [&] {
269 const double dt = env.model->opt.timestep;
270 if (phase == Phase::Approach) {
271 // The base stands on the table, so base-frame z is the height above it.
272 const double v = std::clamp(kDescentGain * tracked.p.z(), kVTouch, kVMaxLin);
273 tracked.p -= KDL::Vector(0.0, 0.0, v * dt);
274 } else if (phase == Phase::Slide) {
275 const KDL::Vector to_goal = target.p - tracked.p;
276 const double dist = to_goal.Norm();
277 if (dist > 1e-4) tracked.p += (to_goal / dist) * std::min(dist, kVMaxLin * dt);
278 }
279
280 mj_kdl::update(&env);
281 ex::read_q(robot, q, qd);
282 KDL::Frame current;
283 fk.JntToCart(q, current);
284 const KDL::Twist err = KDL::diff(current, tracked);
285 const double e[6] = { err.vel.x(), err.vel.y(), err.vel.z(),
286 err.rot.x(), err.rot.y(), err.rot.z() };
287 if (first) std::copy(e, e + 6, err_prev.begin());
288 first = false;
289 double b[6];
290 for (unsigned i = 0; i < 6; ++i) {
291 const double kp = i < 3 ? kKpLin : kKpRot, kd = i < 3 ? kKdLin : kKdRot;
292 b[i] = ex::clamp_abs(kp * e[i] + kd * (e[i] - err_prev[i]) / dt, kBetaMax);
293 err_prev[i] = e[i];
294 }
295 // Gravity as feed-forward, so only the commanded press pushes the free linear Z down.
296 if (rnea.CartToJnt(q, zero, zero, f_zero, ff_tau) < 0) return false;
297 if (phase == Phase::Approach) {
298 for (unsigned i = 0; i < 6; ++i) beta6(i) = b[i];
299 if (achd_approach.CartToJnt(q, qd, qdd, alpha6, beta6, f_zero, ff_tau, ctau) < 0)
300 return false;
301 } else {
302 const double ramp = phase == Phase::Press
303 ? std::min(1.0, static_cast<double>(phase_steps) / kPressSteps)
304 : 1.0;
305 f_ext.back() =
306 KDL::Wrench(KDL::Vector(0.0, 0.0, -ramp * kPressForce), KDL::Vector::Zero());
307 const unsigned no_z[5] = { 0, 1, 3, 4, 5 };
308 for (unsigned i = 0; i < 5; ++i) beta5(i) = b[no_z[i]];
309 if (achd.CartToJnt(q, qd, qdd, alpha5, beta5, f_ext, ff_tau, ctau) < 0) return false;
310 }
311 if (rnea.CartToJnt(q, qd, qdd, f_zero, tau) < 0) return false;
312 // update() clamps each torque to its joint's limit and reports it in jnt_saturated.
313 for (unsigned i = 0; i < n; ++i) robot.jnt_trq_cmd[i] = tau(i);
314 ++phase_steps;
315 return true;
316 };
317
318 if (!headless && !mj_kdl::open_viewer(&env)) return 1;
319 restarted = false;
320 bool solver_ok = true;
321 bool finished = false;
322 while (true) {
323 if (!control()) {
324 solver_ok = false;
325 break;
326 }
327 if (!mj_kdl::step(&env)) break;
328 elbow_min = std::min(elbow_min, elbow_height(env));
329 if (phase == Phase::Approach && touches(env.data, table_geom)) {
330 touchdown = site_sink_speed(env.model, env.data, tcp_site);
331 target = tracked;
332 target.p += KDL::Vector(kMoveX, 0.0, 0.0);
333 phase = Phase::Press;
334 phase_steps = 0;
335 } else if (phase == Phase::Press && phase_steps >= kPressSteps) {
336 phase = Phase::Slide;
337 phase_steps = 0;
338 } else if (phase == Phase::Slide && phase_steps > kSlideSteps / 2) {
339 const double reaction = contact_normal_force(env.model, env.data, table_geom);
340 if (reaction > 0.0) ++contact_steps;
341 reaction_sum += reaction;
342 ++reaction_count;
343 }
344 if (restarted) {
345 restarted = false;
346 restart_task();
347 continue;
348 }
350 if (phase == Phase::Approach && phase_steps >= kApproachSteps) break;
351 if (phase == Phase::Slide && phase_steps >= kSlideSteps) {
352 finished = true;
353 break;
354 }
355 }
356 mj_kdl::update(&env);
357
358 ex::read_q(robot, q, qd);
359 KDL::Frame current;
360 fk.JntToCart(q, current);
361 const KDL::Twist err = KDL::diff(current, target);
362 print_contact_heights(env);
363 std::cout << std::fixed << std::setprecision(3)
364 << "tcp_xy_err_mm=" << std::hypot(err.vel.x(), err.vel.y()) * 1000.0
365 << " tcp_z_error_unconstrained_mm=" << err.vel.z() * 1000.0
366 << " tcp_rot_err_rad=" << err.rot.Norm() << "\n";
367
368 const double contact_fraction =
369 reaction_count > 0 ? static_cast<double>(contact_steps) / reaction_count : 0.0;
370 const double mean_reaction = reaction_count > 0 ? reaction_sum / reaction_count : 0.0;
371 std::cout << "table_contact_fraction=" << contact_fraction
372 << " mean_table_reaction_N=" << mean_reaction << " commanded_press_N=" << kPressForce
373 << " reaction_over_command=" << mean_reaction / kPressForce << "\n"
374 << "touchdown_speed_m_s=" << std::setprecision(4) << touchdown << " (limit "
375 << kMaxTouchdown << ") elbow_min_z_above_table=" << elbow_min << " (limit "
376 << kMinElbowHeight << ")\n";
377 mj_kdl::cleanup(&env);
378 const bool ok = solver_ok && finished && contact_fraction >= kContactHeld
379 && touchdown <= kMaxTouchdown && elbow_min >= kMinElbowHeight;
380 return headless
381 ? ex::verdict(ok, "the TCP touched down gently and held the table during the slide")
382 : 0;
383}
virtual int JntToCart(const JntArray &q_in, Frame &p_out, int segmentNr=-1)
int CartToJnt(const JntArray &q, const JntArray &q_dot, JntArray &q_dotdot, const Jacobian &alfa, const JntArray &beta, const Wrenches &f_ext, const JntArray &ff_torques, JntArray &constraint_torques)
int setDriverWeights(const Eigen::VectorXd &w_f_ext, const Eigen::VectorXd &w_ff_torques)
int CartToJnt(const JntArray &q, const JntArray &q_dot, const JntArray &q_dotdot, const Wrenches &f_ext, JntArray &torques)
void setLambda(const double lambda)
unsigned int getNrOfJoints() const
unsigned int getNrOfSegments() const
void setColumn(unsigned int i, const Twist &t)
static Rotation RotX(double angle)
Vector rot
Vector vel
double z() const
double x() const
static Vector Zero()
double Norm(double eps=epsilon) const
double y() const
static Wrench Zero()
int main(int argc, char **argv)
ResetInfo reset(Env *env, const ResetOptions *options=nullptr)
Status init_env(Env *env, const SceneSpec *spec)
void update(Env *env)
bool step(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_body_frame(Env *env, const char *body_name, KDL::Frame *out)
bool get_site_frame(Env *env, const char *site_name, KDL::Frame *out)
Status open_viewer(Env *env, const char *title="MuJoCo")
void pace_realtime(Env *env)
IMETHOD Vector diff(const Vector &p_w_a, const Vector &p_w_b, double dt=1)
std::vector< Wrench > Wrenches
std::string asset(const fs::path &relative)
double clamp_abs(double v, double limit)
Definition common.hpp:66
int verdict(bool ok, const std::string &goal)
Definition common.hpp:58
mj_kdl::SceneSpec scene_spec()
Definition common.hpp:138
KDL::JntArray home_q(unsigned n)
Definition common.hpp:68
void read_q(const mj_kdl::Robot &robot, KDL::JntArray &q)
Definition common.hpp:80
mj_kdl::SceneObject table_object(const std::string &path, double surface_z)
Definition common.hpp:168
bool solve_near_seed(KDL::ChainIkSolverVel_wdls &ik_vel, KDL::ChainFkSolverPos_recursive &fk, const mj_kdl::Robot &robot, const KDL::JntArray &seed, const KDL::Frame &target, KDL::JntArray &out)
Definition common.hpp:204
Args parse_args(int argc, char **argv, const std::string &default_record_path="")
Definition common.hpp:41
ResetHook on_reset
std::vector< double > jnt_pos_cmd
std::vector< double > jnt_trq_cmd
std::vector< RobotSpec > robots
std::vector< SceneObject > objects