Mujoco KDL Wrapper  0.1.0
MuJoCo + KDL bridge for robot kinematics and dynamics
Loading...
Searching...
No Matches
test_table_scene.cpp
Go to the documentation of this file.
1/* test_table_scene.cpp
2 * Load a robot arm on a table with pickable objects (cubes and spheres).
3 * Runs KDL gravity compensation so the arm holds position.
4 * Also tests runtime scene_add_object / scene_remove_object.
5 * Self-skips when third_party/menagerie is absent. */
6
7#include <gtest/gtest.h>
8
10
11#include <kdl/chainfksolverpos_recursive.hpp>
12#include <kdl/chaindynparam.hpp>
13
14#include <memory>
15#include <string>
16#include <filesystem>
17
18static constexpr double kHomePose[7] = { 0.0, 0.2618, 3.1416, -2.2689, 0.0, 0.9599, 1.5708 };
19
20namespace fs = std::filesystem;
21static fs::path repo_root() { return fs::path(__FILE__).parent_path().parent_path(); }
22
23static mj_kdl::SceneObject make_box(
24 const char *name, double x, double y, double hx, double hy, double hz,
25 float r, float g, float b, double surface_z
26)
27{
29 .name = name,
30 .mjcf_path = "",
31 .shape = mj_kdl::Shape::BOX,
32 .size = { hx, hy, hz },
33 .pos = { x, y, surface_z + hz },
34 .rgba = { r, g, b, 1.0f },
35 };
36}
37
38static mj_kdl::SceneObject make_sphere(
39 const char *name, double x, double y, double radius,
40 float r, float g, float b, double surface_z
41)
42{
44 .name = name,
45 .mjcf_path = "",
46 .shape = mj_kdl::Shape::SPHERE,
47 .size = { radius, 0.0, 0.0 },
48 .pos = { x, y, surface_z + radius },
49 .rgba = { r, g, b, 1.0f },
50 };
51}
52
53class TableSceneTest : public testing::Test
54{
55 protected:
56 std::string mjcf_;
59 mjModel *model_ = nullptr;
60 mjData *data_ = nullptr;
62 bool s_cleaned_ = false;
63
64 std::unique_ptr<KDL::ChainFkSolverPos_recursive> fk_;
65 std::unique_ptr<KDL::ChainDynParam> dyn_;
66
67 unsigned n_ = 0;
68 KDL::JntArray q_home_;
69
70 void SetUp() override
71 {
72 fs::path root = repo_root();
73 mjcf_ = (root / "third_party/menagerie/kinova_gen3/gen3.xml").string();
74 if (!fs::exists(mjcf_)) {
75 GTEST_SKIP() << mjcf_ << " not found";
76 return;
77 }
78 std::string table_mjcf = (root / "src/examples/assets/table.xml").string();
79
80 // Table asset origin is the tabletop surface center.
81 double surface_z = 0.7;
82
83 std::vector<mj_kdl::SceneObject> objects;
84 table_obj_ = {
85 .name = "table",
86 .mjcf_path = table_mjcf,
87 .pos = { 0.0, 0.0, surface_z },
88 .fixed = true,
89 };
90 objects.push_back(table_obj_);
91 objects.push_back(
92 make_box("red_cube", 0.35, 0.10, 0.03, 0.03, 0.03, 1.0f, 0.2f, 0.2f, surface_z)
93 );
94 objects.push_back(
95 make_box("green_cube", 0.35, -0.10, 0.03, 0.03, 0.03, 0.2f, 1.0f, 0.2f, surface_z)
96 );
97 objects.push_back(
98 make_box("blue_cube", 0.35, 0.30, 0.04, 0.04, 0.04, 0.2f, 0.2f, 1.0f, surface_z)
99 );
100 objects.push_back(
101 make_sphere("orange_sphere", -0.20, 0.20, 0.035, 1.0f, 0.55f, 0.0f, surface_z)
102 );
103 objects.push_back(
104 make_sphere("purple_sphere", -0.20, -0.20, 0.025, 0.7f, 0.0f, 0.9f, surface_z)
105 );
106
107 spec_.objects = objects;
108 spec_.add_floor = true;
109 spec_.gravity_z = -9.81;
110
111 spec_.robots.push_back(mj_kdl::RobotSpec{ .path = mjcf_.c_str(), .pos = { 0.0, 0.0, surface_z }, .attachments = {} });
112
113 ASSERT_TRUE(mj_kdl::build_scene(&model_, &data_, &spec_));
114 KDL::Frame world_T_table_top;
115 std::string table_top_site = mj_kdl::scene_object_site_name(table_obj_, "table_top");
116 ASSERT_TRUE(mj_kdl::get_site_frame(model_, data_, table_top_site.c_str(), &world_T_table_top));
117 EXPECT_NEAR(world_T_table_top.p.z(), surface_z, 1e-9);
118
119 ASSERT_TRUE(mj_kdl::init_robot_from_mjcf(&s_, model_, data_, "base_link", "bracelet_link"));
120
121 n_ = static_cast<unsigned>(s_.n_joints);
122
123 fk_ = std::make_unique<KDL::ChainFkSolverPos_recursive>(s_.chain);
124 dyn_ = std::make_unique<KDL::ChainDynParam>(s_.chain, KDL::Vector(0, 0, spec_.gravity_z));
125
126 q_home_.resize(n_);
127 for (unsigned i = 0; i < n_; ++i) q_home_(i) = kHomePose[i];
129 mj_forward(model_, data_);
130 }
131
132 void TearDown() override
133 {
136 }
137};
138
139TEST_F(TableSceneTest, GravityCompDrift)
140{
141 KDL::Frame ee_init;
142 fk_->JntToCart(q_home_, ee_init);
143
144 s_.ctrl_mode = mj_kdl::CtrlMode::TORQUE;
145 KDL::JntArray q(n_), g(n_);
146 // Prime jnt_trq_cmd so the first update() applies compensation immediately.
147 dyn_->JntToGravity(q_home_, g);
148 for (unsigned j = 0; j < n_; ++j) s_.jnt_trq_cmd[j] = g(j);
149 for (int i = 0; i < 500; ++i) {
150 mj_kdl::update(&s_);
151 for (unsigned j = 0; j < n_; ++j) q(j) = s_.jnt_pos_msr[j];
152 dyn_->JntToGravity(q, g);
153 for (unsigned j = 0; j < n_; ++j) s_.jnt_trq_cmd[j] = g(j);
154 mj_kdl::step(&s_);
155 }
156
157 KDL::JntArray q_end(n_);
158 KDL::Frame ee_end;
159 for (unsigned j = 0; j < n_; ++j) q_end(j) = s_.jnt_pos_msr[j];
160 fk_->JntToCart(q_end, ee_end);
161 double drift = (ee_init.p - ee_end.p).Norm();
162
163 ASSERT_LE(drift, 0.001) << "drift " << drift * 1000.0 << " mm exceeds 1 mm threshold";
164}
165
166TEST_F(TableSceneTest, AddRemoveObject)
167{
168 mj_kdl::cleanup(&s_);
169 s_cleaned_ = true;
170
171 double surface_z = 0.7;
172 mj_kdl::SceneObject extra =
173 make_box("yellow_cube", 0.0, 0.4, 0.03, 0.03, 0.03, 1.0f, 1.0f, 0.0f, surface_z);
174
175 ASSERT_TRUE(mj_kdl::scene_add_object(&model_, &data_, &spec_, extra))
176 << "scene_add_object() returned false";
177
178 ASSERT_TRUE(mj_kdl::scene_remove_object(&model_, &data_, &spec_, "yellow_cube"))
179 << "scene_remove_object() returned false";
180}
181
182TEST_F(TableSceneTest, EnvAddRemoveReinitsRobot)
183{
184 mj_kdl::Env env;
185 env.spec = spec_;
186 env.model = model_;
187 env.data = data_;
188 mj_kdl::env_add_robot(&env, &s_);
189 // env now owns the rebuild lifecycle; null out fixture pointers to prevent double-free
190 model_ = nullptr;
191 data_ = nullptr;
192
193 int nq_before = env.model->nq;
194
195 double surface_z = 0.7;
196 mj_kdl::SceneObject extra =
197 make_box("yellow_cube", 0.0, 0.4, 0.03, 0.03, 0.03, 1.0f, 1.0f, 0.0f, surface_z);
198
199 ASSERT_TRUE(mj_kdl::scene_add_object(&env, extra)) << "Env scene_add_object() failed";
200 EXPECT_GT(env.model->nq, nq_before) << "nq should grow after adding a free object";
201 EXPECT_EQ(s_.n_joints, 7) << "robot chain should still have 7 joints after rebuild";
202 EXPECT_EQ(s_.model, env.model) << "robot model pointer should be updated";
203
204 ASSERT_TRUE(mj_kdl::scene_remove_object(&env, "yellow_cube")) << "Env scene_remove_object() failed";
205 EXPECT_EQ(env.model->nq, nq_before) << "nq should return to original after removal";
206 EXPECT_EQ(s_.n_joints, 7) << "robot chain should still have 7 joints after removal";
207
208 mj_kdl::cleanup(&env);
209 s_cleaned_ = true;
210}
211
212int main(int argc, char *argv[])
213{
214 testing::InitGoogleTest(&argc, argv);
215 return RUN_ALL_TESTS();
216}
KDL::JntArray q_home_
mj_kdl::SceneObject table_obj_
void SetUp() override
void TearDown() override
std::unique_ptr< KDL::ChainDynParam > dyn_
mj_kdl::SceneSpec spec_
std::unique_ptr< KDL::ChainFkSolverPos_recursive > fk_
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 get_site_frame(const mjModel *model, mjData *data, const char *site_name, KDL::Frame *out)
bool build_scene(mjModel **out_model, mjData **out_data, const SceneSpec *spec)
bool scene_add_object(mjModel **model, mjData **data, SceneSpec *spec, const SceneObject &obj)
bool scene_remove_object(mjModel **model, mjData **data, SceneSpec *spec, const std::string &name)
void destroy_scene(mjModel *model, mjData *data)
std::string scene_object_site_name(const SceneObject &obj, const char *site_name)
void env_add_robot(Env *env, Robot *robot)
std::vector< RobotSpec > robots
std::vector< SceneObject > objects
int main(int argc, char *argv[])
TEST_F(TableSceneTest, GravityCompDrift)