Mujoco KDL Wrapper  0.5.1
MuJoCo + KDL bridge for robot kinematics and dynamics
Loading...
Searching...
No Matches
test_mjcf_load.cpp
Go to the documentation of this file.
1/* test_mjcf_load.cpp
2 * Building scenes: the Kinova GEN3 alone and with a Robotiq 2F-85 (KDL chain vs the MuJoCo
3 * frames, joint limits, saving the model), scene cameras and sites, contact exclusions, attaching
4 * to a body or a named frame, a relative meshdir, and joints a chain must refuse. */
5
7#include "common.hpp"
8#include "example_paths.hpp"
9
10#include <gtest/gtest.h>
11
12#include <kdl/chainfksolverpos_recursive.hpp>
13
14#include <cmath>
15#include <filesystem>
16#include <string>
17
18namespace ex = mj_kdl_examples;
19namespace fs = std::filesystem;
20
21static mj_kdl::SceneSpec bare_scene()
22{
24 sc.timestep = 0.002;
25 sc.add_floor = false;
26 sc.add_skybox = false;
27 return sc;
28}
29
30// The frame of a body or site expressed in base_link's frame.
31static KDL::Frame in_base(mj_kdl::Env &env, const KDL::Frame &world_T_x)
32{
33 KDL::Frame world_T_base;
34 mj_kdl::get_body_frame(&env, "base_link", &world_T_base);
35 return world_T_base.Inverse() * world_T_x;
36}
37
38// The bundled arm alone.
39class MjcfLoadTest : public testing::Test
40{
41 protected:
44
45 void SetUp() override
46 {
47 const std::string mjcf = ex::find_asset("kinova_gen3/gen3.xml");
48 if (!fs::exists(mjcf)) GTEST_SKIP() << mjcf << " not found";
49 mj_kdl::SceneSpec sc = bare_scene();
51 rs.path = mjcf;
52 sc.robots.push_back(rs);
53 ASSERT_TRUE(mj_kdl::init_env(&env_, &sc));
54 ASSERT_EQ(env_.model->nv, 7);
55 ASSERT_TRUE(mj_kdl::init_robot_from_mjcf(&s_, &env_, "base_link", "bracelet_link"));
56 ASSERT_EQ(s_.n_joints, 7);
58 }
59};
60
61TEST_F(MjcfLoadTest, ChainFkMatchesTheModelFrames)
62{
64 KDL::Frame fk_tip, world_T_tip;
65 ASSERT_GE(fk.JntToCart(ex::home_q(7), fk_tip), 0);
66 ASSERT_TRUE(mj_kdl::get_body_frame(&env_, "bracelet_link", &world_T_tip));
67 EXPECT_TRUE(KDL::Equal(fk_tip, in_base(env_, world_T_tip), 1e-9));
68}
69
70TEST_F(MjcfLoadTest, JointLimitsFollowTheModel)
71{
72 ASSERT_EQ(s_.joint_limits.size(), 7u);
73 int unlimited = 0;
74 for (int i = 0; i < 7; ++i) {
75 const int jid = mj_name2id(env_.model, mjOBJ_JOINT, s_.joint_names[i].c_str());
76 ASSERT_GE(jid, 0);
77 if (env_.model->jnt_limited[jid]) {
78 EXPECT_DOUBLE_EQ(s_.joint_limits[i].first, env_.model->jnt_range[2 * jid]);
79 EXPECT_DOUBLE_EQ(s_.joint_limits[i].second, env_.model->jnt_range[2 * jid + 1]);
80 } else {
81 EXPECT_TRUE(std::isinf(s_.joint_limits[i].first) && s_.joint_limits[i].first < 0);
82 EXPECT_TRUE(std::isinf(s_.joint_limits[i].second) && s_.joint_limits[i].second > 0);
83 ++unlimited;
84 }
85 }
86 EXPECT_GT(unlimited, 0) << "Gen3 has continuous joints";
87}
88
89TEST_F(MjcfLoadTest, SaveModelXmlRoundTrips)
90{
91 const int bid = mj_name2id(env_.model, mjOBJ_BODY, "base_link");
92 ASSERT_GE(bid, 0);
93 env_.model->body_mass[bid] = 1.77;
94
95 const fs::path path = fs::temp_directory_path() / "mj_kdl_save_model_xml_test.xml";
96 ASSERT_TRUE(mj_kdl::save_model_xml(env_.model, path.c_str()));
97
98 char err[1000] = {};
99 mjModel *loaded = mj_loadXML(path.c_str(), nullptr, err, sizeof(err));
100 fs::remove(path);
101 ASSERT_NE(loaded, nullptr) << err;
102 EXPECT_EQ(loaded->nq, env_.model->nq);
103 EXPECT_EQ(loaded->nbody, env_.model->nbody);
104 const int loaded_bid = mj_name2id(loaded, mjOBJ_BODY, "base_link");
105 ASSERT_GE(loaded_bid, 0);
106 EXPECT_DOUBLE_EQ(loaded->body_mass[loaded_bid], 1.77) << "runtime change was saved";
107 mj_deleteModel(loaded);
108}
109
110// The arm with the bundled 2F-85 on its pinch site.
111class MjcfGripperTest : public testing::Test
112{
113 protected:
114 std::string arm_;
115 std::string gripper_;
118
119 void SetUp() override
120 {
121 arm_ = ex::find_asset("kinova_gen3/gen3.xml");
122 gripper_ = ex::find_asset("robotiq_2f85/2f85.xml");
123 if (!fs::exists(arm_)) GTEST_SKIP() << arm_ << " not found";
124 if (!fs::exists(gripper_)) GTEST_SKIP() << gripper_ << " not found";
125 sc_ = bare_scene();
127 rs.path = arm_;
129 sc_.robots.push_back(rs);
130 }
131
132 int body(const char *name) const { return mj_name2id(env_.model, mjOBJ_BODY, name); }
133};
134
135TEST_F(MjcfGripperTest, TcpChainMatchesThePinchSite)
136{
137 ASSERT_TRUE(mj_kdl::init_env(&env_, &sc_));
139 mj_kdl::Robot robot;
140 ASSERT_TRUE(mj_kdl::init_robot_from_mjcf(&robot, &env_, "base_link", "bracelet_link", "", &tool)
141 );
142 ASSERT_EQ(robot.n_joints, 7);
144
146 KDL::Frame fk_tcp, world_T_pinch;
147 ASSERT_GE(fk.JntToCart(ex::home_q(7), fk_tcp), 0);
148 ASSERT_TRUE(mj_kdl::get_site_frame(&env_, "g_pinch", &world_T_pinch));
149 EXPECT_TRUE(KDL::Equal(fk_tcp, in_base(env_, world_T_pinch), 1e-9));
150}
151
152TEST_F(MjcfGripperTest, GripperDriverRangeAndCtrlrange)
153{
154 ASSERT_TRUE(mj_kdl::init_env(&env_, &sc_));
155 const int rdriver = mj_name2id(env_.model, mjOBJ_JOINT, "g_right_driver_joint");
156 const int fingers = mj_name2id(env_.model, mjOBJ_ACTUATOR, "g_fingers_actuator");
157 ASSERT_GE(rdriver, 0);
158 ASSERT_GE(fingers, 0);
159 EXPECT_DOUBLE_EQ(env_.model->jnt_range[2 * rdriver], 0.0);
160 EXPECT_DOUBLE_EQ(env_.model->jnt_range[2 * rdriver + 1], 0.8);
161 EXPECT_DOUBLE_EQ(env_.model->actuator_ctrlrange[2 * fingers + 1], ex::kGripperClosed);
162}
163
164TEST_F(MjcfGripperTest, JointSlotReadsAGripperJointByName)
165{
166 ASSERT_TRUE(mj_kdl::init_env(&env_, &sc_));
167 const int rdriver = mj_name2id(env_.model, mjOBJ_JOINT, "g_right_driver_joint");
168 const int ldriver = mj_name2id(env_.model, mjOBJ_JOINT, "g_left_driver_joint");
169 ASSERT_GE(rdriver, 0);
170 ASSERT_GE(ldriver, 0);
171 env_.data->qpos[env_.model->jnt_qposadr[rdriver]] = 0.42;
172 env_.data->qpos[env_.model->jnt_qposadr[ldriver]] = 0.42;
173
174 mj_kdl::SceneJointSlot *slot = mj_kdl::bind_scene_joint(&env_.scene, "g_right_driver_joint");
175 ASSERT_NE(slot, nullptr);
176 mj_kdl::update(&env_);
177 EXPECT_NEAR(slot->position, 0.42, 1e-9);
178
179 EXPECT_EQ(mj_kdl::bind_scene_joint(&env_.scene, "no_such_joint"), nullptr);
180}
181
182TEST_F(MjcfGripperTest, ContactExclusionsAreAdded)
183{
184 mjModel *plain = nullptr;
185 mjData *data = nullptr;
186 ASSERT_TRUE(mj_kdl::build_scene(&plain, &data, &sc_));
187 const int nexclude = plain->nexclude;
188 mj_kdl::destroy_scene(plain, data);
189
190 sc_.robots[0].attachments[0].contact_exclusions = { { "bracelet_link", "g_base" } };
191 ASSERT_TRUE(mj_kdl::init_env(&env_, &sc_));
192 ASSERT_EQ(env_.model->nexclude, nexclude + 1);
193 const int b1 = body("bracelet_link"), b2 = body("g_base");
194 ASSERT_GE(b1, 0);
195 ASSERT_GE(b2, 0);
196 bool found = false;
197 for (int i = 0; i < env_.model->nexclude; ++i) {
198 const int sig = env_.model->exclude_signature[i];
199 found |= sig == (b1 << 16) + b2 || sig == (b2 << 16) + b1;
200 }
201 EXPECT_TRUE(found) << "bracelet_link <-> g_base excluded";
202}
203
204TEST_F(MjcfGripperTest, AttachToABodyWithAnOffset)
205{
206 mj_kdl::AttachmentSpec &gripper = sc_.robots[0].attachments[0];
207 gripper.attach_to = { mj_kdl::AttachKind::Body, "bracelet_link" };
208 gripper.pos[2] = 0.02;
209 ASSERT_TRUE(mj_kdl::init_env(&env_, &sc_));
210
211 const int mount = body("g_base_mount");
212 ASSERT_GE(mount, 0);
213 EXPECT_EQ(env_.model->body_parentid[mount], body("bracelet_link"));
214 KDL::Frame world_T_bracelet, world_T_mount;
215 ASSERT_TRUE(mj_kdl::get_body_frame(&env_, "bracelet_link", &world_T_bracelet));
216 ASSERT_TRUE(mj_kdl::get_body_frame(&env_, "g_base_mount", &world_T_mount));
217 // The offset adds to the mount's own 7 mm in 2f85.xml.
218 const KDL::Vector p = (world_T_bracelet.Inverse() * world_T_mount).p;
219 EXPECT_NEAR(p.x(), 0.0, 1e-9);
220 EXPECT_NEAR(p.y(), 0.0, 1e-9);
221 EXPECT_NEAR(p.z(), 0.027, 1e-9);
222}
223
224TEST_F(MjcfGripperTest, CamerasAreAddedWhereTheSpecSays)
225{
226 mj_kdl::CameraSpec world_cam;
227 world_cam.name = "overview";
228 world_cam.pos[0] = 1.0;
229 world_cam.pos[1] = 0.0;
230 world_cam.pos[2] = 1.5;
231 // 90 deg about z, [x, y, z, w].
232 world_cam.quat[2] = std::sqrt(0.5);
233 world_cam.quat[3] = std::sqrt(0.5);
234 world_cam.fovy = 50.0;
235 mj_kdl::CameraSpec wrist_cam;
236 wrist_cam.name = "mount_cam";
237 wrist_cam.body = "bracelet_link";
238 wrist_cam.pos[0] = wrist_cam.pos[1] = 0.0;
239 wrist_cam.pos[2] = 0.05;
240 wrist_cam.fovy = 60.0;
241 sc_.cameras = { world_cam, wrist_cam };
242 ASSERT_TRUE(mj_kdl::init_env(&env_, &sc_));
243
244 const mjModel *m = env_.model;
245 const int a = mj_name2id(m, mjOBJ_CAMERA, "overview");
246 const int b = mj_name2id(m, mjOBJ_CAMERA, "mount_cam");
247 ASSERT_GE(a, 0);
248 ASSERT_GE(b, 0);
249 EXPECT_EQ(m->cam_bodyid[a], 0);
250 EXPECT_EQ(m->cam_bodyid[b], body("bracelet_link"));
251 EXPECT_DOUBLE_EQ(m->cam_fovy[a], 50.0);
252 EXPECT_DOUBLE_EQ(m->cam_fovy[b], 60.0);
253 EXPECT_DOUBLE_EQ(m->cam_pos[3 * a + 2], 1.5);
254 EXPECT_DOUBLE_EQ(m->cam_pos[3 * b + 2], 0.05);
255 // MuJoCo stores [w, x, y, z].
256 EXPECT_NEAR(m->cam_quat[4 * a + 0], std::sqrt(0.5), 1e-12);
257 EXPECT_NEAR(m->cam_quat[4 * a + 3], std::sqrt(0.5), 1e-12);
258}
259
260TEST_F(MjcfGripperTest, SitesAreAddedButAnAuthoredSiteWins)
261{
262 mj_kdl::SiteSpec mark;
263 mark.body = "bracelet_link";
264 mark.name = "mark";
265 mark.pos[2] = 0.1;
266 mark.quat[2] = std::sqrt(0.5);
267 mark.quat[3] = std::sqrt(0.5);
268 mj_kdl::SiteSpec clash;
269 clash.body = "base_link";
270 clash.name = "pinch_site";
271 sc_.sites = { mark, clash };
272 ASSERT_TRUE(mj_kdl::init_env(&env_, &sc_));
273
274 KDL::Frame world_T_bracelet, world_T_mark;
275 ASSERT_TRUE(mj_kdl::get_body_frame(&env_, "bracelet_link", &world_T_bracelet));
276 ASSERT_TRUE(mj_kdl::get_site_frame(&env_, "mark", &world_T_mark));
277 const KDL::Frame expected(KDL::Rotation::RotZ(M_PI / 2), KDL::Vector(0.0, 0.0, 0.1));
278 EXPECT_TRUE(KDL::Equal(world_T_bracelet.Inverse() * world_T_mark, expected, 1e-9));
279
280 const int pinch = mj_name2id(env_.model, mjOBJ_SITE, "pinch_site");
281 ASSERT_GE(pinch, 0);
282 EXPECT_NE(env_.model->site_bodyid[pinch], body("base_link"));
283}
284
285TEST(MjcfPathTest, RelativeModelPathWithRelativeMeshdir)
286{
287 const fs::path model = fs::path(MJ_KDL_TEST_FIXTURES) / "meshdir/mjcf/mesh_link.xml";
288 const std::string relative = fs::relative(model).string();
289 ASSERT_FALSE(fs::path(relative).is_absolute());
290
291 mj_kdl::SceneSpec spec = bare_scene();
293 rs.path = relative;
294 spec.robots.push_back(rs);
295
296 mjModel *m = nullptr;
297 mjData *d = nullptr;
298 ASSERT_TRUE(mj_kdl::build_scene(&m, &d, &spec)) << relative;
299 EXPECT_EQ(m->nmesh, 1);
301}
302
303class JointEdgeCaseTest : public testing::Test
304{
305 protected:
306 mj_kdl::SceneSpec spec_ = bare_scene();
309
310 void SetUp() override
311 {
313 rs.path = std::string(MJ_KDL_TEST_FIXTURES) + "/joint_edge_cases.xml";
314 rs.modes = {};
315 spec_.robots.push_back(rs);
316 }
317};
318
319TEST_F(JointEdgeCaseTest, ChainRefusesABodyWithTwoJoints)
320{
321 ASSERT_TRUE(mj_kdl::init_env(&env_, &spec_));
322 EXPECT_FALSE(mj_kdl::init_robot_from_mjcf(&robot_, &env_, "two_base", "two_tip"));
323 EXPECT_TRUE(env_.robots.empty());
324}
325
326TEST_F(JointEdgeCaseTest, ChainRefusesABallJointOnThePath)
327{
328 ASSERT_TRUE(mj_kdl::init_env(&env_, &spec_));
329 EXPECT_FALSE(mj_kdl::init_robot_from_mjcf(&robot_, &env_, "ball_base", "ball_tip"));
330}
331
332TEST_F(JointEdgeCaseTest, PlainHingeChainStillBuilds)
333{
334 ASSERT_TRUE(mj_kdl::init_env(&env_, &spec_));
335 ASSERT_TRUE(mj_kdl::init_robot_from_mjcf(&robot_, &env_, "plain_base", "plain_tip"));
336 EXPECT_EQ(robot_.n_joints, 1);
337}
338
339TEST_F(JointEdgeCaseTest, JointSlotRefusesWhatIsNotAScalarJoint)
340{
341 ASSERT_TRUE(mj_kdl::init_env(&env_, &spec_));
342 EXPECT_NE(mj_kdl::bind_scene_joint(&env_.scene, "plain"), nullptr);
343 EXPECT_EQ(mj_kdl::bind_scene_joint(&env_.scene, "ball"), nullptr);
344}
345
346TEST(AttachToFrame, ARobotStandsOnAnObjectsNamedFrame)
347{
348 const std::string arm = ex::find_asset("kinova_gen3/gen3.xml");
349 if (!fs::exists(arm)) GTEST_SKIP() << arm << " not found";
350
351 // Objects are added before robots, so a robot can stand on an object's frame.
352 mj_kdl::SceneSpec spec = bare_scene();
354 stand.name = "stand";
355 stand.mjcf_path = std::string(MJ_KDL_TEST_FIXTURES) + "/joint_edge_cases.xml";
356 stand.fixed = true;
357 spec.objects.push_back(stand);
359 rs.path = arm;
360 rs.attach_to = { mj_kdl::AttachKind::Frame, "plain_frame" };
361 rs.pos[0] = 0.05;
362 spec.robots.push_back(rs);
363
364 mj_kdl::Env env;
365 ASSERT_TRUE(mj_kdl::init_env(&env, &spec));
366 const int base = mj_name2id(env.model, mjOBJ_BODY, "base_link");
367 ASSERT_GE(base, 0);
368 EXPECT_EQ(env.model->body_parentid[base], mj_name2id(env.model, mjOBJ_BODY, "plain_base"));
369 // plain_base at (2, 0, 0); the frame 0.1 along y, 0.2 up, turned 90 deg about z.
370 KDL::Frame world_T_base;
371 ASSERT_TRUE(mj_kdl::get_body_frame(&env, "base_link", &world_T_base));
372 EXPECT_TRUE(KDL::Equal(world_T_base.p, KDL::Vector(2.0, 0.15, 0.2), 1e-9));
373}
374
375int main(int argc, char *argv[])
376{
377 testing::InitGoogleTest(&argc, argv);
378 return RUN_ALL_TESTS();
379}
mj_kdl::SceneSpec spec_
void SetUp() override
virtual int JntToCart(const JntArray &q_in, Frame &p_out, int segmentNr=-1)
Frame Inverse() const
static Rotation RotZ(double angle)
double z() const
double x() const
double y() const
void SetUp() override
mj_kdl::SceneSpec sc_
int body(const char *name) const
mj_kdl::Env env_
void SetUp() override
mj_kdl::Robot s_
Status init_env(Env *env, const SceneSpec *spec)
void update(Env *env)
Status init_robot_from_mjcf(Robot *r, Env *env, 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 get_body_frame(Env *env, const char *body_name, KDL::Frame *out)
Status build_scene(mjModel **out_model, mjData **out_data, const SceneSpec *spec)
bool get_site_frame(Env *env, const char *site_name, KDL::Frame *out)
void destroy_scene(mjModel *model, mjData *data)
Status save_model_xml(const mjModel *model, const char *path)
SceneJointSlot * bind_scene_joint(SceneState *s, const char *joint_name)
IMETHOD bool Equal(const FrameAcc &r1, const FrameAcc &r2, double eps=epsilon)
constexpr double kGripperClosed
Definition common.hpp:19
mj_kdl::ToolFrameSpec gripper_tool(const std::string &prefix="")
Definition common.hpp:159
std::string find_asset(const fs::path &relative)
mj_kdl::AttachmentSpec gripper_attachment(const std::string &path, const std::string &site="pinch_site")
Definition common.hpp:149
KDL::JntArray home_q(unsigned n)
Definition common.hpp:68
std::vector< AttachmentSpec > attachments
std::vector< CtrlModeSpec > modes
std::vector< RobotSpec > robots
std::vector< SceneObject > objects
int main(int argc, char *argv[])
TEST_F(MjcfLoadTest, ChainFkMatchesTheModelFrames)
TEST(MjcfPathTest, RelativeModelPathWithRelativeMeshdir)