Mujoco KDL Wrapper  0.5.1
MuJoCo + KDL bridge for robot kinematics and dynamics
Loading...
Searching...
No Matches
ex_admittance_ft.cpp
Go to the documentation of this file.
1/* ex_admittance_ft.cpp
2 * Kinova Gen3 + wrist F/T sensor + Robotiq 2F-85 on a table: admittance control driven by the
3 * named F/T sensor, with an RNEA computed-torque inner loop in task space (TORQUE mode).
4 *
5 * Outer admittance law per Cartesian axis (K = 0: hand-guiding, the pose holds on release):
6 * M * a = F_ext - D * v, v += a * dt, offset += v * dt
7 * Inner loop:
8 * beta = Cartesian PD on the TCP pose error (desired TCP acceleration)
9 * qddot_des = WDLS(beta) + null-space posture (resolved acceleration, holds the elbow)
10 * tau = RNEA(q, qdot, qddot_des) (KDL ChainIdSolver_RNE)
11 *
12 * The run: a scripted helix force drives the admittance, the F/T sensor is tared, then the
13 * controller follows the measured force (a scripted push headless, the mouse in the viewer).
14 *
15 * Usage:
16 * ex_admittance_ft [--headless]
17 *
18 * --headless runs the self-check and exits non-zero on failure; with the viewer the same
19 * sequence runs once and exits. */
20
21#include "common.hpp"
22#include "example_paths.hpp"
23
24#include <kdl/chainidsolver_recursive_newton_euler.hpp>
25#include <kdl/chainjnttojacsolver.hpp>
26
27#include <algorithm>
28#include <cmath>
29#include <iomanip>
30#include <iostream>
31#include <string>
32
33namespace ex = mj_kdl_examples;
34
35// Admittance: virtual mass, damping, stiffness (isotropic).
36static constexpr double kMAdm = 8.0;
37static constexpr double kDAdm = 80.0;
38static constexpr double kKAdm = 0.0;
39static constexpr double kForceDeadband = 2.5; // [N]
40static constexpr double kMaxOffset = 0.20; // [m]
41static constexpr double kMaxVel = 0.25; // [m/s]
42
43// Helix intro.
44static constexpr double kTeachTime = 16.0; // [s]
45static constexpr double kTeachRadius = 0.04; // [m]
46static constexpr double kTeachRise = 0.10; // [m]
47static constexpr double kTeachTurns = 5.0;
48
49// Sequence and self-check.
50static constexpr int kSettleSteps = 300;
51static constexpr double kHandoffTareTime = 1.0; // [s]
52static constexpr int kHandoffSteps = 100;
53static constexpr double kSettleTime = 0.5; // [s]
54static constexpr double kPushTime = 4.0; // [s]
55static constexpr double kSelfcheckPush[3] = { 8.0, 12.0, 6.0 }; // [N]
56static constexpr double kMinElbowHeight = 0.64; // [m] above the table
57
58// Inner loop: Cartesian PD gains and acceleration limits.
59static constexpr double kKpLin = 2500.0;
60static constexpr double kKdLin = 100.0;
61static constexpr double kKpRot = 2500.0;
62static constexpr double kKdRot = 100.0;
63static constexpr double kBetaLinMax = 300.0;
64static constexpr double kBetaRotMax = 300.0;
65static constexpr double kKpNull = 100.0; // posture gains in the task's null space
66static constexpr double kKdNull = 20.0;
67
68static constexpr const char *kFtSensor = "wrist_ft";
69static constexpr const char *kToolBody = "g_base"; // where the self-check pushes
70static constexpr const char *kGripperActuator = "g_fingers_actuator";
71static constexpr const char *kElbowBody = "forearm_link"; // its origin is the elbow joint
72
73static KDL::Vector vclamp(const KDL::Vector &v, double limit)
74{
75 return KDL::Vector(
76 std::clamp(v.x(), -limit, limit),
77 std::clamp(v.y(), -limit, limit),
78 std::clamp(v.z(), -limit, limit)
79 );
80}
81
82static double norm3(const KDL::Vector &v)
83{
84 return std::sqrt(v.x() * v.x() + v.y() * v.y() + v.z() * v.z());
85}
86
87// Scripted force whose admittance response is a helix of kTeachTurns turns.
88static KDL::Vector spiral_force(double t)
89{
90 if (t < 0.0 || t > kTeachTime) return KDL::Vector::Zero();
91 const double theta = 2.0 * M_PI * kTeachTurns * t / kTeachTime;
92 const double theta_dot = 2.0 * M_PI * kTeachTurns / kTeachTime;
93 return KDL::Vector(
94 kDAdm * (-kTeachRadius * theta_dot * std::sin(theta)),
95 kDAdm * (kTeachRadius * theta_dot * std::cos(theta)),
96 kDAdm * (kTeachRise / kTeachTime)
97 );
98}
99
106
107static void admittance_update(Admittance &a, const KDL::Vector &force, double dt)
108{
109 if (norm3(force) == 0.0) {
111 return;
112 }
113 const KDL::Vector acc = (force - kDAdm * a.vel - kKAdm * a.offset) / kMAdm;
114 a.vel = vclamp(a.vel + acc * dt, kMaxVel);
115 a.offset = vclamp(a.offset + a.vel * dt, kMaxOffset);
116}
117
118// The sensor's force rotated into the world frame.
119static KDL::Vector ft_force_world(const mj_kdl::Robot &robot)
120{
121 const auto ft =
122 std::find_if(robot.ft_sensors.begin(), robot.ft_sensors.end(), [](const auto &s) {
123 return s.name == kFtSensor;
124 });
125 if (ft == robot.ft_sensors.end() || ft->frame_site_id < 0) return KDL::Vector::Zero();
126 const double *m = robot.data->site_xmat + 9 * ft->frame_site_id;
127 const KDL::Rotation R(m[0], m[1], m[2], m[3], m[4], m[5], m[6], m[7], m[8]);
128 return R * ft->wrench.force;
129}
130
131static KDL::Vector external_force(const mj_kdl::Robot &robot, const Admittance &a)
132{
133 const KDL::Vector f = ft_force_world(robot) - a.bias;
134 return norm3(f) < kForceDeadband ? KDL::Vector::Zero() : f;
135}
136
137struct Scene
138{
143 mj_kdl::SceneWrenchSlot *push = nullptr; // the self-check's hand on the tool
144 bool restarted = false; // set by env.on_reset (the viewer's reset)
145};
146
147static bool build_scene(Scene &s)
148{
150 ft_spec.mjcf_path = ex::asset("ft_sensor.xml");
151 ft_spec.attach_to = { mj_kdl::AttachKind::Site, "pinch_site" };
152
153 mj_kdl::RobotSpec robot_spec;
154 robot_spec.path = ex::asset("kinova_gen3/gen3.xml");
155 robot_spec.attach_to = { mj_kdl::AttachKind::Site, "table_top" };
156 robot_spec.attachments.push_back(ft_spec);
157 robot_spec.attachments.push_back(
158 ex::gripper_attachment(ex::asset("robotiq_2f85/2f85.xml"), "wrist_ft_site")
159 );
160
161 s.spec = ex::scene_spec();
162 s.spec.objects.push_back(ex::table_object(ex::asset("table.xml"), ex::kTableZ));
163 s.spec.robots.push_back(robot_spec);
164
165 if (!mj_kdl::init_env(&s.env, &s.spec)) return false;
166
168 ft_sensor.name = kFtSensor;
169 ft_sensor.frame_site = "wrist_ft_site";
171 tool.ft_sensors.push_back(ft_sensor);
172
173 if (!mj_kdl::init_robot_from_mjcf(&s.robot, &s.env, "base_link", "bracelet_link", "", &tool))
174 return false;
175 s.gripper = mj_kdl::bind_scene_actuator(&s.env.scene, kGripperActuator);
176 s.push = mj_kdl::bind_scene_wrench(&s.env.scene, kToolBody);
177 return s.gripper && s.push;
178}
179
180// Task-space computed torque: Cartesian PD -> WDLS acceleration IK + null-space posture -> RNEA.
182{
183 RneaController(Scene &scene, const KDL::JntArray &q_posture)
184 : s(scene), fk(s.robot.chain), jac_solver(s.robot.chain), ik_acc(s.robot.chain),
185 rnea(s.robot.chain, KDL::Vector(0.0, 0.0, s.spec.gravity_z)), posture(q_posture),
186 q(s.robot.n_joints), qd(s.robot.n_joints), qdd(s.robot.n_joints), z(s.robot.n_joints),
187 tau(s.robot.n_joints), jac(s.robot.n_joints),
188 f_ext(s.robot.chain.getNrOfSegments(), KDL::Wrench::Zero())
189 {
190 ik_acc.setLambda(0.10);
191 }
192
194 {
195 for (int i = 0; i < s.robot.n_joints; ++i) q(i) = s.robot.jnt_pos_msr[i];
196 KDL::Frame out;
197 fk.JntToCart(q, out);
198 return out;
199 }
200
201 void track(const KDL::Frame &target)
202 {
203 for (int i = 0; i < s.robot.n_joints; ++i) qd(i) = s.robot.jnt_vel_msr[i];
204 const KDL::Twist err = KDL::diff(tcp(), target);
205
206 jac_solver.JntToJac(q, jac);
207 KDL::Twist tcp_vel = KDL::Twist::Zero();
208 for (unsigned j = 0; j < q.rows(); ++j) tcp_vel += jac.getColumn(j) * qd(j);
209
210 const auto pd = [](double kp, double kd, double e, double v, double lim) {
211 return std::clamp(kp * e - kd * v, -lim, lim);
212 };
213 const KDL::Twist beta(
215 pd(kKpLin, kKdLin, err.vel.x(), tcp_vel.vel.x(), kBetaLinMax),
216 pd(kKpLin, kKdLin, err.vel.y(), tcp_vel.vel.y(), kBetaLinMax),
217 pd(kKpLin, kKdLin, err.vel.z(), tcp_vel.vel.z(), kBetaLinMax)
218 ),
220 pd(kKpRot, kKdRot, err.rot.x(), tcp_vel.rot.x(), kBetaRotMax),
221 pd(kKpRot, kKdRot, err.rot.y(), tcp_vel.rot.y(), kBetaRotMax),
222 pd(kKpRot, kKdRot, err.rot.z(), tcp_vel.rot.z(), kBetaRotMax)
223 )
224 );
225
226 // qdd = J#(beta - J z) + z = J# beta + (I - J# J) z (Siciliano et al. 2009, Sec. 3.5.1).
228 for (unsigned j = 0; j < q.rows(); ++j) {
229 z(j) = kKpNull * (posture(j) - q(j)) - kKdNull * qd(j);
230 jz += jac.getColumn(j) * z(j);
231 }
232 if (ik_acc.CartToJnt(q, beta - jz, qdd) < 0) return;
233 for (unsigned j = 0; j < q.rows(); ++j) qdd(j) += z(j);
234 if (rnea.CartToJnt(q, qd, qdd, f_ext, tau) < 0) return;
235 // update() clamps each torque to its joint's limit and reports it in jnt_saturated.
236 for (int i = 0; i < s.robot.n_joints; ++i) s.robot.jnt_trq_cmd[i] = tau(i);
237 }
238
248};
249
250// Admits the force into the offset and tracks the offset target; returns that target.
251static KDL::Frame control(
252 Scene &s,
253 RneaController &ctrl,
254 Admittance &a,
255 const KDL::Frame &nominal,
256 const KDL::Vector &force
257)
258{
259 admittance_update(a, force, s.spec.timestep);
260 const KDL::Frame target(nominal.M, nominal.p + a.offset);
261 ctrl.track(target);
262 return target;
263}
264
265static void close_gripper(Scene &s) { s.gripper->command = ex::kGripperClosed; }
266
267// Hold home with the gripper closed until the wrist load settles, then tare the sensor.
268static void settle_and_tare(Scene &s, RneaController &ctrl, Admittance &a)
269{
271 const KDL::Frame home = ctrl.tcp();
272 for (int i = 0; i < kSettleSteps; ++i) {
274 close_gripper(s);
275 ctrl.track(home);
276 if (!mj_kdl::step(&s.env)) break;
278 }
280 a.bias = ft_force_world(s.robot);
281}
282
284{
285 double helix_react = 0.0;
286 double helix_track_err = 0.0;
287 double helix_settle_err = 0.0;
288 double handoff_force = 0.0;
289 double push_response = 0.0;
290 double push_dy = 0.0;
291 double push_recovery_err = 0.0;
292 double hold_drift = 0.0;
293 double elbow_start = 0.0; // [m] elbow height above the table
294 double elbow_min = 0.0;
295};
296
297static double elbow_height(Scene &s)
298{
299 KDL::Frame elbow;
300 mj_kdl::get_body_frame(&s.env, kElbowBody, &elbow);
301 return elbow.p.z() - ex::kTableZ;
302}
303
304static Metrics
305 run_selfcheck(Scene &s, RneaController &ctrl, Admittance &a, const KDL::Frame &nominal)
306{
307 Metrics m;
308 m.elbow_start = elbow_height(s);
310
311 const double t0 = s.env.data->time;
312 while (s.env.data->time - t0 < kTeachTime) {
313 const double t = s.env.data->time - t0;
315 close_gripper(s);
316 const KDL::Frame target = control(s, ctrl, a, nominal, spiral_force(t));
317 m.helix_react = std::max(m.helix_react, norm3(a.offset));
318 m.helix_track_err = std::max(m.helix_track_err, norm3(ctrl.tcp().p - target.p));
319 if (!mj_kdl::step(&s.env)) break;
320 m.elbow_min = std::min(m.elbow_min, elbow_height(s));
322 }
323
324 const double th = s.env.data->time;
325 while (s.env.data->time - th < kHandoffTareTime) {
327 close_gripper(s);
328 const KDL::Frame target = control(s, ctrl, a, nominal, KDL::Vector::Zero());
329 m.helix_track_err = std::max(m.helix_track_err, norm3(ctrl.tcp().p - target.p));
330 if (!mj_kdl::step(&s.env)) break;
331 m.elbow_min = std::min(m.elbow_min, elbow_height(s));
333 }
334
336 a.bias = ft_force_world(s.robot);
337 for (int i = 0; i < kHandoffSteps; ++i) {
339 close_gripper(s);
340 const KDL::Vector f = external_force(s.robot, a);
341 m.handoff_force = std::max(m.handoff_force, norm3(f));
342 control(s, ctrl, a, nominal, f);
343 if (!mj_kdl::step(&s.env)) break;
344 m.elbow_min = std::min(m.elbow_min, elbow_height(s));
346 }
347
348 const double ts = s.env.data->time;
349 while (s.env.data->time - ts < kSettleTime) {
351 close_gripper(s);
352 const KDL::Frame target = control(s, ctrl, a, nominal, KDL::Vector::Zero());
353 m.helix_settle_err = std::max(m.helix_settle_err, norm3(ctrl.tcp().p - target.p));
354 if (!mj_kdl::step(&s.env)) break;
355 m.elbow_min = std::min(m.elbow_min, elbow_height(s));
357 }
358
359 const KDL::Vector push(kSelfcheckPush[0], kSelfcheckPush[1], kSelfcheckPush[2]);
360 const KDL::Vector pre_push = a.offset;
361 KDL::Vector settled = pre_push;
362 bool have_recovery = false;
363 bool have_settled = false;
364 const double tp = s.env.data->time;
365 while (s.env.data->time - tp < kPushTime) {
366 const double t = s.env.data->time - tp;
367 s.push->wrench = KDL::Wrench(t < 1.0 ? push : KDL::Vector::Zero(), KDL::Vector::Zero());
369 close_gripper(s);
370 const KDL::Frame target = control(s, ctrl, a, nominal, external_force(s.robot, a));
371 if (!have_recovery && t >= 2.0) {
372 m.push_recovery_err = norm3(ctrl.tcp().p - target.p);
373 have_recovery = true;
374 }
375 // Once the release transient (~1.5 s) has died; the drift is judged from here to the end.
376 if (!have_settled && t >= 2.5) {
377 settled = a.offset;
378 have_settled = true;
379 }
380 if (!mj_kdl::step(&s.env)) break;
381 m.elbow_min = std::min(m.elbow_min, elbow_height(s));
383 }
385
386 const KDL::Vector response = settled - pre_push;
387 m.push_response = norm3(response);
388 m.push_dy = response.y();
389 m.hold_drift = norm3(a.offset - settled);
390 return m;
391}
392
393static bool report(const Metrics &m)
394{
395 std::cout << std::fixed << std::setprecision(4)
396 << "helix force response (max offset): " << m.helix_react << " m\n"
397 << "helix TCP tracking error: " << m.helix_track_err << " m\n"
398 << "helix settle error: " << m.helix_settle_err << " m\n"
399 << "FT handoff residual force: " << m.handoff_force << " N\n"
400 << "FT push response (offset norm): " << m.push_response << " m\n"
401 << "FT push response (offset dY): " << m.push_dy << " m\n"
402 << "push release recovery error: " << m.push_recovery_err << " m\n"
403 << "hold drift after push released: " << m.hold_drift << " m\n"
404 << "elbow height start / lowest: " << m.elbow_start << " / " << m.elbow_min
405 << " m (limit " << kMinElbowHeight << " m)\n";
406 return m.helix_react > 0.10 && m.helix_track_err < 0.006 && m.helix_settle_err < 0.006
407 && m.handoff_force == 0.0 && m.push_response > 0.12 && m.push_recovery_err < 0.002
408 && m.hold_drift < 0.001 && m.elbow_min >= kMinElbowHeight;
409}
410
411// The same sequence with the viewer: helix, tare, then the mouse pushes the tool; ends on its own.
412static void run_gui(Scene &s, RneaController &ctrl, Admittance &a, const KDL::Frame &nominal)
413{
414 mj_kdl::Viewer *viewer = &s.env.viewer;
415 mj_kdl::set_free_camera(viewer, 1.55, 145.0, -24.0, { 0.05, 0.0, ex::kTableZ + 0.35 });
416 if (!mj_kdl::open_viewer(&s.env, "ex_admittance_ft")) return;
417
418 const double run_time =
419 kTeachTime + kHandoffTareTime + kHandoffSteps * s.spec.timestep + kSettleTime + kPushTime;
420 double start = s.env.data->time;
421 bool handoff_tared = false;
422 bool have_prev = false;
423 KDL::Vector target_prev, tcp_prev;
424 int trace_step = 0;
425
426 s.restarted = false;
427 while (mj_kdl::is_running(viewer)) {
428 if (s.restarted) {
429 s.restarted = false;
430 a = Admittance{};
431 start = s.env.data->time;
432 handoff_tared = false;
433 have_prev = false;
434 mj_kdl::clear_trace(viewer);
435 }
436 const double t = s.env.data->time - start;
437 if (t >= run_time) break;
439 close_gripper(s);
440
442 if (t < kTeachTime) {
443 force = spiral_force(t);
444 } else if (t >= kTeachTime + kHandoffTareTime) {
445 if (!handoff_tared) {
446 a.bias = ft_force_world(s.robot);
447 handoff_tared = true;
448 }
449 force = external_force(s.robot, a);
450 }
451 const KDL::Frame target = control(s, ctrl, a, nominal, force);
452
453 KDL::Frame world_base;
454 mj_kdl::get_body_frame(&s.env, "base_link", &world_base);
455 const KDL::Vector target_xyz = world_base * target.p;
456 const KDL::Vector tcp_xyz = world_base * ctrl.tcp().p;
457 ++trace_step;
458 if (have_prev && trace_step % 5 == 0) {
459 const float yellow[4] = { 1.0f, 0.95f, 0.0f, 1.0f };
460 const float green[4] = { 0.0f, 1.0f, 0.2f, 1.0f };
461 mj_kdl::add_trace_segment(viewer, target_prev, target_xyz, yellow);
462 mj_kdl::add_trace_segment(viewer, tcp_prev, tcp_xyz, green);
463 }
464 target_prev = target_xyz;
465 tcp_prev = tcp_xyz;
466 have_prev = true;
467
468 if (!mj_kdl::step(&s.env)) break;
470 }
471}
472
473int main(int argc, char **argv)
474{
475 const bool headless = ex::parse_args(argc, argv).headless;
476
477 Scene s;
478 if (!build_scene(s)) {
479 std::cerr << "failed to build admittance FT scene\n";
480 return 1;
481 }
484 RneaController ctrl(s, q_home);
485
487 mj_kdl::set_joint_pos(&s.robot, q_home);
488 s.restarted = true;
489 };
490 mj_kdl::reset(&s.env);
491
492 Admittance a;
493 settle_and_tare(s, ctrl, a);
494 const KDL::Frame nominal = ctrl.tcp();
495
496 std::cout << std::fixed << std::setprecision(3) << "FT bias: [" << a.bias.x() << ", "
497 << a.bias.y() << ", " << a.bias.z() << "] N\n";
498 int rc = 0;
499 if (headless) {
500 rc = ex::verdict(
501 report(run_selfcheck(s, ctrl, a, nominal)),
502 "the admittance responded "
503 "to the helix and the FT push and held on release"
504 );
505 } else {
506 run_gui(s, ctrl, a, nominal);
507 std::cout << std::fixed << std::setprecision(4) << "final offset: [" << a.offset.x() << ", "
508 << a.offset.y() << ", " << a.offset.z() << "] m\n";
509 }
511 return rc;
512}
unsigned int getNrOfJoints() const
Rotation M
Vector rot
Vector vel
static Twist Zero()
double z() const
double x() const
static Vector Zero()
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)
SceneWrenchSlot * bind_scene_wrench(SceneState *s, const char *body_name)
SceneActuatorSlot * bind_scene_actuator(SceneState *s, const char *name)
Status open_viewer(Env *env, const char *title="MuJoCo")
void pace_realtime(Env *env)
void clear_trace(Viewer *v)
void add_trace_segment(Viewer *v, const KDL::Vector &a, const KDL::Vector &b, const float rgba[4]=nullptr)
void set_free_camera(Viewer *v, double distance, double azimuth, double elevation, const std::array< double, 3 > &lookat)
bool is_running(const Viewer *v)
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)
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
mj_kdl::SceneObject table_object(const std::string &path, double surface_z)
Definition common.hpp:168
Args parse_args(int argc, char **argv, const std::string &default_record_path="")
Definition common.hpp:41
KDL::Vector bias
KDL::Vector offset
KDL::Vector vel
double helix_track_err
double handoff_force
double push_response
double push_recovery_err
double helix_settle_err
KDL::ChainIkSolverVel_wdls ik_acc
void track(const KDL::Frame &target)
KDL::ChainFkSolverPos_recursive fk
KDL::Wrenches f_ext
KDL::ChainJntToJacSolver jac_solver
RneaController(Scene &scene, const KDL::JntArray &q_posture)
const KDL::JntArray posture
KDL::ChainIdSolver_RNE rnea
mj_kdl::SceneWrenchSlot * push
mj_kdl::SceneActuatorSlot * gripper
mj_kdl::Robot robot
mj_kdl::SceneSpec spec
mj_kdl::Env env
ResetHook on_reset
SceneState scene
std::vector< AttachmentSpec > attachments
std::vector< ForceTorqueSensor > ft_sensors
std::vector< RobotSpec > robots
std::vector< SceneObject > objects
std::vector< ForceTorqueSensorSpec > ft_sensors