Mujoco KDL Wrapper  0.3.18
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 Menagerie is absent. */
6
7#include <gtest/gtest.h>
8
10#include "example_paths.hpp"
11
12#include <kdl/chainfksolverpos_recursive.hpp>
13#include <kdl/chaindynparam.hpp>
14
15#include <memory>
16#include <string>
17#include <filesystem>
18
19static constexpr double kHomePose[7] = { 0.0, 0.2618, 3.1416, -2.2689, 0.0, 0.9599, 1.5708 };
20
21namespace fs = std::filesystem;
22// Free-jointed primitives must stay world-anchored (MuJoCo: freejoint only on
23// top level), so their z is surface_z + half_height. The robot, which has no
24// freejoint at its root, attaches to the table via a site instead.
25static constexpr double kCubeMass = 0.1; // [kg]
26static constexpr double kSphereMass = 0.05;
27static constexpr double kPickFriction[3] = { 0.8, 0.02, 0.001 };
28
29TEST(SceneObjectTransform, PathBackedObjectAppliesQuat)
30{
31 std::string table_mjcf = mj_kdl_examples::find_asset("table.xml");
33 spec.timestep = 0.002;
34 // extrinsic XYZ euler (30, 40, 50) deg
35 spec.objects.push_back({
36 .name = "turned",
37 .mjcf_path = table_mjcf,
38 .quat = { 0.08080468869083995, 0.40219849353410964, 0.30337177447125957, 0.860042173697679 },
39 .fixed = true,
40 });
41
42 mjModel *model = nullptr;
43 mjData *data = nullptr;
44 ASSERT_TRUE(mj_kdl::build_scene(&model, &data, &spec));
45 KDL::Frame frame;
46 ASSERT_TRUE(mj_kdl::get_site_frame(model, data, "turned_table_top", &frame));
47 const KDL::Vector y = frame.M * KDL::Vector(0.0, 1.0, 0.0);
48 EXPECT_NEAR(y.x(), -0.456825992585671, 1e-9);
49 EXPECT_NEAR(y.y(), 0.802872337479472, 1e-9);
50 EXPECT_NEAR(y.z(), 0.383022221559489, 1e-9);
51 mj_kdl::destroy_scene(model, data);
52}
53
54static mj_kdl::SceneObject make_box(
55 const char *name, double x, double y, double hx, double hy, double hz,
56 float r, float g, float b, double surface_z
57)
58{
60 .name = name,
61 .mjcf_path = "",
62 .shape = mj_kdl::Shape::BOX,
63 .size = { hx, hy, hz },
64 .pos = { x, y, surface_z + hz },
65 .rgba = { r, g, b, 1.0f },
66 .mass = kCubeMass,
67 .friction = { kPickFriction[0], kPickFriction[1], kPickFriction[2] },
68 };
69}
70
71static mj_kdl::SceneObject make_sphere(
72 const char *name, double x, double y, double radius,
73 float r, float g, float b, double surface_z
74)
75{
77 .name = name,
78 .mjcf_path = "",
79 .shape = mj_kdl::Shape::SPHERE,
80 .size = { radius, 0.0, 0.0 },
81 .pos = { x, y, surface_z + radius },
82 .rgba = { r, g, b, 1.0f },
83 .mass = kSphereMass,
84 .friction = { kPickFriction[0], kPickFriction[1], kPickFriction[2] },
85 };
86}
87
88class TableSceneTest : public testing::Test
89{
90 protected:
91 std::string mjcf_;
94 std::string table_mount_site_; // compiled site name; lifetime backs RobotSpec.attach_to.name
95 mjModel *model_ = nullptr;
96 mjData *data_ = nullptr;
98 bool s_cleaned_ = false;
99
100 std::unique_ptr<KDL::ChainFkSolverPos_recursive> fk_;
101 std::unique_ptr<KDL::ChainDynParam> dyn_;
102
103 unsigned n_ = 0;
104 KDL::JntArray q_home_;
105
106 void SetUp() override
107 {
108 mjcf_ = mj_kdl_examples::find_menagerie_model("kinova_gen3/gen3.xml");
109 if (!fs::exists(mjcf_)) {
110 GTEST_SKIP() << mjcf_ << " not found";
111 return;
112 }
113 std::string table_mjcf = mj_kdl_examples::find_asset("table.xml");
114
115 // Table asset origin is the tabletop surface center; only the table
116 // itself carries a world-frame z. Robot and objects derive their
117 // height from attach_to.
118 const double surface_z = 0.7;
119
120 table_obj_ = {
121 .name = "table",
122 .mjcf_path = table_mjcf,
123 .pos = { 0.0, 0.0, surface_z },
124 .fixed = true,
125 };
127
128 std::vector<mj_kdl::SceneObject> objects;
129 objects.push_back(table_obj_);
130 objects.push_back(make_box("red_cube", 0.35, 0.10, 0.03, 0.03, 0.03, 1.0f, 0.2f, 0.2f, surface_z));
131 objects.push_back(make_box("green_cube", 0.35, -0.10, 0.03, 0.03, 0.03, 0.2f, 1.0f, 0.2f, surface_z));
132 objects.push_back(make_box("blue_cube", 0.35, 0.30, 0.04, 0.04, 0.04, 0.2f, 0.2f, 1.0f, surface_z));
133 objects.push_back(make_sphere("orange_sphere", -0.20, 0.20, 0.035, 1.0f, 0.55f, 0.0f, surface_z));
134 objects.push_back(make_sphere("purple_sphere", -0.20, -0.20, 0.025, 0.7f, 0.0f, 0.9f, surface_z));
135
136 spec_.objects = objects;
137 spec_.timestep = 0.002;
138 spec_.gravity_z = -9.81;
139 spec_.add_floor = true;
140 spec_.add_skybox = true;
141
142 spec_.robots.push_back(mj_kdl::RobotSpec{
143 .path = mjcf_.c_str(),
144 .attach_to = { mj_kdl::AttachKind::Site, table_mount_site_.c_str() },
145 .attachments = {},
146 });
147
148 ASSERT_TRUE(mj_kdl::build_scene(&model_, &data_, &spec_));
149 KDL::Frame world_T_table_top;
150 ASSERT_TRUE(mj_kdl::get_site_frame(model_, data_, table_mount_site_.c_str(), &world_T_table_top));
151 EXPECT_NEAR(world_T_table_top.p.z(), surface_z, 1e-9);
152
153 ASSERT_TRUE(mj_kdl::init_robot_from_mjcf(&s_, model_, data_, "base_link", "bracelet_link"));
154
155 n_ = static_cast<unsigned>(s_.n_joints);
156
157 fk_ = std::make_unique<KDL::ChainFkSolverPos_recursive>(s_.chain);
158 dyn_ = std::make_unique<KDL::ChainDynParam>(s_.chain, KDL::Vector(0, 0, spec_.gravity_z));
159
160 q_home_.resize(n_);
161 for (unsigned i = 0; i < n_; ++i) q_home_(i) = kHomePose[i];
163 mj_forward(model_, data_);
164 }
165
166 void TearDown() override
167 {
170 }
171};
172
173TEST_F(TableSceneTest, GravityCompDrift)
174{
175 KDL::Frame ee_init;
176 fk_->JntToCart(q_home_, ee_init);
177
178 s_.ctrl_mode = mj_kdl::CtrlMode::TORQUE;
179 KDL::JntArray q(n_), g(n_);
180 // Prime jnt_trq_cmd so the first update() applies compensation immediately.
181 dyn_->JntToGravity(q_home_, g);
182 for (unsigned j = 0; j < n_; ++j) s_.jnt_trq_cmd[j] = g(j);
183 for (int i = 0; i < 500; ++i) {
184 mj_kdl::update(&s_);
185 for (unsigned j = 0; j < n_; ++j) q(j) = s_.jnt_pos_msr[j];
186 dyn_->JntToGravity(q, g);
187 for (unsigned j = 0; j < n_; ++j) s_.jnt_trq_cmd[j] = g(j);
188 mj_kdl::step(&s_);
189 }
190
191 KDL::JntArray q_end(n_);
192 KDL::Frame ee_end;
193 for (unsigned j = 0; j < n_; ++j) q_end(j) = s_.jnt_pos_msr[j];
194 fk_->JntToCart(q_end, ee_end);
195 double drift = (ee_init.p - ee_end.p).Norm();
196
197 ASSERT_LE(drift, 0.001) << "drift " << drift * 1000.0 << " mm exceeds 1 mm threshold";
198}
199
200TEST_F(TableSceneTest, AddRemoveObject)
201{
202 mj_kdl::cleanup(&s_);
203 s_cleaned_ = true;
204
205 const double surface_z = 0.7;
206 mj_kdl::SceneObject extra =
207 make_box("yellow_cube", 0.0, 0.4, 0.03, 0.03, 0.03, 1.0f, 1.0f, 0.0f, surface_z);
208
209 ASSERT_TRUE(mj_kdl::scene_add_object(&model_, &data_, &spec_, extra))
210 << "scene_add_object() returned false";
211
212 ASSERT_TRUE(mj_kdl::scene_remove_object(&model_, &data_, &spec_, "yellow_cube"))
213 << "scene_remove_object() returned false";
214}
215
216TEST_F(TableSceneTest, EnvAddRemoveReinitsRobot)
217{
218 mj_kdl::Env env;
219 env.spec = spec_;
220 env.model = model_;
221 env.data = data_;
222 mj_kdl::env_add_robot(&env, &s_);
223 // env now owns the rebuild lifecycle; null out fixture pointers to prevent double-free
224 model_ = nullptr;
225 data_ = nullptr;
226
227 int nq_before = env.model->nq;
228
229 const double surface_z = 0.7;
230 mj_kdl::SceneObject extra =
231 make_box("yellow_cube", 0.0, 0.4, 0.03, 0.03, 0.03, 1.0f, 1.0f, 0.0f, surface_z);
232
233 ASSERT_TRUE(mj_kdl::scene_add_object(&env, extra)) << "Env scene_add_object() failed";
234 EXPECT_GT(env.model->nq, nq_before) << "nq should grow after adding a free object";
235 EXPECT_EQ(s_.n_joints, 7) << "robot chain should still have 7 joints after rebuild";
236 EXPECT_EQ(s_.model, env.model) << "robot model pointer should be updated";
237
238 ASSERT_TRUE(mj_kdl::scene_remove_object(&env, "yellow_cube")) << "Env scene_remove_object() failed";
239 EXPECT_EQ(env.model->nq, nq_before) << "nq should return to original after removal";
240 EXPECT_EQ(s_.n_joints, 7) << "robot chain should still have 7 joints after removal";
241
242 mj_kdl::cleanup(&env);
243 s_cleaned_ = true;
244}
245
246int main(int argc, char *argv[])
247{
248 testing::InitGoogleTest(&argc, argv);
249 return RUN_ALL_TESTS();
250}
KDL::JntArray q_home_
mj_kdl::SceneObject table_obj_
std::string table_mount_site_
void SetUp() override
void TearDown() override
std::unique_ptr< KDL::ChainDynParam > dyn_
mj_kdl::SceneSpec spec_
std::unique_ptr< KDL::ChainFkSolverPos_recursive > fk_
void env_add_robot(Env *env, Robot *robot)
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)
std::string find_asset(const fs::path &relative)
std::string find_menagerie_model(const fs::path &relative)
std::vector< SceneObject > objects
int main(int argc, char *argv[])
TEST_F(TableSceneTest, GravityCompDrift)
TEST(SceneObjectTransform, PathBackedObjectAppliesQuat)