mj-kdl-wrapper  0.1.0
MuJoCo + KDL bridge for robot kinematics and dynamics
Loading...
Searching...
No Matches
test_dual_arm.cpp
Go to the documentation of this file.
1/* test_dual_arm.cpp
2 * Two Kinova GEN3 arms in one shared MuJoCo scene, facing each other.
3 * Each arm is its own mj_kdl::Robot (sharing model/data).
4 * Gravity compensation uses KDL::ChainDynParam::JntToGravity - not qfrc_bias.
5 *
6 * GravityInformational - KDL vs MuJoCo gravity comparison at home pose
7 * (zero velocity, so qfrc_bias == gravity torques); logged only, no assertion.
8 * DualArmDrift - 500-step closed-loop gravity comp; EE drift must be < 1 mm.
9 *
10 * Self-skips when third_party/menagerie is absent. */
11
13#include "test_utils.hpp"
14
15#include <gtest/gtest.h>
16
17#include <kdl/chainfksolverpos_recursive.hpp>
18#include <kdl/chaindynparam.hpp>
19
20#include <iostream>
21#include <iomanip>
22#include <cmath>
23#include <memory>
24#include <string>
25#include <filesystem>
26
27static constexpr double kHomePose[7] = { 0.0, 0.2618, 3.1416, -2.2689, 0.0, 0.9599, 1.5708 };
28
29// Read state, compute KDL gravity torques, store in jnt_trq_cmd.
30static void apply_grav_comp(mj_kdl::Robot *s, KDL::ChainDynParam &dyn)
31{
33 KDL::JntArray q(s->n_joints), g(s->n_joints);
34 for (int i = 0; i < s->n_joints; ++i) q(i) = s->jnt_pos_msr[i];
35 dyn.JntToGravity(q, g);
36 for (int i = 0; i < s->n_joints; ++i) s->jnt_trq_cmd[i] = g(i);
37}
38
39namespace fs = std::filesystem;
40static fs::path repo_root() { return fs::path(__FILE__).parent_path().parent_path(); }
41
42class DualArmTest : public testing::Test
43{
44 protected:
45 mjModel *model = nullptr;
46 mjData *data = nullptr;
48 std::unique_ptr<KDL::ChainFkSolverPos_recursive> fk1;
49 std::unique_ptr<KDL::ChainFkSolverPos_recursive> fk2;
50 std::unique_ptr<KDL::ChainDynParam> dyn1;
51 std::unique_ptr<KDL::ChainDynParam> dyn2;
52 KDL::JntArray q_home;
53 int n = 0;
54
55 void SetUp() override
56 {
57 fs::path root = repo_root();
58 if (!fs::exists(root / "third_party/menagerie")) {
59 GTEST_SKIP() << "third_party/menagerie/ not found";
60 return;
61 }
62
63 std::string mjcf = (root / "third_party/menagerie/kinova_gen3/gen3.xml").string();
64
65 /* Build a single MuJoCo scene with two arms facing each other.
66 * arm1: at x = -0.5 m, facing +X (default orientation)
67 * arm2: at x = +0.5 m, facing -X (rotated 180 degrees around Z)
68 * arm2 joints are prefixed "r2_" in MuJoCo to avoid name collisions. */
70 scene.timestep = 0.002;
71 scene.gravity_z = -9.81;
72 scene.add_floor = true;
73
74 mj_kdl::RobotSpec r1, r2;
75 r1.path = mjcf.c_str();
76 r1.prefix = "";
77 r1.pos[0] = -0.5;
78
79 r2.path = mjcf.c_str();
80 r2.prefix = "r2_";
81 r2.pos[0] = 0.5;
82 r2.euler[2] = 180.0;
83
84 scene.robots = { r1, r2 };
85
86 ASSERT_TRUE(mj_kdl::build_scene(&model, &data, &scene)) << "build_scene() returned false";
87 TEST_INFO(model->nbody << " bodies, " << model->nq << " DOFs");
88
89 ASSERT_TRUE(
90 mj_kdl::init_robot_from_mjcf(&arm1, model, data, "base_link", "bracelet_link", "")
91 ) << "arm1 init_robot_from_mjcf() returned false";
92
93 ASSERT_TRUE(
94 mj_kdl::init_robot_from_mjcf(&arm2, model, data, "base_link", "bracelet_link", "r2_")
95 ) << "arm2 init_robot_from_mjcf() returned false";
96
97 n = arm1.n_joints;
98 fk1 = std::make_unique<KDL::ChainFkSolverPos_recursive>(arm1.chain);
99 fk2 = std::make_unique<KDL::ChainFkSolverPos_recursive>(arm2.chain);
100 dyn1 = std::make_unique<KDL::ChainDynParam>(arm1.chain, KDL::Vector(0, 0, -9.81));
101 dyn2 = std::make_unique<KDL::ChainDynParam>(arm2.chain, KDL::Vector(0, 0, -9.81));
102
103 q_home.resize(n);
104 for (int j = 0; j < n; ++j) q_home(j) = kHomePose[j];
105 }
106
107 void TearDown() override
108 {
112 }
113};
114
115TEST_F(DualArmTest, GravityInformational)
116{
117 mj_kdl::set_joint_pos(&arm1, q_home);
118 mj_kdl::set_joint_pos(&arm2, q_home);
119 mj_forward(model, data);
120
121 KDL::JntArray g1(n), g2(n);
122 dyn1->JntToGravity(q_home, g1);
123 dyn2->JntToGravity(q_home, g2);
124
125 double err1 = 0.0, err2 = 0.0;
126 for (int j = 0; j < n; ++j) {
127 err1 = std::max(err1, std::abs(g1(j) - data->qfrc_bias[arm1.kdl_to_mj_dof[j]]));
128 err2 = std::max(err2, std::abs(g2(j) - data->qfrc_bias[arm2.kdl_to_mj_dof[j]]));
129 }
130 TEST_INFO("arm1 max |KDL - qfrc_bias| = " << err1 << " Nm");
131 TEST_INFO("arm2 max |KDL - qfrc_bias| = " << err2 << " Nm");
132}
133
134TEST_F(DualArmTest, DualArmDrift)
135{
136 // Sync both arms to home pose and record initial EE frames.
137 mj_kdl::set_joint_pos(&arm1, q_home);
138 mj_kdl::set_joint_pos(&arm2, q_home);
139 mj_forward(model, data);
140 arm1.ctrl_mode = mj_kdl::CtrlMode::TORQUE;
141 arm2.ctrl_mode = mj_kdl::CtrlMode::TORQUE;
142
143 KDL::Frame ee1_init, ee2_init;
144 fk1->JntToCart(q_home, ee1_init);
145 fk2->JntToCart(q_home, ee2_init);
146
147 TEST_INFO(
148 "Arm 1 initial EE: [" << std::fixed << std::setprecision(4) << ee1_init.p.x() << ", "
149 << ee1_init.p.y() << ", " << ee1_init.p.z() << "]"
150 );
151 TEST_INFO(
152 "Arm 2 initial EE: [" << ee2_init.p.x() << ", " << ee2_init.p.y() << ", " << ee2_init.p.z()
153 << "]"
154 );
155
156 /* Prime jnt_trq_cmd for both arms so the first update() applies compensation
157 * immediately, not zero torques (which would impart velocity that gravity comp
158 * cannot damp). */
159 {
160 KDL::JntArray g1(n), g2(n);
161 dyn1->JntToGravity(q_home, g1);
162 dyn2->JntToGravity(q_home, g2);
163 for (int j = 0; j < n; ++j) arm1.jnt_trq_cmd[j] = g1(j);
164 for (int j = 0; j < n; ++j) arm2.jnt_trq_cmd[j] = g2(j);
165 }
166
167 /* Run 500-step closed-loop gravity compensation. Both arms share the same
168 * model/data; step() on arm1 advances the entire world. */
169 for (int i = 0; i < 500; ++i) {
170 apply_grav_comp(&arm1, *dyn1);
171 apply_grav_comp(&arm2, *dyn2);
172 mj_kdl::step(&arm1);
173 }
174
175 KDL::JntArray q1_end(n), q2_end(n);
176 for (int j = 0; j < n; ++j) {
177 q1_end(j) = arm1.jnt_pos_msr[j];
178 q2_end(j) = arm2.jnt_pos_msr[j];
179 }
180 KDL::Frame ee1_end, ee2_end;
181 fk1->JntToCart(q1_end, ee1_end);
182 fk2->JntToCart(q2_end, ee2_end);
183
184 double drift1 = (ee1_init.p - ee1_end.p).Norm();
185 double drift2 = (ee2_init.p - ee2_end.p).Norm();
186
187 TEST_INFO(
188 "EE drift after 500 steps: arm1=" << std::setprecision(3) << drift1 * 1000.0
189 << " mm arm2=" << drift2 * 1000.0 << " mm"
190 );
191
192 ASSERT_LE(drift1, 0.001) << "arm1 drift " << drift1 * 1000.0 << " mm exceeds 1 mm threshold";
193 ASSERT_LE(drift2, 0.001) << "arm2 drift " << drift2 * 1000.0 << " mm exceeds 1 mm threshold";
194}
195
196int main(int argc, char *argv[])
197{
198 testing::InitGoogleTest(&argc, argv);
199 return RUN_ALL_TESTS();
200}
std::unique_ptr< KDL::ChainDynParam > dyn2
std::unique_ptr< KDL::ChainDynParam > dyn1
std::unique_ptr< KDL::ChainFkSolverPos_recursive > fk2
mjModel * model
void TearDown() override
std::unique_ptr< KDL::ChainFkSolverPos_recursive > fk1
void SetUp() override
mj_kdl::Robot arm2
mj_kdl::Robot arm1
KDL::JntArray q_home
void step(Robot *s)
bool init_robot_from_mjcf(Robot *r, mjModel *model, mjData *data, const char *base_body, const char *tip_body, const char *prefix="", const char *tool_body=nullptr)
void cleanup(Robot *r)
void set_joint_pos(Robot *r, const KDL::JntArray &q, bool call_forward=true)
void update(Robot *r)
bool build_scene(mjModel **out_model, mjData **out_data, const SceneSpec *spec)
void destroy_scene(mjModel *model, mjData *data)
std::vector< double > jnt_pos_msr
std::vector< double > jnt_trq_cmd
std::vector< RobotSpec > robots
int main(int argc, char *argv[])
TEST_F(DualArmTest, GravityInformational)
#define TEST_INFO(msg)