Mujoco 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
14#include <gtest/gtest.h>
15
16#include <kdl/chainfksolverpos_recursive.hpp>
17#include <kdl/chaindynparam.hpp>
18
19#include <cmath>
20#include <memory>
21#include <string>
22#include <filesystem>
23
24static constexpr double kHomePose[7] = { 0.0, 0.2618, 3.1416, -2.2689, 0.0, 0.9599, 1.5708 };
25
26// Read state, compute KDL gravity torques, store in jnt_trq_cmd.
27static void apply_grav_comp(mj_kdl::Robot *s, KDL::ChainDynParam &dyn)
28{
30 KDL::JntArray q(s->n_joints), g(s->n_joints);
31 for (int i = 0; i < s->n_joints; ++i) q(i) = s->jnt_pos_msr[i];
32 dyn.JntToGravity(q, g);
33 for (int i = 0; i < s->n_joints; ++i) s->jnt_trq_cmd[i] = g(i);
34}
35
36namespace fs = std::filesystem;
37static fs::path repo_root() { return fs::path(__FILE__).parent_path().parent_path(); }
38
39class DualArmTest : public testing::Test
40{
41 protected:
42 mjModel *model = nullptr;
43 mjData *data = nullptr;
45 std::unique_ptr<KDL::ChainFkSolverPos_recursive> fk1;
46 std::unique_ptr<KDL::ChainFkSolverPos_recursive> fk2;
47 std::unique_ptr<KDL::ChainDynParam> dyn1;
48 std::unique_ptr<KDL::ChainDynParam> dyn2;
49 KDL::JntArray q_home;
50 int n = 0;
51
52 void SetUp() override
53 {
54 fs::path root = repo_root();
55 std::string mjcf = (root / "third_party/menagerie/kinova_gen3/gen3.xml").string();
56 if (!fs::exists(mjcf)) {
57 GTEST_SKIP() << mjcf << " not found";
58 return;
59 }
60
61 /* Build a single MuJoCo scene with two arms facing each other.
62 * arm1: at x = -0.5 m, facing +X (default orientation)
63 * arm2: at x = +0.5 m, facing -X (rotated 180 degrees around Z)
64 * arm2 joints are prefixed "r2_" in MuJoCo to avoid name collisions. */
66 scene.timestep = 0.002;
67 scene.gravity_z = -9.81;
68 scene.add_floor = true;
69
70 scene.robots = {
71 mj_kdl::RobotSpec{ .path = mjcf.c_str(), .pos = { -0.5, 0.0, 0.0 }, .attachments = {} },
72 mj_kdl::RobotSpec{ .path = mjcf.c_str(), .prefix = "r2_", .pos = { 0.5, 0.0, 0.0 }, .euler = { 0.0, 0.0, 180.0 }, .attachments = {} },
73 };
74
75 ASSERT_TRUE(mj_kdl::build_scene(&model, &data, &scene)) << "build_scene() returned false";
76
77 ASSERT_TRUE(
78 mj_kdl::init_robot_from_mjcf(&arm1, model, data, "base_link", "bracelet_link", "")
79 ) << "arm1 init_robot_from_mjcf() returned false";
80
81 ASSERT_TRUE(
82 mj_kdl::init_robot_from_mjcf(&arm2, model, data, "base_link", "bracelet_link", "r2_")
83 ) << "arm2 init_robot_from_mjcf() returned false";
84
85 n = arm1.n_joints;
86 fk1 = std::make_unique<KDL::ChainFkSolverPos_recursive>(arm1.chain);
87 fk2 = std::make_unique<KDL::ChainFkSolverPos_recursive>(arm2.chain);
88 dyn1 = std::make_unique<KDL::ChainDynParam>(arm1.chain, KDL::Vector(0, 0, -9.81));
89 dyn2 = std::make_unique<KDL::ChainDynParam>(arm2.chain, KDL::Vector(0, 0, -9.81));
90
91 q_home.resize(n);
92 for (int j = 0; j < n; ++j) q_home(j) = kHomePose[j];
93 }
94
95 void TearDown() override
96 {
100 }
101};
102
103TEST_F(DualArmTest, GravityInformational)
104{
105 mj_kdl::set_joint_pos(&arm1, q_home);
106 mj_kdl::set_joint_pos(&arm2, q_home);
107 mj_forward(model, data);
108
109 KDL::JntArray g1(n), g2(n);
110 dyn1->JntToGravity(q_home, g1);
111 dyn2->JntToGravity(q_home, g2);
112
113 double err1 = 0.0, err2 = 0.0;
114 for (int j = 0; j < n; ++j) {
115 err1 = std::max(err1, std::abs(g1(j) - data->qfrc_bias[arm1.kdl_to_mj_dof[j]]));
116 err2 = std::max(err2, std::abs(g2(j) - data->qfrc_bias[arm2.kdl_to_mj_dof[j]]));
117 }
118 (void)err1; (void)err2;
119}
120
121TEST_F(DualArmTest, DualArmDrift)
122{
123 // Sync both arms to home pose and record initial EE frames.
124 mj_kdl::set_joint_pos(&arm1, q_home);
125 mj_kdl::set_joint_pos(&arm2, q_home);
126 mj_forward(model, data);
127 arm1.ctrl_mode = mj_kdl::CtrlMode::TORQUE;
128 arm2.ctrl_mode = mj_kdl::CtrlMode::TORQUE;
129
130 KDL::Frame ee1_init, ee2_init;
131 fk1->JntToCart(q_home, ee1_init);
132 fk2->JntToCart(q_home, ee2_init);
133
134 /* Prime jnt_trq_cmd for both arms so the first update() applies compensation
135 * immediately, not zero torques (which would impart velocity that gravity comp
136 * cannot damp). */
137 {
138 KDL::JntArray g1(n), g2(n);
139 dyn1->JntToGravity(q_home, g1);
140 dyn2->JntToGravity(q_home, g2);
141 for (int j = 0; j < n; ++j) arm1.jnt_trq_cmd[j] = g1(j);
142 for (int j = 0; j < n; ++j) arm2.jnt_trq_cmd[j] = g2(j);
143 }
144
145 /* Run 500-step closed-loop gravity compensation. Both arms share the same
146 * model/data; step() on arm1 advances the entire world. */
147 for (int i = 0; i < 500; ++i) {
148 apply_grav_comp(&arm1, *dyn1);
149 apply_grav_comp(&arm2, *dyn2);
150 mj_kdl::step(&arm1);
151 }
152
153 KDL::JntArray q1_end(n), q2_end(n);
154 for (int j = 0; j < n; ++j) {
155 q1_end(j) = arm1.jnt_pos_msr[j];
156 q2_end(j) = arm2.jnt_pos_msr[j];
157 }
158 KDL::Frame ee1_end, ee2_end;
159 fk1->JntToCart(q1_end, ee1_end);
160 fk2->JntToCart(q2_end, ee2_end);
161
162 double drift1 = (ee1_init.p - ee1_end.p).Norm();
163 double drift2 = (ee2_init.p - ee2_end.p).Norm();
164
165 ASSERT_LE(drift1, 0.001) << "arm1 drift " << drift1 * 1000.0 << " mm exceeds 1 mm threshold";
166 ASSERT_LE(drift2, 0.001) << "arm2 drift " << drift2 * 1000.0 << " mm exceeds 1 mm threshold";
167}
168
169int main(int argc, char *argv[])
170{
171 testing::InitGoogleTest(&argc, argv);
172 return RUN_ALL_TESTS();
173}
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
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 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)