diff --git a/doc/XMLreference.rst b/doc/XMLreference.rst index 2d35e07b..c901e7e9 100644 --- a/doc/XMLreference.rst +++ b/doc/XMLreference.rst @@ -3913,7 +3913,7 @@ at a specified body, in global coordinates. .. _sensor-clock: :el-prefix:`sensor/` **clock** (*) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This element creates sensor that returns the simulation time. @@ -3934,12 +3934,12 @@ bodies whose center of mass is of interest. :at:`name`, :at:`noise`, :at:`cutoff`, :at:`user` See :ref:`CSensor`. -:at:`objtype`: :at-val:`(any element type that can be named), required` +:at:`objtype`: :at-val:`(any element type that can be named), optional` Type of the MuJoCo object to which the sensor is attached. This together with the objname attribute determines the - actual object. -:at:`objname`: :at-val:`string, required` + actual object. If unspecified, will be :ref:`mjOBJ_UNKNOWN`. +:at:`objname`: :at-val:`string, optional` Name of the MuJoCo object to which the sensor is attached. -:at:`datatype`: :at-val:`[real, positive, axis, quaternion], required` +:at:`datatype`: :at-val:`[real, positive, axis, quaternion], "real"` The type of output generated by this sensor. "axis" means a unit-length 3D vector. "quat" means a unit quaternion. These need to be declared because when MuJoCo adds noise, it must respect the vector normalization. "real" means a generic array (or scalar) of real values to which noise can be added independently. @@ -3952,7 +3952,7 @@ bodies whose center of mass is of interest. .. _keyframe: **keyframe** (*) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~ This is a grouping element for keyframe definitions. It does not have attributes. Keyframes can be used to create a library of states that are of interest to the user, and to initialize the simulation state to one of the states in the diff --git a/doc/changelog.rst b/doc/changelog.rst index b3dbbfc4..b3431695 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -54,6 +54,8 @@ General ``dlopen`` on a POSIX system, and to ``LoadLibraryA`` on Windows. The second function scans a specified directory for all dynamic libraries file and loads each library found. Dynamic libraries opened by these functions are assumed to register one or more MuJoCo plugins on load. +- Sensors of type :ref:`user` no longer require :at:`objtype` and :at:`objname`. If unspecified, the + objtype will be :ref:`mjOBJ_UNKNOWN`. ``user`` sensors :at:`datatype` default is now :at-val:`"real"`. Simulate ^^^^^^^^ diff --git a/src/user/user_objects.cc b/src/user/user_objects.cc index 845dc2a0..3f190a95 100644 --- a/src/user/user_objects.cc +++ b/src/user/user_objects.cc @@ -3697,7 +3697,7 @@ void mjCSensor::Compile(void) { // get sensorized object id objid = pobj->id; - } else if (type != mjSENS_CLOCK && type != mjSENS_PLUGIN) { + } else if (type != mjSENS_CLOCK && type != mjSENS_PLUGIN && type != mjSENS_USER) { throw mjCError(this, "invalid type in sensor '%s' (id = %d)", name.c_str(), id); } @@ -3779,7 +3779,7 @@ void mjCSensor::Compile(void) { "joint must be slide or hinge in sensor '%s' (id = %d)", name.c_str(), id); } - //set + // set dim = 1; datatype = mjDATATYPE_REAL; if (type==mjSENS_JOINTPOS) { diff --git a/src/xml/xml_native_reader.cc b/src/xml/xml_native_reader.cc index 9aba1b76..3576856e 100644 --- a/src/xml/xml_native_reader.cc +++ b/src/xml/xml_native_reader.cc @@ -3147,16 +3147,23 @@ void mjXReader::Sensor(XMLElement* section) { // user-defined sensor else if (type=="user") { psen->type = mjSENS_USER; - ReadAttrTxt(elem, "objtype", text, true); - psen->objtype = (mjtObj)mju_str2Type(text.c_str()); - ReadAttrTxt(elem, "objname", psen->objname, true); + bool objname_given = ReadAttrTxt(elem, "objname", psen->objname); + if (ReadAttrTxt(elem, "objtype", text)) { + if (!objname_given) { + throw mjXError(elem, "objtype '%s' given but objname is missing", text.c_str()); + } + psen->objtype = (mjtObj)mju_str2Type(text.c_str()); + } else if (objname_given) { + throw mjXError(elem, "objname '%s' given but objtype is missing", psen->objname.c_str()); + } ReadAttrInt(elem, "dim", &psen->dim, true); // keywords MapValue(elem, "needstage", &n, stage_map, stage_sz, true); psen->needstage = (mjtStage)n; - MapValue(elem, "datatype", &n, datatype_map, datatype_sz, true); - psen->datatype = (mjtDataType)n; + if (MapValue(elem, "datatype", &n, datatype_map, datatype_sz)) { + psen->datatype = (mjtDataType)n; + } } else if (type=="plugin") { diff --git a/src/xml/xml_native_writer.cc b/src/xml/xml_native_writer.cc index 088bdc2a..6097ef3c 100644 --- a/src/xml/xml_native_writer.cc +++ b/src/xml/xml_native_writer.cc @@ -1768,7 +1768,7 @@ void mjXWriter::Sensor(XMLElement* root) { // user-defined sensor case mjSENS_USER: elem = InsertEnd(section, "user"); - WriteAttrTxt(elem, "objtype", mju_type2Str(psen->objtype)); + if (mju_type2Str(psen->objtype)) WriteAttrTxt(elem, "objtype", mju_type2Str(psen->objtype)); WriteAttrTxt(elem, "objname", psen->objname); WriteAttrInt(elem, "dim", psen->dim); WriteAttrKey(elem, "needstage", stage_map, stage_sz, (int)psen->needstage); diff --git a/test/user/user_objects_test.cc b/test/user/user_objects_test.cc index 4a258fc0..6c959d53 100644 --- a/test/user/user_objects_test.cc +++ b/test/user/user_objects_test.cc @@ -202,6 +202,29 @@ TEST_F(RelativeFrameSensorParsingTest, BadRefType) { EXPECT_THAT(error.data(), HasSubstr("reference frame object must be")); } +// ------------- sensor compilation -------------------------------------------- + +using SensorTest = MujocoTest; +TEST_F(SensorTest, OjbtypeParsedButNotRequired) { + static constexpr char xml[] = R"( + + + + + + + )"; + mjModel* model = LoadModelFromString(xml, 0, 0); + ASSERT_THAT(model, NotNull()); + EXPECT_EQ(model->sensor_datatype[0], mjDATATYPE_AXIS); + EXPECT_EQ(model->sensor_objtype[0], mjOBJ_UNKNOWN); + EXPECT_EQ(model->sensor_dim[0], 3); + EXPECT_EQ(model->sensor_datatype[1], mjDATATYPE_REAL); + EXPECT_EQ(model->sensor_objtype[1], mjOBJ_BODY); + EXPECT_EQ(model->sensor_objid[1], 0); + mj_deleteModel(model); +} + // ------------- test capsule inertias ----------------------------------------- static const char* const kCapsuleInertiaPath = diff --git a/test/xml/xml_native_reader_test.cc b/test/xml/xml_native_reader_test.cc index 5d04f65b..1365bd35 100644 --- a/test/xml/xml_native_reader_test.cc +++ b/test/xml/xml_native_reader_test.cc @@ -935,6 +935,38 @@ TEST_F(ActuatorParseTest, ActdimDefaultsPropagate) { mj_deleteModel(model); } +// ------------- test sensor parsing ------------------------------------------- + +using SensorParseTest = MujocoTest; + +TEST_F(SensorParseTest, UserObjTypeNoName) { + static constexpr char xml[] = R"( + + + + + + )"; + std::array error; + mjModel* model = LoadModelFromString(xml, error.data(), error.size()); + ASSERT_THAT(model, IsNull()); + EXPECT_THAT(error.data(), HasSubstr("objtype 'site' given but")); +} + +TEST_F(SensorParseTest, UserObjNameNoType) { + static constexpr char xml[] = R"( + + + + + + )"; + std::array error; + mjModel* model = LoadModelFromString(xml, error.data(), error.size()); + ASSERT_THAT(model, IsNull()); + EXPECT_THAT(error.data(), HasSubstr("objname 'kevin' given but")); +} + // ------------- test general parsing ------------------------------------------ TEST_F(XMLReaderTest, ZnearZeroNotAllowed) { diff --git a/test/xml/xml_native_writer_test.cc b/test/xml/xml_native_writer_test.cc index 84c7722d..5d962045 100644 --- a/test/xml/xml_native_writer_test.cc +++ b/test/xml/xml_native_writer_test.cc @@ -129,6 +129,24 @@ TEST_F(XMLWriterTest, SavesDisableSensor) { mj_deleteModel(model); } +TEST_F(XMLWriterTest, EmptyUserSensor) { + static constexpr char xml[] = R"( + + + + + + )"; + mjModel* model = LoadModelFromString(xml); + std::string saved_xml = SaveAndReadXml(model); + EXPECT_THAT(saved_xml, HasSubstr("dim=\"2\"")); + EXPECT_THAT(saved_xml, HasSubstr("needstage=\"vel\"")); + EXPECT_THAT(saved_xml, HasSubstr("datatype=\"real\"")); + EXPECT_THAT(saved_xml, Not(HasSubstr("objtype"))); + EXPECT_THAT(saved_xml, Not(HasSubstr("objname"))); + mj_deleteModel(model); +} + TEST_F(XMLWriterTest, KeepsEmptyClasses) { static constexpr char xml[] = R"(