add mjs_detachDefault to delete a default class from mjSpec

PiperOrigin-RevId: 738452369
Change-Id: I5a53a6dc8f89e1e8136aeefc09d7d9b7b7204e1f
This commit is contained in:
Tom Power
2025-03-19 10:30:08 -07:00
committed by Copybara-Service
parent a1b9ffd124
commit be64747166
18 changed files with 261 additions and 24 deletions
+12 -2
View File
@@ -3851,7 +3851,16 @@ Attach child frame to a parent site, return the attached frame if success or NUL
.. mujoco-include:: mjs_detachBody
Detach body from mjSpec, remove all references and delete the body, return 0 on success.
Delete body and descendants from mjSpec, remove all references, return 0 on success.
.. _mjs_detachDefault:
`mjs_detachDefault <#mjs_detachDefault>`__
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. mujoco-include:: mjs_detachDefault
Delete default class and descendants from mjSpec, remove all references, return 0 on success.
.. _AddTreeElements:
@@ -3936,7 +3945,8 @@ Add frame to body.
.. mujoco-include:: mjs_delete
Delete object corresponding to the given element, return 0 on success.
Delete object corresponding to the given element, return 0 on success. This function should only be used for element
types that cannot have children, i.e. excluding bodies and default classes.
.. _AddNonTreeElements:
+5
View File
@@ -704,3 +704,8 @@ to the inputs. Below, :math:`\bar q` denotes the pre-modified quaternion:
Note that derivatives depend only on :math:`h` and :math:`v` (in fact, on :math:`s = h v`).
All outputs are optional.
.. _mjs_delete:
Delete object corresponding to the given element, return 0 on success. This function should only be used for element
types that cannot have children, i.e. excluding bodies and default classes.
+5 -2
View File
@@ -603,7 +603,9 @@ typedef enum mjtObj_ { // type of MujoCo object
mjNOBJECT, // number of object types
// meta elements, do not appear in mjModel
mjOBJ_FRAME = 100 // frame
mjOBJ_FRAME = 100, // frame
mjOBJ_DEFAULT // default
} mjtObj;
typedef enum mjtConstraint_ { // type of constraint
mjCNSTR_EQUALITY = 0, // equality constraint
@@ -3612,6 +3614,7 @@ mjsBody* mjs_attachToSite(mjsSite* parent, const mjsBody* child,
mjsFrame* mjs_attachFrameToSite(mjsSite* parent, const mjsFrame* child,
const char* prefix, const char* suffix);
int mjs_detachBody(mjSpec* s, mjsBody* b);
int mjs_detachDefault(mjSpec* s, mjsDefault* d);
mjsBody* mjs_addBody(mjsBody* body, const mjsDefault* def);
mjsSite* mjs_addSite(mjsBody* body, const mjsDefault* def);
mjsJoint* mjs_addJoint(mjsBody* body, const mjsDefault* def);
@@ -3652,7 +3655,7 @@ mjsBody* mjs_getParent(mjsElement* element);
mjsFrame* mjs_getFrame(mjsElement* element);
mjsFrame* mjs_findFrame(mjSpec* s, const char* name);
mjsDefault* mjs_getDefault(mjsElement* element);
const mjsDefault* mjs_findDefault(mjSpec* s, const char* classname);
mjsDefault* mjs_findDefault(mjSpec* s, const char* classname);
mjsDefault* mjs_getSpecDefault(mjSpec* s);
int mjs_getId(mjsElement* element);
mjsElement* mjs_firstChild(mjsBody* body, mjtObj type, int recurse);
+3 -1
View File
@@ -268,7 +268,9 @@ typedef enum mjtObj_ { // type of MujoCo object
mjNOBJECT, // number of object types
// meta elements, do not appear in mjModel
mjOBJ_FRAME = 100 // frame
mjOBJ_FRAME = 100, // frame
mjOBJ_DEFAULT // default
} mjtObj;
+4 -2
View File
@@ -1430,9 +1430,11 @@ MJAPI mjsBody* mjs_attachToSite(mjsSite* parent, const mjsBody* child,
MJAPI mjsFrame* mjs_attachFrameToSite(mjsSite* parent, const mjsFrame* child,
const char* prefix, const char* suffix);
// Detach body from mjSpec, remove all references and delete the body, return 0 on success.
// Delete body and descendants from mjSpec, remove all references, return 0 on success.
MJAPI int mjs_detachBody(mjSpec* s, mjsBody* b);
// Delete default class and descendants from mjSpec, remove all references, return 0 on success.
MJAPI int mjs_detachDefault(mjSpec* s, mjsDefault* d);
//---------------------------------- Tree elements -------------------------------------------------
@@ -1566,7 +1568,7 @@ MJAPI mjsFrame* mjs_findFrame(mjSpec* s, const char* name);
MJAPI mjsDefault* mjs_getDefault(mjsElement* element);
// Find default in model by class name.
MJAPI const mjsDefault* mjs_findDefault(mjSpec* s, const char* classname);
MJAPI mjsDefault* mjs_findDefault(mjSpec* s, const char* classname);
// Get global default from model.
MJAPI mjsDefault* mjs_getSpecDefault(mjSpec* s);
+1
View File
@@ -287,6 +287,7 @@ ENUMS: Mapping[str, EnumDecl] = dict([
('mjOBJ_PLUGIN', 25),
('mjNOBJECT', 26),
('mjOBJ_FRAME', 100),
('mjOBJ_DEFAULT', 101),
]),
)),
('mjtConstraint',
+22 -2
View File
@@ -9154,7 +9154,27 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([
),
),
),
doc='Detach body from mjSpec, remove all references and delete the body, return 0 on success.', # pylint: disable=line-too-long
doc='Delete body and descendants from mjSpec, remove all references, return 0 on success.', # pylint: disable=line-too-long
)),
('mjs_detachDefault',
FunctionDecl(
name='mjs_detachDefault',
return_type=ValueType(name='int'),
parameters=(
FunctionParameterDecl(
name='s',
type=PointerType(
inner_type=ValueType(name='mjSpec'),
),
),
FunctionParameterDecl(
name='d',
type=PointerType(
inner_type=ValueType(name='mjsDefault'),
),
),
),
doc='Delete default class and descendants from mjSpec, remove all references, return 0 on success.', # pylint: disable=line-too-long
)),
('mjs_addBody',
FunctionDecl(
@@ -9954,7 +9974,7 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([
FunctionDecl(
name='mjs_findDefault',
return_type=PointerType(
inner_type=ValueType(name='mjsDefault', is_const=True),
inner_type=ValueType(name='mjsDefault'),
),
parameters=(
FunctionParameterDecl(
+6 -1
View File
@@ -448,7 +448,7 @@ PYBIND11_MODULE(_specs, m) {
py::return_value_policy::reference_internal);
mjSpec.def(
"find_default",
[](MjSpec& self, std::string& classname) -> const raw::MjsDefault* {
[](MjSpec& self, std::string& classname) -> raw::MjsDefault* {
return mjs_findDefault(self.ptr, classname.c_str());
},
py::return_value_policy::reference_internal);
@@ -499,6 +499,11 @@ PYBIND11_MODULE(_specs, m) {
return mjs_addDefault(spec->ptr, classname.c_str(), parent);
},
py::return_value_policy::reference_internal);
mjSpec.def("detach_default", [](MjSpec& self, raw::MjsDefault& def) {
if (mjs_detachDefault(self.ptr, &def) != 0) {
throw pybind11::value_error(mjs_getError(self.ptr));
}
});
mjSpec.def_property_readonly(
"default",
[](MjSpec& self) -> raw::MjsDefault* {
+6
View File
@@ -589,6 +589,12 @@ class SpecsTest(absltest.TestCase):
spec.compile()
self.assertEqual(spec.to_xml(), XML)
# test delete default
def1 = spec.find_default('def1')
spec.detach_default(def1)
def1 = spec.find_default('def1')
self.assertIsNone(def1)
def test_element_list(self):
spec = mujoco.MjSpec()
sensor1 = spec.add_sensor()
+1
View File
@@ -2137,6 +2137,7 @@ static int sensorSize(mjtSensor sensor_type, int sensor_dim) {
// -2: invalid objtype
static int numObjects(const mjModel* m, mjtObj objtype) {
switch (objtype) {
case mjOBJ_DEFAULT:
case mjOBJ_FRAME:
case mjOBJ_UNKNOWN:
return -1;
+26 -7
View File
@@ -227,7 +227,7 @@ const char* mjs_getError(mjSpec* s) {
// Detach body from mjSpec, return 0 if success.
// detach body from mjSpec, return 0 on success
int mjs_detachBody(mjSpec* s, mjsBody* b) {
mjCModel* model = static_cast<mjCModel*>(s->element);
mjCBody* body = static_cast<mjCBody*>(b->element);
@@ -241,7 +241,22 @@ int mjs_detachBody(mjSpec* s, mjsBody* b) {
return 0;
}
// detach default from mjSpec, return 0 on success
int mjs_detachDefault(mjSpec* s, mjsDefault* def) {
mjCModel* modelC = static_cast<mjCModel*>(s->element);
if (!def) {
modelC->SetError(mjCError(0, "Cannot detach, default is null"));
return -1;
}
mjCDef* defC = static_cast<mjCDef*>(def->element);
try {
*modelC -= *defC;
} catch (mjCError& e) {
modelC->SetError(e);
return -1;
}
return 0;
}
// check if model has warnings
int mjs_isWarning(mjSpec* s) {
@@ -292,15 +307,19 @@ int mjs_setDeepCopy(mjSpec* s, int deepcopy) {
// delete object, return 0 if success
// delete object, return 0 on success
int mjs_delete(mjsElement* element) {
mjCBase* object = static_cast<mjCBase*>(element);
mjCModel* model;
if (element->elemtype == mjOBJ_DEFAULT)
model = static_cast<mjCDef*>(element)->model;
else
model = static_cast<mjCBase*>(element)->model;
try {
// it will call the appropriate destructor since ~mjCBase is virtual
object->model->DeleteElement(element);
model->DeleteElement(element);
return 0;
} catch (mjCError& e) {
object->model->SetError(e);
model->SetError(e);
return -1;
}
}
@@ -625,7 +644,7 @@ mjsDefault* mjs_getDefault(mjsElement* element) {
// Find default with given name in model.
const mjsDefault* mjs_findDefault(mjSpec* s, const char* classname) {
mjsDefault* mjs_findDefault(mjSpec* s, const char* classname) {
mjCModel* modelC = static_cast<mjCModel*>(s->element);
mjCDef* cdef = modelC->FindDefault(classname);
if (!cdef) {
+3 -1
View File
@@ -88,6 +88,8 @@ MJAPI mjsFrame* mjs_attachFrameToSite(mjsSite* parent, const mjsFrame* child,
// Detach body from mjSpec, remove all references and delete the body, return 0 on success.
MJAPI int mjs_detachBody(mjSpec* s, mjsBody* b);
// Detach default from mjSpec, remove all references and delete the default, return 0 on success.
MJAPI int mjs_detachDefault(mjSpec* s, mjsDefault* d);
//---------------------------------- Add tree elements ---------------------------------------------
@@ -221,7 +223,7 @@ MJAPI mjsFrame* mjs_findFrame(mjSpec* s, const char* name);
MJAPI mjsDefault* mjs_getDefault(mjsElement* element);
// Find default in model by class name.
MJAPI const mjsDefault* mjs_findDefault(mjSpec* s, const char* classname);
MJAPI mjsDefault* mjs_findDefault(mjSpec* s, const char* classname);
// Get global default from model.
MJAPI mjsDefault* mjs_getSpecDefault(mjSpec* s);
+60 -2
View File
@@ -24,6 +24,7 @@
#include <cstdlib>
#include <cstring>
#include <exception>
#include <functional>
#include <mutex>
#include <string>
#include <string_view>
@@ -144,7 +145,7 @@ mjCModel::mjCModel() {
Clear();
//------------------------ master default set
defaults_.push_back(new mjCDef);
defaults_.push_back(new mjCDef(this));
defaults_.back()->name = "main";
// point to model from spec
@@ -564,6 +565,7 @@ mjCModel& mjCModel::operator-=(const mjCBody& subtree) {
mjCModel_& mjCModel::operator+=(mjCDef& subtree) {
defaults_.push_back(&subtree);
def_map[subtree.name] = &subtree;
subtree.model = this;
// set parent to the main default if this is not the only default in the model
if (!subtree.parent && &subtree != defaults_[0]) {
@@ -579,6 +581,58 @@ mjCModel_& mjCModel::operator+=(mjCDef& subtree) {
// remove default class from array
mjCModel& mjCModel::operator-=(const mjCDef& subtree) {
// check we aren't trying to remove the 'main' default
if (subtree.id == 0) {
throw mjCError(0, "cannot remove the global default ('main')");
}
// remove this default from parent's child list
mjCDef* parent = subtree.parent;
if (parent) {
for (int i = 0; i < parent->child.size(); ++i) {
if (parent->child[i] == &subtree) {
parent->child.erase(parent->child.begin() + i);
break;
}
}
}
// traverse tree to find all descendants starting from subtree.id
std::vector<int> default_ids_to_remove;
std::vector<int> stack;
stack.push_back(subtree.id);
while (!stack.empty()) {
int id = stack.back();
stack.pop_back();
default_ids_to_remove.push_back(id);
for (int i=0; i<defaults_[id]->child.size(); i++) {
stack.push_back(defaults_[id]->child[i]->id);
}
}
// remove from the tree
std::sort(default_ids_to_remove.begin(),
default_ids_to_remove.end(),
std::greater<int>());
for (int id : default_ids_to_remove) {
delete defaults_[id];
defaults_.erase(defaults_.begin() + id);
}
// reset default ids
for (int i = 0; i < defaults_.size(); ++i) {
defaults_[i]->id = i;
}
return *this;
}
template <class T>
void deletefromlist(std::vector<T*>* list, mjsElement* element) {
if (!list) {
@@ -609,6 +663,10 @@ void mjCModel::DeleteElement(mjsElement* el) {
throw mjCError(nullptr, "bodies cannot be deleted, use detach instead");
break;
case mjOBJ_DEFAULT:
throw mjCError(nullptr, "defaults cannot be deleted, use detach instead");
break;
case mjOBJ_GEOM:
{
mjCGeom* geom = static_cast<mjCGeom*>(el);
@@ -1180,7 +1238,7 @@ mjCDef* mjCModel::AddDefault(string name, mjCDef* parent) {
}
// create new object
mjCDef* def = new mjCDef;
mjCDef* def = new mjCDef(parent->model);
defaults_.push_back(def);
def->id = thisid;
+8 -4
View File
@@ -183,10 +183,11 @@ class mjCModel : public mjCModel_, private mjSpec {
void CopyFromSpec(); // copy spec to private attributes
void PointToLocal();
mjCModel& operator=(const mjCModel& other); // copy other into this, if they are not the same
mjCModel& operator+=(const mjCModel& other); // add other into this, even if they are the same
mjCModel& operator-=(const mjCBody& subtree); // remove subtree and all references from model
mjCModel_& operator+=(mjCDef& subtree); // add default tree to this model
mjCModel& operator=(const mjCModel& other); // copy other into this, if they are not the same
mjCModel& operator+=(const mjCModel& other); // add other into this, even if they are the same
mjCModel& operator-=(const mjCBody& subtree); // remove subtree and all references from model
mjCModel_& operator+=(mjCDef& subtree); // add default tree to this model
mjCModel& operator-=(const mjCDef& subtree); // remove default tree from this model
mjSpec spec;
@@ -225,6 +226,9 @@ class mjCModel : public mjCModel_, private mjSpec {
// delete object from the corresponding list
void DeleteElement(mjsElement* el);
// delete default and all descendants
void RemoveDefault(mjCDef* def);
// detach subtree from model
void Detach(mjCBody* subtree);
+11
View File
@@ -536,12 +536,16 @@ int mjCBoundingVolumeHierarchy::MakeBVH(
//------------------------- class mjCDef implementation --------------------------------------------
// constructor
mjCDef::mjCDef() {
name.clear();
id = 0;
parent = nullptr;
model = 0;
child.clear();
elemtype = mjOBJ_DEFAULT;
mjs_defaultJoint(&joint_.spec);
mjs_defaultGeom(&geom_.spec);
mjs_defaultSite(&site_.spec);
@@ -561,6 +565,13 @@ mjCDef::mjCDef() {
// constructor with model
mjCDef::mjCDef(mjCModel* _model) : mjCDef() {
model = _model;
}
// copy constructor
mjCDef::mjCDef(const mjCDef& other) {
*this = other;
+2
View File
@@ -1866,6 +1866,7 @@ class mjCDef : public mjsElement {
public:
mjCDef();
mjCDef(mjCModel*);
mjCDef(const mjCDef& other);
mjCDef& operator=(const mjCDef& other);
mjCDef& operator+=(const mjCDef& other);
@@ -1898,6 +1899,7 @@ class mjCDef : public mjsElement {
std::vector<mjCDef*> child; // child classes
mjsDefault spec;
mjCModel* model; // pointer to model that owns object
private:
mjCJoint joint_;
+85
View File
@@ -2544,6 +2544,91 @@ TEST_F(MujocoTest, ApplyNameSpaceToDefaults) {
mj_deleteVFS(vfs.get());
}
TEST_F(MujocoTest, DetachDefault) {
static constexpr char xml_c[] = R"(
<mujoco>
<default>
<default class="parent">
<default class="child1">
<mesh scale="0.001 0.001 0.001"/>
</default>
<default class="child2">
<mesh scale="0.001 0.001 0.001"/>
</default>
</default>
</default>
<asset>
<mesh file="cube.obj" class="child2"/>
</asset>
<worldbody>
<body name="body">
<geom type="mesh" mesh="cube"/>
</body>
</worldbody>
</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)";
auto vfs = std::make_unique<mjVFS>();
mj_defaultVFS(vfs.get());
mj_addBufferVFS(vfs.get(), "cube.obj", cube, sizeof(cube));
std::array<char, 1024> err;
mjSpec* spec = mj_parseXMLString(xml_c, vfs.get(), err.data(), err.size());
EXPECT_THAT(spec, NotNull()) << err.data();
// get default
mjsDefault* child = mjs_findDefault(spec, "child1");
EXPECT_THAT(child, NotNull());
// try using mjs_delete to remove default, should fail
EXPECT_EQ(mjs_delete(child->element), -1);
// detach default
EXPECT_EQ(mjs_detachDefault(spec, child), 0);
child = mjs_findDefault(spec, "child1");
EXPECT_THAT(child, IsNull());
// try and detach previously detached default, should fail
EXPECT_EQ(mjs_detachDefault(spec, child), -1);
child = mjs_findDefault(spec, "child1");
EXPECT_THAT(child, IsNull());
EXPECT_THAT(mjs_getError(spec),
HasSubstr("Cannot detach, default is null"));
// detach parent
mjsDefault* parent = mjs_findDefault(spec, "parent");
EXPECT_THAT(parent, NotNull());
mjs_detachDefault(spec, parent);
// both parent and remaining child should be removed
parent = mjs_findDefault(spec, "parent");
EXPECT_THAT(parent, IsNull());
child = mjs_findDefault(spec, "child2");
EXPECT_THAT(child, IsNull());
// error when trying to detach the 'main' default
mjsDefault* main = mjs_findDefault(spec, "main");
EXPECT_THAT(main, NotNull());
EXPECT_EQ(mjs_detachDefault(spec, main), -1);
EXPECT_THAT(mjs_getError(spec),
HasSubstr("cannot remove the global default ('main')"));
main = mjs_findDefault(spec, "main");
EXPECT_THAT(main, NotNull());
mj_deleteVFS(vfs.get());
mj_deleteSpec(spec);
}
TEST_F(MujocoTest, ErrorWhenCompilingOrphanedSpec) {
static constexpr char xml[] = R"(
<mujoco>
+1
View File
@@ -319,6 +319,7 @@ public enum mjtObj : int{
mjOBJ_PLUGIN = 25,
mjNOBJECT = 26,
mjOBJ_FRAME = 100,
mjOBJ_DEFAULT = 101,
}
public enum mjtConstraint : int{
mjCNSTR_EQUALITY = 0,