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