2f1843f4a7
The dcmotor input block is any subset of the canonical list [pos, vel, ff, voltage], selected with input="pos vel ff voltage" and recorded as mjtCtrlInput bits in actuator_ctrlspec like pid. Tokens are required in canonical order: the attribute denotes a set, the block always packs canonically, and accepting permutations invites reading the string as a layout choice. The mode flag in gainprm[8] is retired (reserved, written 0). Controller gains are now in torque space, as for pid: the controller commands tau = kp*(q*-l) + kd*(v*-ldot) + ki*x_I + tau_ff over the present inputs (absent setpoints frozen at zero) and converts to drive voltage V = R/K * tau + K*ldot. The second term compensates back-EMF, as the current loop of a real torque-mode driver does (torque commands are current commands): commanded torque is delivered exactly until a limit binds, and the torque-speed envelope emerges from the Vmax clamp. The map uses the nameplate R: thermal resistance growth is not compensated, so a hot motor under-delivers by R/R(T). A stateless setpoint dcmotor now matches <pid> exactly, for any K and R; the old back-EMF droop remains available as the physical behavior of the raw voltage path. Voltage-space datasheet gains convert by K/R. Controller inputs require a positive motor constant (the map divides by K), and controller gains require a controller input. ff and voltage are distinct inputs, different in kind: ff is a torque feedforward added to the controller output, uniform with pid's ff (feedforward in the actuator's output space), while voltage is the raw terminal voltage of the physical device, injected downstream of the controller and its Vmax clamp, unclamped (ctrlrange bounds it if desired). input="voltage" is the default: the plain voltage-commanded motor, whose behavior is unchanged by this commit. The integrator always accumulates position error; the old velocity mode's integral term, ki*(int(u)dt - theta), which tracked the integral of the velocity command, is retired without replacement, keeping ki mode-independent -- commanded integrated velocity belongs to an integrator activation state, not to controller gains. slewmax rate-limits the first controller input -- position setpoint (rad/s), velocity setpoint (rad/s^2) or torque feedforward (N*m/s), each a real driver feature (reference ramping, ramped-velocity and ramped-torque input modes); the raw voltage input is never rate-limited and slewmax requires a controller input. input="none" selects the empty signature: the actuator owns no controls at all (nu = 0 is now legal with actuators present) and is purely passive -- LuGre friction, cogging and back-EMF braking as passive joint forces. This exists because auxiliary dynamic states (the LuGre bristle) attach to actuators, not joints. The terminal voltage is identically zero, i.e. a shorted motor (dynamic braking); motorconst=0 decouples the electrical branch. mjINPUT_NONE is a distinct enum value because ctrlspec = 0 means "unset, use the type default". History and delay require an input; the controller voltage override and input read in mj_fwdActuation are gated on a nonempty block. The analytic velocity derivative of the controller becomes dV/dw = -kd*R/K + K, whose second term cancels the back-EMF bias exactly: the net damping of an unclipped torque-mode motor is -kd, and of a voltage-mode or passive motor -K^2/R. Viewers label inputs via mj_actuatorInputName: pos, vel, ff, voltage. The dcmotor LaTeX design doc is updated accordingly: torque-space units, the tau->V map and its saturation-generated envelope, the input-block pipeline figure, and a Passive Operation section. PiperOrigin-RevId: 965795351 Change-Id: Ibc308ca21bd6bad014e77f950ee08feaad449b73
4027 lines
121 KiB
C++
4027 lines
121 KiB
C++
// Copyright 2021 DeepMind Technologies Limited
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
// Tests for xml/xml_native_reader.cc.
|
|
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <limits>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include <gmock/gmock.h>
|
|
#include <gtest/gtest.h>
|
|
#include <mujoco/mjmodel.h>
|
|
#include <mujoco/mjspec.h>
|
|
#include <mujoco/mujoco.h>
|
|
#include "src/cc/array_safety.h"
|
|
#include "src/user/user_api.h"
|
|
#include "src/xml/xml_api.h"
|
|
#include "test/compare_model.h"
|
|
#include "test/fixture.h"
|
|
|
|
namespace mujoco {
|
|
namespace {
|
|
|
|
using ::std::string;
|
|
using ::testing::AllOf;
|
|
using ::testing::ElementsAre;
|
|
using ::testing::Eq;
|
|
using ::testing::FloatEq;
|
|
using ::testing::HasSubstr;
|
|
using ::testing::IsNan;
|
|
using ::testing::IsNull;
|
|
using ::testing::NotNull;
|
|
|
|
using XMLReaderTest = MujocoTest;
|
|
|
|
TEST_F(XMLReaderTest, UniqueElementTest) {
|
|
std::array<char, 1024> error;
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<option>
|
|
<flag sensor="disable"/>
|
|
<flag sensor="disable"/>
|
|
</option>
|
|
</mujoco>
|
|
)";
|
|
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(error.data(), HasSubstr("unique element 'flag' found 2 times"));
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, AuthoredFromXml) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<compiler boundmass="1"/>
|
|
<option timestep="0.01" gravity="0 0 -5">
|
|
<flag constraint="disable"/>
|
|
</option>
|
|
<visual>
|
|
<global fovy="60"/>
|
|
<quality shadowsize="1024"/>
|
|
</visual>
|
|
<worldbody/>
|
|
</mujoco>
|
|
)";
|
|
|
|
std::array<char, 1024> error;
|
|
mjSpec* spec = mj_parseXMLString(xml, 0, error.data(), error.size());
|
|
ASSERT_THAT(spec, NotNull()) << error.data();
|
|
|
|
// option: timestep authored, gravity authored, density not authored
|
|
EXPECT_EQ(mjs_isAuthored(spec, &spec->option.timestep), 1);
|
|
EXPECT_EQ(mjs_isAuthored(spec, spec->option.gravity), 1);
|
|
EXPECT_EQ(mjs_isAuthored(spec, &spec->option.density), 0);
|
|
|
|
// compiler: boundmass authored, boundinertia not authored
|
|
EXPECT_EQ(mjs_isAuthored(spec, &spec->compiler.boundmass), 1);
|
|
EXPECT_EQ(mjs_isAuthored(spec, &spec->compiler.boundinertia), 0);
|
|
|
|
// flags: constraint disable authored, contact not authored
|
|
EXPECT_NE(spec->authored.disableflags & mjDSBL_CONSTRAINT, 0);
|
|
EXPECT_EQ(spec->authored.disableflags & mjDSBL_CONTACT, 0);
|
|
|
|
// visual global: fovy authored, ipd not authored
|
|
EXPECT_EQ(mjs_isAuthored(spec, &spec->visual.global.fovy), 1);
|
|
EXPECT_EQ(mjs_isAuthored(spec, &spec->visual.global.ipd), 0);
|
|
|
|
// visual quality: shadowsize authored, offsamples not authored
|
|
EXPECT_EQ(mjs_isAuthored(spec, &spec->visual.quality.shadowsize), 1);
|
|
EXPECT_EQ(mjs_isAuthored(spec, &spec->visual.quality.offsamples), 0);
|
|
|
|
mj_deleteSpec(spec);
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, AuthoredDefaultsZero) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody/>
|
|
</mujoco>
|
|
)";
|
|
|
|
std::array<char, 1024> error;
|
|
mjSpec* spec = mj_parseXMLString(xml, 0, error.data(), error.size());
|
|
ASSERT_THAT(spec, NotNull()) << error.data();
|
|
|
|
// nothing authored in an empty model
|
|
EXPECT_EQ(mjs_isAuthored(spec, &spec->option.timestep), 0);
|
|
EXPECT_EQ(mjs_isAuthored(spec, &spec->option.gravity), 0);
|
|
EXPECT_EQ(mjs_isAuthored(spec, &spec->compiler.boundmass), 0);
|
|
EXPECT_EQ(spec->authored.disableflags, 0);
|
|
EXPECT_EQ(spec->authored.enableflags, 0);
|
|
EXPECT_EQ(spec->authored.disableactuator, 0);
|
|
EXPECT_EQ(mjs_isAuthored(spec, &spec->visual.global.fovy), 0);
|
|
EXPECT_EQ(mjs_isAuthored(spec, &spec->visual.map.znear), 0);
|
|
|
|
mj_deleteSpec(spec);
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, MemorySize) {
|
|
std::array<char, 1024> error;
|
|
{
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<size memory="512"/>
|
|
</mujoco>
|
|
)";
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
EXPECT_EQ(model->narena, 512);
|
|
}
|
|
{
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<size memory="1K "/>
|
|
</mujoco>
|
|
)";
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
EXPECT_EQ(model->narena, 1024);
|
|
}
|
|
{
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<size memory=" 10K"/>
|
|
</mujoco>
|
|
)";
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
EXPECT_EQ(model->narena, 10240);
|
|
}
|
|
{
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<size memory=" 4M "/>
|
|
</mujoco>
|
|
)";
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
EXPECT_EQ(model->narena, 4*1024*1024);
|
|
}
|
|
{
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<size memory="1G"/>
|
|
</mujoco>
|
|
)";
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
EXPECT_EQ(model->narena, 1024*1024*1024);
|
|
}
|
|
{
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<size memory="1073741824"/>
|
|
</mujoco>
|
|
)";
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
EXPECT_EQ(model->narena, 1024*1024*1024);
|
|
}
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, InvalidMemorySize) {
|
|
std::array<char, 1024> error;
|
|
{
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<size memory="-3"/>
|
|
</mujoco>
|
|
)";
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
}
|
|
{
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<size memory="1 M"/>
|
|
</mujoco>
|
|
)";
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
}
|
|
{
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<size memory="2X"/>
|
|
</mujoco>
|
|
)";
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
}
|
|
{
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<size memory="K"/>
|
|
</mujoco>
|
|
)";
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
}
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, InvalidNUserBody) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<size nuser_body="-2"/>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(error.data(), HasSubstr("nuser_body"));
|
|
EXPECT_THAT(error.data(), HasSubstr("line 3"));
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, InvalidNUserJoint) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<size nuser_jnt="-2"/>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(error.data(), HasSubstr("nuser_jnt"));
|
|
EXPECT_THAT(error.data(), HasSubstr("line 3"));
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, InvalidNUserGeom) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<size nuser_geom="-2"/>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(error.data(), HasSubstr("nuser_geom"));
|
|
EXPECT_THAT(error.data(), HasSubstr("line 3"));
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, InvalidNUserSite) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<size nuser_site="-2"/>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(error.data(), HasSubstr("nuser_site"));
|
|
EXPECT_THAT(error.data(), HasSubstr("line 3"));
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, InvalidNUserCamera) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<size nuser_cam="-2"/>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(error.data(), HasSubstr("nuser_cam"));
|
|
EXPECT_THAT(error.data(), HasSubstr("line 3"));
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, InvalidNUserTendon) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<size nuser_tendon="-2"/>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(error.data(), HasSubstr("nuser_tendon"));
|
|
EXPECT_THAT(error.data(), HasSubstr("line 3"));
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, InvalidNUserActuator) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<size nuser_actuator="-2"/>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(error.data(), HasSubstr("nuser_actuator"));
|
|
EXPECT_THAT(error.data(), HasSubstr("line 3"));
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, InvalidNUserSensor) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<size nuser_sensor="-2"/>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(error.data(), HasSubstr("nuser_sensor"));
|
|
EXPECT_THAT(error.data(), HasSubstr("line 3"));
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, CanParseInf) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1" pos="5e-1 -INF iNf"/>
|
|
<geom size="1" pos="inF -inf Inf"/>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
const double inf = std::numeric_limits<double>::infinity();
|
|
MjModelPtr model = LoadModelFromString(xml);
|
|
ASSERT_THAT(model.get(), NotNull());
|
|
EXPECT_EQ(model->geom_pos[0], 0.5);
|
|
EXPECT_EQ(model->geom_pos[1], -inf);
|
|
EXPECT_THAT(model->geom_pos[2], inf);
|
|
EXPECT_EQ(model->geom_pos[3], inf);
|
|
EXPECT_EQ(model->geom_pos[4], -inf);
|
|
EXPECT_EQ(model->geom_pos[5], inf);
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, CanParseNanAndRaisesWarning) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1" pos="nan NaN NAN"/>
|
|
<geom size="1" pos="1.0 0.0 nAn"/>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MockWarningHandler warning_handler;
|
|
warning_handler.ExpectWarnings("XML contains a 'NaN'");
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull());
|
|
EXPECT_THAT(model->geom_pos[0], IsNan());
|
|
EXPECT_THAT(model->geom_pos[1], IsNan());
|
|
EXPECT_THAT(model->geom_pos[2], IsNan());
|
|
EXPECT_EQ(model->geom_pos[3], 1);
|
|
EXPECT_EQ(model->geom_pos[4], 0);
|
|
EXPECT_THAT(model->geom_pos[5], IsNan());
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, InvalidArrayElement) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1" axisangle="1.0 0.0 0.0 [[1]]"/>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(error.data(), HasSubstr("bad format in attribute 'axisangle'"));
|
|
EXPECT_THAT(error.data(), HasSubstr("line 5"));
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, InvalidArrayLength) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1" axisangle="1 0 0 0 1"/>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(error.data(), HasSubstr("has too much data"));
|
|
EXPECT_THAT(error.data(), HasSubstr("line 5"));
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, InvalidQuaternion) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1" quat="0 0 0 0"/>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(error.data(), HasSubstr("zero quaternion is not allowed"));
|
|
EXPECT_THAT(error.data(), HasSubstr("line 5"));
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, InvalidNumber) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1" axisangle="1 0.1.2.3"/>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(error.data(), HasSubstr("bad format in attribute 'axisangle'"));
|
|
EXPECT_THAT(error.data(), HasSubstr("line 5"));
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, InvalidNumberRange) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<asset>
|
|
<mesh name="mesh" file="mesh.stl" face="100000000000000000000000"/>
|
|
</asset>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(error.data(),
|
|
HasSubstr("number is too large in attribute 'face'"));
|
|
EXPECT_THAT(error.data(), HasSubstr("line 4"));
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, InvalidNumberOfAttributes) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body pos="0 0 .3">
|
|
<freejoint/>
|
|
<geom name="ellipsoid" type="ellipsoid" size=".1 .1"/>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(error.data(), HasSubstr("size 2 must be positive"));
|
|
EXPECT_THAT(error.data(), HasSubstr("line 6"));
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, AllowsSpaces) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1" axisangle="1 0 0 0 "/>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull());
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, InvalidDoubleOrientation) {
|
|
std::string prefix = "<mujoco><worldbody><";
|
|
std::string suffix = "/></worldbody></mujoco>";
|
|
std::vector<std::string> orientations = {
|
|
R"( quat="0 1 0 0" )",
|
|
R"( euler="1.7 2.9 0.1" )",
|
|
R"( zaxis="1.7 2.9 0.1" )",
|
|
R"( axisangle="1.7 2.9 0.1 0" )",
|
|
R"( xyaxes="1.7 2.9 0.1 0.4 1.4 0.6" )",
|
|
};
|
|
std::vector<std::string> fields = {
|
|
"geom", "body", "camera", "site"
|
|
};
|
|
for (auto const& field : fields) {
|
|
for (auto const& orient1 : orientations) {
|
|
for (auto const& orient2 : orientations) {
|
|
if (orient1 == orient2) continue;
|
|
std::string xml = prefix + field + orient1 + orient2 + suffix;
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model =
|
|
LoadModelFromString(xml.c_str(), error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(
|
|
error.data(),
|
|
HasSubstr("multiple orientation specifiers are not allowed"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, ClassOverridesChildclass) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<default>
|
|
<default class="size2">
|
|
<geom size="2"/>
|
|
</default>
|
|
<default class="size3">
|
|
<geom size="3"/>
|
|
</default>
|
|
</default>
|
|
<worldbody>
|
|
<frame childclass="size2">
|
|
<geom/>
|
|
<geom class="size3"/>
|
|
<body childclass="size2">
|
|
<geom/>
|
|
<geom class="size3"/>
|
|
</body>
|
|
</frame>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
EXPECT_EQ(model->geom_size[3*0], 2);
|
|
EXPECT_EQ(model->geom_size[3*1], 3);
|
|
EXPECT_EQ(model->geom_size[3*2], 2);
|
|
EXPECT_EQ(model->geom_size[3*3], 3);
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, RepeatedDefaultName) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<default>
|
|
<default class="sphere">
|
|
<geom type="sphere" size="1"/>
|
|
</default>
|
|
<default class="sphere">
|
|
<geom type="capsule" size="1 1"/>
|
|
</default>
|
|
</default>
|
|
<worldbody>
|
|
<body>
|
|
<geom class="sphere"/>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull()) << error.data();
|
|
EXPECT_THAT(error.data(), HasSubstr("repeated default class name"));
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, InvalidDefaultClassName) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<default>
|
|
<default class="sphere">
|
|
<geom type="sphere" size="1"/>
|
|
</default>
|
|
</default>
|
|
<worldbody>
|
|
<body>
|
|
<geom class="invalid"/>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull()) << error.data();
|
|
EXPECT_THAT(error.data(),
|
|
AllOf(HasSubstr("unknown default class name 'invalid'"),
|
|
HasSubstr("Element 'geom'"), HasSubstr("line 10")));
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, InvalidTopDefaultClassName) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<default class="sphere">
|
|
<geom type="sphere" size="1"/>
|
|
</default>
|
|
<worldbody>
|
|
<body>
|
|
<geom class="sphere"/>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull()) << error.data();
|
|
EXPECT_THAT(error.data(),
|
|
HasSubstr("top-level default class 'main' cannot be renamed"));
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, ValidTopDefaultClassName) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<default class="main">
|
|
<geom type="sphere" size="1"/>
|
|
</default>
|
|
<worldbody>
|
|
<body>
|
|
<geom class="main"/>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
}
|
|
|
|
// ------------------------ test including -------------------------------------
|
|
|
|
// tiny RGB 2 x 3 PNG file
|
|
static constexpr unsigned char kTinyPng[] = {
|
|
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
|
|
0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x02,
|
|
0x08, 0x02, 0x00, 0x00, 0x00, 0x12, 0x16, 0xf1, 0x4d, 0x00, 0x00, 0x00,
|
|
0x1c, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x63, 0x78, 0xc1, 0xc0, 0xc0,
|
|
0xc0, 0xf0, 0xbf, 0xb8, 0xb8, 0x98, 0x81, 0xe1, 0x3f, 0xc3, 0xff, 0xff,
|
|
0xff, 0xc5, 0xc4, 0xc4, 0x00, 0x46, 0xd7, 0x07, 0x7f, 0xd2, 0x52, 0xa1,
|
|
0x41, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60,
|
|
0x82
|
|
};
|
|
|
|
// mesh OBJ file of a cube
|
|
static constexpr char kTinyObj[] = R"(
|
|
v -1 -1 1
|
|
v 1 -1 1
|
|
v -1 1 1
|
|
v 1 1 1
|
|
v -1 1 -1
|
|
v 1 1 -1
|
|
v -1 -1 -1
|
|
v 1 -1 -1)";
|
|
|
|
|
|
TEST_F(XMLReaderTest, IncludeTest) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<geom name="plane" type="plane" size="1 1 1"/>
|
|
<include file="model1.xml"/>
|
|
<include file="model2.xml"/>
|
|
</worldbody>
|
|
</mujoco>)";
|
|
|
|
static constexpr char xml1[] = R"(
|
|
<mujoco>
|
|
<geom name="box" type="box" size="1 1 1"/>
|
|
</mujoco>)";
|
|
|
|
static constexpr char xml2[]= R"(
|
|
<mujoco>
|
|
<geom name="ball" type="sphere" size="2"/>
|
|
<include file="model3.xml"/>
|
|
</mujoco>)";
|
|
|
|
static constexpr char xml3[]= R"(
|
|
<mujoco>
|
|
<geom name="another_box" type="box" size="2 2 2"/>
|
|
</mujoco>)";
|
|
|
|
auto vfs = std::make_unique<mjVFS>();
|
|
mj_defaultVFS(vfs.get());
|
|
|
|
mj_addBufferVFS(vfs.get(), "model1.xml", xml1, sizeof(xml1));
|
|
mj_addBufferVFS(vfs.get(), "model2.xml", xml2, sizeof(xml2));
|
|
mj_addBufferVFS(vfs.get(), "model3.xml", xml3, sizeof(xml3));
|
|
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(),
|
|
error.size(), vfs.get());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
EXPECT_EQ(mj_name2id(model.get(), mjOBJ_GEOM, "ball"), 2);
|
|
EXPECT_EQ(mj_name2id(model.get(), mjOBJ_GEOM, "another_box"), 3);
|
|
mj_deleteVFS(vfs.get());
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, IncludeChildTest) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<geom name="plane" type="plane" size="1 1 1"/>
|
|
<include file="model1.xml">
|
|
<geom name="box" type="box" size="1 1 1"/>
|
|
</include>
|
|
</worldbody>
|
|
</mujoco>)";
|
|
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(error.data(), HasSubstr("Include element cannot have children"));
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, IncludeSameFileTest) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<include file="model1.xml"/>
|
|
<include file="model1.xml"/>
|
|
</mujoco>)";
|
|
|
|
static constexpr char xml1[] = R"(
|
|
<mujoco>
|
|
<geom name="box" type="box" size="1 1 1"/>
|
|
</mujoco>)";
|
|
|
|
auto vfs = std::make_unique<mjVFS>();
|
|
mj_defaultVFS(vfs.get());
|
|
|
|
mj_addBufferVFS(vfs.get(), "model1.xml", xml1, sizeof(xml1));
|
|
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size(),
|
|
vfs.get());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(error.data(), HasSubstr("File 'model1.xml' already included"));
|
|
mj_deleteVFS(vfs.get());
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, IncludePathTest) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<geom name="plane" type="plane" size="1 1 1"/>
|
|
<include file="submodels/model1.xml"/>
|
|
<include file="submodels/model2.xml"/>
|
|
</worldbody>
|
|
</mujoco>)";
|
|
|
|
static constexpr char xml1[] = R"(
|
|
<mujoco>
|
|
<geom name="box" type="box" size="1 1 1"/>
|
|
</mujoco>)";
|
|
|
|
static constexpr char xml2[]= R"(
|
|
<mujoco>
|
|
<geom name="ball" type="sphere" size="2"/>
|
|
<include file="subsubmodels/model3.xml"/>
|
|
</mujoco>)";
|
|
|
|
static constexpr char xml3[]= R"(
|
|
<mujoco>
|
|
<geom name="another_box" type="box" size="2 2 2"/>
|
|
</mujoco>)";
|
|
|
|
mjVFS vfs;
|
|
mj_defaultVFS(&vfs);
|
|
mj_addBufferVFS(&vfs, "model.xml", xml, sizeof(xml));
|
|
mj_addBufferVFS(&vfs, "submodels/model1.xml", xml1, sizeof(xml1));
|
|
mj_addBufferVFS(&vfs, "submodels/model2.xml", xml2, sizeof(xml2));
|
|
mj_addBufferVFS(&vfs, "submodels/subsubmodels/model3.xml", xml3,
|
|
sizeof(xml3));
|
|
|
|
std::array<char, 1024> error;
|
|
mjModel* model = mj_loadXML("model.xml", &vfs, error.data(),
|
|
error.size());
|
|
ASSERT_THAT(model, NotNull()) << error.data();
|
|
EXPECT_EQ(mj_name2id(model, mjOBJ_GEOM, "ball"), 2);
|
|
EXPECT_EQ(mj_name2id(model, mjOBJ_GEOM, "another_box"), 3);
|
|
mj_deleteModel(model);
|
|
mj_deleteVFS(&vfs);
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, FallbackIncludePathTest) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<geom name="plane" type="plane" size="1 1 1"/>
|
|
<include file="model1.xml"/>
|
|
<include file="submodels/model2.xml"/>
|
|
</worldbody>
|
|
</mujoco>)";
|
|
|
|
static constexpr char xml1[] = R"(
|
|
<mujoco>
|
|
<geom name="box" type="box" size="1 1 1"/>
|
|
</mujoco>)";
|
|
|
|
static constexpr char xml2[]= R"(
|
|
<mujoco>
|
|
<geom name="ball" type="sphere" size="2"/>
|
|
<include file="subsubmodels/model3.xml"/>
|
|
</mujoco>)";
|
|
|
|
static constexpr char xml3[]= R"(
|
|
<mujoco>
|
|
<geom name="another_box" type="box" size="2 2 2"/>
|
|
</mujoco>)";
|
|
|
|
mjVFS vfs;
|
|
mj_defaultVFS(&vfs);
|
|
mj_addBufferVFS(&vfs, "model.xml", xml, sizeof(xml));
|
|
mj_addBufferVFS(&vfs, "model1.xml", xml1, sizeof(xml1));
|
|
mj_addBufferVFS(&vfs, "submodels/model2.xml", xml2, sizeof(xml2));
|
|
mj_addBufferVFS(&vfs, "subsubmodels/model3.xml", xml3, sizeof(xml3));
|
|
|
|
std::array<char, 1024> error;
|
|
mjModel* model = mj_loadXML("model.xml", &vfs,
|
|
error.data(), error.size());
|
|
ASSERT_THAT(model, NotNull()) << error.data();
|
|
EXPECT_EQ(mj_name2id(model, mjOBJ_GEOM, "ball"), 2);
|
|
EXPECT_EQ(mj_name2id(model, mjOBJ_GEOM, "another_box"), 3);
|
|
mj_deleteModel(model);
|
|
mj_deleteVFS(&vfs);
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, MaterialTextureTest) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<asset>
|
|
<texture file="tiny0.png" type="2d" name="tiny0"/>
|
|
<texture file="tiny1.png" type="2d" name="tiny1"/>
|
|
<material name="material">
|
|
<layer role="occlusion" texture="tiny0"/>
|
|
<layer role="roughness" texture="tiny0"/>
|
|
<layer role="metallic" texture="tiny0"/>
|
|
<layer role="rgb" texture="tiny1"/>
|
|
</material>
|
|
</asset>
|
|
<worldbody>
|
|
<geom type="plane" material="material" size="4 4 4"/>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
|
|
mjVFS vfs;
|
|
mj_defaultVFS(&vfs);
|
|
mj_addBufferVFS(&vfs, "tiny0.png", kTinyPng, sizeof(kTinyPng));
|
|
mj_addBufferVFS(&vfs, "tiny1.png", kTinyPng, sizeof(kTinyPng));
|
|
mj_addBufferVFS(&vfs, "model.xml", xml, sizeof(xml));
|
|
|
|
char error[1024];
|
|
mjModel* model = mj_loadXML("model.xml", &vfs, error, 1024);
|
|
|
|
EXPECT_THAT(model, NotNull()) << error;
|
|
EXPECT_EQ(model->mat_texid[mjTEXROLE_RGB], 1);
|
|
EXPECT_EQ(model->mat_texid[mjTEXROLE_METALLIC], 0);
|
|
EXPECT_EQ(model->mat_texid[mjTEXROLE_ROUGHNESS], 0);
|
|
EXPECT_EQ(model->mat_texid[mjTEXROLE_OCCLUSION], 0);
|
|
|
|
mj_deleteModel(model);
|
|
mj_deleteVFS(&vfs);
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, LegacyMaterialTextureTest) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<asset>
|
|
<texture file="tiny0.png" type="2d" name="tiny0"/>
|
|
<texture file="tiny1.png" type="2d" name="tiny1"/>
|
|
<material name="material" texture="tiny1"/>
|
|
</asset>
|
|
<worldbody>
|
|
<geom type="plane" material="material" size="4 4 4"/>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
|
|
mjVFS vfs;
|
|
mj_defaultVFS(&vfs);
|
|
mj_addBufferVFS(&vfs, "tiny0.png", kTinyPng, sizeof(kTinyPng));
|
|
mj_addBufferVFS(&vfs, "tiny1.png", kTinyPng, sizeof(kTinyPng));
|
|
mj_addBufferVFS(&vfs, "model.xml", xml, sizeof(xml));
|
|
|
|
char error[1024];
|
|
mjModel* model = mj_loadXML("model.xml", &vfs, error, 1024);
|
|
|
|
EXPECT_THAT(model, NotNull()) << error;
|
|
EXPECT_EQ(model->mat_texid[mjTEXROLE_RGB], 1);
|
|
|
|
mj_deleteModel(model);
|
|
mj_deleteVFS(&vfs);
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, MaterialTextureFailTest) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<asset>
|
|
<texture file="tiny0.png" type="2d" name="tiny0"/>
|
|
<texture file="tiny1.png" type="2d" name="tiny1"/>
|
|
<material name="material" texture="tiny1">
|
|
<layer role="rgb" texture="tiny1"/>
|
|
<layer role="occlusion" texture="tiny0"/>
|
|
</material>
|
|
</asset>
|
|
<worldbody>
|
|
<geom type="plane" material="material" size="4 4 4"/>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
|
|
std::array<char, 1024> error;
|
|
MjModelPtr m = LoadModelFromString(xml, error.data(), error.size());
|
|
EXPECT_THAT(m.get(), IsNull());
|
|
EXPECT_THAT(error.data(), HasSubstr("A material with a texture attribute "
|
|
"cannot have layer sub-elements"));
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, LargeTextureTest) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<asset>
|
|
<!--
|
|
Use a texture width that exceeds the size representable by an int.
|
|
For cube textures, the height is ignored and set to width*6.
|
|
The default number of channels is 3.
|
|
The width in this test is chosen so that 6*width*width*3 is too large to
|
|
be represented as a 32-bit integer.
|
|
-->
|
|
<texture name="tex" builtin="gradient" width="10923" height="2"/>
|
|
</asset>
|
|
</mujoco>
|
|
)";
|
|
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
|
|
EXPECT_THAT(model.get(), NotNull());
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, LargeTextureAddressTest) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<asset>
|
|
<!--
|
|
Test that tex_adr can correctly index into a texture buffer larger than
|
|
what a 32-bit signed integer can represent.
|
|
The first texture has size 6*10923*10923*3 > 2^31-1 bytes.
|
|
tex_adr[1] should correctly point beyond this offset.
|
|
-->
|
|
<texture name="tex0" builtin="gradient" width="10923" height="2"/>
|
|
<texture name="tex1" builtin="flat" width="2" height="2"/>
|
|
</asset>
|
|
</mujoco>
|
|
)";
|
|
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
EXPECT_EQ(model->ntex, 2);
|
|
EXPECT_GT(model->tex_adr[1], INT32_MAX);
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, IncludeAssetsTest) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<include file="assets/assets.xml"/>
|
|
<worldbody>
|
|
<geom type="plane" material="material" size="4 4 4"/>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
static constexpr char assets[] = R"(
|
|
<mujoco>
|
|
<asset>
|
|
<texture file="tiny.png" type="2d"/>
|
|
<material name="material" texture="tiny"/>
|
|
<include file="subassets/assets.xml"/>
|
|
</asset>
|
|
</mujoco>
|
|
)";
|
|
static constexpr char subassets[] = R"(
|
|
<mujoco>
|
|
<texture file="subtiny.png" type="2d"/>
|
|
<mesh name="cube" file="cube.obj"/>
|
|
<material name="submaterial" texture="subtiny"/>
|
|
</mujoco>
|
|
)";
|
|
|
|
mjVFS vfs;
|
|
mj_defaultVFS(&vfs);
|
|
mj_addBufferVFS(&vfs, "assets/tiny.png", kTinyPng, sizeof(kTinyPng));
|
|
mj_addBufferVFS(&vfs, "assets/subassets/subtiny.png", kTinyPng,
|
|
sizeof(kTinyPng));
|
|
mj_addBufferVFS(&vfs, "assets/subassets/cube.obj", kTinyObj,
|
|
sizeof(kTinyObj));
|
|
mj_addBufferVFS(&vfs, "assets/assets.xml", assets, sizeof(assets));
|
|
mj_addBufferVFS(&vfs, "assets/subassets/assets.xml", subassets,
|
|
sizeof(subassets));
|
|
mj_addBufferVFS(&vfs, "model.xml", xml, sizeof(xml));
|
|
|
|
// loading the file should be successful
|
|
std::array<char, 1024> error;
|
|
mjModel* model = mj_loadXML("model.xml", &vfs, error.data(),
|
|
error.size());
|
|
|
|
ASSERT_THAT(model, NotNull()) << error.data();
|
|
|
|
mj_deleteModel(model);
|
|
mj_deleteVFS(&vfs);
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, FallbackIncludeAssetsTest) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<include file="assets/assets.xml"/>
|
|
<worldbody>
|
|
<geom type="plane" material="material" size="4 4 4"/>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
static constexpr char assets[] = R"(
|
|
<mujoco>
|
|
<asset>
|
|
<texture file="tiny.png" type="2d"/>
|
|
<material name="material" texture="tiny"/>
|
|
<include file="subassets/assets.xml"/>
|
|
</asset>
|
|
</mujoco>
|
|
)";
|
|
static constexpr char subassets[] = R"(
|
|
<mujoco>
|
|
<texture file="subtiny.png" type="2d"/>
|
|
<material name="submaterial" texture="subtiny"/>
|
|
</mujoco>
|
|
)";
|
|
|
|
mjVFS vfs;
|
|
mj_defaultVFS(&vfs);
|
|
mj_addBufferVFS(&vfs, "assets/tiny.png", kTinyPng, sizeof(kTinyPng));
|
|
|
|
// need to fallback for backwards compatibility
|
|
mj_addBufferVFS(&vfs, "subtiny.png", kTinyPng, sizeof(kTinyPng));
|
|
|
|
mj_addBufferVFS(&vfs, "assets/assets.xml", assets, sizeof(assets));
|
|
mj_addBufferVFS(&vfs, "assets/subassets/assets.xml", subassets,
|
|
sizeof(subassets));
|
|
mj_addBufferVFS(&vfs, "model.xml", xml, sizeof(xml));
|
|
|
|
// loading the file should be successful
|
|
std::array<char, 1024> error;
|
|
mjModel* model = mj_loadXML("model.xml", &vfs,
|
|
error.data(), error.size());
|
|
ASSERT_THAT(model, NotNull()) << error.data();
|
|
|
|
mj_deleteModel(model);
|
|
mj_deleteVFS(&vfs);
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, IncludeAbsoluteTest) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<include file="assets/assets.xml"/>
|
|
<worldbody>
|
|
<geom type="plane" material="material" size="4 4 4"/>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
static constexpr char assets[] = R"(
|
|
<mujoco>
|
|
<asset>
|
|
<texture file="tiny.png" type="2d"/>
|
|
<material name="material" texture="tiny"/>
|
|
<include file="subassets/assets.xml"/>
|
|
</asset>
|
|
</mujoco>
|
|
)";
|
|
static constexpr char subassets[] = R"(
|
|
<mujoco>
|
|
<texture file="MjMock.IncludeAbsoluteTest:assets/subtiny.png" type="2d"/>
|
|
<material name="submaterial" texture="subtiny"/>
|
|
</mujoco>
|
|
)";
|
|
|
|
MockFilesystem fs("IncludeAbsoluteTest");
|
|
fs.AddFile("assets/tiny.png", kTinyPng, sizeof(kTinyPng));
|
|
fs.AddFile("assets/subtiny.png", kTinyPng, sizeof(kTinyPng));
|
|
fs.AddFile("assets/assets.xml", (const unsigned char*) assets,
|
|
sizeof(assets));
|
|
fs.AddFile("assets/subassets/assets.xml", (const unsigned char*) subassets,
|
|
sizeof(subassets));
|
|
fs.AddFile("model.xml", (const unsigned char*) xml, sizeof(xml));
|
|
std::string modelpath = fs.FullPath("model.xml");
|
|
|
|
std::array<char, 1024> error;
|
|
// loading the file should be successful
|
|
mjModel* model = mj_loadXML(modelpath.c_str(), nullptr,
|
|
error.data(), error.size());
|
|
ASSERT_THAT(model, NotNull()) << error.data();
|
|
|
|
mj_deleteModel(model);
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, IncludeAbsoluteMeshDirTest) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<include file="assets.xml"/>
|
|
</mujoco>
|
|
)";
|
|
static constexpr char assets[] = R"(
|
|
<mujoco>
|
|
<compiler meshdir="mjMock.IncludeAbsoluteMeshDirTest:/assets"/>
|
|
<asset>
|
|
<mesh file="cube.obj"/>
|
|
</asset>
|
|
</mujoco>
|
|
)";
|
|
|
|
static constexpr char cube[] = R"(
|
|
v -0.500000 -0.500000 0.500000
|
|
v 0.500000 -0.500000 0.500000
|
|
v -0.500000 0.500000 0.500000
|
|
v 0.500000 0.500000 0.500000
|
|
v -0.500000 0.500000 -0.500000
|
|
v 0.500000 0.500000 -0.500000
|
|
v -0.500000 -0.500000 -0.500000
|
|
v 0.500000 -0.500000 -0.500000)";
|
|
|
|
MockFilesystem fs("IncludeAbsoluteMeshDirTest");
|
|
fs.AddFile("/assets/cube.obj", (const unsigned char*) cube, sizeof(cube));
|
|
fs.AddFile("assets.xml", (const unsigned char*) assets,
|
|
sizeof(assets));
|
|
fs.AddFile("model.xml", (const unsigned char*) xml, sizeof(xml));
|
|
std::string modelpath = fs.FullPath("model.xml");
|
|
|
|
std::array<char, 1024> error;
|
|
// loading the file should be successful
|
|
mjModel* model = mj_loadXML(modelpath.c_str(), nullptr,
|
|
error.data(), error.size());
|
|
ASSERT_THAT(model, NotNull()) << error.data();
|
|
|
|
mj_deleteModel(model);
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, ParsePolycoef) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<joint name="0"/>
|
|
<geom size="1"/>
|
|
</body>
|
|
<body>
|
|
<joint name="1"/>
|
|
<geom size="1"/>
|
|
</body>
|
|
</worldbody>
|
|
<equality>
|
|
<joint joint1="0" joint2="1"/>
|
|
<joint joint1="0" joint2="1" polycoef="2"/>
|
|
<joint joint1="0" joint2="1" polycoef="3 4"/>
|
|
<joint joint1="0" joint2="1" polycoef="5 6 7 8 9"/>
|
|
</equality>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr m = LoadModelFromString(xml, error.data(), error.size());
|
|
EXPECT_THAT(m.get(), NotNull()) << error.data();
|
|
EXPECT_THAT(AsVector(m->eq_data + 0*mjNEQDATA, 5),
|
|
ElementsAre(0, 1, 0, 0, 0));
|
|
EXPECT_THAT(AsVector(m->eq_data + 1*mjNEQDATA, 5),
|
|
ElementsAre(2, 1, 0, 0, 0));
|
|
EXPECT_THAT(AsVector(m->eq_data + 2*mjNEQDATA, 5),
|
|
ElementsAre(3, 4, 0, 0, 0));
|
|
EXPECT_THAT(AsVector(m->eq_data + 3*mjNEQDATA, 5),
|
|
ElementsAre(5, 6, 7, 8, 9));
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, TendonArmature) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<site name="a"/>
|
|
<body pos="1 0 0">
|
|
<joint name="slide" type="slide"/>
|
|
<geom size=".1"/>
|
|
<site name="b"/>
|
|
</body>
|
|
</worldbody>
|
|
|
|
<tendon>
|
|
<spatial armature="1.5">
|
|
<site site="a"/>
|
|
<site site="b"/>
|
|
</spatial>
|
|
<fixed armature="2.5">
|
|
<joint joint="slide" coef="1"/>
|
|
</fixed>
|
|
<fixed>
|
|
<joint joint="slide" coef="2"/>
|
|
</fixed>
|
|
</tendon>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr m = LoadModelFromString(xml, error.data(), error.size());
|
|
EXPECT_THAT(m.get(), NotNull()) << error.data();
|
|
EXPECT_EQ(m->ntendon, 3);
|
|
EXPECT_MJTNUM_EQ(m->tendon_armature[0], 1.5);
|
|
EXPECT_MJTNUM_EQ(m->tendon_armature[1], 2.5);
|
|
EXPECT_MJTNUM_EQ(m->tendon_armature[2], 0);
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, TendonArmatureNegative) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<site name="a"/>
|
|
<site name="b" pos="1 0 0"/>
|
|
</worldbody>
|
|
|
|
<tendon>
|
|
<spatial armature="-1.5">
|
|
<site site="a"/>
|
|
<site site="b"/>
|
|
</spatial>
|
|
</tendon>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr m = LoadModelFromString(xml, error.data(), error.size());
|
|
EXPECT_THAT(m.get(), IsNull());
|
|
EXPECT_THAT(error.data(), HasSubstr("tendon armature cannot be negative"));
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, TendonArmatureGeomWrap) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<site name="a"/>
|
|
<geom name="g" type="sphere" size=".1"/>
|
|
<site name="b" pos="1 0 0"/>
|
|
</worldbody>
|
|
|
|
<tendon>
|
|
<spatial armature="1.5">
|
|
<site site="a"/>
|
|
<geom geom="g"/>
|
|
<site site="b"/>
|
|
</spatial>
|
|
</tendon>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr m = LoadModelFromString(xml, error.data(), error.size());
|
|
EXPECT_THAT(m.get(), IsNull());
|
|
EXPECT_THAT(error.data(), HasSubstr("geom wrapping not supported"));
|
|
}
|
|
|
|
// ------------------------ test frame parsing ---------------------------------
|
|
TEST_F(XMLReaderTest, ParseFrame) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<default>
|
|
<default class="frame">
|
|
<geom size=".1"/>
|
|
</default>
|
|
<default class="body">
|
|
<geom size=".2"/>
|
|
</default>
|
|
<default class="geom">
|
|
<geom size=".3"/>
|
|
</default>
|
|
</default>
|
|
|
|
<worldbody>
|
|
<frame euler="0 0 30">
|
|
<geom size=".5" euler="0 0 20"/>
|
|
</frame>
|
|
|
|
<body>
|
|
<frame pos="0 1 0" childclass="frame">
|
|
<geom pos="0 1 0"/>
|
|
<body pos="1 0 0" childclass="body">
|
|
<geom pos="0 0 1"/>
|
|
<geom pos="0 0 1" class="geom"/>
|
|
</body>
|
|
</frame>
|
|
</body>
|
|
|
|
<frame euler="0 0 30">
|
|
<frame euler="0 0 20">
|
|
<geom size=".6"/>
|
|
</frame>
|
|
</frame>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr m = LoadModelFromString(xml, error.data(), error.size());
|
|
EXPECT_THAT(m.get(), NotNull()) << error.data();
|
|
EXPECT_THAT(m->geom_size[ 0], .5);
|
|
EXPECT_THAT(m->geom_size[ 3], .6);
|
|
EXPECT_THAT(m->geom_size[ 6], .1);
|
|
EXPECT_THAT(m->geom_size[ 9], .2);
|
|
EXPECT_THAT(m->geom_size[12], .3);
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, DuplicateFrameName) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<frame name="frame1" euler="0 0 30">
|
|
<geom size=".1"/>
|
|
</frame>
|
|
|
|
<frame name="frame1" euler="0 0 30">
|
|
<geom size=".1"/>
|
|
</frame>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr m = LoadModelFromString(xml, error.data(), error.size());
|
|
EXPECT_THAT(m.get(), IsNull()) << error.data();
|
|
EXPECT_THAT(error.data(), HasSubstr("repeated name 'frame1'"));
|
|
}
|
|
|
|
|
|
// ---------------------- test replicate parsing -------------------------------
|
|
|
|
TEST_F(XMLReaderTest, ParseReplicate) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<asset>
|
|
<texture name="texture" type="2d" builtin="checker" width="32" height="32"/>
|
|
<material name="material" texture="texture" texrepeat="1 1" texuniform="true"/>
|
|
</asset>
|
|
|
|
<worldbody>
|
|
<replicate count="101" offset="3 0 .1" euler="0 0 1.8">
|
|
<body name="body" pos="0 -1 0">
|
|
<joint type="slide"/>
|
|
<geom name="g" size="1"/>
|
|
</body>
|
|
</replicate>
|
|
|
|
<replicate count="2" offset="1 0 0">
|
|
<replicate count="2" offset="0 1 0" sep="_">
|
|
<geom name="geom" size="1" pos="0 0 1" material="material"/>
|
|
<site name="site" pos="1 0 0"/>
|
|
</replicate>
|
|
</replicate>
|
|
</worldbody>
|
|
|
|
<sensor>
|
|
<framepos name="sensor" objtype="site" objname="site"/>
|
|
</sensor>
|
|
|
|
<keyframe>
|
|
<key name="keyframe" qpos="1"/>
|
|
</keyframe>
|
|
</mujoco>
|
|
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr m = LoadModelFromString(xml, error.data(), error.size());
|
|
EXPECT_THAT(m.get(), testing::NotNull()) << error.data();
|
|
EXPECT_THAT(m->ngeom, 105);
|
|
EXPECT_THAT(m->nsensor, 4);
|
|
EXPECT_THAT(m->nbody, 102);
|
|
|
|
// check that the separator is used correctly
|
|
for (int i = 0; i < 2; ++i) {
|
|
for (int j = 0; j < 2; ++j) {
|
|
char geom_name[mjMAXUINAME] = "";
|
|
util::strcat_arr(geom_name, m->names + m->name_geomadr[2*i+j]);
|
|
EXPECT_THAT(m->geom_pos[6*i+3*j+0], i);
|
|
EXPECT_THAT(m->geom_pos[6*i+3*j+1], j);
|
|
EXPECT_THAT(m->geom_pos[6*i+3*j+2], 1);
|
|
EXPECT_THAT(std::string(geom_name),
|
|
"geom_" + std::to_string(j) + std::to_string(i));
|
|
}
|
|
}
|
|
|
|
// check that 3 digits are used if count > 99
|
|
for (int i = 0; i < 2; ++i) {
|
|
for (int j = 0; j < 10; ++j) {
|
|
for (int k = 0; k < 10; ++k) {
|
|
int ngeom = 100*i+10*j+k;
|
|
if (ngeom > 99) break;
|
|
char geom_name[mjMAXUINAME] = "";
|
|
util::strcat_arr(geom_name, m->names + m->name_geomadr[4+ngeom]);
|
|
EXPECT_THAT(
|
|
std::string(geom_name),
|
|
"g" + std::to_string(i) + std::to_string(j) + std::to_string(k));
|
|
}
|
|
}
|
|
}
|
|
|
|
// check body positions
|
|
mjtNum pos[2] = {0, 0};
|
|
const mjtNum tol = MjTol(1e-8, 1e-3);
|
|
for (int i = 1; i < 102; ++i) {
|
|
mjtNum theta = (i-1) * 1.8 * mjPI / 180;
|
|
EXPECT_NEAR(m->body_pos[3*i+0], pos[0] + sin(theta), tol) << i;
|
|
EXPECT_NEAR(m->body_pos[3*i+1], pos[1] - cos(theta), tol) << i;
|
|
EXPECT_NEAR(m->body_pos[3*i+2], (i-1) * .1, tol);
|
|
pos[0] += 3 * cos(theta);
|
|
pos[1] += 3 * sin(theta);
|
|
}
|
|
|
|
// check that the final pose is correct
|
|
int n = m->nbody-1;
|
|
EXPECT_NEAR(m->body_quat[4*n+0], 0, tol);
|
|
EXPECT_EQ(m->body_quat[4*n+1], 0);
|
|
EXPECT_EQ(m->body_quat[4*n+2], 0);
|
|
EXPECT_EQ(m->body_quat[4*n+3], 1);
|
|
|
|
// check that the keyframe is resized
|
|
EXPECT_THAT(m->nkey, 102);
|
|
EXPECT_THAT(m->nq, 101);
|
|
for (int i = 0; i < m->nkey; i++) {
|
|
for (int j = 0; j < m->nq; j++) {
|
|
EXPECT_THAT(m->key_qpos[i*m->nq+j], i == j ? 1 : 0) << i << " " << j;
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, ParseReplicatePartialReference) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<geom name="floor" type="plane" size="1 1 1"/>
|
|
<site name="world"/>
|
|
|
|
<replicate count="2" offset="1 0 0">
|
|
<body name="replicated">
|
|
<freejoint/>
|
|
<site name="replicated"/>
|
|
<geom name="replicated" size="1"/>
|
|
</body>
|
|
</replicate>
|
|
</worldbody>
|
|
|
|
<contact>
|
|
<pair geom1="floor" geom2="replicated" condim="1"/>
|
|
<exclude body1="world" body2="replicated" />
|
|
</contact>
|
|
|
|
<tendon>
|
|
<spatial>
|
|
<site site="world"/>
|
|
<site site="replicated"/>
|
|
</spatial>
|
|
</tendon>
|
|
|
|
<sensor>
|
|
<framepos name="sensor" objtype="site" objname="replicated" reftype="site" refname="world"/>
|
|
</sensor>
|
|
</mujoco>
|
|
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr m = LoadModelFromString(xml, error.data(), error.size());
|
|
EXPECT_THAT(m.get(), testing::NotNull()) << error.data();
|
|
|
|
EXPECT_THAT(m->nbody, 3);
|
|
EXPECT_THAT(m->ngeom, 3);
|
|
EXPECT_THAT(m->npair, 2);
|
|
EXPECT_THAT(m->nexclude, 2);
|
|
EXPECT_THAT(m->ntendon, 2);
|
|
EXPECT_THAT(m->nsensor, 2);
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, ParseReplicateDefaultPropagate) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<default>
|
|
<default class="body">
|
|
<geom type="capsule"/>
|
|
</default>
|
|
</default>
|
|
|
|
<worldbody>
|
|
<replicate count="2" euler="0 0 16.36" sep="-">
|
|
<body name="torso" pos="-5 0 1.282" childclass="body">
|
|
<body name="head" pos="0 0 .19">
|
|
<geom name="head" type="sphere" size=".09"/>
|
|
</body>
|
|
</body>
|
|
</replicate>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
mjSpec* spec = mj_parseXMLString(xml, 0, error.data(), error.size());
|
|
EXPECT_THAT(spec, NotNull()) << error.data();
|
|
|
|
mjsBody* torso = mjs_findBody(spec, "torso-0");
|
|
EXPECT_THAT(torso, NotNull());
|
|
|
|
mjsDefault* def = mjs_getDefault(torso->element);
|
|
EXPECT_THAT(def, NotNull());
|
|
EXPECT_THAT(def->geom->type, mjGEOM_CAPSULE);
|
|
|
|
mjsBody* head = mjs_findBody(spec, "head-0");
|
|
EXPECT_THAT(head, NotNull());
|
|
|
|
def = mjs_getDefault(head->element);
|
|
EXPECT_THAT(def, NotNull());
|
|
EXPECT_THAT(def->geom->type, mjGEOM_CAPSULE);
|
|
|
|
mj_deleteSpec(spec);
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, ParseReplicateRepeatedName) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<compiler>
|
|
<lengthrange mode="all"/>
|
|
</compiler>
|
|
|
|
<worldbody>
|
|
<light pos="0 0 10"/>
|
|
|
|
<replicate offset="0 .1 0" count="2">
|
|
<site name="a" size=".02"/>
|
|
<body pos="1 0 1">
|
|
<joint axis="0 -1 0" range="0 90"/>
|
|
<geom type="capsule" size=".02" fromto="0 0 0 0 0 -1"/>
|
|
<site name="b" pos="0 0 -1"/>
|
|
</body>
|
|
</replicate>
|
|
</worldbody>
|
|
|
|
<tendon>
|
|
<spatial name="b">
|
|
<site site="a"/>
|
|
<site site="b"/>
|
|
</spatial>
|
|
</tendon>
|
|
|
|
<actuator>
|
|
<position name="b" tendon="b0" ctrlrange="0 3" kp="100" dampratio="1"/>
|
|
<position name="b" tendon="b1" ctrlrange="0 3" kp="100" dampratio="1"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
mjSpec* spec = mj_parseXMLString(xml, 0, error.data(), error.size());
|
|
EXPECT_THAT(spec, IsNull()) << error.data();
|
|
EXPECT_THAT(error.data(), HasSubstr("repeated name 'b' in actuator"));
|
|
EXPECT_THAT(error.data(), HasSubstr("Element 'position'"));
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, RepeatedPrefix) {
|
|
static constexpr char parent[] = R"(
|
|
<mujoco>
|
|
<asset>
|
|
<model name="1" file="child_1.xml" content_type="text/xml" />
|
|
<model name="2" file="child_2.xml" content_type="text/xml" />
|
|
</asset>
|
|
|
|
<worldbody>
|
|
<attach model="1" body="1" prefix="prefix-"/>
|
|
<replicate count="1">
|
|
<attach model="2" body="2" prefix="prefix-"/>
|
|
</replicate>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
|
|
static constexpr char child_1[] = R"(
|
|
<mujoco>
|
|
<asset>
|
|
<model name="2" file="child_2.xml" content_type="text/xml"/>
|
|
</asset>
|
|
|
|
<worldbody>
|
|
<body name="1">
|
|
<body name="2">
|
|
<attach model="2" body="2" prefix="prefix2"/>
|
|
</body>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
|
|
static constexpr char child_2[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body name="2"/>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
|
|
auto vfs = std::make_unique<mjVFS>();
|
|
mj_defaultVFS(vfs.get());
|
|
mj_addBufferVFS(vfs.get(), "child_1.xml", child_1, sizeof(child_1));
|
|
mj_addBufferVFS(vfs.get(), "child_2.xml", child_2, sizeof(child_2));
|
|
|
|
std::array<char, 1024> err;
|
|
mjSpec* c2 = mj_parseXMLString(child_2, 0, err.data(), err.size());
|
|
EXPECT_THAT(c2, NotNull()) << err.data();
|
|
mjSpec* c1 = mj_parseXMLString(child_1, vfs.get(), err.data(), err.size());
|
|
EXPECT_THAT(c1, NotNull()) << err.data();
|
|
mj_deleteSpec(c1);
|
|
mj_deleteSpec(c2);
|
|
|
|
mjSpec* spec = mj_parseXMLString(parent, vfs.get(), err.data(), err.size());
|
|
EXPECT_THAT(spec, IsNull());
|
|
EXPECT_THAT(err.data(), HasSubstr("cannot attach"));
|
|
mj_deleteSpec(spec);
|
|
mj_deleteVFS(vfs.get());
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, ParseReplicateExcludeTendon) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body name="winch" pos="-.01 0 .35">
|
|
<joint name="winch" damping="1"/>
|
|
<geom type="cylinder" size=".015 .01"/>
|
|
<site name="anchor" pos=".1 0 .04"/>
|
|
</body>
|
|
<site name="pulley" pos=".1 0 .32"/>
|
|
<site name="hook_left" pos=".08 0 .3"/>
|
|
<site name="hook_right" pos=".12 0 .3"/>
|
|
<body name="sphere" pos=".1 0 .2">
|
|
<freejoint/>
|
|
<geom type="sphere" size=".03"/>
|
|
<site name="pin_left" pos="-.025 0 .025"/>
|
|
<site name="pin_right" pos=".025 0 .025"/>
|
|
</body>
|
|
|
|
<replicate count="4" offset=".025 0 0">
|
|
<replicate count="4" offset="0 .025 0">
|
|
<replicate count="4" offset="0 0 .025">
|
|
<body pos=".06 -.04 .05">
|
|
<geom type="sphere" size=".012"/>
|
|
</body>
|
|
</replicate>
|
|
</replicate>
|
|
</replicate>
|
|
</worldbody>
|
|
|
|
<tendon>
|
|
<spatial range="0 .19" limited="true">
|
|
<site site="anchor"/>
|
|
<site site="pulley"/>
|
|
<pulley divisor="3"/>
|
|
<site site="pulley"/>
|
|
<site site="hook_left"/>
|
|
<site site="pin_left"/>
|
|
<pulley divisor="3"/>
|
|
<site site="pulley"/>
|
|
<site site="hook_right"/>
|
|
<site site="pin_right"/>
|
|
</spatial>
|
|
</tendon>
|
|
|
|
<actuator>
|
|
<position name="winch" joint="winch" ctrlrange="-.7 .5" ctrllimited="true" kp="10"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
mjSpec* spec = mj_parseXMLString(xml, 0, error.data(), error.size());
|
|
EXPECT_THAT(spec, NotNull()) << error.data();
|
|
mjModel* m = mj_compile(spec, 0);
|
|
EXPECT_THAT(m, NotNull());
|
|
EXPECT_THAT(m->nbody, 67);
|
|
EXPECT_THAT(m->ngeom, 66);
|
|
EXPECT_THAT(m->nsite, 6);
|
|
EXPECT_THAT(m->nu, 1);
|
|
EXPECT_THAT(m->ntendon, 1);
|
|
mj_deleteModel(m);
|
|
mj_deleteSpec(spec);
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, ParseReplicateWithTendon) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<replicate count="2" offset=".025 0 0">
|
|
<replicate count="2" offset="0 .025 0">
|
|
<replicate count="2" offset="0 0 .025">
|
|
<body name="winch" pos="-.01 0 .35">
|
|
<joint name="winch" damping="1"/>
|
|
<geom type="cylinder" size=".015 .01"/>
|
|
<site name="anchor" pos=".1 0 .04"/>
|
|
</body>
|
|
<site name="pulley" pos=".1 0 .32"/>
|
|
<site name="hook_left" pos=".08 0 .3"/>
|
|
<site name="hook_right" pos=".12 0 .3"/>
|
|
<body name="sphere" pos=".1 0 .2">
|
|
<freejoint/>
|
|
<geom type="sphere" size=".03"/>
|
|
<site name="pin_left" pos="-.025 0 .025"/>
|
|
<site name="pin_right" pos=".025 0 .025"/>
|
|
</body>
|
|
<body pos=".06 -.04 .05">
|
|
<geom type="sphere" size=".012"/>
|
|
</body>
|
|
</replicate>
|
|
</replicate>
|
|
</replicate>
|
|
</worldbody>
|
|
|
|
<tendon>
|
|
<spatial range="0 .19" limited="true" name="tendon">
|
|
<site site="anchor"/>
|
|
<site site="pulley"/>
|
|
<pulley divisor="3"/>
|
|
<site site="pulley"/>
|
|
<site site="hook_left"/>
|
|
<site site="pin_left"/>
|
|
<pulley divisor="3"/>
|
|
<site site="pulley"/>
|
|
<site site="hook_right"/>
|
|
<site site="pin_right"/>
|
|
</spatial>
|
|
</tendon>
|
|
|
|
<actuator>
|
|
<position name="winch" joint="winch" ctrlrange="-.7 .5" ctrllimited="true" kp="10"/>
|
|
<position name="tendon" tendon="tendon" ctrlrange="0 1" kp="100" dampratio="1"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
mjSpec* spec = mj_parseXMLString(xml, 0, error.data(), error.size());
|
|
EXPECT_THAT(spec, NotNull()) << error.data();
|
|
mjModel* m = mj_compile(spec, 0);
|
|
EXPECT_THAT(m, NotNull()) << mjs_getError(spec);
|
|
EXPECT_THAT(m->nbody, 25);
|
|
EXPECT_THAT(m->ngeom, 24);
|
|
EXPECT_THAT(m->nsite, 48);
|
|
EXPECT_THAT(m->nu, 16);
|
|
EXPECT_THAT(m->ntendon, 8);
|
|
mj_deleteModel(m);
|
|
mj_deleteSpec(spec);
|
|
}
|
|
|
|
// ---------------------- test spec assets parsing -----------------------------
|
|
|
|
TEST_F(XMLReaderTest, ParseSpecAssets) {
|
|
std::array<char, 1024> er;
|
|
|
|
static const char* const kParentPath =
|
|
"xml/testdata/parent.xml";
|
|
static const char* const kChildPath =
|
|
"xml/testdata/child.xml";
|
|
|
|
const std::string xml_parent = GetTestDataFilePath(kParentPath);
|
|
const std::string xml_child = GetTestDataFilePath(kChildPath);
|
|
|
|
mjModel* parent = mj_loadXML(xml_parent.c_str(), 0, er.data(), er.size());
|
|
EXPECT_THAT(parent, NotNull()) << er.data();
|
|
|
|
mjModel* child = mj_loadXML(xml_child.c_str(), 0, er.data(), er.size());
|
|
EXPECT_THAT(child, NotNull()) << er.data();
|
|
|
|
mj_deleteModel(parent);
|
|
mj_deleteModel(child);
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, AttachSpecAssets) {
|
|
static constexpr char xml_parent[] = R"(
|
|
<mujoco>
|
|
<asset>
|
|
<model name="child" file="xml_child.xml"/>
|
|
</asset>
|
|
<worldbody>
|
|
<body name="parent">
|
|
<geom name="geom" size="2"/>
|
|
<replicate count="2" offset="0 0 1">
|
|
<attach model="child" body="body" prefix="other"/>
|
|
</replicate>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
|
|
static constexpr char xml_child[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body name="body">
|
|
<geom name="geom" size="1" pos="2 0 0"/>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
|
|
static constexpr char xml_expected[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body name="parent">
|
|
<geom name="geom" size="2"/>
|
|
<body name="otherbody0">
|
|
<geom name="othergeom0" size="1" pos="2 0 0"/>
|
|
</body>
|
|
<body name="otherbody1" pos="0 0 1">
|
|
<geom name="othergeom1" size="1" pos="2 0 0"/>
|
|
</body>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
|
|
auto vfs = std::make_unique<mjVFS>();
|
|
mj_defaultVFS(vfs.get());
|
|
mj_addBufferVFS(vfs.get(), "xml_child.xml", xml_child, sizeof(xml_child));
|
|
|
|
std::array<char, 1024> er;
|
|
MjModelPtr model =
|
|
LoadModelFromString(xml_parent, er.data(), er.size(), vfs.get());
|
|
EXPECT_THAT(model.get(), NotNull()) << er.data();
|
|
|
|
MjModelPtr expected = LoadModelFromString(xml_expected, er.data(), er.size());
|
|
EXPECT_THAT(expected.get(), NotNull()) << er.data();
|
|
|
|
mjtNum tol = 0;
|
|
std::string field = "";
|
|
EXPECT_LE(CompareModel(model.get(), expected.get(), field), tol)
|
|
<< "Expected and attached models are different!\n"
|
|
<< "Different field: " << field << '\n';;
|
|
mj_deleteVFS(vfs.get());
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, InvalidAttach) {
|
|
static constexpr char xml_parent[] = R"(
|
|
<mujoco>
|
|
<asset>
|
|
<model name="other" file="child.xml"/>
|
|
</asset>
|
|
<worldbody>
|
|
<body name="parent">
|
|
<joint name="joint1"/>
|
|
<geom size="2"/>
|
|
<attach model="other" body="body" prefix="_"/>
|
|
</body>
|
|
</worldbody>
|
|
|
|
<actuator>
|
|
<motor name="_actuator" joint="joint1"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
|
|
static constexpr char xml_child[] = R"(
|
|
<mujoco model="child">
|
|
<worldbody>
|
|
<body name="body">
|
|
<joint name="joint2"/>
|
|
<geom size="1" pos="2 0 0"/>
|
|
</body>
|
|
</worldbody>
|
|
|
|
<actuator>
|
|
<motor name="actuator" joint="joint2"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
|
|
auto vfs = std::make_unique<mjVFS>();
|
|
mj_defaultVFS(vfs.get());
|
|
mj_addBufferVFS(vfs.get(), "child.xml", xml_child, sizeof(xml_child));
|
|
|
|
std::array<char, 1024> er;
|
|
MjModelPtr model =
|
|
LoadModelFromString(xml_parent, er.data(), er.size(), vfs.get());
|
|
|
|
EXPECT_THAT(model.get(), IsNull()) << er.data();
|
|
EXPECT_THAT(er.data(), HasSubstr("repeated name '_actuator' in actuator"));
|
|
EXPECT_THAT(er.data(), HasSubstr("Element 'attach'"));
|
|
mj_deleteVFS(vfs.get());
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, LookupCompilerOptionWithoutSpecCopy) {
|
|
static constexpr char child_xml[] = R"(
|
|
<mujoco>
|
|
<compiler angle="radian"/>
|
|
|
|
<worldbody>
|
|
<body name="child">
|
|
<geom type="box" size="1 1 1"/>
|
|
<joint name="child_joint" range="-3.1415926 3.1415926"/>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
|
|
static constexpr char parent_xml[] = R"(
|
|
<mujoco>
|
|
<asset>
|
|
<model name="child" file="child.xml"/>
|
|
</asset>
|
|
|
|
<worldbody>
|
|
<body name="parent">
|
|
<geom type="box" size="1 1 1"/>
|
|
<joint name="parent_joint" range="-180 180"/>
|
|
<attach model="child" body="child" prefix="child_"/>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
|
|
auto vfs = std::make_unique<mjVFS>();
|
|
mj_defaultVFS(vfs.get());
|
|
mj_addBufferVFS(vfs.get(), "child.xml", child_xml, sizeof(child_xml));
|
|
mj_addBufferVFS(vfs.get(), "parent.xml", parent_xml, sizeof(parent_xml));
|
|
|
|
std::array<char, 1024> error;
|
|
auto* spec = mj_parseXMLString(parent_xml, vfs.get(), error.data(),
|
|
error.size());
|
|
ASSERT_THAT(spec, NotNull()) << error.data();
|
|
|
|
mjModel* model = mj_compile(spec, vfs.get());
|
|
EXPECT_THAT(model, NotNull());
|
|
EXPECT_THAT(model->njnt, 2);
|
|
EXPECT_NEAR(model->jnt_range[0], -3.14159, 1e-5);
|
|
EXPECT_NEAR(model->jnt_range[1], 3.14159, 1e-5);
|
|
EXPECT_NEAR(model->jnt_range[2], -3.14159, 1e-5);
|
|
EXPECT_NEAR(model->jnt_range[3], 3.14159, 1e-5);
|
|
|
|
mjSpec* copied_spec = mj_copySpec(spec);
|
|
ASSERT_THAT(copied_spec, NotNull());
|
|
mjModel* copied_model = mj_compile(copied_spec, vfs.get());
|
|
EXPECT_THAT(copied_model, NotNull());
|
|
EXPECT_THAT(copied_model->njnt, 2);
|
|
EXPECT_NEAR(copied_model->jnt_range[0], -3.14159, 1e-5);
|
|
EXPECT_NEAR(copied_model->jnt_range[1], 3.14159, 1e-5);
|
|
EXPECT_NEAR(copied_model->jnt_range[2], -3.14159, 1e-5);
|
|
EXPECT_NEAR(copied_model->jnt_range[3], 3.14159, 1e-5);
|
|
|
|
mj_deleteSpec(spec);
|
|
mj_deleteSpec(copied_spec);
|
|
mj_deleteModel(model);
|
|
mj_deleteModel(copied_model);
|
|
|
|
mj_deleteVFS(vfs.get());
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, ResizeKeyframeAfterParsing) {
|
|
static constexpr char parent_xml[] = R"(
|
|
<mujoco>
|
|
<asset>
|
|
<model name="child" file="child.xml"/>
|
|
</asset>
|
|
<worldbody>
|
|
<attach model="child" body="world" prefix="child_"/>
|
|
<body name="body">
|
|
<joint name="joint"/>
|
|
<geom size="1"/>
|
|
</body>
|
|
</worldbody>
|
|
<keyframe>
|
|
<key name="key" qpos="1"/>
|
|
</keyframe>
|
|
</mujoco>
|
|
)";
|
|
|
|
static constexpr char child_xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body name="body">
|
|
<joint name="joint"/>
|
|
<geom size="1"/>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
|
|
auto vfs = std::make_unique<mjVFS>();
|
|
mj_defaultVFS(vfs.get());
|
|
mj_addBufferVFS(vfs.get(), "child.xml", child_xml, sizeof(child_xml));
|
|
|
|
std::array<char, 1024> error;
|
|
MjModelPtr m =
|
|
LoadModelFromString(parent_xml, error.data(), error.size(), vfs.get());
|
|
EXPECT_THAT(m.get(), NotNull()) << error.data();
|
|
mj_deleteVFS(vfs.get());
|
|
}
|
|
|
|
// ----------------------- test camera parsing ---------------------------------
|
|
|
|
TEST_F(XMLReaderTest, CameraInvalidFovyAndSensorsize) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1"/>
|
|
<camera fovy="1" sensorsize="1 1" resolution="100 100"/>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr m = LoadModelFromString(xml, error.data(), error.size());
|
|
EXPECT_THAT(m.get(), IsNull());
|
|
EXPECT_THAT(error.data(), HasSubstr("at most one of 'fovy', 'sensorsize'"));
|
|
EXPECT_THAT(error.data(), HasSubstr("line 6"));
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, CameraPrincipalRequiresSensorsize) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1"/>
|
|
<camera principal="1 1"/>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr m = LoadModelFromString(xml, error.data(), error.size());
|
|
EXPECT_THAT(m.get(), IsNull());
|
|
EXPECT_THAT(error.data(), HasSubstr("focal/principal require sensorsize"));
|
|
EXPECT_THAT(error.data(), HasSubstr("line 6"));
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, CameraSensorsizeRequiresResolution) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1"/>
|
|
<camera sensorsize="1 1" resolution="0 0"/>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr m = LoadModelFromString(xml, error.data(), error.size());
|
|
EXPECT_THAT(m.get(), IsNull());
|
|
EXPECT_THAT(error.data(), HasSubstr("requires positive resolution"));
|
|
EXPECT_THAT(error.data(), HasSubstr("line 6"));
|
|
}
|
|
|
|
// ----------------------- test inertia parsing --------------------------------
|
|
|
|
TEST_F(XMLReaderTest, InvalidInertialOrientation) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<inertial pos="0 0 0" mass="1" quat="1 0 0 0" fullinertia="1 1 1 0 0 0"/>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(
|
|
error.data(),
|
|
HasSubstr("at most one of 'fullinertia', 'quat', 'axisangle', "
|
|
"'xyaxes', 'zaxis', 'euler' can be specified"));
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, ReadShellParameter) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<asset>
|
|
<mesh name="example_mesh"
|
|
vertex="0 0 0 1 0 0 0 1 0 0 0 1"
|
|
face="0 2 1 2 0 3" inertia="shell"/>
|
|
</asset>
|
|
<worldbody>
|
|
<geom type="mesh" mesh="example_mesh"/>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull());
|
|
}
|
|
|
|
// ----------------------- test builtin mesh parsing ---------------------------
|
|
|
|
TEST_F(XMLReaderTest, ReadWedgeMesh) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<asset>
|
|
<mesh name="wedge" builtin="wedge" params="25 25 180 90 0"/>
|
|
</asset>
|
|
<worldbody>
|
|
<geom type="mesh" mesh="wedge"/>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, BuiltinAndFile) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<asset>
|
|
<mesh name="wedge" builtin="wedge" vertex="0 0 0 1 0 0 0 1 0 0 1 0"
|
|
params="25 25 180 90 0"/>
|
|
</asset>
|
|
<worldbody>
|
|
<geom type="mesh" mesh="wedge"/>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(error.data(),
|
|
HasSubstr("at most one of 'builtin', 'vertex' can be specified"));
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, MakePlateNoParameters) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<asset>
|
|
<mesh name="plate" builtin="plate"/>
|
|
</asset>
|
|
<worldbody>
|
|
<geom type="mesh" mesh="plate" contype="0" conaffinity="0"/>
|
|
</worldbody>
|
|
</mujoco>)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(error.data(), HasSubstr("required attribute missing: 'params'"));
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, MakePlateTooFewParameters) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<asset>
|
|
<mesh name="plate" builtin="plate" params="1"/>
|
|
</asset>
|
|
<worldbody>
|
|
<geom type="mesh" mesh="plate" contype="0" conaffinity="0"/>
|
|
</worldbody>
|
|
</mujoco>)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(
|
|
error.data(),
|
|
HasSubstr("Plate builtin mesh type requires 2 parameters"));
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, MakePlateInvalidParameters) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<asset>
|
|
<mesh name="plate" builtin="plate" params="-1 -1"/>
|
|
</asset>
|
|
<worldbody>
|
|
<geom type="mesh" mesh="plate" contype="0" conaffinity="0"/>
|
|
</worldbody>
|
|
</mujoco>)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(error.data(), HasSubstr("resolutions must be positive"));
|
|
}
|
|
|
|
// ----------------------- test skin parsing --------------------------------
|
|
|
|
TEST_F(XMLReaderTest, ReadsSkinGroups) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body pos="1 -1 .6" name="B0_parent">
|
|
<flexcomp name="B0" type="grid" count="4 4 1" spacing=".2 .2 .2" group="2" radius=".1" dim="2">
|
|
<edge equality="true"/>
|
|
<contact internal="false" selfcollide="none"/>
|
|
</flexcomp>
|
|
</body>
|
|
<body pos="-1 1 .6" name="B1_parent">
|
|
<flexcomp name="B1" type="grid" count="4 4 1" spacing=".2 .2 .2" group="4" radius=".1" dim="2">
|
|
<edge equality="true"/>
|
|
<contact internal="false" selfcollide="none"/>
|
|
</flexcomp>
|
|
</body>
|
|
</worldbody>
|
|
<deformable>
|
|
<skin name="B0Skin" group="4" rgba="1 1 0 1" inflate="0.1"
|
|
vertex="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
|
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
|
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0"
|
|
face="0 4 5 0 5 1 1 5 6 1 6 2 2 6 7 2 7 3 4 8 9 4 9 5 5 9 10 5 10 6 6 10 11 6 11 7 8
|
|
12 13 8 13 9 9 13 14 9 14 10 10 14 15 10 15 11 16 21 20 16 17 21 17 22 21 17 18
|
|
22 18 23 22 18 19 23 20 25 24 20 21 25 21 26 25 21 22 26 22 27 26 22 23 27 24 29
|
|
28 24 25 29 25 30 29 25 26 30 26 31 30 26 27 31 0 20 4 0 16 20 4 24 8 4 20 24 8
|
|
28 12 8 24 28 3 7 23 3 23 19 7 11 27 7 27 23 11 15 31 11 31 27 0 1 17 0 17 16 1
|
|
2 18 1 18 17 2 3 19 2 19 18 12 29 13 12 28 29 13 30 14 13 29 30 14 31 15 14 30 31">
|
|
<bone body="B0_0" bindpos="0 0 0" bindquat="1 0 0 0" vertid="0 16" vertweight="1 1"/>
|
|
<bone body="B0_1" bindpos="0 0 0" bindquat="1 0 0 0" vertid="1 17" vertweight="1 1"/>
|
|
<bone body="B0_2" bindpos="0 0 0" bindquat="1 0 0 0" vertid="2 18" vertweight="1 1"/>
|
|
<bone body="B0_3" bindpos="0 0 0" bindquat="1 0 0 0" vertid="3 19" vertweight="1 1"/>
|
|
<bone body="B0_4" bindpos="0 0 0" bindquat="1 0 0 0" vertid="4 20" vertweight="1 1"/>
|
|
<bone body="B0_5" bindpos="0 0 0" bindquat="1 0 0 0" vertid="5 21" vertweight="1 1"/>
|
|
<bone body="B0_6" bindpos="0 0 0" bindquat="1 0 0 0" vertid="6 22" vertweight="1 1"/>
|
|
<bone body="B0_7" bindpos="0 0 0" bindquat="1 0 0 0" vertid="7 23" vertweight="1 1"/>
|
|
<bone body="B0_8" bindpos="0 0 0" bindquat="1 0 0 0" vertid="8 24" vertweight="1 1"/>
|
|
<bone body="B0_9" bindpos="0 0 0" bindquat="1 0 0 0" vertid="9 25" vertweight="1 1"/>
|
|
<bone body="B0_10" bindpos="0 0 0" bindquat="1 0 0 0" vertid="10 26" vertweight="1 1"/>
|
|
<bone body="B0_11" bindpos="0 0 0" bindquat="1 0 0 0" vertid="11 27" vertweight="1 1"/>
|
|
<bone body="B0_12" bindpos="0 0 0" bindquat="1 0 0 0" vertid="12 28" vertweight="1 1"/>
|
|
<bone body="B0_13" bindpos="0 0 0" bindquat="1 0 0 0" vertid="13 29" vertweight="1 1"/>
|
|
<bone body="B0_14" bindpos="0 0 0" bindquat="1 0 0 0" vertid="14 30" vertweight="1 1"/>
|
|
<bone body="B0_15" bindpos="0 0 0" bindquat="1 0 0 0" vertid="15 31" vertweight="1 1"/>
|
|
</skin>
|
|
<skin name="B1Skin" group="2" rgba="0 1 1 1" inflate="0.1"
|
|
vertex="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
|
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
|
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0"
|
|
face="0 4 5 0 5 1 1 5 6 1 6 2 2 6 7 2 7 3 4 8 9 4 9 5 5 9 10 5 10 6 6 10 11 6 11 7 8
|
|
12 13 8 13 9 9 13 14 9 14 10 10 14 15 10 15 11 16 21 20 16 17 21 17 22 21 17 18
|
|
22 18 23 22 18 19 23 20 25 24 20 21 25 21 26 25 21 22 26 22 27 26 22 23 27 24 29
|
|
28 24 25 29 25 30 29 25 26 30 26 31 30 26 27 31 0 20 4 0 16 20 4 24 8 4 20 24 8
|
|
28 12 8 24 28 3 7 23 3 23 19 7 11 27 7 27 23 11 15 31 11 31 27 0 1 17 0 17 16 1
|
|
2 18 1 18 17 2 3 19 2 19 18 12 29 13 12 28 29 13 30 14 13 29 30 14 31 15 14 30 31">
|
|
<bone body="B1_0" bindpos="0 0 0" bindquat="1 0 0 0" vertid="0 16" vertweight="1 1"/>
|
|
<bone body="B1_1" bindpos="0 0 0" bindquat="1 0 0 0" vertid="1 17" vertweight="1 1"/>
|
|
<bone body="B1_2" bindpos="0 0 0" bindquat="1 0 0 0" vertid="2 18" vertweight="1 1"/>
|
|
<bone body="B1_3" bindpos="0 0 0" bindquat="1 0 0 0" vertid="3 19" vertweight="1 1"/>
|
|
<bone body="B1_4" bindpos="0 0 0" bindquat="1 0 0 0" vertid="4 20" vertweight="1 1"/>
|
|
<bone body="B1_5" bindpos="0 0 0" bindquat="1 0 0 0" vertid="5 21" vertweight="1 1"/>
|
|
<bone body="B1_6" bindpos="0 0 0" bindquat="1 0 0 0" vertid="6 22" vertweight="1 1"/>
|
|
<bone body="B1_7" bindpos="0 0 0" bindquat="1 0 0 0" vertid="7 23" vertweight="1 1"/>
|
|
<bone body="B1_8" bindpos="0 0 0" bindquat="1 0 0 0" vertid="8 24" vertweight="1 1"/>
|
|
<bone body="B1_9" bindpos="0 0 0" bindquat="1 0 0 0" vertid="9 25" vertweight="1 1"/>
|
|
<bone body="B1_10" bindpos="0 0 0" bindquat="1 0 0 0" vertid="10 26" vertweight="1 1"/>
|
|
<bone body="B1_11" bindpos="0 0 0" bindquat="1 0 0 0" vertid="11 27" vertweight="1 1"/>
|
|
<bone body="B1_12" bindpos="0 0 0" bindquat="1 0 0 0" vertid="12 28" vertweight="1 1"/>
|
|
<bone body="B1_13" bindpos="0 0 0" bindquat="1 0 0 0" vertid="13 29" vertweight="1 1"/>
|
|
<bone body="B1_14" bindpos="0 0 0" bindquat="1 0 0 0" vertid="14 30" vertweight="1 1"/>
|
|
<bone body="B1_15" bindpos="0 0 0" bindquat="1 0 0 0" vertid="15 31" vertweight="1 1"/>
|
|
</skin>
|
|
</deformable>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
int flexid1 = mj_name2id(model.get(), mjOBJ_FLEX, "B0");
|
|
int flexid2 = mj_name2id(model.get(), mjOBJ_FLEX, "B1");
|
|
EXPECT_THAT(model->flex_group[flexid1], 2);
|
|
EXPECT_THAT(model->skin_group[0], 4);
|
|
EXPECT_THAT(model->flex_group[flexid2], 4);
|
|
EXPECT_THAT(model->skin_group[1], 2);
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, InvalidSkinGroup) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<composite prefix="B0" type="grid" count="6 6 1">
|
|
<geom size=".1"/>
|
|
<skin group="6"/>
|
|
</composite>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
EXPECT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(
|
|
error.data(),
|
|
HasSubstr("skin group must be between 0 and 5\nElement 'skin', line 7"));
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, Orthographic) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<visual>
|
|
<global orthographic="true" fovy="5"/>
|
|
<map znear="0.01"/>
|
|
</visual>
|
|
|
|
<default>
|
|
<camera projection="orthographic"/>
|
|
</default>
|
|
|
|
<worldbody>
|
|
<camera name="fovy=1" pos=".5 .5 2" projection="orthographic" fovy="1"/>
|
|
<camera name="fovy=2" pos="0 0 2" fovy="2"/>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
EXPECT_THAT(model.get(), NotNull()) << error.data();
|
|
|
|
EXPECT_EQ(model->vis.global.orthographic, 1);
|
|
EXPECT_EQ(model->cam_projection[0], mjPROJ_ORTHOGRAPHIC);
|
|
EXPECT_EQ(model->cam_projection[1], mjPROJ_ORTHOGRAPHIC);
|
|
EXPECT_EQ(model->cam_fovy[0], 1);
|
|
EXPECT_EQ(model->cam_fovy[1], 2);
|
|
}
|
|
|
|
// ------------- test height-field parsing -------------------------------------
|
|
|
|
using HfieldParsingTest = MujocoTest;
|
|
|
|
TEST_F(HfieldParsingTest, NoData) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<asset>
|
|
<hfield name="hf" nrow="4" ncol="3" size="0.5 0.5 1 0.1"/>
|
|
</asset>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
EXPECT_THAT(model.get(), NotNull()) << error.data();
|
|
EXPECT_EQ(model->hfield_nrow[0], 4);
|
|
EXPECT_EQ(model->hfield_ncol[0], 3);
|
|
EXPECT_EQ(model->hfield_size[0], 0.5);
|
|
EXPECT_EQ(model->hfield_size[1], 0.5);
|
|
EXPECT_EQ(model->hfield_size[2], 1);
|
|
EXPECT_MJTNUM_EQ(model->hfield_size[3], 0.1);
|
|
}
|
|
|
|
TEST_F(HfieldParsingTest, HasDataBadSize) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<asset>
|
|
<hfield name="hf" nrow="3" ncol="2" size="0.5 0.5 1 0.1"
|
|
elevation="1 2
|
|
3 4
|
|
5 6 7"/>
|
|
</asset>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
EXPECT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(error.data(), HasSubstr("data length must match nrow*ncol"));
|
|
EXPECT_THAT(error.data(), HasSubstr("line 4"));
|
|
}
|
|
|
|
TEST_F(HfieldParsingTest, HasData) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<asset>
|
|
<hfield name="hf" nrow="3" ncol="2" size="0.5 0.5 1 0.1"
|
|
elevation="1 2
|
|
3 4
|
|
5 6"/>
|
|
</asset>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
EXPECT_THAT(model.get(), NotNull()) << error.data();
|
|
EXPECT_EQ(model->hfield_nrow[0], 3);
|
|
EXPECT_EQ(model->hfield_ncol[0], 2);
|
|
EXPECT_EQ(model->hfield_size[0], 0.5);
|
|
EXPECT_EQ(model->hfield_size[1], 0.5);
|
|
EXPECT_EQ(model->hfield_size[2], 1);
|
|
EXPECT_MJTNUM_EQ(model->hfield_size[3], 0.1);
|
|
|
|
// offset (minimum) and scaling (maximum) from normalizing operation
|
|
float offset = 1.0;
|
|
float scale = 6.0 - offset;
|
|
|
|
// compare data, note: reverse row order
|
|
EXPECT_THAT(model->hfield_data[0], FloatEq((5-offset)/scale));
|
|
EXPECT_THAT(model->hfield_data[1], FloatEq((6-offset)/scale));
|
|
EXPECT_THAT(model->hfield_data[2], FloatEq((3-offset)/scale));
|
|
EXPECT_THAT(model->hfield_data[3], FloatEq((4-offset)/scale));
|
|
EXPECT_THAT(model->hfield_data[4], FloatEq((1-offset)/scale));
|
|
EXPECT_THAT(model->hfield_data[5], FloatEq((2-offset)/scale));
|
|
}
|
|
|
|
|
|
// ------------- test relative frame sensor parsing ----------------------------
|
|
|
|
using RelativeFrameSensorParsingTest = MujocoTest;
|
|
|
|
TEST_F(RelativeFrameSensorParsingTest, RefNameButNoType) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<site name="reference"/>
|
|
<body name="sensorized"/>
|
|
</worldbody>
|
|
<sensor>
|
|
<framepos objname="sensorized" objtype="body" refname="reference"/>
|
|
</sensor>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
LoadModelFromString(xml, error.data(), error.size());
|
|
EXPECT_THAT(
|
|
error.data(),
|
|
HasSubstr("attributes 'reftype', 'refname' must be specified together"));
|
|
EXPECT_THAT(error.data(), HasSubstr("line 8"));
|
|
}
|
|
|
|
TEST_F(RelativeFrameSensorParsingTest, RefTypeButNoName) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<site name="reference"/>
|
|
<body name="sensorized"/>
|
|
</worldbody>
|
|
<sensor>
|
|
<framepos objname="sensorized" objtype="body" reftype="site"/>
|
|
</sensor>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
LoadModelFromString(xml, error.data(), error.size());
|
|
EXPECT_THAT(
|
|
error.data(),
|
|
HasSubstr("attributes 'reftype', 'refname' must be specified together"));
|
|
EXPECT_THAT(error.data(), HasSubstr("line 8"));
|
|
}
|
|
|
|
// ------------- test actlimited parsing ---------------------------------------
|
|
|
|
using ActuatorTest = MujocoTest;
|
|
|
|
TEST_F(ActuatorTest, InvalidActlimited) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<joint name="hinge"/>
|
|
<geom size="1"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<motor joint="hinge" actlimited="invalid" actrange="-1 1"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(error.data(), HasSubstr("unrecognized attribute"));
|
|
EXPECT_THAT(error.data(), HasSubstr("line 10"));
|
|
}
|
|
|
|
TEST_F(ActuatorTest, IncompleteActlimited) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<joint name="hinge"/>
|
|
<geom size="1"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<general joint="hinge" actlimited="true" dyntype="filter" actrange="-1"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(error.data(),
|
|
HasSubstr("attribute 'actrange' does not have enough data"));
|
|
}
|
|
|
|
TEST_F(ActuatorTest, ReadsByte) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<joint name="hinge"/>
|
|
<geom size="1"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<general joint="hinge" dyntype="filter" actlimited="true" actrange="-1 1"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull());
|
|
EXPECT_EQ(*(model->actuator_actlimited), true);
|
|
}
|
|
|
|
// ---------------- test actuator parsing --------------------------------------
|
|
|
|
using ActuatorParseTest = MujocoTest;
|
|
|
|
TEST_F(ActuatorParseTest, PositionTimeconst) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1"/>
|
|
<joint name="jnt"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<position joint="jnt" timeconst="2"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull());
|
|
ASSERT_NEAR(model->actuator_dynprm[0], 2.0, 1e-6);
|
|
EXPECT_THAT(model->actuator_dyntype[0], Eq(mjDYN_FILTEREXACT));
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, PositionTimeconstInheritrange) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1"/>
|
|
<joint name="jnt" range="-1 1"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<position joint="jnt" inheritrange="1" timeconst="2"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull());
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, PositionTimeconstDefault) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<default>
|
|
<position timeconst="1"/>
|
|
</default>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1"/>
|
|
<joint name="jnt"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<position joint="jnt"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull());
|
|
ASSERT_NEAR(model->actuator_dynprm[0], 1.0, 1e-6);
|
|
EXPECT_THAT(model->actuator_dyntype[0], Eq(mjDYN_FILTEREXACT));
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, PositionTimeconstDefaultOverride) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<default>
|
|
<position timeconst="1"/>
|
|
</default>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1"/>
|
|
<joint name="jnt"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<position joint="jnt" timeconst="0"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull());
|
|
EXPECT_FALSE(model->actuator_dynprm[0]);
|
|
EXPECT_THAT(model->actuator_dyntype[0], Eq(mjDYN_NONE));
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, ReadsDamper) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1"/>
|
|
<joint name="jnt" type="slide" axis="1 0 0" range="-10 10"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<damper joint="jnt" ctrlrange="0 1"/>
|
|
<general joint="jnt" ctrllimited="true" ctrlrange="0 1" gaintype="affine" biastype="none"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull());
|
|
EXPECT_THAT(model->actuator_gaintype[0], Eq(mjGAIN_AFFINE));
|
|
EXPECT_THAT(model->actuator_gaintype[1], Eq(mjGAIN_AFFINE));
|
|
EXPECT_THAT(model->actuator_biastype[0], Eq(mjBIAS_NONE));
|
|
EXPECT_THAT(model->actuator_biastype[1], Eq(mjBIAS_NONE));
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, DamperRequiresPositiveDamping) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1"/>
|
|
<joint name="jnt" type="slide" axis="1 0 0" range="-10 10"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<damper joint="jnt" kv="-1"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(error.data(),
|
|
HasSubstr("damping coefficient cannot be negative"));
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, DamperRequiresControlRange) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1"/>
|
|
<joint name="jnt" type="slide" axis="1 0 0" range="-10 10" limited="true"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<damper joint="jnt" kv="1"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(error.data(), HasSubstr("invalid control range"));
|
|
EXPECT_THAT(error.data(), HasSubstr("line 10"));
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, DamperPositiveControlRange) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1"/>
|
|
<joint name="jnt" type="slide" axis="1 0 0" range="-10 10"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<damper joint="jnt" kv="1" ctrlrange="-1 0"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(error.data(), HasSubstr("control range cannot be negative"));
|
|
EXPECT_THAT(error.data(), HasSubstr("line 10"));
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, ReadsPositionIntvelKv) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1"/>
|
|
<joint name="jnt" type="slide" axis="1 0 0"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<position joint="jnt" kv="2"/>
|
|
<intvelocity joint="jnt" actrange="-1 1" kv="3"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull());
|
|
EXPECT_THAT(model->actuator_biasprm[0*mjNBIAS + 2], Eq(-2.0));
|
|
EXPECT_THAT(model->actuator_biasprm[1*mjNBIAS + 2], Eq(-3.0));
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, RequirePositiveKv) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1"/>
|
|
<joint name="jnt" type="slide" axis="1 0 0"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<position joint="jnt" kv="-2"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(error.data(), HasSubstr("kv cannot be negative"));
|
|
EXPECT_THAT(error.data(), HasSubstr("line 10"));
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, PositionIntvelocityVelocityDefaultsPropagate) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<default>
|
|
<default class="position">
|
|
<position kp="3" kv="4" inheritrange="2"/>
|
|
</default>
|
|
<default class="intvelocity">
|
|
<intvelocity kp="5" kv="6" inheritrange="0.5"/>
|
|
</default>
|
|
<default class="velocity">
|
|
<velocity kv="7"/>
|
|
</default>
|
|
</default>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1"/>
|
|
<joint name="jnt" type="slide" axis="1 0 0" range="0 2"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<position joint="jnt" class="position"/>
|
|
<intvelocity joint="jnt" class="intvelocity"/>
|
|
<velocity joint="jnt" class="velocity"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
EXPECT_EQ(model->actuator_gainprm[0*mjNGAIN + 0], 3.0);
|
|
EXPECT_EQ(model->actuator_biasprm[0*mjNBIAS + 1], -3.0);
|
|
EXPECT_EQ(model->actuator_biasprm[0*mjNBIAS + 2], -4.0);
|
|
EXPECT_EQ(model->actuator_gainprm[1*mjNGAIN + 0], 5.0);
|
|
EXPECT_EQ(model->actuator_biasprm[1*mjNBIAS + 1], -5.0);
|
|
EXPECT_EQ(model->actuator_biasprm[1*mjNBIAS + 2], -6.0);
|
|
EXPECT_EQ(model->actuator_gainprm[2*mjNGAIN + 0], 7.0);
|
|
EXPECT_EQ(model->actuator_biasprm[2*mjNBIAS + 1], 0.0);
|
|
EXPECT_EQ(model->actuator_biasprm[2*mjNBIAS + 2], -7.0);
|
|
for (int i = 0; i < model->nu; i++) {
|
|
for (int j = 3; j < mjNBIAS; j++) {
|
|
EXPECT_EQ(model->actuator_biasprm[i*mjNBIAS + j], 0.0);
|
|
}
|
|
for (int j = 3; j < mjNGAIN; j++) {
|
|
EXPECT_EQ(model->actuator_gainprm[i*mjNGAIN + j], 0.0);
|
|
}
|
|
}
|
|
EXPECT_EQ(model->actuator_ctrlrange[0*2 + 0], -1.0);
|
|
EXPECT_EQ(model->actuator_ctrlrange[0*2 + 1], 3.0);
|
|
EXPECT_EQ(model->actuator_actrange[1*2 + 0], 0.5);
|
|
EXPECT_EQ(model->actuator_actrange[1*2 + 1], 1.5);
|
|
}
|
|
|
|
|
|
TEST_F(ActuatorParseTest, IntvelocityCheckEquivalence) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<joint name="hinge"/>
|
|
<geom size="1"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<intvelocity joint="hinge" kp="2.5" actrange="-1.57 1.57"/>
|
|
<general joint="hinge" actlimited="true" actrange="-1.57 1.57"
|
|
dyntype="integrator" biastype="affine" gainprm="2.5" biasprm="0 -2.5 0"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull());
|
|
// same actlimited
|
|
EXPECT_EQ(model->actuator_actlimited[0], 1);
|
|
EXPECT_EQ(model->actuator_actlimited[1], 1);
|
|
// same dyntype
|
|
EXPECT_EQ(model->actuator_dyntype[0], mjDYN_INTEGRATOR);
|
|
EXPECT_EQ(model->actuator_dyntype[1], mjDYN_INTEGRATOR);
|
|
// same biastype
|
|
EXPECT_EQ(model->actuator_biastype[0], mjBIAS_AFFINE);
|
|
EXPECT_EQ(model->actuator_biastype[1], mjBIAS_AFFINE);
|
|
// same gaintype
|
|
EXPECT_EQ(model->actuator_gaintype[0], mjGAIN_FIXED);
|
|
EXPECT_EQ(model->actuator_gaintype[1], mjGAIN_FIXED);
|
|
// same gainprm
|
|
EXPECT_DOUBLE_EQ(model->actuator_gainprm[0], 2.5);
|
|
EXPECT_DOUBLE_EQ(model->actuator_gainprm[mjNGAIN], 2.5);
|
|
// same biasprm
|
|
EXPECT_DOUBLE_EQ(model->actuator_biasprm[0], 0.0);
|
|
EXPECT_DOUBLE_EQ(model->actuator_biasprm[1], -2.5);
|
|
EXPECT_DOUBLE_EQ(model->actuator_biasprm[2], 0.0);
|
|
EXPECT_DOUBLE_EQ(model->actuator_biasprm[mjNBIAS], 0.0);
|
|
EXPECT_DOUBLE_EQ(model->actuator_biasprm[mjNBIAS + 1], -2.5);
|
|
EXPECT_DOUBLE_EQ(model->actuator_biasprm[mjNBIAS + 2], 0.0);
|
|
// same actrange
|
|
EXPECT_MJTNUM_EQ(model->actuator_actrange[0 + 0], -1.57);
|
|
EXPECT_MJTNUM_EQ(model->actuator_actrange[0 + 1], 1.57);
|
|
EXPECT_MJTNUM_EQ(model->actuator_actrange[0 + 2], -1.57);
|
|
EXPECT_MJTNUM_EQ(model->actuator_actrange[0 + 3], 1.57);
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, IntvelocityCheckDefaultsIfNotSpecified) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<joint name="hinge"/>
|
|
<geom size="1"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<intvelocity joint="hinge" actrange="-1 1"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull());
|
|
// check that by default kp = 1
|
|
EXPECT_DOUBLE_EQ(model->actuator_gainprm[0], 1.0);
|
|
// check that biasprm is (0, -1, 0)
|
|
EXPECT_DOUBLE_EQ(model->actuator_biasprm[0], 0.0);
|
|
EXPECT_DOUBLE_EQ(model->actuator_biasprm[1], -1.0);
|
|
EXPECT_DOUBLE_EQ(model->actuator_biasprm[2], 0.0);
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, IntvelocityNoActrangeIsValid) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<joint name="hinge"/>
|
|
<geom size="1"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<intvelocity joint="hinge"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
// actlimited resolves to false when no actrange is provided
|
|
EXPECT_EQ(model->actuator_actlimited[0], 0);
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, IntvelocityDefaultsPropagate) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<default>
|
|
<intvelocity kp="5"/>
|
|
<default class="withactrange">
|
|
<intvelocity kp="1" actrange="-1 1"/>
|
|
</default>
|
|
</default>
|
|
<worldbody>
|
|
<body>
|
|
<joint name="hinge1"/>
|
|
<joint name="hinge2"/>
|
|
<geom type="box" size=".025 .025 .025"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<intvelocity joint="hinge1" actrange="0 1"/>
|
|
<intvelocity joint="hinge2" class="withactrange"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
EXPECT_DOUBLE_EQ(model->actuator_gainprm[0], 5);
|
|
EXPECT_DOUBLE_EQ(model->actuator_gainprm[mjNGAIN], 1);
|
|
EXPECT_DOUBLE_EQ(model->actuator_actrange[0 + 0], 0);
|
|
EXPECT_DOUBLE_EQ(model->actuator_actrange[0 + 1], 1);
|
|
EXPECT_DOUBLE_EQ(model->actuator_actrange[0 + 2], -1);
|
|
EXPECT_DOUBLE_EQ(model->actuator_actrange[0 + 3], 1);
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, AdhesionDefaultsPropagate) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<default>
|
|
<adhesion ctrlrange="0 3"/>
|
|
</default>
|
|
<worldbody>
|
|
<body name="sphere">
|
|
<geom size="1"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<adhesion body="sphere"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull());
|
|
EXPECT_EQ(model->actuator_ctrlrange[0], 0);
|
|
EXPECT_EQ(model->actuator_ctrlrange[1], 3);
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, ErrorBadAdhesionDefaults) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<default>
|
|
<adhesion ctrlrange="-1 0"/>
|
|
</default>
|
|
<worldbody>
|
|
<body name="sphere">
|
|
<geom size="1"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<adhesion body="sphere"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(error.data(),
|
|
HasSubstr("adhesion control range cannot be negative"));
|
|
}
|
|
|
|
// make sure range requirement is not enforced at parse time
|
|
TEST_F(ActuatorParseTest, DampersDontRequireRange) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<default>
|
|
<damper ctrlrange="0 2"/>
|
|
</default>
|
|
<worldbody>
|
|
<body name="sphere">
|
|
<joint name="hinge"/>
|
|
<geom size="1"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<damper joint="hinge"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull());
|
|
EXPECT_EQ(model->actuator_ctrlrange[0], 0);
|
|
EXPECT_EQ(model->actuator_ctrlrange[1], 2);
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, DamperInheritsKv) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<default>
|
|
<damper kv="5" ctrlrange="0 2"/>
|
|
</default>
|
|
<worldbody>
|
|
<body name="sphere">
|
|
<joint name="hinge"/>
|
|
<geom size="1"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<damper joint="hinge"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
EXPECT_EQ(model->actuator_gainprm[2], -5.0);
|
|
}
|
|
|
|
// adhesion actuators inherit from general defaults
|
|
TEST_F(ActuatorParseTest, AdhesionInheritsFromGeneral) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<default>
|
|
<general dyntype="filter" dynprm="123" gainprm="5"/>
|
|
<adhesion ctrlrange="0 2"/>
|
|
</default>
|
|
<worldbody>
|
|
<body name="sphere">
|
|
<geom name="sphere" size="1"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<adhesion name="adhere" body="sphere"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
|
|
// expect that gainprm was inherited from the general default
|
|
EXPECT_EQ(model->actuator_gainprm[0], 5);
|
|
// expect that dynprm was inherited from the general default
|
|
EXPECT_EQ(model->actuator_dynprm[0], 123);
|
|
// expect that dyntype was inherited from the general default
|
|
EXPECT_EQ(model->actuator_dyntype[0], mjDYN_FILTER);
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, DCMotorBasicParsing) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1"/>
|
|
<joint name="jnt"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<dcmotor joint="jnt" motorconst="0.05" resistance="2.0" damping="1 2 3" armature="0.1"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
EXPECT_EQ(model->actuator_dyntype[0], mjDYN_DCMOTOR);
|
|
EXPECT_EQ(model->actuator_gaintype[0], mjGAIN_DCMOTOR);
|
|
EXPECT_EQ(model->actuator_biastype[0], mjBIAS_DCMOTOR);
|
|
EXPECT_MJTNUM_EQ(model->actuator_gainprm[0], 2.0);
|
|
EXPECT_MJTNUM_EQ(model->actuator_gainprm[1], 0.05);
|
|
EXPECT_MJTNUM_EQ(model->actuator_damping[0], 1.0);
|
|
EXPECT_MJTNUM_EQ(model->actuator_dampingpoly[0], 2.0);
|
|
EXPECT_MJTNUM_EQ(model->actuator_dampingpoly[1], 3.0);
|
|
EXPECT_MJTNUM_EQ(model->actuator_armature[0], 0.1);
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, DCMotorNominalDerivation) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1"/>
|
|
<joint name="jnt"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<!-- no damping: Ke = vn/omega0 -->
|
|
<dcmotor joint="jnt" nominal="12 0.6 600"/>
|
|
<!-- B > 0, R given: quadratic -->
|
|
<dcmotor joint="jnt" nominal="12 0 600" resistance="0.4" damping="0.0001"/>
|
|
<!-- B > 0, R from nominal: linear -->
|
|
<dcmotor joint="jnt" nominal="12 0.6 600" damping="0.0001"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
|
|
// actuator 0: B = 0, Ke = vn/omega0
|
|
{
|
|
double K = 12.0 / 600.0;
|
|
double R = K * 12.0 / 0.6;
|
|
EXPECT_MJTNUM_EQ(model->actuator_gainprm[0*mjNGAIN + 0], R);
|
|
EXPECT_MJTNUM_EQ(model->actuator_gainprm[0*mjNGAIN + 1], K);
|
|
}
|
|
|
|
// actuator 1: B > 0, R given, quadratic Ke^2*omega0 - Ke*vn + R*B*omega0 = 0
|
|
{
|
|
double B = 0.0001, R = 0.4, vn = 12.0, omega0 = 600.0;
|
|
double disc = vn*vn - 4*R*B*omega0*omega0;
|
|
double Ke = (vn + sqrt(disc)) / (2*omega0);
|
|
EXPECT_MJTNUM_EQ(model->actuator_gainprm[1*mjNGAIN + 0], R);
|
|
EXPECT_MJTNUM_EQ(model->actuator_gainprm[1*mjNGAIN + 1], Ke);
|
|
}
|
|
|
|
// actuator 2: B > 0, R from nominal, Ke = vn/omega0 - vn*B/tau0
|
|
{
|
|
double B = 0.0001, vn = 12.0, tau0 = 0.6, omega0 = 600.0;
|
|
double Ke = vn / omega0 - vn*B / tau0;
|
|
double R = Ke * vn / tau0;
|
|
EXPECT_MJTNUM_EQ(model->actuator_gainprm[2*mjNGAIN + 0], R);
|
|
EXPECT_MJTNUM_EQ(model->actuator_gainprm[2*mjNGAIN + 1], Ke);
|
|
}
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, DCMotorSaturation) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1"/>
|
|
<joint name="jnt"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<dcmotor joint="jnt" motorconst="0.05" resistance="2.0"
|
|
saturation="1.5 0"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
EXPECT_EQ(model->actuator_forcelimited[0], 1);
|
|
EXPECT_MJTNUM_EQ(model->actuator_forcerange[0], -1.5);
|
|
EXPECT_MJTNUM_EQ(model->actuator_forcerange[1], 1.5);
|
|
}
|
|
|
|
|
|
TEST_F(ActuatorParseTest, DCMotorInheritedDefaults) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<default>
|
|
<dcmotor motorconst="1.0" resistance="1.0" controller="2.0 0.5 0.1 10.0 5.0 12.0"
|
|
saturation="0 0 0" inductance="0 0.01" input="pos vel"/>
|
|
</default>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1"/>
|
|
<joint name="jnt"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<dcmotor joint="jnt" motorconst="0.05" resistance="2.0"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
|
|
// check motorconst and resistance are overridden by instance
|
|
EXPECT_MJTNUM_EQ(model->actuator_gainprm[1], 0.05);
|
|
EXPECT_MJTNUM_EQ(model->actuator_gainprm[0], 2.0);
|
|
|
|
// check controller gains (kp, ki, kd) in gainprm[4:6]
|
|
EXPECT_MJTNUM_EQ(model->actuator_gainprm[4], 2.0);
|
|
EXPECT_MJTNUM_EQ(model->actuator_gainprm[5], 0.5);
|
|
EXPECT_MJTNUM_EQ(model->actuator_gainprm[6], 0.1);
|
|
|
|
// check controller limits (slewmax, Imax) in dynprm[7,8]
|
|
EXPECT_MJTNUM_EQ(model->actuator_dynprm[7], 10.0);
|
|
EXPECT_MJTNUM_EQ(model->actuator_dynprm[8], 5.0);
|
|
|
|
// check Vmax in gainprm[7]
|
|
EXPECT_MJTNUM_EQ(model->actuator_gainprm[7], 12.0);
|
|
|
|
// check setpoint input block (gainprm[8] is retired)
|
|
EXPECT_MJTNUM_EQ(model->actuator_gainprm[8], 0.0);
|
|
EXPECT_EQ(model->actuator_ctrlnum[0], 2);
|
|
|
|
// check inductance (te) in dynprm[0]
|
|
EXPECT_MJTNUM_EQ(model->actuator_dynprm[0], 0.01);
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, DCMotorControllerFull) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1"/>
|
|
<joint name="jnt"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<dcmotor joint="jnt" motorconst="0.05" resistance="2.0" input="pos vel"
|
|
controller="1.0 2.0 3.0 4.0 5.0 6.0"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
|
|
EXPECT_MJTNUM_EQ(model->actuator_gainprm[4], 1.0);
|
|
EXPECT_MJTNUM_EQ(model->actuator_gainprm[5], 2.0);
|
|
EXPECT_MJTNUM_EQ(model->actuator_gainprm[6], 3.0);
|
|
EXPECT_MJTNUM_EQ(model->actuator_dynprm[7], 4.0);
|
|
EXPECT_MJTNUM_EQ(model->actuator_dynprm[8], 5.0);
|
|
EXPECT_MJTNUM_EQ(model->actuator_gainprm[7], 6.0);
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, DCMotorLuGreRemapping) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1"/>
|
|
<joint name="jnt"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<dcmotor joint="jnt" motorconst="0.05" resistance="2.0" damping="0.01"
|
|
lugre="100 1 0.5 0.7 10"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
EXPECT_MJTNUM_EQ(model->actuator_dynprm[5], 100);
|
|
EXPECT_MJTNUM_EQ(model->actuator_dynprm[6], 1);
|
|
EXPECT_MJTNUM_EQ(model->actuator_damping[0], 0.01);
|
|
EXPECT_MJTNUM_EQ(model->actuator_biasprm[3], 0.5);
|
|
EXPECT_MJTNUM_EQ(model->actuator_biasprm[4], 0.7);
|
|
EXPECT_MJTNUM_EQ(model->actuator_biasprm[5], 10);
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, DCMotorLuGreInheritedDefaults) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<default>
|
|
<dcmotor motorconst="0.05" resistance="2.0" damping="0.01" lugre="100 1 0.5 0.7 10"/>
|
|
</default>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1"/>
|
|
<joint name="jnt"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<dcmotor joint="jnt"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
EXPECT_MJTNUM_EQ(model->actuator_damping[0], 0.01);
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, DCMotorActdimStateless) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1"/>
|
|
<joint name="jnt"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<dcmotor joint="jnt" motorconst="0.05" resistance="2.0"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
EXPECT_EQ(model->actuator_actnum[0], 0);
|
|
EXPECT_EQ(model->actuator_actadr[0], -1);
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, DCMotorActdimCurrentOnly) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1"/>
|
|
<joint name="jnt"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<dcmotor joint="jnt" motorconst="0.05" resistance="2.0"
|
|
inductance="0.001 0"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
EXPECT_EQ(model->actuator_actnum[0], 1);
|
|
EXPECT_MJTNUM_EQ(model->actuator_dynprm[0], 0.001 / 2.0);
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, DCMotorActdimThermalOnly) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1"/>
|
|
<joint name="jnt"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<dcmotor joint="jnt" motorconst="0.05" resistance="2.0"
|
|
thermal="10 5 0 0 0 25"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
EXPECT_EQ(model->actuator_actnum[0], 1);
|
|
EXPECT_MJTNUM_EQ(model->actuator_dynprm[2], 10);
|
|
EXPECT_MJTNUM_EQ(model->actuator_dynprm[3], 5);
|
|
EXPECT_MJTNUM_EQ(model->actuator_dynprm[4], 25);
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, DCMotorActdimLuGreOnly) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1"/>
|
|
<joint name="jnt"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<dcmotor joint="jnt" motorconst="0.05" resistance="2.0"
|
|
lugre="100 1 0.5 0.7 10"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
EXPECT_EQ(model->actuator_actnum[0], 1);
|
|
EXPECT_MJTNUM_EQ(model->actuator_dynprm[5], 100);
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, DCMotorActdimAllThree) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1"/>
|
|
<joint name="jnt"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<dcmotor joint="jnt" motorconst="0.05" resistance="2.0"
|
|
inductance="0.001 0"
|
|
thermal="10 5 0 0 0 25"
|
|
lugre="100 1 0.5 0.7 10"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
EXPECT_EQ(model->actuator_actnum[0], 3);
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, DCMotorMissingKError) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1"/>
|
|
<joint name="jnt"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<dcmotor joint="jnt" resistance="2.0"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(error.data(), HasSubstr("motor constant K must be positive"));
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, DCMotorDefaultsPropagate) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<default>
|
|
<dcmotor motorconst="0.03" resistance="1.5"/>
|
|
</default>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1"/>
|
|
<joint name="jnt"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<dcmotor joint="jnt"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
EXPECT_MJTNUM_EQ(model->actuator_gainprm[0], 1.5);
|
|
EXPECT_MJTNUM_EQ(model->actuator_gainprm[1], 0.03);
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, DCMotorMotorconstGeometricMean) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1"/>
|
|
<joint name="jnt"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<dcmotor joint="jnt" motorconst="0.03 0.05" resistance="2.0"/>
|
|
<dcmotor joint="jnt" motorconst="0.03" resistance="2.0"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
double K = std::sqrt(0.03 * 0.05);
|
|
EXPECT_MJTNUM_EQ(model->actuator_gainprm[0], 2.0);
|
|
EXPECT_MJTNUM_EQ(model->actuator_gainprm[1], K);
|
|
EXPECT_MJTNUM_EQ(model->actuator_gainprm[mjNGAIN + 1], 0.03);
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, ActdimDefaultsPropagate) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<default>
|
|
<general actdim="2"/>
|
|
</default>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1"/>
|
|
<joint name="hinge"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<general joint="hinge" dyntype="user"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
|
|
// expect that actdim was inherited from the general default
|
|
EXPECT_EQ(model->actuator_actnum[0], 2);
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, MusclesParseSmoothdyn) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1"/>
|
|
<joint name="hinge" limited="true" range="-1 1"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<muscle joint="hinge"/>
|
|
<muscle joint="hinge" tausmooth="0.4"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
EXPECT_EQ(model->actuator_dynprm[2], 0.0);
|
|
EXPECT_MJTNUM_EQ(model->actuator_dynprm[mjNDYN + 2], 0.4);
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, MusclesSmoothdynNegative) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1"/>
|
|
<joint name="hinge" limited="true" range="-1 1"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<muscle joint="hinge" tausmooth="-0.4"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(error.data(), HasSubstr("muscle tausmooth cannot be negative"));
|
|
EXPECT_THAT(error.data(), HasSubstr("line 10"));
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, GroupDisable) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<option actuatorgroupdisable="0 3 8 3"/>
|
|
<!-- note: repeated numbers are okay -->
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull());
|
|
EXPECT_EQ(model->opt.disableactuator, (1<<0) + (1<<3) + (1<<8));
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, GroupDisableNegative) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<option actuatorgroupdisable="0 -3 5"/>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(error.data(), HasSubstr("must be non-negative"));
|
|
EXPECT_THAT(error.data(), HasSubstr("line 3"));
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, GroupDisableTooBig) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<option actuatorgroupdisable="0 31"/>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(error.data(), HasSubstr("cannot exceed 30"));
|
|
EXPECT_THAT(error.data(), HasSubstr("line 3"));
|
|
}
|
|
|
|
// ------------- test sensor parsing -------------------------------------------
|
|
|
|
using SensorParseTest = MujocoTest;
|
|
|
|
TEST_F(SensorParseTest, UserObjTypeNoName) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<sensor>
|
|
<user dim="1" needstage="vel" objtype="site"/>
|
|
</sensor>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(
|
|
error.data(),
|
|
HasSubstr("attributes 'objtype', 'objname' must be specified together"));
|
|
EXPECT_THAT(error.data(), HasSubstr("line 4"));
|
|
}
|
|
|
|
TEST_F(SensorParseTest, UserObjNameNoType) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<sensor>
|
|
<user dim="1" needstage="vel" objname="kevin"/>
|
|
</sensor>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(
|
|
error.data(),
|
|
HasSubstr("attributes 'objtype', 'objname' must be specified together"));
|
|
EXPECT_THAT(error.data(), HasSubstr("line 4"));
|
|
}
|
|
|
|
TEST_F(SensorParseTest, UserNeedstageAcc) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<sensor>
|
|
<user dim="1" needstage="vel"/>
|
|
<user dim="1"/>
|
|
<user dim="1" needstage="pos"/>
|
|
</sensor>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
EXPECT_EQ(model->sensor_needstage[0], mjSTAGE_VEL);
|
|
EXPECT_EQ(model->sensor_needstage[1], mjSTAGE_ACC);
|
|
EXPECT_EQ(model->sensor_needstage[2], mjSTAGE_POS);
|
|
}
|
|
|
|
// ------------- test general parsing ------------------------------------------
|
|
|
|
TEST_F(XMLReaderTest, ZnearZeroNotAllowed) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<visual>
|
|
<map znear="0"/>
|
|
</visual>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(error.data(), HasSubstr("znear must be strictly positive"));
|
|
EXPECT_THAT(error.data(), HasSubstr("line 4"));
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, ZnearNegativeNotAllowed) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<visual>
|
|
<map znear="-1"/>
|
|
</visual>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(error.data(), HasSubstr("znear must be strictly positive"));
|
|
EXPECT_THAT(error.data(), HasSubstr("line 4"));
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, ExtentZeroNotAllowed) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<statistic extent="0"/>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(error.data(), HasSubstr("extent must be strictly positive"));
|
|
EXPECT_THAT(error.data(), HasSubstr("line 3"));
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, ExtentNegativeNotAllowed) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<statistic extent="-1"/>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(error.data(), HasSubstr("extent must be strictly positive"));
|
|
EXPECT_THAT(error.data(), HasSubstr("line 3"));
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, LightRadius) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<default>
|
|
<default class="r1">
|
|
<light bulbradius="1"/>
|
|
</default>
|
|
</default>
|
|
<worldbody>
|
|
<light/>
|
|
<light class="r1"/>
|
|
<light class="r1" bulbradius="2"/>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
EXPECT_FLOAT_EQ(model->light_bulbradius[0], 0.02);
|
|
EXPECT_FLOAT_EQ(model->light_bulbradius[1], 1);
|
|
EXPECT_FLOAT_EQ(model->light_bulbradius[2], 2);
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, CameraOutput) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<camera name="default_cam"/>
|
|
<camera name="single_cam" output="depth"/>
|
|
<camera name="multi_cam" output="rgb normal segmentation"/>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
EXPECT_EQ(model->ncam, 3);
|
|
EXPECT_EQ(model->cam_output[0], mjCAMOUT_RGB); // default
|
|
EXPECT_EQ(model->cam_output[1], mjCAMOUT_DEPTH);
|
|
EXPECT_EQ(model->cam_output[2],
|
|
mjCAMOUT_RGB | mjCAMOUT_NORMAL | mjCAMOUT_SEG);
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, CameraOutputDefault) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<default>
|
|
<default class="multi">
|
|
<camera output="depth distance"/>
|
|
</default>
|
|
</default>
|
|
<worldbody>
|
|
<camera name="default_cam"/>
|
|
<camera name="class_cam" class="multi"/>
|
|
<camera name="override_cam" class="multi" output="normal"/>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
EXPECT_EQ(model->ncam, 3);
|
|
EXPECT_EQ(model->cam_output[0], mjCAMOUT_RGB); // main default
|
|
EXPECT_EQ(model->cam_output[1], mjCAMOUT_DEPTH | mjCAMOUT_DIST);
|
|
EXPECT_EQ(model->cam_output[2], mjCAMOUT_NORMAL);
|
|
}
|
|
|
|
// ------------- test delay attribute parsing ----------------------------------
|
|
|
|
TEST_F(ActuatorParseTest, ActuatorDelayParsed) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<option timestep="1"/>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1"/>
|
|
<joint name="jnt1"/>
|
|
<joint name="jnt2"/>
|
|
<joint name="jnt3"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<motor joint="jnt1"/>
|
|
<motor joint="jnt2" delay="3" nsample="3" interp="zoh"/>
|
|
<motor joint="jnt3" delay="10" nsample="10" interp="cubic"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
ASSERT_EQ(model->nu, 3);
|
|
// actuator_history[2*i] = nsample, actuator_history[2*i+1] = interp
|
|
EXPECT_EQ(model->actuator_history[0], 0); // jnt1 nsample
|
|
EXPECT_EQ(model->actuator_history[1], 0); // jnt1 interp (no buffer, def 0)
|
|
EXPECT_EQ(model->actuator_history[2], 3); // jnt2 nsample
|
|
EXPECT_EQ(model->actuator_history[3], 0); // jnt2 interp (ZOH)
|
|
EXPECT_EQ(model->actuator_history[4], 10); // jnt3 nsample
|
|
EXPECT_EQ(model->actuator_history[5], 2); // jnt3 interp (cubic)
|
|
EXPECT_EQ(model->actuator_historyadr[0], -1);
|
|
EXPECT_EQ(model->actuator_historyadr[1], 0);
|
|
EXPECT_EQ(model->actuator_historyadr[2], 8); // 2+2*3 = 8
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, ActuatorDelayDefault) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<option timestep="1"/>
|
|
<default>
|
|
<motor delay="5" nsample="5"/>
|
|
</default>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1"/>
|
|
<joint name="jnt1"/>
|
|
<joint name="jnt2"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<motor joint="jnt1"/>
|
|
<motor joint="jnt2" delay="0" nsample="0"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
ASSERT_EQ(model->nu, 2);
|
|
EXPECT_EQ(model->actuator_history[0], 5);
|
|
EXPECT_EQ(model->actuator_history[2], 0);
|
|
EXPECT_EQ(model->actuator_historyadr[0], 0);
|
|
EXPECT_EQ(model->actuator_historyadr[1], -1);
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, ActuatorDelayRequiresHistory) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1"/>
|
|
<joint name="jnt"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<motor joint="jnt" delay="1"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), IsNull());
|
|
EXPECT_THAT(error.data(),
|
|
HasSubstr("setting delay > 0 without a history buffer"));
|
|
}
|
|
|
|
TEST_F(ActuatorParseTest, DampingArmatureDefaultsPropagate) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco>
|
|
<default>
|
|
<motor damping="3" armature="0.5"/>
|
|
</default>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1"/>
|
|
<joint name="jnt" type="slide" axis="1 0 0"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator>
|
|
<motor joint="jnt"/>
|
|
</actuator>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
MjModelPtr model = LoadModelFromString(xml, error.data(), error.size());
|
|
ASSERT_THAT(model.get(), NotNull()) << error.data();
|
|
EXPECT_EQ(model->actuator_damping[0], 3);
|
|
EXPECT_EQ(model->actuator_armature[0], 0.5);
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, AttachConflictXMLWarning) {
|
|
mock_warning_handler.ExpectWarnings();
|
|
std::array<char, 1024> error;
|
|
std::string path = GetTestDataFilePath("xml/testdata/parent_warn.xml");
|
|
mjSpec* spec = mj_parseXML(path.c_str(), nullptr, error.data(), error.size());
|
|
ASSERT_THAT(spec, NotNull()) << error.data();
|
|
|
|
// one grouped warning per attach, containing gravity and damper flag
|
|
// (constraint is only authored by child, not a conflict)
|
|
EXPECT_EQ(mjs_numWarnings(spec), 1);
|
|
std::string w = mjs_getWarning(spec, 0);
|
|
EXPECT_THAT(w, HasSubstr("gravity: parent has 0 0 -10, child has 0 0 0,"
|
|
" keeping parent value"));
|
|
EXPECT_THAT(w, HasSubstr("flag 'Damper'"));
|
|
|
|
mjModel* m = mj_compile(spec, nullptr);
|
|
ASSERT_THAT(m, NotNull());
|
|
// Gravity should be parent's value (0 0 -10)
|
|
EXPECT_MJTNUM_EQ(m->opt.gravity[0], 0);
|
|
EXPECT_MJTNUM_EQ(m->opt.gravity[1], 0);
|
|
EXPECT_MJTNUM_EQ(m->opt.gravity[2], -10);
|
|
|
|
mj_deleteModel(m);
|
|
mj_deleteSpec(spec);
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, AttachConflictXMLMerge) {
|
|
mock_warning_handler.ExpectWarnings();
|
|
std::array<char, 1024> error;
|
|
std::string path = GetTestDataFilePath("xml/testdata/parent_merge.xml");
|
|
mjSpec* spec = mj_parseXML(path.c_str(), nullptr, error.data(), error.size());
|
|
ASSERT_THAT(spec, NotNull()) << error.data();
|
|
|
|
// one grouped warning per attach, containing timestep, iterations, and flag
|
|
EXPECT_GE(mjs_numWarnings(spec), 1);
|
|
std::string w = mjs_getWarning(spec, 0);
|
|
EXPECT_THAT(w, HasSubstr("timestep: parent has 0.005, child has 0.002,"
|
|
" taking the minimum"));
|
|
EXPECT_THAT(w, HasSubstr("iterations: parent has 50, child has 100,"
|
|
" taking the maximum"));
|
|
|
|
mjModel* m = mj_compile(spec, nullptr);
|
|
ASSERT_THAT(m, NotNull());
|
|
// Timestep should be min (0.002)
|
|
EXPECT_MJTNUM_EQ(m->opt.timestep, 0.002);
|
|
// Iterations should be max (100)
|
|
EXPECT_EQ(m->opt.iterations, 100);
|
|
|
|
mj_deleteModel(m);
|
|
mj_deleteSpec(spec);
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, AttachConflictXMLError) {
|
|
std::array<char, 1024> error;
|
|
std::string path = GetTestDataFilePath("xml/testdata/parent_error.xml");
|
|
mjSpec* spec = mj_parseXML(path.c_str(), nullptr, error.data(), error.size());
|
|
|
|
// Should fail to parse because of conflict in timestep in error mode
|
|
EXPECT_THAT(spec, IsNull());
|
|
EXPECT_THAT(error.data(),
|
|
HasSubstr("timestep: parent has 0.005, child has 0.002"));
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, AttachConflictXMLMergeUnmergableError) {
|
|
std::array<char, 1024> error;
|
|
std::string path =
|
|
GetTestDataFilePath("xml/testdata/parent_merge_unmergable.xml");
|
|
mjSpec* spec = mj_parseXML(path.c_str(), nullptr, error.data(), error.size());
|
|
|
|
// Should fail to parse because gravity is unmergeable in merge mode
|
|
EXPECT_THAT(spec, IsNull());
|
|
EXPECT_THAT(error.data(),
|
|
HasSubstr("gravity: parent has 0 0 -10, child has 0 0 0"));
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, SelfAttach) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco model="self-attach-test">
|
|
<worldbody>
|
|
<body name="body1">
|
|
<geom name="geom1" size="1"/>
|
|
</body>
|
|
<body name="body2">
|
|
<attach body="body1" prefix="attached_"/>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
mjSpec* spec = mj_parseXMLString(xml, nullptr, error.data(), error.size());
|
|
ASSERT_THAT(spec, NotNull()) << error.data();
|
|
|
|
mjModel* m = mj_compile(spec, nullptr);
|
|
ASSERT_THAT(m, NotNull());
|
|
|
|
int body2_id = mj_name2id(m, mjOBJ_BODY, "body2");
|
|
int attached_body1_id = mj_name2id(m, mjOBJ_BODY, "attached_body1");
|
|
|
|
EXPECT_GE(body2_id, 0);
|
|
EXPECT_GE(attached_body1_id, 0);
|
|
EXPECT_EQ(m->body_parentid[attached_body1_id], body2_id);
|
|
|
|
mj_deleteModel(m);
|
|
mj_deleteSpec(spec);
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, SelfAttachCollisionError) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco model="self-attach-collision">
|
|
<worldbody>
|
|
<body name="body1"/>
|
|
<body name="attached_body1"/>
|
|
<body name="body2">
|
|
<attach body="body1" prefix="attached_"/>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
mjSpec* spec = mj_parseXMLString(xml, nullptr, error.data(), error.size());
|
|
EXPECT_THAT(spec, IsNull());
|
|
EXPECT_THAT(
|
|
error.data(),
|
|
HasSubstr("cannot self-attach: element attached_body1 already exists"));
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, SelfAttachMissingError) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco model="self-attach-missing">
|
|
<worldbody>
|
|
<body name="body1">
|
|
<attach body="nonexistent" prefix="attached_"/>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
mjSpec* spec = mj_parseXMLString(xml, nullptr, error.data(), error.size());
|
|
EXPECT_THAT(spec, IsNull());
|
|
EXPECT_THAT(error.data(), HasSubstr("could not find body 'nonexistent' in "
|
|
"the current model for self-attachment"));
|
|
}
|
|
|
|
TEST_F(XMLReaderTest, SelfAttachFrame) {
|
|
static constexpr char xml[] = R"(
|
|
<mujoco model="self-attach-frame-test">
|
|
<worldbody>
|
|
<frame name="frame1">
|
|
<body name="body1" pos="-1 0 0">
|
|
<geom name="geom1" type="box" size="1 1 1"/>
|
|
</body>
|
|
</frame>
|
|
<body name="body2">
|
|
<attach frame="frame1" prefix="attached_"/>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
std::array<char, 1024> error;
|
|
mjSpec* spec = mj_parseXMLString(xml, nullptr, error.data(), error.size());
|
|
ASSERT_THAT(spec, NotNull()) << error.data();
|
|
|
|
mjModel* m = mj_compile(spec, nullptr);
|
|
ASSERT_THAT(m, NotNull()) << mjs_getError(spec);
|
|
|
|
int body2_id = mj_name2id(m, mjOBJ_BODY, "body2");
|
|
int attached_body1_id = mj_name2id(m, mjOBJ_BODY, "attached_body1");
|
|
|
|
EXPECT_GE(body2_id, 0);
|
|
EXPECT_GE(attached_body1_id, 0);
|
|
EXPECT_EQ(m->body_parentid[attached_body1_id], body2_id);
|
|
|
|
mj_deleteModel(m);
|
|
mj_deleteSpec(spec);
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace mujoco
|