Mujoco KDL Wrapper  0.5.1
MuJoCo + KDL bridge for robot kinematics and dynamics
Loading...
Searching...
No Matches
test_scene_state.cpp
Go to the documentation of this file.
1/* test_scene_state.cpp
2 * Env scene slots: free-body poses sampled from qpos rather than the derived body frames, scalar
3 * scene joints, wrench and actuator slots, what update() reads and applies, and what reset()
4 * restores. Self-skips when a bundled asset is missing. */
5
6#include <gtest/gtest.h>
7
9#include "example_paths.hpp"
10
11#include <atomic>
12#include <chrono>
13#include <cstring>
14#include <filesystem>
15#include <string>
16#include <thread>
17#include <vector>
18
19namespace fs = std::filesystem;
20
21static constexpr int kFillerBodies = 50;
22
23static mj_kdl::SceneObject fixed_box(const std::string &name, double half, double x, double y)
24{
26 box.name = name;
28 box.pos[0] = x;
29 box.pos[1] = y;
30 box.pos[2] = 0.5;
31 box.fixed = true;
32 for (int k = 0; k < 3; ++k) box.size[k] = half;
33 for (int k = 0; k < 3; ++k) box.rgba[k] = 0.5f;
34 box.rgba[3] = 1.0f;
35 box.friction[0] = 1.0;
36 box.friction[1] = 0.005;
37 box.friction[2] = 0.0001;
38 return box;
39}
40
41class SceneStateTest : public testing::Test
42{
43 protected:
46 mjModel *model_ = nullptr;
47 mjData *data_ = nullptr;
49
50 void SetUp() override
51 {
52 const std::string mjcf = mj_kdl_examples::find_asset("kinova_gen3/gen3.xml");
53 const std::string cube = mj_kdl_examples::find_asset("cube.xml");
54 if (!fs::exists(mjcf) || !fs::exists(cube)) {
55 GTEST_SKIP() << "kinova_gen3/gen3.xml or cube.xml missing from assets/";
56 return;
57 }
58
59 spec_.timestep = 0.002;
60 spec_.add_floor = true;
61 spec_.add_skybox = true;
63 rs.path = mjcf;
64 spec_.robots.push_back(rs);
65 mj_kdl::SceneObject falling;
66 falling.name = "cube";
67 falling.mjcf_path = cube;
68 falling.pos[0] = 0.6;
69 falling.pos[2] = 1.0;
70 spec_.objects.push_back(falling);
71 spec_.objects.push_back(fixed_box("block", 0.02, -0.6, 0.0));
72 for (int i = 0; i < kFillerBodies; ++i)
73 spec_.objects.push_back(fixed_box("filler_" + std::to_string(i), 0.01, -1.0, 0.05 * i));
74
75 ASSERT_TRUE(mj_kdl::init_env(&env_, &spec_));
76 model_ = env_.model;
77 data_ = env_.data;
78 ASSERT_TRUE(mj_kdl::init_robot_from_mjcf(&robot_, &env_, "base_link", "bracelet_link"));
79 }
80
81 int actuator(int joint, int group) const
82 {
83 const int jid = mj_name2id(model_, mjOBJ_JOINT, robot_.joint_names[joint].c_str());
84 for (int a = 0; a < model_->nu; ++a) {
85 if (model_->actuator_trnid[2 * a] == jid && model_->actuator_group[a] == group)
86 return a;
87 }
88 return -1;
89 }
90};
91
92TEST_F(SceneStateTest, FreeBodyPoseAndDerivedFrameAgreeAfterAStep)
93{
95 ASSERT_NE(cube, nullptr);
96
97 for (int i = 0; i < 3; ++i) mj_kdl::step(&env_);
98 mj_kdl::update(&env_);
99
100 const double *q = data_->qpos + cube->qpos_adr;
101 EXPECT_EQ(cube->pose.p.x(), q[0]);
102 EXPECT_EQ(cube->pose.p.y(), q[1]);
103 EXPECT_EQ(cube->pose.p.z(), q[2]);
104 EXPECT_EQ(cube->seq, 1u);
105
106 // The cube is falling, so a frame one step behind qpos would differ by ~1e-4.
107 KDL::Frame derived;
108 ASSERT_TRUE(mj_kdl::get_body_frame(&env_, "cube", &derived));
109 EXPECT_NEAR(derived.p.x(), cube->pose.p.x(), 1e-12);
110 EXPECT_NEAR(derived.p.y(), cube->pose.p.y(), 1e-12);
111 EXPECT_NEAR(derived.p.z(), cube->pose.p.z(), 1e-12);
112}
113
114TEST_F(SceneStateTest, AFrameFollowsAQposWrittenDirectly)
115{
116 const mj_kdl::SceneFreeBodySlot *slot = mj_kdl::bind_scene_free_body(&env_.scene, "cube");
117 ASSERT_NE(slot, nullptr);
118 const int adr = slot->qpos_adr;
119 KDL::Frame cube;
120 ASSERT_TRUE(mj_kdl::get_body_frame(&env_, "cube", &cube));
121
122 data_->qpos[adr + 2] = 2.5;
123 ASSERT_TRUE(mj_kdl::get_body_frame(&env_, "cube", &cube));
124 EXPECT_NEAR(cube.p.z(), 2.5, 1e-12);
125}
126
127TEST_F(SceneStateTest, StepMatchesMjStepBitwise)
128{
129 mjData *reference = mj_makeData(model_);
130 mj_copyData(reference, model_, data_);
131 for (int i = 0; i < 200; ++i) {
132 mj_kdl::step(&env_);
133 mj_step(model_, reference);
134 }
135 EXPECT_EQ(std::memcmp(data_->qpos, reference->qpos, sizeof(mjtNum) * model_->nq), 0);
136 EXPECT_EQ(std::memcmp(data_->qvel, reference->qvel, sizeof(mjtNum) * model_->nv), 0);
137 mj_deleteData(reference);
138}
139
140TEST_F(SceneStateTest, StepHonoursAQposWrittenBetweenSteps)
141{
142 const mj_kdl::SceneFreeBodySlot *slot = mj_kdl::bind_scene_free_body(&env_.scene, "cube");
143 ASSERT_NE(slot, nullptr);
144 const int adr = slot->qpos_adr;
145 mj_kdl::step(&env_);
146 mjData *reference = mj_makeData(model_);
147 mj_copyData(reference, model_, data_);
148
149 data_->qpos[adr + 2] = reference->qpos[adr + 2] = 2.0;
150 mj_kdl::step(&env_);
151 mj_step(model_, reference);
152 EXPECT_EQ(data_->qpos[adr + 2], reference->qpos[adr + 2]);
153
154 KDL::Frame cube;
155 ASSERT_TRUE(mj_kdl::get_body_frame(&env_, "cube", &cube));
156 EXPECT_NEAR(cube.p.z(), data_->qpos[adr + 2], 1e-12);
157 mj_deleteData(reference);
158}
159
160// Opens a Simulate window, so it is opt-in: --gtest_also_run_disabled_tests.
161TEST_F(SceneStateTest, DISABLED_ViewerKeepsUserWrenchesWhileAnotherThreadReads)
162{
163 ASSERT_TRUE(mj_kdl::open_viewer(&env_, "viewer lock test"));
164
165 mj_kdl::SceneWrenchSlot *push = mj_kdl::bind_scene_wrench(&env_.scene, "cube");
166 ASSERT_NE(push, nullptr);
167 push->wrench = KDL::Wrench(KDL::Vector(0.0, 0.0, 5.0), KDL::Vector::Zero());
168 const int fz = 6 * push->body_id + 2;
169
170 std::atomic<bool> stop{ false };
171 std::thread reader([&] {
172 KDL::Frame frame;
173 while (!stop) mj_kdl::get_body_frame(&env_, "cube", &frame);
174 });
175
176 int lost = 0;
177 for (int i = 0; i < 1000; ++i) {
178 mj_kdl::update(&env_);
179 std::this_thread::sleep_for(std::chrono::milliseconds(1)); // let the render thread run
180 if (data_->xfrc_applied[fz] != 5.0) ++lost;
181 ASSERT_TRUE(mj_kdl::step(&env_));
182 }
183 stop = true;
184 reader.join();
185 EXPECT_EQ(lost, 0) << "the render thread cleared a user wrench";
186}
187
188TEST_F(SceneStateTest, BindFreeBodyRejectsFixedUnknownAndDuplicate)
189{
190 const auto level = mj_kdl::get_log_level();
192
193 EXPECT_EQ(mj_kdl::bind_scene_free_body(&env_.scene, "block"), nullptr);
194 EXPECT_EQ(mj_kdl::bind_scene_free_body(&env_.scene, "no_such_body"), nullptr);
195 ASSERT_NE(mj_kdl::bind_scene_free_body(&env_.scene, "cube"), nullptr);
196 EXPECT_EQ(mj_kdl::bind_scene_free_body(&env_.scene, "cube"), nullptr);
197
199}
200
201TEST_F(SceneStateTest, SceneJointTracksQposAndRejectsAFreeJoint)
202{
203 mj_kdl::SceneJointSlot *slot = mj_kdl::bind_scene_joint(&env_.scene, "joint_4");
204 ASSERT_NE(slot, nullptr);
205
206 KDL::JntArray q(robot_.n_joints);
207 for (int i = 0; i < robot_.n_joints; ++i) q(i) = 0.0;
208 q(3) = -1.25;
209 mj_kdl::set_joint_pos(&robot_, q);
210 mj_kdl::update(&env_);
211 EXPECT_NEAR(slot->position, -1.25, 1e-12);
212 EXPECT_EQ(slot->velocity, data_->qvel[slot->dof_adr]);
213
214 const auto level = mj_kdl::get_log_level();
216 EXPECT_EQ(mj_kdl::bind_scene_joint(&env_.scene, "cube_free"), nullptr);
218}
219
220TEST_F(SceneStateTest, ASlotAddressSurvivesFurtherBinds)
221{
222 mj_kdl::SceneWrenchSlot *first = mj_kdl::bind_scene_wrench(&env_.scene, "cube");
223 ASSERT_NE(first, nullptr);
224
225 for (int i = 0; i < kFillerBodies; ++i) {
226 ASSERT_NE(
227 mj_kdl::bind_scene_wrench(&env_.scene, ("filler_" + std::to_string(i)).c_str()), nullptr
228 );
229 }
230
231 EXPECT_EQ(first, &env_.scene.wrenches.front());
232 EXPECT_EQ(first->name, "cube");
233 EXPECT_EQ(env_.scene.wrenches.size(), static_cast<std::size_t>(kFillerBodies) + 1);
234}
235
236TEST_F(SceneStateTest, UpdateReadsThenAppliesRobotsAndSlots)
237{
238 mj_kdl::SceneWrenchSlot *push = mj_kdl::bind_scene_wrench(&env_.scene, "cube");
239 ASSERT_NE(push, nullptr);
240 push->wrench = KDL::Wrench(KDL::Vector(0.0, 0.0, 2.0), KDL::Vector::Zero());
241
243 for (int i = 0; i < robot_.n_joints; ++i) robot_.jnt_trq_cmd[i] = 0.1 * (i + 1);
244 mj_kdl::step(&env_);
245 mj_kdl::update(&env_);
246
247 for (int i = 0; i < robot_.n_joints; ++i) {
248 const int jid = mj_name2id(model_, mjOBJ_JOINT, robot_.joint_names[i].c_str());
249 EXPECT_EQ(robot_.jnt_pos_msr[i], data_->qpos[model_->jnt_qposadr[jid]]);
250 const int motor = actuator(i, 2); // robot 0's TORQUE group
251 ASSERT_GE(motor, 0);
252 EXPECT_DOUBLE_EQ(data_->ctrl[motor], 0.1 * (i + 1));
253 EXPECT_EQ(data_->qfrc_applied[model_->jnt_dofadr[jid]], 0.0);
254 }
255 EXPECT_EQ(data_->xfrc_applied[6 * push->body_id + 2], 2.0);
256}
257
258TEST_F(SceneStateTest, ApplyClearsAWrenchThatIsNoLongerPushed)
259{
260 mj_kdl::SceneWrenchSlot *slot = mj_kdl::bind_scene_wrench(&env_.scene, "cube");
261 ASSERT_NE(slot, nullptr);
262
263 slot->wrench = KDL::Wrench(KDL::Vector(1.0, -2.0, 3.0), KDL::Vector(0.4, 0.5, 0.6));
264 mj_kdl::update(&env_);
265 const double *applied = data_->xfrc_applied + 6 * slot->body_id;
266 EXPECT_EQ(applied[0], 1.0);
267 EXPECT_EQ(applied[5], 0.6);
268
269 slot->wrench = KDL::Wrench::Zero();
270 mj_kdl::update(&env_);
271 for (int k = 0; k < 6; ++k) EXPECT_EQ(applied[k], 0.0) << "component " << k;
272}
273
274TEST_F(SceneStateTest, ResetRestoresEverySlot)
275{
276 mj_kdl::SceneJointSlot *joint = mj_kdl::bind_scene_joint(&env_.scene, "joint_4");
277 mj_kdl::SceneFreeBodySlot *cube = mj_kdl::bind_scene_free_body(&env_.scene, "cube");
278 mj_kdl::SceneWrenchSlot *push = mj_kdl::bind_scene_wrench(&env_.scene, "cube");
279 mj_kdl::SceneActuatorSlot *drive = mj_kdl::bind_scene_actuator(&env_.scene, "joint_4");
280 ASSERT_NE(joint, nullptr);
281 ASSERT_NE(cube, nullptr);
282 ASSERT_NE(push, nullptr);
283 ASSERT_NE(drive, nullptr);
284
285 push->wrench = KDL::Wrench(KDL::Vector(1.0, 2.0, 3.0), KDL::Vector(4.0, 5.0, 6.0));
286 drive->command = 1e9;
287 for (int i = 0; i < 20; ++i) {
288 mj_kdl::update(&env_);
289 mj_kdl::step(&env_);
290 }
291 ASSERT_TRUE(drive->saturated);
292 ASSERT_GT(joint->seq, 1u);
293
294 mj_kdl::reset(&env_);
295
296 EXPECT_EQ(push->wrench, KDL::Wrench::Zero());
297 EXPECT_EQ(drive->command, data_->ctrl[drive->ctrl_id]);
298 EXPECT_FALSE(drive->saturated);
299 EXPECT_EQ(joint->seq, 1u) << "re-read once from the reset state";
300 EXPECT_EQ(joint->position, data_->qpos[joint->qpos_adr]);
301 EXPECT_EQ(cube->seq, 1u);
302 EXPECT_EQ(cube->pose.p.z(), data_->qpos[cube->qpos_adr + 2]);
303
304 mj_kdl::update(&env_);
305 for (int k = 0; k < 6; ++k) EXPECT_EQ(data_->xfrc_applied[6 * push->body_id + k], 0.0);
306}
307
308int main(int argc, char *argv[])
309{
310 testing::InitGoogleTest(&argc, argv);
311 return RUN_ALL_TESTS();
312}
double z() const
double x() const
static Vector Zero()
double y() const
static Wrench Zero()
mj_kdl::SceneSpec spec_
void SetUp() override
mj_kdl::Robot robot_
int actuator(int joint, int group) const
ResetInfo reset(Env *env, const ResetOptions *options=nullptr)
Status init_env(Env *env, const SceneSpec *spec)
void update(Env *env)
bool step(Env *env)
void set_log_level(LogLevel level)
LogLevel get_log_level()
Status set_control_mode(Robot *r, CtrlMode mode)
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)
SceneWrenchSlot * bind_scene_wrench(SceneState *s, const char *body_name)
SceneActuatorSlot * bind_scene_actuator(SceneState *s, const char *name)
SceneFreeBodySlot * bind_scene_free_body(SceneState *s, const char *body_name)
SceneJointSlot * bind_scene_joint(SceneState *s, const char *joint_name)
Status open_viewer(Env *env, const char *title="MuJoCo")
std::string find_asset(const fs::path &relative)
int main(int argc, char *argv[])
TEST_F(SceneStateTest, FreeBodyPoseAndDerivedFrameAgreeAfterAStep)